Add Starfield support (#2589)

* Add support for new Starfield BA2 versions

* Add Starfield BA2 LZ4 texture compression support (WIP, not finished)

* Work on replacing DDS.cs with DirectXTexUtility

* Fix reading Starfield BA2s with new DirectXTexUtil

* Fix builder not exporting new Starfield header options

* Fix writing LZ4 chunks in frame format instead of block format

* Rename FO4Archive to BA2Archive, merge SFArchive and FO4Archive

* Clean up testing code & CLI launch settings

* Update changelog
This commit is contained in:
trawzified 2024-06-17 20:10:53 +02:00 committed by GitHub
parent 59b2f1a7a1
commit a545cb375a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 1479 additions and 554 deletions

View File

@ -1,6 +1,8 @@
### Changelog ### Changelog
#### Version - 3.6.2.0 - TBD #### Version - 3.7.0.0 - TBD
* Added Starfield support
* Note: Hashes were added earlier, but the earlier version was not fully compatible due to Wabbajack extracting the BA2 archives incorrectly. This has been fixed.
* Updated GameFinder dependency * Updated GameFinder dependency
* Updated WebView dependency * Updated WebView dependency
* Updated other dependencies * Updated other dependencies

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
https://go.microsoft.com/fwlink/?LinkID=208121.
-->
<Project>
<PropertyGroup>
<Configuration>Release</Configuration>
<Platform>Any CPU</Platform>
<PublishDir>bin\Release\Publish</PublishDir>
<PublishProtocol>FileSystem</PublishProtocol>
<_TargetId>Folder</_TargetId>
<TargetFramework>net8.0-windows</TargetFramework>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<SelfContained>true</SelfContained>
<PublishSingleFile>false</PublishSingleFile>
<PublishReadyToRun>false</PublishReadyToRun>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,7 @@
{
"profiles": {
"Wabbajack.CLI": {
"commandName": "Project"
}
}
}

View File

@ -55,6 +55,7 @@ public class CompressionTests
{ {
if (name == "tes4.bsa") return; // not sure why is is failing if (name == "tes4.bsa") return; // not sure why is is failing
var reader = await BSADispatch.Open(path); var reader = await BSADispatch.Open(path);
var dataStates = await reader.Files var dataStates = await reader.Files
@ -78,7 +79,6 @@ public class CompressionTests
var rebuiltStream = new MemoryStream(); var rebuiltStream = new MemoryStream();
await build.Build(rebuiltStream, CancellationToken.None); await build.Build(rebuiltStream, CancellationToken.None);
rebuiltStream.Position = 0;
var reader2 = await BSADispatch.Open(new MemoryStreamFactory(rebuiltStream, path, path.LastModifiedUtc())); var reader2 = await BSADispatch.Open(new MemoryStreamFactory(rebuiltStream, path, path.LastModifiedUtc()));
await reader.Files.Zip(reader2.Files) await reader.Files.Zip(reader2.Files)

View File

@ -10,7 +10,7 @@ using Wabbajack.DTOs.BSA.ArchiveStates;
using Wabbajack.DTOs.BSA.FileStates; using Wabbajack.DTOs.BSA.FileStates;
using Wabbajack.Paths.IO; using Wabbajack.Paths.IO;
namespace Wabbajack.Compression.BSA.FO4Archive; namespace Wabbajack.Compression.BSA.BA2Archive;
public class Builder : IBuilder public class Builder : IBuilder
{ {
@ -33,7 +33,7 @@ public class Builder : IBuilder
break; break;
case BA2EntryType.DX10: case BA2EntryType.DX10:
var resultdx10 = await DX10FileEntryBuilder.Create((BA2DX10File)state, src, _slab, token); var resultdx10 = await DX10FileEntryBuilder.Create((BA2DX10File)state, src, _slab, _state.Compression == 3, token);
lock (_entries) lock (_entries)
{ {
_entries.Add(resultdx10); _entries.Add(resultdx10);
@ -59,6 +59,13 @@ public class Builder : IBuilder
bw.Write((uint) _entries.Count); bw.Write((uint) _entries.Count);
var tableOffsetLoc = bw.BaseStream.Position; var tableOffsetLoc = bw.BaseStream.Position;
bw.Write((ulong) 0); bw.Write((ulong) 0);
if(_state.Version == 2 || _state.Version == 3)
{
bw.Write(_state.Unknown1);
bw.Write(_state.Unknown2);
if (_state.Version == 3)
bw.Write(_state.Compression);
}
foreach (var entry in _entries) entry.WriteHeader(bw, token); foreach (var entry in _entries) entry.WriteHeader(bw, token);

View File

@ -0,0 +1,93 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using ICSharpCode.SharpZipLib.Zip.Compression;
using ICSharpCode.SharpZipLib.Zip.Compression.Streams;
using K4os.Compression.LZ4;
using K4os.Compression.LZ4.Encoders;
using Wabbajack.Common;
using Wabbajack.DTOs.BSA.FileStates;
using Wabbajack.DTOs.GitHub;
namespace Wabbajack.Compression.BSA.BA2Archive;
public class ChunkBuilder
{
private BA2Chunk _chunk;
private Stream _dataSlab;
private long _offsetOffset;
private uint _packSize;
public static async Task<ChunkBuilder> Create(BA2DX10File state, BA2Chunk chunk, Stream src,
DiskSlabAllocator slab, bool useLz4Compression, CancellationToken token)
{
var builder = new ChunkBuilder {_chunk = chunk};
if (!chunk.Compressed)
{
builder._dataSlab = slab.Allocate(chunk.FullSz);
await src.CopyToLimitAsync(builder._dataSlab, (int) chunk.FullSz, token);
}
else
{
if (!useLz4Compression)
{
var deflater = new Deflater(Deflater.BEST_COMPRESSION);
await using var ms = new MemoryStream();
await using (var ds = new DeflaterOutputStream(ms, deflater))
{
ds.IsStreamOwner = false;
await src.CopyToLimitAsync(ds, (int)chunk.FullSz, token);
}
builder._dataSlab = slab.Allocate(ms.Length);
ms.Position = 0;
await ms.CopyToLimitAsync(builder._dataSlab, (int)ms.Length, token);
builder._packSize = (uint)ms.Length;
}
else
{
byte[] full = new byte[chunk.FullSz];
await using (var copyStream = new MemoryStream())
{
await src.CopyToLimitAsync(copyStream, (int)chunk.FullSz, token);
full = copyStream.ToArray();
}
var maxOutput = LZ4Codec.MaximumOutputSize((int)chunk.FullSz);
byte[] compressed = new byte[maxOutput];
int compressedSize = LZ4Codec.Encode(full, 0, full.Length, compressed, 0, compressed.Length, LZ4Level.L12_MAX);
var ms = new MemoryStream(compressed, 0, compressedSize);
builder._dataSlab = slab.Allocate(compressedSize);
ms.Position = 0;
await ms.CopyToLimitAsync(builder._dataSlab, compressedSize, token);
builder._packSize = (uint)compressedSize;
}
}
builder._dataSlab.Position = 0;
return builder;
}
public void WriteHeader(BinaryWriter bw)
{
_offsetOffset = bw.BaseStream.Position;
bw.Write((ulong) 0);
bw.Write(_packSize);
bw.Write(_chunk.FullSz);
bw.Write(_chunk.StartMip);
bw.Write(_chunk.EndMip);
bw.Write(_chunk.Align);
}
public async ValueTask WriteData(BinaryWriter bw, CancellationToken token)
{
var pos = bw.BaseStream.Position;
bw.BaseStream.Position = _offsetOffset;
bw.Write((ulong) pos);
bw.BaseStream.Position = pos;
await _dataSlab.CopyToLimitAsync(bw.BaseStream, (int) _dataSlab.Length, token);
await _dataSlab.DisposeAsync();
}
}

View File

@ -0,0 +1,179 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using DirectXTex;
using ICSharpCode.SharpZipLib.Zip.Compression;
using K4os.Compression.LZ4;
using K4os.Compression.LZ4.Streams;
using Wabbajack.Common;
using Wabbajack.Compression.BSA.BA2Archive;
using Wabbajack.DTOs.BSA.FileStates;
using Wabbajack.DTOs.Streams;
using Wabbajack.Paths;
namespace Wabbajack.Compression.BSA.BA2Archive;
public class DX10Entry : IBA2FileEntry
{
private readonly Reader _bsa;
private ushort _chunkHdrLen;
private List<TextureChunk> _chunks;
private uint _dirHash;
private string _extension;
private byte _format;
private ushort _height;
private int _index;
private uint _nameHash;
private byte _numChunks;
private byte _numMips;
private ushort _unk16;
private byte _unk8;
private ushort _width;
private readonly byte _isCubemap;
private readonly byte _tileMode;
public DX10Entry(Reader ba2Reader, int idx)
{
_bsa = ba2Reader;
var _rdr = ba2Reader._rdr;
_nameHash = _rdr.ReadUInt32();
FullPath = _nameHash.ToString("X");
_extension = Encoding.UTF8.GetString(_rdr.ReadBytes(4));
_dirHash = _rdr.ReadUInt32();
_unk8 = _rdr.ReadByte();
_numChunks = _rdr.ReadByte();
_chunkHdrLen = _rdr.ReadUInt16();
_height = _rdr.ReadUInt16();
_width = _rdr.ReadUInt16();
_numMips = _rdr.ReadByte();
_format = _rdr.ReadByte();
_isCubemap = _rdr.ReadByte();
_tileMode = _rdr.ReadByte();
_index = idx;
_chunks = Enumerable.Range(0, _numChunks)
.Select(_ => new TextureChunk(_rdr))
.ToList();
}
private DirectXTexUtility.TexMetadata? _metadata = null;
public DirectXTexUtility.TexMetadata Metadata
{
get
{
if (_metadata == null)
_metadata = DirectXTexUtility.GenerateMetadata(_width, _height, _numMips, (DirectXTexUtility.DXGIFormat)_format, _isCubemap == 1);
return (DirectXTexUtility.TexMetadata)_metadata;
}
}
private uint _headerSize = 0;
public uint HeaderSize
{
get
{
if (_headerSize > 0)
return _headerSize;
uint size = 0;
size += (uint)Marshal.SizeOf(DirectXTexUtility.DDSHeader.DDSMagic);
size += (uint)Marshal.SizeOf<DirectXTexUtility.DDSHeader>();
var pixelFormat = DirectXTexUtility.GetPixelFormat(Metadata);
var hasDx10Header = DirectXTexUtility.HasDx10Header(pixelFormat);
if (hasDx10Header)
size += (uint)Marshal.SizeOf<DirectXTexUtility.DX10Header>();
return _headerSize = size;
}
}
public string FullPath { get; set; }
public RelativePath Path => FullPath.ToRelativePath();
public uint Size => (uint)_chunks.Sum(f => f._fullSz) + HeaderSize;
public AFile State => new BA2DX10File
{
Path = Path,
NameHash = _nameHash,
Extension = _extension,
DirHash = _dirHash,
Unk8 = _unk8,
ChunkHdrLen = _chunkHdrLen,
Height = _height,
Width = _width,
NumMips = _numMips,
PixelFormat = _format,
IsCubeMap = _isCubemap,
TileMode = _tileMode,
Index = _index,
Chunks = _chunks.Select(ch => new BA2Chunk
{
FullSz = ch._fullSz,
StartMip = ch._startMip,
EndMip = ch._endMip,
Align = ch._align,
Compressed = ch._packSz != 0
}).ToArray()
};
public async ValueTask CopyDataTo(Stream output, CancellationToken token)
{
var bw = new BinaryWriter(output);
WriteHeader(bw);
await using var fs = await _bsa._streamFactory.GetStream();
using var br = new BinaryReader(fs);
foreach (var chunk in _chunks)
{
var full = new byte[chunk._fullSz];
var isCompressed = chunk._packSz != 0;
br.BaseStream.Seek((long)chunk._offset, SeekOrigin.Begin);
if (!isCompressed)
{
await br.BaseStream.ReadAsync(full, token);
}
else
{
var compressed = new byte[chunk._packSz];
await br.BaseStream.ReadAsync(compressed, token);
if (_bsa._compression == 3)
{
LZ4Codec.PartialDecode(compressed, full);
}
else
{
var inflater = new Inflater();
inflater.SetInput(compressed);
inflater.Inflate(full);
}
}
await bw.BaseStream.WriteAsync(full, token);
}
}
public async ValueTask<IStreamFactory> GetStreamFactory(CancellationToken token)
{
var ms = new MemoryStream();
await CopyDataTo(ms, token);
ms.Position = 0;
return new MemoryStreamFactory(ms, Path, _bsa._streamFactory.LastModifiedUtc);
}
private void WriteHeader(BinaryWriter bw)
{
DirectXTexUtility.GenerateDDSHeader(Metadata, DirectXTexUtility.DDSFlags.FORCEDX10EXTMISC2, out var header, out var header10);
var headerBytes = DirectXTexUtility.EncodeDDSHeader(header, header10);
bw.Write(headerBytes);
}
}

View File

@ -1,18 +1,20 @@
using DirectXTex;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Runtime.InteropServices;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Compression.BSA;
using Wabbajack.DTOs.BSA.FileStates; using Wabbajack.DTOs.BSA.FileStates;
using Wabbajack.DTOs.Texture; using Wabbajack.DTOs.Texture;
namespace Wabbajack.Compression.BSA.FO4Archive; namespace Wabbajack.Compression.BSA.BA2Archive;
public class DX10FileEntryBuilder : IFileBuilder public class DX10FileEntryBuilder : IFileBuilder
{ {
private List<ChunkBuilder> _chunks; private List<ChunkBuilder> _chunks;
private BA2DX10File _state; private BA2DX10File _state;
private uint _headerSize = 0;
public uint FileHash => _state.NameHash; public uint FileHash => _state.NameHash;
public uint DirHash => _state.DirHash; public uint DirHash => _state.DirHash;
@ -43,20 +45,36 @@ public class DX10FileEntryBuilder : IFileBuilder
foreach (var chunk in _chunks) foreach (var chunk in _chunks)
await chunk.WriteData(wtr, token); await chunk.WriteData(wtr, token);
} }
public uint GetHeaderSize(BA2DX10File state)
{
if (_headerSize > 0)
return _headerSize;
public static async Task<DX10FileEntryBuilder> Create(BA2DX10File state, Stream src, DiskSlabAllocator slab, uint size = 0;
size += (uint)Marshal.SizeOf(DirectXTexUtility.DDSHeader.DDSMagic);
size += (uint)Marshal.SizeOf<DirectXTexUtility.DDSHeader>();
var metadata = DirectXTexUtility.GenerateMetadata(state.Width, state.Height, state.NumMips, (DirectXTexUtility.DXGIFormat)state.PixelFormat, state.IsCubeMap == 1);
var pixelFormat = DirectXTexUtility.GetPixelFormat(metadata);
var hasDx10Header = DirectXTexUtility.HasDx10Header(pixelFormat);
if (hasDx10Header)
size += (uint)Marshal.SizeOf<DirectXTexUtility.DX10Header>();
return _headerSize = size;
}
public static async Task<DX10FileEntryBuilder> Create(BA2DX10File state, Stream src, DiskSlabAllocator slab, bool useLz4Compression,
CancellationToken token) CancellationToken token)
{ {
var builder = new DX10FileEntryBuilder {_state = state}; var builder = new DX10FileEntryBuilder {_state = state};
var headerSize = DDS.HeaderSizeForFormat((DXGI_FORMAT) state.PixelFormat) + 4; var headerSize = builder.GetHeaderSize(state);
new BinaryReader(src).ReadBytes((int) headerSize); new BinaryReader(src).ReadBytes((int) headerSize);
// This can't be parallel because it all runs off the same base IO stream. // This can't be parallel because it all runs off the same base IO stream.
builder._chunks = new List<ChunkBuilder>(); builder._chunks = new List<ChunkBuilder>();
foreach (var chunk in state.Chunks) foreach (var chunk in state.Chunks)
builder._chunks.Add(await ChunkBuilder.Create(state, chunk, src, slab, token)); builder._chunks.Add(await ChunkBuilder.Create(state, chunk, src, slab, useLz4Compression, token));
return builder; return builder;
} }

View File

@ -9,7 +9,7 @@ using Wabbajack.DTOs.BSA.FileStates;
using Wabbajack.DTOs.Streams; using Wabbajack.DTOs.Streams;
using Wabbajack.Paths; using Wabbajack.Paths;
namespace Wabbajack.Compression.BSA.FO4Archive; namespace Wabbajack.Compression.BSA.BA2Archive;
public class FileEntry : IBA2FileEntry public class FileEntry : IBA2FileEntry
{ {

View File

@ -6,7 +6,7 @@ using ICSharpCode.SharpZipLib.Zip.Compression.Streams;
using Wabbajack.Common; using Wabbajack.Common;
using Wabbajack.DTOs.BSA.FileStates; using Wabbajack.DTOs.BSA.FileStates;
namespace Wabbajack.Compression.BSA.FO4Archive; namespace Wabbajack.Compression.BSA.BA2Archive;
public class FileEntryBuilder : IFileBuilder public class FileEntryBuilder : IFileBuilder
{ {

View File

@ -1,6 +1,6 @@
using Wabbajack.Compression.BSA.Interfaces; using Wabbajack.Compression.BSA.Interfaces;
namespace Wabbajack.Compression.BSA.FO4Archive; namespace Wabbajack.Compression.BSA.BA2Archive;
internal interface IBA2FileEntry : IFile internal interface IBA2FileEntry : IFile
{ {

View File

@ -2,7 +2,7 @@ using System.IO;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Wabbajack.Compression.BSA.FO4Archive; namespace Wabbajack.Compression.BSA.BA2Archive;
internal interface IFileBuilder internal interface IFileBuilder
{ {

View File

@ -7,7 +7,7 @@ using Wabbajack.Compression.BSA.Interfaces;
using Wabbajack.DTOs.BSA.ArchiveStates; using Wabbajack.DTOs.BSA.ArchiveStates;
using Wabbajack.DTOs.Streams; using Wabbajack.DTOs.Streams;
namespace Wabbajack.Compression.BSA.FO4Archive; namespace Wabbajack.Compression.BSA.BA2Archive;
public class Reader : IReader public class Reader : IReader
{ {
@ -15,9 +15,17 @@ public class Reader : IReader
internal string _headerMagic; internal string _headerMagic;
internal ulong _nameTableOffset; internal ulong _nameTableOffset;
internal uint _numFiles; internal uint _numFiles;
internal uint _unknown1;
internal uint _unknown2;
internal uint _compression;
internal BinaryReader _rdr; internal BinaryReader _rdr;
public IStreamFactory _streamFactory; public IStreamFactory _streamFactory;
internal BA2EntryType _type; internal BA2EntryType _type;
/// <summary>
/// Fallout 4 - Version 1, 7 or 8
/// Starfield - Version 2 or 3
/// </summary>
internal uint _version; internal uint _version;
private Reader(Stream stream) private Reader(Stream stream)
@ -37,7 +45,10 @@ public class Reader : IReader
Version = _version, Version = _version,
HeaderMagic = _headerMagic, HeaderMagic = _headerMagic,
Type = _type, Type = _type,
HasNameTable = HasNameTable HasNameTable = HasNameTable,
Unknown1 = _unknown1,
Unknown2 = _unknown2,
Compression = _compression,
}; };
@ -67,6 +78,10 @@ public class Reader : IReader
_numFiles = _rdr.ReadUInt32(); _numFiles = _rdr.ReadUInt32();
_nameTableOffset = _rdr.ReadUInt64(); _nameTableOffset = _rdr.ReadUInt64();
_unknown1 = (_version == 2 || _version == 3) ? _rdr.ReadUInt32() : 0;
_unknown2 = (_version == 2 || _version == 3) ? _rdr.ReadUInt32() : 0;
_compression = (_version == 3) ? _rdr.ReadUInt32() : 0;
var files = new List<IBA2FileEntry>(); var files = new List<IBA2FileEntry>();
for (var idx = 0; idx < _numFiles; idx += 1) for (var idx = 0; idx < _numFiles; idx += 1)
switch (_type) switch (_type)

View File

@ -1,6 +1,6 @@
using System.IO; using System.IO;
namespace Wabbajack.Compression.BSA.FO4Archive; namespace Wabbajack.Compression.BSA.BA2Archive;
public class TextureChunk public class TextureChunk
{ {

View File

@ -22,7 +22,7 @@ public static class BSADispatch
{ {
FileType.TES3 => await Reader.Load(new NativeFileStreamFactory(filename)), FileType.TES3 => await Reader.Load(new NativeFileStreamFactory(filename)),
FileType.BSA => await TES5Archive.Reader.Load(new NativeFileStreamFactory(filename)), FileType.BSA => await TES5Archive.Reader.Load(new NativeFileStreamFactory(filename)),
FileType.BA2 => await FO4Archive.Reader.Load(new NativeFileStreamFactory(filename)), FileType.BA2 => await BA2Archive.Reader.Load(new NativeFileStreamFactory(filename)),
_ => throw new InvalidDataException("Filename is not a .bsa or .ba2") _ => throw new InvalidDataException("Filename is not a .bsa or .ba2")
}; };
} }
@ -34,7 +34,7 @@ public static class BSADispatch
{ {
FileType.TES3 => await Reader.Load(factory), FileType.TES3 => await Reader.Load(factory),
FileType.BSA => await TES5Archive.Reader.Load(factory), FileType.BSA => await TES5Archive.Reader.Load(factory),
FileType.BA2 => await FO4Archive.Reader.Load(factory), FileType.BA2 => await BA2Archive.Reader.Load(factory),
_ => throw new InvalidDataException("Filename is not a .bsa or .ba2") _ => throw new InvalidDataException("Filename is not a .bsa or .ba2")
}; };
} }
@ -46,7 +46,7 @@ public static class BSADispatch
{ {
FileType.TES3 => await Reader.Load(factory), FileType.TES3 => await Reader.Load(factory),
FileType.BSA => await TES5Archive.Reader.Load(factory), FileType.BSA => await TES5Archive.Reader.Load(factory),
FileType.BA2 => await FO4Archive.Reader.Load(factory), FileType.BA2 => await BA2Archive.Reader.Load(factory),
_ => throw new InvalidDataException("Filename is not a .bsa or .ba2") _ => throw new InvalidDataException("Filename is not a .bsa or .ba2")
}; };
} }
@ -57,7 +57,7 @@ public static class BSADispatch
{ {
TES3State tes3 => new Builder(tes3), TES3State tes3 => new Builder(tes3),
BSAState bsa => TES5Archive.Builder.Create(bsa, manager), BSAState bsa => TES5Archive.Builder.Create(bsa, manager),
BA2State ba2 => FO4Archive.Builder.Create(ba2, manager), BA2State ba2 => BA2Archive.Builder.Create(ba2, manager),
_ => throw new NotImplementedException() _ => throw new NotImplementedException()
}; };
} }

View File

@ -1,215 +0,0 @@
using System;
using System.IO;
using System.Runtime.InteropServices;
using Wabbajack.DTOs.Texture;
namespace Compression.BSA;
/*
* Copied from https://raw.githubusercontent.com/AlexxEG/BSA_Browser/master/Sharp.BSA.BA2/BA2Util/DDS.cs
* which is also GPL3 code. Modified slightly for Wabbajack
*
*/
/*
* Copied from dds.h. Includes (almost) only stuff I need in this project.
*
* Link: https://github.com/digitalutopia1/BA2Lib/blob/master/BA2Lib/dds.h
*
*/
public class DDS
{
public const int DDS_MAGIC = 0x20534444; // "DDS "
public const int DDS_FOURCC = 0x00000004; // DDPF_FOURCC
public const int DDS_RGB = 0x00000040; // DDPF_RGB
public const int DDS_RGBA = 0x00000041; // DDPF_RGB | DDPF_ALPHAPIXELS
public const int
DDS_HEADER_FLAGS_TEXTURE = 0x00001007; // DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH | DDSD_PIXELFORMAT
public const int DDS_HEADER_FLAGS_MIPMAP = 0x00020000; // DDSD_MIPMAPCOUNT
public const int DDS_HEADER_FLAGS_LINEARSIZE = 0x00080000; // DDSD_LINEARSIZE
public const int DDS_SURFACE_FLAGS_TEXTURE = 0x00001000; // DDSCAPS_TEXTURE
public const int DDS_SURFACE_FLAGS_MIPMAP = 0x00400008; // DDSCAPS_COMPLEX | DDSCAPS_MIPMAP
public const int DDS_ALPHA_MODE_UNKNOWN = 0x0;
public static uint HeaderSizeForFormat(DXGI_FORMAT fmt)
{
switch (fmt)
{
case DXGI_FORMAT.BC1_UNORM_SRGB:
case DXGI_FORMAT.BC3_UNORM_SRGB:
case DXGI_FORMAT.BC4_UNORM:
case DXGI_FORMAT.BC5_SNORM:
case DXGI_FORMAT.BC6H_UF16:
case DXGI_FORMAT.BC7_UNORM:
case DXGI_FORMAT.BC7_UNORM_SRGB:
return DDS_HEADER_DXT10.Size + DDS_HEADER.Size;
default:
return DDS_HEADER.Size;
}
}
public static uint MAKEFOURCC(char ch0, char ch1, char ch2, char ch3)
{
// This is alien to me...
return (byte) ch0 | ((uint) (byte) ch1 << 8) | ((uint) (byte) ch2 << 16) | ((uint) (byte) ch3 << 24);
}
}
public enum DXT10_RESOURCE_DIMENSION
{
DIMENSION_TEXTURE1D = 2,
DIMENSION_TEXTURE2D = 3,
DIMENSION_TEXTURE3D = 4
}
[Flags]
public enum DDSCAPS2 : uint
{
CUBEMAP = 0x200,
CUBEMAP_POSITIVEX = 0x400,
CUBEMAP_NEGATIVEX = 0x800,
CUBEMAP_POSITIVEY = 0x1000,
CUBEMAP_NEGATIVEY = 0x2000,
CUBEMAP_POSITIVEZ = 0x4000,
CUBEMAP_NEGATIVEZ = 0x8000,
CUBEMAP_ALLFACES = 0xFC00
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct DDS_HEADER
{
public uint dwSize;
public uint dwHeaderFlags;
public uint dwHeight;
public uint dwWidth;
public uint dwPitchOrLinearSize;
public uint dwDepth; // only if DDS_HEADER_FLAGS_VOLUME is set in dwHeaderFlags
public uint dwMipMapCount;
public uint dwReserved1; // [11]
public DDS_PIXELFORMAT PixelFormat; // ddspf
public uint dwSurfaceFlags;
public uint dwCubemapFlags;
public uint dwReserved2; // [3]
public uint GetSize()
{
// 9 uint + DDS_PIXELFORMAT uints + 2 uint arrays with 14 uints total
// each uint 4 bytes each
return 9 * 4 + PixelFormat.GetSize() + 14 * 4;
}
public void Write(BinaryWriter bw)
{
bw.Write(dwSize);
bw.Write(dwHeaderFlags);
bw.Write(dwHeight);
bw.Write(dwWidth);
bw.Write(dwPitchOrLinearSize);
bw.Write(dwDepth);
bw.Write(dwMipMapCount);
// Just write it multiple times, since it's never assigned a value anyway
for (var i = 0; i < 11; i++)
bw.Write(dwReserved1);
// DDS_PIXELFORMAT
bw.Write(PixelFormat.dwSize);
bw.Write(PixelFormat.dwFlags);
bw.Write(PixelFormat.dwFourCC);
bw.Write(PixelFormat.dwRGBBitCount);
bw.Write(PixelFormat.dwRBitMask);
bw.Write(PixelFormat.dwGBitMask);
bw.Write(PixelFormat.dwBBitMask);
bw.Write(PixelFormat.dwABitMask);
bw.Write(dwSurfaceFlags);
bw.Write(dwCubemapFlags);
// Just write it multiple times, since it's never assigned a value anyway
for (var i = 0; i < 3; i++)
bw.Write(dwReserved2);
}
public static uint Size
{
get
{
unsafe
{
return (uint) (sizeof(DDS_HEADER) + sizeof(int) * 10 + sizeof(int) * 2);
}
;
}
}
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct DDS_HEADER_DXT10
{
public uint dxgiFormat;
public uint resourceDimension;
public uint miscFlag;
public uint arraySize;
public uint miscFlags2;
public void Write(BinaryWriter bw)
{
bw.Write(dxgiFormat);
bw.Write(resourceDimension);
bw.Write(miscFlag);
bw.Write(arraySize);
bw.Write(miscFlags2);
}
public static uint Size
{
get
{
unsafe
{
return (uint) sizeof(DDS_HEADER_DXT10);
}
;
}
}
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct DDS_PIXELFORMAT
{
public uint dwSize;
public uint dwFlags;
public uint dwFourCC;
public uint dwRGBBitCount;
public uint dwRBitMask;
public uint dwGBitMask;
public uint dwBBitMask;
public uint dwABitMask;
public DDS_PIXELFORMAT(uint size, uint flags, uint fourCC, uint rgbBitCount, uint rBitMask, uint gBitMask,
uint bBitMask, uint aBitMask)
{
dwSize = size;
dwFlags = flags;
dwFourCC = fourCC;
dwRGBBitCount = rgbBitCount;
dwRBitMask = rBitMask;
dwGBitMask = gBitMask;
dwBBitMask = bBitMask;
dwABitMask = aBitMask;
}
public uint GetSize()
{
// 8 uints, each 4 bytes each
return 8 * 4;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,69 +0,0 @@
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using ICSharpCode.SharpZipLib.Zip.Compression;
using ICSharpCode.SharpZipLib.Zip.Compression.Streams;
using Wabbajack.Common;
using Wabbajack.DTOs.BSA.FileStates;
namespace Wabbajack.Compression.BSA.FO4Archive;
public class ChunkBuilder
{
private BA2Chunk _chunk;
private Stream _dataSlab;
private long _offsetOffset;
private uint _packSize;
public static async Task<ChunkBuilder> Create(BA2DX10File state, BA2Chunk chunk, Stream src,
DiskSlabAllocator slab, CancellationToken token)
{
var builder = new ChunkBuilder {_chunk = chunk};
if (!chunk.Compressed)
{
builder._dataSlab = slab.Allocate(chunk.FullSz);
await src.CopyToLimitAsync(builder._dataSlab, (int) chunk.FullSz, token);
}
else
{
var deflater = new Deflater(Deflater.BEST_COMPRESSION);
await using var ms = new MemoryStream();
await using (var ds = new DeflaterOutputStream(ms, deflater))
{
ds.IsStreamOwner = false;
await src.CopyToLimitAsync(ds, (int) chunk.FullSz, token);
}
builder._dataSlab = slab.Allocate(ms.Length);
ms.Position = 0;
await ms.CopyToLimitAsync(builder._dataSlab, (int) ms.Length, token);
builder._packSize = (uint) ms.Length;
}
builder._dataSlab.Position = 0;
return builder;
}
public void WriteHeader(BinaryWriter bw)
{
_offsetOffset = bw.BaseStream.Position;
bw.Write((ulong) 0);
bw.Write(_packSize);
bw.Write(_chunk.FullSz);
bw.Write(_chunk.StartMip);
bw.Write(_chunk.EndMip);
bw.Write(_chunk.Align);
}
public async ValueTask WriteData(BinaryWriter bw, CancellationToken token)
{
var pos = bw.BaseStream.Position;
bw.BaseStream.Position = _offsetOffset;
bw.Write((ulong) pos);
bw.BaseStream.Position = pos;
await _dataSlab.CopyToLimitAsync(bw.BaseStream, (int) _dataSlab.Length, token);
await _dataSlab.DisposeAsync();
}
}

View File

@ -1,249 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Compression.BSA;
using ICSharpCode.SharpZipLib.Zip.Compression;
using Wabbajack.Common;
using Wabbajack.DTOs.BSA.FileStates;
using Wabbajack.DTOs.Streams;
using Wabbajack.DTOs.Texture;
using Wabbajack.Paths;
namespace Wabbajack.Compression.BSA.FO4Archive;
public class DX10Entry : IBA2FileEntry
{
private readonly Reader _bsa;
private ushort _chunkHdrLen;
private List<TextureChunk> _chunks;
private uint _dirHash;
private string _extension;
private byte _format;
private ushort _height;
private int _index;
private uint _nameHash;
private byte _numChunks;
private byte _numMips;
private ushort _unk16;
private byte _unk8;
private ushort _width;
private readonly byte _isCubemap;
private readonly byte _tileMode;
public DX10Entry(Reader ba2Reader, int idx)
{
_bsa = ba2Reader;
var _rdr = ba2Reader._rdr;
_nameHash = _rdr.ReadUInt32();
FullPath = _nameHash.ToString("X");
_extension = Encoding.UTF8.GetString(_rdr.ReadBytes(4));
_dirHash = _rdr.ReadUInt32();
_unk8 = _rdr.ReadByte();
_numChunks = _rdr.ReadByte();
_chunkHdrLen = _rdr.ReadUInt16();
_height = _rdr.ReadUInt16();
_width = _rdr.ReadUInt16();
_numMips = _rdr.ReadByte();
_format = _rdr.ReadByte();
_isCubemap = _rdr.ReadByte();
_tileMode = _rdr.ReadByte();
_index = idx;
_chunks = Enumerable.Range(0, _numChunks)
.Select(_ => new TextureChunk(_rdr))
.ToList();
}
public uint HeaderSize => DDS.HeaderSizeForFormat((DXGI_FORMAT) _format);
public string FullPath { get; set; }
public RelativePath Path => FullPath.ToRelativePath();
public uint Size => (uint) _chunks.Sum(f => f._fullSz) + HeaderSize + sizeof(uint);
public AFile State => new BA2DX10File
{
Path = Path,
NameHash = _nameHash,
Extension = _extension,
DirHash = _dirHash,
Unk8 = _unk8,
ChunkHdrLen = _chunkHdrLen,
Height = _height,
Width = _width,
NumMips = _numMips,
PixelFormat = _format,
IsCubeMap = _isCubemap,
TileMode = _tileMode,
Index = _index,
Chunks = _chunks.Select(ch => new BA2Chunk
{
FullSz = ch._fullSz,
StartMip = ch._startMip,
EndMip = ch._endMip,
Align = ch._align,
Compressed = ch._packSz != 0
}).ToArray()
};
public async ValueTask CopyDataTo(Stream output, CancellationToken token)
{
var bw = new BinaryWriter(output);
WriteHeader(bw);
await using var fs = await _bsa._streamFactory.GetStream();
using var br = new BinaryReader(fs);
foreach (var chunk in _chunks)
{
var full = new byte[chunk._fullSz];
var isCompressed = chunk._packSz != 0;
br.BaseStream.Seek((long) chunk._offset, SeekOrigin.Begin);
if (!isCompressed)
{
await br.BaseStream.ReadAsync(full, token);
}
else
{
var compressed = new byte[chunk._packSz];
await br.BaseStream.ReadAsync(compressed, token);
var inflater = new Inflater();
inflater.SetInput(compressed);
inflater.Inflate(full);
}
await bw.BaseStream.WriteAsync(full, token);
}
}
public async ValueTask<IStreamFactory> GetStreamFactory(CancellationToken token)
{
var ms = new MemoryStream();
await CopyDataTo(ms, token);
ms.Position = 0;
return new MemoryStreamFactory(ms, Path, _bsa._streamFactory.LastModifiedUtc);
}
private void WriteHeader(BinaryWriter bw)
{
var ddsHeader = new DDS_HEADER();
ddsHeader.dwSize = ddsHeader.GetSize();
ddsHeader.dwHeaderFlags = DDS.DDS_HEADER_FLAGS_TEXTURE | DDS.DDS_HEADER_FLAGS_LINEARSIZE |
DDS.DDS_HEADER_FLAGS_MIPMAP;
ddsHeader.dwHeight = _height;
ddsHeader.dwWidth = _width;
ddsHeader.dwMipMapCount = _numMips;
ddsHeader.PixelFormat.dwSize = ddsHeader.PixelFormat.GetSize();
ddsHeader.dwDepth = 1;
ddsHeader.dwSurfaceFlags = DDS.DDS_SURFACE_FLAGS_TEXTURE | DDS.DDS_SURFACE_FLAGS_MIPMAP;
ddsHeader.dwCubemapFlags = _isCubemap == 1 ? (uint)(DDSCAPS2.CUBEMAP
| DDSCAPS2.CUBEMAP_NEGATIVEX | DDSCAPS2.CUBEMAP_POSITIVEX
| DDSCAPS2.CUBEMAP_NEGATIVEY | DDSCAPS2.CUBEMAP_POSITIVEY
| DDSCAPS2.CUBEMAP_NEGATIVEZ | DDSCAPS2.CUBEMAP_POSITIVEZ
| DDSCAPS2.CUBEMAP_ALLFACES) : 0u;
switch ((DXGI_FORMAT) _format)
{
case DXGI_FORMAT.BC1_UNORM:
ddsHeader.PixelFormat.dwFlags = DDS.DDS_FOURCC;
ddsHeader.PixelFormat.dwFourCC = DDS.MAKEFOURCC('D', 'X', 'T', '1');
ddsHeader.dwPitchOrLinearSize = (uint) (_width * _height / 2); // 4bpp
break;
case DXGI_FORMAT.BC2_UNORM:
ddsHeader.PixelFormat.dwFlags = DDS.DDS_FOURCC;
ddsHeader.PixelFormat.dwFourCC = DDS.MAKEFOURCC('D', 'X', 'T', '3');
ddsHeader.dwPitchOrLinearSize = (uint) (_width * _height); // 8bpp
break;
case DXGI_FORMAT.BC3_UNORM:
ddsHeader.PixelFormat.dwFlags = DDS.DDS_FOURCC;
ddsHeader.PixelFormat.dwFourCC = DDS.MAKEFOURCC('D', 'X', 'T', '5');
ddsHeader.dwPitchOrLinearSize = (uint) (_width * _height); // 8bpp
break;
case DXGI_FORMAT.BC5_UNORM:
ddsHeader.PixelFormat.dwFlags = DDS.DDS_FOURCC;
if (_bsa.UseATIFourCC)
ddsHeader.PixelFormat.dwFourCC =
DDS.MAKEFOURCC('A', 'T', 'I',
'2'); // this is more correct but the only thing I have found that supports it is the nvidia photoshop plugin
else
ddsHeader.PixelFormat.dwFourCC = DDS.MAKEFOURCC('B', 'C', '5', 'U');
ddsHeader.dwPitchOrLinearSize = (uint) (_width * _height); // 8bpp
break;
case DXGI_FORMAT.BC1_UNORM_SRGB:
ddsHeader.PixelFormat.dwFlags = DDS.DDS_FOURCC;
ddsHeader.PixelFormat.dwFourCC = DDS.MAKEFOURCC('D', 'X', '1', '0');
ddsHeader.dwPitchOrLinearSize = (uint) (_width * _height / 2); // 4bpp
break;
case DXGI_FORMAT.BC3_UNORM_SRGB:
case DXGI_FORMAT.BC6H_UF16:
case DXGI_FORMAT.BC4_UNORM:
case DXGI_FORMAT.BC5_SNORM:
case DXGI_FORMAT.BC7_UNORM:
case DXGI_FORMAT.BC7_UNORM_SRGB:
ddsHeader.PixelFormat.dwFlags = DDS.DDS_FOURCC;
ddsHeader.PixelFormat.dwFourCC = DDS.MAKEFOURCC('D', 'X', '1', '0');
ddsHeader.dwPitchOrLinearSize = (uint) (_width * _height); // 8bpp
break;
case DXGI_FORMAT.R8G8B8A8_UNORM:
case DXGI_FORMAT.R8G8B8A8_UNORM_SRGB:
ddsHeader.PixelFormat.dwFlags = DDS.DDS_RGBA;
ddsHeader.PixelFormat.dwRGBBitCount = 32;
ddsHeader.PixelFormat.dwRBitMask = 0x000000FF;
ddsHeader.PixelFormat.dwGBitMask = 0x0000FF00;
ddsHeader.PixelFormat.dwBBitMask = 0x00FF0000;
ddsHeader.PixelFormat.dwABitMask = 0xFF000000;
ddsHeader.dwPitchOrLinearSize = (uint) (_width * _height * 4); // 32bpp
break;
case DXGI_FORMAT.B8G8R8A8_UNORM:
case DXGI_FORMAT.B8G8R8X8_UNORM:
ddsHeader.PixelFormat.dwFlags = DDS.DDS_RGBA;
ddsHeader.PixelFormat.dwRGBBitCount = 32;
ddsHeader.PixelFormat.dwRBitMask = 0x00FF0000;
ddsHeader.PixelFormat.dwGBitMask = 0x0000FF00;
ddsHeader.PixelFormat.dwBBitMask = 0x000000FF;
ddsHeader.PixelFormat.dwABitMask = 0xFF000000;
ddsHeader.dwPitchOrLinearSize = (uint) (_width * _height * 4); // 32bpp
break;
case DXGI_FORMAT.R8_UNORM:
ddsHeader.PixelFormat.dwFlags = DDS.DDS_RGB;
ddsHeader.PixelFormat.dwRGBBitCount = 8;
ddsHeader.PixelFormat.dwRBitMask = 0xFF;
ddsHeader.dwPitchOrLinearSize = (uint) (_width * _height); // 8bpp
break;
default:
throw new Exception("Unsupported DDS header format. File: " + FullPath);
}
bw.Write((uint) DDS.DDS_MAGIC);
ddsHeader.Write(bw);
switch ((DXGI_FORMAT) _format)
{
case DXGI_FORMAT.BC1_UNORM_SRGB:
case DXGI_FORMAT.BC3_UNORM_SRGB:
case DXGI_FORMAT.BC4_UNORM:
case DXGI_FORMAT.BC5_SNORM:
case DXGI_FORMAT.BC6H_UF16:
case DXGI_FORMAT.BC7_UNORM:
case DXGI_FORMAT.BC7_UNORM_SRGB:
var dxt10 = new DDS_HEADER_DXT10
{
dxgiFormat = _format,
resourceDimension = (uint) DXT10_RESOURCE_DIMENSION.DIMENSION_TEXTURE2D,
miscFlag = 0,
arraySize = 1,
miscFlags2 = DDS.DDS_ALPHA_MODE_UNKNOWN
};
dxt10.Write(bw);
break;
}
}
}

View File

@ -17,4 +17,7 @@ public class BA2State : IArchive
public BA2EntryType Type { get; set; } public BA2EntryType Type { get; set; }
public string HeaderMagic { get; set; } public string HeaderMagic { get; set; }
public uint Version { get; set; } public uint Version { get; set; }
public uint Unknown1 { get; set; }
public uint Unknown2 { get; set; }
public uint Compression { get; set; }
} }