From 90366750fa7d85940b0da3853ff51ee6c806d95e Mon Sep 17 00:00:00 2001 From: ole kristian homelien Date: Thu, 16 Apr 2026 21:09:08 +0200 Subject: [PATCH 1/8] First pass --- .github/agents/theasseteditor.agent.md | 1 + .../PackFiles/Models/PackFileContainer.cs | 147 ++++++- .../Shared.Core/PackFiles/PackFileService.cs | 142 +----- .../Models/PackFileContainerTests.cs | 411 ++++++++++++++++++ 4 files changed, 579 insertions(+), 122 deletions(-) create mode 100644 Shared/SharedCore/Shared.CoreTest/PackFiles/Models/PackFileContainerTests.cs diff --git a/.github/agents/theasseteditor.agent.md b/.github/agents/theasseteditor.agent.md index b6a4f3c75..53e81f141 100644 --- a/.github/agents/theasseteditor.agent.md +++ b/.github/agents/theasseteditor.agent.md @@ -70,6 +70,7 @@ When tests are mixed-framework, preserve existing framework choice (NUnit and MS - Avoid coupling business logic to UI framework types where possible; keep logic behind testable seams. - Prefer existing abstractions and dependency injection instead of creating new layers purely for mocking. - Avoid designs that require implementing fake classes to test core behavior. +- Test files should follow the same folder stucture as the file being tested. ## Safety Checklist Before Finalizing - Change is limited to requested scope. diff --git a/Shared/SharedCore/Shared.Core/PackFiles/Models/PackFileContainer.cs b/Shared/SharedCore/Shared.Core/PackFiles/Models/PackFileContainer.cs index 79c76ad11..f76dec334 100644 --- a/Shared/SharedCore/Shared.Core/PackFiles/Models/PackFileContainer.cs +++ b/Shared/SharedCore/Shared.Core/PackFiles/Models/PackFileContainer.cs @@ -1,4 +1,8 @@ -namespace Shared.Core.PackFiles.Models +using Shared.Core.Misc; +using Shared.Core.PackFiles.Serialization; +using Shared.Core.Settings; + +namespace Shared.Core.PackFiles.Models { public class PackFileContainer { @@ -22,5 +26,146 @@ public void MergePackFileContainer(PackFileContainer other) FileList[item.Key] = item.Value; return; } + + public virtual List AddFiles(List newFiles) + { + foreach (var file in newFiles) + { + if (string.IsNullOrWhiteSpace(file.PackFile.Name)) + throw new Exception("PackFile name can not be empty"); + } + + foreach (var file in newFiles) + { + file.PackFile.Name = file.PackFile.Name.Trim(); + + var path = file.DirectoyPath.Trim(); + if (!string.IsNullOrWhiteSpace(path)) + path += "\\"; + path += file.PackFile.Name; + FileList[path.ToLower()] = file.PackFile; + } + + return newFiles.Select(x => x.PackFile).ToList(); + } + + public virtual PackFile DeleteFile(PackFile file) + { + var key = FileList.FirstOrDefault(x => x.Value == file).Key; + FileList.Remove(key); + return file; + } + + public virtual void DeleteFolder(string folder) + { + var filesToDelete = new List(); + foreach (var file in FileList) + { + var directory = Path.GetDirectoryName(file.Key); + if (directory == null) + continue; + + if (directory.StartsWith(folder, StringComparison.InvariantCultureIgnoreCase)) + filesToDelete.Add(file.Key); + } + + foreach (var item in filesToDelete) + FileList.Remove(item); + } + + public virtual PackFile? FindFile(string path) + { + var lowerPath = path.Replace('/', '\\').ToLower().Trim(); + return FileList.TryGetValue(lowerPath, out var value) ? value : null; + } + + public virtual string? GetFullPath(PackFile file) + { + var res = FileList.FirstOrDefault(x => ReferenceEquals(x.Value, file) + || string.Equals(x.Value.Name, file.Name, StringComparison.OrdinalIgnoreCase)).Key; + return string.IsNullOrWhiteSpace(res) ? null : res; + } + + public virtual void MoveFile(PackFile file, string newFolderPath) + { + var newFullPath = newFolderPath + "\\" + file.Name; + var key = FileList.FirstOrDefault(x => x.Value == file).Key; + FileList.Remove(key); + FileList[newFullPath] = file; + } + + public virtual string RenameDirectory(string currentNodeName, string newName) + { + var oldNodePath = currentNodeName; + var newNodePath = newName; + var lastSeparatorIndex = currentNodeName.LastIndexOf(Path.DirectorySeparatorChar); + if (lastSeparatorIndex != -1) + { + var parentPath = currentNodeName.Substring(0, lastSeparatorIndex); + newNodePath = parentPath + Path.DirectorySeparatorChar + newName; + } + + var oldPathPrefix = oldNodePath + Path.DirectorySeparatorChar; + var files = FileList + .Where(x => x.Key.Equals(oldNodePath, StringComparison.InvariantCultureIgnoreCase) + || x.Key.StartsWith(oldPathPrefix, StringComparison.InvariantCultureIgnoreCase)) + .ToList(); + + foreach (var (path, file) in files) + { + FileList.Remove(path); + var newPath = newNodePath; + if (oldNodePath.Length != 0 && path.Length > oldNodePath.Length) + newPath = newNodePath + path.Substring(oldNodePath.Length); + + FileList[newPath] = file; + } + + return newNodePath; + } + + public virtual void RenameFile(PackFile file, string newName) + { + var key = FileList.FirstOrDefault(x => x.Value == file).Key; + FileList.Remove(key); + + var dir = Path.GetDirectoryName(key); + file.Name = newName; + FileList[dir + "\\" + file.Name] = file; + } + + public virtual void SaveFileData(PackFile file, byte[] data) + { + file.DataSource = new MemorySource(data); + } + + public virtual void SaveToDisk(string path, bool createBackup, GameInformation gameInformation) + { + if (File.Exists(path) && DirectoryHelper.IsFileLocked(path)) + throw new IOException($"Cannot access {path} because another process has locked it, most likely the game."); + + if (createBackup) + SaveUtility.CreateFileBackup(path); + + if (OriginalLoadByteSize != -1) + { + var fileInfo = new FileInfo(SystemFilePath); + var byteSize = fileInfo.Length; + if (byteSize != OriginalLoadByteSize) + throw new Exception("File has been changed outside of AssetEditor. Can not save the file as it will cause corruptions"); + } + + using (var memoryStream = new FileStream(path + "_temp", FileMode.Create)) + { + using var writer = new BinaryWriter(memoryStream); + PackFileSerializerWriter.SaveToByteArray(path, this, writer, gameInformation); + } + + File.Delete(path); + File.Move(path + "_temp", path); + + SystemFilePath = path; + OriginalLoadByteSize = new FileInfo(path).Length; + } } } diff --git a/Shared/SharedCore/Shared.Core/PackFiles/PackFileService.cs b/Shared/SharedCore/Shared.Core/PackFiles/PackFileService.cs index 5a1cfa836..bb1867227 100644 --- a/Shared/SharedCore/Shared.Core/PackFiles/PackFileService.cs +++ b/Shared/SharedCore/Shared.Core/PackFiles/PackFileService.cs @@ -86,25 +86,8 @@ public void AddFilesToPack(PackFileContainer container, List n if (container.IsCaPackFile) throw new Exception("Can not add files to ca pack file"); - foreach (var file in newFiles) - { - if (string.IsNullOrWhiteSpace(file.PackFile.Name)) - throw new Exception("PackFile name can not be empty"); - } - - foreach (var file in newFiles) - { - file.PackFile.Name = file.PackFile.Name.Trim(); - - var path = file.DirectoyPath.Trim(); - if (!string.IsNullOrWhiteSpace(path)) - path += "\\"; - path += file.PackFile.Name; - container.FileList[path.ToLower()] = file.PackFile; - } - - var files = newFiles.Select(x => x.PackFile).ToList(); - _globalEventHub?.PublishGlobalEvent(new PackFileContainerFilesAddedEvent(container, files)); + var addedFiles = container.AddFiles(newFiles); + _globalEventHub?.PublishGlobalEvent(new PackFileContainerFilesAddedEvent(container, addedFiles)); } public void CopyFileFromOtherPackFile(PackFileContainer source, string path, PackFileContainer target) @@ -151,24 +134,8 @@ public void DeleteFolder(PackFileContainer pf, string folder) if (pf.IsCaPackFile) throw new Exception("Can not delete folder inside CA pack file"); - var filesToDelete = new List(); - foreach (var file in pf.FileList) - { - var directory = Path.GetDirectoryName(file.Key); - if (directory == null) - continue; - - if (directory.StartsWith(folder, StringComparison.InvariantCultureIgnoreCase)) - filesToDelete.Add(file.Key); - } - _globalEventHub?.PublishGlobalEvent(new PackFileContainerFolderRemovedEvent(pf, folder)); - - foreach (var item in filesToDelete) - { - _logger.Here().Information($"Deleting file {item} in directory {folder}"); - pf.FileList.Remove(item); - } + pf.DeleteFolder(folder); } public void DeleteFile(PackFileContainer pf, PackFile file) @@ -176,11 +143,9 @@ public void DeleteFile(PackFileContainer pf, PackFile file) if (pf.IsCaPackFile) throw new Exception("Can not delete files inside CA pack file"); - var key = pf.FileList.FirstOrDefault(x => x.Value == file).Key; - _logger.Here().Information($"Deleting file {key}"); - + _logger.Here().Information($"Deleting file {pf.GetFullPath(file)}"); _globalEventHub?.PublishGlobalEvent(new PackFileContainerFilesRemovedEvent(pf, [file])); - pf.FileList.Remove(key); + pf.DeleteFile(file); } public void MoveFile(PackFileContainer pf, PackFile file, string newFolderPath) @@ -188,16 +153,10 @@ public void MoveFile(PackFileContainer pf, PackFile file, string newFolderPath) if (pf.IsCaPackFile) throw new Exception("Can not move files inside CA pack file"); - var newFullPath = newFolderPath + "\\" + file.Name; - - var key = pf.FileList.FirstOrDefault(x => x.Value == file).Key; + var key = pf.GetFullPath(file); _globalEventHub?.PublishGlobalEvent(new PackFileContainerFilesRemovedEvent(pf, [file])); - - pf.FileList.Remove(key); - pf.FileList[newFullPath] = file; - + pf.MoveFile(file, newFolderPath); _logger.Here().Information($"Moving file {key}"); - _globalEventHub?.PublishGlobalEvent(new PackFileContainerFilesAddedEvent(pf, [file])); } @@ -209,31 +168,7 @@ public void RenameDirectory(PackFileContainer pf, string currentNodeName, string if (string.IsNullOrWhiteSpace(newName)) throw new Exception("Name can not be empty"); - var oldNodePath = currentNodeName; - var newNodePath = newName; - var lastSeparatorIndex = currentNodeName.LastIndexOf(Path.DirectorySeparatorChar); - if (lastSeparatorIndex != -1) - { - var parentPath = currentNodeName.Substring(0, lastSeparatorIndex); - newNodePath = parentPath + Path.DirectorySeparatorChar + newName; - } - - var oldPathPrefix = oldNodePath + Path.DirectorySeparatorChar; - var files = pf.FileList - .Where(x => x.Key.Equals(oldNodePath, StringComparison.InvariantCultureIgnoreCase) - || x.Key.StartsWith(oldPathPrefix, StringComparison.InvariantCultureIgnoreCase)) - .ToList(); - - foreach (var (path, file) in files) - { - pf.FileList.Remove(path); - var newPath = newNodePath; - if (oldNodePath.Length != 0 && path.Length > oldNodePath.Length) - newPath = newNodePath + path.Substring(oldNodePath.Length); - - pf.FileList[newPath] = file; - } - + var newNodePath = pf.RenameDirectory(currentNodeName, newName); _globalEventHub?.PublishGlobalEvent(new PackFileContainerFolderRenamedEvent(pf, newNodePath)); } @@ -245,13 +180,7 @@ public void RenameFile(PackFileContainer pf, PackFile file, string newName) if (string.IsNullOrWhiteSpace(newName)) throw new Exception("Name can not be empty"); - var key = pf.FileList.FirstOrDefault(x => x.Value == file).Key; - pf.FileList.Remove(key); - - var dir = Path.GetDirectoryName(key); - file.Name = newName; - pf.FileList[dir + "\\" + file.Name] = file; - + pf.RenameFile(file, newName); _globalEventHub?.PublishGlobalEvent(new PackFileContainerFilesUpdatedEvent(pf, [file])); } @@ -260,45 +189,18 @@ public void SaveFile(PackFile file, byte[] data) var pf = GetEditablePack(); if (pf.IsCaPackFile) throw new Exception("Can not save ca pack file"); - file.DataSource = new MemorySource(data); + pf.SaveFileData(file, data); _globalEventHub?.PublishGlobalEvent(new PackFileContainerFilesUpdatedEvent(pf, [file])); _globalEventHub?.PublishGlobalEvent(new PackFileSavedEvent(file)); } public void SavePackContainer(PackFileContainer pf, string path, bool createBackup, GameInformation gameInformation) { - if (File.Exists(path) && DirectoryHelper.IsFileLocked(path)) - { - throw new IOException($"Cannot access {path} because another process has locked it, most likely the game."); - } - if (pf.IsCaPackFile) throw new Exception("Can not save ca pack file"); - if (createBackup) - SaveUtility.CreateFileBackup(path); - - // Check if file has changed in size - if (pf.OriginalLoadByteSize != -1) - { - var fileInfo = new FileInfo(pf.SystemFilePath); - var byteSize = fileInfo.Length; - if (byteSize != pf.OriginalLoadByteSize) - throw new Exception("File has been changed outside of AssetEditor. Can not save the file as it will cause corruptions"); - } - - using (var memoryStream = new FileStream(path + "_temp", FileMode.Create)) - { - using var writer = new BinaryWriter(memoryStream); - PackFileSerializerWriter.SaveToByteArray(path, pf, writer, gameInformation); - } - - File.Delete(path); - File.Move(path + "_temp", path); - - pf.SystemFilePath = path; - pf.OriginalLoadByteSize = new FileInfo(path).Length; + pf.SaveToDisk(path, createBackup, gameInformation); _globalEventHub?.PublishGlobalEvent(new PackFileContainerSavedEvent(pf)); } @@ -316,27 +218,27 @@ public void SavePackContainer(PackFileContainer pf, string path, bool createBack public PackFile? FindFile(string path, PackFileContainer? container = null) { - var lowerPath = path.Replace('/', '\\').ToLower().Trim(); - if (container == null) { for (var i = _packFileContainers.Count - 1; i >= 0; i--) { - if (_packFileContainers[i].FileList.TryGetValue(lowerPath, out var value)) + var result = _packFileContainers[i].FindFile(path); + if (result != null) { if (EnableFileLookUpEvents) _globalEventHub?.PublishGlobalEvent(new PackFileLookUpEvent(path, _packFileContainers[i], true)); - return value; + return result; } } } else { - if (container.FileList.TryGetValue(lowerPath, out var value)) + var result = container.FindFile(path); + if (result != null) { if (EnableFileLookUpEvents) _globalEventHub?.PublishGlobalEvent(new PackFileLookUpEvent(path, container, true)); - return value; + return result; } } @@ -351,17 +253,15 @@ public string GetFullPath(PackFile file, PackFileContainer? container = null) { foreach (var pf in _packFileContainers) { - 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) + var res = pf.GetFullPath(file); + if (res != null) return res; } } else { - 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) + var res = container.GetFullPath(file); + if (res != null) return res; } diff --git a/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/PackFileContainerTests.cs b/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/PackFileContainerTests.cs new file mode 100644 index 000000000..1413559ba --- /dev/null +++ b/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/PackFileContainerTests.cs @@ -0,0 +1,411 @@ +using Shared.Core.PackFiles; +using Shared.Core.PackFiles.Models; + +namespace Shared.CoreTest.PackFiles.Models +{ + internal class PackFileContainer_AddFiles + { + [Test] + public void AddFiles_MultipleFiles() + { + var container = new PackFileContainer("Test"); + var newFiles = new List + { + new("Directory_0", new PackFile("file0.txt", null)), + new("Directory_0", new PackFile("file1.txt", null)), + new("Directory_1", new PackFile("file0.txt", null)), + new("", new PackFile("rootFile.txt", null)) + }; + + var result = container.AddFiles(newFiles); + + Assert.That(container.FileList.Count, Is.EqualTo(4)); + Assert.That(result.Count, Is.EqualTo(4)); + } + + [Test] + public void AddFiles_AddToRoot() + { + var container = new PackFileContainer("Test"); + var newFiles = new List + { + new("", new PackFile("rootFile.txt", null)) + }; + + container.AddFiles(newFiles); + + Assert.That(container.FileList.Count, Is.EqualTo(1)); + Assert.That(container.FileList.ContainsKey("rootfile.txt"), Is.True); + } + + [Test] + public void AddFiles_AddToFolder() + { + var container = new PackFileContainer("Test"); + var newFiles = new List + { + new("Directory_0", new PackFile("file0.txt", null)), + new("Directory_0", new PackFile("file1.txt", null)), + new("Directory_1", new PackFile("file0.txt", null)), + }; + + container.AddFiles(newFiles); + + Assert.That(container.FileList.Count, Is.EqualTo(3)); + } + + [Test] + public void AddFiles_FileNameConflict_Override() + { + var container = new PackFileContainer("Test"); + var newFiles = new List + { + new("Directory_0", new PackFile("file0.txt", null)), + }; + + container.AddFiles(newFiles); + container.AddFiles(newFiles); + + Assert.That(container.FileList.Count, Is.EqualTo(1)); + } + + [Test] + public void AddFiles_WhiteSpaceInName() + { + var container = new PackFileContainer("Test"); + var newFiles = new List + { + new("Directory_0 ", new PackFile("file0.txt ", null)), + }; + + container.AddFiles(newFiles); + + Assert.That(container.FileList.Count, Is.EqualTo(1)); + Assert.That(container.FileList.First().Key.Any(char.IsWhiteSpace), Is.False); + Assert.That(container.FileList.First().Value.Name.Any(char.IsWhiteSpace), Is.False); + } + + [Test] + public void AddFiles_EmptyFileName_Throws() + { + var container = new PackFileContainer("Test"); + var newFiles = new List + { + new("Directory_0", new PackFile("", null)), + }; + + Assert.Throws(() => container.AddFiles(newFiles)); + } + + [Test] + public void AddFiles_ReturnsAddedFiles() + { + var container = new PackFileContainer("Test"); + var file0 = new PackFile("file0.txt", null); + var file1 = new PackFile("file1.txt", null); + var newFiles = new List + { + new("Dir", file0), + new("Dir", file1), + }; + + var result = container.AddFiles(newFiles); + + Assert.That(result, Contains.Item(file0)); + Assert.That(result, Contains.Item(file1)); + } + } + + internal class PackFileContainer_DeleteFile + { + [Test] + public void DeleteFile_RemovesFile() + { + var container = CreateContainerWithFiles(); + var file = container.FileList.Values.First(); + + container.DeleteFile(file); + + Assert.That(container.FileList.Count, Is.EqualTo(3)); + Assert.That(container.FileList.Values, Does.Not.Contain(file)); + } + + [Test] + public void DeleteFile_ReturnsDeletedFile() + { + var container = CreateContainerWithFiles(); + var file = container.FileList.Values.First(); + + var result = container.DeleteFile(file); + + Assert.That(result, Is.EqualTo(file)); + } + + static PackFileContainer CreateContainerWithFiles() + { + var container = new PackFileContainer("Test"); + container.AddFiles(new List + { + new("Dir_0", new PackFile("file0.txt", null)), + new("Dir_0", new PackFile("file1.txt", null)), + new("Dir_1", new PackFile("file0.txt", null)), + new("", new PackFile("rootFile.txt", null)), + }); + return container; + } + } + + internal class PackFileContainer_DeleteFolder + { + [Test] + public void DeleteFolder_RemovesFilesInFolder() + { + var container = CreateTestContainer(); + + container.DeleteFolder("directory_0"); + + Assert.That(container.FileList.Count, Is.EqualTo(2)); + } + + [Test] + public void DeleteFolder_MissingFolder_NoEffect() + { + var container = CreateTestContainer(); + + container.DeleteFolder("nonexistent"); + + Assert.That(container.FileList.Count, Is.EqualTo(8)); + } + + [Test] + public void DeleteFolder_WithSubFolder() + { + var container = CreateTestContainer(); + + container.DeleteFolder("directory_0\\subfolder"); + + Assert.That(container.FileList.Count, Is.EqualTo(4)); + } + + static PackFileContainer CreateTestContainer() + { + var container = new PackFileContainer("Test"); + container.AddFiles(new List + { + new("Directory_0", new PackFile("file0.txt", null)), + new("Directory_0", new PackFile("file1.txt", null)), + new("Directory_0\\subfolder", new PackFile("subfile0.txt", null)), + new("Directory_0\\subfolder", new PackFile("subfile1.txt", null)), + new("Directory_0\\subfolder\\child", new PackFile("childFile0.txt", null)), + new("Directory_0\\subfolder\\child", new PackFile("childFile1.txt", null)), + new("Directory_1", new PackFile("file0.txt", null)), + new("", new PackFile("rootFile.txt", null)) + }); + Assert.That(container.FileList.Count, Is.EqualTo(8)); + return container; + } + } + + internal class PackFileContainer_FindFile + { + [Test] + public void FindFile_ExistingFile() + { + var container = new PackFileContainer("Test"); + var file = new PackFile("file0.txt", null); + container.AddFiles(new List { new("Dir", file) }); + + var result = container.FindFile("Dir\\file0.txt"); + + Assert.That(result, Is.EqualTo(file)); + } + + [Test] + public void FindFile_CaseInsensitive() + { + var container = new PackFileContainer("Test"); + var file = new PackFile("file0.txt", null); + container.AddFiles(new List { new("Dir", file) }); + + var result = container.FindFile("DIR\\FILE0.TXT"); + + Assert.That(result, Is.EqualTo(file)); + } + + [Test] + public void FindFile_ForwardSlash() + { + var container = new PackFileContainer("Test"); + var file = new PackFile("file0.txt", null); + container.AddFiles(new List { new("Dir", file) }); + + var result = container.FindFile("Dir/file0.txt"); + + Assert.That(result, Is.EqualTo(file)); + } + + [Test] + public void FindFile_NotFound_ReturnsNull() + { + var container = new PackFileContainer("Test"); + + var result = container.FindFile("nonexistent.txt"); + + Assert.That(result, Is.Null); + } + + [Test] + public void FindFile_TrimWhitespace() + { + var container = new PackFileContainer("Test"); + var file = new PackFile("file0.txt", null); + container.AddFiles(new List { new("Dir", file) }); + + var result = container.FindFile(" Dir\\file0.txt "); + + Assert.That(result, Is.EqualTo(file)); + } + } + + internal class PackFileContainer_GetFullPath + { + [Test] + public void GetFullPath_ByReference() + { + var container = new PackFileContainer("Test"); + var file = new PackFile("file0.txt", null); + container.AddFiles(new List { new("Dir", file) }); + + var result = container.GetFullPath(file); + + Assert.That(result, Is.EqualTo("dir\\file0.txt")); + } + + [Test] + public void GetFullPath_ByName() + { + var container = new PackFileContainer("Test"); + var file = new PackFile("file0.txt", null); + container.AddFiles(new List { new("Dir", file) }); + + var otherRef = new PackFile("file0.txt", null); + var result = container.GetFullPath(otherRef); + + Assert.That(result, Is.EqualTo("dir\\file0.txt")); + } + + [Test] + public void GetFullPath_NotFound_ReturnsNull() + { + var container = new PackFileContainer("Test"); + + var result = container.GetFullPath(new PackFile("nonexistent.txt", null)); + + Assert.That(result, Is.Null); + } + } + + internal class PackFileContainer_MoveFile + { + [Test] + public void MoveFile_MovesToNewFolder() + { + var container = new PackFileContainer("Test"); + var file = new PackFile("file0.txt", null); + container.AddFiles(new List { new("OldDir", file) }); + + container.MoveFile(file, "NewDir"); + + Assert.That(container.FileList.Count, Is.EqualTo(1)); + Assert.That(container.FileList.ContainsKey("NewDir\\file0.txt"), Is.True); + Assert.That(container.FileList.ContainsKey("olddir\\file0.txt"), Is.False); + } + + } + + internal class PackFileContainer_RenameDirectory + { + [Test] + public void RenameDirectory_TopLevel() + { + var container = new PackFileContainer("Test"); + container.AddFiles(new List + { + new("OldName", new PackFile("file0.txt", null)), + new("OldName", new PackFile("file1.txt", null)), + }); + + var newNodePath = container.RenameDirectory("oldname", "NewName"); + + Assert.That(newNodePath, Is.EqualTo("NewName")); + Assert.That(container.FileList.Count, Is.EqualTo(2)); + Assert.That(container.FileList.Keys.All(k => k.StartsWith("NewName")), Is.True); + } + + [Test] + public void RenameDirectory_Nested() + { + var container = new PackFileContainer("Test"); + container.AddFiles(new List + { + new("Parent\\OldChild", new PackFile("file0.txt", null)), + new("Parent\\OldChild\\Sub", new PackFile("file1.txt", null)), + }); + + var newNodePath = container.RenameDirectory("parent\\oldchild", "NewChild"); + + Assert.That(newNodePath, Is.EqualTo("parent\\NewChild")); + Assert.That(container.FileList.ContainsKey("parent\\NewChild\\file0.txt"), Is.True); + Assert.That(container.FileList.ContainsKey("parent\\NewChild\\sub\\file1.txt"), Is.True); + } + + [Test] + public void RenameDirectory_ReturnsNewNodePath() + { + var container = new PackFileContainer("Test"); + container.AddFiles(new List + { + new("Parent\\Child", new PackFile("file0.txt", null)), + }); + + var result = container.RenameDirectory("parent\\child", "RenamedChild"); + + Assert.That(result, Is.EqualTo("parent\\RenamedChild")); + } + } + + internal class PackFileContainer_RenameFile + { + [Test] + public void RenameFile_UpdatesNameAndPath() + { + var container = new PackFileContainer("Test"); + var file = new PackFile("old.txt", null); + container.AddFiles(new List { new("Dir", file) }); + + container.RenameFile(file, "new.txt"); + + Assert.That(file.Name, Is.EqualTo("new.txt")); + Assert.That(container.FileList.Count, Is.EqualTo(1)); + Assert.That(container.FileList.ContainsKey("dir\\new.txt"), Is.True); + } + + } + + internal class PackFileContainer_SaveFileData + { + [Test] + public void SaveFileData_UpdatesDataSource() + { + var container = new PackFileContainer("Test"); + var file = new PackFile("file.txt", new MemorySource([1, 2, 3])); + container.AddFiles(new List { new("Dir", file) }); + + var newData = new byte[] { 4, 5, 6, 7 }; + container.SaveFileData(file, newData); + + Assert.That(file.DataSource.ReadData(), Is.EqualTo(newData)); + } + + } +} From bd80cd220714474a3274929ca8f22c38bb1a848a Mon Sep 17 00:00:00 2001 From: ole kristian homelien Date: Thu, 16 Apr 2026 21:24:24 +0200 Subject: [PATCH 2/8] Second pass --- .../PackFiles/Models/PackFileContainer.cs | 14 +-- .../Shared.Core/PackFiles/PackFileService.cs | 4 +- .../Models/PackFileContainerTests.cs | 90 ++++++++++++++++++- .../PackFiles/PackFileServiceTest.cs | 30 +++++++ 4 files changed, 128 insertions(+), 10 deletions(-) diff --git a/Shared/SharedCore/Shared.Core/PackFiles/Models/PackFileContainer.cs b/Shared/SharedCore/Shared.Core/PackFiles/Models/PackFileContainer.cs index f76dec334..915bd7b7b 100644 --- a/Shared/SharedCore/Shared.Core/PackFiles/Models/PackFileContainer.cs +++ b/Shared/SharedCore/Shared.Core/PackFiles/Models/PackFileContainer.cs @@ -49,9 +49,11 @@ public virtual List AddFiles(List newFiles) return newFiles.Select(x => x.PackFile).ToList(); } - public virtual PackFile DeleteFile(PackFile file) + public virtual PackFile? DeleteFile(PackFile file) { var key = FileList.FirstOrDefault(x => x.Value == file).Key; + if (key == null) + return null; FileList.Remove(key); return file; } @@ -65,7 +67,8 @@ public virtual void DeleteFolder(string folder) if (directory == null) continue; - if (directory.StartsWith(folder, StringComparison.InvariantCultureIgnoreCase)) + if (directory.Equals(folder, StringComparison.InvariantCultureIgnoreCase) + || directory.StartsWith(folder + Path.DirectorySeparatorChar, StringComparison.InvariantCultureIgnoreCase)) filesToDelete.Add(file.Key); } @@ -91,7 +94,7 @@ public virtual void MoveFile(PackFile file, string newFolderPath) var newFullPath = newFolderPath + "\\" + file.Name; var key = FileList.FirstOrDefault(x => x.Value == file).Key; FileList.Remove(key); - FileList[newFullPath] = file; + FileList[newFullPath.ToLower()] = file; } public virtual string RenameDirectory(string currentNodeName, string newName) @@ -118,7 +121,7 @@ public virtual string RenameDirectory(string currentNodeName, string newName) if (oldNodePath.Length != 0 && path.Length > oldNodePath.Length) newPath = newNodePath + path.Substring(oldNodePath.Length); - FileList[newPath] = file; + FileList[newPath.ToLower()] = file; } return newNodePath; @@ -131,7 +134,8 @@ public virtual void RenameFile(PackFile file, string newName) var dir = Path.GetDirectoryName(key); file.Name = newName; - FileList[dir + "\\" + file.Name] = file; + var newPath = string.IsNullOrEmpty(dir) ? file.Name : dir + "\\" + file.Name; + FileList[newPath.ToLower()] = file; } public virtual void SaveFileData(PackFile file, byte[] data) diff --git a/Shared/SharedCore/Shared.Core/PackFiles/PackFileService.cs b/Shared/SharedCore/Shared.Core/PackFiles/PackFileService.cs index bb1867227..e52032b1f 100644 --- a/Shared/SharedCore/Shared.Core/PackFiles/PackFileService.cs +++ b/Shared/SharedCore/Shared.Core/PackFiles/PackFileService.cs @@ -44,7 +44,7 @@ public PackFileService(IGlobalEventHub? globalEventHub) // Check if already added! foreach (var packFile in _packFileContainers) { - if (packFile.SystemFilePath == container.SystemFilePath) + if (packFile.SystemFilePath != null && packFile.SystemFilePath == container.SystemFilePath) { MessageBoxProvider.ShowDialogBox($"Pack file \"{packFile.SystemFilePath}\" is already loaded.", "Error"); return null; @@ -187,6 +187,8 @@ public void RenameFile(PackFileContainer pf, PackFile file, string newName) public void SaveFile(PackFile file, byte[] data) { var pf = GetEditablePack(); + if (pf == null) + throw new Exception("No editable pack file is set"); if (pf.IsCaPackFile) throw new Exception("Can not save ca pack file"); diff --git a/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/PackFileContainerTests.cs b/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/PackFileContainerTests.cs index 1413559ba..3e6453566 100644 --- a/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/PackFileContainerTests.cs +++ b/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/PackFileContainerTests.cs @@ -153,6 +153,18 @@ static PackFileContainer CreateContainerWithFiles() }); return container; } + + [Test] + public void DeleteFile_FileNotInContainer_ReturnsNull() + { + var container = CreateContainerWithFiles(); + var orphanFile = new PackFile("orphan.txt", null); + + var result = container.DeleteFile(orphanFile); + + Assert.That(result, Is.Null); + Assert.That(container.FileList.Count, Is.EqualTo(4)); + } } internal class PackFileContainer_DeleteFolder @@ -187,6 +199,22 @@ public void DeleteFolder_WithSubFolder() Assert.That(container.FileList.Count, Is.EqualTo(4)); } + [Test] + public void DeleteFolder_SimilarPrefixFolder_DoesNotDeleteWrongFiles() + { + var container = new PackFileContainer("Test"); + container.AddFiles(new List + { + new("dir", new PackFile("file0.txt", null)), + new("directory", new PackFile("file1.txt", null)), + }); + + container.DeleteFolder("dir"); + + Assert.That(container.FileList.Count, Is.EqualTo(1)); + Assert.That(container.FindFile("directory\\file1.txt"), Is.Not.Null); + } + static PackFileContainer CreateTestContainer() { var container = new PackFileContainer("Test"); @@ -317,10 +345,23 @@ public void MoveFile_MovesToNewFolder() container.MoveFile(file, "NewDir"); Assert.That(container.FileList.Count, Is.EqualTo(1)); - Assert.That(container.FileList.ContainsKey("NewDir\\file0.txt"), Is.True); + Assert.That(container.FileList.ContainsKey("newdir\\file0.txt"), Is.True); Assert.That(container.FileList.ContainsKey("olddir\\file0.txt"), Is.False); } + [Test] + public void MoveFile_NormalizesPathCasing() + { + var container = new PackFileContainer("Test"); + var file = new PackFile("file0.txt", null); + container.AddFiles(new List { new("OldDir", file) }); + + container.MoveFile(file, "NewDir"); + + var found = container.FindFile("NewDir\\file0.txt"); + Assert.That(found, Is.Not.Null); + } + } internal class PackFileContainer_RenameDirectory @@ -339,7 +380,7 @@ public void RenameDirectory_TopLevel() Assert.That(newNodePath, Is.EqualTo("NewName")); Assert.That(container.FileList.Count, Is.EqualTo(2)); - Assert.That(container.FileList.Keys.All(k => k.StartsWith("NewName")), Is.True); + Assert.That(container.FileList.Keys.All(k => k.StartsWith("newname", StringComparison.OrdinalIgnoreCase)), Is.True); } [Test] @@ -355,8 +396,8 @@ public void RenameDirectory_Nested() var newNodePath = container.RenameDirectory("parent\\oldchild", "NewChild"); Assert.That(newNodePath, Is.EqualTo("parent\\NewChild")); - Assert.That(container.FileList.ContainsKey("parent\\NewChild\\file0.txt"), Is.True); - Assert.That(container.FileList.ContainsKey("parent\\NewChild\\sub\\file1.txt"), Is.True); + Assert.That(container.FileList.ContainsKey("parent\\newchild\\file0.txt"), Is.True); + Assert.That(container.FileList.ContainsKey("parent\\newchild\\sub\\file1.txt"), Is.True); } [Test] @@ -372,6 +413,21 @@ public void RenameDirectory_ReturnsNewNodePath() Assert.That(result, Is.EqualTo("parent\\RenamedChild")); } + + [Test] + public void RenameDirectory_NormalizesPathCasing() + { + var container = new PackFileContainer("Test"); + container.AddFiles(new List + { + new("OldDir", new PackFile("file.txt", null)), + }); + + container.RenameDirectory("olddir", "NewDir"); + + var found = container.FindFile("NewDir\\file.txt"); + Assert.That(found, Is.Not.Null); + } } internal class PackFileContainer_RenameFile @@ -390,6 +446,32 @@ public void RenameFile_UpdatesNameAndPath() Assert.That(container.FileList.ContainsKey("dir\\new.txt"), Is.True); } + [Test] + public void RenameFile_RootFile_ProducesValidPath() + { + var container = new PackFileContainer("Test"); + var file = new PackFile("old.txt", null); + container.AddFiles(new List { new("", file) }); + + container.RenameFile(file, "new.txt"); + + Assert.That(container.FileList.ContainsKey("new.txt"), Is.True); + Assert.That(container.FileList.Keys.Any(k => k.StartsWith("\\")), Is.False); + } + + [Test] + public void RenameFile_NormalizesPathCasing() + { + var container = new PackFileContainer("Test"); + var file = new PackFile("old.txt", null); + container.AddFiles(new List { new("Dir", file) }); + + container.RenameFile(file, "New.txt"); + + var found = container.FindFile("Dir\\New.txt"); + Assert.That(found, Is.Not.Null); + } + } internal class PackFileContainer_SaveFileData diff --git a/Shared/SharedCore/Shared.CoreTest/PackFiles/PackFileServiceTest.cs b/Shared/SharedCore/Shared.CoreTest/PackFiles/PackFileServiceTest.cs index 37fdf4c27..ace4369a1 100644 --- a/Shared/SharedCore/Shared.CoreTest/PackFiles/PackFileServiceTest.cs +++ b/Shared/SharedCore/Shared.CoreTest/PackFiles/PackFileServiceTest.cs @@ -161,5 +161,35 @@ public void CreateNewPackFileContainer_Rome() // UnloadPackContainer // SaveFile // SavePackContainer + + [Test] + public void SaveFile_NoEditablePack_ThrowsDescriptiveException() + { + var pfs = new PackFileService(null); + pfs.AddContainer(new PackFileContainer("Ca") { IsCaPackFile = true }); + var file = new PackFile("file.txt", new MemorySource([1, 2, 3])); + + var ex = Assert.Throws(() => pfs.SaveFile(file, [4, 5, 6])); + Assert.That(ex.Message, Does.Contain("No editable pack file is set")); + } + + [Test] + public void AddContainer_TwoContainersWithNullSystemFilePath_BothAdded() + { + var dialogProvider = new Mock(); + var pfs = new PackFileService(null); + pfs.MessageBoxProvider = dialogProvider.Object; + var ca = new PackFileContainer("Ca") { IsCaPackFile = true }; + pfs.AddContainer(ca); + + var container1 = new PackFileContainer("Pack1") { SystemFilePath = null }; + var container2 = new PackFileContainer("Pack2") { SystemFilePath = null }; + + pfs.AddContainer(container1); + var result = pfs.AddContainer(container2); + + Assert.That(result, Is.Not.Null); + Assert.That(pfs.GetAllPackfileContainers().Count, Is.EqualTo(3)); + } } } From 1b8bcdc2c6b154e0003ff2acd9a7860b411f196b Mon Sep 17 00:00:00 2001 From: ole kristian homelien Date: Sun, 19 Apr 2026 20:26:26 +0200 Subject: [PATCH 3/8] Code update --- .../AnimationBatchExportViewModel.cs | 4 +- .../AnimationRetargeTool_ComplexUseCase.cs | 2 +- .../AudioFilesExplorerViewModel.cs | 4 +- .../AudioFilesTreeBuilderService.cs | 4 +- .../Importing/DisplayImportFileToolCommand.cs | 2 +- .../GltfToRmv/GltfImporterSettings.cs | 2 +- .../GltfToRmv/Helper/AnimationBuilder.cs | 2 +- .../GltfToRmv/RmvToGltfImporterViewModel.cs | 2 +- .../Presentation/IImporterViewModel.cs | 2 +- .../Presentation/ImportWindow.xaml.cs | 2 +- .../Presentation/ImporterCoreViewModel.cs | 4 +- Editors/Ipc/IpcEditor/ExternalPackLoader.cs | 2 +- .../Services/SkeletonAnimationLookUpHelper.cs | 14 +- .../MaterialToWsMaterialSerializerTests.cs | 2 +- Shared/SharedCore/Shared.Core/Assembly.cs | 3 + .../Shared.Core/ErrorHandling/PackFileLog.cs | 4 +- .../Events/Global/PackFileSavedEvent.cs | 24 ++-- .../Shared.Core/PackFiles/IPackFileService.cs | 34 ++--- .../PackFiles/Models/IPackFileContainer.cs | 11 ++ .../PackFiles/Models/PackFileContainer.cs | 2 +- .../Shared.Core/PackFiles/PackFileService.cs | 131 ++++++++++-------- .../Utility/PackFileContainerLoader.cs | 12 +- .../Utility/PackFileServiceUtility.cs | 4 +- .../Services/TouchedFilesRecorder.cs | 2 +- .../PackFiles/PackFileService_DeleteFolder.cs | 2 +- .../PackFiles/Utility/FileCompressionTests.cs | 2 +- .../PackFiles/Utility/FileEncryptionTests.cs | 2 +- .../Services/FileSaveServiceTests.cs | 2 +- .../PackFileTree/PackFileBrowserViewModel.cs | 16 +-- .../BaseDialogs/PackFileTree/TreeNode.cs | 4 +- .../PackFileTree/TreeNodeSource.cs | 4 +- .../PackFileBrowserViewModelTests.cs | 8 +- .../Shared/Shared/AssetEditorTestRunner.cs | 10 +- 33 files changed, 175 insertions(+), 150 deletions(-) create mode 100644 Shared/SharedCore/Shared.Core/PackFiles/Models/IPackFileContainer.cs diff --git a/Editors/AnimationFragmentEditor/Editor.AnimationFragmentEditor/AnimationBatchExporter/AnimationBatchExportViewModel.cs b/Editors/AnimationFragmentEditor/Editor.AnimationFragmentEditor/AnimationBatchExporter/AnimationBatchExportViewModel.cs index 94e1784b5..4c1ef91cd 100644 --- a/Editors/AnimationFragmentEditor/Editor.AnimationFragmentEditor/AnimationBatchExporter/AnimationBatchExportViewModel.cs +++ b/Editors/AnimationFragmentEditor/Editor.AnimationFragmentEditor/AnimationBatchExporter/AnimationBatchExportViewModel.cs @@ -125,9 +125,9 @@ public void Close() public class PackFileListItem { - public PackFileContainer Container { get; private set; } + public IPackFileContainer Container { get; private set; } - public PackFileListItem(PackFileContainer item) + public PackFileListItem(IPackFileContainer item) { Name.Value = item.Name; Container = item; diff --git a/Editors/AnimationReTarget/Test.AnimatioReTarget/AnimationRetargeTool_ComplexUseCase.cs b/Editors/AnimationReTarget/Test.AnimatioReTarget/AnimationRetargeTool_ComplexUseCase.cs index cd834609c..a8cd5c4ff 100644 --- a/Editors/AnimationReTarget/Test.AnimatioReTarget/AnimationRetargeTool_ComplexUseCase.cs +++ b/Editors/AnimationReTarget/Test.AnimatioReTarget/AnimationRetargeTool_ComplexUseCase.cs @@ -108,7 +108,7 @@ private void Step4_GenerateAnimation(AssetEditorTestRunner runner, AnimationReta // Check player } - private void Step5_UpdateAnimationSettingsAndGenerateAnimation(AssetEditorTestRunner runner, AnimationRetargetEditor editor, PackFileContainer outputPackFile) + private void Step5_UpdateAnimationSettingsAndGenerateAnimation(AssetEditorTestRunner runner, AnimationRetargetEditor editor, IPackFileContainer outputPackFile) { Assert.That(outputPackFile.FileList.Count, Is.EqualTo(0)); diff --git a/Editors/Audio/AudioEditor/Presentation/AudioFilesExplorer/AudioFilesExplorerViewModel.cs b/Editors/Audio/AudioEditor/Presentation/AudioFilesExplorer/AudioFilesExplorerViewModel.cs index 670b3e728..d52e69be5 100644 --- a/Editors/Audio/AudioEditor/Presentation/AudioFilesExplorer/AudioFilesExplorerViewModel.cs +++ b/Editors/Audio/AudioEditor/Presentation/AudioFilesExplorer/AudioFilesExplorerViewModel.cs @@ -137,7 +137,7 @@ private void OnAudioProjectExplorerNodeSelected(AudioProjectExplorerNodeSelected private void OnAudioFilesChanged(AudioFilesChangedEvent e) => SetButtonEnablement(); - private void OnPackFileContainerSetAsMainEditable(PackFileContainer packFileContainer) + private void OnPackFileContainerSetAsMainEditable(IPackFileContainer packFileContainer) { if (packFileContainer == null) return; @@ -146,7 +146,7 @@ private void OnPackFileContainerSetAsMainEditable(PackFileContainer packFileCont RefreshAudioFilesTree(packFileContainer); } - private void RefreshAudioFilesTree(PackFileContainer packFileContainer) + private void RefreshAudioFilesTree(IPackFileContainer packFileContainer) { AudioFilesTree = _audioFilesTreeBuilder.BuildTree(packFileContainer); CacheRootWaveformVisualisations(); diff --git a/Editors/Audio/AudioEditor/Presentation/AudioFilesExplorer/AudioFilesTreeBuilderService.cs b/Editors/Audio/AudioEditor/Presentation/AudioFilesExplorer/AudioFilesTreeBuilderService.cs index 0d6c57178..a71c95f7a 100644 --- a/Editors/Audio/AudioEditor/Presentation/AudioFilesExplorer/AudioFilesTreeBuilderService.cs +++ b/Editors/Audio/AudioEditor/Presentation/AudioFilesExplorer/AudioFilesTreeBuilderService.cs @@ -9,12 +9,12 @@ namespace Editors.Audio.AudioEditor.Presentation.AudioFilesExplorer { public interface IAudioFilesTreeBuilderService { - ObservableCollection BuildTree(PackFileContainer editablePack); + ObservableCollection BuildTree(IPackFileContainer editablePack); } public class AudioFilesTreeBuilderService() : IAudioFilesTreeBuilderService { - public ObservableCollection BuildTree(PackFileContainer editablePack) + public ObservableCollection BuildTree(IPackFileContainer editablePack) { var wavFilePaths = editablePack.FileList .Where(x => x.Key.EndsWith(".wav", StringComparison.OrdinalIgnoreCase)) diff --git a/Editors/ImportExportEditor/Editors.ImportExport/Importing/DisplayImportFileToolCommand.cs b/Editors/ImportExportEditor/Editors.ImportExport/Importing/DisplayImportFileToolCommand.cs index 242096344..0af7dee63 100644 --- a/Editors/ImportExportEditor/Editors.ImportExport/Importing/DisplayImportFileToolCommand.cs +++ b/Editors/ImportExportEditor/Editors.ImportExport/Importing/DisplayImportFileToolCommand.cs @@ -22,7 +22,7 @@ public DisplayImportFileToolCommand(IAbstractFormFactory exportWin _importerViewModels = exporterViewModels; } - public void Execute(PackFileContainer packFileContainer, string packPath) + public void Execute(IPackFileContainer packFileContainer, string packPath) { var fileExtentionFilters = GetFileDialogFilters(); diff --git a/Editors/ImportExportEditor/Editors.ImportExport/Importing/Importers/GltfToRmv/GltfImporterSettings.cs b/Editors/ImportExportEditor/Editors.ImportExport/Importing/Importers/GltfToRmv/GltfImporterSettings.cs index 568882dce..73f8d28fd 100644 --- a/Editors/ImportExportEditor/Editors.ImportExport/Importing/Importers/GltfToRmv/GltfImporterSettings.cs +++ b/Editors/ImportExportEditor/Editors.ImportExport/Importing/Importers/GltfToRmv/GltfImporterSettings.cs @@ -7,7 +7,7 @@ public record GltfImporterSettings ( string InputGltfFile, string DestinationPackPath, - PackFileContainer DestinationPackFileContainer, + IPackFileContainer DestinationPackFileContainer, GameTypeEnum SelectedGame, bool ImportMeshes, bool ImportMaterials, diff --git a/Editors/ImportExportEditor/Editors.ImportExport/Importing/Importers/GltfToRmv/Helper/AnimationBuilder.cs b/Editors/ImportExportEditor/Editors.ImportExport/Importing/Importers/GltfToRmv/Helper/AnimationBuilder.cs index 082ba0761..af0eb085c 100644 --- a/Editors/ImportExportEditor/Editors.ImportExport/Importing/Importers/GltfToRmv/Helper/AnimationBuilder.cs +++ b/Editors/ImportExportEditor/Editors.ImportExport/Importing/Importers/GltfToRmv/Helper/AnimationBuilder.cs @@ -25,7 +25,7 @@ public record AnimationBuilderSettings( ModelRoot modelRoot, string skeletonName, float keysPerSecond, - PackFileContainer packFileContainer, + IPackFileContainer packFileContainer, string packPath ); diff --git a/Editors/ImportExportEditor/Editors.ImportExport/Importing/Presentation/GltfToRmv/RmvToGltfImporterViewModel.cs b/Editors/ImportExportEditor/Editors.ImportExport/Importing/Presentation/GltfToRmv/RmvToGltfImporterViewModel.cs index 2dbbd6513..81c8eca73 100644 --- a/Editors/ImportExportEditor/Editors.ImportExport/Importing/Presentation/GltfToRmv/RmvToGltfImporterViewModel.cs +++ b/Editors/ImportExportEditor/Editors.ImportExport/Importing/Presentation/GltfToRmv/RmvToGltfImporterViewModel.cs @@ -34,7 +34,7 @@ public RmvToGltfImporterViewModel(GltfImporter Importer) public ImportSupportEnum CanImportFile(PackFile file) => _Importer.CanImportFile(file); - public void Execute(PackFile importSource, string outputPath, PackFileContainer packFileContainer, GameTypeEnum gameType) + public void Execute(PackFile importSource, string outputPath, IPackFileContainer packFileContainer, GameTypeEnum gameType) { var settings = new GltfImporterSettings( InputGltfFile: importSource.Name, diff --git a/Editors/ImportExportEditor/Editors.ImportExport/Importing/Presentation/IImporterViewModel.cs b/Editors/ImportExportEditor/Editors.ImportExport/Importing/Presentation/IImporterViewModel.cs index 05b32a441..b71ca7f9b 100644 --- a/Editors/ImportExportEditor/Editors.ImportExport/Importing/Presentation/IImporterViewModel.cs +++ b/Editors/ImportExportEditor/Editors.ImportExport/Importing/Presentation/IImporterViewModel.cs @@ -13,7 +13,7 @@ public interface IImporterViewModel public string DisplayName { get; } string OutputExtension { get; } string[] InputExtensions { get; } // ADDed THIS! - public void Execute(PackFile exportSource, string outputPath, PackFileContainer packFileContainer, GameTypeEnum gameType); + public void Execute(PackFile exportSource, string outputPath, IPackFileContainer packFileContainer, GameTypeEnum gameType); public ImportSupportEnum CanImportFile(PackFile file); } diff --git a/Editors/ImportExportEditor/Editors.ImportExport/Importing/Presentation/ImportWindow.xaml.cs b/Editors/ImportExportEditor/Editors.ImportExport/Importing/Presentation/ImportWindow.xaml.cs index d3c1d94fb..5b42a6df6 100644 --- a/Editors/ImportExportEditor/Editors.ImportExport/Importing/Presentation/ImportWindow.xaml.cs +++ b/Editors/ImportExportEditor/Editors.ImportExport/Importing/Presentation/ImportWindow.xaml.cs @@ -29,7 +29,7 @@ public ImportWindow(ImporterCoreViewModel viewModel) DataContext = _viewModel; } - internal void Initialize(PackFileContainer packFileContainer, string packPath, string diskFile) + internal void Initialize(IPackFileContainer packFileContainer, string packPath, string diskFile) { _viewModel.Initialize(packFileContainer, packPath, diskFile); } diff --git a/Editors/ImportExportEditor/Editors.ImportExport/Importing/Presentation/ImporterCoreViewModel.cs b/Editors/ImportExportEditor/Editors.ImportExport/Importing/Presentation/ImporterCoreViewModel.cs index 2415322fb..4d1db4316 100644 --- a/Editors/ImportExportEditor/Editors.ImportExport/Importing/Presentation/ImporterCoreViewModel.cs +++ b/Editors/ImportExportEditor/Editors.ImportExport/Importing/Presentation/ImporterCoreViewModel.cs @@ -24,7 +24,7 @@ public partial class ImporterCoreViewModel : ObservableObject private readonly IEnumerable _exporterViewModels; PackFile? _inputFile; - PackFileContainer? _destPackFileContainer; + IPackFileContainer? _destPackFileContainer; string _packPath = ""; [ObservableProperty] IImporterViewModel? _selectedImporterViewModel; @@ -39,7 +39,7 @@ public ImporterCoreViewModel(IEnumerable exporterViewModels, _applicationSettings = applicationSettings; } - public void Initialize(PackFileContainer packFile, string packPath, string diskFile) + public void Initialize(IPackFileContainer packFile, string packPath, string diskFile) { _destPackFileContainer = packFile; _packPath = packPath; diff --git a/Editors/Ipc/IpcEditor/ExternalPackLoader.cs b/Editors/Ipc/IpcEditor/ExternalPackLoader.cs index e688481df..6383c4a2e 100644 --- a/Editors/Ipc/IpcEditor/ExternalPackLoader.cs +++ b/Editors/Ipc/IpcEditor/ExternalPackLoader.cs @@ -59,7 +59,7 @@ public Task EnsureLoadedAsync(string packPathOnDisk, Cancellatio } } - private PackFileContainer AddContainerOnUiThread(PackFileContainer container) + private IPackFileContainer AddContainerOnUiThread(IPackFileContainer container) { var app = Application.Current; if (app?.Dispatcher == null || app.Dispatcher.CheckAccess()) diff --git a/GameWorld/GameWorldCore/GameWorld.Core/Services/SkeletonAnimationLookUpHelper.cs b/GameWorld/GameWorldCore/GameWorld.Core/Services/SkeletonAnimationLookUpHelper.cs index 6e1e6fd37..0da908631 100644 --- a/GameWorld/GameWorldCore/GameWorld.Core/Services/SkeletonAnimationLookUpHelper.cs +++ b/GameWorld/GameWorldCore/GameWorld.Core/Services/SkeletonAnimationLookUpHelper.cs @@ -54,18 +54,18 @@ public void Dispose() _globalEventHub.UnRegister(this); } - void PackfileContainerRefresh(PackFileContainer packFileContainer) + void PackfileContainerRefresh(IPackFileContainer packFileContainer) { UnloadAnimationFromContainer(packFileContainer); LoadFromPackFileContainer(packFileContainer); } - void PackfileContainerRemove(PackFileContainer packFileContainer) + void PackfileContainerRemove(IPackFileContainer packFileContainer) { UnloadAnimationFromContainer(packFileContainer); } - void LoadFromPackFileContainer(PackFileContainer packFileContainer) + void LoadFromPackFileContainer(IPackFileContainer packFileContainer) { List skeletonFileNameList = []; Dictionary> animationList = []; @@ -127,7 +127,7 @@ void LoadFromPackFileContainer(PackFileContainer packFileContainer) } } - void FileDiscovered(byte[] byteChunk, PackFileContainer container, string fullPath, ref List skeletonFileNameList, ref Dictionary> animationList) + void FileDiscovered(byte[] byteChunk, IPackFileContainer container, string fullPath, ref List skeletonFileNameList, ref Dictionary> animationList) { var brokenFiles = new string[] { @@ -172,7 +172,7 @@ void FileDiscovered(byte[] byteChunk, PackFileContainer container, string fullPa } } - void UnloadAnimationFromContainer(PackFileContainer packFileContainer) + void UnloadAnimationFromContainer(IPackFileContainer packFileContainer) { lock (_threadLock) { @@ -266,13 +266,13 @@ public ObservableCollection GetAnimationsForSkeleton(string // Delete this piece of shit public class AnimationReference { - public AnimationReference(string animationFile, PackFileContainer container) + public AnimationReference(string animationFile, IPackFileContainer container) { AnimationFile = animationFile; Container = container; } public string AnimationFile { get; set; } - public PackFileContainer Container { get; set; } + public IPackFileContainer Container { get; set; } public override string ToString() { diff --git a/GameWorld/GameWorldCore/GameWorld.CoreTest/Rendering/Materials/Serialization/MaterialToWsMaterialSerializerTests.cs b/GameWorld/GameWorldCore/GameWorld.CoreTest/Rendering/Materials/Serialization/MaterialToWsMaterialSerializerTests.cs index e22016fed..d3a17c605 100644 --- a/GameWorld/GameWorldCore/GameWorld.CoreTest/Rendering/Materials/Serialization/MaterialToWsMaterialSerializerTests.cs +++ b/GameWorld/GameWorldCore/GameWorld.CoreTest/Rendering/Materials/Serialization/MaterialToWsMaterialSerializerTests.cs @@ -15,7 +15,7 @@ namespace GameWorld.Core.Test.Rendering.Materials.Serialization { internal class MaterialToWsMaterialSerializerTests { - PackFileContainer _outputPack; + IPackFileContainer _outputPack; IPackFileService _pfs; MaterialToWsMaterialSerializer _wsMaterialSerializer; diff --git a/Shared/SharedCore/Shared.Core/Assembly.cs b/Shared/SharedCore/Shared.Core/Assembly.cs index d68c11bf2..5884d3835 100644 --- a/Shared/SharedCore/Shared.Core/Assembly.cs +++ b/Shared/SharedCore/Shared.Core/Assembly.cs @@ -4,3 +4,6 @@ [assembly: InternalsVisibleTo("Test.GameWorld.Core")] [assembly: InternalsVisibleTo("GameWorld.CoreTest")] [assembly: InternalsVisibleTo("Shared.CoreTest")] +[assembly: InternalsVisibleTo("Shared.UiTest")] +[assembly: InternalsVisibleTo("Test.ImportExport")] +[assembly: InternalsVisibleTo("Test.AnimatioReTarget")] diff --git a/Shared/SharedCore/Shared.Core/ErrorHandling/PackFileLog.cs b/Shared/SharedCore/Shared.Core/ErrorHandling/PackFileLog.cs index d1e82dcea..25d9f8507 100644 --- a/Shared/SharedCore/Shared.Core/ErrorHandling/PackFileLog.cs +++ b/Shared/SharedCore/Shared.Core/ErrorHandling/PackFileLog.cs @@ -20,7 +20,7 @@ public static class PackFileLog private static readonly ILogger s_logger = Logging.CreateStatic(typeof(PackFileLog)); public static bool IsLoggingEnabled { get; set; } = true; - public static Dictionary GetCompressionInformation(PackFileContainer container) + internal static Dictionary GetCompressionInformation(PackFileContainer container) { var compressionInformation = new Dictionary(); if(IsLoggingEnabled == false) @@ -46,7 +46,7 @@ public static Dictionary GetCompressi return compressionInformation; } - public static void LogPackCompression(PackFileContainer container) + internal static void LogPackCompression(PackFileContainer container) { if (IsLoggingEnabled == false) return; diff --git a/Shared/SharedCore/Shared.Core/Events/Global/PackFileSavedEvent.cs b/Shared/SharedCore/Shared.Core/Events/Global/PackFileSavedEvent.cs index 74f2af1ba..760413d6b 100644 --- a/Shared/SharedCore/Shared.Core/Events/Global/PackFileSavedEvent.cs +++ b/Shared/SharedCore/Shared.Core/Events/Global/PackFileSavedEvent.cs @@ -3,22 +3,22 @@ namespace Shared.Core.Events.Global { public record PackFileSavedEvent(PackFile File); - public record PackFileContainerSavedEvent(PackFileContainer Container); - public record PackFileLookUpEvent(string FileName, PackFileContainer? Container, bool Found); + public record PackFileContainerSavedEvent(IPackFileContainer Container); + public record PackFileLookUpEvent(string FileName, IPackFileContainer? Container, bool Found); public abstract record PackFileContainerManipulationEvent(); - public record PackFileContainerAddedEvent(PackFileContainer Container) : PackFileContainerManipulationEvent; - public record PackFileContainerRemovedEvent(PackFileContainer Container) : PackFileContainerManipulationEvent; - public record PackFileContainerSetAsMainEditableEvent(PackFileContainer? Container); - public record PackFileContainerFilesUpdatedEvent(PackFileContainer Container, List ChangedFiles) : PackFileContainerManipulationEvent; - public record PackFileContainerFilesAddedEvent(PackFileContainer Container, List AddedFiles) : PackFileContainerManipulationEvent; - public record PackFileContainerFilesRemovedEvent(PackFileContainer Container, List RemovedFiles) : PackFileContainerManipulationEvent; - public record PackFileContainerFolderRemovedEvent(PackFileContainer Container, string Folder) : PackFileContainerManipulationEvent; - public record PackFileContainerFolderRenamedEvent(PackFileContainer Container, string NewNodePath) : PackFileContainerManipulationEvent; + public record PackFileContainerAddedEvent(IPackFileContainer Container) : PackFileContainerManipulationEvent; + public record PackFileContainerRemovedEvent(IPackFileContainer Container) : PackFileContainerManipulationEvent; + public record PackFileContainerSetAsMainEditableEvent(IPackFileContainer? Container); + public record PackFileContainerFilesUpdatedEvent(IPackFileContainer Container, List ChangedFiles) : PackFileContainerManipulationEvent; + public record PackFileContainerFilesAddedEvent(IPackFileContainer Container, List AddedFiles) : PackFileContainerManipulationEvent; + public record PackFileContainerFilesRemovedEvent(IPackFileContainer Container, List RemovedFiles) : PackFileContainerManipulationEvent; + public record PackFileContainerFolderRemovedEvent(IPackFileContainer Container, string Folder) : PackFileContainerManipulationEvent; + public record PackFileContainerFolderRenamedEvent(IPackFileContainer Container, string NewNodePath) : PackFileContainerManipulationEvent; - public class BeforePackFileContainerRemovedEvent(PackFileContainer removed) + public class BeforePackFileContainerRemovedEvent(IPackFileContainer removed) { - public PackFileContainer Removed { get; internal set; } = removed; + public IPackFileContainer Removed { get; internal set; } = removed; public bool AllowClose { get; set; } = true; } } diff --git a/Shared/SharedCore/Shared.Core/PackFiles/IPackFileService.cs b/Shared/SharedCore/Shared.Core/PackFiles/IPackFileService.cs index 5da08175d..50d354f68 100644 --- a/Shared/SharedCore/Shared.Core/PackFiles/IPackFileService.cs +++ b/Shared/SharedCore/Shared.Core/PackFiles/IPackFileService.cs @@ -8,23 +8,23 @@ public interface IPackFileService bool EnableFileLookUpEvents { get; set; } bool EnforceGameFilesMustBeLoaded { get; set; } - PackFileContainer? AddContainer(PackFileContainer container, bool setToMainPackIfFirst = false); - void AddFilesToPack(PackFileContainer container, List newFiles); - void CopyFileFromOtherPackFile(PackFileContainer source, string path, PackFileContainer target); - PackFileContainer CreateNewPackFileContainer(string name, PackFileVersion packFileVersion, PackFileCAType type, bool setEditablePack = false); - void DeleteFile(PackFileContainer pf, PackFile file); - void DeleteFolder(PackFileContainer pf, string folder); - PackFile? FindFile(string path, PackFileContainer? container = null); - List GetAllPackfileContainers(); - PackFileContainer? GetEditablePack(); - string GetFullPath(PackFile file, PackFileContainer? container = null); - PackFileContainer? GetPackFileContainer(PackFile file); - void MoveFile(PackFileContainer pf, PackFile file, string newFolderPath); - void RenameDirectory(PackFileContainer pf, string currentNodeName, string newName); - void RenameFile(PackFileContainer pf, PackFile file, string newName); + IPackFileContainer? AddContainer(IPackFileContainer container, bool setToMainPackIfFirst = false); + void AddFilesToPack(IPackFileContainer container, List newFiles); + void CopyFileFromOtherPackFile(IPackFileContainer source, string path, IPackFileContainer target); + IPackFileContainer CreateNewPackFileContainer(string name, PackFileVersion packFileVersion, PackFileCAType type, bool setEditablePack = false); + void DeleteFile(IPackFileContainer pf, PackFile file); + void DeleteFolder(IPackFileContainer pf, string folder); + PackFile? FindFile(string path, IPackFileContainer? container = null); + List GetAllPackfileContainers(); + IPackFileContainer? GetEditablePack(); + string GetFullPath(PackFile file, IPackFileContainer? container = null); + IPackFileContainer? GetPackFileContainer(PackFile file); + void MoveFile(IPackFileContainer pf, PackFile file, string newFolderPath); + void RenameDirectory(IPackFileContainer pf, string currentNodeName, string newName); + void RenameFile(IPackFileContainer pf, PackFile file, string newName); void SaveFile(PackFile file, byte[] data); - void SavePackContainer(PackFileContainer pf, string path, bool createBackup, GameInformation gameInformation); - void SetEditablePack(PackFileContainer? pf); - void UnloadPackContainer(PackFileContainer pf); + void SavePackContainer(IPackFileContainer pf, string path, bool createBackup, GameInformation gameInformation); + void SetEditablePack(IPackFileContainer? pf); + void UnloadPackContainer(IPackFileContainer pf); } } diff --git a/Shared/SharedCore/Shared.Core/PackFiles/Models/IPackFileContainer.cs b/Shared/SharedCore/Shared.Core/PackFiles/Models/IPackFileContainer.cs new file mode 100644 index 000000000..67a7a095d --- /dev/null +++ b/Shared/SharedCore/Shared.Core/PackFiles/Models/IPackFileContainer.cs @@ -0,0 +1,11 @@ +namespace Shared.Core.PackFiles.Models +{ + public interface IPackFileContainer + { + string Name { get; } + bool IsCaPackFile { get; set; } + string SystemFilePath { get; } + Dictionary FileList { get; } + HashSet SourcePackFilePaths { get; } + } +} diff --git a/Shared/SharedCore/Shared.Core/PackFiles/Models/PackFileContainer.cs b/Shared/SharedCore/Shared.Core/PackFiles/Models/PackFileContainer.cs index 915bd7b7b..5c446958e 100644 --- a/Shared/SharedCore/Shared.Core/PackFiles/Models/PackFileContainer.cs +++ b/Shared/SharedCore/Shared.Core/PackFiles/Models/PackFileContainer.cs @@ -4,7 +4,7 @@ namespace Shared.Core.PackFiles.Models { - public class PackFileContainer + internal class PackFileContainer : IPackFileContainer { public string Name { get; set; } public PFHeader Header { get; set; } diff --git a/Shared/SharedCore/Shared.Core/PackFiles/PackFileService.cs b/Shared/SharedCore/Shared.Core/PackFiles/PackFileService.cs index e52032b1f..01208cb72 100644 --- a/Shared/SharedCore/Shared.Core/PackFiles/PackFileService.cs +++ b/Shared/SharedCore/Shared.Core/PackFiles/PackFileService.cs @@ -27,14 +27,17 @@ public PackFileService(IGlobalEventHub? globalEventHub) _globalEventHub = globalEventHub; } - public List GetAllPackfileContainers() => _packFileContainers.ToList(); // Return a list of the list to avoid bugs! + private static PackFileContainer CastContainer(IPackFileContainer container) => (PackFileContainer)container; - public PackFileContainer? AddContainer(PackFileContainer container, bool setToMainPackIfFirst = false) + public List GetAllPackfileContainers() => _packFileContainers.Cast().ToList(); + + public IPackFileContainer? AddContainer(IPackFileContainer container, bool setToMainPackIfFirst = false) { + var pf = CastContainer(container); if (EnforceGameFilesMustBeLoaded) { var caPacksLoaded = _packFileContainers.Count(x => x.IsCaPackFile); - if (caPacksLoaded == 0 && container.IsCaPackFile == false) + if (caPacksLoaded == 0 && pf.IsCaPackFile == false) { MessageBoxProvider.ShowDialogBox("You are trying to load a pack file before loading CA packfile. Most editors EXPECT the CA packfiles to be loaded and will cause issues if they are not.\nFile not loaded!", "Error"); return null; @@ -44,15 +47,15 @@ public PackFileService(IGlobalEventHub? globalEventHub) // Check if already added! foreach (var packFile in _packFileContainers) { - if (packFile.SystemFilePath != null && packFile.SystemFilePath == container.SystemFilePath) + if (packFile.SystemFilePath != null && packFile.SystemFilePath == pf.SystemFilePath) { MessageBoxProvider.ShowDialogBox($"Pack file \"{packFile.SystemFilePath}\" is already loaded.", "Error"); return null; } } - AddContainerInternal(container, setToMainPackIfFirst); - return container; + AddContainerInternal(pf, setToMainPackIfFirst); + return pf; } void AddContainerInternal(PackFileContainer container, bool setToMainPackIfFirst = false) @@ -65,7 +68,7 @@ void AddContainerInternal(PackFileContainer container, bool setToMainPackIfFirst SetEditablePack(container); } - public PackFileContainer CreateNewPackFileContainer(string name, PackFileVersion packFileVersion, PackFileCAType type, bool setEditablePack = false) + public IPackFileContainer CreateNewPackFileContainer(string name, PackFileVersion packFileVersion, PackFileCAType type, bool setEditablePack = false) { if (string.IsNullOrWhiteSpace(name)) throw new Exception("Name can not be empty"); @@ -81,112 +84,121 @@ public PackFileContainer CreateNewPackFileContainer(string name, PackFileVersion return newPackFile; } - public void AddFilesToPack(PackFileContainer container, List newFiles) + public void AddFilesToPack(IPackFileContainer container, List newFiles) { - if (container.IsCaPackFile) + var pf = CastContainer(container); + if (pf.IsCaPackFile) throw new Exception("Can not add files to ca pack file"); - var addedFiles = container.AddFiles(newFiles); - _globalEventHub?.PublishGlobalEvent(new PackFileContainerFilesAddedEvent(container, addedFiles)); + var addedFiles = pf.AddFiles(newFiles); + _globalEventHub?.PublishGlobalEvent(new PackFileContainerFilesAddedEvent(pf, addedFiles)); } - public void CopyFileFromOtherPackFile(PackFileContainer source, string path, PackFileContainer target) + public void CopyFileFromOtherPackFile(IPackFileContainer source, string path, IPackFileContainer target) { + var sourceContainer = CastContainer(source); + var targetContainer = CastContainer(target); var lowerPath = path.Replace('/', '\\').ToLower().Trim(); - if (source.FileList.ContainsKey(lowerPath)) + if (sourceContainer.FileList.ContainsKey(lowerPath)) { - var file = source.FileList[lowerPath]; + var file = sourceContainer.FileList[lowerPath]; var data = file.DataSource.ReadData(); var newFile = new PackFile(file.Name, new MemorySource(data)); - target.FileList[lowerPath] = newFile; + targetContainer.FileList[lowerPath] = newFile; - _globalEventHub?.PublishGlobalEvent(new PackFileContainerFilesAddedEvent(target, [newFile])); + _globalEventHub?.PublishGlobalEvent(new PackFileContainerFilesAddedEvent(targetContainer, [newFile])); } } - public void SetEditablePack(PackFileContainer? pf) + public void SetEditablePack(IPackFileContainer? pf) { if (pf != null && pf.IsCaPackFile) throw new Exception("Trying to set CA packfile container to be editable - this is not legal!"); - _packFileContainerSelectedForEdit = pf; + _packFileContainerSelectedForEdit = pf != null ? CastContainer(pf) : null; _globalEventHub?.PublishGlobalEvent(new PackFileContainerSetAsMainEditableEvent(pf)); } - public PackFileContainer? GetEditablePack() => _packFileContainerSelectedForEdit; + public IPackFileContainer? GetEditablePack() => _packFileContainerSelectedForEdit; - public void UnloadPackContainer(PackFileContainer pf) + public void UnloadPackContainer(IPackFileContainer pf) { - var e = new BeforePackFileContainerRemovedEvent(pf); + var container = CastContainer(pf); + var e = new BeforePackFileContainerRemovedEvent(container); _globalEventHub?.PublishGlobalEvent(e); if (e.AllowClose == false) return; - _packFileContainers.Remove(pf); - if (_packFileContainerSelectedForEdit == pf) + _packFileContainers.Remove(container); + if (_packFileContainerSelectedForEdit == container) SetEditablePack(null); - _globalEventHub?.PublishGlobalEvent(new PackFileContainerRemovedEvent(pf)); + _globalEventHub?.PublishGlobalEvent(new PackFileContainerRemovedEvent(container)); } - public void DeleteFolder(PackFileContainer pf, string folder) + public void DeleteFolder(IPackFileContainer pf, string folder) { - if (pf.IsCaPackFile) + var container = CastContainer(pf); + if (container.IsCaPackFile) throw new Exception("Can not delete folder inside CA pack file"); - _globalEventHub?.PublishGlobalEvent(new PackFileContainerFolderRemovedEvent(pf, folder)); - pf.DeleteFolder(folder); + _globalEventHub?.PublishGlobalEvent(new PackFileContainerFolderRemovedEvent(container, folder)); + container.DeleteFolder(folder); } - public void DeleteFile(PackFileContainer pf, PackFile file) + public void DeleteFile(IPackFileContainer pf, PackFile file) { - if (pf.IsCaPackFile) + var container = CastContainer(pf); + if (container.IsCaPackFile) throw new Exception("Can not delete files inside CA pack file"); - _logger.Here().Information($"Deleting file {pf.GetFullPath(file)}"); - _globalEventHub?.PublishGlobalEvent(new PackFileContainerFilesRemovedEvent(pf, [file])); - pf.DeleteFile(file); + _logger.Here().Information($"Deleting file {container.GetFullPath(file)}"); + _globalEventHub?.PublishGlobalEvent(new PackFileContainerFilesRemovedEvent(container, [file])); + container.DeleteFile(file); } - public void MoveFile(PackFileContainer pf, PackFile file, string newFolderPath) + public void MoveFile(IPackFileContainer pf, PackFile file, string newFolderPath) { - if (pf.IsCaPackFile) + var container = CastContainer(pf); + if (container.IsCaPackFile) throw new Exception("Can not move files inside CA pack file"); - var key = pf.GetFullPath(file); - _globalEventHub?.PublishGlobalEvent(new PackFileContainerFilesRemovedEvent(pf, [file])); - pf.MoveFile(file, newFolderPath); + var key = container.GetFullPath(file); + _globalEventHub?.PublishGlobalEvent(new PackFileContainerFilesRemovedEvent(container, [file])); + container.MoveFile(file, newFolderPath); _logger.Here().Information($"Moving file {key}"); - _globalEventHub?.PublishGlobalEvent(new PackFileContainerFilesAddedEvent(pf, [file])); + _globalEventHub?.PublishGlobalEvent(new PackFileContainerFilesAddedEvent(container, [file])); } - public void RenameDirectory(PackFileContainer pf, string currentNodeName, string newName) + public void RenameDirectory(IPackFileContainer pf, string currentNodeName, string newName) { - if (pf.IsCaPackFile) + var container = CastContainer(pf); + if (container.IsCaPackFile) throw new Exception("Can not rename in ca pack file"); if (string.IsNullOrWhiteSpace(newName)) throw new Exception("Name can not be empty"); - var newNodePath = pf.RenameDirectory(currentNodeName, newName); - _globalEventHub?.PublishGlobalEvent(new PackFileContainerFolderRenamedEvent(pf, newNodePath)); + var newNodePath = container.RenameDirectory(currentNodeName, newName); + _globalEventHub?.PublishGlobalEvent(new PackFileContainerFolderRenamedEvent(container, newNodePath)); } - public void RenameFile(PackFileContainer pf, PackFile file, string newName) + public void RenameFile(IPackFileContainer pf, PackFile file, string newName) { - if (pf.IsCaPackFile) + var container = CastContainer(pf); + if (container.IsCaPackFile) throw new Exception("Can not rename file in ca pack file"); if (string.IsNullOrWhiteSpace(newName)) throw new Exception("Name can not be empty"); - pf.RenameFile(file, newName); - _globalEventHub?.PublishGlobalEvent(new PackFileContainerFilesUpdatedEvent(pf, [file])); + container.RenameFile(file, newName); + _globalEventHub?.PublishGlobalEvent(new PackFileContainerFilesUpdatedEvent(container, [file])); } public void SaveFile(PackFile file, byte[] data) { - var pf = GetEditablePack(); + var pf = _packFileContainerSelectedForEdit; if (pf == null) throw new Exception("No editable pack file is set"); if (pf.IsCaPackFile) @@ -197,16 +209,17 @@ public void SaveFile(PackFile file, byte[] data) _globalEventHub?.PublishGlobalEvent(new PackFileSavedEvent(file)); } - public void SavePackContainer(PackFileContainer pf, string path, bool createBackup, GameInformation gameInformation) + public void SavePackContainer(IPackFileContainer pf, string path, bool createBackup, GameInformation gameInformation) { - if (pf.IsCaPackFile) + var container = CastContainer(pf); + if (container.IsCaPackFile) throw new Exception("Can not save ca pack file"); - pf.SaveToDisk(path, createBackup, gameInformation); - _globalEventHub?.PublishGlobalEvent(new PackFileContainerSavedEvent(pf)); + container.SaveToDisk(path, createBackup, gameInformation); + _globalEventHub?.PublishGlobalEvent(new PackFileContainerSavedEvent(container)); } - public PackFileContainer? GetPackFileContainer(PackFile file) + public IPackFileContainer? GetPackFileContainer(PackFile file) { foreach (var pf in _packFileContainers) { @@ -218,7 +231,7 @@ public void SavePackContainer(PackFileContainer pf, string path, bool createBack return null; } - public PackFile? FindFile(string path, PackFileContainer? container = null) + public PackFile? FindFile(string path, IPackFileContainer? container = null) { if (container == null) { @@ -235,11 +248,12 @@ public void SavePackContainer(PackFileContainer pf, string path, bool createBack } else { - var result = container.FindFile(path); + var concreteContainer = CastContainer(container); + var result = concreteContainer.FindFile(path); if (result != null) { if (EnableFileLookUpEvents) - _globalEventHub?.PublishGlobalEvent(new PackFileLookUpEvent(path, container, true)); + _globalEventHub?.PublishGlobalEvent(new PackFileLookUpEvent(path, concreteContainer, true)); return result; } } @@ -249,7 +263,7 @@ public void SavePackContainer(PackFileContainer pf, string path, bool createBack return null; } - public string GetFullPath(PackFile file, PackFileContainer? container = null) + public string GetFullPath(PackFile file, IPackFileContainer? container = null) { if (container == null) { @@ -262,7 +276,8 @@ public string GetFullPath(PackFile file, PackFileContainer? container = null) } else { - var res = container.GetFullPath(file); + var concreteContainer = CastContainer(container); + var res = concreteContainer.GetFullPath(file); if (res != null) return res; } diff --git a/Shared/SharedCore/Shared.Core/PackFiles/Utility/PackFileContainerLoader.cs b/Shared/SharedCore/Shared.Core/PackFiles/Utility/PackFileContainerLoader.cs index ead316b42..d6a725587 100644 --- a/Shared/SharedCore/Shared.Core/PackFiles/Utility/PackFileContainerLoader.cs +++ b/Shared/SharedCore/Shared.Core/PackFiles/Utility/PackFileContainerLoader.cs @@ -10,9 +10,9 @@ namespace Shared.Core.PackFiles.Utility { public interface IPackFileContainerLoader { - PackFileContainer? Load(string packFileSystemPath); - PackFileContainer? LoadAllCaFiles(GameTypeEnum gameEnum); - PackFileContainer LoadSystemFolderAsPackFileContainer(string packFileSystemPath); + IPackFileContainer? Load(string packFileSystemPath); + IPackFileContainer? LoadAllCaFiles(GameTypeEnum gameEnum); + IPackFileContainer LoadSystemFolderAsPackFileContainer(string packFileSystemPath); } @@ -26,7 +26,7 @@ public PackFileContainerLoader(ApplicationSettingsService settingsService) _settingsService = settingsService; } - public PackFileContainer LoadSystemFolderAsPackFileContainer(string packFileSystemPath) + public IPackFileContainer LoadSystemFolderAsPackFileContainer(string packFileSystemPath) { if (Directory.Exists(packFileSystemPath) == false) { @@ -61,7 +61,7 @@ private static void AddFolderContentToPackFile(PackFileContainer container, stri AddFolderContentToPackFile(container, folder, rootPath); } - public PackFileContainer? Load(string packFileSystemPath) + public IPackFileContainer? Load(string packFileSystemPath) { try { @@ -89,7 +89,7 @@ private static void AddFolderContentToPackFile(PackFileContainer container, stri } } - public PackFileContainer? LoadAllCaFiles(GameTypeEnum gameEnum) + public IPackFileContainer? LoadAllCaFiles(GameTypeEnum gameEnum) { var game = GameInformationDatabase.GetGameById(gameEnum); var gamePathInfo = _settingsService.CurrentSettings.GameDirectories.FirstOrDefault(x => x.Game == game.Type); diff --git a/Shared/SharedCore/Shared.Core/PackFiles/Utility/PackFileServiceUtility.cs b/Shared/SharedCore/Shared.Core/PackFiles/Utility/PackFileServiceUtility.cs index 7034be8e7..ceeb2bb63 100644 --- a/Shared/SharedCore/Shared.Core/PackFiles/Utility/PackFileServiceUtility.cs +++ b/Shared/SharedCore/Shared.Core/PackFiles/Utility/PackFileServiceUtility.cs @@ -61,13 +61,13 @@ public static List GetAllAnimPacks(IPackFileService pfs) } - public static List FindAllWithExtention(IPackFileService pfs, string extention, PackFileContainer? packFileContainer = null) + public static List FindAllWithExtention(IPackFileService pfs, string extention, IPackFileContainer? packFileContainer = null) { return FindAllWithExtentionIncludePaths(pfs, extention, packFileContainer).Select(x => x.Item2).ToList(); } - public static List<(string FileName, PackFile Pack)> FindAllWithExtentionIncludePaths(IPackFileService pfs, string extention, PackFileContainer? packFileContainer = null) + public static List<(string FileName, PackFile Pack)> FindAllWithExtentionIncludePaths(IPackFileService pfs, string extention, IPackFileContainer? packFileContainer = null) { extention = extention.ToLower(); var output = new List>(); diff --git a/Shared/SharedCore/Shared.Core/Services/TouchedFilesRecorder.cs b/Shared/SharedCore/Shared.Core/Services/TouchedFilesRecorder.cs index 5d9e2e223..221d836b4 100644 --- a/Shared/SharedCore/Shared.Core/Services/TouchedFilesRecorder.cs +++ b/Shared/SharedCore/Shared.Core/Services/TouchedFilesRecorder.cs @@ -10,7 +10,7 @@ namespace Shared.Core.Services public class TouchedFilesRecorder { readonly ILogger _logger = Logging.Create(); - readonly List<(string FilePath, PackFileContainer Container)> _files = new(); + readonly List<(string FilePath, IPackFileContainer Container)> _files = new(); readonly IPackFileService _pfs; private readonly IGlobalEventHub _eventHub; private readonly ApplicationSettingsService _applicationSettingsService; diff --git a/Shared/SharedCore/Shared.CoreTest/PackFiles/PackFileService_DeleteFolder.cs b/Shared/SharedCore/Shared.CoreTest/PackFiles/PackFileService_DeleteFolder.cs index 45064ba11..376cf6843 100644 --- a/Shared/SharedCore/Shared.CoreTest/PackFiles/PackFileService_DeleteFolder.cs +++ b/Shared/SharedCore/Shared.CoreTest/PackFiles/PackFileService_DeleteFolder.cs @@ -53,7 +53,7 @@ public void DeleteFolderWithSubFolder() static PackFileContainer CreateTestPack(IPackFileService pfs) { - var container = pfs.AddContainer(new PackFileContainer("Custom") { SystemFilePath = "SystemPath" }, true)!; + var container = (PackFileContainer)pfs.AddContainer(new PackFileContainer("Custom") { SystemFilePath = "SystemPath" }, true)!; var newFiles = new List { new("Directory_0", new PackFile("file0.txt", null)), diff --git a/Shared/SharedCore/Shared.CoreTest/PackFiles/Utility/FileCompressionTests.cs b/Shared/SharedCore/Shared.CoreTest/PackFiles/Utility/FileCompressionTests.cs index d147d2a9a..533dc86cc 100644 --- a/Shared/SharedCore/Shared.CoreTest/PackFiles/Utility/FileCompressionTests.cs +++ b/Shared/SharedCore/Shared.CoreTest/PackFiles/Utility/FileCompressionTests.cs @@ -11,7 +11,7 @@ namespace Shared.CoreTest.PackFiles.Utility internal class FileCompressionTests { private IPackFileService _packFileService; - private PackFileContainer _container; + private IPackFileContainer _container; [SetUp] public void Setup() diff --git a/Shared/SharedCore/Shared.CoreTest/PackFiles/Utility/FileEncryptionTests.cs b/Shared/SharedCore/Shared.CoreTest/PackFiles/Utility/FileEncryptionTests.cs index abe36dd3e..08134a013 100644 --- a/Shared/SharedCore/Shared.CoreTest/PackFiles/Utility/FileEncryptionTests.cs +++ b/Shared/SharedCore/Shared.CoreTest/PackFiles/Utility/FileEncryptionTests.cs @@ -11,7 +11,7 @@ namespace Shared.CoreTest.PackFiles.Utility internal class FileEncryptionTests { private IPackFileService _packFileService; - private PackFileContainer _container; + private IPackFileContainer _container; [SetUp] public void Setup() diff --git a/Shared/SharedCore/Shared.CoreTest/Services/FileSaveServiceTests.cs b/Shared/SharedCore/Shared.CoreTest/Services/FileSaveServiceTests.cs index 07cb74e2a..9df60cd3d 100644 --- a/Shared/SharedCore/Shared.CoreTest/Services/FileSaveServiceTests.cs +++ b/Shared/SharedCore/Shared.CoreTest/Services/FileSaveServiceTests.cs @@ -11,7 +11,7 @@ namespace Shared.CoreTest.Services internal class FileSaveServiceTests { IPackFileService _pfs; - PackFileContainer _container; + IPackFileContainer _container; Mock _uiProvider = new(); Mock _eventHub = new(); PackFile _fileHandle; diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/PackFileBrowserViewModel.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/PackFileBrowserViewModel.cs index 444ca35f0..7fcdd6611 100644 --- a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/PackFileBrowserViewModel.cs +++ b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/PackFileBrowserViewModel.cs @@ -39,7 +39,7 @@ public PackFileTreeState(TreeNodeSource rootSource, TreeNode rootNode) private readonly IWindowsKeyboard _windowKeyboard; private readonly ApplicationSettingsService _applicationSettingsService; private readonly IContextMenuBuilder _contextMenuBuilder; - private readonly Dictionary _treeStates = []; + private readonly Dictionary _treeStates = []; public event FileSelectedDelegate FileOpen; public event NodeSelectedDelegate NodeSelected; @@ -97,7 +97,7 @@ partial void OnSelectedItemChanged(TreeNode value) NodeSelected?.Invoke(_selectedItem); } - private void Database_PackFileFolderRemoved(PackFileContainer container, string folder) + private void Database_PackFileFolderRemoved(IPackFileContainer container, string folder) { var state = GetPackFileTreeState(container); var root = state.RootNode; @@ -118,7 +118,7 @@ private void Database_PackFileFolderRemoved(PackFileContainer container, string Filter.Reapply(); } - private void Database_PackFileFolderRenamed(PackFileContainer container, string folder) + private void Database_PackFileFolderRenamed(IPackFileContainer container, string folder) { var state = GetPackFileTreeState(container); var node = GetNodeFromPath(state.RootSource, folder, false); @@ -142,7 +142,7 @@ private void ContainerSaved(PackFileContainerSavedEvent e) Filter.Reapply(); } - private void Database_PackFilesRemoved(PackFileContainer container, List files) + private void Database_PackFilesRemoved(IPackFileContainer container, List files) { var state = GetPackFileTreeState(container); var root = state.RootNode; @@ -236,7 +236,7 @@ private void MainEditablePackChanged(PackFileContainerSetAsMainEditableEvent e) newContiner.IsMainEditabelPack = true; } - private void AddFiles(PackFileContainer container, List files) + private void AddFiles(IPackFileContainer container, List files) { var state = GetPackFileTreeState(container); var root = state.RootNode; @@ -394,7 +394,7 @@ private void AddFiles(PackFileContainer container, List files) return null; } - private TreeNodeSource? GetNodeFromPackFile(PackFileContainer container, PackFile pf, bool createIfMissing = true) + private TreeNodeSource? GetNodeFromPackFile(IPackFileContainer container, PackFile pf, bool createIfMissing = true) { var state = GetPackFileTreeState(container); var root = state.RootSource; @@ -415,7 +415,7 @@ private void AddFiles(PackFileContainer container, List files) } } - private void ReloadTree(PackFileContainer container) + private void ReloadTree(IPackFileContainer container) { if (_treeStates.TryGetValue(container, out var existingState)) { @@ -541,7 +541,7 @@ public bool Drop(TreeNode node, TreeNode targeNode) return true; } - private PackFileTreeState GetPackFileTreeState(PackFileContainer container) + private PackFileTreeState GetPackFileTreeState(IPackFileContainer container) { return _treeStates[container]; } diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/TreeNode.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/TreeNode.cs index f65ae5179..6167dfd1f 100644 --- a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/TreeNode.cs +++ b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/TreeNode.cs @@ -28,7 +28,7 @@ public partial class TreeNode : ObservableObject private bool _isExpandedByFilter; private readonly bool _isPlaceholder; - public PackFileContainer FileOwner { get; private set; } + public IPackFileContainer FileOwner { get; private set; } public PackFile? Item { get; set; } public TreeNode? Parent { get; set; } @@ -43,7 +43,7 @@ public partial class TreeNode : ObservableObject internal TreeNodeSource? Source => _source; internal bool HasMaterializedChildren => Children.Any(x => x._isPlaceholder == false); - public TreeNode(string name, NodeType type, PackFileContainer ower, TreeNode? parent, PackFile? packFile = null) + public TreeNode(string name, NodeType type, IPackFileContainer ower, TreeNode? parent, PackFile? packFile = null) { Name = name; Item = packFile; diff --git a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/TreeNodeSource.cs b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/TreeNodeSource.cs index e5b23b8f8..e3ab15fc1 100644 --- a/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/TreeNodeSource.cs +++ b/Shared/SharedUI/Shared.Ui/BaseDialogs/PackFileTree/TreeNodeSource.cs @@ -13,7 +13,7 @@ internal sealed class TreeNodeSource { public string Name { get; set; } public NodeType NodeType { get; } - public PackFileContainer FileOwner { get; } + public IPackFileContainer FileOwner { get; } public PackFile? Item { get; } public TreeNodeSource? Parent { get; private set; } public List Children { get; } = []; @@ -23,7 +23,7 @@ internal sealed class TreeNodeSource public bool HasChildren => Children.Count > 0; - public TreeNodeSource(string name, NodeType nodeType, PackFileContainer fileOwner, TreeNodeSource? parent, PackFile? item = null) + public TreeNodeSource(string name, NodeType nodeType, IPackFileContainer fileOwner, TreeNodeSource? parent, PackFile? item = null) { Name = name; NodeType = nodeType; diff --git a/Shared/SharedUI/Shared.UiTest/PackFileBrowserViewModelTests.cs b/Shared/SharedUI/Shared.UiTest/PackFileBrowserViewModelTests.cs index bf0d1440c..dd28fbee0 100644 --- a/Shared/SharedUI/Shared.UiTest/PackFileBrowserViewModelTests.cs +++ b/Shared/SharedUI/Shared.UiTest/PackFileBrowserViewModelTests.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.DependencyInjection; using Shared.Core.PackFiles; using Shared.Core.PackFiles.Models; +using Shared.Core.Settings; using Shared.Ui.BaseDialogs.PackFileTree; using Test.TestingUtility.Shared; using Test.TestingUtility.TestUtility; @@ -234,12 +235,7 @@ public void RenameFileUsingPfs_EnsureTreeViewUpdated() private void CreatePackfiles(params (string Path, string FileName)[] files) { - var container = new PackFileContainer("test.pack") - { - SystemFilePath = "test.pack", - Header = new PFHeader("PFH5", PackFileCAType.MOD) - }; - + var container = _packageFileService.CreateNewPackFileContainer("test.pack", PackFileVersion.PFH5, PackFileCAType.MOD); foreach (var (path, fileName) in files) { container.FileList[path.ToLowerInvariant()] = PackFile.CreateFromASCII(fileName, fileName); diff --git a/Testing/Shared/Shared/AssetEditorTestRunner.cs b/Testing/Shared/Shared/AssetEditorTestRunner.cs index a249c8792..b6c20826f 100644 --- a/Testing/Shared/Shared/AssetEditorTestRunner.cs +++ b/Testing/Shared/Shared/AssetEditorTestRunner.cs @@ -45,7 +45,7 @@ public AssetEditorTestRunner(GameTypeEnum gameEnum = GameTypeEnum.Warhammer3, bo Keyboard = ServiceProvider.GetRequiredService() as TestKeyboard; } - public PackFileContainer? LoadPackFile(string path, bool createOutputPackFile = true) + public IPackFileContainer? LoadPackFile(string path, bool createOutputPackFile = true) { var loader = ServiceProvider.GetRequiredService(); var container = loader.Load(path); @@ -56,7 +56,7 @@ public AssetEditorTestRunner(GameTypeEnum gameEnum = GameTypeEnum.Warhammer3, bo return null; } - public PackFileContainer? CreateCaContainer() + public IPackFileContainer? CreateCaContainer() { var caConainter = new PackFileContainer("CA") { @@ -73,7 +73,7 @@ public T GetRequiredServiceInCurrentEditorScope() return ScopeRepository.GetRequiredService(handle); } - public PackFileContainer LoadFolderPackFile(string path) + public IPackFileContainer LoadFolderPackFile(string path) { var loader = ServiceProvider.GetRequiredService(); var container = loader.LoadSystemFolderAsPackFileContainer(path); @@ -82,13 +82,13 @@ public PackFileContainer LoadFolderPackFile(string path) return container; } - public PackFileContainer CreateOutputPack() + public IPackFileContainer CreateOutputPack() { return PackFileService.CreateNewPackFileContainer("TestOutput", PackFileVersion.PFH5, PackFileCAType.MOD, true); } - public PackFileContainer CreateEmptyPackFile(string packFileName, bool setAsEditable) + public IPackFileContainer CreateEmptyPackFile(string packFileName, bool setAsEditable) { return PackFileService.CreateNewPackFileContainer(packFileName, PackFileVersion.PFH5, PackFileCAType.MOD, setAsEditable); } From 3bef6bfddffbfcfdfbe93e65195aabdb8b7ec616 Mon Sep 17 00:00:00 2001 From: ole kristian homelien Date: Sun, 19 Apr 2026 20:33:02 +0200 Subject: [PATCH 4/8] New interface --- .../Models/IPackFileContainerInternal.cs | 18 ++++++++++++++++++ .../PackFiles/Models/PackFileContainer.cs | 2 +- .../Shared.Core/PackFiles/PackFileService.cs | 9 ++++----- 3 files changed, 23 insertions(+), 6 deletions(-) create mode 100644 Shared/SharedCore/Shared.Core/PackFiles/Models/IPackFileContainerInternal.cs diff --git a/Shared/SharedCore/Shared.Core/PackFiles/Models/IPackFileContainerInternal.cs b/Shared/SharedCore/Shared.Core/PackFiles/Models/IPackFileContainerInternal.cs new file mode 100644 index 000000000..aaa7bc9e6 --- /dev/null +++ b/Shared/SharedCore/Shared.Core/PackFiles/Models/IPackFileContainerInternal.cs @@ -0,0 +1,18 @@ +using Shared.Core.Settings; + +namespace Shared.Core.PackFiles.Models +{ + internal interface IPackFileContainerInternal : IPackFileContainer + { + List AddFiles(List newFiles); + PackFile? DeleteFile(PackFile file); + void DeleteFolder(string folder); + PackFile? FindFile(string path); + string? GetFullPath(PackFile file); + void MoveFile(PackFile file, string newFolderPath); + string RenameDirectory(string currentNodeName, string newName); + void RenameFile(PackFile file, string newName); + void SaveFileData(PackFile file, byte[] data); + void SaveToDisk(string path, bool createBackup, GameInformation gameInformation); + } +} \ No newline at end of file diff --git a/Shared/SharedCore/Shared.Core/PackFiles/Models/PackFileContainer.cs b/Shared/SharedCore/Shared.Core/PackFiles/Models/PackFileContainer.cs index 5c446958e..b73d9fd7c 100644 --- a/Shared/SharedCore/Shared.Core/PackFiles/Models/PackFileContainer.cs +++ b/Shared/SharedCore/Shared.Core/PackFiles/Models/PackFileContainer.cs @@ -4,7 +4,7 @@ namespace Shared.Core.PackFiles.Models { - internal class PackFileContainer : IPackFileContainer + internal class PackFileContainer : IPackFileContainerInternal { public string Name { get; set; } public PFHeader Header { get; set; } diff --git a/Shared/SharedCore/Shared.Core/PackFiles/PackFileService.cs b/Shared/SharedCore/Shared.Core/PackFiles/PackFileService.cs index 01208cb72..3bc7a5015 100644 --- a/Shared/SharedCore/Shared.Core/PackFiles/PackFileService.cs +++ b/Shared/SharedCore/Shared.Core/PackFiles/PackFileService.cs @@ -2,7 +2,6 @@ using Shared.Core.ErrorHandling; using Shared.Core.Events; using Shared.Core.Events.Global; -using Shared.Core.Misc; using Shared.Core.PackFiles.Models; using Shared.Core.PackFiles.Serialization; using Shared.Core.Settings; @@ -14,8 +13,8 @@ class PackFileService : IPackFileService private readonly ILogger _logger = Logging.Create(); private readonly IGlobalEventHub? _globalEventHub; - private readonly List _packFileContainers = []; - private PackFileContainer? _packFileContainerSelectedForEdit; + private readonly List _packFileContainers = []; + private IPackFileContainerInternal? _packFileContainerSelectedForEdit; // We use this instead of the standard dialog helper, to avaid a circular dependency public ISimpleMessageBox MessageBoxProvider { get; set; } = new SimpleMessageBox(); @@ -27,7 +26,7 @@ public PackFileService(IGlobalEventHub? globalEventHub) _globalEventHub = globalEventHub; } - private static PackFileContainer CastContainer(IPackFileContainer container) => (PackFileContainer)container; + private static IPackFileContainerInternal CastContainer(IPackFileContainer container) => (IPackFileContainerInternal)container; public List GetAllPackfileContainers() => _packFileContainers.Cast().ToList(); @@ -58,7 +57,7 @@ public PackFileService(IGlobalEventHub? globalEventHub) return pf; } - void AddContainerInternal(PackFileContainer container, bool setToMainPackIfFirst = false) + void AddContainerInternal(IPackFileContainerInternal container, bool setToMainPackIfFirst = false) { _packFileContainers.Add(container); _globalEventHub?.PublishGlobalEvent(new PackFileContainerAddedEvent(container)); From 86c26cf36112188fdb3087a7cfad7b6b3614d105 Mon Sep 17 00:00:00 2001 From: ole kristian homelien Date: Sun, 19 Apr 2026 20:51:51 +0200 Subject: [PATCH 5/8] Code --- .../Models/CachedPackFileContainer.cs | 55 +++++ .../PackFileContainerCacheHelper.cs | 165 +++++++++++++ .../Utility/PackFileContainerLoader.cs | 141 +++++++---- .../Models/CachedPackFileContainerTests.cs | 129 ++++++++++ .../PackFileContainerCacheHelperTests.cs | 233 ++++++++++++++++++ 5 files changed, 674 insertions(+), 49 deletions(-) create mode 100644 Shared/SharedCore/Shared.Core/PackFiles/Models/CachedPackFileContainer.cs create mode 100644 Shared/SharedCore/Shared.Core/PackFiles/Serialization/PackFileContainerCacheHelper.cs create mode 100644 Shared/SharedCore/Shared.CoreTest/PackFiles/Models/CachedPackFileContainerTests.cs create mode 100644 Shared/SharedCore/Shared.CoreTest/PackFiles/Serialization/PackFileContainerCacheHelperTests.cs diff --git a/Shared/SharedCore/Shared.Core/PackFiles/Models/CachedPackFileContainer.cs b/Shared/SharedCore/Shared.Core/PackFiles/Models/CachedPackFileContainer.cs new file mode 100644 index 000000000..199d3ce13 --- /dev/null +++ b/Shared/SharedCore/Shared.Core/PackFiles/Models/CachedPackFileContainer.cs @@ -0,0 +1,55 @@ +using Shared.Core.Settings; + +namespace Shared.Core.PackFiles.Models +{ + internal class CachedPackFileContainer : IPackFileContainerInternal + { + public string Name { get; set; } + public bool IsCaPackFile { get => true; set { } } + public string SystemFilePath { get; set; } + public Dictionary FileList { get; set; } = []; + public HashSet SourcePackFilePaths { get; set; } = []; + + public CachedPackFileContainer(string name) + { + Name = name; + } + + public PackFile? FindFile(string path) + { + var lowerPath = path.Replace('/', '\\').ToLower().Trim(); + return FileList.TryGetValue(lowerPath, out var value) ? value : null; + } + + public string? GetFullPath(PackFile file) + { + var res = FileList.FirstOrDefault(x => ReferenceEquals(x.Value, file) + || string.Equals(x.Value.Name, file.Name, StringComparison.OrdinalIgnoreCase)).Key; + return string.IsNullOrWhiteSpace(res) ? null : res; + } + + public List AddFiles(List newFiles) => + throw new InvalidOperationException("Cannot modify a cached CA pack file container."); + + public PackFile? DeleteFile(PackFile file) => + throw new InvalidOperationException("Cannot modify a cached CA pack file container."); + + public void DeleteFolder(string folder) => + throw new InvalidOperationException("Cannot modify a cached CA pack file container."); + + public void MoveFile(PackFile file, string newFolderPath) => + throw new InvalidOperationException("Cannot modify a cached CA pack file container."); + + public string RenameDirectory(string currentNodeName, string newName) => + throw new InvalidOperationException("Cannot modify a cached CA pack file container."); + + public void RenameFile(PackFile file, string newName) => + throw new InvalidOperationException("Cannot modify a cached CA pack file container."); + + public void SaveFileData(PackFile file, byte[] data) => + throw new InvalidOperationException("Cannot modify a cached CA pack file container."); + + public void SaveToDisk(string path, bool createBackup, GameInformation gameInformation) => + throw new InvalidOperationException("Cannot modify a cached CA pack file container."); + } +} diff --git a/Shared/SharedCore/Shared.Core/PackFiles/Serialization/PackFileContainerCacheHelper.cs b/Shared/SharedCore/Shared.Core/PackFiles/Serialization/PackFileContainerCacheHelper.cs new file mode 100644 index 000000000..242150741 --- /dev/null +++ b/Shared/SharedCore/Shared.Core/PackFiles/Serialization/PackFileContainerCacheHelper.cs @@ -0,0 +1,165 @@ +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using Shared.Core.PackFiles.Models; +using Shared.Core.PackFiles.Utility; + +namespace Shared.Core.PackFiles.Serialization +{ + internal record CachedFileEntry( + string RelativePath, + string FileName, + string SourcePackFilePath, + long Offset, + long Size, + bool IsEncrypted, + bool IsCompressed, + [property: JsonConverter(typeof(JsonStringEnumConverter))] + CompressionFormat CompressionFormat, + uint UncompressedSize); + + internal class CachedContainerData + { + public string Fingerprint { get; set; } = ""; + public string ContainerName { get; set; } = ""; + public string SystemFilePath { get; set; } = ""; + public List SourcePackFilePaths { get; set; } = []; + public List Files { get; set; } = []; + } + + internal static class PackFileContainerCacheHelper + { + private static readonly JsonSerializerOptions s_jsonOptions = new() + { + WriteIndented = false, + }; + + public static string GetCacheFilePath(string gameDataFolder, string gameName) + { + var cacheDir = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "AssetEditor", + "Cache"); + + Directory.CreateDirectory(cacheDir); + + var safeGameName = string.Join("_", gameName.Split(Path.GetInvalidFileNameChars())); + return Path.Combine(cacheDir, $"ca_pack_cache_{safeGameName}.json"); + } + + public static string ComputeFingerprint(string gameDataFolder, List packFileNames) + { + using var sha = SHA256.Create(); + var sb = new StringBuilder(); + + foreach (var packFileName in packFileNames.OrderBy(x => x, StringComparer.OrdinalIgnoreCase)) + { + var fullPath = Path.Combine(gameDataFolder, packFileName); + if (File.Exists(fullPath)) + { + var info = new FileInfo(fullPath); + sb.Append(packFileName); + sb.Append('|'); + sb.Append(info.Length); + sb.Append('|'); + sb.Append(info.LastWriteTimeUtc.Ticks); + sb.Append(';'); + } + } + + var manifestPath = Path.Combine(gameDataFolder, "manifest.txt"); + if (File.Exists(manifestPath)) + { + var info = new FileInfo(manifestPath); + sb.Append("manifest.txt|"); + sb.Append(info.Length); + sb.Append('|'); + sb.Append(info.LastWriteTimeUtc.Ticks); + } + + var hash = sha.ComputeHash(Encoding.UTF8.GetBytes(sb.ToString())); + return Convert.ToHexString(hash); + } + + public static void SaveCache(CachedContainerData data, string cacheFilePath) + { + var json = JsonSerializer.Serialize(data, s_jsonOptions); + File.WriteAllText(cacheFilePath, json); + } + + public static CachedContainerData? LoadCache(string cacheFilePath) + { + if (!File.Exists(cacheFilePath)) + return null; + + var json = File.ReadAllText(cacheFilePath); + return JsonSerializer.Deserialize(json, s_jsonOptions); + } + + public static CachedContainerData BuildCacheData(string fingerprint, PackFileContainer container) + { + var data = new CachedContainerData + { + Fingerprint = fingerprint, + ContainerName = container.Name, + SystemFilePath = container.SystemFilePath, + SourcePackFilePaths = container.SourcePackFilePaths.ToList(), + }; + + foreach (var (relativePath, packFile) in container.FileList) + { + if (packFile.DataSource is PackedFileSource source) + { + data.Files.Add(new CachedFileEntry( + relativePath, + packFile.Name, + source.Parent.FilePath, + source.Offset, + source.Size, + source.IsEncrypted, + source.IsCompressed, + source.CompressionFormat, + source.UncompressedSize)); + } + } + + return data; + } + + public static CachedPackFileContainer RestoreFromCache(CachedContainerData data) + { + var container = new CachedPackFileContainer(data.ContainerName) + { + SystemFilePath = data.SystemFilePath, + }; + + foreach (var sourcePath in data.SourcePackFilePaths) + container.SourcePackFilePaths.Add(sourcePath); + + var parentCache = new Dictionary(StringComparer.OrdinalIgnoreCase); + + foreach (var entry in data.Files) + { + if (!parentCache.TryGetValue(entry.SourcePackFilePath, out var parent)) + { + parent = new PackedFileSourceParent { FilePath = entry.SourcePackFilePath }; + parentCache[entry.SourcePackFilePath] = parent; + } + + var source = new PackedFileSource( + parent, + entry.Offset, + entry.Size, + entry.IsEncrypted, + entry.IsCompressed, + entry.CompressionFormat, + entry.UncompressedSize); + + container.FileList[entry.RelativePath] = new PackFile(entry.FileName, source); + } + + return container; + } + } +} diff --git a/Shared/SharedCore/Shared.Core/PackFiles/Utility/PackFileContainerLoader.cs b/Shared/SharedCore/Shared.Core/PackFiles/Utility/PackFileContainerLoader.cs index d6a725587..8940022d8 100644 --- a/Shared/SharedCore/Shared.Core/PackFiles/Utility/PackFileContainerLoader.cs +++ b/Shared/SharedCore/Shared.Core/PackFiles/Utility/PackFileContainerLoader.cs @@ -101,71 +101,114 @@ private static void AddFolderContentToPackFile(PackFileContainer container, stri _logger.Here().Information($"Loading pack files for {gameName} located in {gameDataFolder}"); var allCaPackFiles = ManifestHelper.GetPackFilesFromManifest(gameDataFolder, out var manifestFileFound); - // When loading ca pack packs, we want to use the CA resolver as its faster. - // If there is no manifest file, we need to use the duplicate resolver as it loads all file in the folder. - // There might be custom mods in there that does not follow the rules! - IDuplicateFileResolver packfileResolver = new CaPackDuplicateFileResolver(); - if (manifestFileFound == false) + var fingerprint = PackFileContainerCacheHelper.ComputeFingerprint(gameDataFolder, allCaPackFiles); + var cacheFilePath = PackFileContainerCacheHelper.GetCacheFilePath(gameDataFolder, gameName); + + var cached = TryLoadFromCache(cacheFilePath, fingerprint, gameName); + if (cached != null) + return cached; + + var container = LoadAllCaFilesFromDisk(gameDataFolder, gameName, allCaPackFiles, manifestFileFound); + + try + { + var cacheData = PackFileContainerCacheHelper.BuildCacheData(fingerprint, container); + PackFileContainerCacheHelper.SaveCache(cacheData, cacheFilePath); + _logger.Here().Information($"Saved CA pack cache for {gameName} to {cacheFilePath}"); + } + catch (Exception cacheEx) { - _logger.Here().Warning($"Loading pack files for {gameName}, which does not uses manifest.txt. If there are MODs in the game folder, this might cause issues!"); - packfileResolver = new CustomPackDuplicateFileResolver(); + _logger.Here().Warning($"Failed to save CA pack cache for {gameName}: {cacheEx.Message}"); } - var packList = new List(); - var packsCompressionStats = new ConcurrentDictionary(); + return container; + } + catch (Exception e) + { + _logger.Here().Error($"Trying to get all CA packs in {gameDataFolder}. Error : {e.ToString()}"); + return null; + } + } - Parallel.ForEach(allCaPackFiles, packFilePath => + private CachedPackFileContainer? TryLoadFromCache(string cacheFilePath, string fingerprint, string gameName) + { + try + { + var cacheData = PackFileContainerCacheHelper.LoadCache(cacheFilePath); + if (cacheData != null && cacheData.Fingerprint == fingerprint) { - var path = gameDataFolder + "\\" + packFilePath; - if (File.Exists(path)) - { - using var fileStream = File.OpenRead(path); - using var reader = new BinaryReader(fileStream, Encoding.ASCII); - - var packFileSize = new FileInfo(path).Length; - var pack = PackFileSerializerLoader.Load(path, packFileSize, reader, packfileResolver); - packList.Add(pack); - - PackFileLog.LogPackCompression(pack); - var packCompressionStats = PackFileLog.GetCompressionInformation(pack); - foreach (var kvp in packCompressionStats) - { - if (!packsCompressionStats.TryGetValue(kvp.Key, out var existingStats)) - packsCompressionStats[kvp.Key] = new CompressionInformation(kvp.Value.DiskSize, kvp.Value.UncompressedSize); - else - existingStats.Add(kvp.Value); - } - } - else - _logger.Here().Warning($"{gameName} pack file '{path}' not found, loading skipped"); + _logger.Here().Information($"Loading CA packs for {gameName} from cache: {cacheFilePath}"); + return PackFileContainerCacheHelper.RestoreFromCache(cacheData); } - ); + } + catch (Exception ex) + { + _logger.Here().Warning($"Failed to read CA pack cache for {gameName}, will rebuild: {ex.Message}"); + } + return null; + } - PackFileLog.LogPacksCompression(packsCompressionStats); + private PackFileContainer LoadAllCaFilesFromDisk(string gameDataFolder, string gameName, List allCaPackFiles, bool manifestFileFound) + { + // When loading ca pack packs, we want to use the CA resolver as its faster. + // If there is no manifest file, we need to use the duplicate resolver as it loads all file in the folder. + // There might be custom mods in there that does not follow the rules! + IDuplicateFileResolver packfileResolver = new CaPackDuplicateFileResolver(); + if (manifestFileFound == false) + { + _logger.Here().Warning($"Loading pack files for {gameName}, which does not uses manifest.txt. If there are MODs in the game folder, this might cause issues!"); + packfileResolver = new CustomPackDuplicateFileResolver(); + } - var caPackFileContainer = new PackFileContainer($"All Game Packs - {gameName}"); - caPackFileContainer.IsCaPackFile = true; - caPackFileContainer.SystemFilePath = gameDataFolder; - var packFilesOrderedByGroup = packList.GroupBy(x => x.Header.LoadOrder).OrderBy(x => x.Key); + var packList = new List(); + var packsCompressionStats = new ConcurrentDictionary(); - foreach (var group in packFilesOrderedByGroup) + Parallel.ForEach(allCaPackFiles, packFilePath => + { + var path = gameDataFolder + "\\" + packFilePath; + if (File.Exists(path)) { - var packFilesOrderedByName = group.OrderBy(x => x.Name); - foreach (var packfile in packFilesOrderedByName) + using var fileStream = File.OpenRead(path); + using var reader = new BinaryReader(fileStream, Encoding.ASCII); + + var packFileSize = new FileInfo(path).Length; + var pack = PackFileSerializerLoader.Load(path, packFileSize, reader, packfileResolver); + packList.Add(pack); + + PackFileLog.LogPackCompression(pack); + var packCompressionStats = PackFileLog.GetCompressionInformation(pack); + foreach (var kvp in packCompressionStats) { - if (string.IsNullOrWhiteSpace(packfile.SystemFilePath) == false) - caPackFileContainer.SourcePackFilePaths.Add(packfile.SystemFilePath); - caPackFileContainer.MergePackFileContainer(packfile); + if (!packsCompressionStats.TryGetValue(kvp.Key, out var existingStats)) + packsCompressionStats[kvp.Key] = new CompressionInformation(kvp.Value.DiskSize, kvp.Value.UncompressedSize); + else + existingStats.Add(kvp.Value); } } - - return caPackFileContainer; + else + _logger.Here().Warning($"{gameName} pack file '{path}' not found, loading skipped"); } - catch (Exception e) + ); + + PackFileLog.LogPacksCompression(packsCompressionStats); + + var caPackFileContainer = new PackFileContainer($"All Game Packs - {gameName}"); + caPackFileContainer.IsCaPackFile = true; + caPackFileContainer.SystemFilePath = gameDataFolder; + var packFilesOrderedByGroup = packList.GroupBy(x => x.Header.LoadOrder).OrderBy(x => x.Key); + + foreach (var group in packFilesOrderedByGroup) { - _logger.Here().Error($"Trying to get all CA packs in {gameDataFolder}. Error : {e.ToString()}"); - return null; + var packFilesOrderedByName = group.OrderBy(x => x.Name); + foreach (var packfile in packFilesOrderedByName) + { + if (string.IsNullOrWhiteSpace(packfile.SystemFilePath) == false) + caPackFileContainer.SourcePackFilePaths.Add(packfile.SystemFilePath); + caPackFileContainer.MergePackFileContainer(packfile); + } } + + return caPackFileContainer; } } } diff --git a/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/CachedPackFileContainerTests.cs b/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/CachedPackFileContainerTests.cs new file mode 100644 index 000000000..bc4997661 --- /dev/null +++ b/Shared/SharedCore/Shared.CoreTest/PackFiles/Models/CachedPackFileContainerTests.cs @@ -0,0 +1,129 @@ +using Shared.Core.PackFiles; +using Shared.Core.PackFiles.Models; +using Shared.Core.Settings; + +namespace Shared.CoreTest.PackFiles.Models +{ + internal class CachedPackFileContainer_ReadOnly + { + private CachedPackFileContainer _container; + + [SetUp] + public void Setup() + { + _container = new CachedPackFileContainer("TestCache") + { + SystemFilePath = @"c:\game\data" + }; + _container.FileList["folder\\file.txt"] = new PackFile("file.txt", null); + } + + [Test] + public void IsCaPackFile_AlwaysTrue() + { + Assert.That(_container.IsCaPackFile, Is.True); + } + + [Test] + public void IsCaPackFile_SetterDoesNotChangeValue() + { + _container.IsCaPackFile = false; + Assert.That(_container.IsCaPackFile, Is.True); + } + + [Test] + public void FindFile_ReturnsFile() + { + var result = _container.FindFile("folder\\file.txt"); + Assert.That(result, Is.Not.Null); + Assert.That(result.Name, Is.EqualTo("file.txt")); + } + + [Test] + public void FindFile_ReturnsNullForMissing() + { + var result = _container.FindFile("missing\\path.txt"); + Assert.That(result, Is.Null); + } + + [Test] + public void FindFile_NormalizesPath() + { + var result = _container.FindFile("FOLDER/FILE.TXT"); + Assert.That(result, Is.Not.Null); + } + + [Test] + public void GetFullPath_ReturnsPath() + { + var file = _container.FileList["folder\\file.txt"]; + var path = _container.GetFullPath(file); + Assert.That(path, Is.EqualTo("folder\\file.txt")); + } + + [Test] + public void GetFullPath_ReturnsNullForMissing() + { + var unknownFile = new PackFile("unknown.txt", null); + var path = _container.GetFullPath(unknownFile); + Assert.That(path, Is.Null); + } + + [Test] + public void AddFiles_Throws() + { + var newFiles = new List + { + new("folder", new PackFile("new.txt", null)) + }; + Assert.Throws(() => _container.AddFiles(newFiles)); + } + + [Test] + public void DeleteFile_Throws() + { + var file = _container.FileList["folder\\file.txt"]; + Assert.Throws(() => _container.DeleteFile(file)); + } + + [Test] + public void DeleteFolder_Throws() + { + Assert.Throws(() => _container.DeleteFolder("folder")); + } + + [Test] + public void MoveFile_Throws() + { + var file = _container.FileList["folder\\file.txt"]; + Assert.Throws(() => _container.MoveFile(file, "other")); + } + + [Test] + public void RenameDirectory_Throws() + { + Assert.Throws(() => _container.RenameDirectory("folder", "renamed")); + } + + [Test] + public void RenameFile_Throws() + { + var file = _container.FileList["folder\\file.txt"]; + Assert.Throws(() => _container.RenameFile(file, "renamed.txt")); + } + + [Test] + public void SaveFileData_Throws() + { + var file = _container.FileList["folder\\file.txt"]; + Assert.Throws(() => _container.SaveFileData(file, [1, 2, 3])); + } + + [Test] + public void SaveToDisk_Throws() + { + var gameInfo = GameInformationDatabase.GetGameById(GameTypeEnum.Warhammer3); + Assert.Throws(() => _container.SaveToDisk("path", false, gameInfo)); + } + } +} diff --git a/Shared/SharedCore/Shared.CoreTest/PackFiles/Serialization/PackFileContainerCacheHelperTests.cs b/Shared/SharedCore/Shared.CoreTest/PackFiles/Serialization/PackFileContainerCacheHelperTests.cs new file mode 100644 index 000000000..934993a02 --- /dev/null +++ b/Shared/SharedCore/Shared.CoreTest/PackFiles/Serialization/PackFileContainerCacheHelperTests.cs @@ -0,0 +1,233 @@ +using Shared.Core.PackFiles.Models; +using Shared.Core.PackFiles.Serialization; +using Shared.Core.PackFiles.Utility; + +namespace Shared.CoreTest.PackFiles.Serialization +{ + internal class PackFileContainerCacheHelperTests + { + private string _tempDir; + private string _cacheFilePath; + + [SetUp] + public void Setup() + { + _tempDir = Path.Combine(Path.GetTempPath(), "AssetEditorCacheTests_" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(_tempDir); + _cacheFilePath = Path.Combine(_tempDir, "test_cache.json"); + } + + [TearDown] + public void TearDown() + { + if (Directory.Exists(_tempDir)) + Directory.Delete(_tempDir, true); + } + + [Test] + public void RoundTrip_PreservesMetadata() + { + // Arrange + var container = new PackFileContainer("All Game Packs - TestGame") + { + IsCaPackFile = true, + SystemFilePath = @"c:\game\data" + }; + + container.SourcePackFilePaths.Add(@"c:\game\data\pack1.pack"); + container.SourcePackFilePaths.Add(@"c:\game\data\pack2.pack"); + + var parent1 = new PackedFileSourceParent { FilePath = @"c:\game\data\pack1.pack" }; + var parent2 = new PackedFileSourceParent { FilePath = @"c:\game\data\pack2.pack" }; + + var source1 = new PackedFileSource(parent1, 100, 500, false, false, CompressionFormat.None, 0); + var source2 = new PackedFileSource(parent2, 200, 1000, false, true, CompressionFormat.Lz4, 2000); + + container.FileList["folder\\file1.txt"] = new PackFile("file1.txt", source1); + container.FileList["folder\\file2.bin"] = new PackFile("file2.bin", source2); + + // Act + var cacheData = PackFileContainerCacheHelper.BuildCacheData("fingerprint123", container); + PackFileContainerCacheHelper.SaveCache(cacheData, _cacheFilePath); + var loaded = PackFileContainerCacheHelper.LoadCache(_cacheFilePath); + + // Assert + Assert.That(loaded, Is.Not.Null); + Assert.That(loaded.Fingerprint, Is.EqualTo("fingerprint123")); + Assert.That(loaded.ContainerName, Is.EqualTo("All Game Packs - TestGame")); + Assert.That(loaded.SystemFilePath, Is.EqualTo(@"c:\game\data")); + Assert.That(loaded.SourcePackFilePaths.Count, Is.EqualTo(2)); + Assert.That(loaded.Files.Count, Is.EqualTo(2)); + } + + [Test] + public void RestoreFromCache_CreatesCorrectContainer() + { + // Arrange + var cacheData = new CachedContainerData + { + Fingerprint = "fp", + ContainerName = "Test Container", + SystemFilePath = @"c:\game\data", + SourcePackFilePaths = [@"c:\game\data\pack1.pack"], + Files = + [ + new CachedFileEntry( + "folder\\file.txt", + "file.txt", + @"c:\game\data\pack1.pack", + Offset: 512, + Size: 1024, + IsEncrypted: false, + IsCompressed: false, + CompressionFormat.None, + UncompressedSize: 0), + new CachedFileEntry( + "other\\data.bin", + "data.bin", + @"c:\game\data\pack1.pack", + Offset: 2048, + Size: 4096, + IsEncrypted: false, + IsCompressed: true, + CompressionFormat.Lz4, + UncompressedSize: 8192) + ] + }; + + // Act + var container = PackFileContainerCacheHelper.RestoreFromCache(cacheData); + + // Assert + Assert.That(container, Is.InstanceOf()); + Assert.That(container.Name, Is.EqualTo("Test Container")); + Assert.That(container.IsCaPackFile, Is.True); + Assert.That(container.SystemFilePath, Is.EqualTo(@"c:\game\data")); + Assert.That(container.SourcePackFilePaths.Count, Is.EqualTo(1)); + Assert.That(container.FileList.Count, Is.EqualTo(2)); + + var file1 = container.FileList["folder\\file.txt"]; + Assert.That(file1.Name, Is.EqualTo("file.txt")); + var file1Source = file1.DataSource as PackedFileSource; + Assert.That(file1Source, Is.Not.Null); + Assert.That(file1Source.Offset, Is.EqualTo(512)); + Assert.That(file1Source.Size, Is.EqualTo(1024)); + Assert.That(file1Source.IsCompressed, Is.False); + Assert.That(file1Source.Parent.FilePath, Is.EqualTo(@"c:\game\data\pack1.pack")); + + var file2 = container.FileList["other\\data.bin"]; + var file2Source = file2.DataSource as PackedFileSource; + Assert.That(file2Source, Is.Not.Null); + Assert.That(file2Source.Offset, Is.EqualTo(2048)); + Assert.That(file2Source.Size, Is.EqualTo(4096)); + Assert.That(file2Source.IsCompressed, Is.True); + Assert.That(file2Source.CompressionFormat, Is.EqualTo(CompressionFormat.Lz4)); + Assert.That(file2Source.UncompressedSize, Is.EqualTo(8192)); + } + + [Test] + public void RestoreFromCache_SharesPackedFileSourceParents() + { + var cacheData = new CachedContainerData + { + Fingerprint = "fp", + ContainerName = "Test", + SystemFilePath = @"c:\game", + Files = + [ + new CachedFileEntry("a.txt", "a.txt", @"c:\pack.pack", 0, 10, false, false, CompressionFormat.None, 0), + new CachedFileEntry("b.txt", "b.txt", @"c:\pack.pack", 10, 20, false, false, CompressionFormat.None, 0), + ] + }; + + var container = PackFileContainerCacheHelper.RestoreFromCache(cacheData); + + var sourceA = (PackedFileSource)container.FileList["a.txt"].DataSource; + var sourceB = (PackedFileSource)container.FileList["b.txt"].DataSource; + Assert.That(ReferenceEquals(sourceA.Parent, sourceB.Parent), Is.True, + "Files from the same pack should share PackedFileSourceParent instances"); + } + + [Test] + public void LoadCache_ReturnsNullForMissingFile() + { + var result = PackFileContainerCacheHelper.LoadCache(Path.Combine(_tempDir, "nonexistent.json")); + Assert.That(result, Is.Null); + } + + [Test] + public void ComputeFingerprint_DeterministicForSameInputs() + { + // Create fake pack files + var packDir = Path.Combine(_tempDir, "gamedata"); + Directory.CreateDirectory(packDir); + File.WriteAllText(Path.Combine(packDir, "a.pack"), "data_a"); + File.WriteAllText(Path.Combine(packDir, "b.pack"), "data_b"); + var packFiles = new List { "a.pack", "b.pack" }; + + var fp1 = PackFileContainerCacheHelper.ComputeFingerprint(packDir, packFiles); + var fp2 = PackFileContainerCacheHelper.ComputeFingerprint(packDir, packFiles); + + Assert.That(fp1, Is.EqualTo(fp2)); + } + + [Test] + public void ComputeFingerprint_ChangesWhenFileChanges() + { + var packDir = Path.Combine(_tempDir, "gamedata2"); + Directory.CreateDirectory(packDir); + File.WriteAllText(Path.Combine(packDir, "a.pack"), "data_a"); + var packFiles = new List { "a.pack" }; + + var fp1 = PackFileContainerCacheHelper.ComputeFingerprint(packDir, packFiles); + + // Modify the file (change size) + File.WriteAllText(Path.Combine(packDir, "a.pack"), "data_a_modified_longer"); + + var fp2 = PackFileContainerCacheHelper.ComputeFingerprint(packDir, packFiles); + + Assert.That(fp1, Is.Not.EqualTo(fp2)); + } + + [Test] + public void RoundTrip_FullCycle_BuildSaveLoadRestore() + { + // Arrange + var container = new PackFileContainer("Full Cycle Test") + { + IsCaPackFile = true, + SystemFilePath = @"c:\game\data" + }; + + var parent = new PackedFileSourceParent { FilePath = @"c:\game\data\main.pack" }; + container.SourcePackFilePaths.Add(parent.FilePath); + + container.FileList["db\\units.bin"] = new PackFile("units.bin", + new PackedFileSource(parent, 0, 256, false, false, CompressionFormat.None, 0)); + container.FileList["text\\localisation.loc"] = new PackFile("localisation.loc", + new PackedFileSource(parent, 256, 512, false, true, CompressionFormat.Lz4, 1024)); + + // Act: build → save → load → restore + var cacheData = PackFileContainerCacheHelper.BuildCacheData("test_fp", container); + PackFileContainerCacheHelper.SaveCache(cacheData, _cacheFilePath); + var loadedData = PackFileContainerCacheHelper.LoadCache(_cacheFilePath); + var restored = PackFileContainerCacheHelper.RestoreFromCache(loadedData!); + + // Assert: restored container matches original + Assert.That(restored.Name, Is.EqualTo("Full Cycle Test")); + Assert.That(restored.IsCaPackFile, Is.True); + Assert.That(restored.SystemFilePath, Is.EqualTo(@"c:\game\data")); + Assert.That(restored.FileList.Count, Is.EqualTo(2)); + + Assert.That(restored.FindFile("db\\units.bin"), Is.Not.Null); + Assert.That(restored.FindFile("text\\localisation.loc"), Is.Not.Null); + + var restoredSource = (PackedFileSource)restored.FileList["text\\localisation.loc"].DataSource; + Assert.That(restoredSource.Offset, Is.EqualTo(256)); + Assert.That(restoredSource.Size, Is.EqualTo(512)); + Assert.That(restoredSource.IsCompressed, Is.True); + Assert.That(restoredSource.CompressionFormat, Is.EqualTo(CompressionFormat.Lz4)); + Assert.That(restoredSource.UncompressedSize, Is.EqualTo(1024)); + } + } +} From 0c2527852549dc4a5d5503957efc51a23f59693e Mon Sep 17 00:00:00 2001 From: ole kristian homelien Date: Mon, 20 Apr 2026 20:22:11 +0200 Subject: [PATCH 6/8] Code --- Shared/SharedCore/Shared.Core/PackFiles/PackFileService.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Shared/SharedCore/Shared.Core/PackFiles/PackFileService.cs b/Shared/SharedCore/Shared.Core/PackFiles/PackFileService.cs index 3bc7a5015..5aa7f32c4 100644 --- a/Shared/SharedCore/Shared.Core/PackFiles/PackFileService.cs +++ b/Shared/SharedCore/Shared.Core/PackFiles/PackFileService.cs @@ -95,6 +95,10 @@ public void AddFilesToPack(IPackFileContainer container, List public void CopyFileFromOtherPackFile(IPackFileContainer source, string path, IPackFileContainer target) { + var pf = CastContainer(target); + if (pf.IsCaPackFile) + throw new Exception("Can not add files to ca pack file"); + var sourceContainer = CastContainer(source); var targetContainer = CastContainer(target); var lowerPath = path.Replace('/', '\\').ToLower().Trim(); From a91f5b58229e3ae8efa373f3cfc08fe9afd8dc07 Mon Sep 17 00:00:00 2001 From: ole kristian homelien Date: Mon, 20 Apr 2026 20:28:55 +0200 Subject: [PATCH 7/8] Code --- .../PackFileContainerCacheHelper.cs | 80 ++++++++++++++++--- .../PackFileContainerCacheHelperTests.cs | 26 +++++- 2 files changed, 92 insertions(+), 14 deletions(-) diff --git a/Shared/SharedCore/Shared.Core/PackFiles/Serialization/PackFileContainerCacheHelper.cs b/Shared/SharedCore/Shared.Core/PackFiles/Serialization/PackFileContainerCacheHelper.cs index 242150741..3f9fee70f 100644 --- a/Shared/SharedCore/Shared.Core/PackFiles/Serialization/PackFileContainerCacheHelper.cs +++ b/Shared/SharedCore/Shared.Core/PackFiles/Serialization/PackFileContainerCacheHelper.cs @@ -1,7 +1,5 @@ using System.Security.Cryptography; using System.Text; -using System.Text.Json; -using System.Text.Json.Serialization; using Shared.Core.PackFiles.Models; using Shared.Core.PackFiles.Utility; @@ -15,7 +13,6 @@ internal record CachedFileEntry( long Size, bool IsEncrypted, bool IsCompressed, - [property: JsonConverter(typeof(JsonStringEnumConverter))] CompressionFormat CompressionFormat, uint UncompressedSize); @@ -30,10 +27,8 @@ internal class CachedContainerData internal static class PackFileContainerCacheHelper { - private static readonly JsonSerializerOptions s_jsonOptions = new() - { - WriteIndented = false, - }; + private static readonly byte[] s_magic = "AEPC"u8.ToArray(); + private const int CacheVersion = 1; public static string GetCacheFilePath(string gameDataFolder, string gameName) { @@ -45,7 +40,7 @@ public static string GetCacheFilePath(string gameDataFolder, string gameName) Directory.CreateDirectory(cacheDir); var safeGameName = string.Join("_", gameName.Split(Path.GetInvalidFileNameChars())); - return Path.Combine(cacheDir, $"ca_pack_cache_{safeGameName}.json"); + return Path.Combine(cacheDir, $"ca_pack_cache_{safeGameName}.bin"); } public static string ComputeFingerprint(string gameDataFolder, List packFileNames) @@ -84,8 +79,33 @@ public static string ComputeFingerprint(string gameDataFolder, List pack public static void SaveCache(CachedContainerData data, string cacheFilePath) { - var json = JsonSerializer.Serialize(data, s_jsonOptions); - File.WriteAllText(cacheFilePath, json); + using var stream = File.Create(cacheFilePath); + using var writer = new BinaryWriter(stream, Encoding.UTF8); + + writer.Write(s_magic); + writer.Write(CacheVersion); + + writer.Write(data.Fingerprint); + writer.Write(data.ContainerName); + writer.Write(data.SystemFilePath); + + writer.Write(data.SourcePackFilePaths.Count); + foreach (var path in data.SourcePackFilePaths) + writer.Write(path); + + writer.Write(data.Files.Count); + foreach (var entry in data.Files) + { + writer.Write(entry.RelativePath); + writer.Write(entry.FileName); + writer.Write(entry.SourcePackFilePath); + writer.Write(entry.Offset); + writer.Write(entry.Size); + writer.Write(entry.IsEncrypted); + writer.Write(entry.IsCompressed); + writer.Write((int)entry.CompressionFormat); + writer.Write(entry.UncompressedSize); + } } public static CachedContainerData? LoadCache(string cacheFilePath) @@ -93,8 +113,44 @@ public static void SaveCache(CachedContainerData data, string cacheFilePath) if (!File.Exists(cacheFilePath)) return null; - var json = File.ReadAllText(cacheFilePath); - return JsonSerializer.Deserialize(json, s_jsonOptions); + using var stream = File.OpenRead(cacheFilePath); + using var reader = new BinaryReader(stream, Encoding.UTF8); + + var magic = reader.ReadBytes(s_magic.Length); + if (!magic.AsSpan().SequenceEqual(s_magic)) + return null; + + var version = reader.ReadInt32(); + if (version != CacheVersion) + return null; + + var data = new CachedContainerData + { + Fingerprint = reader.ReadString(), + ContainerName = reader.ReadString(), + SystemFilePath = reader.ReadString(), + }; + + var sourcePathCount = reader.ReadInt32(); + for (var i = 0; i < sourcePathCount; i++) + data.SourcePackFilePaths.Add(reader.ReadString()); + + var fileCount = reader.ReadInt32(); + for (var i = 0; i < fileCount; i++) + { + data.Files.Add(new CachedFileEntry( + RelativePath: reader.ReadString(), + FileName: reader.ReadString(), + SourcePackFilePath: reader.ReadString(), + Offset: reader.ReadInt64(), + Size: reader.ReadInt64(), + IsEncrypted: reader.ReadBoolean(), + IsCompressed: reader.ReadBoolean(), + (CompressionFormat)reader.ReadInt32(), + UncompressedSize: reader.ReadUInt32())); + } + + return data; } public static CachedContainerData BuildCacheData(string fingerprint, PackFileContainer container) diff --git a/Shared/SharedCore/Shared.CoreTest/PackFiles/Serialization/PackFileContainerCacheHelperTests.cs b/Shared/SharedCore/Shared.CoreTest/PackFiles/Serialization/PackFileContainerCacheHelperTests.cs index 934993a02..c4a9a5c2d 100644 --- a/Shared/SharedCore/Shared.CoreTest/PackFiles/Serialization/PackFileContainerCacheHelperTests.cs +++ b/Shared/SharedCore/Shared.CoreTest/PackFiles/Serialization/PackFileContainerCacheHelperTests.cs @@ -14,7 +14,7 @@ public void Setup() { _tempDir = Path.Combine(Path.GetTempPath(), "AssetEditorCacheTests_" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(_tempDir); - _cacheFilePath = Path.Combine(_tempDir, "test_cache.json"); + _cacheFilePath = Path.Combine(_tempDir, "test_cache.bin"); } [TearDown] @@ -151,7 +151,7 @@ public void RestoreFromCache_SharesPackedFileSourceParents() [Test] public void LoadCache_ReturnsNullForMissingFile() { - var result = PackFileContainerCacheHelper.LoadCache(Path.Combine(_tempDir, "nonexistent.json")); + var result = PackFileContainerCacheHelper.LoadCache(Path.Combine(_tempDir, "nonexistent.bin")); Assert.That(result, Is.Null); } @@ -229,5 +229,27 @@ public void RoundTrip_FullCycle_BuildSaveLoadRestore() Assert.That(restoredSource.CompressionFormat, Is.EqualTo(CompressionFormat.Lz4)); Assert.That(restoredSource.UncompressedSize, Is.EqualTo(1024)); } + + [Test] + public void LoadCache_ReturnsNullForBadMagic() + { + File.WriteAllBytes(_cacheFilePath, [0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00]); + var result = PackFileContainerCacheHelper.LoadCache(_cacheFilePath); + Assert.That(result, Is.Null); + } + + [Test] + public void LoadCache_ReturnsNullForWrongVersion() + { + using (var stream = File.Create(_cacheFilePath)) + using (var writer = new BinaryWriter(stream)) + { + writer.Write("AEPC"u8); + writer.Write(999); // wrong version + } + + var result = PackFileContainerCacheHelper.LoadCache(_cacheFilePath); + Assert.That(result, Is.Null); + } } } From 3d07690ec75369003d054b7cdf184ab3d8fe44e3 Mon Sep 17 00:00:00 2001 From: ole kristian homelien Date: Mon, 20 Apr 2026 20:35:17 +0200 Subject: [PATCH 8/8] Code --- .../PackFileContainerCacheHelper.cs | 69 ++++++++++++++----- .../Utility/PackFileContainerLoader.cs | 6 +- .../PackFileContainerCacheHelperTests.cs | 60 +++++++++++++--- 3 files changed, 103 insertions(+), 32 deletions(-) diff --git a/Shared/SharedCore/Shared.Core/PackFiles/Serialization/PackFileContainerCacheHelper.cs b/Shared/SharedCore/Shared.Core/PackFiles/Serialization/PackFileContainerCacheHelper.cs index 3f9fee70f..83f5b32c1 100644 --- a/Shared/SharedCore/Shared.Core/PackFiles/Serialization/PackFileContainerCacheHelper.cs +++ b/Shared/SharedCore/Shared.Core/PackFiles/Serialization/PackFileContainerCacheHelper.cs @@ -108,13 +108,19 @@ public static void SaveCache(CachedContainerData data, string cacheFilePath) } } - public static CachedContainerData? LoadCache(string cacheFilePath) + /// + /// Single-pass optimized load: reads binary cache directly into a CachedPackFileContainer, + /// skipping intermediate CachedContainerData/CachedFileEntry allocations. + /// Returns null if the file is missing, corrupt, or the fingerprint doesn't match. + /// + public static CachedPackFileContainer? LoadContainerFromCache(string cacheFilePath, string expectedFingerprint) { if (!File.Exists(cacheFilePath)) return null; - using var stream = File.OpenRead(cacheFilePath); - using var reader = new BinaryReader(stream, Encoding.UTF8); + using var fileStream = File.OpenRead(cacheFilePath); + using var buffered = new BufferedStream(fileStream, 1024 * 64); + using var reader = new BinaryReader(buffered, Encoding.UTF8); var magic = reader.ReadBytes(s_magic.Length); if (!magic.AsSpan().SequenceEqual(s_magic)) @@ -124,33 +130,58 @@ public static void SaveCache(CachedContainerData data, string cacheFilePath) if (version != CacheVersion) return null; - var data = new CachedContainerData + var fingerprint = reader.ReadString(); + if (fingerprint != expectedFingerprint) + return null; + + var containerName = reader.ReadString(); + var systemFilePath = reader.ReadString(); + + var container = new CachedPackFileContainer(containerName) { - Fingerprint = reader.ReadString(), - ContainerName = reader.ReadString(), - SystemFilePath = reader.ReadString(), + SystemFilePath = systemFilePath, }; var sourcePathCount = reader.ReadInt32(); for (var i = 0; i < sourcePathCount; i++) - data.SourcePackFilePaths.Add(reader.ReadString()); + container.SourcePackFilePaths.Add(reader.ReadString()); var fileCount = reader.ReadInt32(); + var fileList = new Dictionary(fileCount); + var parentCache = new Dictionary(sourcePathCount, StringComparer.OrdinalIgnoreCase); + var stringPool = new Dictionary(sourcePathCount, StringComparer.Ordinal); + for (var i = 0; i < fileCount; i++) { - data.Files.Add(new CachedFileEntry( - RelativePath: reader.ReadString(), - FileName: reader.ReadString(), - SourcePackFilePath: reader.ReadString(), - Offset: reader.ReadInt64(), - Size: reader.ReadInt64(), - IsEncrypted: reader.ReadBoolean(), - IsCompressed: reader.ReadBoolean(), - (CompressionFormat)reader.ReadInt32(), - UncompressedSize: reader.ReadUInt32())); + var relativePath = reader.ReadString(); + var fileName = reader.ReadString(); + var sourcePackFilePath = reader.ReadString(); + var offset = reader.ReadInt64(); + var size = reader.ReadInt64(); + var isEncrypted = reader.ReadBoolean(); + var isCompressed = reader.ReadBoolean(); + var compressionFormat = (CompressionFormat)reader.ReadInt32(); + var uncompressedSize = reader.ReadUInt32(); + + // Intern the source pack path string — ~20 unique values across 600K entries + if (!stringPool.TryGetValue(sourcePackFilePath, out var internedPath)) + { + internedPath = sourcePackFilePath; + stringPool[internedPath] = internedPath; + } + + if (!parentCache.TryGetValue(internedPath, out var parent)) + { + parent = new PackedFileSourceParent { FilePath = internedPath }; + parentCache[internedPath] = parent; + } + + var source = new PackedFileSource(parent, offset, size, isEncrypted, isCompressed, compressionFormat, uncompressedSize); + fileList[relativePath] = new PackFile(fileName, source); } - return data; + container.FileList = fileList; + return container; } public static CachedContainerData BuildCacheData(string fingerprint, PackFileContainer container) diff --git a/Shared/SharedCore/Shared.Core/PackFiles/Utility/PackFileContainerLoader.cs b/Shared/SharedCore/Shared.Core/PackFiles/Utility/PackFileContainerLoader.cs index 8940022d8..81660f866 100644 --- a/Shared/SharedCore/Shared.Core/PackFiles/Utility/PackFileContainerLoader.cs +++ b/Shared/SharedCore/Shared.Core/PackFiles/Utility/PackFileContainerLoader.cs @@ -134,11 +134,11 @@ private static void AddFolderContentToPackFile(PackFileContainer container, stri { try { - var cacheData = PackFileContainerCacheHelper.LoadCache(cacheFilePath); - if (cacheData != null && cacheData.Fingerprint == fingerprint) + var container = PackFileContainerCacheHelper.LoadContainerFromCache(cacheFilePath, fingerprint); + if (container != null) { _logger.Here().Information($"Loading CA packs for {gameName} from cache: {cacheFilePath}"); - return PackFileContainerCacheHelper.RestoreFromCache(cacheData); + return container; } } catch (Exception ex) diff --git a/Shared/SharedCore/Shared.CoreTest/PackFiles/Serialization/PackFileContainerCacheHelperTests.cs b/Shared/SharedCore/Shared.CoreTest/PackFiles/Serialization/PackFileContainerCacheHelperTests.cs index c4a9a5c2d..4194efa9b 100644 --- a/Shared/SharedCore/Shared.CoreTest/PackFiles/Serialization/PackFileContainerCacheHelperTests.cs +++ b/Shared/SharedCore/Shared.CoreTest/PackFiles/Serialization/PackFileContainerCacheHelperTests.cs @@ -49,15 +49,14 @@ public void RoundTrip_PreservesMetadata() // Act var cacheData = PackFileContainerCacheHelper.BuildCacheData("fingerprint123", container); PackFileContainerCacheHelper.SaveCache(cacheData, _cacheFilePath); - var loaded = PackFileContainerCacheHelper.LoadCache(_cacheFilePath); + var loaded = PackFileContainerCacheHelper.LoadContainerFromCache(_cacheFilePath, "fingerprint123"); // Assert Assert.That(loaded, Is.Not.Null); - Assert.That(loaded.Fingerprint, Is.EqualTo("fingerprint123")); - Assert.That(loaded.ContainerName, Is.EqualTo("All Game Packs - TestGame")); + Assert.That(loaded.Name, Is.EqualTo("All Game Packs - TestGame")); Assert.That(loaded.SystemFilePath, Is.EqualTo(@"c:\game\data")); Assert.That(loaded.SourcePackFilePaths.Count, Is.EqualTo(2)); - Assert.That(loaded.Files.Count, Is.EqualTo(2)); + Assert.That(loaded.FileList.Count, Is.EqualTo(2)); } [Test] @@ -151,7 +150,7 @@ public void RestoreFromCache_SharesPackedFileSourceParents() [Test] public void LoadCache_ReturnsNullForMissingFile() { - var result = PackFileContainerCacheHelper.LoadCache(Path.Combine(_tempDir, "nonexistent.bin")); + var result = PackFileContainerCacheHelper.LoadContainerFromCache(Path.Combine(_tempDir, "nonexistent.bin"), "fp"); Assert.That(result, Is.Null); } @@ -207,11 +206,10 @@ public void RoundTrip_FullCycle_BuildSaveLoadRestore() container.FileList["text\\localisation.loc"] = new PackFile("localisation.loc", new PackedFileSource(parent, 256, 512, false, true, CompressionFormat.Lz4, 1024)); - // Act: build → save → load → restore + // Act: build → save → load var cacheData = PackFileContainerCacheHelper.BuildCacheData("test_fp", container); PackFileContainerCacheHelper.SaveCache(cacheData, _cacheFilePath); - var loadedData = PackFileContainerCacheHelper.LoadCache(_cacheFilePath); - var restored = PackFileContainerCacheHelper.RestoreFromCache(loadedData!); + var restored = PackFileContainerCacheHelper.LoadContainerFromCache(_cacheFilePath, "test_fp"); // Assert: restored container matches original Assert.That(restored.Name, Is.EqualTo("Full Cycle Test")); @@ -234,7 +232,7 @@ public void RoundTrip_FullCycle_BuildSaveLoadRestore() public void LoadCache_ReturnsNullForBadMagic() { File.WriteAllBytes(_cacheFilePath, [0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00]); - var result = PackFileContainerCacheHelper.LoadCache(_cacheFilePath); + var result = PackFileContainerCacheHelper.LoadContainerFromCache(_cacheFilePath, "fp"); Assert.That(result, Is.Null); } @@ -248,8 +246,50 @@ public void LoadCache_ReturnsNullForWrongVersion() writer.Write(999); // wrong version } - var result = PackFileContainerCacheHelper.LoadCache(_cacheFilePath); + var result = PackFileContainerCacheHelper.LoadContainerFromCache(_cacheFilePath, "fp"); Assert.That(result, Is.Null); } + + [Test] + public void LoadContainerFromCache_ReturnsNullForFingerprintMismatch() + { + var container = new PackFileContainer("Test") + { + IsCaPackFile = true, + SystemFilePath = @"c:\game\data" + }; + var parent = new PackedFileSourceParent { FilePath = @"c:\game\data\pack.pack" }; + container.FileList["a.txt"] = new PackFile("a.txt", + new PackedFileSource(parent, 0, 10, false, false, CompressionFormat.None, 0)); + + var cacheData = PackFileContainerCacheHelper.BuildCacheData("original_fp", container); + PackFileContainerCacheHelper.SaveCache(cacheData, _cacheFilePath); + + var result = PackFileContainerCacheHelper.LoadContainerFromCache(_cacheFilePath, "different_fp"); + Assert.That(result, Is.Null); + } + + [Test] + public void LoadContainerFromCache_SharesParentInstances() + { + var container = new PackFileContainer("Test") + { + IsCaPackFile = true, + SystemFilePath = @"c:\game" + }; + var parent = new PackedFileSourceParent { FilePath = @"c:\pack.pack" }; + container.FileList["a.txt"] = new PackFile("a.txt", + new PackedFileSource(parent, 0, 10, false, false, CompressionFormat.None, 0)); + container.FileList["b.txt"] = new PackFile("b.txt", + new PackedFileSource(parent, 10, 20, false, false, CompressionFormat.None, 0)); + + var cacheData = PackFileContainerCacheHelper.BuildCacheData("fp", container); + PackFileContainerCacheHelper.SaveCache(cacheData, _cacheFilePath); + + var restored = PackFileContainerCacheHelper.LoadContainerFromCache(_cacheFilePath, "fp"); + var sourceA = (PackedFileSource)restored!.FileList["a.txt"].DataSource; + var sourceB = (PackedFileSource)restored.FileList["b.txt"].DataSource; + Assert.That(ReferenceEquals(sourceA.Parent, sourceB.Parent), Is.True); + } } }