mirror of
https://github.com/wabbajack-tools/wabbajack.git
synced 2024-08-30 18:42:17 +00:00
Merge pull request #556 from wabbajack-tools/morrowind-bsas
Add support for extracting/building TES3 .bsa files
This commit is contained in:
commit
6f80a40dc9
@ -5,6 +5,7 @@
|
|||||||
* Block popups in the in-app browser (#535)
|
* Block popups in the in-app browser (#535)
|
||||||
* Don't print API keys in logs (#533)
|
* Don't print API keys in logs (#533)
|
||||||
* Store xxHash caches in binary format (#530)
|
* Store xxHash caches in binary format (#530)
|
||||||
|
* Added support for Morrowind BSA creation/unpacking
|
||||||
|
|
||||||
|
|
||||||
#### Version - 0.9.19.0
|
#### Version - 0.9.19.0
|
||||||
|
@ -45,7 +45,8 @@ namespace Compression.BSA.Test
|
|||||||
(Game.Skyrim, 3863), // SkyUI
|
(Game.Skyrim, 3863), // SkyUI
|
||||||
(Game.Skyrim, 51473), // iNeed
|
(Game.Skyrim, 51473), // iNeed
|
||||||
//(Game.Fallout4, 22223) // 10mm SMG
|
//(Game.Fallout4, 22223) // 10mm SMG
|
||||||
(Game.Fallout4, 4472) // True Storms
|
(Game.Fallout4, 4472), // True Storms
|
||||||
|
(Game.Morrowind, 44537) // Morrowind TAMRIEL_DATA
|
||||||
};
|
};
|
||||||
|
|
||||||
await Task.WhenAll(modIDs.Select(async (info) =>
|
await Task.WhenAll(modIDs.Select(async (info) =>
|
||||||
|
@ -13,6 +13,8 @@ namespace Compression.BSA
|
|||||||
fourcc = Encoding.ASCII.GetString(new BinaryReader(file).ReadBytes(4));
|
fourcc = Encoding.ASCII.GetString(new BinaryReader(file).ReadBytes(4));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (fourcc == TES3Reader.TES3_MAGIC)
|
||||||
|
return new TES3Reader(filename);
|
||||||
if (fourcc == "BSA\0")
|
if (fourcc == "BSA\0")
|
||||||
return new BSAReader(filename);
|
return new BSAReader(filename);
|
||||||
if (fourcc == "BTDX")
|
if (fourcc == "BTDX")
|
||||||
|
@ -13,7 +13,8 @@ namespace Compression.BSA
|
|||||||
TES4 = 0x67,
|
TES4 = 0x67,
|
||||||
FO3 = 0x68, // FO3, FNV, TES5
|
FO3 = 0x68, // FO3, FNV, TES5
|
||||||
SSE = 0x69,
|
SSE = 0x69,
|
||||||
FO4 = 0x01
|
FO4 = 0x01,
|
||||||
|
TES3 = 0xFF // Not a real Bethesda version number
|
||||||
}
|
}
|
||||||
|
|
||||||
[Flags]
|
[Flags]
|
||||||
|
77
Compression.BSA/TES3Builder.cs
Normal file
77
Compression.BSA/TES3Builder.cs
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using File = Alphaleonis.Win32.Filesystem.File;
|
||||||
|
|
||||||
|
namespace Compression.BSA
|
||||||
|
{
|
||||||
|
public class TES3Builder : IBSABuilder
|
||||||
|
{
|
||||||
|
private TES3ArchiveState _state;
|
||||||
|
private (TES3FileState state, byte[] data)[] _files;
|
||||||
|
|
||||||
|
public TES3Builder(TES3ArchiveState state)
|
||||||
|
{
|
||||||
|
_state = state;
|
||||||
|
_files = new (TES3FileState state, byte[] data)[_state.FileCount];
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddFile(FileStateObject state, Stream src)
|
||||||
|
{
|
||||||
|
using var br = new BinaryReader(src);
|
||||||
|
var cstate = (TES3FileState)state;
|
||||||
|
_files[state.Index] = (cstate, br.ReadBytes((int)cstate.Size));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Build(string filename)
|
||||||
|
{
|
||||||
|
using var fs = File.Create(filename);
|
||||||
|
using var bw = new BinaryWriter(fs);
|
||||||
|
|
||||||
|
bw.Write(_state.VersionNumber);
|
||||||
|
bw.Write(_state.HashOffset);
|
||||||
|
bw.Write(_state.FileCount);
|
||||||
|
|
||||||
|
foreach (var (state, _) in _files)
|
||||||
|
{
|
||||||
|
bw.Write(state.Size);
|
||||||
|
bw.Write(state.Offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var (state, _) in _files)
|
||||||
|
{
|
||||||
|
bw.Write(state.NameOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
var orgPos = bw.BaseStream.Position;
|
||||||
|
|
||||||
|
foreach (var (state, _) in _files)
|
||||||
|
{
|
||||||
|
if (bw.BaseStream.Position != orgPos + state.NameOffset)
|
||||||
|
throw new InvalidDataException("Offsets don't match when writing TES3 BSA");
|
||||||
|
bw.Write(Encoding.ASCII.GetBytes(state.Path));
|
||||||
|
bw.Write((byte)0);
|
||||||
|
}
|
||||||
|
|
||||||
|
bw.BaseStream.Position = _state.HashOffset + 12;
|
||||||
|
foreach (var (state, _) in _files)
|
||||||
|
{
|
||||||
|
bw.Write(state.Hash1);
|
||||||
|
bw.Write(state.Hash2);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bw.BaseStream.Position != _state.DataOffset)
|
||||||
|
throw new InvalidDataException("Data offset doesn't match when writing TES3 BSA");
|
||||||
|
|
||||||
|
foreach (var (state, data) in _files)
|
||||||
|
{
|
||||||
|
bw.BaseStream.Position = _state.DataOffset + state.Offset;
|
||||||
|
bw.Write(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
139
Compression.BSA/TES3Reader.cs
Normal file
139
Compression.BSA/TES3Reader.cs
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Compression.BSA
|
||||||
|
{
|
||||||
|
public class TES3Reader : IBSAReader
|
||||||
|
{
|
||||||
|
public static string TES3_MAGIC = Encoding.ASCII.GetString(new byte[] {0, 1, 0, 0});
|
||||||
|
private uint _versionNumber;
|
||||||
|
private uint _hashTableOffset;
|
||||||
|
private uint _fileCount;
|
||||||
|
private TES3FileEntry[] _files;
|
||||||
|
internal long _dataOffset;
|
||||||
|
internal string _filename;
|
||||||
|
|
||||||
|
public TES3Reader(string filename)
|
||||||
|
{
|
||||||
|
_filename = filename;
|
||||||
|
using var fs = File.OpenRead(filename);
|
||||||
|
using var br = new BinaryReader(fs);
|
||||||
|
_versionNumber = br.ReadUInt32();
|
||||||
|
_hashTableOffset = br.ReadUInt32();
|
||||||
|
_fileCount = br.ReadUInt32();
|
||||||
|
|
||||||
|
_files = new TES3FileEntry[_fileCount];
|
||||||
|
for (int i = 0; i < _fileCount; i++)
|
||||||
|
{
|
||||||
|
var file = new TES3FileEntry {
|
||||||
|
Index = i,
|
||||||
|
Archive = this,
|
||||||
|
Size = br.ReadUInt32(),
|
||||||
|
Offset = br.ReadUInt32()
|
||||||
|
|
||||||
|
};
|
||||||
|
_files[i] = file;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < _fileCount; i++)
|
||||||
|
{
|
||||||
|
_files[i].NameOffset = br.ReadUInt32();
|
||||||
|
}
|
||||||
|
|
||||||
|
var origPos = br.BaseStream.Position;
|
||||||
|
for (int i = 0; i < _fileCount; i++)
|
||||||
|
{
|
||||||
|
br.BaseStream.Position = origPos + _files[i].NameOffset;
|
||||||
|
_files[i].Path = br.ReadStringTerm(VersionType.TES3);
|
||||||
|
}
|
||||||
|
|
||||||
|
br.BaseStream.Position = _hashTableOffset + 12;
|
||||||
|
for (int i = 0; i < _fileCount; i++)
|
||||||
|
{
|
||||||
|
_files[i].Hash1 = br.ReadUInt32();
|
||||||
|
_files[i].Hash2 = br.ReadUInt32();
|
||||||
|
}
|
||||||
|
|
||||||
|
_dataOffset = br.BaseStream.Position;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<IFile> Files => _files;
|
||||||
|
public ArchiveStateObject State
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return new TES3ArchiveState
|
||||||
|
{
|
||||||
|
FileCount = _fileCount,
|
||||||
|
DataOffset = _dataOffset,
|
||||||
|
HashOffset = _hashTableOffset,
|
||||||
|
VersionNumber = _versionNumber,
|
||||||
|
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TES3ArchiveState : ArchiveStateObject
|
||||||
|
{
|
||||||
|
public uint FileCount { get; set; }
|
||||||
|
public long DataOffset { get; set; }
|
||||||
|
public uint HashOffset { get; set; }
|
||||||
|
public uint VersionNumber { get; set; }
|
||||||
|
|
||||||
|
public override IBSABuilder MakeBuilder()
|
||||||
|
{
|
||||||
|
return new TES3Builder(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TES3FileEntry : IFile
|
||||||
|
{
|
||||||
|
public string Path { get; set; }
|
||||||
|
public uint Size { get; set; }
|
||||||
|
public FileStateObject State =>
|
||||||
|
new TES3FileState
|
||||||
|
{
|
||||||
|
Index = Index,
|
||||||
|
Path = Path,
|
||||||
|
Size = Size,
|
||||||
|
Offset = Offset,
|
||||||
|
NameOffset = NameOffset,
|
||||||
|
Hash1 = Hash1,
|
||||||
|
Hash2 = Hash2
|
||||||
|
};
|
||||||
|
|
||||||
|
public void CopyDataTo(Stream output)
|
||||||
|
{
|
||||||
|
using var fs = File.OpenRead(Archive._filename);
|
||||||
|
fs.Position = Archive._dataOffset + Offset;
|
||||||
|
fs.CopyToLimit(output, (int)Size);
|
||||||
|
}
|
||||||
|
|
||||||
|
public uint Offset { get; set; }
|
||||||
|
public uint NameOffset { get; set; }
|
||||||
|
public uint Hash1 { get; set; }
|
||||||
|
public uint Hash2 { get; set; }
|
||||||
|
public TES3Reader Archive { get; set; }
|
||||||
|
public int Index { get; set; }
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public class TES3FileState : FileStateObject
|
||||||
|
{
|
||||||
|
public uint Offset { get; set; }
|
||||||
|
public uint NameOffset { get; set; }
|
||||||
|
public uint Hash1 { get; set; }
|
||||||
|
public uint Hash2 { get; set; }
|
||||||
|
public uint Size { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -18,6 +18,8 @@ namespace Compression.BSA
|
|||||||
|
|
||||||
private static Encoding GetEncoding(VersionType version)
|
private static Encoding GetEncoding(VersionType version)
|
||||||
{
|
{
|
||||||
|
if (version == VersionType.TES3)
|
||||||
|
return Encoding.ASCII;
|
||||||
if (version == VersionType.SSE)
|
if (version == VersionType.SSE)
|
||||||
return Windows1252;
|
return Windows1252;
|
||||||
return Encoding.UTF7;
|
return Encoding.UTF7;
|
||||||
|
@ -30,7 +30,7 @@ namespace Wabbajack.Lib
|
|||||||
typeof(PropertyFile), typeof(SteamMeta), typeof(SteamWorkshopDownloader), typeof(SteamWorkshopDownloader.State),
|
typeof(PropertyFile), typeof(SteamMeta), typeof(SteamWorkshopDownloader), typeof(SteamWorkshopDownloader.State),
|
||||||
typeof(LoversLabDownloader.State), typeof(GameFileSourceDownloader.State), typeof(VectorPlexusDownloader.State),
|
typeof(LoversLabDownloader.State), typeof(GameFileSourceDownloader.State), typeof(VectorPlexusDownloader.State),
|
||||||
typeof(DeadlyStreamDownloader.State), typeof(AFKModsDownloader.State), typeof(TESAllianceDownloader.State),
|
typeof(DeadlyStreamDownloader.State), typeof(AFKModsDownloader.State), typeof(TESAllianceDownloader.State),
|
||||||
typeof(BethesdaNetDownloader.State)
|
typeof(TES3ArchiveState), typeof(TES3FileState), typeof(BethesdaNetDownloader.State)
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
Config.VersionTolerance.Mode = VersionToleranceMode.Standard;
|
Config.VersionTolerance.Mode = VersionToleranceMode.Standard;
|
||||||
|
Loading…
Reference in New Issue
Block a user