code reformatting

This commit is contained in:
Timothy Baldridge 2019-09-13 22:35:42 -06:00
parent 1a7dec86c1
commit 454cff052e
42 changed files with 1385 additions and 1799 deletions

View File

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8" ?>
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
</startup>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
</startup>
</configuration>

View File

@ -1,33 +1,28 @@
using ICSharpCode.SharpZipLib;
using System;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Compression.BSA.Test
{
class Program
internal class Program
{
const string TestDir = @"D:\MO2 Instances\";
const string TempDir = "c:\\tmp\\out";
static void Main(string[] args)
private const string TestDir = @"D:\MO2 Instances\";
private const string TempDir = "c:\\tmp\\out";
private static void Main(string[] args)
{
foreach (var bsa in Directory.EnumerateFiles(TestDir, "*.bsa", SearchOption.AllDirectories).Skip(0))
{
Console.WriteLine($"From {bsa}");
Console.WriteLine("Cleaning Output Dir");
if (Directory.Exists(TempDir))
{
Directory.Delete(TempDir, true);
}
if (Directory.Exists(TempDir)) Directory.Delete(TempDir, true);
Directory.CreateDirectory(TempDir);
Console.WriteLine($"Reading {bsa}");
using (var a = new BSAReader(bsa))
{
Parallel.ForEach(a.Files, file =>
{
var abs_name = Path.Combine(TempDir, file.Path);
@ -36,8 +31,11 @@ namespace Compression.BSA.Test
Directory.CreateDirectory(Path.GetDirectoryName(abs_name));
using (var fs = File.OpenWrite(abs_name))
{
file.CopyDataTo(fs);
Equal((long)file.Size, new FileInfo(abs_name).Length);
}
Equal(file.Size, new FileInfo(abs_name).Length);
});
@ -56,9 +54,8 @@ namespace Compression.BSA.Test
{
var entry = w.AddFile(file.Path, str, file.FlipCompression);
}
});
w.Build("c:\\tmp\\tmp.bsa");
// Sanity Checks
@ -70,33 +67,31 @@ namespace Compression.BSA.Test
Console.WriteLine($"{pair.ai.Path}, {pair.ai.Hash}, {pair.bi.Path}, {pair.bi.Hash}");
}*/
foreach (var pair in Enumerable.Zip(a.Files, w.Files, (ai, bi) => (ai, bi)))
foreach (var pair in a.Files.Zip(w.Files, (ai, bi) => (ai, bi)))
{
Equal(pair.ai.Path, pair.bi.Path);
Equal(pair.ai.Hash, pair.bi.Hash);
}
}
Console.WriteLine($"Verifying {bsa}");
using (var b = new BSAReader("c:\\tmp\\tmp.bsa"))
{
Console.WriteLine($"Performing A/B tests on {bsa}");
Equal((uint)a.ArchiveFlags, (uint)b.ArchiveFlags);
Equal((uint)a.FileFlags, (uint)b.FileFlags);
Equal((uint) a.ArchiveFlags, (uint) b.ArchiveFlags);
Equal((uint) a.FileFlags, (uint) b.FileFlags);
// Check same number of files
Equal(a.Files.Count(), b.Files.Count());
int idx = 0;
foreach (var pair in Enumerable.Zip(a.Files, b.Files, (ai, bi) => (ai, bi)))
var idx = 0;
foreach (var pair in a.Files.Zip(b.Files, (ai, bi) => (ai, bi)))
{
idx ++;
idx++;
//Console.WriteLine($" - {pair.ai.Path}");
Equal(pair.ai.Path, pair.bi.Path);
Equal(pair.ai.Compressed, pair.bi.Compressed);
Equal(pair.ai.Size, pair.bi.Size);
Equal(pair.ai.GetData(), pair.bi.GetData());
}
}
}
@ -109,7 +104,6 @@ namespace Compression.BSA.Test
foreach (var itm in a)
Equal(b.Contains(itm));
}
private static void Equal(bool v)
@ -161,16 +155,11 @@ namespace Compression.BSA.Test
public static void Equal(byte[] a, byte[] b)
{
if (a.Length != b.Length)
{
throw new InvalidDataException($"Byte array sizes are not equal");
}
if (a.Length != b.Length) throw new InvalidDataException("Byte array sizes are not equal");
for (var idx = 0; idx < a.Length; idx ++)
{
for (var idx = 0; idx < a.Length; idx++)
if (a[idx] != b[idx])
throw new InvalidDataException($"Byte array contents not equal at {idx}");
}
}
}
}
}

View File

@ -1,5 +1,4 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
@ -33,4 +32,4 @@ using System.Runtime.InteropServices;
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

View File

@ -1,11 +1,11 @@
using ICSharpCode.SharpZipLib.Zip.Compression.Streams;
using K4os.Compression.LZ4;
using K4os.Compression.LZ4.Streams;
using System;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using ICSharpCode.SharpZipLib.Zip.Compression.Streams;
using K4os.Compression.LZ4;
using K4os.Compression.LZ4.Streams;
using File = Alphaleonis.Win32.Filesystem.File;
using Path = Alphaleonis.Win32.Filesystem.Path;
@ -13,18 +13,18 @@ namespace Compression.BSA
{
public class BSABuilder : IDisposable
{
internal byte[] _fileId;
internal uint _version;
internal uint _offset;
internal uint _archiveFlags;
internal uint _folderCount;
internal uint _fileCount;
internal uint _totalFolderNameLength;
internal uint _totalFileNameLength;
internal uint _fileFlags;
internal byte[] _fileId;
private List<FileEntry> _files = new List<FileEntry>();
internal uint _folderCount;
internal List<FolderRecordBuilder> _folders = new List<FolderRecordBuilder>();
internal uint _offset;
internal uint _totalFileNameLength;
internal uint _totalFolderNameLength;
internal uint _version;
public BSABuilder()
{
@ -32,59 +32,24 @@ namespace Compression.BSA
_offset = 0x24;
}
public IEnumerable<FileEntry> Files
{
get
{
return _files;
}
}
public IEnumerable<FileEntry> Files => _files;
public ArchiveFlags ArchiveFlags
{
get
{
return (ArchiveFlags)_archiveFlags;
}
set
{
_archiveFlags = (uint)value;
}
get => (ArchiveFlags) _archiveFlags;
set => _archiveFlags = (uint) value;
}
public FileFlags FileFlags
{
get
{
return (FileFlags)_archiveFlags;
}
set
{
_archiveFlags = (uint)value;
}
get => (FileFlags) _archiveFlags;
set => _archiveFlags = (uint) value;
}
public VersionType HeaderType
{
get
{
return (VersionType)_version;
}
set
{
_version = (uint)value;
}
}
public FileEntry AddFile(string path, Stream src, bool flipCompression = false)
{
FileEntry r = new FileEntry(this, path, src, flipCompression);
lock (this)
{
_files.Add(r);
}
return r;
get => (VersionType) _version;
set => _version = (uint) value;
}
public IEnumerable<string> FolderNames
@ -92,40 +57,32 @@ namespace Compression.BSA
get
{
return _files.Select(f => Path.GetDirectoryName(f.Path))
.ToHashSet();
.ToHashSet();
}
}
public bool HasFolderNames
public bool HasFolderNames => (_archiveFlags & 0x1) > 0;
public bool HasFileNames => (_archiveFlags & 0x2) > 0;
public bool CompressedByDefault => (_archiveFlags & 0x4) > 0;
public bool HasNameBlobs => (_archiveFlags & 0x100) > 0;
public void Dispose()
{
get
{
return (_archiveFlags & 0x1) > 0;
}
}
public bool HasFileNames
public FileEntry AddFile(string path, Stream src, bool flipCompression = false)
{
get
{
return (_archiveFlags & 0x2) > 0;
}
}
var r = new FileEntry(this, path, src, flipCompression);
public bool CompressedByDefault
{
get
lock (this)
{
return (_archiveFlags & 0x4) > 0;
_files.Add(r);
}
}
public bool HasNameBlobs
{
get
{
return (_archiveFlags & 0x100) > 0;
}
return r;
}
public void Build(string outputName)
@ -141,107 +98,82 @@ namespace Compression.BSA
wtr.Write(_offset);
wtr.Write(_archiveFlags);
var folders = FolderNames.ToList();
wtr.Write((uint)folders.Count);
wtr.Write((uint)_files.Count);
wtr.Write((uint)_folders.Select(f => f._nameBytes.Count() - 1).Sum()); // totalFolderNameLength
wtr.Write((uint) folders.Count);
wtr.Write((uint) _files.Count);
wtr.Write((uint) _folders.Select(f => f._nameBytes.Count() - 1).Sum()); // totalFolderNameLength
var s = _files.Select(f => f._pathBytes.Count()).Sum();
_totalFileNameLength = (uint)_files.Select(f => f._nameBytes.Count()).Sum();
_totalFileNameLength = (uint) _files.Select(f => f._nameBytes.Count()).Sum();
wtr.Write(_totalFileNameLength); // totalFileNameLength
wtr.Write(_fileFlags);
foreach (var folder in _folders)
{
folder.WriteFolderRecord(wtr);
}
foreach (var folder in _folders) folder.WriteFolderRecord(wtr);
foreach(var folder in _folders)
foreach (var folder in _folders)
{
if (HasFolderNames)
wtr.Write(folder._nameBytes);
foreach (var file in folder._files)
{
file.WriteFileRecord(wtr);
}
foreach (var file in folder._files) file.WriteFileRecord(wtr);
}
foreach(var file in _files)
{
wtr.Write(file._nameBytes);
}
foreach (var file in _files) wtr.Write(file._nameBytes);
foreach(var file in _files)
{
file.WriteData(wtr);
}
foreach (var file in _files) file.WriteData(wtr);
}
}
public void RegenFolderRecords()
{
_folders = _files.GroupBy(f => Path.GetDirectoryName(f.Path.ToLowerInvariant()))
.Select(f => new FolderRecordBuilder(this, f.Key, f.ToList()))
.OrderBy(f => f._hash)
.ToList();
.Select(f => new FolderRecordBuilder(this, f.Key, f.ToList()))
.OrderBy(f => f._hash)
.ToList();
var lnk = _files.Where(f => f.Path.EndsWith(".lnk")).FirstOrDefault();
foreach (var folder in _folders)
foreach (var file in folder._files)
file._folder = folder;
foreach (var file in folder._files)
file._folder = folder;
_files = (from folder in _folders
from file in folder._files
orderby folder._hash, file._hash
select file).ToList();
}
public void Dispose()
{
from file in folder._files
orderby folder._hash, file._hash
select file).ToList();
}
}
public class FolderRecordBuilder
{
internal IEnumerable<FileEntry> _files;
private string _name;
internal BSABuilder _bsa;
internal ulong _hash;
internal uint _fileCount;
internal IEnumerable<FileEntry> _files;
internal ulong _hash;
internal byte[] _nameBytes;
internal uint _recordSize;
internal ulong _offset;
internal uint _recordSize;
public ulong Hash
public FolderRecordBuilder(BSABuilder bsa, string folderName, IEnumerable<FileEntry> files)
{
get
{
return _hash;
}
_files = files.OrderBy(f => f._hash);
Name = folderName.ToLowerInvariant();
_bsa = bsa;
// Folders don't have extensions, so let's make sure we cut it out
_hash = Name.GetBSAHash("");
_fileCount = (uint) files.Count();
_nameBytes = folderName.ToBZString(_bsa.HeaderType);
_recordSize = sizeof(ulong) + sizeof(uint) + sizeof(uint);
}
public string Name
{
get
{
return _name;
}
}
public ulong Hash => _hash;
public string Name { get; }
public ulong SelfSize
{
get
{
if (_bsa.HeaderType == VersionType.SSE)
{
return sizeof(ulong) + sizeof(uint) + sizeof(uint) + sizeof(ulong);
}
else
{
return sizeof(ulong) + sizeof(uint) + sizeof(uint);
}
return sizeof(ulong) + sizeof(uint) + sizeof(uint);
}
}
@ -251,69 +183,56 @@ namespace Compression.BSA
{
ulong size = 0;
if (_bsa.HasFolderNames)
size += (ulong)_nameBytes.Length;
size += (ulong)_files.Select(f => sizeof(ulong) + sizeof(uint) + sizeof(uint)).Sum();
size += (ulong) _nameBytes.Length;
size += (ulong) _files.Select(f => sizeof(ulong) + sizeof(uint) + sizeof(uint)).Sum();
return size;
}
}
public FolderRecordBuilder(BSABuilder bsa, string folderName, IEnumerable<FileEntry> files)
{
_files = files.OrderBy(f => f._hash);
_name = folderName.ToLowerInvariant();
_bsa = bsa;
// Folders don't have extensions, so let's make sure we cut it out
_hash = _name.GetBSAHash("");
_fileCount = (uint)files.Count();
_nameBytes = folderName.ToBZString(_bsa.HeaderType);
_recordSize = sizeof(ulong) + sizeof(uint) + sizeof(uint);
}
public void WriteFolderRecord(BinaryWriter wtr)
{
var idx = _bsa._folders.IndexOf(this);
_offset = (ulong)wtr.BaseStream.Position;
_offset += (ulong)_bsa._folders.Skip((int)idx).Select(f => (long)f.SelfSize).Sum();
_offset = (ulong) wtr.BaseStream.Position;
_offset += (ulong) _bsa._folders.Skip(idx).Select(f => (long) f.SelfSize).Sum();
_offset += _bsa._totalFileNameLength;
_offset += (ulong)_bsa._folders.Take((int)idx).Select(f => (long)f.FileRecordSize).Sum();
_offset += (ulong) _bsa._folders.Take(idx).Select(f => (long) f.FileRecordSize).Sum();
var sp = wtr.BaseStream.Position;
var sp = wtr.BaseStream.Position;
wtr.Write(_hash);
wtr.Write(_fileCount);
if (_bsa.HeaderType == VersionType.SSE)
{
wtr.Write((uint)0); // unk
wtr.Write((ulong)_offset); // offset
wtr.Write((uint) 0); // unk
wtr.Write(_offset); // offset
}
else if (_bsa.HeaderType == VersionType.FO3 || _bsa.HeaderType == VersionType.TES4)
{
wtr.Write((uint)_offset);
wtr.Write((uint) _offset);
}
else
{
throw new NotImplementedException($"Cannot write to BSAs of type {_bsa.HeaderType}");
}
}
}
public class FileEntry
{
internal FolderRecordBuilder _folder;
internal BSABuilder _bsa;
internal string _path;
internal string _name;
internal string _filenameSource;
internal Stream _bytesSource;
internal string _filenameSource;
internal bool _flipCompression;
internal FolderRecordBuilder _folder;
internal ulong _hash;
internal string _name;
internal byte[] _nameBytes;
internal byte[] _pathBytes;
private byte[] _pathBSBytes;
internal byte[] _rawData;
internal int _originalSize;
private long _offsetOffset;
internal int _originalSize;
internal string _path;
private readonly byte[] _pathBSBytes;
internal byte[] _pathBytes;
internal byte[] _rawData;
public FileEntry(BSABuilder bsa, string path, Stream src, bool flipCompression)
{
@ -333,16 +252,35 @@ namespace Compression.BSA
if (Compressed)
CompressData();
}
public bool Compressed
{
get
{
if (_flipCompression)
return !_bsa.CompressedByDefault;
return _bsa.CompressedByDefault;
}
}
public string Path => _path;
public bool FlipCompression => _flipCompression;
public ulong Hash => _hash;
public FolderRecordBuilder Folder => _folder;
private void CompressData()
{
if (_bsa.HeaderType == VersionType.SSE)
{
var r = new MemoryStream();
using (var w = LZ4Stream.Encode(r, new LZ4EncoderSettings() { CompressionLevel = LZ4Level.L10_OPT}))
(new MemoryStream(_rawData)).CopyTo(w);
using (var w = LZ4Stream.Encode(r, new LZ4EncoderSettings {CompressionLevel = LZ4Level.L10_OPT}))
{
new MemoryStream(_rawData).CopyTo(w);
}
_rawData = r.ToArray();
}
@ -350,7 +288,9 @@ namespace Compression.BSA
{
var r = new MemoryStream();
using (var w = new DeflaterOutputStream(r))
(new MemoryStream(_rawData)).CopyTo(w);
{
new MemoryStream(_rawData).CopyTo(w);
}
_rawData = r.ToArray();
}
@ -360,85 +300,34 @@ namespace Compression.BSA
}
}
public bool Compressed
{
get
{
if (_flipCompression)
return !_bsa.CompressedByDefault;
else
return _bsa.CompressedByDefault;
}
}
public string Path
{
get
{
return _path;
}
}
public bool FlipCompression
{
get
{
return _flipCompression;
}
}
public ulong Hash { get
{
return _hash;
}
}
public FolderRecordBuilder Folder
{
get
{
return _folder;
}
}
internal void WriteFileRecord(BinaryWriter wtr)
{
wtr.Write(_hash);
var size = _rawData.Length;
if (_bsa.HasNameBlobs)
{
size += _pathBSBytes.Length;
}
if (Compressed)
{
size += 4;
}
if (_bsa.HasNameBlobs) size += _pathBSBytes.Length;
if (Compressed) size += 4;
if (_flipCompression)
wtr.Write((uint)size | (0x1 << 30));
wtr.Write((uint) size | (0x1 << 30));
else
wtr.Write((uint)size);
wtr.Write((uint) size);
_offsetOffset = wtr.BaseStream.Position;
wtr.Write((uint)0xDEADBEEF);
wtr.Write(0xDEADBEEF);
}
internal void WriteData(BinaryWriter wtr)
{
uint offset = (uint)wtr.BaseStream.Position;
var offset = (uint) wtr.BaseStream.Position;
wtr.BaseStream.Position = _offsetOffset;
wtr.Write((uint)offset);
wtr.Write(offset);
wtr.BaseStream.Position = offset;
if (_bsa.HasNameBlobs)
{
wtr.Write(_pathBSBytes);
}
if (_bsa.HasNameBlobs) wtr.Write(_pathBSBytes);
if (Compressed)
{
wtr.Write((uint)_originalSize);
wtr.Write((uint) _originalSize);
wtr.Write(_rawData);
}
else
@ -447,4 +336,4 @@ namespace Compression.BSA
}
}
}
}
}

View File

@ -1,5 +1,4 @@

using System;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
@ -15,7 +14,7 @@ namespace Compression.BSA
FO3 = 0x68, // FO3, FNV, TES5
SSE = 0x69,
FO4 = 0x01
};
}
[Flags]
public enum ArchiveFlags : uint
@ -49,104 +48,23 @@ namespace Compression.BSA
public class BSAReader : IDisposable
{
private Stream _stream;
private BinaryReader _rdr;
private string _magic;
private uint _version;
private uint _folderRecordOffset;
private uint _archiveFlags;
private uint _folderCount;
private uint _fileCount;
private uint _totalFolderNameLength;
private uint _totalFileNameLength;
private uint _fileFlags;
private List<FolderRecord> _folders;
internal string _fileName;
public IEnumerable<FileRecord> Files
{
get
{
foreach (var folder in _folders)
foreach (var file in folder._files)
yield return file;
}
}
public VersionType HeaderType
{
get
{
return (VersionType)_version;
}
}
public ArchiveFlags ArchiveFlags
{
get
{
return (ArchiveFlags)_archiveFlags;
}
}
public FileFlags FileFlags
{
get
{
return (FileFlags)_archiveFlags;
}
}
public bool HasFolderNames
{
get
{
return (_archiveFlags & 0x1) > 0;
}
}
public bool HasFileNames
{
get
{
return (_archiveFlags & 0x2) > 0;
}
}
public bool CompressedByDefault
{
get
{
return (_archiveFlags & 0x4) > 0;
}
}
public bool Bit9Set
{
get
{
return (_archiveFlags & 0x100) > 0;
}
}
public bool HasNameBlobs
{
get
{
if (HeaderType == VersionType.FO3 || HeaderType == VersionType.SSE)
{
return (_archiveFlags & 0x100) > 0;
}
return false;
}
}
private uint _folderCount;
private uint _folderRecordOffset;
private List<FolderRecord> _folders;
private string _magic;
private readonly BinaryReader _rdr;
private readonly Stream _stream;
private uint _totalFileNameLength;
private uint _totalFolderNameLength;
private uint _version;
public BSAReader(string filename) : this(File.OpenRead(filename))
{
_fileName = filename;
}
public BSAReader(Stream stream)
@ -156,6 +74,40 @@ namespace Compression.BSA
LoadHeaders();
}
public IEnumerable<FileRecord> Files
{
get
{
foreach (var folder in _folders)
foreach (var file in folder._files)
yield return file;
}
}
public VersionType HeaderType => (VersionType) _version;
public ArchiveFlags ArchiveFlags => (ArchiveFlags) _archiveFlags;
public FileFlags FileFlags => (FileFlags) _archiveFlags;
public bool HasFolderNames => (_archiveFlags & 0x1) > 0;
public bool HasFileNames => (_archiveFlags & 0x2) > 0;
public bool CompressedByDefault => (_archiveFlags & 0x4) > 0;
public bool Bit9Set => (_archiveFlags & 0x100) > 0;
public bool HasNameBlobs
{
get
{
if (HeaderType == VersionType.FO3 || HeaderType == VersionType.SSE) return (_archiveFlags & 0x100) > 0;
return false;
}
}
public void Dispose()
{
_stream.Close();
@ -184,30 +136,28 @@ namespace Compression.BSA
private void LoadFolderRecords()
{
_folders = new List<FolderRecord>();
for (int idx = 0; idx < _folderCount; idx += 1)
for (var idx = 0; idx < _folderCount; idx += 1)
_folders.Add(new FolderRecord(this, _rdr));
foreach (var folder in _folders)
folder.LoadFileRecordBlock(this, _rdr);
foreach (var folder in _folders)
foreach (var file in folder._files)
file.LoadFileRecord(this, folder, file, _rdr);
foreach (var file in folder._files)
file.LoadFileRecord(this, folder, file, _rdr);
}
}
public class FolderRecord
{
private ulong _nameHash;
private uint _fileCount;
private uint _unk;
private ulong _offset;
private readonly uint _fileCount;
internal List<FileRecord> _files;
private ulong _offset;
private uint _unk;
internal FolderRecord(BSAReader bsa, BinaryReader src)
{
_nameHash = src.ReadUInt64();
Hash = src.ReadUInt64();
_fileCount = src.ReadUInt32();
if (bsa.HeaderType == VersionType.SSE)
{
@ -221,53 +171,38 @@ namespace Compression.BSA
}
public string Name { get; private set; }
public ulong Hash
{
get
{
return _nameHash;
}
}
public ulong Hash { get; }
internal void LoadFileRecordBlock(BSAReader bsa, BinaryReader src)
{
if (bsa.HasFolderNames)
{
Name = src.ReadStringLen(bsa.HeaderType);
}
if (bsa.HasFolderNames) Name = src.ReadStringLen(bsa.HeaderType);
_files = new List<FileRecord>();
for (int idx = 0; idx < _fileCount; idx += 1)
{
_files.Add(new FileRecord(bsa, this, src));
}
for (var idx = 0; idx < _fileCount; idx += 1) _files.Add(new FileRecord(bsa, this, src));
}
}
public class FileRecord
{
private BSAReader _bsa;
private ulong _hash;
private bool _compressedFlag;
private uint _size;
private uint _offset;
private FolderRecord _folder;
private readonly BSAReader _bsa;
private readonly long _dataOffset;
private readonly uint _dataSize;
private string _name;
private uint _originalSize;
private uint _dataSize;
private uint _onDiskSize;
private string _nameBlob;
private long _dataOffset;
private readonly string _nameBlob;
private readonly uint _offset;
private readonly uint _onDiskSize;
private readonly uint _originalSize;
private readonly uint _size;
public FileRecord(BSAReader bsa, FolderRecord folderRecord, BinaryReader src)
{
_bsa = bsa;
_hash = src.ReadUInt64();
Hash = src.ReadUInt64();
var size = src.ReadUInt32();
_compressedFlag = (size & (0x1 << 30)) > 0;
FlipCompression = (size & (0x1 << 30)) > 0;
if (_compressedFlag)
if (FlipCompression)
_size = size ^ (0x1 << 30);
else
_size = size;
@ -276,7 +211,7 @@ namespace Compression.BSA
_size -= 4;
_offset = src.ReadUInt32();
_folder = folderRecord;
Folder = folderRecord;
var old_pos = src.BaseStream.Position;
@ -285,11 +220,11 @@ namespace Compression.BSA
if (bsa.HasNameBlobs)
_nameBlob = src.ReadStringLenNoTerm(bsa.HeaderType);
if (Compressed)
_originalSize = src.ReadUInt32();
_onDiskSize = (uint)(_size - (_nameBlob == null ? 0 : _nameBlob.Length + 1));
_onDiskSize = (uint) (_size - (_nameBlob == null ? 0 : _nameBlob.Length + 1));
if (Compressed)
{
@ -297,24 +232,21 @@ namespace Compression.BSA
_onDiskSize -= 4;
}
else
{
_dataSize = _onDiskSize;
}
_dataOffset = src.BaseStream.Position;
src.BaseStream.Position = old_pos;
}
internal void LoadFileRecord(BSAReader bsaReader, FolderRecord folder, FileRecord file, BinaryReader rdr)
{
_name = rdr.ReadStringTerm(_bsa.HeaderType);
}
public string Path
{
get
{
if (string.IsNullOrEmpty(_folder.Name)) return _name;
return _folder.Name + "\\" + _name;
if (string.IsNullOrEmpty(Folder.Name)) return _name;
return Folder.Name + "\\" + _name;
}
}
@ -322,36 +254,24 @@ namespace Compression.BSA
{
get
{
if (_compressedFlag) return !_bsa.CompressedByDefault;
if (FlipCompression) return !_bsa.CompressedByDefault;
return _bsa.CompressedByDefault;
}
}
public int Size
public int Size => (int) _dataSize;
public ulong Hash { get; }
public FolderRecord Folder { get; }
public bool FlipCompression { get; }
internal void LoadFileRecord(BSAReader bsaReader, FolderRecord folder, FileRecord file, BinaryReader rdr)
{
get
{
return (int)_dataSize;
}
_name = rdr.ReadStringTerm(_bsa.HeaderType);
}
public ulong Hash {
get
{
return _hash;
}
}
public FolderRecord Folder
{
get
{
return _folder;
}
}
public bool FlipCompression { get => _compressedFlag; }
public void CopyDataTo(Stream output)
{
using (var in_file = File.OpenRead(_bsa._fileName))
@ -361,31 +281,25 @@ namespace Compression.BSA
if (_bsa.HeaderType == VersionType.SSE)
{
if (Compressed)
{
var r = LZ4Stream.Decode(rdr.BaseStream);
r.CopyToLimit(output, (int)_originalSize);
r.CopyToLimit(output, (int) _originalSize);
}
else
{
rdr.BaseStream.CopyToLimit(output, (int)_onDiskSize);
rdr.BaseStream.CopyToLimit(output, (int) _onDiskSize);
}
}
else
{
if (Compressed)
{
using (var z = new InflaterInputStream(rdr.BaseStream))
z.CopyToLimit(output, (int)_originalSize);
}
{
z.CopyToLimit(output, (int) _originalSize);
}
else
{
rdr.BaseStream.CopyToLimit(output, (int)_onDiskSize);
}
rdr.BaseStream.CopyToLimit(output, (int) _onDiskSize);
}
}
}
@ -396,7 +310,5 @@ namespace Compression.BSA
CopyDataTo(ms);
return ms.ToArray();
}
}
}
}

View File

@ -1,5 +1,4 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
@ -33,4 +32,4 @@ using System.Runtime.InteropServices;
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

View File

@ -8,7 +8,8 @@ namespace Compression.BSA
{
internal static class Utils
{
private static Encoding Windows1252 = Encoding.GetEncoding(1252);
private static readonly Encoding Windows1252 = Encoding.GetEncoding(1252);
private static Encoding GetEncoding(VersionType version)
{
if (version == VersionType.SSE)
@ -20,10 +21,8 @@ namespace Compression.BSA
{
var len = rdr.ReadByte();
if (len == 0)
{
//rdr.ReadByte();
return "";
}
var bytes = rdr.ReadBytes(len - 1);
rdr.ReadByte();
@ -39,7 +38,7 @@ namespace Compression.BSA
public static string ReadStringTerm(this BinaryReader rdr, VersionType version)
{
List<byte> acc = new List<byte>();
var acc = new List<byte>();
while (true)
{
var c = rdr.ReadByte();
@ -48,12 +47,13 @@ namespace Compression.BSA
acc.Add(c);
}
return GetEncoding(version).GetString(acc.ToArray());
}
/// <summary>
/// Returns bytes for a \0 terminated string
/// Returns bytes for a \0 terminated string
/// </summary>
/// <param name="val"></param>
/// <returns></returns>
@ -62,12 +62,12 @@ namespace Compression.BSA
var b = GetEncoding(version).GetBytes(val);
var b2 = new byte[b.Length + 2];
b.CopyTo(b2, 1);
b2[0] = (byte)(b.Length + 1);
b2[0] = (byte) (b.Length + 1);
return b2;
}
/// <summary>
/// Returns bytes for unterminated string with a count at the start
/// Returns bytes for unterminated string with a count at the start
/// </summary>
/// <param name="val"></param>
/// <returns></returns>
@ -76,13 +76,13 @@ namespace Compression.BSA
var b = Encoding.ASCII.GetBytes(val);
var b2 = new byte[b.Length + 1];
b.CopyTo(b2, 1);
b2[0] = (byte)b.Length;
b2[0] = (byte) b.Length;
return b2;
}
/// <summary>
/// Returns bytes for a \0 terminated string prefixed by a length
/// Returns bytes for a \0 terminated string prefixed by a length
/// </summary>
/// <param name="val"></param>
/// <returns></returns>
@ -91,7 +91,7 @@ namespace Compression.BSA
var b = GetEncoding(version).GetBytes(val);
var b2 = new byte[b.Length + 1];
b.CopyTo(b2, 0);
b[0] = (byte)b.Length;
b[0] = (byte) b.Length;
return b2;
}
@ -111,10 +111,10 @@ namespace Compression.BSA
var hashBytes = new[]
{
(byte)(name.Length == 0 ? '\0' : name[name.Length - 1]),
(byte)(name.Length < 3 ? '\0' : name[name.Length - 2]),
(byte)name.Length,
(byte)name[0]
(byte) (name.Length == 0 ? '\0' : name[name.Length - 1]),
(byte) (name.Length < 3 ? '\0' : name[name.Length - 2]),
(byte) name.Length,
(byte) name[0]
};
var hash1 = BitConverter.ToUInt32(hashBytes, 0);
switch (ext)
@ -134,32 +134,26 @@ namespace Compression.BSA
}
uint hash2 = 0;
for (var i = 1; i < name.Length - 2; i++)
{
hash2 = hash2 * 0x1003f + (byte)name[i];
}
for (var i = 1; i < name.Length - 2; i++) hash2 = hash2 * 0x1003f + (byte) name[i];
uint hash3 = 0;
for (var i = 0; i < ext.Length; i++)
{
hash3 = hash3 * 0x1003f + (byte)ext[i];
}
for (var i = 0; i < ext.Length; i++) hash3 = hash3 * 0x1003f + (byte) ext[i];
return (((ulong)(hash2 + hash3)) << 32) + hash1;
return ((ulong) (hash2 + hash3) << 32) + hash1;
}
public static void CopyToLimit(this Stream frm, Stream tw, int limit)
{
byte[] buff = new byte[1024];
var buff = new byte[1024];
while (limit > 0)
{
int to_read = Math.Min(buff.Length, limit);
int read = frm.Read(buff, 0, to_read);
var to_read = Math.Min(buff.Length, limit);
var read = frm.Read(buff, 0, to_read);
tw.Write(buff, 0, read);
limit -= read;
}
tw.Flush();
}
}
}
}

View File

@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="AlphaFS" version="2.2.6" targetFramework="net472" />
<package id="K4os.Compression.LZ4" version="1.1.11" targetFramework="net472" />

View File

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8" ?>
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
</startup>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
</startup>
</configuration>

View File

@ -3,15 +3,15 @@ using Wabbajack.Common;
namespace VirtualFileSystem.Test
{
class Program
internal class Program
{
static void Main(string[] args)
private static void Main(string[] args)
{
Utils.SetLoggerFn(s => Console.WriteLine(s));
Utils.SetStatusFn((s, i) => Console.WriteLine(s));
WorkQueue.Init((a, b, c) => { return; },
(a, b) => { return; });
WorkQueue.Init((a, b, c) => { },
(a, b) => { });
VFS.VirtualFileSystem.VFS.AddRoot(@"D:\tmp\Interesting NPCs SSE 3.42\Data");
}
}
}
}

View File

@ -1,5 +1,4 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
@ -33,4 +32,4 @@ using System.Runtime.InteropServices;
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

View File

@ -1,5 +1,4 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
@ -33,4 +32,4 @@ using System.Runtime.InteropServices;
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

View File

@ -1,17 +1,13 @@
using Compression.BSA;
using ICSharpCode.SharpZipLib.Zip;
using Newtonsoft.Json;
using System;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Security.AccessControl;
using Alphaleonis.Win32.Filesystem;
using Compression.BSA;
using ICSharpCode.SharpZipLib.Zip;
using Newtonsoft.Json;
using Wabbajack.Common;
using Directory = Alphaleonis.Win32.Filesystem.Directory;
using DirectoryInfo = Alphaleonis.Win32.Filesystem.DirectoryInfo;
using File = Alphaleonis.Win32.Filesystem.File;
using FileInfo = Alphaleonis.Win32.Filesystem.FileInfo;
using Path = Alphaleonis.Win32.Filesystem.Path;
@ -20,16 +16,12 @@ namespace VFS
{
public class VirtualFileSystem
{
internal static string _stagedRoot;
public static VirtualFileSystem VFS;
private Dictionary<string, VirtualFile> _files = new Dictionary<string, VirtualFile>();
private bool _disableDiskCache;
private Dictionary<string, VirtualFile> _files = new Dictionary<string, VirtualFile>();
private volatile bool _isSyncing;
public static string RootFolder { get; }
public Dictionary<string, IEnumerable<VirtualFile>> HashIndex { get; private set; }
static VirtualFileSystem()
{
VFS = new VirtualFileSystem();
@ -45,40 +37,38 @@ namespace VFS
Directory.CreateDirectory(_stagedRoot);
}
public VirtualFileSystem()
{
LoadFromDisk();
}
public static string RootFolder { get; }
public Dictionary<string, IEnumerable<VirtualFile>> HashIndex { get; private set; }
public VirtualFile this[string path] => Lookup(path);
private static void DeleteDirectory(string path, bool recursive = true)
{
if (recursive)
{
var subfolders = Directory.GetDirectories(path);
foreach (var s in subfolders)
{
DeleteDirectory(s, recursive);
}
foreach (var s in subfolders) DeleteDirectory(s, recursive);
}
var files = Directory.GetFiles(path);
foreach (var f in files)
{
try
{
var attr = File.GetAttributes(f);
if ((attr & FileAttributes.ReadOnly) == FileAttributes.ReadOnly)
{
File.SetAttributes(f, attr ^ FileAttributes.ReadOnly);
}
File.Delete(f);
}
catch (IOException)
{
}
}
Directory.Delete(path, true);
}
public VirtualFileSystem ()
{
LoadFromDisk();
}
private void LoadFromDisk()
@ -96,7 +86,6 @@ namespace VFS
{
while (true)
{
var fr = VirtualFile.Read(br);
_files.Add(fr.FullPath, fr);
}
@ -135,10 +124,7 @@ namespace VFS
using (var bw = new BinaryWriter(fs))
{
Utils.Log($"Syncing VFS to Disk: {_files.Count} entries");
foreach (var f in _files.Values)
{
f.Write(bw);
}
foreach (var f in _files.Values) f.Write(bw);
}
if (File.Exists("vfs_cache.bin"))
@ -158,8 +144,8 @@ namespace VFS
{
var path = f.FullPath + "|";
return _files.Values
.Where(v => v.FullPath.StartsWith(path))
.ToList();
.Where(v => v.FullPath.StartsWith(path))
.ToList();
}
@ -169,11 +155,9 @@ namespace VFS
lock (this)
{
_files.Values
.Where(v => v.FullPath.StartsWith(path) || v.FullPath == f.FullPath)
.ToList()
.Do(r => {
_files.Remove(r.FullPath);
});
.Where(v => v.FullPath.StartsWith(path) || v.FullPath == f.FullPath)
.ToList()
.Do(r => { _files.Remove(r.FullPath); });
}
}
@ -198,7 +182,7 @@ namespace VFS
}
/// <summary>
/// Remove any orphaned files in the DB.
/// Remove any orphaned files in the DB.
/// </summary>
private void CleanDB()
{
@ -206,51 +190,49 @@ namespace VFS
lock (this)
{
_files.Values
.Where(f =>
{
if (f.IsConcrete)
return !File.Exists(f.StagedPath);
if (f.Hash == null)
return true;
while (f.ParentPath != null)
{
if (Lookup(f.ParentPath) == null)
return true;
if (f.Hash == null)
return true;
f = Lookup(f.ParentPath);
}
return false;
})
.ToList()
.Do(f => _files.Remove(f.FullPath));
.Where(f =>
{
if (f.IsConcrete)
return !File.Exists(f.StagedPath);
if (f.Hash == null)
return true;
while (f.ParentPath != null)
{
if (Lookup(f.ParentPath) == null)
return true;
if (f.Hash == null)
return true;
f = Lookup(f.ParentPath);
}
return false;
})
.ToList()
.Do(f => _files.Remove(f.FullPath));
}
}
public void BackfillMissing()
{
lock(this)
lock (this)
{
_files.Values
.Select(f => f.ParentPath)
.Where(s => s != null)
.Where(s => !_files.ContainsKey(s))
.ToHashSet()
.Do(s =>
{
AddKnown(new VirtualFile() { Paths = s.Split('|') });
});
.Select(f => f.ParentPath)
.Where(s => s != null)
.Where(s => !_files.ContainsKey(s))
.ToHashSet()
.Do(s => { AddKnown(new VirtualFile {Paths = s.Split('|')}); });
}
}
/// <summary>
/// Add a known file to the index, bit of a hack as we won't assume that all the fields for the archive are filled in.
/// you will need to manually update the SHA hash when you are done adding files, by calling `RefreshIndexes`
/// Add a known file to the index, bit of a hack as we won't assume that all the fields for the archive are filled in.
/// you will need to manually update the SHA hash when you are done adding files, by calling `RefreshIndexes`
/// </summary>
/// <param name="virtualFile"></param>
public void AddKnown(VirtualFile virtualFile)
{
lock(this)
lock (this)
{
// We don't know enough about these files to be able to store them in the disk cache
_disableDiskCache = true;
@ -259,8 +241,8 @@ namespace VFS
}
/// <summary>
/// Adds the root path to the filesystem. This may take quite some time as every file in the folder will be hashed,
/// and every archive examined.
/// Adds the root path to the filesystem. This may take quite some time as every file in the folder will be hashed,
/// and every archive examined.
/// </summary>
/// <param name="path"></param>
public void AddRoot(string path)
@ -273,11 +255,11 @@ namespace VFS
public void RefreshIndexes()
{
Utils.Log("Building Hash Index");
lock(this)
lock (this)
{
HashIndex = _files.Values
.GroupBy(f => f.Hash)
.ToDictionary(f => f.Key, f => (IEnumerable<VirtualFile>)f);
.GroupBy(f => f.Hash)
.ToDictionary(f => f.Key, f => (IEnumerable<VirtualFile>) f);
}
}
@ -291,25 +273,23 @@ namespace VFS
private void UpdateFile(string f)
{
TOP:
TOP:
var lv = Lookup(f);
if (lv == null)
{
Utils.Status($"Analyzing {f}");
lv = new VirtualFile()
lv = new VirtualFile
{
Paths = new string[] { f }
Paths = new[] {f}
};
lv.Analyze();
Add(lv);
if (lv.IsArchive)
{
UpdateArchive(lv);
}
if (lv.IsArchive) UpdateArchive(lv);
// Upsert after extraction incase extraction fails
}
if (lv.IsOutdated)
{
Purge(lv);
@ -331,20 +311,21 @@ namespace VFS
Utils.Status($"Updating Archive {Path.GetFileName(f.StagedPath)}");
var entries = Directory.EnumerateFiles(tmp_dir, "*", SearchOption.AllDirectories)
.Select(path => path.RelativeTo(tmp_dir));
.Select(path => path.RelativeTo(tmp_dir));
var new_files = entries.Select(e => {
var new_files = entries.Select(e =>
{
var new_path = new string[f.Paths.Length + 1];
f.Paths.CopyTo(new_path, 0);
new_path[f.Paths.Length] = e;
var nf = new VirtualFile()
var nf = new VirtualFile
{
Paths = new_path,
Paths = new_path
};
nf._stagedPath = Path.Combine(tmp_dir, e);
Add(nf);
return nf;
}).ToList();
}).ToList();
// Analyze them
new_files.PMap(file =>
@ -362,19 +343,18 @@ namespace VFS
Utils.Status("Cleaning Directory");
DeleteDirectory(tmp_dir);
}
public Action Stage(IEnumerable<VirtualFile> files)
{
var grouped = files.SelectMany(f => f.FilesInPath)
.Distinct()
.Where(f => f.ParentArchive != null)
.GroupBy(f => f.ParentArchive)
.OrderBy(f => f.Key == null ? 0 : f.Key.Paths.Length)
.ToList();
.Distinct()
.Where(f => f.ParentArchive != null)
.GroupBy(f => f.ParentArchive)
.OrderBy(f => f.Key == null ? 0 : f.Key.Paths.Length)
.ToList();
List<string> Paths = new List<string>();
var Paths = new List<string>();
foreach (var group in grouped)
{
@ -383,7 +363,6 @@ namespace VFS
Paths.Add(tmp_path);
foreach (var file in group)
file._stagedPath = Path.Combine(tmp_path, file.Paths[group.Key.Paths.Length]);
}
return () =>
@ -395,7 +374,6 @@ namespace VFS
};
}
public StagingGroup StageWith(IEnumerable<VirtualFile> files)
{
@ -404,42 +382,30 @@ namespace VFS
return grp;
}
public VirtualFile this[string path]
{
get
{
return Lookup(path);
}
}
internal List<string> GetArchiveEntryNames(VirtualFile file)
{
if (!file.IsStaged)
throw new InvalidDataException("File is not staged");
if (file.Extension == ".bsa") {
if (file.Extension == ".bsa")
using (var ar = new BSAReader(file.StagedPath))
{
return ar.Files.Select(f => f.Path).ToList();
}
}
if (file.Extension == ".zip")
{
using (var s = new ZipFile(File.OpenRead(file.StagedPath)))
{
s.IsStreamOwner = true;
s.UseZip64 = UseZip64.On;
if (s.OfType<ZipEntry>().FirstOrDefault(e => !e.CanDecompress) == null)
{
return s.OfType<ZipEntry>()
.Where(f => f.IsFile)
.Select(f => f.Name.Replace('/', '\\'))
.ToList();
}
.Where(f => f.IsFile)
.Select(f => f.Name.Replace('/', '\\'))
.ToList();
}
}
/*
using (var e = new ArchiveFile(file.StagedPath))
{
@ -448,11 +414,10 @@ namespace VFS
.Select(f => f.FileName).ToList();
}*/
return null;
}
/// <summary>
/// Given a path that starts with a HASH, return the Virtual file referenced
/// Given a path that starts with a HASH, return the Virtual file referenced
/// </summary>
/// <param name="archiveHashPath"></param>
/// <returns></returns>
@ -461,14 +426,16 @@ namespace VFS
if (archiveHashPath.Length == 1)
return HashIndex[archiveHashPath[0]].First();
var archive = HashIndex[archiveHashPath[0]].Where(a => a.IsArchive).OrderByDescending(a => a.LastModified).First();
string fullPath = archive.FullPath + "|" + String.Join("|", archiveHashPath.Skip(1));
var archive = HashIndex[archiveHashPath[0]].Where(a => a.IsArchive).OrderByDescending(a => a.LastModified)
.First();
var fullPath = archive.FullPath + "|" + string.Join("|", archiveHashPath.Skip(1));
return Lookup(fullPath);
}
public IDictionary<VirtualFile, IEnumerable<VirtualFile>> GroupedByArchive()
{
return _files.Values.GroupBy(f => f.TopLevelArchive).ToDictionary(f => f.Key, f => (IEnumerable<VirtualFile>)f);
return _files.Values.GroupBy(f => f.TopLevelArchive)
.ToDictionary(f => f.Key, f => (IEnumerable<VirtualFile>) f);
}
}
@ -492,64 +459,52 @@ namespace VFS
[JsonObject(MemberSerialization = MemberSerialization.OptIn)]
public class VirtualFile
{
private string _fullPath;
private bool? _isArchive;
private string _parentPath;
public string[] _paths;
internal string _stagedPath;
[JsonProperty]
public string[] Paths
{
get
{
return _paths;
}
get => _paths;
set
{
for (int idx = 0; idx < value.Length; idx += 1)
value[idx] = String.Intern(value[idx]);
for (var idx = 0; idx < value.Length; idx += 1)
value[idx] = string.Intern(value[idx]);
_paths = value;
}
}
[JsonProperty]
public string Hash { get; set; }
[JsonProperty]
public long Size { get; set; }
[JsonProperty]
public ulong LastModified { get; set; }
[JsonProperty] public string Hash { get; set; }
[JsonProperty] public long Size { get; set; }
[JsonProperty] public ulong LastModified { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public bool? FinishedIndexing { get; set; }
private string _fullPath;
public VirtualFile()
{
}
internal string _stagedPath;
public string FullPath
{
get
{
if (_fullPath != null) return _fullPath;
_fullPath = String.Join("|", Paths);
_fullPath = string.Join("|", Paths);
return _fullPath;
}
}
public string Extension
{
get
{
return Path.GetExtension(Paths.Last());
}
}
public string Extension => Path.GetExtension(Paths.Last());
/// <summary>
/// If this file is in an archive, return the Archive File, otherwise return null.
/// If this file is in an archive, return the Archive File, otherwise return null.
/// </summary>
public VirtualFile TopLevelArchive
{
@ -569,14 +524,13 @@ namespace VFS
}
}
private bool? _isArchive;
public bool IsArchive
{
get
{
if (_isArchive == null)
_isArchive = FileExtractor.CanExtract(Extension);
return (bool)_isArchive;
return (bool) _isArchive;
}
}
@ -606,58 +560,10 @@ namespace VFS
}
}
public FileStream OpenRead()
{
if (!IsStaged)
throw new InvalidDataException("File is not staged, cannot open");
return File.OpenRead(_stagedPath);
}
/// <summary>
/// Calculate the file's SHA, size and last modified
/// Returns true if this file always exists on-disk, and doesn't need to be staged.
/// </summary>
internal void Analyze()
{
if (!IsStaged)
throw new InvalidDataException("Cannot analyze an unstaged file");
var fio = new FileInfo(StagedPath);
Size = fio.Length;
Hash = Utils.FileSHA256(StagedPath);
LastModified = fio.LastWriteTime.ToMilliseconds();
}
/// <summary>
/// Delete the temoporary file associated with this file
/// </summary>
internal void Unstage()
{
if (IsStaged && !IsConcrete)
{
File.Delete(_stagedPath);
_stagedPath = null;
}
}
internal string GenerateStagedName()
{
if (_stagedPath != null) return _stagedPath;
_stagedPath = Path.Combine(VirtualFileSystem._stagedRoot, Guid.NewGuid().ToString() + Path.GetExtension(Paths.Last()));
return _stagedPath;
}
/// <summary>
/// Returns true if this file always exists on-disk, and doesn't need to be staged.
/// </summary>
public bool IsConcrete
{
get
{
return Paths.Length == 1;
}
}
public bool IsConcrete => Paths.Length == 1;
public bool IsOutdated
{
@ -672,46 +578,81 @@ namespace VFS
if (!FinishedIndexing ?? true)
return true;
}
return false;
}
}
private string _parentPath;
public string ParentPath
{
get {
if (_parentPath == null && !IsConcrete)
_parentPath = String.Join("|", Paths.Take(Paths.Length - 1));
return _parentPath;
}
}
public IEnumerable<VirtualFile> FileInArchive
public string ParentPath
{
get
{
return VirtualFileSystem.VFS.FilesInArchive(this);
if (_parentPath == null && !IsConcrete)
_parentPath = string.Join("|", Paths.Take(Paths.Length - 1));
return _parentPath;
}
}
public IEnumerable<VirtualFile> FileInArchive => VirtualFileSystem.VFS.FilesInArchive(this);
public IEnumerable<VirtualFile> FilesInPath
{
get
{
return Enumerable.Range(1, Paths.Length)
.Select(i => Paths.Take(i))
.Select(path => VirtualFileSystem.VFS.Lookup(string.Join("|", path)));
}
}
public FileStream OpenRead()
{
if (!IsStaged)
throw new InvalidDataException("File is not staged, cannot open");
return File.OpenRead(_stagedPath);
}
/// <summary>
/// Calculate the file's SHA, size and last modified
/// </summary>
internal void Analyze()
{
if (!IsStaged)
throw new InvalidDataException("Cannot analyze an unstaged file");
var fio = new FileInfo(StagedPath);
Size = fio.Length;
Hash = StagedPath.FileSHA256();
LastModified = fio.LastWriteTime.ToMilliseconds();
}
/// <summary>
/// Delete the temoporary file associated with this file
/// </summary>
internal void Unstage()
{
if (IsStaged && !IsConcrete)
{
File.Delete(_stagedPath);
_stagedPath = null;
}
}
internal string GenerateStagedName()
{
if (_stagedPath != null) return _stagedPath;
_stagedPath = Path.Combine(VirtualFileSystem._stagedRoot, Guid.NewGuid() + Path.GetExtension(Paths.Last()));
return _stagedPath;
}
public string[] MakeRelativePaths()
{
var path_copy = (string[])Paths.Clone();
var path_copy = (string[]) Paths.Clone();
path_copy[0] = VirtualFileSystem.VFS.Lookup(Paths[0]).Hash;
return path_copy;
}
public IEnumerable<VirtualFile> FilesInPath
{
get {
return Enumerable.Range(1, Paths.Length)
.Select(i => Paths.Take(i))
.Select(path => VirtualFileSystem.VFS.Lookup(String.Join("|", path)));
}
}
public void Write(BinaryWriter bw)
{
bw.Write(FullPath);
@ -727,8 +668,8 @@ namespace VFS
var full_path = rdr.ReadString();
vf.Paths = full_path.Split('|');
for (int x = 0; x < vf.Paths.Length; x++)
vf.Paths[x] = String.Intern(vf.Paths[x]);
for (var x = 0; x < vf.Paths.Length; x++)
vf.Paths[x] = string.Intern(vf.Paths[x]);
vf._fullPath = full_path;
vf.Hash = rdr.ReadString();
@ -738,9 +679,6 @@ namespace VFS
vf.FinishedIndexing = rdr.ReadBoolean();
if (vf.FinishedIndexing == false) vf.FinishedIndexing = null;
return vf;
}
}
}
}

View File

@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="AlphaFS" version="2.2.6" targetFramework="net472" />
<package id="Newtonsoft.Json" version="12.0.2" targetFramework="net472" />

View File

@ -1,8 +1,9 @@
namespace Wabbajack.Common
using System;
using System.IO;
using ICSharpCode.SharpZipLib.BZip2;
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:
@ -38,13 +39,16 @@
*/
public class BSDiff
{
private const long c_fileSignature = 0x3034464649445342L;
private const int c_headerSize = 32;
/// <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"/>.
/// 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>
/// <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
@ -69,59 +73,57 @@
32 ?? Bzip2ed ctrl block
?? ?? Bzip2ed diff block
?? ?? Bzip2ed extra block */
byte[] header = new byte[c_headerSize];
var 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;
var startPosition = output.Position;
output.Write(header, 0, header.Length);
int[] I = SuffixSort(oldData);
var I = SuffixSort(oldData);
byte[] db = new byte[newData.Length];
byte[] eb = new byte[newData.Length];
var db = new byte[newData.Length];
var eb = new byte[newData.Length];
int dblen = 0;
int eblen = 0;
var dblen = 0;
var eblen = 0;
using (BZip2OutputStream bz2Stream = new BZip2OutputStream(output) { IsStreamOwner = false })
using (var 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;
var scan = 0;
var pos = 0;
var len = 0;
var lastscan = 0;
var lastpos = 0;
var lastoffset = 0;
while (scan < newData.Length)
{
int oldscore = 0;
var oldscore = 0;
for (int scsc = scan += len; scan < newData.Length; scan++)
for (var 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]))
if (scsc + lastoffset < oldData.Length && oldData[scsc + lastoffset] == newData[scsc])
oldscore++;
}
if ((len == oldscore && len != 0) || (len > oldscore + 8))
if (len == oldscore && len != 0 || len > oldscore + 8)
break;
if ((scan + lastoffset < oldData.Length) && (oldData[scan + lastoffset] == newData[scan]))
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);)
var s = 0;
var sf = 0;
var lenf = 0;
for (var i = 0; lastscan + i < scan && lastpos + i < oldData.Length;)
{
if (oldData[lastpos + i] == newData[lastscan + i])
s++;
@ -133,12 +135,12 @@
}
}
int lenb = 0;
var lenb = 0;
if (scan < newData.Length)
{
s = 0;
int sb = 0;
for (int i = 1; (scan >= lastscan + i) && (pos >= i); i++)
var sb = 0;
for (var i = 1; scan >= lastscan + i && pos >= i; i++)
{
if (oldData[pos - i] == newData[scan - i])
s++;
@ -152,11 +154,11 @@
if (lastscan + lenf > scan - lenb)
{
int overlap = (lastscan + lenf) - (scan - lenb);
var overlap = lastscan + lenf - (scan - lenb);
s = 0;
int ss = 0;
int lens = 0;
for (int i = 0; i < overlap; i++)
var ss = 0;
var lens = 0;
for (var i = 0; i < overlap; i++)
{
if (newData[lastscan + lenf - overlap + i] == oldData[lastpos + lenf - overlap + i])
s++;
@ -173,22 +175,22 @@
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++)
for (var i = 0; i < lenf; i++)
db[dblen + i] = (byte) (newData[lastscan + i] - oldData[lastpos + i]);
for (var i = 0; i < scan - lenb - (lastscan + lenf); i++)
eb[eblen + i] = newData[lastscan + lenf + i];
dblen += lenf;
eblen += (scan - lenb) - (lastscan + lenf);
eblen += scan - lenb - (lastscan + lenf);
byte[] buf = new byte[8];
var buf = new byte[8];
WriteInt64(lenf, buf, 0);
bz2Stream.Write(buf, 0, 8);
WriteInt64((scan - lenb) - (lastscan + lenf), buf, 0);
WriteInt64(scan - lenb - (lastscan + lenf), buf, 0);
bz2Stream.Write(buf, 0, 8);
WriteInt64((pos - lenb) - (lastpos + lenf), buf, 0);
WriteInt64(pos - lenb - (lastpos + lenf), buf, 0);
bz2Stream.Write(buf, 0, 8);
lastscan = scan - lenb;
@ -199,41 +201,44 @@
}
// compute size of compressed ctrl data
long controlEndPosition = output.Position;
var controlEndPosition = output.Position;
WriteInt64(controlEndPosition - startPosition - c_headerSize, header, 8);
// write compressed diff data
using (BZip2OutputStream bz2Stream = new BZip2OutputStream(output) { IsStreamOwner = false })
using (var bz2Stream = new BZip2OutputStream(output) {IsStreamOwner = false})
{
bz2Stream.Write(db, 0, dblen);
}
// compute size of compressed diff data
long diffEndPosition = output.Position;
var diffEndPosition = output.Position;
WriteInt64(diffEndPosition - controlEndPosition, header, 16);
// write compressed extra data
using (BZip2OutputStream bz2Stream = new BZip2OutputStream(output) { IsStreamOwner = false })
using (var 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;
var 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"/>.
/// 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>
/// <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
@ -259,7 +264,7 @@
*/
// read header
long controlLength, diffLength, newSize;
using (Stream patchStream = openPatchStream())
using (var patchStream = openPatchStream())
{
// check patch stream capabilities
if (!patchStream.CanRead)
@ -267,10 +272,10 @@
if (!patchStream.CanSeek)
throw new ArgumentException("Patch stream must be seekable.", "openPatchStream");
byte[] header = ReadExactly(patchStream, c_headerSize);
var header = ReadExactly(patchStream, c_headerSize);
// check for appropriate magic
long signature = ReadInt64(header, 0);
var signature = ReadInt64(header, 0);
if (signature != c_fileSignature)
throw new InvalidOperationException("Corrupt patch.");
@ -284,13 +289,13 @@
// preallocate buffers for reading and writing
const int c_bufferSize = 1048576;
byte[] newData = new byte[c_bufferSize];
byte[] oldData = new byte[c_bufferSize];
var newData = new byte[c_bufferSize];
var 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())
using (var compressedControlStream = openPatchStream())
using (var compressedDiffStream = openPatchStream())
using (var compressedExtraStream = openPatchStream())
{
// seek to the start of each part
compressedControlStream.Seek(c_headerSize, SeekOrigin.Current);
@ -298,19 +303,19 @@
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))
using (var controlStream = new BZip2InputStream(compressedControlStream))
using (var diffStream = new BZip2InputStream(compressedDiffStream))
using (var extraStream = new BZip2InputStream(compressedExtraStream))
{
long[] control = new long[3];
byte[] buffer = new byte[8];
var control = new long[3];
var buffer = new byte[8];
int oldPosition = 0;
int newPosition = 0;
var oldPosition = 0;
var newPosition = 0;
while (newPosition < newSize)
{
// read control data
for (int i = 0; i < 3; i++)
for (var i = 0; i < 3; i++)
{
ReadExactly(controlStream, buffer, 0, 8);
control[i] = ReadInt64(buffer, 0);
@ -323,19 +328,20 @@
// seek old file to the position that the new data is diffed against
input.Position = oldPosition;
int bytesToCopy = (int)control[0];
var bytesToCopy = (int) control[0];
while (bytesToCopy > 0)
{
int actualBytesToCopy = Math.Min(bytesToCopy, c_bufferSize);
var 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));
var availableInputBytes =
Math.Min(actualBytesToCopy, (int) (input.Length - input.Position));
ReadExactly(input, oldData, 0, availableInputBytes);
for (int index = 0; index < availableInputBytes; index++)
for (var index = 0; index < availableInputBytes; index++)
newData[index] += oldData[index];
output.Write(newData, 0, actualBytesToCopy);
@ -351,10 +357,10 @@
throw new InvalidOperationException("Corrupt patch.");
// read extra string
bytesToCopy = (int)control[1];
bytesToCopy = (int) control[1];
while (bytesToCopy > 0)
{
int actualBytesToCopy = Math.Min(bytesToCopy, c_bufferSize);
var actualBytesToCopy = Math.Min(bytesToCopy, c_bufferSize);
ReadExactly(extraStream, newData, 0, actualBytesToCopy);
output.Write(newData, 0, actualBytesToCopy);
@ -364,7 +370,7 @@
}
// adjust position
oldPosition = (int)(oldPosition + control[2]);
oldPosition = (int) (oldPosition + control[2]);
}
}
}
@ -372,12 +378,13 @@
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++)
for (var index = 0; index < left.Length - leftOffset && index < right.Length - rightOffset; index++)
{
int diff = left[index + leftOffset] - right[index + rightOffset];
var diff = left[index + leftOffset] - right[index + rightOffset];
if (diff != 0)
return diff;
}
return 0;
}
@ -385,38 +392,33 @@
{
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)
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);
var startLength = MatchLength(oldData, I[start], newData, newOffset);
var 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);
pos = I[end];
return endLength;
}
var 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)
@ -424,24 +426,26 @@
if (len < 16)
{
int j;
for (int k = start; k < start + len; k += j)
for (var k = start; k < start + len; k += j)
{
j = 1;
int x = v[I[k] + h];
for (int i = 1; k + i < start + len; i++)
var x = v[I[k] + h];
for (var 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++)
for (var i = 0; i < j; i++)
v[I[k + i]] = k + j - 1;
if (j == 1)
I[k] = -1;
@ -456,33 +460,36 @@
y = v[I[start] + h];
z = v[I[k] + h];
if (len > 40)
{ /* Big array: Pseudomedian of 9 */
{
/* Big array: Pseudomedian of 9 */
tmp = len / 8;
x = median3(x, v[I[j - tmp] + h], v[I[j + tmp] + h]);
y = median3(y, v[I[start + tmp] + h], v[I[start + tmp + tmp] + h]);
z = median3(z, v[I[k - tmp] + h], v[I[k - tmp - tmp] + h]);
}; /* Else medium array: Pseudomedian of 3 */
}
; /* Else medium array: Pseudomedian of 3 */
x = median3(x, y, z);
//int x = v[I[start + len / 2] + h];
int jj = 0;
int kk = 0;
for (int i2 = start; i2 < start + len; i2++)
var jj = 0;
var kk = 0;
for (var 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;
var i = start;
j = 0;
k = 0;
while (i < jj)
{
if (v[I[i] + h] < x)
{
i++;
@ -497,10 +504,8 @@
Swap(ref I[i], ref I[kk + k]);
k++;
}
}
while (jj + j < kk)
{
if (v[I[jj + j] + h] == x)
{
j++;
@ -510,7 +515,6 @@
Swap(ref I[jj + j], ref I[kk + k]);
k++;
}
}
if (jj > start)
Split(I, v, start, jj - start, h);
@ -527,37 +531,34 @@
private static int[] SuffixSort(byte[] oldData)
{
int[] buckets = new int[256];
var buckets = new int[256];
foreach (byte oldByte in oldData)
foreach (var oldByte in oldData)
buckets[oldByte]++;
for (int i = 1; i < 256; i++)
for (var i = 1; i < 256; i++)
buckets[i] += buckets[i - 1];
for (int i = 255; i > 0; i--)
for (var 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++)
var I = new int[oldData.Length + 1];
for (var 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++)
var v = new int[oldData.Length + 1];
for (var i = 0; i < oldData.Length; i++)
v[i] = buckets[oldData[i]];
for (int i = 1; i < 256; i++)
{
for (var 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)
for (var h = 1; I[0] != -(oldData.Length + 1); h += h)
{
int len = 0;
int i = 0;
var len = 0;
var i = 0;
while (i < oldData.Length + 1)
{
if (I[i] < 0)
{
len -= I[i];
@ -572,13 +573,12 @@
i += len;
len = 0;
}
}
if (len != 0)
I[i - len] = -len;
}
for (int i = 0; i < oldData.Length + 1; i++)
for (var i = 0; i < oldData.Length + 1; i++)
I[v[i]] = i;
return I;
@ -586,7 +586,7 @@
private static void Swap(ref int first, ref int second)
{
int temp = first;
var temp = first;
first = second;
second = temp;
}
@ -595,7 +595,7 @@
{
long value = buf[offset + 7] & 0x7F;
for (int index = 6; index >= 0; index--)
for (var index = 6; index >= 0; index--)
{
value *= 256;
value += buf[offset + index];
@ -609,11 +609,11 @@
private static void WriteInt64(long value, byte[] buf, int offset)
{
long valueToWrite = value < 0 ? -value : value;
var valueToWrite = value < 0 ? -value : value;
for (int byteIndex = 0; byteIndex < 8; byteIndex++)
for (var byteIndex = 0; byteIndex < 8; byteIndex++)
{
buf[offset + byteIndex] = unchecked((byte)valueToWrite);
buf[offset + byteIndex] = unchecked((byte) valueToWrite);
valueToWrite >>= 8;
}
@ -622,7 +622,7 @@
}
/// <summary>
/// Reads exactly <paramref name="count"/> bytes from <paramref name="stream"/>.
/// 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>
@ -631,14 +631,14 @@
{
if (count < 0)
throw new ArgumentOutOfRangeException("count");
byte[] buffer = new byte[count];
var 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"/>.
/// 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>
@ -659,7 +659,7 @@
while (count > 0)
{
// read data
int bytesRead = stream.Read(buffer, offset, count);
var bytesRead = stream.Read(buffer, offset, count);
// check for failure to read
if (bytesRead == 0)
@ -671,14 +671,10 @@
}
}
const long c_fileSignature = 0x3034464649445342L;
const int c_headerSize = 32;
private static int median3(int a, int b, int c)
{
return (((a) < (b)) ? ((b) < (c) ? (b) : ((a) < (c) ? (c) : (a))) : ((b) > (c) ? (b) : ((a) > (c) ? (c) : (a))));
return a < b ? b < c ? b : a < c ? c : a : b > c ? b : a > c ? c : a;
}
}
}
}

View File

@ -1,36 +1,25 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
namespace Wabbajack.Common
{
/// <summary>
/// Allows processes to be automatically killed if this parent process unexpectedly quits.
/// This feature requires Windows 8 or greater. On Windows 7, nothing is done.</summary>
/// <remarks>References:
/// https://stackoverflow.com/a/4657392/386091
/// https://stackoverflow.com/a/9164742/386091 </remarks>
/// Allows processes to be automatically killed if this parent process unexpectedly quits.
/// This feature requires Windows 8 or greater. On Windows 7, nothing is done.
/// </summary>
/// <remarks>
/// References:
/// https://stackoverflow.com/a/4657392/386091
/// https://stackoverflow.com/a/9164742/386091
/// </remarks>
public static class ChildProcessTracker
{
/// <summary>
/// Add the process to be tracked. If our current process is killed, the child processes
/// that we are tracking will be automatically killed, too. If the child process terminates
/// first, that's fine, too.</summary>
/// <param name="process"></param>
public static void AddProcess(Process process)
{
if (s_jobHandle != IntPtr.Zero)
{
bool success = AssignProcessToJobObject(s_jobHandle, process.Handle);
if (!success && !process.HasExited)
throw new Win32Exception();
}
}
// Windows will automatically close any open job handles when our process terminates.
// This can be verified by using SysInternals' Handle utility. When the job handle
// is closed, the child processes will be killed.
private static readonly IntPtr s_jobHandle;
static ChildProcessTracker()
{
@ -45,7 +34,7 @@ namespace Wabbajack.Common
// The job name is optional (and can be null) but it helps with diagnostics.
// If it's not null, it has to be unique. Use SysInternals' Handle command-line
// utility: handle -a ChildProcessTracker
string jobName = "ChildProcessTracker" + Process.GetCurrentProcess().Id;
var jobName = "ChildProcessTracker" + Process.GetCurrentProcess().Id;
s_jobHandle = CreateJobObject(IntPtr.Zero, jobName);
var info = new JOBOBJECT_BASIC_LIMIT_INFORMATION();
@ -58,17 +47,15 @@ namespace Wabbajack.Common
var extendedInfo = new JOBOBJECT_EXTENDED_LIMIT_INFORMATION();
extendedInfo.BasicLimitInformation = info;
int length = Marshal.SizeOf(typeof(JOBOBJECT_EXTENDED_LIMIT_INFORMATION));
IntPtr extendedInfoPtr = Marshal.AllocHGlobal(length);
var length = Marshal.SizeOf(typeof(JOBOBJECT_EXTENDED_LIMIT_INFORMATION));
var extendedInfoPtr = Marshal.AllocHGlobal(length);
try
{
Marshal.StructureToPtr(extendedInfo, extendedInfoPtr, false);
if (!SetInformationJobObject(s_jobHandle, JobObjectInfoType.ExtendedLimitInformation,
extendedInfoPtr, (uint)length))
{
extendedInfoPtr, (uint) length))
throw new Win32Exception();
}
}
finally
{
@ -76,20 +63,31 @@ namespace Wabbajack.Common
}
}
/// <summary>
/// Add the process to be tracked. If our current process is killed, the child processes
/// that we are tracking will be automatically killed, too. If the child process terminates
/// first, that's fine, too.
/// </summary>
/// <param name="process"></param>
public static void AddProcess(Process process)
{
if (s_jobHandle != IntPtr.Zero)
{
var success = AssignProcessToJobObject(s_jobHandle, process.Handle);
if (!success && !process.HasExited)
throw new Win32Exception();
}
}
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
static extern IntPtr CreateJobObject(IntPtr lpJobAttributes, string name);
private static extern IntPtr CreateJobObject(IntPtr lpJobAttributes, string name);
[DllImport("kernel32.dll")]
static extern bool SetInformationJobObject(IntPtr job, JobObjectInfoType infoType,
private static extern bool SetInformationJobObject(IntPtr job, JobObjectInfoType infoType,
IntPtr lpJobObjectInfo, uint cbJobObjectInfoLength);
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool AssignProcessToJobObject(IntPtr job, IntPtr process);
// Windows will automatically close any open job handles when our process terminates.
// This can be verified by using SysInternals' Handle utility. When the job handle
// is closed, the child processes will be killed.
private static readonly IntPtr s_jobHandle;
private static extern bool AssignProcessToJobObject(IntPtr job, IntPtr process);
}
public enum JobObjectInfoType
@ -106,15 +104,15 @@ namespace Wabbajack.Common
[StructLayout(LayoutKind.Sequential)]
public struct JOBOBJECT_BASIC_LIMIT_INFORMATION
{
public Int64 PerProcessUserTimeLimit;
public Int64 PerJobUserTimeLimit;
public long PerProcessUserTimeLimit;
public long PerJobUserTimeLimit;
public JOBOBJECTLIMIT LimitFlags;
public UIntPtr MinimumWorkingSetSize;
public UIntPtr MaximumWorkingSetSize;
public UInt32 ActiveProcessLimit;
public Int64 Affinity;
public UInt32 PriorityClass;
public UInt32 SchedulingClass;
public uint ActiveProcessLimit;
public long Affinity;
public uint PriorityClass;
public uint SchedulingClass;
}
[Flags]
@ -126,12 +124,12 @@ namespace Wabbajack.Common
[StructLayout(LayoutKind.Sequential)]
public struct IO_COUNTERS
{
public UInt64 ReadOperationCount;
public UInt64 WriteOperationCount;
public UInt64 OtherOperationCount;
public UInt64 ReadTransferCount;
public UInt64 WriteTransferCount;
public UInt64 OtherTransferCount;
public ulong ReadOperationCount;
public ulong WriteOperationCount;
public ulong OtherOperationCount;
public ulong ReadTransferCount;
public ulong WriteTransferCount;
public ulong OtherTransferCount;
}
[StructLayout(LayoutKind.Sequential)]
@ -144,4 +142,4 @@ namespace Wabbajack.Common
public UIntPtr PeakProcessMemoryUsed;
public UIntPtr PeakJobMemoryUsed;
}
}
}

View File

@ -1,10 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
namespace Wabbajack.Common
{
@ -16,17 +13,8 @@ namespace Wabbajack.Common
public static string BSACreationDir = "TEMP_BSA_FILES";
public static string MegaPrefix = "https://mega.nz/#!";
public static HashSet<string> SupportedArchives = new HashSet<string> { ".zip", ".rar", ".7z", ".7zip" };
public static HashSet<string> SupportedBSAs = new HashSet<string> { ".bsa" };
public static String UserAgent {
get
{
var platformType = Environment.Is64BitOperatingSystem ? "x64" : "x86";
var headerString = $"{AppName}/{Assembly.GetEntryAssembly().GetName().Version} ({Environment.OSVersion.VersionString}; {platformType}) {RuntimeInformation.FrameworkDescription}";
return headerString;
}
}
public static HashSet<string> SupportedArchives = new HashSet<string> {".zip", ".rar", ".7z", ".7zip"};
public static HashSet<string> SupportedBSAs = new HashSet<string> {".bsa"};
public static HashSet<string> ConfigFileExtensions = new HashSet<string> {".json", ".ini", ".yml"};
@ -48,9 +36,21 @@ namespace Wabbajack.Common
public static string DOWNLOAD_PATH_MAGIC_FORWARD = "{--||DOWNLOAD_PATH_MAGIC_FORWARD||--}";
public static String AppName = "Wabbajack";
public static string AppName = "Wabbajack";
public static string HashCacheName = "Wabbajack.hash_cache";
public static HashSet<string> GameESMs = new HashSet<string>() { "Skyrim.esm", "Update.esm", "Dawnguard.esm", "HearthFires.esm", "Dragonborn.esm" };
public static HashSet<string> GameESMs = new HashSet<string>
{"Skyrim.esm", "Update.esm", "Dawnguard.esm", "HearthFires.esm", "Dragonborn.esm"};
public static string UserAgent
{
get
{
var platformType = Environment.Is64BitOperatingSystem ? "x64" : "x86";
var headerString =
$"{AppName}/{Assembly.GetEntryAssembly().GetName().Version} ({Environment.OSVersion.VersionString}; {platformType}) {RuntimeInformation.FrameworkDescription}";
return headerString;
}
}
}
}
}

View File

@ -1,16 +1,13 @@
using IniParser;
using IniParser.Model;
using System;
using System.Collections.Generic;
using System.Dynamic;
using System.Text;
using System.Dynamic;
using System.Text.RegularExpressions;
using IniParser;
using IniParser.Model;
namespace Wabbajack.Common
{
public class DynamicIniData : DynamicObject
{
private IniData value;
private readonly IniData value;
public DynamicIniData(IniData value) //
{
@ -35,24 +32,20 @@ namespace Wabbajack.Common
}
}
class SectionData : DynamicObject
internal class SectionData : DynamicObject
{
private KeyDataCollection _coll;
private readonly KeyDataCollection _coll;
public SectionData(KeyDataCollection coll)
{
this._coll = coll;
_coll = coll;
}
public override bool TryGetMember(GetMemberBinder binder, out object result)
{
result = _coll[binder.Name];
if (result is string)
{
result = Regex.Unescape(((string)result).Trim('"'));
}
if (result is string) result = Regex.Unescape(((string) result).Trim('"'));
return true;
}
}
}
}

View File

@ -1,16 +1,9 @@
using Compression.BSA;
using ICSharpCode.SharpZipLib.Zip;
using System;
using System.Collections.Generic;
using System;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using Alphaleonis.Win32.Filesystem;
using Compression.BSA;
using ICSharpCode.SharpZipLib.GZip;
using ICSharpCode.SharpZipLib.Zip.Compression.Streams;
namespace Wabbajack.Common
{
@ -34,25 +27,15 @@ namespace Wabbajack.Common
}
}
public class Entry
{
public string Name;
public ulong Size;
}
public static void ExtractAll(string source, string dest)
{
try
{
if (source.EndsWith(".bsa"))
{
ExtractAllWithBSA(source, dest);
}
else
{
ExtractAllWith7Zip(source, dest);
}
}
catch (Exception ex)
{
@ -105,12 +88,12 @@ namespace Wabbajack.Common
RedirectStandardInput = true,
RedirectStandardOutput = true,
UseShellExecute = false,
CreateNoWindow = true,
CreateNoWindow = true
};
var p = new Process
{
StartInfo = info,
StartInfo = info
};
p.Start();
@ -119,7 +102,9 @@ namespace Wabbajack.Common
{
p.PriorityClass = ProcessPriorityClass.BelowNormal;
}
catch (Exception) { }
catch (Exception)
{
}
var name = Path.GetFileName(source);
try
@ -129,18 +114,16 @@ namespace Wabbajack.Common
var line = p.StandardOutput.ReadLine();
if (line == null)
break;
int percent = 0;
var percent = 0;
if (line.Length > 4 && line[3] == '%')
{
Int32.TryParse(line.Substring(0, 3), out percent);
int.TryParse(line.Substring(0, 3), out percent);
Utils.Status($"Extracting {name} - {line.Trim()}", percent);
}
}
}
catch (Exception ex)
{
}
p.WaitForExit();
@ -149,11 +132,10 @@ namespace Wabbajack.Common
Utils.Log(p.StandardOutput.ReadToEnd());
Utils.Log($"Extraction error extracting {source}");
}
}
/// <summary>
/// Returns true if the given extension type can be extracted
/// Returns true if the given extension type can be extracted
/// </summary>
/// <param name="v"></param>
/// <returns></returns>
@ -162,5 +144,10 @@ namespace Wabbajack.Common
return Consts.SupportedArchives.Contains(v) || v == ".bsa";
}
public class Entry
{
public string Name;
public ulong Size;
}
}
}
}

View File

@ -1,5 +1,4 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
@ -33,4 +32,4 @@ using System.Runtime.InteropServices;
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

View File

@ -1,18 +1,22 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Wabbajack.Common
{
public class SplittingStream : Stream
{
private Stream _a;
private Stream _b;
private bool _leave_a_open;
private bool _leave_b_open;
private readonly Stream _a;
private readonly Stream _b;
private readonly bool _leave_a_open;
private readonly bool _leave_b_open;
public SplittingStream(Stream a, bool leave_a_open, Stream b, bool leave_b_open)
{
_a = a;
_b = b;
_leave_a_open = leave_a_open;
_leave_b_open = leave_b_open;
}
public override bool CanRead => false;
@ -22,14 +26,10 @@ namespace Wabbajack.Common
public override long Length => throw new NotImplementedException();
public override long Position { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
public SplittingStream(Stream a, bool leave_a_open, Stream b, bool leave_b_open)
public override long Position
{
_a = a;
_b = b;
_leave_a_open = leave_a_open;
_leave_b_open = leave_b_open;
get => throw new NotImplementedException();
set => throw new NotImplementedException();
}
public override void Flush()
@ -68,4 +68,4 @@ namespace Wabbajack.Common
}
}
}
}
}

View File

@ -1,18 +1,17 @@
using ICSharpCode.SharpZipLib.BZip2;
using IniParser;
using Newtonsoft.Json;
using Newtonsoft.Json.Bson;
using System;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.Configuration;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using ICSharpCode.SharpZipLib.BZip2;
using IniParser;
using Newtonsoft.Json;
using Newtonsoft.Json.Bson;
using File = Alphaleonis.Win32.Filesystem.File;
using FileInfo = Alphaleonis.Win32.Filesystem.FileInfo;
using Path = Alphaleonis.Win32.Filesystem.Path;
@ -24,6 +23,8 @@ namespace Wabbajack.Common
private static Action<string> _loggerFn;
private static Action<string, int> _statusFn;
private static readonly string[] Suffix = {"B", "KB", "MB", "GB", "TB", "PB", "EB"}; //Longs run out around EB
public static void SetLoggerFn(Action<string> f)
{
_loggerFn = f;
@ -43,10 +44,10 @@ namespace Wabbajack.Common
{
_statusFn?.Invoke(msg, progress);
}
/// <summary>
/// MurMur3 hashes the file pointed to by this string
/// MurMur3 hashes the file pointed to by this string
/// </summary>
/// <param name="file"></param>
/// <returns></returns>
@ -56,10 +57,12 @@ namespace Wabbajack.Common
using (var o = new CryptoStream(Stream.Null, sha, CryptoStreamMode.Write))
{
using (var i = File.OpenRead(file))
{
i.CopyToWithStatus(new FileInfo(file).Length, o, $"Hashing {Path.GetFileName(file)}");
}
}
return sha.Hash.ToBase64();
return sha.Hash.ToBase64();
}
public static void CopyToWithStatus(this Stream istream, long maxSize, Stream ostream, string status)
@ -69,22 +72,21 @@ namespace Wabbajack.Common
long total_read = 0;
while (true)
{
int read = istream.Read(buffer, 0, buffer.Length);
var read = istream.Read(buffer, 0, buffer.Length);
if (read == 0) break;
total_read += read;
ostream.Write(buffer, 0, read);
Status(status, (int)(total_read * 100 / maxSize));
Status(status, (int) (total_read * 100 / maxSize));
}
}
public static string SHA256(this byte[] data)
{
return new SHA256Managed().ComputeHash(data).ToBase64();
}
/// <summary>
/// Returns a Base64 encoding of these bytes
/// Returns a Base64 encoding of these bytes
/// </summary>
/// <param name="data"></param>
/// <returns></returns>
@ -95,16 +97,13 @@ namespace Wabbajack.Common
public static string ToHEX(this byte[] bytes)
{
StringBuilder builder = new StringBuilder();
for (int i = 0; i < bytes.Length; i++)
{
builder.Append(bytes[i].ToString("x2"));
}
var builder = new StringBuilder();
for (var i = 0; i < bytes.Length; i++) builder.Append(bytes[i].ToString("x2"));
return builder.ToString();
}
/// <summary>
/// Returns data from a base64 stream
/// Returns data from a base64 stream
/// </summary>
/// <param name="data"></param>
/// <returns></returns>
@ -114,7 +113,7 @@ namespace Wabbajack.Common
}
/// <summary>
/// Executes the action for every item in coll
/// Executes the action for every item in coll
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="coll"></param>
@ -126,7 +125,7 @@ namespace Wabbajack.Common
public static void DoIndexed<T>(this IEnumerable<T> coll, Action<int, T> f)
{
int idx = 0;
var idx = 0;
foreach (var i in coll)
{
f(idx, i);
@ -136,8 +135,8 @@ namespace Wabbajack.Common
/// <summary>
/// Loads INI data from the given filename and returns a dynamic type that
/// can use . operators to navigate the INI.
/// Loads INI data from the given filename and returns a dynamic type that
/// can use . operators to navigate the INI.
/// </summary>
/// <param name="file"></param>
/// <returns></returns>
@ -148,50 +147,57 @@ namespace Wabbajack.Common
public static void ToJSON<T>(this T obj, string filename)
{
File.WriteAllText(filename, JsonConvert.SerializeObject(obj, Formatting.Indented, new JsonSerializerSettings() {TypeNameHandling = TypeNameHandling.Auto}));
File.WriteAllText(filename,
JsonConvert.SerializeObject(obj, Formatting.Indented,
new JsonSerializerSettings {TypeNameHandling = TypeNameHandling.Auto}));
}
public static void ToBSON<T>(this T obj, string filename)
{
using(var fo = File.OpenWrite(filename))
using(var br = new BsonDataWriter(fo))
using (var fo = File.OpenWrite(filename))
using (var br = new BsonDataWriter(fo))
{
fo.SetLength(0);
var serializer = JsonSerializer.Create(new JsonSerializerSettings() { TypeNameHandling = TypeNameHandling.Auto });
var serializer = JsonSerializer.Create(new JsonSerializerSettings
{TypeNameHandling = TypeNameHandling.Auto});
serializer.Serialize(br, obj);
}
}
public static ulong ToMilliseconds(this DateTime date)
{
return (ulong)(date - new DateTime(1970, 1, 1)).TotalMilliseconds;
return (ulong) (date - new DateTime(1970, 1, 1)).TotalMilliseconds;
}
public static string ToJSON<T>(this T obj)
{
return JsonConvert.SerializeObject(obj, Formatting.Indented, new JsonSerializerSettings() { TypeNameHandling = TypeNameHandling.Auto });
return JsonConvert.SerializeObject(obj, Formatting.Indented,
new JsonSerializerSettings {TypeNameHandling = TypeNameHandling.Auto});
}
public static T FromJSON<T>(this string filename)
{
return JsonConvert.DeserializeObject<T>(File.ReadAllText(filename), new JsonSerializerSettings() { TypeNameHandling = TypeNameHandling.Auto });
return JsonConvert.DeserializeObject<T>(File.ReadAllText(filename),
new JsonSerializerSettings {TypeNameHandling = TypeNameHandling.Auto});
}
public static T FromBSON<T>(this string filename, bool root_is_array = false)
{
using (var fo = File.OpenRead(filename))
using (var br = new BsonDataReader(fo, readRootValueAsArray: root_is_array, DateTimeKind.Local))
using (var br = new BsonDataReader(fo, root_is_array, DateTimeKind.Local))
{
var serializer = JsonSerializer.Create(new JsonSerializerSettings() { TypeNameHandling = TypeNameHandling.Auto });
var serializer = JsonSerializer.Create(new JsonSerializerSettings
{TypeNameHandling = TypeNameHandling.Auto});
return serializer.Deserialize<T>(br);
}
}
public static T FromJSONString<T>(this string data)
{
return JsonConvert.DeserializeObject<T>(data, new JsonSerializerSettings() { TypeNameHandling = TypeNameHandling.Auto });
return JsonConvert.DeserializeObject<T>(data,
new JsonSerializerSettings {TypeNameHandling = TypeNameHandling.Auto});
}
public static T FromJSON<T>(this Stream data)
{
var s = Encoding.UTF8.GetString(data.ReadAll());
@ -209,7 +215,7 @@ namespace Wabbajack.Common
}
/// <summary>
/// Returns the string compressed via BZip2
/// Returns the string compressed via BZip2
/// </summary>
/// <param name="data"></param>
/// <returns></returns>
@ -220,14 +226,17 @@ namespace Wabbajack.Common
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
/// Returns the string compressed via BZip2
/// </summary>
/// <param name="data"></param>
/// <returns></returns>
@ -238,7 +247,9 @@ namespace Wabbajack.Common
using (var bz = new BZip2InputStream(s))
{
using (var bw = new BinaryReader(bz))
{
return bw.ReadString();
}
}
}
}
@ -258,11 +269,11 @@ namespace Wabbajack.Common
Interlocked.Add(ref WorkQueue.MaxQueueSize, colllst.Count);
//WorkQueue.CurrentQueueSize = 0;
int remaining_tasks = colllst.Count;
var remaining_tasks = colllst.Count;
var tasks = coll.Select(i =>
{
TaskCompletionSource<TR> tc = new TaskCompletionSource<TR>();
var tc = new TaskCompletionSource<TR>();
WorkQueue.QueueTask(() =>
{
try
@ -273,6 +284,7 @@ namespace Wabbajack.Common
{
tc.SetException(ex);
}
Interlocked.Increment(ref WorkQueue.CurrentQueueSize);
Interlocked.Decrement(ref remaining_tasks);
WorkQueue.ReportNow();
@ -282,13 +294,9 @@ namespace Wabbajack.Common
// To avoid thread starvation, we'll start to help out in the work queue
if (WorkQueue.WorkerThread)
while(remaining_tasks > 0)
{
if(WorkQueue.Queue.TryTake(out var a, 500))
{
a();
}
}
while (remaining_tasks > 0)
if (WorkQueue.Queue.TryTake(out var a, 500))
a();
if (WorkQueue.CurrentQueueSize == WorkQueue.MaxQueueSize)
{
@ -307,14 +315,14 @@ namespace Wabbajack.Common
public static void PMap<TI>(this IEnumerable<TI> coll, Action<TI> f)
{
coll.PMap<TI, bool>(i =>
coll.PMap(i =>
{
f(i);
return false;
});
}
public static void DoProgress<T>(this IEnumerable<T> coll, String msg, Action<T> f)
public static void DoProgress<T>(this IEnumerable<T> coll, string msg, Action<T> f)
{
var lst = coll.ToList();
lst.DoIndexed((idx, i) =>
@ -335,6 +343,7 @@ namespace Wabbajack.Common
result.Wait();
return result.Result;
}
public static string GetStringSync(this HttpClient client, string url)
{
var result = client.GetStringAsync(url);
@ -360,15 +369,14 @@ namespace Wabbajack.Common
public static string ExceptionToString(this Exception ex)
{
StringBuilder sb = new StringBuilder();
var sb = new StringBuilder();
while (ex != null)
{
sb.AppendLine(ex.Message);
var st = new StackTrace(ex, true);
foreach (var frame in st.GetFrames())
{
sb.AppendLine($"{frame.GetFileName()}:{frame.GetMethod().Name}:{frame.GetFileLineNumber()}:{frame.GetFileColumnNumber()}");
}
sb.AppendLine(
$"{frame.GetFileName()}:{frame.GetMethod().Name}:{frame.GetFileLineNumber()}:{frame.GetFileColumnNumber()}");
ex = ex.InnerException;
}
@ -383,13 +391,13 @@ namespace Wabbajack.Common
public static IEnumerable<T> DistinctBy<T, V>(this IEnumerable<T> vs, Func<T, V> select)
{
HashSet<V> set = new HashSet<V>();
foreach (var v in vs) {
var set = new HashSet<V>();
foreach (var v in vs)
{
var key = select(v);
if (set.Contains(key)) continue;
yield return v;
}
}
public static T Last<T>(this T[] a)
@ -401,19 +409,18 @@ namespace Wabbajack.Common
public static V GetOrDefault<K, V>(this IDictionary<K, V> dict, K key)
{
if (dict.TryGetValue(key, out V v)) return v;
return default(V);
if (dict.TryGetValue(key, out var v)) return v;
return default;
}
private static string[] Suffix = { "B", "KB", "MB", "GB", "TB", "PB", "EB" }; //Longs run out around EB
public static string ToFileSizeString(this long byteCount)
{
if (byteCount == 0)
return "0" + Suffix[0];
long bytes = Math.Abs(byteCount);
int place = Convert.ToInt32(Math.Floor(Math.Log(bytes, 1024)));
double num = Math.Round(bytes / Math.Pow(1024, place), 1);
return (Math.Sign(byteCount) * num).ToString() + Suffix[place];
var bytes = Math.Abs(byteCount);
var place = Convert.ToInt32(Math.Floor(Math.Log(bytes, 1024)));
var num = Math.Round(bytes / Math.Pow(1024, place), 1);
return Math.Sign(byteCount) * num + Suffix[place];
}
public static void CreatePatch(byte[] a, byte[] b, Stream output)
@ -452,8 +459,9 @@ namespace Wabbajack.Common
public static void TryGetPatch(string foundHash, string fileHash, out byte[] ePatch)
{
var patch_name = Path.Combine("patch_cache", $"{foundHash.FromBase64().ToHEX()}_{fileHash.FromBase64().ToHEX()}.patch");
var patch_name = Path.Combine("patch_cache",
$"{foundHash.FromBase64().ToHEX()}_{fileHash.FromBase64().ToHEX()}.patch");
ePatch = File.Exists(patch_name) ? File.ReadAllBytes(patch_name) : null;
}
}
}
}

View File

@ -2,21 +2,26 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Wabbajack.Common
{
public class WorkQueue
{
internal static BlockingCollection<Action> Queue = new BlockingCollection<Action>(new ConcurrentStack<Action>());
internal static BlockingCollection<Action>
Queue = new BlockingCollection<Action>(new ConcurrentStack<Action>());
[ThreadStatic]
private static int CpuId;
[ThreadStatic] private static int CpuId;
[ThreadStatic]
internal static bool WorkerThread;
[ThreadStatic] internal static bool WorkerThread;
public static int MaxQueueSize;
public static int CurrentQueueSize;
public static Action<int, string, int> ReportFunction { get; private set; }
public static Action<int, int> ReportQueueSize { get; private set; }
public static int ThreadCount { get; private set; }
public static List<Thread> Threads { get; private set; }
public static void Init(Action<int, string, int> report_function, Action<int, int> report_queue_size)
{
@ -29,15 +34,15 @@ namespace Wabbajack.Common
private static void StartThreads()
{
Threads = Enumerable.Range(0, ThreadCount)
.Select(idx =>
{
var thread = new Thread(() => ThreadBody(idx));
thread.Priority = ThreadPriority.BelowNormal;
thread.IsBackground = true;
thread.Name = String.Format("Wabbajack_Worker_{0}", idx);
thread.Start();
return thread;
}).ToList();
.Select(idx =>
{
var thread = new Thread(() => ThreadBody(idx));
thread.Priority = ThreadPriority.BelowNormal;
thread.IsBackground = true;
thread.Name = string.Format("Wabbajack_Worker_{0}", idx);
thread.Start();
return thread;
}).ToList();
}
private static void ThreadBody(int idx)
@ -45,14 +50,14 @@ namespace Wabbajack.Common
CpuId = idx;
WorkerThread = true;
while(true)
while (true)
{
Report("Waiting", 0);
var f = Queue.Take();
f();
}
}
public static void Report(string msg, int progress)
{
ReportFunction(CpuId, msg, progress);
@ -63,16 +68,9 @@ namespace Wabbajack.Common
Queue.Add(a);
}
public static Action<int, string, int> ReportFunction { get; private set; }
public static Action<int, int> ReportQueueSize { get; private set; }
public static int ThreadCount { get; private set; }
public static List<Thread> Threads { get; private set; }
public static int MaxQueueSize;
public static int CurrentQueueSize;
internal static void ReportNow()
{
ReportQueueSize(MaxQueueSize, CurrentQueueSize);
}
}
}
}

View File

@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="AlphaFS" version="2.2.6" targetFramework="net472" />
<package id="ini-parser" version="2.5.2" targetFramework="net472" />

View File

@ -1,8 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
</startup>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
</startup>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>

View File

@ -10,4 +10,4 @@
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>
</Application>

View File

@ -1,18 +1,11 @@
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.Windows;
namespace Wabbajack
{
/// <summary>
/// Interaction logic for App.xaml
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
}
}
}

View File

@ -4,160 +4,47 @@ using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Windows;
using System.Windows.Input;
using System.Windows.Threading;
using Ookii.Dialogs.Wpf;
using Wabbajack.Common;
namespace Wabbajack
{
internal class AppState : INotifyPropertyChanged
{
public class CPUStatus
{
public int Progress { get; internal set; }
public string Msg { get; internal set; }
public int ID { get; internal set; }
}
private ICommand _begin;
public volatile bool Dirty;
private ICommand _changeDownloadPath;
private Dispatcher dispatcher;
private ICommand _changePath;
private string _downloadLocation;
public event PropertyChangedEventHandler PropertyChanged;
private string _htmlReport;
public void OnPropertyChanged(string name)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
private bool _ignoreMissingFiles;
private string _location;
public ObservableCollection<string> Log { get; }
public ObservableCollection<CPUStatus> Status { get; }
private string _mo2Folder;
private string _mode;
public string Mode
{
get
{
return _mode;
}
set
{
_mode = value;
OnPropertyChanged("Mode");
}
}
private bool _ignoreMissingFiles = false;
public bool IgnoreMissingFiles
{
get
{
return _ignoreMissingFiles;
}
set
{
if (value)
{
if (MessageBox.Show("Setting this value could result in broken installations. \n Are you sure you want to continue?", "Ignore Missing Files?", MessageBoxButton.OKCancel, MessageBoxImage.Warning)
== MessageBoxResult.OK)
{
_ignoreMissingFiles = value;
}
}
else
{
_ignoreMissingFiles = value;
}
OnPropertyChanged("IgnoreMissingFiles");
}
}
private string _mo2Folder;
private string _modListName;
private ModList _modList;
private string _location;
private string _downloadLocation;
public string ModListName
{
get
{
return _modListName;
}
set
{
_modListName = value;
OnPropertyChanged("ModListName");
}
}
public string Location
{
get
{
return _location;
}
set
{
_location = value;
OnPropertyChanged("Location");
}
}
public string DownloadLocation
{
get
{
return _downloadLocation;
}
set
{
_downloadLocation = value;
OnPropertyChanged("DownloadLocation");
}
}
private string _htmlReport;
public Visibility ShowReportButton => _htmlReport == null ? Visibility.Collapsed : Visibility.Visible;
public string HTMLReport
{
get { return _htmlReport; }
set
{
_htmlReport = value;
OnPropertyChanged("HTMLReport");
OnPropertyChanged("ShowReportButton");
}
}
private string _modListName;
private int _queueProgress;
public int QueueProgress
{
get
{
return _queueProgress;
}
set
{
if (value != _queueProgress)
{
_queueProgress = value;
OnPropertyChanged("QueueProgress");
}
}
}
private ICommand _showReportCommand;
private readonly DateTime _startTime;
private List<CPUStatus> InternalStatus { get; }
public string LogFile { get; private set; }
public volatile bool Dirty;
public AppState(Dispatcher d, String mode)
private readonly Dispatcher dispatcher;
public AppState(Dispatcher d, string mode)
{
if (Assembly.GetEntryAssembly().Location.ToLower().Contains("\\downloads\\"))
{
@ -189,28 +76,164 @@ namespace Wabbajack
th.Start();
}
public ObservableCollection<string> Log { get; }
public ObservableCollection<CPUStatus> Status { get; }
public string Mode
{
get => _mode;
set
{
_mode = value;
OnPropertyChanged("Mode");
}
}
public bool IgnoreMissingFiles
{
get => _ignoreMissingFiles;
set
{
if (value)
{
if (MessageBox.Show(
"Setting this value could result in broken installations. \n Are you sure you want to continue?",
"Ignore Missing Files?", MessageBoxButton.OKCancel, MessageBoxImage.Warning)
== MessageBoxResult.OK)
_ignoreMissingFiles = value;
}
else
{
_ignoreMissingFiles = value;
}
OnPropertyChanged("IgnoreMissingFiles");
}
}
public string ModListName
{
get => _modListName;
set
{
_modListName = value;
OnPropertyChanged("ModListName");
}
}
public string Location
{
get => _location;
set
{
_location = value;
OnPropertyChanged("Location");
}
}
public string DownloadLocation
{
get => _downloadLocation;
set
{
_downloadLocation = value;
OnPropertyChanged("DownloadLocation");
}
}
public Visibility ShowReportButton => _htmlReport == null ? Visibility.Collapsed : Visibility.Visible;
public string HTMLReport
{
get => _htmlReport;
set
{
_htmlReport = value;
OnPropertyChanged("HTMLReport");
OnPropertyChanged("ShowReportButton");
}
}
public int QueueProgress
{
get => _queueProgress;
set
{
if (value != _queueProgress)
{
_queueProgress = value;
OnPropertyChanged("QueueProgress");
}
}
}
private List<CPUStatus> InternalStatus { get; }
public string LogFile { get; }
public ICommand ChangePath
{
get
{
if (_changePath == null) _changePath = new LambdaCommand(() => true, () => ExecuteChangePath());
return _changePath;
}
}
public ICommand ChangeDownloadPath
{
get
{
if (_changeDownloadPath == null)
_changeDownloadPath = new LambdaCommand(() => true, () => ExecuteChangeDownloadPath());
return _changeDownloadPath;
}
}
public ICommand Begin
{
get
{
if (_begin == null) _begin = new LambdaCommand(() => true, () => ExecuteBegin());
return _begin;
}
}
public ICommand ShowReportCommand
{
get
{
if (_showReportCommand == null) _showReportCommand = new LambdaCommand(() => true, () => ShowReport());
return _showReportCommand;
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string name)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
private void UpdateLoop()
{
while (true)
{
if (Dirty)
{
lock (InternalStatus)
{
var data = InternalStatus.ToArray();
dispatcher.Invoke(() =>
{
for (int idx = 0; idx < data.Length; idx += 1)
{
for (var idx = 0; idx < data.Length; idx += 1)
if (idx >= Status.Count)
Status.Add(data[idx]);
else if (Status[idx] != data[idx])
Status[idx] = data[idx];
}
});
Dirty = false;
}
}
Thread.Sleep(1000);
}
}
@ -222,14 +245,14 @@ namespace Wabbajack
ModListName = _modList.Name;
HTMLReport = _modList.ReportHTML;
Location = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
}
public void LogMsg(string msg)
{
msg = $"{(DateTime.Now - _startTime).TotalSeconds:0.##} - {msg}";
dispatcher.Invoke(() => Log.Add(msg));
lock (dispatcher) {
lock (dispatcher)
{
File.AppendAllText(LogFile, msg + "\r\n");
}
}
@ -239,12 +262,9 @@ namespace Wabbajack
lock (InternalStatus)
{
Dirty = true;
while (id >= InternalStatus.Count)
{
InternalStatus.Add(new CPUStatus());
}
while (id >= InternalStatus.Count) InternalStatus.Add(new CPUStatus());
InternalStatus[id] = new CPUStatus() { ID = id, Msg = msg, Progress = progress };
InternalStatus[id] = new CPUStatus {ID = id, Msg = msg, Progress = progress};
}
}
@ -256,37 +276,11 @@ namespace Wabbajack
QueueProgress = total;
}
private ICommand _changePath;
public ICommand ChangePath
{
get
{
if (_changePath == null)
{
_changePath = new LambdaCommand(() => true, () => this.ExecuteChangePath());
}
return _changePath;
}
}
private ICommand _changeDownloadPath;
public ICommand ChangeDownloadPath
{
get
{
if (_changeDownloadPath == null)
{
_changeDownloadPath = new LambdaCommand(() => true, () => this.ExecuteChangeDownloadPath());
}
return _changeDownloadPath;
}
}
private void ExecuteChangePath()
{
if (Mode == "Installing")
{
var ofd = new Ookii.Dialogs.Wpf.VistaFolderBrowserDialog();
var ofd = new VistaFolderBrowserDialog();
ofd.Description = "Select Installation Directory";
ofd.UseDescriptionForTitle = true;
if (ofd.ShowDialog() == true)
@ -298,7 +292,7 @@ namespace Wabbajack
}
else
{
var fsd = new Ookii.Dialogs.Wpf.VistaOpenFileDialog();
var fsd = new VistaOpenFileDialog();
fsd.Title = "Select a ModOrganizer modlist.txt file";
fsd.Filter = "modlist.txt|modlist.txt";
if (fsd.ShowDialog() == true)
@ -311,13 +305,10 @@ namespace Wabbajack
private void ExecuteChangeDownloadPath()
{
var ofd = new Ookii.Dialogs.Wpf.VistaFolderBrowserDialog();
var ofd = new VistaFolderBrowserDialog();
ofd.Description = "Select a location for MO2 downloads";
ofd.UseDescriptionForTitle = true;
if (ofd.ShowDialog() == true)
{
DownloadLocation = ofd.SelectedPath;
}
if (ofd.ShowDialog() == true) DownloadLocation = ofd.SelectedPath;
}
private void ConfigureForBuild()
@ -337,34 +328,6 @@ namespace Wabbajack
_mo2Folder = mo2folder;
}
private ICommand _begin;
private DateTime _startTime;
public ICommand Begin
{
get
{
if (_begin == null)
{
_begin = new LambdaCommand(() => true, () => this.ExecuteBegin());
}
return _begin;
}
}
private ICommand _showReportCommand;
public ICommand ShowReportCommand
{
get
{
if (_showReportCommand == null)
{
_showReportCommand = new LambdaCommand(() => true, () => this.ShowReport());
}
return _showReportCommand;
}
}
private void ShowReport()
{
var file = Path.GetTempFileName() + ".html";
@ -377,7 +340,7 @@ namespace Wabbajack
{
if (Mode == "Installing")
{
var installer = new Installer(_modList, Location, msg => this.LogMsg(msg));
var installer = new Installer(_modList, Location, msg => LogMsg(msg));
installer.IgnoreMissingFiles = IgnoreMissingFiles;
installer.DownloadFolder = DownloadLocation;
var th = new Thread(() =>
@ -408,9 +371,7 @@ namespace Wabbajack
{
compiler.Compile();
if (compiler.ModList != null && compiler.ModList.ReportHTML != null)
{
HTMLReport = compiler.ModList.ReportHTML;
}
}
catch (Exception ex)
{
@ -424,5 +385,12 @@ namespace Wabbajack
th.Start();
}
}
public class CPUStatus
{
public int Progress { get; internal set; }
public string Msg { get; internal set; }
public int ID { get; internal set; }
}
}
}

View File

@ -2,28 +2,15 @@
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
namespace Wabbajack
{
class AutoScrollBehavior
internal class AutoScrollBehavior
{
static readonly Dictionary<ListBox, Capture> Associations =
new Dictionary<ListBox, Capture>();
public static bool GetScrollOnNewItem(DependencyObject obj)
{
return (bool)obj.GetValue(ScrollOnNewItemProperty);
}
public static void SetScrollOnNewItem(DependencyObject obj, bool value)
{
obj.SetValue(ScrollOnNewItemProperty, value);
}
private static readonly Dictionary<ListBox, Capture> Associations =
new Dictionary<ListBox, Capture>();
public static readonly DependencyProperty ScrollOnNewItemProperty =
DependencyProperty.RegisterAttached(
@ -32,13 +19,23 @@ namespace Wabbajack
typeof(AutoScrollBehavior),
new UIPropertyMetadata(false, OnScrollOnNewItemChanged));
public static bool GetScrollOnNewItem(DependencyObject obj)
{
return (bool) obj.GetValue(ScrollOnNewItemProperty);
}
public static void SetScrollOnNewItem(DependencyObject obj, bool value)
{
obj.SetValue(ScrollOnNewItemProperty, value);
}
public static void OnScrollOnNewItemChanged(
DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
var listBox = d as ListBox;
if (listBox == null) return;
bool oldValue = (bool)e.OldValue, newValue = (bool)e.NewValue;
bool oldValue = (bool) e.OldValue, newValue = (bool) e.NewValue;
if (newValue == oldValue) return;
if (newValue)
{
@ -60,51 +57,39 @@ namespace Wabbajack
private static void ListBox_ItemsSourceChanged(object sender, EventArgs e)
{
var listBox = (ListBox)sender;
var listBox = (ListBox) sender;
if (Associations.ContainsKey(listBox))
Associations[listBox].Dispose();
Associations[listBox] = new Capture(listBox);
}
static void ListBox_Unloaded(object sender, RoutedEventArgs e)
private static void ListBox_Unloaded(object sender, RoutedEventArgs e)
{
var listBox = (ListBox)sender;
var listBox = (ListBox) sender;
if (Associations.ContainsKey(listBox))
Associations[listBox].Dispose();
listBox.Unloaded -= ListBox_Unloaded;
}
static void ListBox_Loaded(object sender, RoutedEventArgs e)
private static void ListBox_Loaded(object sender, RoutedEventArgs e)
{
var listBox = (ListBox)sender;
var listBox = (ListBox) sender;
var incc = listBox.Items as INotifyCollectionChanged;
if (incc == null) return;
listBox.Loaded -= ListBox_Loaded;
Associations[listBox] = new Capture(listBox);
}
class Capture : IDisposable
private class Capture : IDisposable
{
private readonly ListBox listBox;
private readonly INotifyCollectionChanged incc;
private readonly ListBox listBox;
public Capture(ListBox listBox)
{
this.listBox = listBox;
incc = listBox.ItemsSource as INotifyCollectionChanged;
if (incc != null)
{
incc.CollectionChanged += incc_CollectionChanged;
}
}
void incc_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add)
{
listBox.ScrollIntoView(e.NewItems[0]);
listBox.SelectedItem = e.NewItems[0];
}
if (incc != null) incc.CollectionChanged += incc_CollectionChanged;
}
public void Dispose()
@ -112,6 +97,15 @@ namespace Wabbajack
if (incc != null)
incc.CollectionChanged -= incc_CollectionChanged;
}
private void incc_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add)
{
listBox.ScrollIntoView(e.NewItems[0]);
listBox.SelectedItem = e.NewItems[0];
}
}
}
}
}
}

View File

@ -11,12 +11,9 @@ using System.Text.RegularExpressions;
using System.Web;
using CommonMark;
using Compression.BSA;
using ICSharpCode.SharpZipLib.BZip2;
using K4os.Compression.LZ4;
using K4os.Compression.LZ4.Streams;
using Microsoft.SqlServer.Server;
using Newtonsoft.Json;
using SharpCompress.Compressors.BZip2;
using VFS;
using Wabbajack.Common;
using static Wabbajack.NexusAPI;
@ -567,7 +564,11 @@ namespace Wabbajack
private Func<RawSourceFile, Directive> IgnoreWabbajackInstallCruft()
{
var cruft_files = new HashSet<string> {"7z.dll", "7z.exe", "vfs_staged_files\\", "nexus.key_cache", "patch_cache\\", Consts.NexusCacheDirectory + "\\"};
var cruft_files = new HashSet<string>
{
"7z.dll", "7z.exe", "vfs_staged_files\\", "nexus.key_cache", "patch_cache\\",
Consts.NexusCacheDirectory + "\\"
};
return source =>
{
if (!cruft_files.Any(f => source.Path.StartsWith(f))) return null;
@ -882,8 +883,8 @@ namespace Wabbajack
}
/// <summary>
/// This matches files based purely on filename, and then creates a binary patch.
/// In practice this is fine, because a single file tends to only come from one archive.
/// This matches files based purely on filename, and then creates a binary patch.
/// In practice this is fine, because a single file tends to only come from one archive.
/// </summary>
/// <returns></returns>
private Func<RawSourceFile, Directive> IncludePatches()
@ -906,7 +907,6 @@ namespace Wabbajack
e.Hash = source.File.Hash;
Utils.TryGetPatch(found.Hash, source.File.Hash, out e.Patch);
return e;
};
}
@ -1120,8 +1120,11 @@ namespace Wabbajack
//bw.Write(data);
var formatter = new BinaryFormatter();
using (var compressed = LZ4Stream.Encode(bw.BaseStream, new LZ4EncoderSettings { CompressionLevel = LZ4Level.L10_OPT }, true))
using (var compressed = LZ4Stream.Encode(bw.BaseStream,
new LZ4EncoderSettings {CompressionLevel = LZ4Level.L10_OPT}, true))
{
formatter.Serialize(compressed, ModList);
}
bw.Write(orig_pos);
bw.Write(Encoding.ASCII.GetBytes(Consts.ModPackMagic));

View File

@ -1,37 +1,24 @@
using Newtonsoft.Json;
using System;
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using VFS;
namespace Wabbajack
{
public class RawSourceFile
public class RawSourceFile
{
public string Path;
public RawSourceFile(VirtualFile file)
{
File = file;
}
public string AbsolutePath
{
get
{
return File.StagedPath;
}
}
public string Path;
public string AbsolutePath => File.StagedPath;
public VirtualFile File { get; private set; }
public VirtualFile File { get; }
public string Hash
{
get
{
return File.Hash;
}
}
public string Hash => File.Hash;
public T EvolveTo<T>() where T : Directive, new()
{
@ -45,22 +32,22 @@ namespace Wabbajack
public class ModList
{
/// <summary>
/// Name of the ModList
/// </summary>
public string Name;
/// <summary>
/// Install directives
/// </summary>
public List<Directive> Directives;
/// <summary>
/// Archives required by this modlist
/// Archives required by this modlist
/// </summary>
public List<Archive> Archives;
/// <summary>
/// Content Report in HTML form
/// Install directives
/// </summary>
public List<Directive> Directives;
/// <summary>
/// Name of the ModList
/// </summary>
public string Name;
/// <summary>
/// Content Report in HTML form
/// </summary>
public string ReportHTML;
}
@ -69,7 +56,7 @@ namespace Wabbajack
public class Directive
{
/// <summary>
/// location the file will be copied to, relative to the install path.
/// location the file will be copied to, relative to the install path.
/// </summary>
public string To;
}
@ -83,14 +70,13 @@ namespace Wabbajack
[Serializable]
public class NoMatch : IgnoredDirectly
{
}
[Serializable]
public class InlineFile : Directive
{
/// <summary>
/// Data that will be written as-is to the destination location;
/// Data that will be written as-is to the destination location;
/// </summary>
public string SourceData;
}
@ -102,7 +88,7 @@ namespace Wabbajack
}
/// <summary>
/// A file that has the game and MO2 folders remapped on installation
/// A file that has the game and MO2 folders remapped on installation
/// </summary>
[Serializable]
public class RemappedInlineFile : InlineFile
@ -112,25 +98,21 @@ namespace Wabbajack
[Serializable]
public class FromArchive : Directive
{
private string _fullPath;
/// <summary>
/// MurMur3 hash of the archive this file comes from
/// MurMur3 hash of the archive this file comes from
/// </summary>
public string[] ArchiveHashPath;
[JsonIgnore]
[NonSerialized]
public VirtualFile FromFile;
private string _fullPath = null;
[JsonIgnore] [NonSerialized] public VirtualFile FromFile;
[JsonIgnore]
public string FullPath
{
get
{
if (_fullPath == null) {
_fullPath = String.Join("|", ArchiveHashPath);
}
if (_fullPath == null) _fullPath = string.Join("|", ArchiveHashPath);
return _fullPath;
}
}
@ -139,11 +121,11 @@ namespace Wabbajack
[Serializable]
public class CreateBSA : Directive
{
public string TempID;
public string IsCompressed;
public uint Version;
public uint Type;
public bool ShareData;
public string TempID;
public uint Type;
public uint Version;
public uint FileFlags { get; set; }
public bool Compress { get; set; }
@ -153,42 +135,44 @@ namespace Wabbajack
[Serializable]
public class PatchedFromArchive : FromArchive
{
public string Hash;
/// <summary>
/// The file to apply to the source file to patch it
/// The file to apply to the source file to patch it
/// </summary>
public byte[] Patch;
public string Hash;
}
[Serializable]
public class Archive
{
/// <summary>
/// MurMur3 Hash of the archive
/// MurMur3 Hash of the archive
/// </summary>
public string Hash;
/// <summary>
/// Human friendly name of this archive
/// </summary>
public string Name;
/// Meta INI for the downloaded archive
/// </summary>
public string Meta;
/// <summary>
/// Human friendly name of this archive
/// </summary>
public string Name;
public long Size;
}
[Serializable]
public class NexusMod : Archive
{
public string Author;
public string FileID;
public string GameName;
public string ModID;
public string FileID;
public string Version;
public string UploaderProfile;
public string UploadedBy;
public string Author;
public string UploaderProfile;
public string Version;
}
[Serializable]
@ -198,19 +182,19 @@ namespace Wabbajack
}
/// <summary>
/// URL that can be downloaded directly without any additional options
/// URL that can be downloaded directly without any additional options
/// </summary>
[Serializable]
public class DirectURLArchive : Archive
{
public string URL;
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public List<string> Headers;
public string URL;
}
/// <summary>
/// A URL that cannot be downloaded automatically and has to be downloaded by hand
/// A URL that cannot be downloaded automatically and has to be downloaded by hand
/// </summary>
[Serializable]
public class ManualURLArchive : Archive
@ -219,7 +203,7 @@ namespace Wabbajack
}
/// <summary>
/// An archive that requires additional HTTP headers.
/// An archive that requires additional HTTP headers.
/// </summary>
[Serializable]
public class DirectURLArchiveEx : DirectURLArchive
@ -228,7 +212,7 @@ namespace Wabbajack
}
/// <summary>
/// Archive that comes from MEGA
/// Archive that comes from MEGA
/// </summary>
[Serializable]
public class MEGAArchive : DirectURLArchive
@ -236,7 +220,7 @@ namespace Wabbajack
}
/// <summary>
/// Archive that comes from MODDB
/// Archive that comes from MODDB
/// </summary>
[Serializable]
public class MODDBArchive : DirectURLArchive
@ -244,7 +228,7 @@ namespace Wabbajack
}
/// <summary>
/// Archive that comes from MediaFire
/// Archive that comes from MediaFire
/// </summary>
[Serializable]
public class MediaFireArchive : DirectURLArchive
@ -255,27 +239,29 @@ namespace Wabbajack
public class IndexedArchive
{
public dynamic IniData;
public string Name;
public string Meta;
public string Name;
public VirtualFile File { get; internal set; }
}
/// <summary>
/// A archive entry
/// A archive entry
/// </summary>
[Serializable]
public class IndexedEntry
{
/// <summary>
/// Path in the archive to this file
/// </summary>
public string Path;
/// <summary>
/// MurMur3 hash of this file
/// MurMur3 hash of this file
/// </summary>
public string Hash;
/// <summary>
/// Size of the file (uncompressed)
/// Path in the archive to this file
/// </summary>
public string Path;
/// <summary>
/// Size of the file (uncompressed)
/// </summary>
public long Size;
}
@ -287,14 +273,14 @@ namespace Wabbajack
}
/// <summary>
/// Data found inside a BSA file in an archive
/// Data found inside a BSA file in an archive
/// </summary>
[Serializable]
public class BSAIndexedEntry : IndexedEntry
{
/// <summary>
/// MurMur3 hash of the BSA this file comes from
/// MurMur3 hash of the BSA this file comes from
/// </summary>
public string BSAHash;
}
}
}

View File

@ -1,7 +1,4 @@
using CG.Web.MegaApiClient;
using Compression.BSA;
using Ookii.Dialogs.Wpf;
using System;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@ -11,10 +8,11 @@ using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Windows;
using K4os.Compression.LZ4.Encoders;
using CG.Web.MegaApiClient;
using Compression.BSA;
using K4os.Compression.LZ4.Streams;
using Ookii.Dialogs.Wpf;
using VFS;
using Wabbajack.Common;
@ -24,14 +22,6 @@ namespace Wabbajack
{
private string _downloadsFolder;
public VirtualFileSystem VFS
{
get
{
return VirtualFileSystem.VFS;
}
}
public Installer(ModList mod_list, string output_folder, Action<string> log_fn)
{
Outputfolder = output_folder;
@ -39,12 +29,16 @@ namespace Wabbajack
Log_Fn = log_fn;
}
public VirtualFileSystem VFS => VirtualFileSystem.VFS;
public string Outputfolder { get; }
public string DownloadFolder
{
get => _downloadsFolder ?? Path.Combine(Outputfolder, "downloads");
set => _downloadsFolder = value;
}
public ModList ModList { get; }
public Action<string> Log_Fn { get; }
public Dictionary<string, string> HashedArchives { get; private set; }
@ -56,50 +50,49 @@ namespace Wabbajack
public void Info(string msg, params object[] args)
{
if (args.Length > 0)
msg = String.Format(msg, args);
msg = string.Format(msg, args);
Log_Fn(msg);
}
public void Status(string msg, params object[] args)
{
if (args.Length > 0)
msg = String.Format(msg, args);
msg = string.Format(msg, args);
WorkQueue.Report(msg, 0);
}
public void Status(int progress, string msg, params object[] args)
{
if (args.Length > 0)
msg = String.Format(msg, args);
msg = string.Format(msg, args);
WorkQueue.Report(msg, progress);
}
private void Error(string msg, params object[] args)
{
if (args.Length > 0)
msg = String.Format(msg, args);
msg = string.Format(msg, args);
Log_Fn(msg);
throw new Exception(msg);
}
public void Install()
{
Directory.CreateDirectory(Outputfolder);
Directory.CreateDirectory(DownloadFolder);
if (Directory.Exists(Path.Combine(Outputfolder, "mods")))
{
if (MessageBox.Show("There already appears to be a Mod Organize 2 install in this folder, are you sure you wish to continue" +
" with installation? If you do, you may render both your existing install and the new modlist inoperable.",
"Existing MO2 installation in install folder",
MessageBoxButton.YesNo,
MessageBoxImage.Exclamation) == MessageBoxResult.No)
if (MessageBox.Show(
"There already appears to be a Mod Organize 2 install in this folder, are you sure you wish to continue" +
" with installation? If you do, you may render both your existing install and the new modlist inoperable.",
"Existing MO2 installation in install folder",
MessageBoxButton.YesNo,
MessageBoxImage.Exclamation) == MessageBoxResult.No)
Utils.Log("Existing installation at the request of the user, existing mods folder found.");
return;
return;
}
if (ModList.Directives.OfType<RemappedInlineFile>().FirstOrDefault() != null ||
ModList.Directives.OfType<CleanedESM>().FirstOrDefault() != null)
{
@ -125,13 +118,9 @@ namespace Wabbajack
foreach (var a in missing)
Info("Unable to download {0}", a.Name);
if (IgnoreMissingFiles)
{
Info("Missing some archives, but continuing anyways at the request of the user");
}
else
{
Error("Cannot continue, was unable to download one or more archives");
}
}
PrimeVFS();
@ -179,7 +168,6 @@ namespace Wabbajack
Utils.Log($"Endorsed {mod.GameName} - {mod.ModID} - Result: {er.message}");
});
Info("Done! You may now exit the application!");
}
private bool LocateGameFolder()
@ -192,38 +180,36 @@ namespace Wabbajack
GameFolder = vf.SelectedPath;
return true;
}
return false;
}
/// <summary>
/// We don't want to make the installer index all the archives, that's just a waste of time, so instead
/// we'll pass just enough information to VFS to let it know about the files we have.
/// We don't want to make the installer index all the archives, that's just a waste of time, so instead
/// we'll pass just enough information to VFS to let it know about the files we have.
/// </summary>
private void PrimeVFS()
{
HashedArchives.Do(a => VFS.AddKnown(new VirtualFile()
HashedArchives.Do(a => VFS.AddKnown(new VirtualFile
{
Paths = new string[] { a.Value },
Paths = new[] {a.Value},
Hash = a.Key
}));
VFS.RefreshIndexes();
ModList.Directives
.OfType<FromArchive>()
.Do(f =>
{
var updated_path = new string[f.ArchiveHashPath.Length];
f.ArchiveHashPath.CopyTo(updated_path, 0);
updated_path[0] = VFS.HashIndex[updated_path[0]].Where(e => e.IsConcrete).First().FullPath;
VFS.AddKnown(new VirtualFile() { Paths = updated_path });
});
.OfType<FromArchive>()
.Do(f =>
{
var updated_path = new string[f.ArchiveHashPath.Length];
f.ArchiveHashPath.CopyTo(updated_path, 0);
updated_path[0] = VFS.HashIndex[updated_path[0]].Where(e => e.IsConcrete).First().FullPath;
VFS.AddKnown(new VirtualFile {Paths = updated_path});
});
VFS.BackfillMissing();
}
private void BuildBSAs()
@ -236,29 +222,29 @@ namespace Wabbajack
Status($"Building {bsa.To}");
var source_dir = Path.Combine(Outputfolder, Consts.BSACreationDir, bsa.TempID);
var source_files = Directory.EnumerateFiles(source_dir, "*", SearchOption.AllDirectories)
.Select(e => e.Substring(source_dir.Length + 1))
.ToList();
.Select(e => e.Substring(source_dir.Length + 1))
.ToList();
if(source_files.Count > 0)
using (var a = new BSABuilder())
{
//a.Create(Path.Combine(Outputfolder, bsa.To), (bsa_archive_type_t)bsa.Type, entries);
a.HeaderType = (VersionType)bsa.Type;
a.FileFlags = (FileFlags)bsa.FileFlags;
a.ArchiveFlags = (ArchiveFlags)bsa.ArchiveFlags;
source_files.PMap(f =>
if (source_files.Count > 0)
using (var a = new BSABuilder())
{
Status($"Adding {f} to BSA");
using (var fs = File.OpenRead(Path.Combine(source_dir, f)))
a.AddFile(f, fs);
});
//a.Create(Path.Combine(Outputfolder, bsa.To), (bsa_archive_type_t)bsa.Type, entries);
a.HeaderType = (VersionType) bsa.Type;
a.FileFlags = (FileFlags) bsa.FileFlags;
a.ArchiveFlags = (ArchiveFlags) bsa.ArchiveFlags;
Info($"Writing {bsa.To}");
a.Build(Path.Combine(Outputfolder, bsa.To));
source_files.PMap(f =>
{
Status($"Adding {f} to BSA");
using (var fs = File.OpenRead(Path.Combine(source_dir, f)))
{
a.AddFile(f, fs);
}
});
}
Info($"Writing {bsa.To}");
a.Build(Path.Combine(Outputfolder, bsa.To));
}
});
if (Directory.Exists(Consts.BSACreationDir))
@ -266,32 +252,25 @@ namespace Wabbajack
Info($"Removing temp folder {Consts.BSACreationDir}");
Directory.Delete(Path.Combine(Outputfolder, Consts.BSACreationDir), true);
}
}
private void InstallIncludedFiles()
{
Info("Writing inline files");
ModList.Directives
.OfType<InlineFile>()
.PMap(directive =>
{
Status("Writing included file {0}", directive.To);
var out_path = Path.Combine(Outputfolder, directive.To);
if (File.Exists(out_path)) File.Delete(out_path);
if (directive is RemappedInlineFile)
{
WriteRemappedFile((RemappedInlineFile)directive);
}
else if (directive is CleanedESM)
{
GenerateCleanedESM((CleanedESM)directive);
}
else
{
File.WriteAllBytes(out_path, directive.SourceData.FromBase64());
}
});
.OfType<InlineFile>()
.PMap(directive =>
{
Status("Writing included file {0}", directive.To);
var out_path = Path.Combine(Outputfolder, directive.To);
if (File.Exists(out_path)) File.Delete(out_path);
if (directive is RemappedInlineFile)
WriteRemappedFile((RemappedInlineFile) directive);
else if (directive is CleanedESM)
GenerateCleanedESM((CleanedESM) directive);
else
File.WriteAllBytes(out_path, directive.SourceData.FromBase64());
});
}
private void GenerateCleanedESM(CleanedESM directive)
@ -299,19 +278,18 @@ namespace Wabbajack
var filename = Path.GetFileName(directive.To);
var game_file = Path.Combine(GameFolder, "Data", filename);
Info($"Generating cleaned ESM for {filename}");
if (!File.Exists(game_file))
{
throw new InvalidDataException($"Missing {filename} at {game_file}");
}
if (!File.Exists(game_file)) throw new InvalidDataException($"Missing {filename} at {game_file}");
Status($"Hashing game version of {filename}");
var sha = Utils.FileSHA256(game_file);
var sha = game_file.FileSHA256();
if (sha != directive.SourceESMHash)
throw new InvalidDataException($"Cannot patch {filename} from the game folder hashes don't match have you already cleaned the file?");
throw new InvalidDataException(
$"Cannot patch {filename} from the game folder hashes don't match have you already cleaned the file?");
var patch_data = directive.SourceData.FromBase64();
var to_file = Path.Combine(Outputfolder, directive.To);
Status($"Patching {filename}");
using (var output = File.OpenWrite(to_file)) {
using (var output = File.OpenWrite(to_file))
{
BSDiff.Apply(File.OpenRead(game_file), () => new MemoryStream(patch_data), output);
}
}
@ -339,12 +317,13 @@ namespace Wabbajack
{
Info("Building Folder Structure");
ModList.Directives
.Select(d => Path.Combine(Outputfolder, Path.GetDirectoryName(d.To)))
.ToHashSet()
.Do(f => {
if (Directory.Exists(f)) return;
Directory.CreateDirectory(f);
});
.Select(d => Path.Combine(Outputfolder, Path.GetDirectoryName(d.To)))
.ToHashSet()
.Do(f =>
{
if (Directory.Exists(f)) return;
Directory.CreateDirectory(f);
});
}
private void InstallArchives()
@ -352,17 +331,16 @@ namespace Wabbajack
Info("Installing Archives");
Info("Grouping Install Files");
var grouped = ModList.Directives
.OfType<FromArchive>()
.GroupBy(e => e.ArchiveHashPath[0])
.ToDictionary(k => k.Key);
.OfType<FromArchive>()
.GroupBy(e => e.ArchiveHashPath[0])
.ToDictionary(k => k.Key);
var archives = ModList.Archives
.Select(a => new { Archive = a, AbsolutePath = HashedArchives.GetOrDefault(a.Hash) })
.Where(a => a.AbsolutePath != null)
.ToList();
.Select(a => new {Archive = a, AbsolutePath = HashedArchives.GetOrDefault(a.Hash)})
.Where(a => a.AbsolutePath != null)
.ToList();
Info("Installing Archives");
archives.PMap(a => InstallArchive(a.Archive, a.AbsolutePath, grouped[a.Archive.Hash]));
}
private void InstallArchive(Archive archive, string absolutePath, IGrouping<string, FromArchive> grouping)
@ -383,7 +361,7 @@ namespace Wabbajack
vfiles.DoIndexed((idx, file) =>
{
Utils.Status($"Installing files", idx * 100 / vfiles.Count);
Utils.Status("Installing files", idx * 100 / vfiles.Count);
File.Copy(file.FromFile.StagedPath, Path.Combine(Outputfolder, file.To));
});
@ -392,7 +370,6 @@ namespace Wabbajack
// Now patch all the files from this archive
foreach (var to_patch in grouping.OfType<PatchedFromArchive>())
{
using (var patch_stream = new MemoryStream())
{
Status("Patching {0}", Path.GetFileName(to_patch.To));
@ -401,7 +378,7 @@ namespace Wabbajack
var patch_data = to_patch.Patch;
var to_file = Path.Combine(Outputfolder, to_patch.To);
MemoryStream old_data = new MemoryStream(File.ReadAllBytes(to_file));
var old_data = new MemoryStream(File.ReadAllBytes(to_file));
// Remove the file we're about to patch
File.Delete(to_file);
@ -413,12 +390,10 @@ namespace Wabbajack
}
Status($"Verifying Patch {Path.GetFileName(to_patch.To)}");
var result_sha = Utils.FileSHA256(to_file);
var result_sha = to_file.FileSHA256();
if (result_sha != to_patch.Hash)
throw new InvalidDataException($"Invalid Hash for {to_patch.To} after patching");
}
}
}
private void DownloadArchives()
@ -431,16 +406,17 @@ namespace Wabbajack
var user_status = NexusAPI.GetUserStatus(NexusAPIKey);
if (!user_status.is_premium) {
Info($"Automated installs with Wabbajack requires a premium nexus account. {user_status.name} is not a premium account");
if (!user_status.is_premium)
{
Info(
$"Automated installs with Wabbajack requires a premium nexus account. {user_status.name} is not a premium account");
return;
}
DownloadMissingArchives(missing);
return;
}
private void DownloadMissingArchives(List<Archive> missing, bool download=true)
private void DownloadMissingArchives(List<Archive> missing, bool download = true)
{
missing.PMap(archive =>
{
@ -465,7 +441,7 @@ namespace Wabbajack
string url;
try
{
url = NexusAPI.GetNexusDownloadLink(a as NexusMod, NexusAPIKey, !download);
url = NexusAPI.GetNexusDownloadLink(a, NexusAPIKey, !download);
if (!download) return true;
}
catch (Exception ex)
@ -473,6 +449,7 @@ namespace Wabbajack
Info($"{a.Name} - Error Getting Nexus Download URL - {ex.Message}");
return false;
}
Info($"Downloading Nexus Archive - {archive.Name} - {a.GameName} - {a.ModID} - {a.FileID}");
DownloadURLDirect(archive, url);
return true;
@ -484,7 +461,7 @@ namespace Wabbajack
return DownloadModDBArchive(archive, (archive as MODDBArchive).URL, download);
case MediaFireArchive a:
return false;
//return DownloadMediaFireArchive(archive, a.URL, download);
//return DownloadMediaFireArchive(archive, a.URL, download);
case DirectURLArchive a:
return DownloadURLDirect(archive, a.URL, headers: a.Headers, download: download);
}
@ -495,6 +472,7 @@ namespace Wabbajack
Utils.Log(ex.ToString());
return false;
}
return false;
}
@ -529,7 +507,8 @@ namespace Wabbajack
var result = client.GetStringSync(initial_url);
var regex = new Regex("(?<=/uc\\?export=download&amp;confirm=).*(?=;id=)");
var confirm = regex.Match(result);
return DownloadURLDirect(a, $"https://drive.google.com/uc?export=download&confirm={confirm}&id={a.Id}", client, download: download);
return DownloadURLDirect(a, $"https://drive.google.com/uc?export=download&confirm={confirm}&id={a.Id}",
client, download);
}
private bool DownloadModDBArchive(Archive archive, string url, bool download)
@ -541,7 +520,8 @@ namespace Wabbajack
return DownloadURLDirect(archive, match.Value, download: download);
}
private bool DownloadURLDirect(Archive archive, string url, HttpClient client = null, bool download = true, List<string> headers = null)
private bool DownloadURLDirect(Archive archive, string url, HttpClient client = null, bool download = true,
List<string> headers = null)
{
try
{
@ -551,7 +531,7 @@ namespace Wabbajack
client.DefaultRequestHeaders.Add("User-Agent", Consts.UserAgent);
}
if (headers != null) {
if (headers != null)
foreach (var header in headers)
{
var idx = header.IndexOf(':');
@ -559,10 +539,9 @@ namespace Wabbajack
var v = header.Substring(idx + 1);
client.DefaultRequestHeaders.Add(k, v);
}
}
long total_read = 0;
int buffer_size = 1024 * 32;
var buffer_size = 1024 * 32;
var response = client.GetSync(url);
var stream = response.Content.ReadAsStreamAsync();
@ -572,24 +551,26 @@ namespace Wabbajack
}
catch (Exception ex)
{
};
}
;
if (stream.IsFaulted)
{
Info($"While downloading {url} - {Utils.ExceptionToString(stream.Exception)}");
Info($"While downloading {url} - {stream.Exception.ExceptionToString()}");
return false;
}
if (!download)
return true;
string header_var = "1";
var header_var = "1";
if (response.Content.Headers.Contains("Content-Length"))
header_var = response.Content.Headers.GetValues("Content-Length").FirstOrDefault();
long content_size = header_var != null ? long.Parse(header_var) : 1;
var content_size = header_var != null ? long.Parse(header_var) : 1;
var output_path = Path.Combine(DownloadFolder, archive.Name);
;
;
using (var webs = stream.Result)
using (var fs = File.OpenWrite(output_path))
@ -599,13 +580,13 @@ namespace Wabbajack
{
var read = webs.Read(buffer, 0, buffer_size);
if (read == 0) break;
Status((int)(total_read * 100 / content_size), "Downloading {0}", archive.Name);
Status((int) (total_read * 100 / content_size), "Downloading {0}", archive.Name);
fs.Write(buffer, 0, read);
total_read += read;
}
}
Status("Hashing {0}", archive.Name);
HashArchive(output_path);
return true;
@ -620,12 +601,12 @@ namespace Wabbajack
private void HashArchives()
{
HashedArchives = Directory.EnumerateFiles(DownloadFolder)
.Where(e => !e.EndsWith(".sha"))
.PMap(e => (HashArchive(e), e))
.OrderByDescending(e => File.GetLastWriteTime(e.Item2))
.GroupBy(e => e.Item1)
.Select(e => e.First())
.ToDictionary(e => e.Item1, e => e.Item2);
.Where(e => !e.EndsWith(".sha"))
.PMap(e => (HashArchive(e), e))
.OrderByDescending(e => File.GetLastWriteTime(e.Item2))
.GroupBy(e => e.Item1)
.Select(e => e.First())
.ToDictionary(e => e.Item1, e => e.Item2);
}
private string HashArchive(string e)
@ -635,7 +616,7 @@ namespace Wabbajack
return File.ReadAllText(cache);
Status("Hashing {0}", Path.GetFileName(e));
File.WriteAllText(cache, Utils.FileSHA256(e));
File.WriteAllText(cache, e.FileSHA256());
return HashArchive(e);
}
@ -650,11 +631,7 @@ namespace Wabbajack
{
var bytes = br.ReadBytes(magic_bytes.Length);
var magic = Encoding.ASCII.GetString(bytes);
if (magic != Consts.ModPackMagic)
{
return null;
}
if (magic != Consts.ModPackMagic) return null;
s.Position = s.Length - magic_bytes.Length - 8;
var start_pos = br.ReadInt64();
@ -665,11 +642,10 @@ namespace Wabbajack
IFormatter formatter = new BinaryFormatter();
var list = formatter.Deserialize(dc);
Utils.Log("Modlist loaded.");
return (ModList)list;
return (ModList) list;
}
}
}
}
}
}
}

View File

@ -5,10 +5,8 @@ namespace Wabbajack
{
internal class LambdaCommand : ICommand
{
private Action _execute;
private Func<bool> _canExecute;
public event EventHandler CanExecuteChanged;
private readonly Func<bool> _canExecute;
private readonly Action _execute;
public LambdaCommand(Func<bool> canExecute, Action execute)
{
@ -16,6 +14,8 @@ namespace Wabbajack
_canExecute = canExecute;
}
public event EventHandler CanExecuteChanged;
public bool CanExecute(object parameter)
{
return _canExecute();

View File

@ -6,79 +6,81 @@
xmlns:local="clr-namespace:Wabbajack"
mc:Ignorable="d"
Title="Wabbajack" Height="800" Width="800"
Style="{StaticResource {x:Type Window}}" Icon="square_transparent_icon.ico" WindowStyle="ToolWindow" Closing="Window_Closing">
Style="{StaticResource {x:Type Window}}" Icon="square_transparent_icon.ico" WindowStyle="ToolWindow"
Closing="Window_Closing">
<Grid Margin="16">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition></RowDefinition>
<RowDefinition Height="10"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition />
<RowDefinition Height="10" />
<RowDefinition Height="Auto" />
<RowDefinition />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" Orientation="Horizontal" Margin="0, 16, 0, 16">
<TextBlock Text="{Binding Mode}" FontSize="16" FontWeight="Bold"></TextBlock>
<TextBlock Text=" : " FontSize="16"></TextBlock>
<TextBlock Text="{Binding ModListName}" FontSize="16"></TextBlock>
<TextBlock Text="{Binding Mode}" FontSize="16" FontWeight="Bold" />
<TextBlock Text=" : " FontSize="16" />
<TextBlock Text="{Binding ModListName}" FontSize="16" />
</StackPanel>
<Grid HorizontalAlignment="Stretch" Grid.Row="1" Margin="0, 10, 0, 10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"></ColumnDefinition>
<ColumnDefinition Width="*"></ColumnDefinition>
<ColumnDefinition Width="Auto"></ColumnDefinition>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition></RowDefinition>
<RowDefinition MinHeight="10"></RowDefinition>
<RowDefinition></RowDefinition>
<RowDefinition />
<RowDefinition MinHeight="10" />
<RowDefinition />
</Grid.RowDefinitions>
<Label Grid.Row="0" Content="MO2 Location:" Grid.Column="0"></Label>
<TextBox Grid.Row="0" Grid.Column="1" Text="{Binding Location}"></TextBox>
<Button Grid.Row="0" Content="Select" MinWidth="80" Grid.Column="2" Command="{Binding ChangePath}"></Button>
<Label Grid.Row="2" Content="Download Location:" Grid.Column="0"></Label>
<TextBox Grid.Row="2" Grid.Column="1" Text="{Binding DownloadLocation}"></TextBox>
<Button Grid.Row="2" Content="Select" MinWidth="80" Grid.Column="2" Command="{Binding ChangeDownloadPath}"></Button>
<Label Grid.Row="0" Content="MO2 Location:" Grid.Column="0" />
<TextBox Grid.Row="0" Grid.Column="1" Text="{Binding Location}" />
<Button Grid.Row="0" Content="Select" MinWidth="80" Grid.Column="2" Command="{Binding ChangePath}" />
<Label Grid.Row="2" Content="Download Location:" Grid.Column="0" />
<TextBox Grid.Row="2" Grid.Column="1" Text="{Binding DownloadLocation}" />
<Button Grid.Row="2" Content="Select" MinWidth="80" Grid.Column="2" Command="{Binding ChangeDownloadPath}" />
</Grid>
<ListBox Grid.Row ="2" ItemsSource="{Binding Status}" Width="Auto" HorizontalAlignment="Stretch">
<ListBox Grid.Row="2" ItemsSource="{Binding Status}" Width="Auto" HorizontalAlignment="Stretch">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid HorizontalAlignment="Stretch">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"></ColumnDefinition>
<ColumnDefinition Width="Auto"></ColumnDefinition>
<ColumnDefinition Width="Auto"></ColumnDefinition>
<ColumnDefinition Width="Auto"></ColumnDefinition>
<ColumnDefinition Width="Auto" ></ColumnDefinition>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<ProgressBar Minimum="0" Maximum="100" Value="{Binding Progress, Mode=OneTime}" Width="100" Grid.Column="0">
<ProgressBar Minimum="0" Maximum="100" Value="{Binding Progress, Mode=OneTime}" Width="100"
Grid.Column="0">
<ProgressBar.Style>
<Style TargetType="ProgressBar">
<Setter Property="Visibility" Value="Visible"></Setter>
<Setter Property="Visibility" Value="Visible" />
<Style.Triggers>
<DataTrigger Binding="{Binding Progress}" Value="0">
<Setter Property="Visibility" Value="Collapsed"></Setter>
<Setter Property="Visibility" Value="Collapsed" />
</DataTrigger>
</Style.Triggers>
</Style>
</ProgressBar.Style>
</ProgressBar>
<TextBlock Text=" CPU " Grid.Column="1"></TextBlock>
<TextBlock Text="{Binding ID}" Grid.Column="2"></TextBlock>
<TextBlock Text=" - " Grid.Column="3"></TextBlock>
<TextBlock Text="{Binding Msg}" Grid.Column="4"></TextBlock>
<TextBlock Text=" CPU " Grid.Column="1" />
<TextBlock Text="{Binding ID}" Grid.Column="2" />
<TextBlock Text=" - " Grid.Column="3" />
<TextBlock Text="{Binding Msg}" Grid.Column="4" />
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<ProgressBar Grid.Row="3" Value="{Binding QueueProgress}" Minimum="0" Maximum="100" Background="#444444"></ProgressBar>
<TextBlock Text="Log:" Grid.Row="4" FontSize="14" Margin="0, 16, 0, 8"></TextBlock>
<ListBox local:AutoScrollBehavior.ScrollOnNewItem="True" Grid.Row ="5" ItemsSource="{Binding Log}">
</ListBox>
<Button Content="View Modlist Contents" Grid.Row="6" Height="30" Visibility="{Binding ShowReportButton}" Command="{Binding ShowReportCommand}"></Button>
<Button Content="Begin" Grid.Row="7" Height="30" Command="{Binding Begin}"></Button>
<CheckBox Content="Ignore Missing Files" Grid.Row="8" Height="20" IsChecked="{Binding IgnoreMissingFiles}"></CheckBox>
<ProgressBar Grid.Row="3" Value="{Binding QueueProgress}" Minimum="0" Maximum="100" Background="#444444" />
<TextBlock Text="Log:" Grid.Row="4" FontSize="14" Margin="0, 16, 0, 8" />
<ListBox local:AutoScrollBehavior.ScrollOnNewItem="True" Grid.Row="5" ItemsSource="{Binding Log}" />
<Button Content="View Modlist Contents" Grid.Row="6" Height="30" Visibility="{Binding ShowReportButton}"
Command="{Binding ShowReportCommand}" />
<Button Content="Begin" Grid.Row="7" Height="30" Command="{Binding Begin}" />
<CheckBox Content="Ignore Missing Files" Grid.Row="8" Height="20" IsChecked="{Binding IgnoreMissingFiles}" />
</Grid>
</Window>
</Window>

View File

@ -1,34 +1,22 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using Wabbajack.Common;
namespace Wabbajack
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
private AppState _state;
public MainWindow()
{
var args = Environment.GetCommandLineArgs();
bool DebugMode = false;
var DebugMode = false;
string MO2Folder = null, InstallFolder = null, MO2Profile = null;
if (args.Length > 1)
@ -44,17 +32,15 @@ namespace Wabbajack
var context = new AppState(Dispatcher, "Building");
context.LogMsg($"Wabbajack Build - {ThisAssembly.Git.Sha}");
SetupHandlers(context);
this.DataContext = context;
DataContext = context;
WorkQueue.Init((id, msg, progress) => context.SetProgress(id, msg, progress),
(max, current) => context.SetQueueSize(max, current));
(max, current) => context.SetQueueSize(max, current));
Utils.SetLoggerFn(s => context.LogMsg(s));
Utils.SetStatusFn((msg, progress) => WorkQueue.Report(msg, progress));
if (DebugMode)
{
new Thread(() =>
{
var compiler = new Compiler(MO2Folder, msg => context.LogMsg(msg));
@ -69,39 +55,28 @@ namespace Wabbajack
compiler = null;
//context.ConfigureForInstall(modlist);
}).Start();
}
else
{
new Thread(() =>
{
var modlist = Installer.CheckForModPack();
if (modlist == null)
{
Utils.Log("No Modlist found, running in Compiler mode.");
}
else
{
context.ConfigureForInstall(modlist);
}
}).Start();
}
}
private AppState _state;
private void SetupHandlers(AppState state)
{
_state = state;
AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(AppHandler);
AppDomain.CurrentDomain.UnhandledException += AppHandler;
}
private void AppHandler(object sender, UnhandledExceptionEventArgs e)
{
_state.LogMsg("Uncaught error:");
_state.LogMsg(Utils.ExceptionToString((Exception)e.ExceptionObject));
_state.LogMsg(((Exception) e.ExceptionObject).ExceptionToString());
}
private void Window_Closing(object sender, CancelEventArgs e)
@ -109,4 +84,4 @@ namespace Wabbajack
Application.Current.Shutdown();
}
}
}
}

View File

@ -5,42 +5,36 @@ using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using System.Security.Authentication;
using System.Threading.Tasks;
using Wabbajack.Common;
using WebSocketSharp;
namespace Wabbajack
{
class NexusAPI
internal class NexusAPI
{
public static string GetNexusAPIKey()
{
FileInfo fi = new FileInfo("nexus.key_cache");
if (fi.Exists && fi.LastWriteTime > DateTime.Now.AddHours(-72))
{
return File.ReadAllText("nexus.key_cache");
}
var fi = new FileInfo("nexus.key_cache");
if (fi.Exists && fi.LastWriteTime > DateTime.Now.AddHours(-72)) return File.ReadAllText("nexus.key_cache");
var guid = Guid.NewGuid();
var _websocket = new WebSocket("wss://sso.nexusmods.com")
{
SslConfiguration = {
EnabledSslProtocols = System.Security.Authentication.SslProtocols.Tls12
SslConfiguration =
{
EnabledSslProtocols = SslProtocols.Tls12
}
};
TaskCompletionSource<string> api_key = new TaskCompletionSource<string>();
_websocket.OnMessage += (sender, msg) =>
{
api_key.SetResult(msg.Data);
return;
};
var api_key = new TaskCompletionSource<string>();
_websocket.OnMessage += (sender, msg) => { api_key.SetResult(msg.Data); };
_websocket.Connect();
_websocket.Send("{\"id\": \"" + guid + "\", \"appid\": \""+ Consts.AppName+"\"}");
_websocket.Send("{\"id\": \"" + guid + "\", \"appid\": \"" + Consts.AppName + "\"}");
Process.Start($"https://www.nexusmods.com/sso?id={guid}&application=" + Consts.AppName);
@ -49,31 +43,30 @@ namespace Wabbajack
File.WriteAllText("nexus.key_cache", result);
return result;
}
class DownloadLink
{
public string URI { get; set; }
}
private static HttpClient BaseNexusClient(string apikey)
{
var _baseHttpClient = new HttpClient();
_baseHttpClient.DefaultRequestHeaders.Add("User-Agent", Consts.UserAgent);
_baseHttpClient.DefaultRequestHeaders.Add("apikey", apikey);
_baseHttpClient.DefaultRequestHeaders.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));
_baseHttpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
_baseHttpClient.DefaultRequestHeaders.Add("Application-Name", Consts.AppName);
_baseHttpClient.DefaultRequestHeaders.Add("Application-Version", $"{Assembly.GetEntryAssembly().GetName().Version}");
_baseHttpClient.DefaultRequestHeaders.Add("Application-Version",
$"{Assembly.GetEntryAssembly().GetName().Version}");
return _baseHttpClient;
}
public static string GetNexusDownloadLink(NexusMod archive, string apikey, bool cache=false)
public static string GetNexusDownloadLink(NexusMod archive, string apikey, bool cache = false)
{
if (cache && TryGetCachedLink(archive, apikey, out string result)) return result;
if (cache && TryGetCachedLink(archive, apikey, out var result)) return result;
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
var client = BaseNexusClient(apikey);
string url;
string get_url_link = String.Format("https://api.nexusmods.com/v1/games/{0}/mods/{1}/files/{2}/download_link.json",
ConvertGameName(archive.GameName), archive.ModID, archive.FileID);
var get_url_link = string.Format(
"https://api.nexusmods.com/v1/games/{0}/mods/{1}/files/{2}/download_link.json",
ConvertGameName(archive.GameName), archive.ModID, archive.FileID);
using (var s = client.GetStreamSync(get_url_link))
{
url = s.FromJSON<List<DownloadLink>>().First().URI;
@ -83,18 +76,20 @@ namespace Wabbajack
private static bool TryGetCachedLink(NexusMod archive, string apikey, out string result)
{
if (!(Directory.Exists(Consts.NexusCacheDirectory)))
if (!Directory.Exists(Consts.NexusCacheDirectory))
Directory.CreateDirectory(Consts.NexusCacheDirectory);
string path = Path.Combine(Consts.NexusCacheDirectory, $"link-{archive.GameName}-{archive.ModID}-{archive.FileID}.txt");
var path = Path.Combine(Consts.NexusCacheDirectory,
$"link-{archive.GameName}-{archive.ModID}-{archive.FileID}.txt");
if (!File.Exists(path) || DateTime.Now - new FileInfo(path).LastWriteTime > new TimeSpan(24, 0, 0))
{
File.Delete(path);
result = GetNexusDownloadLink(archive, apikey, false);
result = GetNexusDownloadLink(archive, apikey);
File.WriteAllText(path, result);
return true;
}
result = File.ReadAllText(path);
return true;
}
@ -107,18 +102,6 @@ namespace Wabbajack
}
public class UserStatus
{
public string user_id;
public string key;
public string name;
public bool is_premium;
public bool is_supporter;
public string email;
public string profile_url;
}
public static UserStatus GetUserStatus(string apikey)
{
var url = "https://api.nexusmods.com/v1/users/validate.json";
@ -130,29 +113,11 @@ namespace Wabbajack
}
}
public class NexusFileInfo
{
public ulong file_id;
public string name;
public string version;
public ulong category_id;
public string category_name;
public bool is_primary;
public ulong size;
public string file_name;
public ulong uploaded_timestamp;
public DateTime uploaded_time;
public string mod_version;
public string external_virus_scan_url;
public string description;
public ulong size_kb;
public string changelog_html;
}
public static NexusFileInfo GetFileInfo(NexusMod mod, string apikey)
{
var url = $"https://api.nexusmods.com/v1/games/{ConvertGameName(mod.GameName)}/mods/{mod.ModID}/files/{mod.FileID}.json";
var url =
$"https://api.nexusmods.com/v1/games/{ConvertGameName(mod.GameName)}/mods/{mod.ModID}/files/{mod.FileID}.json";
var client = BaseNexusClient(apikey);
using (var s = client.GetStreamSync(url))
@ -161,29 +126,23 @@ namespace Wabbajack
}
}
public class ModInfo
{
public string author;
public string uploaded_by;
public string uploaded_users_profile_url;
}
public static ModInfo GetModInfo(NexusMod archive, string apikey)
{
if (!Directory.Exists(Consts.NexusCacheDirectory))
Directory.CreateDirectory(Consts.NexusCacheDirectory);
string path = Path.Combine(Consts.NexusCacheDirectory, $"mod-info-{archive.GameName}-{archive.ModID}.json");
var path = Path.Combine(Consts.NexusCacheDirectory, $"mod-info-{archive.GameName}-{archive.ModID}.json");
if (File.Exists(path))
return path.FromJSON<ModInfo>();
var url = $"https://api.nexusmods.com/v1/games/{ConvertGameName(archive.GameName)}/mods/{archive.ModID}.json";
var url =
$"https://api.nexusmods.com/v1/games/{ConvertGameName(archive.GameName)}/mods/{archive.ModID}.json";
var client = BaseNexusClient(apikey);
using (var s = client.GetStreamSync(url))
{
var result = s.FromJSON<ModInfo>();
var result = s.FromJSON<ModInfo>();
result.ToJSON(path);
return result;
}
@ -193,10 +152,11 @@ namespace Wabbajack
public static EndorsementResponse EndorseMod(NexusMod mod, string apikey)
{
Utils.Status($"Endorsing ${mod.GameName} - ${mod.ModID}");
var url = $"https://api.nexusmods.com/v1/games/{ConvertGameName(mod.GameName)}/mods/{mod.ModID}/endorse.json";
var url =
$"https://api.nexusmods.com/v1/games/{ConvertGameName(mod.GameName)}/mods/{mod.ModID}/endorse.json";
var client = BaseNexusClient(apikey);
var content = new FormUrlEncodedContent(new Dictionary<string, string>() {{"version", mod.Version}});
var content = new FormUrlEncodedContent(new Dictionary<string, string> {{"version", mod.Version}});
using (var s = client.PostStreamSync(url, content))
{
@ -204,6 +164,48 @@ namespace Wabbajack
}
}
private class DownloadLink
{
public string URI { get; set; }
}
public class UserStatus
{
public string email;
public bool is_premium;
public bool is_supporter;
public string key;
public string name;
public string profile_url;
public string user_id;
}
public class NexusFileInfo
{
public ulong category_id;
public string category_name;
public string changelog_html;
public string description;
public string external_virus_scan_url;
public ulong file_id;
public string file_name;
public bool is_primary;
public string mod_version;
public string name;
public ulong size;
public ulong size_kb;
public DateTime uploaded_time;
public ulong uploaded_timestamp;
public string version;
}
public class ModInfo
{
public string author;
public string uploaded_by;
public string uploaded_users_profile_url;
}
}
public class EndorsementResponse
@ -211,4 +213,4 @@ namespace Wabbajack
public string message;
public string status;
}
}
}

View File

@ -1,6 +1,4 @@
using System.Reflection;
using System.Resources;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Windows;
@ -33,11 +31,11 @@ using System.Windows;
[assembly: ThemeInfo(
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
//(used if a resource is not found in the page,
// or application resource dictionaries)
//(used if a resource is not found in the page,
// or application resource dictionaries)
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
//(used if a resource is not found in the page,
// app, or any theme specific resource dictionaries)
//(used if a resource is not found in the page,
// app, or any theme specific resource dictionaries)
)]
@ -52,4 +50,4 @@ using System.Windows;
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

View File

@ -1,4 +1,5 @@
<?xml version='1.0' encoding='utf-8'?>
<SettingsFile xmlns="uri:settings" CurrentProfile="(Default)">
<Profiles>
<Profile Name="(Default)" />

View File

@ -1,36 +1,37 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
using Wabbajack.Common;
namespace Wabbajack
{
public class ReportBuilder : IDisposable
{
private StreamWriter wtr;
private const int WRAP_SIZE = 80;
private readonly StreamWriter wtr;
public ReportBuilder(Stream str)
{
wtr = new StreamWriter(str);
}
private const int WRAP_SIZE = 80;
public void Dispose()
{
wtr.Flush();
wtr?.Dispose();
}
public void Text(string txt)
{
int offset = 0;
var offset = 0;
while (offset + WRAP_SIZE < txt.Length)
{
wtr.WriteLine(txt.Substring(offset, WRAP_SIZE));
offset += WRAP_SIZE;
}
if (offset < txt.Length)
{
wtr.WriteLine(txt.Substring(offset, txt.Length - offset));
}
if (offset < txt.Length) wtr.WriteLine(txt.Substring(offset, txt.Length - offset));
}
public void NoWrapText(string txt)
@ -41,15 +42,18 @@ namespace Wabbajack
public void Build(ModList lst)
{
Text($"### {lst.Name} - Installation Summary");
Text($"#### Download Summary ({lst.Archives.Count} archives - {lst.Archives.Sum(a => a.Size).ToFileSizeString()})");
Text(
$"#### Download Summary ({lst.Archives.Count} archives - {lst.Archives.Sum(a => a.Size).ToFileSizeString()})");
foreach (var archive in SortArchives(lst.Archives))
{
var hash = archive.Hash.FromBase64().ToHEX();
switch (archive)
{
case NexusMod m:
var profile = m.UploaderProfile.Replace("/games/", "/"+NexusAPI.ConvertGameName(m.GameName).ToLower()+"/");
NoWrapText($"* [{m.Name}](http://nexusmods.com/{NexusAPI.ConvertGameName(m.GameName)}/mods/{m.ModID})");
var profile = m.UploaderProfile.Replace("/games/",
"/" + NexusAPI.ConvertGameName(m.GameName).ToLower() + "/");
NoWrapText(
$"* [{m.Name}](http://nexusmods.com/{NexusAPI.ConvertGameName(m.GameName)}/mods/{m.ModID})");
NoWrapText($" * Author : [{m.UploadedBy}]({profile})");
NoWrapText($" * Version : {m.Version}");
break;
@ -60,30 +64,30 @@ namespace Wabbajack
NoWrapText($"* MEGA - [{m.Name}]({m.URL})");
break;
case GoogleDriveMod m:
NoWrapText($"* GoogleDrive - [{m.Name}](https://drive.google.com/uc?id={m.Id}&export=download)");
NoWrapText(
$"* GoogleDrive - [{m.Name}](https://drive.google.com/uc?id={m.Id}&export=download)");
break;
case DirectURLArchive m:
NoWrapText($"* URL - [{m.Name} - {m.URL}]({m.URL})");
break;
}
NoWrapText($" * Size : {archive.Size.ToFileSizeString()}");
NoWrapText($" * SHA256 : [{hash}](https://www.virustotal.com/gui/file/{hash})");
}
Text($"\n\n");
Text("\n\n");
var patched = lst.Directives.OfType<PatchedFromArchive>().OrderBy(p => p.To).ToList();
Text($"#### Summary of ({patched.Count}) patches");
foreach (var directive in patched)
{
NoWrapText($"* Applying {directive.Patch.Length} byte patch `{directive.FullPath}` to create `{directive.To}`");
}
NoWrapText(
$"* Applying {directive.Patch.Length} byte patch `{directive.FullPath}` to create `{directive.To}`");
var files = lst.Directives.OrderBy(d => d.To).ToList();
Text($"\n\n### Install Plan of ({files.Count}) files");
Text($"(ignoring files that are directly copied from archives or listed in the patches section above)");
Text("(ignoring files that are directly copied from archives or listed in the patches section above)");
foreach (var directive in files.OrderBy(f => f.GetType().Name).ThenByDescending(f => f.To))
{
switch (directive)
{
case FromArchive f:
@ -99,10 +103,10 @@ namespace Wabbajack
NoWrapText($"* `{i.To}` from `{i.SourceData.Length}` byte file included in modlist");
break;
case CreateBSA i:
NoWrapText($"* `{i.To}` by creating a BSA of files found in `{Consts.BSACreationDir}\\{i.TempID}`");
NoWrapText(
$"* `{i.To}` by creating a BSA of files found in `{Consts.BSACreationDir}\\{i.TempID}`");
break;
}
}
}
private IEnumerable<Archive> SortArchives(List<Archive> lstArchives)
@ -110,11 +114,5 @@ namespace Wabbajack
var lst = lstArchives.OfType<NexusMod>().OrderBy(m => m.Author).ThenBy(m => m.Name);
return lst.Concat(lstArchives.Where(m => !(m is NexusMod)).OrderBy(m => m.Name));
}
public void Dispose()
{
wtr.Flush();
wtr?.Dispose();
}
}
}
}

View File

@ -1,9 +1,5 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
@ -25,7 +21,7 @@ namespace DarkBlendTheme
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new System.NotImplementedException();
throw new NotImplementedException();
}
}
}
}

View File

@ -1,12 +1,4 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Controls;
using System.Windows.Media;
namespace DarkBlendTheme
@ -16,23 +8,21 @@ namespace DarkBlendTheme
public static int GetDepth(this TreeViewItem item)
{
TreeViewItem parent;
while ((parent = GetParent(item)) != null)
{
return GetDepth(parent) + 1;
}
while ((parent = GetParent(item)) != null) return GetDepth(parent) + 1;
return 0;
}
private static TreeViewItem GetParent(TreeViewItem item)
{
var parent = VisualTreeHelper.GetParent(item);
while (!(parent is TreeViewItem || parent is TreeView))
{
if (parent == null) return null;
if (parent == null) return null;
parent = VisualTreeHelper.GetParent(parent);
}
return parent as TreeViewItem;
}
}
}
}

View File

@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="CommonMark.NET" version="0.15.1" targetFramework="net472" />
<package id="Costura.Fody" version="4.0.0" targetFramework="net472" />