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 + + + embedded AssetEdCommunity @@ -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;