Address some MPQ issues

This commit is contained in:
Matt Nadareski
2022-12-15 16:50:24 -08:00
parent aded5ee03a
commit b3c0e48bdd
3 changed files with 166 additions and 181 deletions

View File

@@ -10,198 +10,46 @@ namespace BurnOutSharp.Builder
{
#region Constants
#region User Data
#region Encryption
public const int UserDataSize = 0x10;
private const uint MPQ_HASH_KEY2_MIX = 0x400;
/// <summary>
/// Human-readable signature
/// Obtained by HashString("(block table)", MPQ_HASH_FILE_KEY)
/// </summary>
public static readonly string UserDataSignatureString = $"MPQ{(char)0x1B}";
private const uint MPQ_KEY_BLOCK_TABLE = 0xEC83B3A3;
/// <summary>
/// Signature as an unsigned Int32 value
/// Obtained by HashString("(hash table)", MPQ_HASH_FILE_KEY)
/// </summary>
public const uint UserDataSignatureValue = 0x1B51504D;
private const uint MPQ_KEY_HASH_TABLE = 0xC3AF3770;
/// <summary>
/// Signature as a byte array
/// </summary>
public static readonly byte[] UserDataSignatureBytes = new byte[] { 0x4D, 0x50, 0x51, 0x1B };
#endregion
#region Archive Header
#region Signatures
/// <summary>
/// Human-readable signature
/// </summary>
public static readonly string ArchiveHeaderSignatureString = $"MPQ{(char)0x1A}";
/// <summary>
/// Signature as an unsigned Int32 value
/// </summary>
public const uint ArchiveHeaderSignatureValue = 0x1A51504D;
/// <summary>
/// Signature as a byte array
/// </summary>
public static readonly byte[] ArchiveHeaderSignatureBytes = new byte[] { 0x4D, 0x50, 0x51, 0x1A };
#endregion
#region Header Sizes
public const int ArchiveHeaderHeaderVersion1Size = 0x20;
public const int ArchiveHeaderHeaderVersion2Size = 0x2C;
public const int ArchiveHeaderHeaderVersion3Size = 0x44;
public const int ArchiveHeaderHeaderVersion4Size = 0xD0;
#endregion
#endregion
#region HET Table
public const int HetTableSize = 0x44;
/// <summary>
/// Human-readable signature
/// </summary>
public static readonly string HetTableSignatureString = $"HET{(char)0x1A}";
/// <summary>
/// Signature as an unsigned Int32 value
/// </summary>
public const uint HetTableSignatureValue = 0x1A544548;
/// <summary>
/// Signature as a byte array
/// </summary>
public static readonly byte[] HetTableSignatureBytes = new byte[] { 0x48, 0x45, 0x54, 0x1A };
#endregion
#region BET Table
public const int BetTableSize = 0x88;
/// <summary>
/// Human-readable signature
/// </summary>
public static readonly string BetTableSignatureString = $"BET{(char)0x1A}";
/// <summary>
/// Signature as an unsigned Int32 value
/// </summary>
public const uint BetTableSignatureValue = 0x1A544542;
/// <summary>
/// Signature as a byte array
/// </summary>
public static readonly byte[] BetTableSignatureBytes = new byte[] { 0x42, 0x45, 0x54, 0x1A };
#endregion
#region Hash Entry
public const int HashEntrySize = 0x10;
#endregion
#region Block Entry
public const int BlockEntrySize = 0x10;
private const uint STORM_BUFFER_SIZE = 0x500;
#endregion
#region Patch Header
#region Signatures
#region Patch Header
/// <summary>
/// Human-readable signature
/// </summary>
public static readonly string PatchSignatureString = $"PTCH";
/// <summary>
/// Signature as an unsigned Int32 value
/// </summary>
public const uint PatchSignatureValue = 0x48435450;
/// <summary>
/// Signature as a byte array
/// </summary>
public static readonly byte[] PatchSignatureBytes = new byte[] { 0x50, 0x54, 0x43, 0x48 };
#endregion
#region MD5 Block
/// <summary>
/// Human-readable signature
/// </summary>
public static readonly string Md5SignatureString = $"MD5_";
/// <summary>
/// Signature as an unsigned Int32 value
/// </summary>
public const uint Md5SignatureValue = 0x5F35444D;
/// <summary>
/// Signature as a byte array
/// </summary>
public static readonly byte[] Md5SignatureBytes = new byte[] { 0x4D, 0x44, 0x35, 0x5F };
#endregion
#region XFRM Block
/// <summary>
/// Human-readable signature
/// </summary>
public static readonly string XFRMSignatureString = $"XFRM";
/// <summary>
/// Signature as an unsigned Int32 value
/// </summary>
public const uint XFRMSignatureValue = 0x4D524658;
/// <summary>
/// Signature as a byte array
/// </summary>
public static readonly byte[] XFRMSignatureBytes = new byte[] { 0x58, 0x46, 0x52, 0x4D };
#endregion
#region BSDIFF Patch Type
/// <summary>
/// Human-readable signature
/// </summary>
public static readonly string BSDIFF40SignatureString = $"BSDIFF40";
/// <summary>
/// Signature as an unsigned Int64 value
/// </summary>
public const ulong BSDIFF40SignatureValue = 0x3034464649445342;
/// <summary>
/// Signature as a byte array
/// </summary>
public static readonly byte[] BSDIFF40SignatureBytes = new byte[] { 0x42, 0x53, 0x44, 0x49, 0x46, 0x46, 0x34, 0x30 };
#endregion
#endregion
#endregion
#endregion
@@ -259,7 +107,7 @@ namespace BurnOutSharp.Builder
// Check for User Data
uint possibleSignature = data.ReadUInt32();
data.Seek(-4, SeekOrigin.Current);
if (possibleSignature == UserDataSignatureValue)
if (possibleSignature == 0x1B51504D)
{
// Save the current position for offset correction
long basePtr = data.Position;
@@ -283,7 +131,7 @@ namespace BurnOutSharp.Builder
// Check for the Header
possibleSignature = data.ReadUInt32();
data.Seek(-4, SeekOrigin.Current);
if (possibleSignature == ArchiveHeaderSignatureValue)
if (possibleSignature == 0x1A51504D)
{
// Try to parse the archive header
var archiveHeader = ParseArchiveHeader(data);
@@ -293,13 +141,19 @@ namespace BurnOutSharp.Builder
// Set the archive header
archive.ArchiveHeader = archiveHeader;
}
else
{
return null;
}
#endregion
#region Hash Table
// TODO: The hash table has to be be decrypted before reading
// Version 1
if (archive.ArchiveHeader.FormatVersion == 0)
if (archive.ArchiveHeader.FormatVersion == FormatVersion.Format1)
{
// If we have a hash table
long hashTableOffset = archive.ArchiveHeader.HashTablePosition;
@@ -328,7 +182,7 @@ namespace BurnOutSharp.Builder
}
// Version 2 and 3
else if (archive.ArchiveHeader.FormatVersion == 1 || archive.ArchiveHeader.FormatVersion == 2)
else if (archive.ArchiveHeader.FormatVersion == FormatVersion.Format2 || archive.ArchiveHeader.FormatVersion == FormatVersion.Format3)
{
// If we have a hash table
long hashTableOffset = ((uint)archive.ArchiveHeader.HashTablePositionHi << 23) | archive.ArchiveHeader.HashTablePosition;
@@ -357,7 +211,7 @@ namespace BurnOutSharp.Builder
}
// Version 4
else if (archive.ArchiveHeader.FormatVersion == 3)
else if (archive.ArchiveHeader.FormatVersion == FormatVersion.Format4)
{
// If we have a hash table
long hashTableOffset = ((uint)archive.ArchiveHeader.HashTablePositionHi << 23) | archive.ArchiveHeader.HashTablePosition;
@@ -390,7 +244,7 @@ namespace BurnOutSharp.Builder
#region Block Table
// Version 1
if (archive.ArchiveHeader.FormatVersion == 0)
if (archive.ArchiveHeader.FormatVersion == FormatVersion.Format1)
{
// If we have a block table
long blockTableOffset = archive.ArchiveHeader.BlockTablePosition;
@@ -419,7 +273,7 @@ namespace BurnOutSharp.Builder
}
// Version 2 and 3
else if (archive.ArchiveHeader.FormatVersion == 1 || archive.ArchiveHeader.FormatVersion == 2)
else if (archive.ArchiveHeader.FormatVersion == FormatVersion.Format2 || archive.ArchiveHeader.FormatVersion == FormatVersion.Format3)
{
// If we have a block table
long blockTableOffset = ((uint)archive.ArchiveHeader.BlockTablePositionHi << 23) | archive.ArchiveHeader.BlockTablePosition;
@@ -448,7 +302,7 @@ namespace BurnOutSharp.Builder
}
// Version 4
else if (archive.ArchiveHeader.FormatVersion == 3)
else if (archive.ArchiveHeader.FormatVersion == FormatVersion.Format4)
{
// If we have a block table
long blockTableOffset = ((uint)archive.ArchiveHeader.BlockTablePositionHi << 23) | archive.ArchiveHeader.BlockTablePosition;
@@ -481,9 +335,7 @@ namespace BurnOutSharp.Builder
#region Hi-Block Table
// Version 2, 3, and 4
if (archive.ArchiveHeader.FormatVersion == 1
|| archive.ArchiveHeader.FormatVersion == 2
|| archive.ArchiveHeader.FormatVersion == 3)
if (archive.ArchiveHeader.FormatVersion >= FormatVersion.Format2)
{
// If we have a hi-block table
long hiBlockTableOffset = (long)archive.ArchiveHeader.HiBlockTablePosition;
@@ -510,7 +362,7 @@ namespace BurnOutSharp.Builder
#region BET Table
// Version 3 and 4
if (archive.ArchiveHeader.FormatVersion == 2 || archive.ArchiveHeader.FormatVersion == 3)
if (archive.ArchiveHeader.FormatVersion >= FormatVersion.Format3)
{
// If we have a BET table
long betTableOffset = (long)archive.ArchiveHeader.BetTablePosition;
@@ -533,7 +385,7 @@ namespace BurnOutSharp.Builder
#region HET Table
// Version 3 and 4
if (archive.ArchiveHeader.FormatVersion == 2 || archive.ArchiveHeader.FormatVersion == 3)
if (archive.ArchiveHeader.FormatVersion >= FormatVersion.Format3)
{
// If we have a HET table
long hetTableOffset = (long)archive.ArchiveHeader.HetTablePosition;
@@ -567,12 +419,12 @@ namespace BurnOutSharp.Builder
// V1 - Common
archiveHeader.Signature = data.ReadUInt32();
if (archiveHeader.Signature != ArchiveHeaderSignatureValue)
if (archiveHeader.Signature != 0x1A51504D)
return null;
archiveHeader.HeaderSize = data.ReadUInt32();
archiveHeader.ArchiveSize = data.ReadUInt32();
archiveHeader.FormatVersion = data.ReadUInt16();
archiveHeader.FormatVersion = (FormatVersion)data.ReadUInt16();
archiveHeader.BlockSize = data.ReadUInt16();
archiveHeader.HashTablePosition = data.ReadUInt32();
archiveHeader.BlockTablePosition = data.ReadUInt32();
@@ -580,7 +432,7 @@ namespace BurnOutSharp.Builder
archiveHeader.BlockTableSize = data.ReadUInt32();
// V2
if (archiveHeader.FormatVersion >= 2 && archiveHeader.HeaderSize >= ArchiveHeaderHeaderVersion2Size)
if (archiveHeader.FormatVersion >= FormatVersion.Format2)
{
archiveHeader.HiBlockTablePosition = data.ReadUInt64();
archiveHeader.HashTablePositionHi = data.ReadUInt16();
@@ -588,7 +440,7 @@ namespace BurnOutSharp.Builder
}
// V3
if (archiveHeader.FormatVersion >= 3 && archiveHeader.HeaderSize >= ArchiveHeaderHeaderVersion3Size)
if (archiveHeader.FormatVersion >= FormatVersion.Format3)
{
archiveHeader.ArchiveSizeLong = data.ReadUInt64();
archiveHeader.BetTablePosition = data.ReadUInt64();
@@ -596,7 +448,7 @@ namespace BurnOutSharp.Builder
}
// V4
if (archiveHeader.FormatVersion >= 4 && archiveHeader.HeaderSize >= ArchiveHeaderHeaderVersion4Size)
if (archiveHeader.FormatVersion >= FormatVersion.Format4)
{
archiveHeader.HashTableSizeLong = data.ReadUInt64();
archiveHeader.BlockTableSizeLong = data.ReadUInt64();
@@ -626,7 +478,7 @@ namespace BurnOutSharp.Builder
UserData userData = new UserData();
userData.Signature = data.ReadUInt32();
if (userData.Signature != UserDataSignatureValue)
if (userData.Signature != 0x1B51504D)
return null;
userData.UserDataSize = data.ReadUInt32();
@@ -647,7 +499,7 @@ namespace BurnOutSharp.Builder
// Common Headers
hetTable.Signature = data.ReadUInt32();
if (hetTable.Signature != HetTableSignatureValue)
if (hetTable.Signature != 0x1A544548)
return null;
hetTable.Version = data.ReadUInt32();
@@ -680,7 +532,7 @@ namespace BurnOutSharp.Builder
// Common Headers
betTable.Signature = data.ReadUInt32();
if (betTable.Signature != BetTableSignatureValue)
if (betTable.Signature != 0x1A544542)
return null;
betTable.Version = data.ReadUInt32();
@@ -777,5 +629,63 @@ namespace BurnOutSharp.Builder
}
#endregion
#region Helpers
/// <summary>
/// Buffer for encryption and decryption
/// </summary>
private uint[] _stormBuffer = new uint[STORM_BUFFER_SIZE];
/// <summary>
/// Prepare the encryption table
/// </summary>
private void PrepareCryptTable()
{
uint seed = 0x00100001;
for (uint index1 = 0; index1 < 0x100; index1++)
{
for (uint index2 = index1, i = 0; i < 5; i++, index2 += 0x100)
{
seed = (seed * 125 + 3) % 0x2AAAAB;
uint temp1 = (seed & 0xFFFF) << 0x10;
seed = (seed * 125 + 3) % 0x2AAAAB;
uint temp2 = (seed & 0xFFFF);
_stormBuffer[index2] = (temp1 | temp2);
}
}
}
/// <summary>
/// Decrypt a single block of data
/// </summary>
private unsafe byte[] DecryptBlock(byte[] block, uint length, uint key)
{
uint seed = 0xEEEEEEEE;
uint[] castBlock = new uint[length / 4];
Buffer.BlockCopy(block, 0, castBlock, 0, (int)length);
int castBlockPtr = 0;
// Round to uints
length >>= 2;
while (length-- > 0)
{
seed += _stormBuffer[MPQ_HASH_KEY2_MIX + (key & 0xFF)];
uint ch = castBlock[castBlockPtr] ^ (key + seed);
key = ((~key << 0x15) + 0x11111111) | (key >> 0x0B);
seed = ch + seed + (seed << 5) + 3;
castBlock[castBlockPtr++] = ch;
}
Buffer.BlockCopy(castBlock, 0, block, 0, (int)length);
return block;
}
#endregion
}
}

View File

@@ -37,7 +37,7 @@ namespace BurnOutSharp.Models.MoPaQ
/// 2 = Format 3 (WoW - Cataclysm beta or newer)
/// 3 = Format 4 (WoW - Cataclysm beta or newer)
/// </summary>
public ushort FormatVersion;
public FormatVersion FormatVersion;
/// <summary>
/// Power of two exponent specifying the number of 512-byte disk sectors in each logical sector

View File

@@ -2,6 +2,58 @@
namespace BurnOutSharp.Models.MoPaQ
{
/// <summary>
/// Compression types for multiple compressions
/// </summary>
[Flags]
public enum CompressionType : uint
{
/// <summary>
/// Huffmann compression (used on WAVE files only)
/// </summary>
MPQ_COMPRESSION_HUFFMANN = 0x01,
/// <summary>
/// ZLIB compression
/// </summary>
MPQ_COMPRESSION_ZLIB = 0x02,
/// <summary>
/// PKWARE DCL compression
/// </summary>
MPQ_COMPRESSION_PKWARE = 0x08,
/// <summary>
/// BZIP2 compression (added in Warcraft III)
/// </summary>
MPQ_COMPRESSION_BZIP2 = 0x10,
/// <summary>
/// Sparse compression (added in Starcraft 2)
/// </summary>
MPQ_COMPRESSION_SPARSE = 0x20,
/// <summary>
/// IMA ADPCM compression (mono)
/// </summary>
MPQ_COMPRESSION_ADPCM_MONO = 0x40,
/// <summary>
/// IMA ADPCM compression (stereo)
/// </summary>
MPQ_COMPRESSION_ADPCM_STEREO = 0x80,
/// <summary>
/// LZMA compression. Added in Starcraft 2. This value is NOT a combination of flags.
/// </summary>
MPQ_COMPRESSION_LZMA = 0x12,
/// <summary>
/// Same compression
/// </summary>
MPQ_COMPRESSION_NEXT_SAME = 0xFFFFFFFF,
}
[Flags]
public enum FileFlags : uint
{
@@ -57,6 +109,29 @@ namespace BurnOutSharp.Models.MoPaQ
MPQ_FILE_EXISTS = 0x80000000,
}
public enum FormatVersion : ushort
{
/// <summary>
/// Format 1 (up to The Burning Crusade)
/// </summary>
Format1 = 0,
/// <summary>
/// Format 2 (The Burning Crusade and newer)
/// </summary>
Format2 = 1,
/// <summary>
/// Format 3 (WoW - Cataclysm beta or newer)
/// </summary>
Format3 = 2,
/// <summary>
/// Format 4 (WoW - Cataclysm beta or newer)
/// </summary>
Format4 = 3,
}
public enum Locale : short
{
Neutral = 0,