diff --git a/AssetEditor/AssetEditor.csproj b/AssetEditor/AssetEditor.csproj
index 0042bb0d1..8c81e80bb 100644
--- a/AssetEditor/AssetEditor.csproj
+++ b/AssetEditor/AssetEditor.csproj
@@ -28,6 +28,14 @@
+
+
+ Content\%(RecursiveDir)%(Filename)%(Extension)
+ PreserveNewest
+ Always
+
+
+
embeddedAssetEdCommunity
@@ -43,22 +51,19 @@
-
-
+ Make sure any documentation comments which are included in code get checked for syntax during the build, but do
+ not report warnings for missing comments.
+ CS1573: Parameter 'parameter' has no matching param tag in the XML comment for 'parameter' (but other parameters do)
+ CS1591: Missing XML comment for publicly visible type or member 'Type_or_Member'
+ CS1712: Type parameter 'type_parameter' has no matching typeparam tag in the XML comment on 'type_or_member' (but other type parameters do)
+ -->
False$(NoWarn),1573,1591,1712
-
\ No newline at end of file
diff --git a/AssetEditor/ViewModels/SettingsViewModel.cs b/AssetEditor/ViewModels/SettingsViewModel.cs
index f29543ab6..4c7e7a5d0 100644
--- a/AssetEditor/ViewModels/SettingsViewModel.cs
+++ b/AssetEditor/ViewModels/SettingsViewModel.cs
@@ -37,12 +37,12 @@ public SettingsViewModel(ApplicationSettingsService settingsService)
RenderEngineBackgroundColours = new ObservableCollection((BackgroundColour[])Enum.GetValues(typeof(BackgroundColour)));
CurrentRenderEngineBackgroundColour = _settingsService.CurrentSettings.RenderEngineBackgroundColour;
StartMaximised = _settingsService.CurrentSettings.StartMaximised;
- Games = new ObservableCollection(GameInformationDatabase.Games.OrderBy(g => g.DisplayName).Select(g => g.Type));
+ Games = new ObservableCollection(GameInformationDatabase.Games.Values.OrderBy(game => game.DisplayName).Select(game => game.Type));
CurrentGame = _settingsService.CurrentSettings.CurrentGame;
LoadCaPacksByDefault = _settingsService.CurrentSettings.LoadCaPacksByDefault;
ShowCAWemFiles = _settingsService.CurrentSettings.ShowCAWemFiles;
OnlyLoadLod0ForReferenceMeshes = _settingsService.CurrentSettings.OnlyLoadLod0ForReferenceMeshes;
- foreach (var game in GameInformationDatabase.Games.OrderBy(g => g.DisplayName))
+ foreach (var game in GameInformationDatabase.Games.Values.OrderBy(game => game.DisplayName))
{
GameDirectores.Add(
new GamePathItem()
diff --git a/GameWorld/ContentProject/ContentProject.csproj b/GameWorld/ContentProject/ContentProject.csproj
index 772569eaf..ccb6f3b59 100644
--- a/GameWorld/ContentProject/ContentProject.csproj
+++ b/GameWorld/ContentProject/ContentProject.csproj
@@ -1,4 +1,4 @@
-
+net9.0-windows
@@ -24,7 +24,6 @@
-
diff --git a/GameWorld/View3D/Services/SkeletonAnimationLookUpHelper.cs b/GameWorld/View3D/Services/SkeletonAnimationLookUpHelper.cs
index f7e47bc3c..2f8c1f039 100644
--- a/GameWorld/View3D/Services/SkeletonAnimationLookUpHelper.cs
+++ b/GameWorld/View3D/Services/SkeletonAnimationLookUpHelper.cs
@@ -1,5 +1,4 @@
using System.Collections.ObjectModel;
-using System.Diagnostics;
using System.IO;
using Serilog;
using Shared.Core.ErrorHandling;
@@ -129,23 +128,19 @@ void LoadFromPackFileContainer(PackFileContainer packFileContainer)
void FileDiscovered(byte[] byteChunk, PackFileContainer container, string fullPath, ref List skeletonFileNameList, ref Dictionary> animationList)
{
- // Skip broken animations, as the errors are annoying when the debuger is attached.
- if (Debugger.IsAttached)
+ var brokenFiles = new string[]
{
- var brokenAnims = new string[]
- {
- "rigidmodels\\buildings\\roman_aqueduct_straight\\roman_aqueduct_straight_piece01_destruct01_anim.anim",
- "animations\\battle\\raptor02\\subset\\colossal_squig\\deaths\\rp2_colossalsquig_death_01.anim",
- "animations\\battle\\humanoid13b\\golgfag\\docking\\hu13b_golgfag_docking_armed_02.anim",
- "animations\\battle\\humanoid13\\ogre\\rider\\hq3b_stonehorn_wb\\sword_and_crossbow\\missile_action\\crossbow\\hu13_hq3b_swc_rider1_shoot_back_crossbow_01.anim",
- "animations\\battle\\humanoid13\\ogre\\rider\\hq3b_stonehorn_wb\\sword_and_crossbow\\missile_action\\crossbow\\hu13_hq3b_swc_rider1_reload_crossbow_01.anim",
- "animations\\battle\\humanoid13\\ogre\\rider\\hq3b_stonehorn_wb\\sword_and_crossbow\\missile_action\\crossbow\\hu13_hq3b_sp_rider1_shoot_ready_crossbow_01.anim"
- };
- if (brokenAnims.Contains(fullPath))
- {
- _logger.Here().Warning("Skipping loading of known broken file - " + fullPath);
- return;
- }
+ "rigidmodels\\buildings\\roman_aqueduct_straight\\roman_aqueduct_straight_piece01_destruct01_anim.anim",
+ "animations\\battle\\raptor02\\subset\\colossal_squig\\deaths\\rp2_colossalsquig_death_01.anim",
+ "animations\\battle\\humanoid13b\\golgfag\\docking\\hu13b_golgfag_docking_armed_02.anim",
+ "animations\\battle\\humanoid13\\ogre\\rider\\hq3b_stonehorn_wb\\sword_and_crossbow\\missile_action\\crossbow\\hu13_hq3b_swc_rider1_shoot_back_crossbow_01.anim",
+ "animations\\battle\\humanoid13\\ogre\\rider\\hq3b_stonehorn_wb\\sword_and_crossbow\\missile_action\\crossbow\\hu13_hq3b_swc_rider1_reload_crossbow_01.anim",
+ "animations\\battle\\humanoid13\\ogre\\rider\\hq3b_stonehorn_wb\\sword_and_crossbow\\missile_action\\crossbow\\hu13_hq3b_sp_rider1_shoot_ready_crossbow_01.anim"
+ };
+ if (brokenFiles.Contains(fullPath))
+ {
+ _logger.Here().Warning("Skipping loading of known broken file - " + fullPath);
+ return;
}
try
diff --git a/Shared/SharedCore/PackFiles/Models/DataSource.cs b/Shared/SharedCore/PackFiles/Models/DataSource.cs
index 1b658c96e..03b64838d 100644
--- a/Shared/SharedCore/PackFiles/Models/DataSource.cs
+++ b/Shared/SharedCore/PackFiles/Models/DataSource.cs
@@ -1,5 +1,4 @@
using Shared.Core.ByteParsing;
-using Shared.Core.Settings;
namespace Shared.Core.PackFiles.Models
{
@@ -159,62 +158,23 @@ public byte[] ReadData(Stream knownStream)
return data;
}
- public ByteChunk ReadDataAsChunk()
- {
- return new ByteChunk(ReadData());
- }
-
- public void SetCompressionInfo(GameInformation gameInformation, string rootFolder, string extension)
+ public byte[] ReadDataWithoutDecompressing()
{
- // Check if the game supports any compression at all
- if (gameInformation.CompressionFormats.All(compressionFormat => compressionFormat == CompressionFormat.None))
- return;
-
- // We use isTable because non-loc tables don't have an extension
- var isTable = rootFolder == "db" || extension == ".loc";
- var hasExtension = !string.IsNullOrEmpty(extension);
-
- // Don't compress files that aren't tables and don't have extensions
- if (!isTable && !hasExtension)
- {
- CompressionFormat = CompressionFormat.None;
- IsCompressed = false;
- return;
- }
-
- // Only in WH3 (and newer games?) is the table compression bug fixed
- if (isTable && gameInformation.CompressionFormats.Contains(CompressionFormat.Zstd) && gameInformation.Type == GameTypeEnum.Warhammer3)
+ var data = new byte[Size];
+ using (Stream stream = File.Open(_parent.FilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
{
- CompressionFormat = CompressionFormat.Zstd;
- IsCompressed = true;
- return;
+ stream.Seek(Offset, SeekOrigin.Begin);
+ stream.Read(data, 0, data.Length);
}
- // Games that support the other formats won't use Lzma1 as it's legacy so if it's set then it's for a game that only uses it so keep it
- if (CompressionFormat == CompressionFormat.Lzma1 && gameInformation.CompressionFormats.Contains(CompressionFormat.Lzma1))
- return;
+ if (IsEncrypted)
+ data = PackFileEncryption.Decrypt(data);
+ return data;
+ }
- // Anything that shouldn't be None or Lz4 is set to Zstd unless the game doesn't support that in which case use None
- if (PackFileCompression.NoneFileTypes.Contains(extension))
- {
- CompressionFormat = CompressionFormat.None;
- IsCompressed = false;
- }
- else if (PackFileCompression.Lz4FileTypes.Contains(extension) && gameInformation.CompressionFormats.Contains(CompressionFormat.Lz4))
- {
- CompressionFormat = CompressionFormat.Lz4;
- IsCompressed = true;
- }
- else if (gameInformation.CompressionFormats.Contains(CompressionFormat.Zstd))
- {
- CompressionFormat = CompressionFormat.Zstd;
- IsCompressed = true;
- }
- else
- {
- CompressionFormat = CompressionFormat.None;
- IsCompressed = false;
- }
+ public ByteChunk ReadDataAsChunk()
+ {
+ return new ByteChunk(ReadData());
}
}
diff --git a/Shared/SharedCore/PackFiles/Models/PackFileContainer.cs b/Shared/SharedCore/PackFiles/Models/PackFileContainer.cs
index 83752f7b2..0aed81a50 100644
--- a/Shared/SharedCore/PackFiles/Models/PackFileContainer.cs
+++ b/Shared/SharedCore/PackFiles/Models/PackFileContainer.cs
@@ -3,6 +3,12 @@
namespace Shared.Core.PackFiles.Models
{
+ public record PackFileWriteInfo(
+ PackFile PackFile,
+ long FileSizeMetadataPosition,
+ CompressionFormat CurrentCompressionFormat,
+ CompressionFormat IntendedCompressionFormat);
+
public class PackFileContainer
{
public string Name { get; set; }
@@ -55,27 +61,25 @@ public void SaveToByteArray(BinaryWriter writer, GameInformation gameInformation
Header.FileCount = (uint)FileList.Count;
PackFileSerializer.WriteHeader(Header, (uint)fileNamesOffset, writer);
- // Save all the files
+ var filesToWrite = new List();
+
+ // Write file metadata
foreach (var file in sortedFiles)
{
var packFile = file.Value;
- var packedFileSource = (PackedFileSource)file.Value.DataSource;
- var data = packedFileSource.ReadData();
+ var fileSize = (int)packFile.DataSource.Size;
- var fileExtension = packFile.Extension;
+ // Determine compression info
+ var currentCompressionFormat = CompressionFormat.None;
+ if (packFile.DataSource is PackedFileSource packedFileSource)
+ currentCompressionFormat = packedFileSource.CompressionFormat;
+ var firstFilePathPart = file.Key.Split(['\\', '/'], StringSplitOptions.RemoveEmptyEntries).First();
+ var intendedCompressionFormat = PackFileCompression.GetCompressionFormat(gameInformation, firstFilePathPart, packFile.Extension);
+ var shouldCompress = intendedCompressionFormat != CompressionFormat.None;
- var segments = file.Key.Split(['\\', '/'], StringSplitOptions.RemoveEmptyEntries);
- var rootFolder = segments.First();
-
- packedFileSource.SetCompressionInfo(gameInformation, rootFolder, fileExtension);
-
- var fileSize = data.Length;
- if (packedFileSource.IsCompressed)
- {
- var compressedData = PackFileCompression.Compress(data, packedFileSource.CompressionFormat);
- fileSize = compressedData.Length;
- }
- writer.Write(fileSize);
+ // File size placeholder (rewritten later)
+ var fileSizePosition = writer.BaseStream.Position;
+ writer.Write(0);
// Timestamp
if (Header.HasIndexWithTimeStamp)
@@ -83,7 +87,7 @@ public void SaveToByteArray(BinaryWriter writer, GameInformation gameInformation
// Compression
if (Header.Version == PackFileVersion.PFH5)
- writer.Write(packedFileSource.IsCompressed);
+ writer.Write(shouldCompress);
// Filename
var fileNameBytes = Encoding.UTF8.GetBytes(file.Key);
@@ -91,34 +95,71 @@ public void SaveToByteArray(BinaryWriter writer, GameInformation gameInformation
// Zero terminator
writer.Write((byte)0);
+
+ filesToWrite.Add(new PackFileWriteInfo(
+ packFile,
+ fileSizePosition,
+ currentCompressionFormat,
+ intendedCompressionFormat));
}
- var packedFileSourceParent = new PackedFileSourceParent()
- {
- FilePath = SystemFilePath,
- };
+ var packedFileSourceParent = new PackedFileSourceParent { FilePath = SystemFilePath };
// Write the files
- foreach (var file in sortedFiles)
+ foreach (var file in filesToWrite)
{
- var packFile = file.Value;
- var packedFileSource = (PackedFileSource)packFile.DataSource;
+ var packFile = file.PackFile;
+ byte[] data;
+ var fileSize = 0;
+ uint uncompressedFileSize = 0;
+
+ // Read the data
+ var shouldCompress = file.IntendedCompressionFormat != CompressionFormat.None;
+ var isCorrectCompressionFormat = file.CurrentCompressionFormat == file.IntendedCompressionFormat;
+ if (shouldCompress && !isCorrectCompressionFormat)
+ {
+ // Decompress the data
+ var uncompressedData = packFile.DataSource.ReadData();
+ uncompressedFileSize = (uint)uncompressedData.Length;
+ // Compress the data into the right format
+ var compressedData = PackFileCompression.Compress(uncompressedData, file.IntendedCompressionFormat);
+ data = compressedData;
+ fileSize = compressedData.Length;
+ }
+ else if (packFile.DataSource is PackedFileSource packedFileSource && isCorrectCompressionFormat)
+ {
+ // The data is already in the right format so just get the compressed data
+ uncompressedFileSize = packedFileSource.UncompressedSize;
+ var compressedData = packedFileSource.ReadDataWithoutDecompressing();
+ data = compressedData;
+ fileSize = data.Length;
+ }
+ else
+ {
+ data = packFile.DataSource.ReadData();
+ fileSize = data.Length;
+ }
+
+ // Write the data
var offset = writer.BaseStream.Position;
- var data = packedFileSource.ReadData();
- if (packedFileSource.IsCompressed)
- data = PackFileCompression.Compress(data, packedFileSource.CompressionFormat);
+ writer.Write(data);
+
+ // Patch the file size metadata placeholder
+ var currentPosition = writer.BaseStream.Position;
+ writer.BaseStream.Position = file.FileSizeMetadataPosition;
+ writer.Write(fileSize);
+ writer.BaseStream.Position = currentPosition;
+ // Update DataSource
packFile.DataSource = new PackedFileSource(
packedFileSourceParent,
offset,
- data.Length,
- packedFileSource.IsEncrypted,
- packedFileSource.IsCompressed,
- packedFileSource.CompressionFormat,
- packedFileSource.UncompressedSize);
-
- writer.Write(data);
+ fileSize,
+ Header.HasEncryptedData,
+ shouldCompress,
+ file.IntendedCompressionFormat,
+ uncompressedFileSize);
}
}
}
diff --git a/Shared/SharedCore/PackFiles/PackFileCompression.cs b/Shared/SharedCore/PackFiles/PackFileCompression.cs
index 988378151..5d766c13f 100644
--- a/Shared/SharedCore/PackFiles/PackFileCompression.cs
+++ b/Shared/SharedCore/PackFiles/PackFileCompression.cs
@@ -1,6 +1,7 @@
using EasyCompressor;
using K4os.Compression.LZ4.Encoders;
using K4os.Compression.LZ4.Streams;
+using Shared.Core.Settings;
using ZstdSharp;
using ZstdSharp.Unsafe;
@@ -8,51 +9,46 @@ namespace Shared.Core.PackFiles
{
public enum CompressionFormat
{
- /// Dummy variant to disable compression.
+ // Dummy variant to disable compression.
None,
- /// Legacy format. Supported by all PFH5 games (all Post-WH2 games).
- ///
- /// Specifically, Total War games use the Non-Streamed LZMA1 format with the following custom header:
- ///
- /// | Bytes | Type | Data |
- /// | ----- | ----- | ----------------------------------------------------------------------------------- |
- /// | 4 | [u32] | Uncompressed size (as u32, max at 4GB). |
- /// | 1 | [u8] | LZMA model properties (lc, lp, pb) in encoded form... I think. Usually it's `0x5D`. |
- /// | 4 | [u32] | Dictionary size (as u32)... I think. It's usually `[0x00, 0x00, 0x40, 0x00]`. |
- ///
- /// For reference, a normal Non-Streamed LZMA1 header (from the original spec) contains:
- ///
- /// | Bytes | Type | Data |
- /// | ----- | ------------- | ----------------------------------------------------------- |
- /// | 1 | [u8] | LZMA model properties (lc, lp, pb) in encoded form. |
- /// | 4 | [u32] | Dictionary size (32-bit unsigned integer, little-endian). |
- /// | 8 | [prim@u64] | Uncompressed size (64-bit unsigned integer, little-endian). |
- ///
- /// This means one has to move the uncompressed size to the correct place in order for a compressed file to be readable,
- /// and one has to remove the uncompressed size and prepend it to the file in order for the game to read the compressed file.
+ // Legacy format. Supported by all PFH5 games (all Post-WH2 games).
+
+ // Specifically, Total War games use the Non-Streamed LZMA1 format with the following custom header:
+ // | Bytes | Type | Data |
+ // | ----- | ----- | ----------------------------------------------------------------------------------- |
+ // | 4 | [u32] | Uncompressed size (as u32, max at 4GB). |
+ // | 1 | [u8] | LZMA model properties (lc, lp, pb) in encoded form... I think. Usually it's `0x5D`. |
+ // | 4 | [u32] | Dictionary size (as u32)... I think. It's usually `[0x00, 0x00, 0x40, 0x00]`. |
+
+ // For reference, a normal Non-Streamed LZMA1 header (from the original spec) contains:
+ // | Bytes | Type | Data |
+ // | ----- | ------------- | ----------------------------------------------------------- |
+ // | 1 | [u8] | LZMA model properties (lc, lp, pb) in encoded form. |
+ // | 4 | [u32] | Dictionary size (32-bit unsigned integer, little-endian). |
+ // | 8 | [prim@u64] | Uncompressed size (64-bit unsigned integer, little-endian). |
+
+ // This means one has to move the uncompressed size to the correct place in order for a compressed file to be readable,
+ // and one has to remove the uncompressed size and prepend it to the file in order for the game to read the compressed file.
Lzma1,
- /// New format introduced in WH3 6.2.
- ///
- /// This is a standard Lz4 implementation, with the following tweaks:
- ///
- /// | Bytes | Type | Data |
- /// | ----- | --------- | --------------------------------------------- |
- /// | 4 | [u32] | Uncompressed size (as u32, max at 4GB). |
- /// | * | &[[`u8`]] | Lz4 data, starting with the Lz4 Magic Number. |
+ // New format introduced in WH3 6.2.
+ // This is a standard Lz4 implementation, with the following tweaks:
+ // | Bytes | Type | Data |
+ // | ----- | --------- | --------------------------------------------- |
+ // | 4 | [u32] | Uncompressed size (as u32, max at 4GB). |
+ // | * | &[[`u8`]] | Lz4 data, starting with the Lz4 Magic Number. |
Lz4,
- /// New format introduced in WH3 6.2.
- ///
- /// This is a standard Zstd implementation, with the following tweaks:
- ///
- /// | Bytes | Type | Data |
- /// | ----- | --------- | ----------------------------------------------- |
- /// | 4 | [u32] | Uncompressed size (as u32, max at 4GB). |
- /// | * | &[[`u8`]] | Zstd data, starting with the Zstd Magic Number. |
- ///
- /// By default the Zstd compression is done with the checksum and content size flags enabled.
+ // New format introduced in WH3 6.2.
+
+ // This is a standard Zstd implementation, with the following tweaks:
+ // | Bytes | Type | Data |
+ // | ----- | --------- | ----------------------------------------------- |
+ // | 4 | [u32] | Uncompressed size (as u32, max at 4GB). |
+ // | * | &[[`u8`]] | Zstd data, starting with the Zstd Magic Number. |
+
+ // By default the Zstd compression is done with the checksum and content size flags enabled.
Zstd
}
@@ -72,9 +68,16 @@ public static class PackFileCompression
private static readonly uint s_magicNumberLz4 = 0x184D_2204;
private static readonly uint s_magicNumberZstd = 0xfd2f_b528;
+ // CA generally compress file types in specific formats, presumably because they compress better in that format.
+ // Sometimes CA compress file types in various formats (though predominantly in one format), presumably by
+ // mistake as they use BOB which compresses by folder not file type. We try to replicate that by assigning
+ // some file types a specific compression format according to whether they are exclusively compressed
+ // in a given format or predominantly compressed in a given format by CA.
+ // Lmza1 is not specified as it's legacy so we only use that for games that support only that format.
+ // Zstd is not specified as by default everything not None or Lz4 is Zstd.
public static List NoneFileTypes { get; } =
[
- // Exclusive - In CA packs the files are exclusively in this format
+ // In CA packs these files are exclusively in this format
".bnk",
".ca_vp8",
".fxc",
@@ -83,17 +86,21 @@ public static class PackFileCompression
".manifest",
".wem",
- // Preferred - In CA packs the files are mostly in this format
- ".dat",
+ // In CA packs these files are mostly in this format
".rigid_model_v2",
+ // Action Events don't play if the .dat file their names are stored in is compressed
+ ".dat",
- // RPFM - How RPFM formats the file
+ // How RPFM formats these files
".rpfm_reserved",
+
+ // .wav files aren't? in CA packs but probably better not to compress them
+ ".wav",
];
public static List Lz4FileTypes { get; } =
[
- // Exclusive - In CA packs the files are exclusively in this format
+ // In CA packs these files are exclusively in this format
".animpack",
".collision",
".cs2",
@@ -103,7 +110,7 @@ public static class PackFileCompression
".wsmodel",
".xt",
- // Preferred - In CA packs the files are mostly in this format
+ // In CA packs these files are mostly in this format
".parsed",
];
@@ -194,13 +201,13 @@ private static byte[] DecompressLzma(byte[] data, uint uncompressedSize)
}
}
- public static byte[] Compress(byte[] data, CompressionFormat format)
+ public static byte[] Compress(byte[] data, CompressionFormat compressionFormat)
{
- if(format == CompressionFormat.Zstd)
+ if(compressionFormat == CompressionFormat.Zstd)
return CompressZstd(data);
- else if(format == CompressionFormat.Lz4)
+ else if(compressionFormat == CompressionFormat.Lz4)
return CompressLz4(data);
- else if (format == CompressionFormat.Lzma1)
+ else if (compressionFormat == CompressionFormat.Lzma1)
return CompressLzma1(data);
return data;
}
@@ -208,14 +215,15 @@ public static byte[] Compress(byte[] data, CompressionFormat format)
private static byte[] CompressZstd(byte[] data)
{
using var stream = new MemoryStream();
- stream.Write(BitConverter.GetBytes((uint)data.Length));
+ var uncompressedSize = data.Length;
+ stream.Write(BitConverter.GetBytes((uint)uncompressedSize));
using (var compressor = new CompressionStream(stream, 3, leaveOpen: true))
{
compressor.SetParameter(ZSTD_cParameter.ZSTD_c_contentSizeFlag, 1);
compressor.SetParameter(ZSTD_cParameter.ZSTD_c_checksumFlag, 1);
- compressor.SetPledgedSrcSize((ulong)data.Length);
- compressor.Write(data, 0, data.Length);
+ compressor.SetPledgedSrcSize((ulong)uncompressedSize);
+ compressor.Write(data, 0, uncompressedSize);
}
return stream.ToArray();
@@ -224,10 +232,11 @@ private static byte[] CompressZstd(byte[] data)
private static byte[] CompressLz4(byte[] data)
{
using var stream = new MemoryStream();
- stream.Write(BitConverter.GetBytes((uint)data.Length));
+ var uncompressedSize = data.Length;
+ stream.Write(BitConverter.GetBytes((uint)uncompressedSize));
using (var encoder = LZ4Stream.Encode(stream, leaveOpen: true))
- encoder.Write(data, 0, data.Length);
+ encoder.Write(data, 0, uncompressedSize);
return stream.ToArray();
}
@@ -235,13 +244,15 @@ private static byte[] CompressLz4(byte[] data)
private static byte[] CompressLzma1(byte[] data)
{
var compressedData = LZMACompressor.Shared.Compress(data);
- if (compressedData.Length < 13)
+ var compressedSize = compressedData.Length;
+ if (compressedSize < 13)
throw new InvalidDataException("Data cannot be compressed");
using var stream = new MemoryStream();
- stream.Write(BitConverter.GetBytes(data.Length), 0, 4);
+ var uncompressedSize = data.Length;
+ stream.Write(BitConverter.GetBytes(uncompressedSize), 0, 4);
stream.Write(compressedData, 0, 5);
- stream.Write(compressedData, 13, compressedData.Length - 13);
+ stream.Write(compressedData, 13, compressedSize - 13);
return stream.ToArray();
}
@@ -257,5 +268,41 @@ public static CompressionFormat GetCompressionFormat(uint magicNumber)
else
return CompressionFormat.None;
}
+
+ public static CompressionFormat GetCompressionFormat(GameInformation gameInformation, string firstFilePathPart, string extension)
+ {
+ var compressionFormats = gameInformation.CompressionFormats;
+
+ // Check if the game supports any compression at all
+ if (compressionFormats.All(compressionFormat => compressionFormat == CompressionFormat.None))
+ return CompressionFormat.None;
+
+ // We use rootFolder for normal db tables because they don't have an extension
+ var isTable = firstFilePathPart == "db" || extension == ".loc";
+ var hasExtension = !string.IsNullOrEmpty(extension);
+
+ // Don't compress files that aren't tables and don't have extensions
+ if (!isTable && !hasExtension)
+ return CompressionFormat.None;
+
+ // Only compress tables in WH3 (and newer games?) as compresse tables are bugged in older games
+ if (isTable && compressionFormats.Contains(CompressionFormat.Zstd) && gameInformation.Type == GameTypeEnum.Warhammer3)
+ return CompressionFormat.Zstd;
+ else if (isTable)
+ return CompressionFormat.None;
+
+ // Anything that isn't preferrably None, Lzma1, or Lz4 is set to Zstd unless the game doesn't support that in which case use None
+ // Lzma1 is a legacy format so only use it if it's all the game can use even though games with other formats can use it
+ if (NoneFileTypes.Contains(extension))
+ return CompressionFormat.None;
+ else if (compressionFormats.Count == 1 && compressionFormats.Contains(CompressionFormat.Lzma1))
+ return CompressionFormat.Lzma1;
+ else if (Lz4FileTypes.Contains(extension) && compressionFormats.Contains(CompressionFormat.Lz4))
+ return CompressionFormat.Lz4;
+ else if (compressionFormats.Contains(CompressionFormat.Zstd))
+ return CompressionFormat.Zstd;
+ else
+ return CompressionFormat.None;
+ }
}
}
diff --git a/Shared/SharedCore/PackFiles/PackFileService.cs b/Shared/SharedCore/PackFiles/PackFileService.cs
index 880a5135f..63430750b 100644
--- a/Shared/SharedCore/PackFiles/PackFileService.cs
+++ b/Shared/SharedCore/PackFiles/PackFileService.cs
@@ -337,14 +337,16 @@ public string GetFullPath(PackFile file, PackFileContainer? container = null)
{
foreach (var pf in _packFileContainers)
{
- var res = pf.FileList.FirstOrDefault(x => x.Value == file).Key;
+ var res = pf.FileList.FirstOrDefault(x => ReferenceEquals(x.Value, file)
+ || string.Equals(x.Value.Name, file.Name, StringComparison.OrdinalIgnoreCase)).Key;
if (string.IsNullOrWhiteSpace(res) == false)
return res;
}
}
else
{
- var res = container.FileList.FirstOrDefault(x => x.Value == file).Key;
+ var res = container.FileList.FirstOrDefault(x => ReferenceEquals(x.Value, file)
+ || string.Equals(x.Value.Name, file.Name, StringComparison.OrdinalIgnoreCase)).Key;
if (string.IsNullOrWhiteSpace(res) == false)
return res;
}
diff --git a/Shared/SharedCore/Settings/GameInformationBuilder.cs b/Shared/SharedCore/Settings/GameInformationBuilder.cs
deleted file mode 100644
index bdf0c0cb1..000000000
--- a/Shared/SharedCore/Settings/GameInformationBuilder.cs
+++ /dev/null
@@ -1,85 +0,0 @@
-namespace Shared.Core.Settings
-{
- //public enum PreferedShaderGroup
- //{ }
- //
- //public enum PreferedRmvVersion
- //{
- //
- //}
- //
- //public enum PreferedWsModelVersion
- //{ }
- //
- //public enum PreferedAnimationBinVersion
- //{ }
- //
- /*
-
-
-
-
-
- Warhammer3 = GameInformationBuilder
- .Create(GameTypeEnum.Warhammer3, "Warhammer III")
- .BankGeneratorVersion(2147483784)
- .WsModelVersion(5)
- .PreferedRmvVersion(7)
- .ShaderVersion(213)
- .AnimationBinVersion(11)
- .TwuiVersion(142)
- .Build();
-
- */
-
-
- public class GameInformationBuilder()
- {
- private GameInformation _instance;
-
- public static GameInformationBuilder Create(GameTypeEnum type, string displayName)
- {
- return new GameInformationBuilder()
- {
- _instance = new GameInformation(type, displayName, Settings.PackFileVersion.PFH5, Settings.GameBnkVersion.Unsupported, Settings.WsModelVersion.Unknown, [])
- };
- }
-
- public GameInformationBuilder PackFileVersion(uint version)
- {
- return this;
- }
-
- public GameInformationBuilder ShaderVersion(int version)
- {
- return this;
- }
-
- public GameInformationBuilder WsModelVersion(int version)
- {
- return this;
- }
-
- public GameInformationBuilder TwuiVersion(int version)
- {
- return this;
- }
-
- public GameInformationBuilder PreferedRmvVersion(int version)
- {
- return this;
- }
-
- public GameInformationBuilder GameBnkVersion(uint version)
- {
- return this;
- }
-
- public GameInformationBuilder AnimationBinVersion(uint version)
- {
- return this;
- }
-
- public GameInformation Build() => _instance;
- }
-}
diff --git a/Shared/SharedCore/Settings/GameInformationDatabase.cs b/Shared/SharedCore/Settings/GameInformationDatabase.cs
index 5640160ea..7e163cf30 100644
--- a/Shared/SharedCore/Settings/GameInformationDatabase.cs
+++ b/Shared/SharedCore/Settings/GameInformationDatabase.cs
@@ -28,6 +28,12 @@ public enum GameBnkVersion : uint
Attila = 112
}
+ public enum WwiseProjectId : uint
+ {
+ Unsupported = 0,
+ Warhammer3 = 2361,
+ }
+
public enum PackFileVersion
{
PFH0,
@@ -48,37 +54,118 @@ public enum WsModelVersion
//RmvVersionEnum
- public class GameInformation(GameTypeEnum gameType, string displayName, PackFileVersion packFileVersion, GameBnkVersion bankGeneratorVersion, WsModelVersion wsModelVersion, List CompressionFormats)
+ public class GameInformation(
+ GameTypeEnum gameType,
+ string displayName,
+ PackFileVersion packFileVersion,
+ GameBnkVersion bankGeneratorVersion,
+ WwiseProjectId wwiseProjectId,
+ WsModelVersion wsModelVersion,
+ List compressionFormats)
{
public GameTypeEnum Type { get; } = gameType;
public string DisplayName { get; } = displayName;
public PackFileVersion PackFileVersion { get; } = packFileVersion;
public GameBnkVersion BankGeneratorVersion { get; } = bankGeneratorVersion;
+ public WwiseProjectId WwiseProjectId { get; } = wwiseProjectId;
public WsModelVersion WsModelVersion { get; } = wsModelVersion;
- public List CompressionFormats { get; } = CompressionFormats;
+ public List CompressionFormats { get; } = compressionFormats;
}
public static class GameInformationDatabase
{
- static public List Games { get; private set; } // Convert to dictionary
+ public static Dictionary Games { get; private set; }
static GameInformationDatabase()
{
- var warhammer = new GameInformation(GameTypeEnum.Warhammer, "Warhammer", PackFileVersion.PFH4, GameBnkVersion.Unsupported, WsModelVersion.Unknown, [CompressionFormat.None]);
- var warhammer2 = new GameInformation(GameTypeEnum.Warhammer2, "Warhammer II", PackFileVersion.PFH5, GameBnkVersion.Unsupported, WsModelVersion.Version1, [CompressionFormat.Lzma1]);
- var warhammer3 = new GameInformation(GameTypeEnum.Warhammer3, "Warhammer III", PackFileVersion.PFH5, GameBnkVersion.Warhammer3, WsModelVersion.Version3, [CompressionFormat.Lzma1, CompressionFormat.Lz4, CompressionFormat.Zstd]);
- var troy = new GameInformation(GameTypeEnum.Troy, "Troy", PackFileVersion.PFH5, GameBnkVersion.Unsupported, WsModelVersion.Unknown, [CompressionFormat.Lzma1]);
- var threeKingdoms = new GameInformation( GameTypeEnum.ThreeKingdoms, "Three Kingdoms", PackFileVersion.PFH5, GameBnkVersion.Unsupported, WsModelVersion.Version1, [CompressionFormat.Lzma1]);
- var rome2 = new GameInformation(GameTypeEnum.Rome2, "Rome II", PackFileVersion.PFH4, GameBnkVersion.Unsupported, WsModelVersion.Unknown, [CompressionFormat.None]);
- var attila = new GameInformation(GameTypeEnum.Attila, "Attila", PackFileVersion.PFH4, GameBnkVersion.Attila, WsModelVersion.Unknown, [CompressionFormat.None]);
- var pharaoh = new GameInformation(GameTypeEnum.Pharaoh, "Pharaoh", PackFileVersion.PFH5, GameBnkVersion.Unsupported, WsModelVersion.Unknown, [CompressionFormat.Lzma1]);
-
- Games = [warhammer, warhammer2, warhammer3, troy, threeKingdoms, rome2, attila, pharaoh];
+ var warhammer = new GameInformation(
+ GameTypeEnum.Warhammer,
+ "Warhammer",
+ PackFileVersion.PFH4,
+ GameBnkVersion.Unsupported,
+ WwiseProjectId.Unsupported,
+ WsModelVersion.Unknown,
+ [CompressionFormat.None]);
+
+ var warhammer2 = new GameInformation(
+ GameTypeEnum.Warhammer2,
+ "Warhammer II",
+ PackFileVersion.PFH5,
+ GameBnkVersion.Unsupported,
+ WwiseProjectId.Unsupported,
+ WsModelVersion.Version1,
+ [CompressionFormat.Lzma1]);
+
+ var warhammer3 = new GameInformation(
+ GameTypeEnum.Warhammer3,
+ "Warhammer III",
+ PackFileVersion.PFH5,
+ GameBnkVersion.Warhammer3,
+ WwiseProjectId.Warhammer3,
+ WsModelVersion.Version3,
+ [CompressionFormat.Lzma1, CompressionFormat.Lz4, CompressionFormat.Zstd]);
+
+ var troy = new GameInformation(
+ GameTypeEnum.Troy,
+ "Troy",
+ PackFileVersion.PFH5,
+ GameBnkVersion.Unsupported,
+ WwiseProjectId.Unsupported,
+ WsModelVersion.Unknown,
+ [CompressionFormat.Lzma1]);
+
+ var threeKingdoms = new GameInformation(
+ GameTypeEnum.ThreeKingdoms,
+ "Three Kingdoms",
+ PackFileVersion.PFH5,
+ GameBnkVersion.Unsupported,
+ WwiseProjectId.Unsupported,
+ WsModelVersion.Version1,
+ [CompressionFormat.Lzma1]);
+
+ var rome2 = new GameInformation(
+ GameTypeEnum.Rome2,
+ "Rome II",
+ PackFileVersion.PFH4,
+ GameBnkVersion.Unsupported,
+ WwiseProjectId.Unsupported,
+ WsModelVersion.Unknown,
+ [CompressionFormat.None]);
+
+ var attila = new GameInformation(
+ GameTypeEnum.Attila,
+ "Attila",
+ PackFileVersion.PFH4,
+ GameBnkVersion.Attila,
+ WwiseProjectId.Unsupported,
+ WsModelVersion.Unknown,
+ [CompressionFormat.None]);
+
+ var pharaoh = new GameInformation(
+ GameTypeEnum.Pharaoh,
+ "Pharaoh",
+ PackFileVersion.PFH5,
+ GameBnkVersion.Unsupported,
+ WwiseProjectId.Unsupported,
+ WsModelVersion.Unknown,
+ [CompressionFormat.Lzma1]);
+
+ Games = new Dictionary
+ {
+ { GameTypeEnum.Warhammer, warhammer },
+ { GameTypeEnum.Warhammer2, warhammer2 },
+ { GameTypeEnum.Warhammer3, warhammer3 },
+ { GameTypeEnum.Troy, troy },
+ { GameTypeEnum.ThreeKingdoms, threeKingdoms },
+ { GameTypeEnum.Rome2, rome2 },
+ { GameTypeEnum.Attila, attila },
+ { GameTypeEnum.Pharaoh, pharaoh }
+ };
}
public static GameInformation GetGameById(GameTypeEnum type)
{
- return Games.First(x => x.Type == type);
+ return Games[type];
}
public static string GetEnumAsString(GameTypeEnum game)
diff --git a/Shared/SharedUI/BaseDialogs/PackFileTree/ContextMenu/Commands/ImportDirectoryCommand.cs b/Shared/SharedUI/BaseDialogs/PackFileTree/ContextMenu/Commands/ImportDirectoryCommand.cs
index f35b247de..d105c6f1a 100644
--- a/Shared/SharedUI/BaseDialogs/PackFileTree/ContextMenu/Commands/ImportDirectoryCommand.cs
+++ b/Shared/SharedUI/BaseDialogs/PackFileTree/ContextMenu/Commands/ImportDirectoryCommand.cs
@@ -23,29 +23,38 @@ public void Execute(TreeNode _selectedNode)
var dialog = new FolderBrowserDialog();
if (dialog.ShowDialog() == DialogResult.OK)
{
- var parentPath = _selectedNode.GetFullPath();
- var originalFilePaths = Directory.GetFiles(parentPath, "*", SearchOption.AllDirectories);
- var filePaths = originalFilePaths.Select(x => x.Replace(dialog.SelectedPath + "\\", "")).ToList();
- if (!string.IsNullOrWhiteSpace(parentPath))
- parentPath += "\\";
+ var folderPath = dialog.SelectedPath;
+ var folderName = new DirectoryInfo(folderPath).Name;
+ var originalFilePaths = Directory.GetFiles(folderPath, "*", SearchOption.AllDirectories);
+ var filePaths = originalFilePaths.Select(x => x.Replace($"{folderPath}\\", "")).ToList();
+
+ var packNodeParentPath = _selectedNode.GetFullPath();
+ if (!string.IsNullOrWhiteSpace(packNodeParentPath))
+ packNodeParentPath += "\\";
var filesAdded = new List();
for (var i = 0; i < filePaths.Count; i++)
{
var currentPath = filePaths[i];
- var filename = Path.GetFileName(currentPath);
+ var fileName = Path.GetFileName(currentPath);
+
+ var packDirectoryPath = $"{packNodeParentPath.ToLower()}{folderName}";
+
+ var directoryPath = string.Empty;
+ if (currentPath != fileName)
+ {
+ directoryPath = currentPath.Replace($"\\{fileName}", string.Empty).ToLower();
+ packDirectoryPath = $"{packDirectoryPath}\\{directoryPath}";
+ }
var source = MemorySource.FromFile(originalFilePaths[i]);
- var file = new PackFile(filename, source);
- filesAdded.Add(new NewPackFileEntry(parentPath.ToLower(), file));
+ var file = new PackFile(fileName, source);
+ filesAdded.Add(new NewPackFileEntry(packDirectoryPath, file));
}
packFileService.AddFilesToPack(_selectedNode.FileOwner, filesAdded);
}
}
}
-
-
-
}
diff --git a/Shared/SharedUI/BaseDialogs/StandardDialog/PackFile/PackFileBrowserWindow.xaml.cs b/Shared/SharedUI/BaseDialogs/StandardDialog/PackFile/PackFileBrowserWindow.xaml.cs
index 659673a19..c2b5d74a5 100644
--- a/Shared/SharedUI/BaseDialogs/StandardDialog/PackFile/PackFileBrowserWindow.xaml.cs
+++ b/Shared/SharedUI/BaseDialogs/StandardDialog/PackFile/PackFileBrowserWindow.xaml.cs
@@ -54,12 +54,23 @@ private void Button_Click(object sender, RoutedEventArgs e)
SelectedFile = ViewModel.SelectedItem?.Item;
if (ViewModel.SelectedItem?.NodeType == NodeType.Directory)
- SelectedFolder = ViewModel.SelectedItem?.Name;
+ SelectedFolder = GetFolderPath(ViewModel.SelectedItem, ViewModel.SelectedItem?.Name);
DialogResult = true;
Close();
}
+ private static string GetFolderPath(TreeNode node, string folderPath)
+ {
+ if (node.Parent?.NodeType == NodeType.Root)
+ return folderPath;
+ else
+ {
+ folderPath = $"{node.Parent.Name}\\{folderPath}";
+ return GetFolderPath(node.Parent, folderPath);
+ }
+ }
+
public void Dispose()
{
PreviewKeyDown -= HandleEsc;