diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml
index 5e52f4a8..3c75ff14 100644
--- a/.github/workflows/build_and_test.yml
+++ b/.github/workflows/build_and_test.yml
@@ -12,7 +12,6 @@ jobs:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- submodules: recursive
- name: Setup .NET
uses: actions/setup-dotnet@v4
diff --git a/.github/workflows/check_pr.yml b/.github/workflows/check_pr.yml
index cccf5f5d..c9e23ecd 100644
--- a/.github/workflows/check_pr.yml
+++ b/.github/workflows/check_pr.yml
@@ -7,8 +7,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- with:
- submodules: recursive
- name: Setup .NET
uses: actions/setup-dotnet@v4
diff --git a/.gitmodules b/.gitmodules
index 9b45d6a2..e69de29b 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,3 +0,0 @@
-[submodule "SabreTools.Serialization/_EXTERNAL/stormlibsharp"]
- path = SabreTools.Serialization/_EXTERNAL/stormlibsharp
- url = https://github.com/robpaveza/stormlibsharp.git
diff --git a/README.MD b/README.MD
index f0180318..a3f96126 100644
--- a/README.MD
+++ b/README.MD
@@ -9,7 +9,7 @@ Find the link to the Nuget package [here](https://www.nuget.org/packages/SabreTo
The following non-project libraries (or ports thereof) are used for file handling:
- [GrindCore.SharpCompress](https://github.com/Nanook/GrindCore.SharpCompress) - Common archive format extraction
-- [StormLibSharp](https://github.com/robpaveza/stormlibsharp) - MoPaQ extraction [Unused in .NET Framework 2.0/3.5/4.0 and non-Windows builds due to Windows-specific libraries]
+- [StormLibSharp](https://github.com/robpaveza/stormlibsharp) - MoPaQ extraction [Unused in non-Windows builds due to Windows-specific libraries]
The following projects have influenced this library:
diff --git a/SabreTools.Serialization/Models/CDROM/DataTrack.cs b/SabreTools.Serialization/Models/CDROM/DataTrack.cs
index 982f0f30..95bf5cbf 100644
--- a/SabreTools.Serialization/Models/CDROM/DataTrack.cs
+++ b/SabreTools.Serialization/Models/CDROM/DataTrack.cs
@@ -16,6 +16,6 @@ namespace SabreTools.Data.Models.CDROM
///
/// ISO9660 volume within the data track
///
- public Volume Volume { get; set; }
+ public Volume Volume { get; set; } = new();
}
}
diff --git a/SabreTools.Serialization/Models/MoPaQ/Constants.cs b/SabreTools.Serialization/Models/MoPaQ/Constants.cs
index 0192e812..02fecae5 100644
--- a/SabreTools.Serialization/Models/MoPaQ/Constants.cs
+++ b/SabreTools.Serialization/Models/MoPaQ/Constants.cs
@@ -79,5 +79,29 @@ namespace SabreTools.Data.Models.MoPaQ
public const uint MPQ_KEY_HASH_TABLE = 0xC3AF3770;
#endregion
+
+ #region Special Entry Names
+
+ ///
+ /// Name of internal listfile
+ ///
+ public const string LISTFILE_NAME = "(listfile)";
+
+ ///
+ /// Name of internal signature
+ ///
+ public const string SIGNATURE_NAME = "(signature)";
+
+ ///
+ /// Name of internal attributes file
+ ///
+ public const string ATTRIBUTES_NAME = "(attributes)";
+
+ ///
+ /// Patch metadata
+ ///
+ public const string PATCH_METADATA_NAME = "(patch_metadata)";
+
+ #endregion
}
}
diff --git a/SabreTools.Serialization/Readers/CDROMVolume.cs b/SabreTools.Serialization/Readers/CDROMVolume.cs
index e16c489e..73874b57 100644
--- a/SabreTools.Serialization/Readers/CDROMVolume.cs
+++ b/SabreTools.Serialization/Readers/CDROMVolume.cs
@@ -58,7 +58,7 @@ namespace SabreTools.Serialization.Readers
///
/// Stream to parse
/// Filled byte[] on success, null on error
- public static byte[]? ParseCDROMSystemArea(Stream data)
+ public static byte[] ParseCDROMSystemArea(Stream data)
{
var systemArea = new byte[Constants.SystemAreaSectors * Constants.MinimumSectorSize];
// Process in sectors
diff --git a/SabreTools.Serialization/SabreTools.Serialization.csproj b/SabreTools.Serialization/SabreTools.Serialization.csproj
index 0132febc..605d3393 100644
--- a/SabreTools.Serialization/SabreTools.Serialization.csproj
+++ b/SabreTools.Serialization/SabreTools.Serialization.csproj
@@ -8,8 +8,6 @@
false
true
latest
-
- CS8600;CS8601;CS8603;CS8604;CS8618;CS8625;CS8634;IL3000
enable
true
snupkg
@@ -43,25 +41,6 @@
net6.0;net7.0;net8.0;net9.0
-
-
-
- $(DefaultItemExcludes);
- **\AssemblyInfo.cs;
- _EXTERNAL\stormlibsharp\lib\**;
- _EXTERNAL\stormlibsharp\src\CascLibSharp\**;
- _EXTERNAL\stormlibsharp\src\TestConsole\**
-
-
-
-
-
-
- $(DefaultItemExcludes);
- _EXTERNAL\**
-
-
-
diff --git a/SabreTools.Serialization/Wrappers/MoPaQ.Extraction.cs b/SabreTools.Serialization/Wrappers/MoPaQ.Extraction.cs
index 5a98ed6b..6b391126 100644
--- a/SabreTools.Serialization/Wrappers/MoPaQ.Extraction.cs
+++ b/SabreTools.Serialization/Wrappers/MoPaQ.Extraction.cs
@@ -1,8 +1,7 @@
using System;
-#if NET452_OR_GREATER || NETCOREAPP
using System.IO;
using StormLibSharp;
-#endif
+using static SabreTools.Data.Models.MoPaQ.Constants;
namespace SabreTools.Serialization.Wrappers
{
@@ -11,7 +10,6 @@ namespace SabreTools.Serialization.Wrappers
///
public bool Extract(string outputDirectory, bool includeDebug)
{
-#if NET452_OR_GREATER || NETCOREAPP
try
{
// Limit use to Windows only
@@ -27,7 +25,7 @@ namespace SabreTools.Serialization.Wrappers
// Try to open the archive and listfile
var mpqArchive = new MpqArchive(Filename, FileAccess.Read);
string? listfile = null;
- MpqFileStream listStream = mpqArchive.OpenFile("(listfile)");
+ MpqFileStream listStream = mpqArchive.OpenFile(LISTFILE_NAME);
// If we can't read the listfile, we just return
if (!listStream.CanRead)
@@ -62,24 +60,19 @@ namespace SabreTools.Serialization.Wrappers
{
mpqArchive.ExtractFile(sub, filename);
}
- catch (System.Exception ex)
+ catch (Exception ex)
{
- if (includeDebug) System.Console.WriteLine(ex);
+ if (includeDebug) Console.WriteLine(ex);
}
}
return true;
}
- catch (System.Exception ex)
+ catch (Exception ex)
{
- if (includeDebug) System.Console.WriteLine(ex);
+ if (includeDebug) Console.WriteLine(ex);
return false;
}
-#else
- Console.WriteLine("Extraction is not supported for this framework!");
- Console.WriteLine();
- return false;
-#endif
}
}
}
diff --git a/SabreTools.Serialization/_EXTERNAL/CascLibSharp/CascApi.cs b/SabreTools.Serialization/_EXTERNAL/CascLibSharp/CascApi.cs
new file mode 100644
index 00000000..0f6568fd
--- /dev/null
+++ b/SabreTools.Serialization/_EXTERNAL/CascLibSharp/CascApi.cs
@@ -0,0 +1,239 @@
+using System;
+using System.ComponentModel;
+using System.IO;
+using System.Reflection;
+using System.Runtime.InteropServices;
+using CascLibSharp.Native;
+
+namespace CascLibSharp
+{
+ internal sealed class CascApi
+ {
+ #region Imported method signature type definitions
+
+ [return: MarshalAs(UnmanagedType.I1)]
+ [UnmanagedFunctionPointer(CallingConvention.Winapi)]
+ internal delegate bool FnCascOpenStorage(
+ [MarshalAs(UnmanagedType.LPTStr)] string szDataPath,
+ uint dwFlags,
+ out CascStorageSafeHandle phStorage);
+
+ [return: MarshalAs(UnmanagedType.I1)]
+ [UnmanagedFunctionPointer(CallingConvention.Winapi)]
+ internal delegate bool FnCascGetStorageInfo(
+ CascStorageSafeHandle hStorage,
+ CascStorageInfoClass infoClass,
+ ref uint pvStorageInfo,
+ IntPtr cbStorageInfo,
+ ref uint pcbLengthNeeded);
+
+ [return: MarshalAs(UnmanagedType.I1)]
+ [UnmanagedFunctionPointer(CallingConvention.Winapi)]
+ internal delegate bool FnCascCloseStorage(IntPtr hStorage);
+
+ [return: MarshalAs(UnmanagedType.I1)]
+ [UnmanagedFunctionPointer(CallingConvention.Winapi)]
+ internal delegate bool FnCascOpenFileByIndexKey(
+ CascStorageSafeHandle hStorage,
+ ref QueryKey pIndexKey,
+ uint dwFlags,
+ out CascStorageFileSafeHandle phFile);
+
+ [return: MarshalAs(UnmanagedType.I1)]
+ [UnmanagedFunctionPointer(CallingConvention.Winapi)]
+ internal delegate bool FnCascOpenFileByEncodingKey(
+ CascStorageSafeHandle hStorage,
+ ref QueryKey pEncodingKey,
+ uint dwFlags,
+ out CascStorageFileSafeHandle phFile);
+
+ [return: MarshalAs(UnmanagedType.I1)]
+ [UnmanagedFunctionPointer(CallingConvention.Winapi)]
+ internal delegate bool FnCascOpenFile(
+ CascStorageSafeHandle hStorage,
+ [MarshalAs(UnmanagedType.LPStr)] string szFileName,
+ uint dwLocale,
+ uint dwFlags,
+ out CascStorageFileSafeHandle phFile);
+
+ [UnmanagedFunctionPointer(CallingConvention.Winapi)]
+ internal delegate uint FnCascGetFileSize(
+ CascStorageFileSafeHandle hFile,
+ out uint pdwFileSizeHigh);
+ [UnmanagedFunctionPointer(CallingConvention.Winapi)]
+ internal delegate uint FnCascSetFilePointer(
+ CascStorageFileSafeHandle hFile,
+ uint lFilePos,
+ ref uint plFilePosHigh,
+ SeekOrigin dwMoveMethod);
+
+ [return: MarshalAs(UnmanagedType.I1)]
+ [UnmanagedFunctionPointer(CallingConvention.Winapi)]
+ internal delegate bool FnCascReadFile(
+ CascStorageFileSafeHandle hFile,
+ IntPtr lpBuffer,
+ uint dwToRead,
+ out uint dwRead);
+
+ [return: MarshalAs(UnmanagedType.I1)]
+ [UnmanagedFunctionPointer(CallingConvention.Winapi)]
+ internal delegate bool FnCascCloseFile(IntPtr hFile);
+
+ [UnmanagedFunctionPointer(CallingConvention.Winapi)]
+ internal delegate CascFileEnumerationSafeHandle FnCascFindFirstFile(
+ CascStorageSafeHandle hStorage,
+ [MarshalAs(UnmanagedType.LPStr)] string szMask,
+ ref CascFindData pFindData,
+ [MarshalAs(UnmanagedType.LPTStr)] string? szListFile);
+
+ [return: MarshalAs(UnmanagedType.I1)]
+ [UnmanagedFunctionPointer(CallingConvention.Winapi)]
+ internal delegate bool FnCascFindNextFile(
+ CascFileEnumerationSafeHandle hFind,
+ ref CascFindData pFindData);
+
+ [return: MarshalAs(UnmanagedType.I1)]
+ [UnmanagedFunctionPointer(CallingConvention.Winapi)]
+ internal delegate bool FnCascFindClose(IntPtr hFind);
+
+ #endregion
+
+ #region Field and method defs
+
+ public readonly FnCascOpenStorage? CascOpenStorage;
+ public readonly FnCascGetStorageInfo? CascGetStorageInfo;
+ public readonly FnCascCloseStorage? CascCloseStorage;
+
+ public readonly FnCascOpenFileByIndexKey? CascOpenFileByIndexKey;
+ public readonly FnCascOpenFileByEncodingKey? CascOpenFileByEncodingKey;
+ public readonly FnCascOpenFile? CascOpenFile;
+
+ public readonly FnCascGetFileSize? CascGetFileSizeBase;
+ public long CascGetFileSize(CascStorageFileSafeHandle hFile)
+ {
+ uint low = CascGetFileSizeBase!(hFile, out uint high);
+ long result = (high << 32) | low;
+
+ return result;
+ }
+ public readonly FnCascSetFilePointer? CascSetFilePointerBase;
+ public long CascSetFilePointer(CascStorageFileSafeHandle hFile, long filePos, SeekOrigin moveMethod)
+ {
+ uint low, high;
+ unchecked
+ {
+ low = (uint)(filePos & 0xffffffff);
+ high = (uint)(((ulong)filePos & 0xffffffff00000000) >> 32);
+ }
+
+ low = CascSetFilePointerBase!(hFile, low, ref high, moveMethod);
+
+ long result = (high << 32) | low;
+
+ return result;
+ }
+ public readonly FnCascReadFile? CascReadFile;
+ public readonly FnCascCloseFile? CascCloseFile;
+
+ public readonly FnCascFindFirstFile? CascFindFirstFile;
+ public readonly FnCascFindNextFile? CascFindNextFile;
+ public readonly FnCascFindClose? CascFindClose;
+
+ #endregion
+
+ internal CascApi(IntPtr hModule)
+ {
+ SetFn(ref CascOpenStorage, hModule, "CascOpenStorage");
+ SetFn(ref CascGetStorageInfo, hModule, "CascGetStorageInfo");
+ SetFn(ref CascCloseStorage, hModule, "CascCloseStorage");
+
+ SetFn(ref CascOpenFileByIndexKey, hModule, "CascOpenFileByIndexKey");
+ SetFn(ref CascOpenFileByEncodingKey, hModule, "CascOpenFileByEncodingKey");
+ SetFn(ref CascOpenFile, hModule, "CascOpenFile");
+ SetFn(ref CascGetFileSizeBase, hModule, "CascGetFileSize");
+ SetFn(ref CascSetFilePointerBase, hModule, "CascSetFilePointer");
+ SetFn(ref CascReadFile, hModule, "CascReadFile");
+ SetFn(ref CascCloseFile, hModule, "CascCloseFile");
+
+ SetFn(ref CascFindFirstFile, hModule, "CascFindFirstFile");
+ SetFn(ref CascFindNextFile, hModule, "CascFindNextFile");
+ SetFn(ref CascFindClose, hModule, "CascFindClose");
+ }
+
+ private static void SetFn(ref T? target, IntPtr hModule, string procName)
+ where T : class
+ {
+ IntPtr procAddr = NativeMethods.GetProcAddress(hModule, procName);
+ if (procAddr == IntPtr.Zero)
+ throw new Win32Exception();
+
+ target = Marshal.GetDelegateForFunctionPointer(procAddr, typeof(T)) as T;
+ }
+
+ private static CascApi Load()
+ {
+ string myPath = Assembly.GetExecutingAssembly().Location;
+ string directory = Path.GetDirectoryName(myPath) ?? string.Empty;
+ string arch = IntPtr.Size == 8 ? "x64" : "x86";
+#if DEBUG
+ string build = "dbg";
+#else
+ string build = "fre";
+#endif
+
+#if NET20 || NET35
+ string mainPath = Path.Combine(directory, Path.Combine("CascLib", Path.Combine(build, Path.Combine(arch, "CascLib.dll"))));
+#else
+ string mainPath = Path.Combine(directory, "CascLib", build, arch, "CascLib.dll");
+#endif
+ if (File.Exists(mainPath))
+ return FromFile(mainPath);
+
+#if NET20 || NET35
+ string alternatePath = Path.Combine(directory, Path.Combine("CascLib", Path.Combine(arch, "CascLib.dll")));
+#else
+ string alternatePath = Path.Combine(directory, "CascLib", arch, "CascLib.dll");
+#endif
+ if (File.Exists(mainPath))
+ return FromFile(alternatePath);
+
+ string localPath = Path.Combine(directory, "CascLib.dll");
+ if (File.Exists(localPath))
+ return FromFile(localPath);
+
+ throw new FileNotFoundException(string.Format("Could not locate a copy of CascLib.dll to load. The following paths were tried:\n\t{0}\n\t{1}\n\t{2}\n\nEnsure that an architecture-appropriate copy of CascLib.dll is included in your project.", mainPath, alternatePath, localPath));
+ }
+
+#if NET20 || NET35
+ private static CascApi? _sharedInstance = null;
+#else
+ private static readonly Lazy _sharedInstance = new(Load);
+#endif
+ public static CascApi Instance
+ {
+ get
+ {
+#if NET20 || NET35
+ if (_sharedInstance == null)
+ _sharedInstance = Load();
+
+ return _sharedInstance;
+#else
+ return _sharedInstance.Value;
+#endif
+ }
+ }
+
+ public static CascApi FromFile(string filePath)
+ {
+ if (!File.Exists(filePath))
+ throw new FileNotFoundException("The CascLib.dll library could not be loaded at the specified path.", filePath);
+
+ IntPtr hMod = NativeMethods.LoadLibraryEx(filePath, IntPtr.Zero, 0);
+ if (hMod == IntPtr.Zero)
+ throw new Win32Exception();
+
+ return new CascApi(hMod);
+ }
+ }
+}
diff --git a/SabreTools.Serialization/_EXTERNAL/CascLibSharp/CascException.cs b/SabreTools.Serialization/_EXTERNAL/CascLibSharp/CascException.cs
new file mode 100644
index 00000000..83a94531
--- /dev/null
+++ b/SabreTools.Serialization/_EXTERNAL/CascLibSharp/CascException.cs
@@ -0,0 +1,51 @@
+using System.ComponentModel;
+using System.Runtime.InteropServices;
+
+namespace CascLibSharp
+{
+ ///
+ /// Represents an exception raised as part of a CASC operation.
+ ///
+ public class CascException : Win32Exception
+ {
+ ///
+ /// Creates a new CascException based on the last Win32 error.
+ ///
+ public CascException()
+ : base(Marshal.GetLastWin32Error())
+ {
+
+ }
+
+ ///
+ /// Creates a new CascException based on the specified Win32 error code.
+ ///
+ /// A Win32 error code.
+ public CascException(int errorCode)
+ : base(errorCode)
+ {
+
+ }
+
+ ///
+ /// Creates a new CascException based on the specified message.
+ ///
+ /// The error message.
+ public CascException(string message)
+ : base(message)
+ {
+
+ }
+
+ ///
+ /// Creates a new CascException based on the specified Win32 error code and custom error message.
+ ///
+ /// A Win32 error code.
+ /// The error message.
+ public CascException(int errorCode, string message)
+ : base(errorCode, message)
+ {
+
+ }
+ }
+}
diff --git a/SabreTools.Serialization/_EXTERNAL/CascLibSharp/CascFileStream.cs b/SabreTools.Serialization/_EXTERNAL/CascLibSharp/CascFileStream.cs
new file mode 100644
index 00000000..3ab5e5f9
--- /dev/null
+++ b/SabreTools.Serialization/_EXTERNAL/CascLibSharp/CascFileStream.cs
@@ -0,0 +1,164 @@
+using System;
+using System.Diagnostics;
+using System.IO;
+using CascLibSharp.Native;
+
+namespace CascLibSharp
+{
+ ///
+ /// Provides a view of the data in a file contained within CASC storage.
+ ///
+ public class CascFileStream : Stream
+ {
+ private readonly CascStorageFileSafeHandle _handle;
+ private readonly CascApi _api;
+
+ internal CascFileStream(CascStorageFileSafeHandle? handle, CascApi? api)
+ {
+ Debug.Assert(handle != null);
+ Debug.Assert(!handle!.IsInvalid);
+ Debug.Assert(api != null);
+
+ _api = api!;
+ _handle = handle;
+ }
+
+ private void AssertValidHandle()
+ {
+ if (_handle == null || _handle.IsInvalid)
+ throw new ObjectDisposedException("CascFileStream");
+ }
+
+ ///
+ /// Gets whether this Stream may be read. Always returns true as long as the Stream has not been disposed.
+ ///
+ public override bool CanRead
+ {
+ get { return _handle != null && !_handle.IsInvalid; }
+ }
+
+ ///
+ /// Gets whether this Stream may seek. Always returns true as long as the Stream has not been disposed.
+ ///
+ public override bool CanSeek
+ {
+ get { return _handle != null && !_handle.IsInvalid; }
+ }
+
+ ///
+ /// Gets whether this Stream may write. Always returns false.
+ ///
+ public override bool CanWrite
+ {
+ get { return false; }
+ }
+
+ ///
+ /// Flushes writes to the backing store. This method always throws because writing is not supported.
+ ///
+ /// Always thrown.
+ public override void Flush()
+ {
+ throw new NotSupportedException();
+ }
+
+ ///
+ /// Gets the length of the Stream.
+ ///
+ /// Thrown if the Stream has been disposed.
+ public override long Length
+ {
+ get
+ {
+ AssertValidHandle();
+ return _api!.CascGetFileSize(_handle);
+ }
+ }
+
+ ///
+ /// Gets or sets the current position within the Stream.
+ ///
+ /// Thrown if the Stream has been disposed.
+ /// Thrown if the attempt to seek fails.
+ public override long Position
+ {
+ get
+ {
+ AssertValidHandle();
+ return _api!.CascSetFilePointer(_handle, 0, SeekOrigin.Current);
+ }
+ set
+ {
+ AssertValidHandle();
+ long result = _api!.CascSetFilePointer(_handle, value, SeekOrigin.Begin);
+ if (result != value)
+ throw new CascException();
+ }
+ }
+
+ ///
+ /// Reads data into the buffer.
+ ///
+ /// The destination into which the data should be read.
+ /// The offset into the buffer to read from the current position of the Stream.
+ /// The number of bytes to read.
+ /// The number of bytes actually read.
+ /// Thrown if the Stream has been disposed.
+ /// Thrown if the CASC provider fails to read.
+ public override unsafe int Read(byte[] buffer, int offset, int count)
+ {
+ AssertValidHandle();
+ long len = Length;
+ if (offset < 0 || offset > len)
+ throw new ArgumentException(nameof(offset));
+ if (count < 0)
+ throw new ArgumentException(nameof(count));
+ if (offset + count > len)
+ throw new ArgumentException(nameof(count));
+
+ uint read = 0;
+ fixed (byte* pBuffer = &buffer[0])
+ {
+ if (!_api.CascReadFile!(_handle, new IntPtr((void*)pBuffer), unchecked((uint)count), out read))
+ throw new CascException();
+ }
+
+ return unchecked((int)read);
+ }
+
+ ///
+ /// Seeks to a specific position within the Stream.
+ ///
+ /// The offset from the specified origin to seek.
+ /// The relative location (beginning, current, or end) from which to seek.
+ /// The new position in the stream.
+ /// Thrown if the Stream has been disposed.
+ public override long Seek(long offset, SeekOrigin origin)
+ {
+ AssertValidHandle();
+ return _api.CascSetFilePointer(_handle, offset, origin);
+ }
+
+ ///
+ /// Sets the length of the Stream. Always throws because writing is not supported.
+ ///
+ /// The new length of the Stream.
+ /// Always thrown.
+ public override void SetLength(long value)
+ {
+ throw new NotSupportedException();
+ }
+
+ ///
+ /// Writes the specified data to the Stream. Always throws because writing is not supported.
+ ///
+ /// The data to write.
+ /// The starting position in the buffer.
+ /// The number of bytes.
+ /// Always thrown.
+ public override void Write(byte[] buffer, int offset, int count)
+ {
+ throw new NotSupportedException();
+ }
+ }
+}
diff --git a/SabreTools.Serialization/_EXTERNAL/CascLibSharp/CascFoundFile.cs b/SabreTools.Serialization/_EXTERNAL/CascLibSharp/CascFoundFile.cs
new file mode 100644
index 00000000..d8c966f0
--- /dev/null
+++ b/SabreTools.Serialization/_EXTERNAL/CascLibSharp/CascFoundFile.cs
@@ -0,0 +1,75 @@
+using System;
+using System.Runtime.InteropServices;
+
+namespace CascLibSharp
+{
+ ///
+ /// Represents a file found in a CASC container search.
+ ///
+ public class CascFoundFile
+ {
+#if NET20 || NET35 || NET40
+ private readonly WeakReference _ownerContext;
+#else
+ private readonly WeakReference _ownerContext;
+#endif
+
+ internal CascFoundFile(string? fileName, IntPtr plainName, byte[] encodingKey, CascLocales locales, long fileSize, CascStorageContext ownerContext)
+ {
+ FileName = fileName;
+ PlainFileName = Marshal.PtrToStringAnsi(plainName);
+ EncodingKey = encodingKey;
+ Locales = locales;
+ FileSize = fileSize;
+
+#if NET20 || NET35 || NET40
+ _ownerContext = new WeakReference(ownerContext);
+#else
+ _ownerContext = new WeakReference(ownerContext);
+#endif
+ }
+
+ ///
+ /// Gets the full path to this file.
+ ///
+ public string? FileName { get; private set; }
+
+ ///
+ /// Gets the plain (no directory-qualified) file name of this file.
+ ///
+ public string? PlainFileName { get; private set; }
+
+ ///
+ /// Gets the CASC encoding key for this file.
+ ///
+ public byte[] EncodingKey { get; private set; }
+
+ ///
+ /// Gets the locales supported by this resource.
+ ///
+ public CascLocales Locales { get; private set; }
+
+ ///
+ /// Gets the length of the file in bytes.
+ ///
+ public long FileSize { get; private set; }
+
+ ///
+ /// Opens the found file for reading.
+ ///
+ /// A CascFileStream, which acts as a Stream for a CASC stored file.
+ public CascFileStream Open()
+ {
+#if NET20 || NET35 || NET40
+ CascStorageContext? context = _ownerContext.Target as CascStorageContext;
+ if (context == null)
+ throw new ObjectDisposedException("The owning context has been closed.");
+#else
+ if (!_ownerContext.TryGetTarget(out CascStorageContext? context) || context == null)
+ throw new ObjectDisposedException("The owning context has been closed.");
+#endif
+
+ return context.OpenFileByEncodingKey(EncodingKey);
+ }
+ }
+}
diff --git a/SabreTools.Serialization/_EXTERNAL/CascLibSharp/CascStorageContext.cs b/SabreTools.Serialization/_EXTERNAL/CascLibSharp/CascStorageContext.cs
new file mode 100644
index 00000000..417cc08f
--- /dev/null
+++ b/SabreTools.Serialization/_EXTERNAL/CascLibSharp/CascStorageContext.cs
@@ -0,0 +1,453 @@
+using System;
+using System.Collections.Generic;
+using CascLibSharp.Native;
+
+namespace CascLibSharp
+{
+ ///
+ /// Represents a CASC storage directory.
+ ///
+ public class CascStorageContext : IDisposable
+ {
+ private readonly CascApi _api;
+ private CascStorageSafeHandle? _handle;
+#if NET20 || NET35
+ private bool? _hasListfile = null;
+ private CascKnownClient? _clientType = null;
+ private long? _fileCount = null;
+ private int? _gameBuild = null;
+#else
+ private readonly Lazy _hasListfile;
+ private readonly Lazy _clientType;
+ private readonly Lazy _fileCount;
+ private readonly Lazy _gameBuild;
+#endif
+
+ ///
+ /// Creates a new CascStorageContext for the specified path.
+ ///
+ /// The path to a game's data directory.
+ /// An example directory is c:\Program Files (x86)\Heroes of the Storm\HeroesData.
+ public CascStorageContext(string dataPath)
+ {
+ _api = CascApi.Instance;
+
+ if (!_api.CascOpenStorage!(dataPath, 0, out _handle) || _handle.IsInvalid)
+ throw new CascException();
+ _handle.Api = _api;
+
+#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER
+ _hasListfile = new Lazy(CheckHasListfile);
+ _clientType = new Lazy(GetClient);
+ _fileCount = new Lazy(GetFileCount);
+ _gameBuild = new Lazy(GetGameBuild);
+#endif
+ }
+
+ ///
+ /// Opens a file by its fully-qualified name.
+ ///
+ /// The name of the file.
+ /// The file's locale (defaults to English-United States).
+ /// A CascFileStream, which implements a Stream.
+ /// Thrown if the CascStorageContext has been disposed.
+ /// Thrown if the file does not exist within the CASC storage container.
+ public CascFileStream OpenFile(string fileName, CascLocales locale = CascLocales.EnUs)
+ {
+ if (_handle == null || _handle.IsInvalid)
+ throw new ObjectDisposedException("CascStorageContext");
+
+ CascStorageFileSafeHandle hFile;
+ if (!_api.CascOpenFile!(_handle, fileName, (uint)locale, 0, out hFile))
+ throw new CascException();
+
+ hFile.Api = _api;
+
+ return new CascFileStream(hFile, _api);
+ }
+
+ ///
+ /// Opens a file by its index key.
+ ///
+ ///
+ /// An index key is a binary representation of a file. I do not know what it comes from; I know that it's used to identify files inside of CASC,
+ /// but I don't know how someone obtains the index key. They are not produced in the public API of CascLib.
+ ///
+ /// The index key to search.
+ /// A CascFileStream, which implements a Stream.
+ /// Thrown if the CascStorageContext has been disposed.
+ /// Thrown if the file does not exist within the CASC storage container.
+ public CascFileStream OpenFileByIndexKey(byte[] indexKey)
+ {
+ if (_handle == null || _handle.IsInvalid)
+ throw new ObjectDisposedException("CascStorageContext");
+
+ using CoTaskMem mem = CoTaskMem.FromBytes(indexKey);
+
+ var qk = new QueryKey();
+ qk.cbData = unchecked((uint)indexKey.Length);
+ qk.pbData = mem.Pointer;
+
+ CascStorageFileSafeHandle hFile;
+ if (!_api.CascOpenFileByIndexKey!(_handle, ref qk, 0, out hFile))
+ throw new CascException();
+
+ hFile.Api = _api;
+
+ return new CascFileStream(hFile, _api);
+ }
+
+ ///
+ /// Opens a file by its encoding key.
+ ///
+ /// A 16-byte key representing the file. Encoding keys may be obtained via .
+ /// A CascFileStream, which implements a Stream.
+ /// Thrown if the CascStorageContext has been disposed.
+ /// Thrown if the file does not exist within the CASC storage container.
+ public CascFileStream OpenFileByEncodingKey(byte[] encodingKey)
+ {
+ if (_handle == null || _handle.IsInvalid)
+ throw new ObjectDisposedException("CascStorageContext");
+
+ using CoTaskMem mem = CoTaskMem.FromBytes(encodingKey);
+
+ var qk = new QueryKey();
+ qk.cbData = unchecked((uint)encodingKey.Length);
+ qk.pbData = mem.Pointer;
+
+ CascStorageFileSafeHandle hFile;
+ if (!_api.CascOpenFileByEncodingKey!(_handle, ref qk, 0, out hFile))
+ throw new CascException();
+
+ hFile.Api = _api;
+
+ return new CascFileStream(hFile, _api);
+ }
+
+ ///
+ /// Searches the files in the CASC container for files that match the specified pattern.
+ ///
+ /// The mask to search. * and ? are valid tokens for substitution.
+ /// A path to a listfile. Required if the CASC container is for World of Warcraft.
+ /// An enumeration of matching file references in the CASC container.
+ public IEnumerable SearchFiles(string mask, string? listFilePath = null)
+ {
+ if (_handle == null || _handle.IsInvalid)
+ throw new ObjectDisposedException("CascStorageContext");
+
+#if NET20 || NET35
+ if (GameClient == CascKnownClient.WorldOfWarcraft && string.IsNullOrEmpty(listFilePath))
+#else
+ if (GameClient == CascKnownClient.WorldOfWarcraft && string.IsNullOrWhiteSpace(listFilePath))
+#endif
+ throw new ArgumentNullException("listFilePath");
+
+ var cfd = new CascFindData();
+ using var handle = _api.CascFindFirstFile!(_handle, mask, ref cfd, listFilePath);
+ if (handle.IsInvalid)
+ yield break;
+
+ handle.Api = _api;
+
+ yield return cfd.ToFoundFile(this);
+
+ while (_api.CascFindNextFile!(handle, ref cfd))
+ {
+ yield return cfd.ToFoundFile(this);
+ }
+ }
+
+ private bool CheckHasListfile()
+ {
+ if (_handle == null || _handle.IsInvalid)
+ throw new ObjectDisposedException("CascStorageContext");
+
+ uint storageInfo = 0, lengthNeeded = 4;
+ if (!_api.CascGetStorageInfo!(_handle, CascStorageInfoClass.Features, ref storageInfo, new IntPtr(4), ref lengthNeeded))
+ throw new CascException();
+
+ CascStorageFeatures features = (CascStorageFeatures)storageInfo;
+#if NET20 || NET35
+ if ((features & CascStorageFeatures.HasListfile) != 0)
+#else
+ if (features.HasFlag(CascStorageFeatures.HasListfile))
+#endif
+ return true;
+
+ return false;
+ }
+
+ private CascKnownClient GetClient()
+ {
+ if (_handle == null || _handle.IsInvalid)
+ throw new ObjectDisposedException("CascStorageContext");
+
+ uint storageInfo = 0, lengthNeeded = 4;
+ if (!_api.CascGetStorageInfo!(_handle, CascStorageInfoClass.GameInfo, ref storageInfo, new IntPtr(4), ref lengthNeeded))
+ throw new CascException();
+
+ CascGameId gameId = (CascGameId)storageInfo;
+
+ return gameId.ToKnownClient();
+ }
+
+ private long GetFileCount()
+ {
+ if (_handle == null || _handle.IsInvalid)
+ throw new ObjectDisposedException("CascStorageContext");
+
+ uint storageInfo = 0, lengthNeeded = 4;
+ if (!_api.CascGetStorageInfo!(_handle, CascStorageInfoClass.Features, ref storageInfo, new IntPtr(4), ref lengthNeeded))
+ throw new CascException();
+
+ return storageInfo;
+ }
+
+ private int GetGameBuild()
+ {
+ if (_handle == null || _handle.IsInvalid)
+ throw new ObjectDisposedException("CascStorageContext");
+
+ uint storageInfo = 0, lengthNeeded = 4;
+ if (!_api.CascGetStorageInfo!(_handle, CascStorageInfoClass.Features, ref storageInfo, new IntPtr(4), ref lengthNeeded))
+ throw new CascException();
+
+ return unchecked((int)storageInfo);
+ }
+
+ ///
+ /// Gets whether the CASC container has a listfile or if one must be supplied while searching.
+ ///
+ public bool HasListfile
+ {
+ get
+ {
+#if NET20 || NET35
+ if (_hasListfile == null)
+ _hasListfile = CheckHasListfile();
+#endif
+ return _hasListfile.Value;
+ }
+ }
+
+ ///
+ /// Gets the number of files in the CASC container.
+ ///
+ public long FileCount
+ {
+ get
+ {
+#if NET20 || NET35
+ if (_fileCount == null)
+ _fileCount = GetFileCount();
+#endif
+ return _fileCount.Value;
+ }
+ }
+
+ ///
+ /// Gets the build number of the game.
+ ///
+ public int GameBuild
+ {
+ get
+ {
+#if NET20 || NET35
+ if (_gameBuild == null)
+ _gameBuild = GetGameBuild();
+#endif
+ return _gameBuild.Value;
+ }
+ }
+
+ ///
+ /// Gets the game client of the container, if it can be determined.
+ ///
+ public CascKnownClient GameClient
+ {
+ get
+ {
+#if NET20 || NET35
+ if (_clientType == null)
+ _clientType = GetClient();
+#endif
+ return _clientType.Value;
+ }
+ }
+
+ #region IDisposable implementation
+
+ ///
+ /// Finalizes the storage context.
+ ///
+ ~CascStorageContext()
+ {
+ Dispose(false);
+ }
+
+ ///
+ /// Disposes the object.
+ ///
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ ///
+ /// Disposes the object, cleaning up the unmanaged objects.
+ ///
+ /// True if this is being called via the Dispose() method; false if it's being called by the finalizer.
+ protected virtual void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ if (_handle != null && !_handle.IsInvalid)
+ {
+ _handle.Close();
+ _handle = null;
+ }
+ }
+ }
+
+ #endregion
+ }
+
+ ///
+ /// Known locales supported by CASC.
+ ///
+ [Flags]
+ public enum CascLocales
+ {
+ ///
+ /// All available locales.
+ ///
+ All = -1,
+
+ ///
+ /// No locales.
+ ///
+ ///
+ None = 0,
+ ///
+ /// Unknown
+ ///
+ Unknown1 = 1,
+
+ ///
+ /// English, United States
+ ///
+ EnUs = 2,
+
+ ///
+ /// Korean, South Korea
+ ///
+ KoKr = 4,
+
+ ///
+ /// Reserved (unknown)
+ ///
+ Reserved = 8,
+
+ ///
+ /// French, France
+ ///
+ FrFr = 0x10,
+
+ ///
+ /// German, Germany
+ ///
+ DeDe = 0x20,
+
+ ///
+ /// Chinese, China
+ ///
+ ZhCn = 0x40,
+
+ ///
+ /// Spanish, Spain
+ ///
+ EsEs = 0x80,
+
+ ///
+ /// Chinese, Taiwan
+ ///
+ ZhTw = 0x100,
+
+ ///
+ /// English, Great Britain
+ ///
+ EnGb = 0x200,
+
+ ///
+ /// English, China
+ ///
+ EnCn = 0x400,
+
+ ///
+ /// English, Taiwan
+ ///
+ EnTw = 0x800,
+
+ ///
+ /// Spanish, Mexico
+ ///
+ EsMx = 0x1000,
+
+ ///
+ /// Russian, Russia
+ ///
+ RuRu = 0x2000,
+
+ ///
+ /// Portuguese, Brazil
+ ///
+ PtBr = 0x4000,
+
+ ///
+ /// Italian, Italy
+ ///
+ ItIt = 0x8000,
+
+ ///
+ /// Portuguese, Portugal
+ ///
+ PtPt = 0x10000,
+ }
+
+ ///
+ /// Known clients supporting CASC
+ ///
+ public enum CascKnownClient
+ {
+ ///
+ /// The game client was unrecognized.
+ ///
+ Unknown = -1,
+
+ ///
+ /// Heroes of the Storm
+ ///
+ HeroesOfTheStorm = 0,
+
+ ///
+ /// Diablo 3
+ ///
+ Diablo3 = 1,
+
+ ///
+ /// World of Warcraft
+ ///
+ WorldOfWarcraft = 2,
+
+ ///
+ /// Overwatch
+ ///
+ Overwatch = 3,
+
+ ///
+ /// Starcraft 2
+ ///
+ Starcraft2 = 4,
+ }
+}
diff --git a/SabreTools.Serialization/_EXTERNAL/CascLibSharp/Native/CascFileEnumerationSafeHandle.cs b/SabreTools.Serialization/_EXTERNAL/CascLibSharp/Native/CascFileEnumerationSafeHandle.cs
new file mode 100644
index 00000000..41ea1269
--- /dev/null
+++ b/SabreTools.Serialization/_EXTERNAL/CascLibSharp/Native/CascFileEnumerationSafeHandle.cs
@@ -0,0 +1,29 @@
+using System;
+using Microsoft.Win32.SafeHandles;
+
+namespace CascLibSharp.Native
+{
+ internal class CascFileEnumerationSafeHandle : SafeHandleZeroOrMinusOneIsInvalid
+ {
+ public CascFileEnumerationSafeHandle()
+ : base(true) { }
+
+ public CascFileEnumerationSafeHandle(IntPtr handle)
+ : this()
+ {
+ SetHandle(handle);
+ }
+
+ protected override bool ReleaseHandle()
+ {
+ var api = Api ?? CascApi.Instance;
+ return api.CascFindClose!(DangerousGetHandle());
+ }
+
+ internal CascApi? Api
+ {
+ get;
+ set;
+ }
+ }
+}
diff --git a/SabreTools.Serialization/_EXTERNAL/CascLibSharp/Native/CascStorageFileSafeHandle.cs b/SabreTools.Serialization/_EXTERNAL/CascLibSharp/Native/CascStorageFileSafeHandle.cs
new file mode 100644
index 00000000..c72cc2da
--- /dev/null
+++ b/SabreTools.Serialization/_EXTERNAL/CascLibSharp/Native/CascStorageFileSafeHandle.cs
@@ -0,0 +1,32 @@
+using System;
+using Microsoft.Win32.SafeHandles;
+
+namespace CascLibSharp.Native
+{
+ internal class CascStorageFileSafeHandle : SafeHandleZeroOrMinusOneIsInvalid
+ {
+ public CascStorageFileSafeHandle()
+ : base(true)
+ {
+
+ }
+
+ public CascStorageFileSafeHandle(IntPtr handle)
+ : this()
+ {
+ SetHandle(handle);
+ }
+
+ protected override bool ReleaseHandle()
+ {
+ var api = Api ?? CascApi.Instance;
+ return api.CascCloseFile!(DangerousGetHandle());
+ }
+
+ internal CascApi? Api
+ {
+ get;
+ set;
+ }
+ }
+}
diff --git a/SabreTools.Serialization/_EXTERNAL/CascLibSharp/Native/CascStorageSafeHandle.cs b/SabreTools.Serialization/_EXTERNAL/CascLibSharp/Native/CascStorageSafeHandle.cs
new file mode 100644
index 00000000..1360e96a
--- /dev/null
+++ b/SabreTools.Serialization/_EXTERNAL/CascLibSharp/Native/CascStorageSafeHandle.cs
@@ -0,0 +1,32 @@
+using System;
+using Microsoft.Win32.SafeHandles;
+
+namespace CascLibSharp.Native
+{
+ internal class CascStorageSafeHandle : SafeHandleZeroOrMinusOneIsInvalid
+ {
+ public CascStorageSafeHandle()
+ : base(true)
+ {
+
+ }
+
+ public CascStorageSafeHandle(IntPtr handle)
+ : this()
+ {
+ SetHandle(handle);
+ }
+
+ protected override bool ReleaseHandle()
+ {
+ var api = Api ?? CascApi.Instance;
+ return api.CascCloseStorage!(DangerousGetHandle());
+ }
+
+ internal CascApi? Api
+ {
+ get;
+ set;
+ }
+ }
+}
diff --git a/SabreTools.Serialization/_EXTERNAL/CascLibSharp/Native/CoTaskMem.cs b/SabreTools.Serialization/_EXTERNAL/CascLibSharp/Native/CoTaskMem.cs
new file mode 100644
index 00000000..a65eb891
--- /dev/null
+++ b/SabreTools.Serialization/_EXTERNAL/CascLibSharp/Native/CoTaskMem.cs
@@ -0,0 +1,60 @@
+using System;
+using System.Runtime.InteropServices;
+
+namespace CascLibSharp.Native
+{
+ internal sealed class CoTaskMem : IDisposable
+ {
+ private IntPtr _mem;
+ private int _size;
+
+ public CoTaskMem(int size)
+ {
+ _mem = Marshal.AllocCoTaskMem(size);
+ if (_mem == IntPtr.Zero)
+ throw new OutOfMemoryException();
+
+ _size = size;
+ }
+
+ public static CoTaskMem FromBytes(byte[] b)
+ {
+ var result = new CoTaskMem(b.Length);
+ Marshal.Copy(b, 0, result._mem, b.Length);
+
+ return result;
+ }
+
+ public IntPtr Pointer
+ {
+ get
+ {
+ if (_mem == IntPtr.Zero)
+ throw new ObjectDisposedException("CoTaskMem");
+
+ return _mem;
+ }
+ }
+
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ private void Dispose(bool disposing)
+ {
+ if (_mem != IntPtr.Zero)
+ {
+ Marshal.FreeCoTaskMem(_mem);
+ _mem = IntPtr.Zero;
+ _size = 0;
+ }
+ }
+
+ ~CoTaskMem()
+ {
+ Dispose(false);
+ }
+ }
+}
diff --git a/SabreTools.Serialization/_EXTERNAL/CascLibSharp/Native/NativeMethods.cs b/SabreTools.Serialization/_EXTERNAL/CascLibSharp/Native/NativeMethods.cs
new file mode 100644
index 00000000..78069085
--- /dev/null
+++ b/SabreTools.Serialization/_EXTERNAL/CascLibSharp/Native/NativeMethods.cs
@@ -0,0 +1,116 @@
+using System;
+using System.Collections.Generic;
+using System.Runtime.InteropServices;
+
+namespace CascLibSharp.Native
+{
+ internal static class NativeMethods
+ {
+ [DllImport("kernel32", SetLastError = true)]
+ public static extern IntPtr GetProcAddress(IntPtr hModule, string procName);
+
+ [DllImport("kernel32", SetLastError = true)]
+ public static extern IntPtr LoadLibraryEx(string lpFileName, IntPtr hFile, uint dwFlags);
+ }
+
+ internal enum CascStorageInfoClass
+ {
+ FileCount,
+ Features,
+ GameInfo,
+ GameBuild,
+ }
+
+ internal enum CascStorageFeatures
+ {
+ None = 0x0,
+ HasListfile = 0x1,
+ }
+
+ /*
+ * #define CASC_GAME_HOTS 0x00010000 // Heroes of the Storm
+#define CASC_GAME_WOW6 0x00020000 // World of Warcraft - Warlords of Draenor
+#define CASC_GAME_DIABLO3 0x00030000 // Diablo 3 since PTR 2.2.0
+#define CASC_GAME_OVERWATCH 0x00040000 // Overwatch since PTR 24919*
+ */
+ internal enum CascGameId
+ {
+ Hots = 0x00010000,
+ Wow6 = 0x00020000,
+ Diablo3 = 0x00030000,
+ Overwatch = 0x00040000,
+ Starcraft2 = 0x00050000,
+ }
+
+ internal static class GameConverterExtensions
+ {
+ private static readonly Dictionary GameClientMap = new()
+ {
+ { CascGameId.Hots, CascKnownClient.HeroesOfTheStorm },
+ { CascGameId.Wow6, CascKnownClient.WorldOfWarcraft },
+ { CascGameId.Diablo3, CascKnownClient.Diablo3 },
+ { CascGameId.Overwatch, CascKnownClient.Overwatch },
+ { CascGameId.Starcraft2, CascKnownClient.Starcraft2 },
+ };
+
+ private static readonly Dictionary ClientGameMap = new()
+ {
+ { CascKnownClient.HeroesOfTheStorm, CascGameId.Hots },
+ { CascKnownClient.WorldOfWarcraft, CascGameId.Wow6 },
+ { CascKnownClient.Diablo3, CascGameId.Diablo3 },
+ { CascKnownClient.Overwatch, CascGameId.Overwatch },
+ { CascKnownClient.Starcraft2, CascGameId.Starcraft2 },
+ };
+
+ public static CascKnownClient ToKnownClient(this CascGameId gameId)
+ {
+ if (!GameClientMap.TryGetValue(gameId, out CascKnownClient result))
+ result = CascKnownClient.Unknown;
+
+ return result;
+ }
+
+ public static CascGameId ToGameId(this CascKnownClient knownClient)
+ {
+ if (!ClientGameMap.TryGetValue(knownClient, out CascGameId result))
+ throw new ArgumentException("Invalid client.");
+
+ return result;
+ }
+ }
+ internal struct QueryKey
+ {
+ public IntPtr pbData;
+ public uint cbData;
+ }
+
+ internal unsafe struct CascFindData
+ {
+ const int MAX_PATH = 260;
+
+ public fixed byte szFileName[MAX_PATH];
+ public IntPtr szPlainName;
+ //public ulong FileNameHash;
+ public fixed byte EncodingKey[16];
+ //public uint dwPackageIndex;
+ public uint dwLocaleFlags;
+ public uint dwFileSize;
+
+ public unsafe CascFoundFile ToFoundFile(CascStorageContext ownerContext)
+ {
+ string? fileName = null;
+ fixed (void* pFileName = szFileName)
+ {
+ fileName = Marshal.PtrToStringAnsi(new IntPtr(pFileName));
+ }
+
+ byte[] encodingKey = new byte[16];
+ fixed (void* pEncodingKey = EncodingKey)
+ {
+ Marshal.Copy(new IntPtr(pEncodingKey), encodingKey, 0, 16);
+ }
+
+ return new CascFoundFile(fileName, szPlainName, encodingKey, (CascLocales)dwLocaleFlags, dwFileSize, ownerContext);
+ }
+ }
+}
diff --git a/SabreTools.Serialization/_EXTERNAL/README.MD b/SabreTools.Serialization/_EXTERNAL/README.MD
index 74cbca55..06c3af9a 100644
--- a/SabreTools.Serialization/_EXTERNAL/README.MD
+++ b/SabreTools.Serialization/_EXTERNAL/README.MD
@@ -4,4 +4,7 @@ This directory contains multiple external libraries. Here is the status of each:
| Directory | Library | Status |
| --------- | ------- | ------ |
-| stormlibsharp | [stormlibsharp](https://github.com/robpaveza/stormlibsharp) | External submodule |
+| CascLibSharp | [stormlibsharp](https://github.com/robpaveza/stormlibsharp) | Direct code duplicate |
+| StormLibSharp | [stormlibsharp](https://github.com/robpaveza/stormlibsharp) | Direct code duplicate |
+
+**CascLibSharp** and **StormLibSharp** have multiple changes made to accomodate modern .NET versions. The original code is MIT licensed.
diff --git a/SabreTools.Serialization/_EXTERNAL/StormLibSharp/MpqArchive.cs b/SabreTools.Serialization/_EXTERNAL/StormLibSharp/MpqArchive.cs
new file mode 100644
index 00000000..9817fef9
--- /dev/null
+++ b/SabreTools.Serialization/_EXTERNAL/StormLibSharp/MpqArchive.cs
@@ -0,0 +1,393 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.IO;
+using StormLibSharp.Native;
+
+namespace StormLibSharp
+{
+ public class MpqArchive : IDisposable
+ {
+ private MpqArchiveSafeHandle? _handle;
+ private readonly List _openFiles = [];
+ private readonly FileAccess _accessType;
+ private readonly List _compactCallbacks = [];
+ private SFILE_COMPACT_CALLBACK? _compactCallback;
+
+ #region Constructors / Factories
+
+ public MpqArchive(string filePath, FileAccess accessType)
+ {
+ _accessType = accessType;
+
+ SFileOpenArchiveFlags flags = SFileOpenArchiveFlags.TypeIsFile;
+ if (accessType == FileAccess.Read)
+ flags |= SFileOpenArchiveFlags.AccessReadOnly;
+ else
+ flags |= SFileOpenArchiveFlags.AccessReadWriteShare;
+
+ // constant 2 = SFILE_OPEN_HARD_DISK_FILE
+ if (!NativeMethods.SFileOpenArchive(filePath, 2, flags, out _handle))
+ throw new Win32Exception(); // Implicitly calls GetLastError
+ }
+
+ private MpqArchive(string filePath, MpqArchiveVersion version, MpqFileStreamAttributes listfileAttributes, MpqFileStreamAttributes attributesFileAttributes, int maxFileCount)
+ {
+ if (maxFileCount < 0)
+ throw new ArgumentException("maxFileCount");
+
+ SFileOpenArchiveFlags flags = SFileOpenArchiveFlags.TypeIsFile | SFileOpenArchiveFlags.AccessReadWriteShare;
+ flags |= (SFileOpenArchiveFlags)version;
+
+ //SFILE_CREATE_MPQ create = new SFILE_CREATE_MPQ()
+ //{
+ // cbSize = unchecked((uint)Marshal.SizeOf(typeof(SFILE_CREATE_MPQ))),
+ // dwMaxFileCount = unchecked((uint)maxFileCount),
+ // dwMpqVersion = (uint)version,
+ // dwFileFlags1 = (uint)listfileAttributes,
+ // dwFileFlags2 = (uint)attributesFileAttributes,
+ // dwStreamFlags = (uint)flags,
+ //};
+
+ //if (!NativeMethods.SFileCreateArchive2(filePath, ref create, out _handle))
+ // throw new Win32Exception();
+ if (!NativeMethods.SFileCreateArchive(filePath, (uint)flags, int.MaxValue, out _handle))
+ throw new Win32Exception();
+ }
+
+ public static MpqArchive CreateNew(string mpqPath, MpqArchiveVersion version)
+ {
+ return CreateNew(mpqPath, version, MpqFileStreamAttributes.None, MpqFileStreamAttributes.None, int.MaxValue);
+ }
+
+ public static MpqArchive CreateNew(string mpqPath, MpqArchiveVersion version, MpqFileStreamAttributes listfileAttributes,
+ MpqFileStreamAttributes attributesFileAttributes, int maxFileCount)
+ {
+ return new MpqArchive(mpqPath, version, listfileAttributes, attributesFileAttributes, maxFileCount);
+ }
+
+ #endregion
+
+ #region Properties
+
+ // TODO: Move to common location.
+ // This is a global setting, not per-archive setting.
+
+ //public int Locale
+ //{
+ // get
+ // {
+ // throw new NotImplementedException();
+ // }
+ // set
+ // {
+ // throw new NotImplementedException();
+ // }
+ //}
+
+ public long MaxFileCount
+ {
+ get
+ {
+ VerifyHandle();
+ return NativeMethods.SFileGetMaxFileCount(_handle);
+ }
+ set
+ {
+ if (value < 0 || value > uint.MaxValue)
+ throw new ArgumentException(nameof(value));
+
+ VerifyHandle();
+
+ if (!NativeMethods.SFileSetMaxFileCount(_handle, unchecked((uint)value)))
+ throw new Win32Exception();
+ }
+ }
+
+ private void VerifyHandle()
+ {
+ if (_handle == null || _handle.IsInvalid)
+ throw new ObjectDisposedException("MpqArchive");
+ }
+
+ public bool IsPatchedArchive
+ {
+ get
+ {
+ VerifyHandle();
+ return NativeMethods.SFileIsPatchedArchive(_handle);
+ }
+ }
+
+ #endregion
+
+ public void Flush()
+ {
+ VerifyHandle();
+ if (!NativeMethods.SFileFlushArchive(_handle))
+ throw new Win32Exception();
+ }
+
+ public int AddListFile(string listfileContents)
+ {
+ VerifyHandle();
+ return NativeMethods.SFileAddListFile(_handle, listfileContents);
+ }
+
+ public void AddFileFromDisk(string filePath, string archiveName)
+ {
+ VerifyHandle();
+
+ if (!NativeMethods.SFileAddFile(_handle, filePath, archiveName, 0))
+ throw new Win32Exception();
+ }
+
+ public void Compact(string listfile)
+ {
+ VerifyHandle();
+ if (!NativeMethods.SFileCompactArchive(_handle, listfile, false))
+ throw new Win32Exception();
+ }
+
+ private void _OnCompact(IntPtr pvUserData, uint dwWorkType, ulong bytesProcessed, ulong totalBytes)
+ {
+ var args = new MpqArchiveCompactingEventArgs(dwWorkType, bytesProcessed, totalBytes);
+ OnCompacting(args);
+ }
+
+ protected virtual void OnCompacting(MpqArchiveCompactingEventArgs e)
+ {
+ foreach (var cb in _compactCallbacks)
+ {
+ cb(this, e);
+ }
+ }
+
+ public event MpqArchiveCompactingEventHandler Compacting
+ {
+ add
+ {
+ VerifyHandle();
+ _compactCallback = _OnCompact;
+ if (!NativeMethods.SFileSetCompactCallback(_handle, _compactCallback, IntPtr.Zero))
+ throw new Win32Exception();
+
+ _compactCallbacks.Add(value);
+ }
+ remove
+ {
+ _compactCallbacks.Remove(value);
+
+ VerifyHandle();
+ if (_compactCallbacks.Count == 0)
+ {
+ if (!NativeMethods.SFileSetCompactCallback(_handle, null, IntPtr.Zero))
+ {
+ // Don't do anything here. Remove shouldn't fail hard.
+ }
+ }
+ }
+ }
+
+ // TODO: Determine if SFileGetAttributes/SFileSetAttributes/SFileUpdateFileAttributes deserves a projection.
+ // It's unclear - these seem to affect the (attributes) file but I can't figure out exactly what that means.
+
+ public void AddPatchArchive(string patchPath)
+ {
+ VerifyHandle();
+
+ if (!NativeMethods.SFileOpenPatchArchive(_handle, patchPath, null, 0))
+ throw new Win32Exception();
+ }
+
+ public void AddPatchArchives(IEnumerable patchPaths)
+ {
+ if (patchPaths == null)
+ throw new ArgumentNullException("patchPaths");
+
+ VerifyHandle();
+
+ foreach (string path in patchPaths)
+ {
+ // Don't sublet to AddPatchArchive to avoid having to repeatedly call VerifyHandle()
+ if (!NativeMethods.SFileOpenPatchArchive(_handle, path, null, 0))
+ throw new Win32Exception();
+ }
+ }
+
+ public bool HasFile(string fileToFind)
+ {
+ VerifyHandle();
+
+ return NativeMethods.SFileHasFile(_handle, fileToFind);
+ }
+
+ public MpqFileStream OpenFile(string fileName)
+ {
+ VerifyHandle();
+
+ if (!NativeMethods.SFileOpenFileEx(_handle, fileName, 0, out MpqFileSafeHandle fileHandle))
+ throw new Win32Exception();
+
+ var fs = new MpqFileStream(fileHandle, _accessType, this);
+ _openFiles.Add(fs);
+ return fs;
+ }
+
+ public void ExtractFile(string fileToExtract, string destinationPath)
+ {
+ VerifyHandle();
+
+ if (!NativeMethods.SFileExtractFile(_handle, fileToExtract, destinationPath, 0))
+ throw new Win32Exception();
+ }
+
+ public MpqFileVerificationResults VerifyFile(string fileToVerify)
+ {
+ VerifyHandle();
+
+ return (MpqFileVerificationResults)NativeMethods.SFileVerifyFile(_handle, fileToVerify, 0);
+ }
+
+ // TODO: Consider SFileVerifyRawData
+
+ public MpqArchiveVerificationResult VerifyArchive()
+ {
+ VerifyHandle();
+
+ return (MpqArchiveVerificationResult)NativeMethods.SFileVerifyArchive(_handle);
+ }
+
+
+ #region IDisposable implementation
+
+ public void Dispose()
+ {
+ Dispose(true);
+ }
+
+ ~MpqArchive()
+ {
+ Dispose(false);
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ // Release owned files first.
+ foreach (var file in _openFiles)
+ {
+ file.Dispose();
+ }
+
+ _openFiles.Clear();
+
+ // Release
+ if (_handle != null && !_handle.IsInvalid)
+ {
+ _handle.Close();
+ _handle = null;
+ }
+ }
+ }
+
+ internal void RemoveOwnedFile(MpqFileStream file)
+ {
+ _openFiles.Remove(file);
+ }
+
+ #endregion
+ }
+
+ public enum MpqArchiveVersion
+ {
+ Version1 = 0,
+ Version2 = 0x01000000,
+ Version3 = 0x02000000,
+ Version4 = 0x03000000,
+ }
+
+ [Flags]
+ public enum MpqFileStreamAttributes
+ {
+ None = 0x0,
+ }
+
+ [Flags]
+ public enum MpqFileVerificationResults
+ {
+ ///
+ /// There were no errors with the file.
+ ///
+ Verified = 0,
+ ///
+ /// Failed to open the file
+ ///
+ Error = 0x1,
+ ///
+ /// Failed to read all data from the file
+ ///
+ ReadError = 0x2,
+ ///
+ /// File has sector CRC
+ ///
+ HasSectorCrc = 0x4,
+ ///
+ /// Sector CRC check failed
+ ///
+ SectorCrcError = 0x8,
+ ///
+ /// File has CRC32
+ ///
+ HasChecksum = 0x10,
+ ///
+ /// CRC32 check failed
+ ///
+ ChecksumError = 0x20,
+ ///
+ /// File has data MD5
+ ///
+ HasMd5 = 0x40,
+ ///
+ /// MD5 check failed
+ ///
+ Md5Error = 0x80,
+ ///
+ /// File has raw data MD5
+ ///
+ HasRawMd5 = 0x100,
+ ///
+ /// Raw MD5 check failed
+ ///
+ RawMd5Error = 0x200,
+ }
+
+ public enum MpqArchiveVerificationResult
+ {
+ ///
+ /// There is no signature in the MPQ
+ ///
+ NoSignature = 0,
+ ///
+ /// There was an error during verifying signature (like no memory)
+ ///
+ VerificationFailed = 1,
+ ///
+ /// There is a weak signature and sign check passed
+ ///
+ WeakSignatureVerified = 2,
+ ///
+ /// There is a weak signature but sign check failed
+ ///
+ WeakSignatureFailed = 3,
+ ///
+ /// There is a strong signature and sign check passed
+ ///
+ StrongSignatureVerified = 4,
+ ///
+ /// There is a strong signature but sign check failed
+ ///
+ StrongSignatureFailed = 5,
+ }
+}
diff --git a/SabreTools.Serialization/_EXTERNAL/StormLibSharp/MpqArchiveCompactingEventArgs.cs b/SabreTools.Serialization/_EXTERNAL/StormLibSharp/MpqArchiveCompactingEventArgs.cs
new file mode 100644
index 00000000..9a88fa15
--- /dev/null
+++ b/SabreTools.Serialization/_EXTERNAL/StormLibSharp/MpqArchiveCompactingEventArgs.cs
@@ -0,0 +1,46 @@
+using System;
+
+namespace StormLibSharp
+{
+ public delegate void MpqArchiveCompactingEventHandler(MpqArchive sender, MpqArchiveCompactingEventArgs e);
+
+ public class MpqArchiveCompactingEventArgs : EventArgs
+ {
+ internal MpqArchiveCompactingEventArgs(uint dwWorkType, ulong processed, ulong total)
+ {
+ unchecked
+ {
+ WorkType = (MpqCompactingWorkType)dwWorkType;
+ BytesProcessed = (long)processed;
+ TotalBytes = (long)total;
+ }
+ }
+
+ public MpqCompactingWorkType WorkType
+ {
+ get;
+ private set;
+ }
+
+ public long BytesProcessed
+ {
+ get;
+ private set;
+ }
+
+ public long TotalBytes
+ {
+ get;
+ private set;
+ }
+ }
+
+ public enum MpqCompactingWorkType
+ {
+ CheckingFiles = 1,
+ CheckingHashTable = 2,
+ CopyingNonMpqData = 3,
+ CompactingFiles = 4,
+ ClosingArchive = 5,
+ }
+}
diff --git a/SabreTools.Serialization/_EXTERNAL/StormLibSharp/MpqFileStream.cs b/SabreTools.Serialization/_EXTERNAL/StormLibSharp/MpqFileStream.cs
new file mode 100644
index 00000000..c1bfa1fc
--- /dev/null
+++ b/SabreTools.Serialization/_EXTERNAL/StormLibSharp/MpqFileStream.cs
@@ -0,0 +1,178 @@
+using System;
+using System.ComponentModel;
+using System.IO;
+using System.Threading;
+using StormLibSharp.Native;
+
+namespace StormLibSharp
+{
+ public class MpqFileStream : Stream
+ {
+ private MpqFileSafeHandle? _handle;
+ private readonly FileAccess _accessType;
+ private MpqArchive? _owner;
+
+ internal MpqFileStream(MpqFileSafeHandle handle, FileAccess accessType, MpqArchive owner)
+ {
+ _handle = handle;
+ _accessType = accessType;
+ _owner = owner;
+ }
+
+ private void VerifyHandle()
+ {
+ if (_handle == null || _handle.IsInvalid || _handle.IsClosed)
+ throw new ObjectDisposedException("MpqFileStream");
+ }
+
+ public override bool CanRead
+ {
+ get { VerifyHandle(); return true; }
+ }
+
+ public override bool CanSeek
+ {
+ get { VerifyHandle(); return true; }
+ }
+
+ public override bool CanWrite
+ {
+ get { VerifyHandle(); return _accessType != FileAccess.Read; }
+ }
+
+ public override void Flush()
+ {
+ VerifyHandle();
+
+ _owner?.Flush();
+ }
+
+ public override long Length
+ {
+ get
+ {
+ VerifyHandle();
+
+ uint high = 0;
+ uint low = NativeMethods.SFileGetFileSize(_handle, ref high);
+
+ ulong val = (high << 32) | low;
+ return unchecked((long)val);
+ }
+ }
+
+ public override long Position
+ {
+ get
+ {
+ VerifyHandle();
+
+ return NativeMethods.SFileGetFilePointer(_handle);
+ }
+ set
+ {
+ Seek(value, SeekOrigin.Begin);
+ }
+ }
+
+ public override unsafe int Read(byte[] buffer, int offset, int count)
+ {
+ if (buffer == null)
+ throw new ArgumentNullException(nameof(buffer));
+ if (offset > buffer.Length || (offset + count) > buffer.Length)
+ throw new ArgumentException();
+ if (count < 0)
+ throw new ArgumentOutOfRangeException(nameof(count));
+
+ VerifyHandle();
+
+ bool success;
+ uint read;
+ fixed (byte* pb = &buffer[offset])
+ {
+ NativeOverlapped overlapped = default(NativeOverlapped);
+ success = NativeMethods.SFileReadFile(_handle, new IntPtr(pb), unchecked((uint)count), out read, ref overlapped);
+ }
+
+ if (!success)
+ {
+ int lastError = Win32Methods.GetLastError();
+ if (lastError != 38) // EOF
+ throw new Win32Exception(lastError);
+ }
+
+ return unchecked((int)read);
+ }
+
+ public override long Seek(long offset, SeekOrigin origin)
+ {
+ VerifyHandle();
+
+ uint low = unchecked((uint)(offset & 0xffffffffu));
+ uint high = unchecked((uint)(offset >> 32));
+ return NativeMethods.SFileSetFilePointer(_handle, low, ref high, (uint)origin);
+ }
+
+ public override void SetLength(long value)
+ {
+ throw new NotSupportedException();
+ }
+
+ public override unsafe void Write(byte[] buffer, int offset, int count)
+ {
+ VerifyHandle();
+
+ if (buffer == null)
+ throw new ArgumentNullException(nameof(buffer));
+ if (offset > buffer.Length || (offset + count) > buffer.Length)
+ throw new ArgumentException();
+ if (count < 0)
+ throw new ArgumentOutOfRangeException(nameof(count));
+
+ VerifyHandle();
+
+ bool success;
+ fixed (byte* pb = &buffer[offset])
+ {
+ success = NativeMethods.SFileWriteFile(_handle, new IntPtr(pb), unchecked((uint)count), 0u);
+ }
+
+ if (!success)
+ throw new Win32Exception();
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+
+ if (disposing)
+ {
+ if (_handle != null && !_handle.IsInvalid)
+ {
+ _handle.Close();
+ _handle = null;
+ }
+
+ _owner?.RemoveOwnedFile(this);
+ _owner = null;
+ }
+ }
+
+ // TODO: Seems like the right place for SFileGetFileInfo, but will need to determine
+ // what value add these features have except for sophisticated debugging purposes
+ // (like in Ladis' MPQ Editor app).
+
+ public int ChecksumCrc32
+ {
+ get
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ public byte[] GetMd5Hash()
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/SabreTools.Serialization/_EXTERNAL/StormLibSharp/Native/Callbacks.cs b/SabreTools.Serialization/_EXTERNAL/StormLibSharp/Native/Callbacks.cs
new file mode 100644
index 00000000..2d257781
--- /dev/null
+++ b/SabreTools.Serialization/_EXTERNAL/StormLibSharp/Native/Callbacks.cs
@@ -0,0 +1,14 @@
+using System;
+using System.Runtime.InteropServices;
+
+namespace StormLibSharp.Native
+{
+ [UnmanagedFunctionPointer(CallingConvention.Winapi)]
+ internal delegate void SFILE_DOWNLOAD_CALLBACK(IntPtr pvUserData, ulong byteOffset, uint dwTotalBytes);
+
+ [UnmanagedFunctionPointer(CallingConvention.Winapi)]
+ internal delegate void SFILE_COMPACT_CALLBACK(IntPtr pvUserData, uint dwWorkType, ulong bytesProcessed, ulong totalBytes);
+
+ [UnmanagedFunctionPointer(CallingConvention.Winapi)]
+ internal delegate void SFILE_ADDFILE_CALLBACK(IntPtr pvUserData, uint dwBytesWritte, uint dwTotalBytes, bool bFinalCall);
+}
diff --git a/SabreTools.Serialization/_EXTERNAL/StormLibSharp/Native/MpqArchiveSafeHandle.cs b/SabreTools.Serialization/_EXTERNAL/StormLibSharp/Native/MpqArchiveSafeHandle.cs
new file mode 100644
index 00000000..edd96659
--- /dev/null
+++ b/SabreTools.Serialization/_EXTERNAL/StormLibSharp/Native/MpqArchiveSafeHandle.cs
@@ -0,0 +1,22 @@
+using System;
+using Microsoft.Win32.SafeHandles;
+
+namespace StormLibSharp.Native
+{
+ internal sealed class MpqArchiveSafeHandle : SafeHandleZeroOrMinusOneIsInvalid
+ {
+ public MpqArchiveSafeHandle(IntPtr handle)
+ : base(true)
+ {
+ SetHandle(handle);
+ }
+
+ public MpqArchiveSafeHandle()
+ : base(true) { }
+
+ protected override bool ReleaseHandle()
+ {
+ return NativeMethods.SFileCloseArchive(handle);
+ }
+ }
+}
diff --git a/SabreTools.Serialization/_EXTERNAL/StormLibSharp/Native/MpqFileSafeHandle.cs b/SabreTools.Serialization/_EXTERNAL/StormLibSharp/Native/MpqFileSafeHandle.cs
new file mode 100644
index 00000000..ca9d45c8
--- /dev/null
+++ b/SabreTools.Serialization/_EXTERNAL/StormLibSharp/Native/MpqFileSafeHandle.cs
@@ -0,0 +1,24 @@
+using System;
+using Microsoft.Win32.SafeHandles;
+
+namespace StormLibSharp.Native
+{
+ internal sealed class MpqFileSafeHandle : SafeHandleZeroOrMinusOneIsInvalid
+ {
+ public MpqFileSafeHandle(IntPtr handle)
+ : base(true)
+ {
+ SetHandle(handle);
+ }
+
+ public MpqFileSafeHandle()
+ : base(true)
+ {
+ }
+
+ protected override bool ReleaseHandle()
+ {
+ return NativeMethods.SFileCloseFile(handle);
+ }
+ }
+}
diff --git a/SabreTools.Serialization/_EXTERNAL/StormLibSharp/Native/NativeMethods.cs b/SabreTools.Serialization/_EXTERNAL/StormLibSharp/Native/NativeMethods.cs
new file mode 100644
index 00000000..85d76c4a
--- /dev/null
+++ b/SabreTools.Serialization/_EXTERNAL/StormLibSharp/Native/NativeMethods.cs
@@ -0,0 +1,520 @@
+using System;
+using System.Runtime.InteropServices;
+
+namespace StormLibSharp.Native
+{
+ internal static class NativeMethods
+ {
+ private const string STORMLIB = "stormlib.dll";
+
+ #region Functions for manipulation with StormLib global flags
+
+ [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)]
+ public static extern uint SFileGetLocale();
+
+ [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)]
+ public static extern uint SFileSetLocale(uint lcNewLocale);
+
+ #endregion
+
+ #region Functions for archive manipulation
+
+ [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)]
+ public static extern bool SFileOpenArchive(
+ [MarshalAs(UnmanagedType.LPTStr)] string szMpqName,
+ uint dwPriority,
+ SFileOpenArchiveFlags dwFlags,
+ out MpqArchiveSafeHandle phMpq
+ );
+
+ [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)]
+ public static extern bool SFileCreateArchive(
+ [MarshalAs(UnmanagedType.LPTStr)] string szMpqName,
+ uint dwCreateFlags,
+ uint dwMaxFileCount,
+ out MpqArchiveSafeHandle phMpq
+ );
+
+ [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)]
+ public static extern bool SFileCreateArchive2(
+ [MarshalAs(UnmanagedType.LPTStr)] string szMpqName,
+ ref SFILE_CREATE_MPQ pCreateInfo,
+ out MpqArchiveSafeHandle phMpq
+ );
+
+ [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)]
+ public static extern bool SFileSetDownloadCallback(
+ MpqArchiveSafeHandle? hMpq,
+ [MarshalAs(UnmanagedType.FunctionPtr)] SFILE_DOWNLOAD_CALLBACK pfnCallback,
+ IntPtr pvUserData
+ );
+
+ [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)]
+ public static extern bool SFileFlushArchive(MpqArchiveSafeHandle? hMpq);
+
+
+ [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)]
+ public static extern bool SFileCloseArchive(IntPtr hMpq);
+
+ [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)]
+ public static extern bool SFileCloseArchive(MpqArchiveSafeHandle? hMpq);
+
+ #endregion
+
+ #region Adds another listfile into MPQ.
+
+ [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)]
+ public static extern int SFileAddListFile(
+ MpqArchiveSafeHandle? hMpq,
+ [MarshalAs(UnmanagedType.LPStr)] string szListFile
+ );
+
+ #endregion
+
+ #region Archive compacting
+
+ [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)]
+ public static extern bool SFileSetCompactCallback(
+ MpqArchiveSafeHandle? hMpq,
+ SFILE_COMPACT_CALLBACK? compactCB,
+ IntPtr pvUserData
+ );
+
+ [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)]
+ public static extern bool SFileCompactArchive(
+ MpqArchiveSafeHandle? hMpq,
+ [MarshalAs(UnmanagedType.LPStr)] string szListFile,
+ bool bReserved
+ );
+
+ #endregion
+
+ #region Maximum file count
+
+ [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)]
+ public static extern uint SFileGetMaxFileCount(MpqArchiveSafeHandle? hMpq);
+
+ [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)]
+ public static extern bool SFileSetMaxFileCount(MpqArchiveSafeHandle? hMpq, uint dwMaxFileCount);
+
+ #endregion
+
+ #region Changing (attributes) file
+
+ [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)]
+ public static extern uint SFileGetAttributes(MpqArchiveSafeHandle? hMpq);
+
+ [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)]
+ public static extern bool SFileSetAttributes(MpqArchiveSafeHandle? hMpq, uint dwFlags);
+
+ [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)]
+ public static extern bool SFileUpdateFileAttributes(
+ MpqArchiveSafeHandle? hMpq,
+ [MarshalAs(UnmanagedType.LPStr)] string szFileName
+ );
+
+ #endregion
+
+ #region Functions for manipulation with patch archives
+
+ [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)]
+ public static extern bool SFileOpenPatchArchive(
+ MpqArchiveSafeHandle? hMpq,
+ [MarshalAs(UnmanagedType.LPTStr)] string szPatchMpqName,
+ [MarshalAs(UnmanagedType.LPStr)] string? szPatchPathPrefix,
+ uint dwFlags
+ );
+
+ [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)]
+ public static extern bool SFileIsPatchedArchive(MpqArchiveSafeHandle? hMpq);
+
+ #endregion
+
+ #region Functions for file manipulation
+
+ [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)]
+ public static extern bool SFileHasFile(
+ MpqArchiveSafeHandle? hMpq,
+ [MarshalAs(UnmanagedType.LPStr)] string szFileName
+ );
+
+ [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)]
+ public static extern bool SFileOpenFileEx(
+ MpqArchiveSafeHandle? hMpq,
+ [MarshalAs(UnmanagedType.LPStr)] string szFileName,
+ uint dwSearchScope,
+ out MpqFileSafeHandle phFile
+ );
+
+ [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)]
+ public static extern uint SFileGetFileSize(MpqFileSafeHandle? hFile, ref uint pdwFileSizeHigh);
+
+ public static unsafe uint SFileGetFilePointer(
+ MpqFileSafeHandle? hFile
+ )
+ {
+ if (hFile == null || hFile.IsInvalid || hFile.IsClosed)
+ throw new InvalidOperationException();
+
+ IntPtr handle = hFile.DangerousGetHandle();
+ _TMPQFileHeader* header = (_TMPQFileHeader*)handle.ToPointer();
+ return header->dwFilePos;
+ }
+
+ //public static unsafe uint SFileGetFileSize(
+ // MpqFileSafeHandle? hFile
+ // )
+ //{
+ // if (hFile.IsInvalid || hFile.IsClosed)
+ // throw new InvalidOperationException();
+
+ // IntPtr handle = hFile.DangerousGetHandle();
+ // _TMPQFileHeader* header = (_TMPQFileHeader*)handle.ToPointer();
+ // return header->pFileEntry->dwFileSize;
+ //}
+
+ [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)]
+ public static extern uint SFileSetFilePointer(
+ MpqFileSafeHandle? hFile,
+ uint lFilePos,
+ ref uint plFilePosHigh,
+ uint dwMoveMethod
+ );
+
+ [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)]
+ public static extern bool SFileReadFile(
+ MpqFileSafeHandle? hFile,
+ IntPtr lpBuffer,
+ uint dwToRead,
+ out uint pdwRead,
+ ref System.Threading.NativeOverlapped lpOverlapped
+ );
+
+ [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)]
+ public static extern bool SFileCloseFile(IntPtr hFile);
+
+ [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)]
+ public static extern bool SFileCloseFile(MpqFileSafeHandle? hFile);
+
+ #region Retrieving info about a file in the archive
+
+ [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)]
+ public static extern bool SFileGetFileInfo(
+ IntPtr hMpqOrFile,
+ SFileInfoClass InfoClass,
+ IntPtr pvFileInfo,
+ uint cbFileInfoSize,
+ out uint pcbLengthNeeded
+ );
+
+ [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)]
+ public static extern bool SFileGetFileInfo(
+ MpqArchiveSafeHandle? hMpqOrFile,
+ SFileInfoClass InfoClass,
+ IntPtr pvFileInfo,
+ uint cbFileInfoSize,
+ out uint pcbLengthNeeded
+ );
+
+ [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)]
+ public static extern bool SFileGetFileInfo(
+ MpqFileSafeHandle hMpqOrFile,
+ SFileInfoClass InfoClass,
+ IntPtr pvFileInfo,
+ uint cbFileInfoSize,
+ out uint pcbLengthNeeded
+ );
+
+ [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)]
+ public static extern bool SFileGetFileName(
+ MpqFileSafeHandle? hFile,
+ [MarshalAs(UnmanagedType.LPStr)] out string szFileName
+ );
+
+ [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)]
+ public static extern bool SFileFreeFileInfo(
+ IntPtr pvFileInfo,
+ SFileInfoClass infoClass
+ );
+
+ #endregion
+
+ [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)]
+ public static extern bool SFileExtractFile(
+ MpqArchiveSafeHandle? hMpq,
+ [MarshalAs(UnmanagedType.LPStr)] string szToExtract,
+ [MarshalAs(UnmanagedType.LPTStr)] string szExtracted,
+ uint dwSearchScope
+ );
+
+ #endregion
+
+ #region Functions for file and archive verification
+
+ [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)]
+ public static extern bool SFileGetFileChecksums(
+ MpqArchiveSafeHandle? hMpq,
+ [MarshalAs(UnmanagedType.LPStr)] string szFileName,
+ out uint pdwCrc32,
+ IntPtr pMD5
+ );
+
+ [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)]
+ public static extern uint SFileVerifyFile(
+ MpqArchiveSafeHandle? hMpq,
+ [MarshalAs(UnmanagedType.LPStr)] string szFileName,
+ uint dwFlags
+ );
+
+ [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)]
+ public static extern int SFileVerifyRawData(
+ MpqArchiveSafeHandle? hMpq,
+ uint dwWhatToVerify,
+ [MarshalAs(UnmanagedType.LPStr)] string szFileName
+ );
+
+ [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)]
+ public static extern uint SFileVerifyArchive(MpqArchiveSafeHandle? hMpq);
+
+ #endregion
+
+ #region Functions for file searching
+
+ [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)]
+ public static extern IntPtr SFileFindFirstFile(
+ MpqArchiveSafeHandle? hMpq,
+ [MarshalAs(UnmanagedType.LPStr)] string szMask,
+ out _SFILE_FIND_DATA lpFindFileData,
+ [MarshalAs(UnmanagedType.LPStr)] string szListFile
+ );
+
+ [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)]
+ public static extern bool SFileFindNextFile(
+ IntPtr hFind,
+ [In, Out] ref _SFILE_FIND_DATA lpFindFileData
+ );
+
+ [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)]
+ public static extern bool SFileFindClose(IntPtr hFind);
+
+ [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)]
+ public static extern IntPtr SListFileFindFirstFile(
+ MpqArchiveSafeHandle? hMpq,
+ [MarshalAs(UnmanagedType.LPStr)] string szListFile,
+ [MarshalAs(UnmanagedType.LPStr)] string szMask,
+ [In, Out] ref _SFILE_FIND_DATA lpFindFileData
+ );
+
+ [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)]
+ public static extern bool SListFileFindNextFile(
+ IntPtr hFind,
+ [In, Out] ref _SFILE_FIND_DATA lpFindFileData
+ );
+
+ [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)]
+ public static extern bool SListFileFindClose(IntPtr hFind);
+
+ #endregion
+
+ #region Locale support
+
+ [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)]
+ public static extern int SFileEnumLocales(
+ MpqArchiveSafeHandle? hMpq,
+ [MarshalAs(UnmanagedType.LPStr)] string szFileName,
+ IntPtr plcLocales,
+ ref uint pdwMaxLocales,
+ uint dwSearchScope
+ );
+
+ #endregion
+
+ #region Support for adding files to the MPQ
+
+ [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)]
+ public static extern bool SFileCreateFile(
+ MpqArchiveSafeHandle? hMpq,
+ [MarshalAs(UnmanagedType.LPStr)] string szArchiveName,
+ ulong fileTime,
+ uint dwFileSize,
+ uint lcLocale,
+ uint dwFlags,
+ out IntPtr phFile
+ );
+
+ [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)]
+ public static extern bool SFileWriteFile(
+ MpqFileSafeHandle? hFile,
+ IntPtr pvData,
+ uint dwSize,
+ uint dwCompression
+ );
+
+ [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)]
+ public static extern bool SFileFinishFile(MpqFileSafeHandle? hFile);
+
+ [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)]
+ public static extern bool SFileAddFileEx(
+ MpqArchiveSafeHandle? hMpq,
+ [MarshalAs(UnmanagedType.LPTStr)] string szFileName,
+ [MarshalAs(UnmanagedType.LPStr)] string szArchivedName,
+ uint dwFlags,
+ uint dwCompression,
+ uint dwCompressionNext
+ );
+
+ [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)]
+ public static extern bool SFileAddFile(
+ MpqArchiveSafeHandle? hMpq,
+ [MarshalAs(UnmanagedType.LPTStr)] string szFileName,
+ [MarshalAs(UnmanagedType.LPStr)] string szArchivedName,
+ uint dwFlags
+ );
+
+ [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)]
+ public static extern bool SFileAddWave(
+ MpqArchiveSafeHandle? hMpq,
+ [MarshalAs(UnmanagedType.LPTStr)] string szFileName,
+ [MarshalAs(UnmanagedType.LPStr)] string szArchivedName,
+ uint dwFlags,
+ uint dwQuality
+ );
+
+ [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)]
+ public static extern bool SFileRemoveFile(
+ MpqArchiveSafeHandle? hMpq,
+ [MarshalAs(UnmanagedType.LPStr)] string szFileName,
+ uint dwSearchScope
+ );
+
+ [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)]
+ public static extern bool SFileRenameFile(
+ MpqArchiveSafeHandle? hMpq,
+ [MarshalAs(UnmanagedType.LPStr)] string szOldFileName,
+ [MarshalAs(UnmanagedType.LPStr)] string szNewFileName
+ );
+
+ [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)]
+ public static extern bool SFileSetFileLocale(
+ MpqFileSafeHandle? hFile,
+ uint lcNewLocale
+ );
+
+ [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)]
+ public static extern bool SFileSetDataCompression(uint DataCompression);
+
+ [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)]
+ public static extern bool SFileSetAddFileCallback(
+ MpqArchiveSafeHandle? hMpq,
+ SFILE_ADDFILE_CALLBACK AddFileCB,
+ IntPtr pvUserData
+ );
+
+ #endregion
+
+ #region Compression and decompression
+
+ [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)]
+ public static extern int SCompImplode(
+ IntPtr pvOutBuffer,
+ ref int pcbOutBuffer,
+ IntPtr pvInBuffer,
+ int cbInBuffer
+ );
+
+ [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)]
+ public static extern int SCompExplode(
+ IntPtr pvOutBuffer,
+ ref int pcbOutBuffer,
+ IntPtr pvInBuffer,
+ int cbInBuffer
+ );
+
+ [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)]
+ public static extern int SCompCompress(
+ IntPtr pvOutBuffer,
+ ref int pcbOutBuffer,
+ IntPtr pvInBuffer,
+ int cbInBuffer,
+ uint uCompressionMask,
+ int nCmpType,
+ int nCmpLevel
+ );
+
+ [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)]
+ public static extern int SCompDecompress(
+ IntPtr pvOutBuffer,
+ ref int pcbOutBuffer,
+ IntPtr pvInBuffer,
+ int cbInBuffer
+ );
+
+ [DllImport(STORMLIB, CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true, ThrowOnUnmappableChar = false)]
+ public static extern int SCompDecompress2(
+ IntPtr pvOutBuffer,
+ ref int pcbOutBuffer,
+ IntPtr pvInBuffer,
+ int cbInBuffer
+ );
+
+ #endregion
+ }
+
+#pragma warning disable CS0169
+ internal struct SFILE_CREATE_MPQ
+ {
+ public uint cbSize;
+ public uint dwMpqVersion;
+ private IntPtr pvUserData;
+ private uint cbUserData;
+ public uint dwStreamFlags;
+ public uint dwFileFlags1;
+ public uint dwFileFlags2;
+ public uint dwAttrFlags;
+ public uint dwSectorSize;
+ public uint dwRawChunkSize;
+ public uint dwMaxFileCount;
+ }
+#pragma warning restore CS0169
+
+ internal unsafe struct _SFILE_FIND_DATA
+ {
+ public fixed char cFileName[260]; // Full name of the found file
+
+ public IntPtr szPlainName; // Plain name of the found file
+ public uint dwHashIndex; // Hash table index for the file
+ public uint dwBlockIndex; // Block table index for the file
+ public uint dwFileSize; // File size in bytes
+ public uint dwFileFlags; // MPQ file flags
+ public uint dwCompSize; // Compressed file size
+ public uint dwFileTimeLo; // Low 32-bits of the file time (0 if not present)
+ public uint dwFileTimeHi; // High 32-bits of the file time (0 if not present)
+ public uint lcLocale; // Locale version
+ }
+
+ internal unsafe struct _TFileEntry
+ {
+ public ulong FileNameHash;
+ public ulong ByteOffset;
+ public ulong FileTime;
+ public uint dwHashIndex;
+ public uint dwFileSize;
+ public uint dwCmpSize;
+ public uint dwFlags;
+ public ushort lcLocale;
+ public ushort wPlatform;
+ public uint dwCrc32;
+ public fixed byte md5[16];
+ public IntPtr szFileName;
+ }
+
+ // Provides enough of _TMPQFile to get to the file size and current position.
+ internal unsafe struct _TMPQFileHeader
+ {
+ public IntPtr pStream;
+ public IntPtr ha;
+ public _TFileEntry* pFileEntry;
+ public uint dwFileKey;
+ public uint dwFilePos;
+ }
+}
diff --git a/SabreTools.Serialization/_EXTERNAL/StormLibSharp/Native/SFileInfoClass.cs b/SabreTools.Serialization/_EXTERNAL/StormLibSharp/Native/SFileInfoClass.cs
new file mode 100644
index 00000000..a53c2ba2
--- /dev/null
+++ b/SabreTools.Serialization/_EXTERNAL/StormLibSharp/Native/SFileInfoClass.cs
@@ -0,0 +1,65 @@
+namespace StormLibSharp.Native
+{
+ internal enum SFileInfoClass
+ {
+ // Info classes for archives
+ SFileMpqFileName, // Name of the archive file (TCHAR [])
+ SFileMpqStreamBitmap, // Array of bits, each bit means availability of one block (BYTE [])
+ SFileMpqUserDataOffset, // Offset of the user data header (ULONGLONG)
+ SFileMpqUserDataHeader, // Raw (unfixed) user data header (TMPQUserData)
+ SFileMpqUserData, // MPQ USer data, without the header (BYTE [])
+ SFileMpqHeaderOffset, // Offset of the MPQ header (ULONGLONG)
+ SFileMpqHeaderSize, // Fixed size of the MPQ header
+ SFileMpqHeader, // Raw (unfixed) archive header (TMPQHeader)
+ SFileMpqHetTableOffset, // Offset of the HET table, relative to MPQ header (ULONGLONG)
+ SFileMpqHetTableSize, // Compressed size of the HET table (ULONGLONG)
+ SFileMpqHetHeader, // HET table header (TMPQHetHeader)
+ SFileMpqHetTable, // HET table as pointer. Must be freed using SFileFreeFileInfo
+ SFileMpqBetTableOffset, // Offset of the BET table, relative to MPQ header (ULONGLONG)
+ SFileMpqBetTableSize, // Compressed size of the BET table (ULONGLONG)
+ SFileMpqBetHeader, // BET table header, followed by the flags (TMPQBetHeader + DWORD[])
+ SFileMpqBetTable, // BET table as pointer. Must be freed using SFileFreeFileInfo
+ SFileMpqHashTableOffset, // Hash table offset, relative to MPQ header (ULONGLONG)
+ SFileMpqHashTableSize64, // Compressed size of the hash table (ULONGLONG)
+ SFileMpqHashTableSize, // Size of the hash table, in entries (DWORD)
+ SFileMpqHashTable, // Raw (unfixed) hash table (TMPQBlock [])
+ SFileMpqBlockTableOffset, // Block table offset, relative to MPQ header (ULONGLONG)
+ SFileMpqBlockTableSize64, // Compressed size of the block table (ULONGLONG)
+ SFileMpqBlockTableSize, // Size of the block table, in entries (DWORD)
+ SFileMpqBlockTable, // Raw (unfixed) block table (TMPQBlock [])
+ SFileMpqHiBlockTableOffset, // Hi-block table offset, relative to MPQ header (ULONGLONG)
+ SFileMpqHiBlockTableSize64, // Compressed size of the hi-block table (ULONGLONG)
+ SFileMpqHiBlockTable, // The hi-block table (USHORT [])
+ SFileMpqSignatures, // Signatures present in the MPQ (DWORD)
+ SFileMpqStrongSignatureOffset, // Byte offset of the strong signature, relative to begin of the file (ULONGLONG)
+ SFileMpqStrongSignatureSize, // Size of the strong signature (DWORD)
+ SFileMpqStrongSignature, // The strong signature (BYTE [])
+ SFileMpqArchiveSize64, // Archive size from the header (ULONGLONG)
+ SFileMpqArchiveSize, // Archive size from the header (DWORD)
+ SFileMpqMaxFileCount, // Max number of files in the archive (DWORD)
+ SFileMpqFileTableSize, // Number of entries in the file table (DWORD)
+ SFileMpqSectorSize, // Sector size (DWORD)
+ SFileMpqNumberOfFiles, // Number of files (DWORD)
+ SFileMpqRawChunkSize, // Size of the raw data chunk for MD5
+ SFileMpqStreamFlags, // Stream flags (DWORD)
+ SFileMpqIsReadOnly, // Nonzero if the MPQ is read only (DWORD)
+
+ // Info classes for files
+ SFileInfoPatchChain, // Chain of patches where the file is (TCHAR [])
+ SFileInfoFileEntry, // The file entry for the file (TFileEntry)
+ SFileInfoHashEntry, // Hash table entry for the file (TMPQHash)
+ SFileInfoHashIndex, // Index of the hash table entry (DWORD)
+ SFileInfoNameHash1, // The first name hash in the hash table (DWORD)
+ SFileInfoNameHash2, // The second name hash in the hash table (DWORD)
+ SFileInfoNameHash3, // 64-bit file name hash for the HET/BET tables (ULONGLONG)
+ SFileInfoLocale, // File locale (DWORD)
+ SFileInfoFileIndex, // Block index (DWORD)
+ SFileInfoByteOffset, // File position in the archive (ULONGLONG)
+ SFileInfoFileTime, // File time (ULONGLONG)
+ SFileInfoFileSize, // Size of the file (DWORD)
+ SFileInfoCompressedSize, // Compressed file size (DWORD)
+ SFileInfoFlags, // File flags from (DWORD)
+ SFileInfoEncryptionKey, // File encryption key
+ SFileInfoEncryptionKeyRaw, // Unfixed value of the file key
+ }
+}
diff --git a/SabreTools.Serialization/_EXTERNAL/StormLibSharp/Native/SFileOpenArchiveFlags.cs b/SabreTools.Serialization/_EXTERNAL/StormLibSharp/Native/SFileOpenArchiveFlags.cs
new file mode 100644
index 00000000..24238c75
--- /dev/null
+++ b/SabreTools.Serialization/_EXTERNAL/StormLibSharp/Native/SFileOpenArchiveFlags.cs
@@ -0,0 +1,23 @@
+using System;
+
+namespace StormLibSharp.Native
+{
+ [Flags]
+ internal enum SFileOpenArchiveFlags : uint
+ {
+ None = 0,
+ TypeIsFile = None,
+ TypeIsMemoryMapped = 1,
+ TypeIsHttp = 2,
+
+ AccessReadOnly = 0x100,
+ AccessReadWriteShare = 0x200,
+ AccessUseBitmap = 0x400,
+
+ DontOpenListfile = 0x10000,
+ DontOpenAttributes = 0x20000,
+ DontSearchHeader = 0x40000,
+ ForceVersion1 = 0x80000,
+ CheckSectorCRC = 0x100000,
+ }
+}
diff --git a/SabreTools.Serialization/_EXTERNAL/StormLibSharp/Native/Win32Methods.cs b/SabreTools.Serialization/_EXTERNAL/StormLibSharp/Native/Win32Methods.cs
new file mode 100644
index 00000000..bf7ba6d0
--- /dev/null
+++ b/SabreTools.Serialization/_EXTERNAL/StormLibSharp/Native/Win32Methods.cs
@@ -0,0 +1,10 @@
+using System.Runtime.InteropServices;
+
+namespace StormLibSharp.Native
+{
+ internal static class Win32Methods
+ {
+ [DllImport("kernel32", SetLastError = false, ExactSpelling = false)]
+ public static extern int GetLastError();
+ }
+}
diff --git a/SabreTools.Serialization/_EXTERNAL/stormlibsharp b/SabreTools.Serialization/_EXTERNAL/stormlibsharp
deleted file mode 160000
index 3bf13753..00000000
--- a/SabreTools.Serialization/_EXTERNAL/stormlibsharp
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit 3bf13753171d8d1e430f5f044fcbc10b1fd7089f