mirror of
https://github.com/wabbajack-tools/wabbajack.git
synced 2024-08-30 18:42:17 +00:00
code reformatting
This commit is contained in:
parent
1a7dec86c1
commit
454cff052e
@ -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>
|
@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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")]
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
@ -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")]
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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" />
|
||||
|
@ -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>
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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")]
|
@ -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")]
|
@ -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;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
@ -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" />
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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")]
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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" />
|
||||
|
@ -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>
|
||||
|
@ -10,4 +10,4 @@
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
</Application>
|
@ -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
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
@ -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];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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));
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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&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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
|
@ -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>
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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")]
|
@ -1,4 +1,5 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
|
||||
<SettingsFile xmlns="uri:settings" CurrentProfile="(Default)">
|
||||
<Profiles>
|
||||
<Profile Name="(Default)" />
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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" />
|
||||
|
Loading…
Reference in New Issue
Block a user