From cdbdc1f99fd233f1ae01f1478e100800d247e149 Mon Sep 17 00:00:00 2001 From: Masusder <59669685+Masusder@users.noreply.github.com> Date: Sat, 20 Jun 2026 15:46:45 +0200 Subject: [PATCH] Improved loose files support --- CUE4Parse | 2 +- FModel/ViewModels/AssetsFolderViewModel.cs | 41 +++++-- FModel/ViewModels/CUE4ParseViewModel.cs | 1 + FModel/ViewModels/Commands/LoadCommand.cs | 13 +- FModel/ViewModels/GameDirectoryViewModel.cs | 60 +++++++++- FModel/Views/Resources/Resources.xaml | 125 +++++++++++++++----- 6 files changed, 193 insertions(+), 49 deletions(-) diff --git a/CUE4Parse b/CUE4Parse index acf11fb8..024b005c 160000 --- a/CUE4Parse +++ b/CUE4Parse @@ -1 +1 @@ -Subproject commit acf11fb81a7239258e8c7ae0d33eedc865d92a03 +Subproject commit 024b005c4d15e8082ecebfb202700d59bb6113c0 diff --git a/FModel/ViewModels/AssetsFolderViewModel.cs b/FModel/ViewModels/AssetsFolderViewModel.cs index 8a4bfb22..d5af8e98 100644 --- a/FModel/ViewModels/AssetsFolderViewModel.cs +++ b/FModel/ViewModels/AssetsFolderViewModel.cs @@ -218,6 +218,17 @@ public class AssetsFolderViewModel var treeItems = new RangeObservableCollection(); treeItems.SetSuppressionState(true); + static TreeItem FindByHeaderOrNull(IReadOnlyList list, string header) + { + for (var i = 0; i < list.Count; i++) + { + if (list[i].Header == header) + return list[i]; + } + + return null; + } + foreach (var entry in entries) { TreeItem lastNode = null; @@ -226,23 +237,31 @@ public class AssetsFolderViewModel var builder = new StringBuilder(64); var parentNode = treeItems; + if (folders.Length <= 1) + { + var rootNode = FindByHeaderOrNull(treeItems, "Content"); + if (rootNode == null) + { + rootNode = new TreeItem("Content", entry, "Content") + { + Parent = null + }; + + rootNode.Folders.SetSuppressionState(true); + rootNode.AssetsList.Assets.SetSuppressionState(true); + treeItems.Add(rootNode); + } + + rootNode.AssetsList.Add(entry); + continue; + } + for (var i = 0; i < folders.Length - 1; i++) { var folder = folders[i]; builder.Append(folder).Append('/'); lastNode = FindByHeaderOrNull(parentNode, folder); - static TreeItem FindByHeaderOrNull(IReadOnlyList list, string header) - { - for (var i = 0; i < list.Count; i++) - { - if (list[i].Header == header) - return list[i]; - } - - return null; - } - if (lastNode == null) { var nodePath = builder.ToString(); diff --git a/FModel/ViewModels/CUE4ParseViewModel.cs b/FModel/ViewModels/CUE4ParseViewModel.cs index cf392929..ee21c2c2 100644 --- a/FModel/ViewModels/CUE4ParseViewModel.cs +++ b/FModel/ViewModels/CUE4ParseViewModel.cs @@ -336,6 +336,7 @@ public class CUE4ParseViewModel : ViewModel } Provider.Initialize(); + GameDirectory.AddLooseFiles(Provider.LooseFileCount); _wwiseProviderLazy = new Lazy(() => new WwiseProvider(Provider, UserSettings.Default.GameDirectory)); _fmodProviderLazy = new Lazy(() => new FModProvider(Provider, UserSettings.Default.GameDirectory)); _criWareProviderLazy = new Lazy(() => new CriWareProvider(Provider, UserSettings.Default.GameDirectory)); diff --git a/FModel/ViewModels/Commands/LoadCommand.cs b/FModel/ViewModels/Commands/LoadCommand.cs index fc4ad6cb..f476ea24 100644 --- a/FModel/ViewModels/Commands/LoadCommand.cs +++ b/FModel/ViewModels/Commands/LoadCommand.cs @@ -113,6 +113,7 @@ public class LoadCommand : ViewModelCommand private void FilterDirectoryFilesToDisplay(CancellationToken cancellationToken, IEnumerable directoryFiles) { HashSet filter; + var includeLooseFiles = false; if (directoryFiles == null) filter = null; else { @@ -120,11 +121,17 @@ public class LoadCommand : ViewModelCommand foreach (var directoryFile in directoryFiles) { if (!directoryFile.IsEnabled) continue; + if (directoryFile.IsLooseFilesContainer) + { + includeLooseFiles = true; + continue; + } filter.Add(directoryFile.Name); } } var hasFilter = filter != null && filter.Count != 0; + var hasSelection = hasFilter || includeLooseFiles; var entries = new List(); foreach (var asset in _applicationView.CUE4Parse.Provider.Files.Values) @@ -132,12 +139,16 @@ public class LoadCommand : ViewModelCommand cancellationToken.ThrowIfCancellationRequested(); // cancel if needed if (asset.IsUePackagePayload) continue; - if (hasFilter) + if (hasSelection) { if (asset is VfsEntry entry && filter.Contains(entry.Vfs.Name)) { entries.Add(asset); } + else if (includeLooseFiles && asset is OsGameFile) + { + entries.Add(asset); + } } else { diff --git a/FModel/ViewModels/GameDirectoryViewModel.cs b/FModel/ViewModels/GameDirectoryViewModel.cs index 730b7895..1d358e6c 100644 --- a/FModel/ViewModels/GameDirectoryViewModel.cs +++ b/FModel/ViewModels/GameDirectoryViewModel.cs @@ -56,6 +56,13 @@ public class FileItem : ViewModel set => SetProperty(ref _isEnabled, value); } + private bool _isLooseFilesContainer; + public bool IsLooseFilesContainer + { + get => _isLooseFilesContainer; + set => SetProperty(ref _isLooseFilesContainer, value); + } + private string _key; public string Key { @@ -83,6 +90,18 @@ public class FileItem : ViewModel Length = length; } + public FileItem(string name, int fileCount, long length, bool isLooseFile) + { + Name = name; + Length = length; + FileCount = fileCount; + IsLooseFilesContainer = isLooseFile; + IsEnabled = true; + Key = string.Empty; + MountPoint = string.Empty; + CompressionMethods = []; + } + public FileItem(IAesVfsReader reader) { Name = reader.Name; @@ -90,6 +109,7 @@ public class FileItem : ViewModel Guid = reader.EncryptionKeyGuid; IsEncrypted = reader.IsEncrypted; IsEnabled = false; + IsLooseFilesContainer = false; Key = string.Empty; FileCount = reader is IoStoreReader storeReader ? (int) storeReader.TocResource.Header.TocEntryCount - 1 : 0; CompressionMethods = reader.CompressionMethods; @@ -101,19 +121,25 @@ public class FileItem : ViewModel } } -public class GameDirectoryViewModel : ViewModel +public partial class GameDirectoryViewModel : ViewModel { - public bool HasNoFile => DirectoryFiles.Count < 1; public readonly ObservableCollection DirectoryFiles; + public ICollectionView DirectoryFilesView { get; } - private readonly Regex _hiddenArchives = new(@"^(?!global|pakchunk.+(optional|ondemand)\-).+(pak|utoc)$", // should be universal - RegexOptions.Compiled | RegexOptions.Singleline | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); + private readonly Regex _hiddenArchives = ArchivesRegex(); public GameDirectoryViewModel() { - DirectoryFiles = new ObservableCollection(); - DirectoryFilesView = new ListCollectionView(DirectoryFiles) { SortDescriptions = { new SortDescription("Name", ListSortDirection.Ascending) } }; + DirectoryFiles = []; + DirectoryFilesView = new ListCollectionView(DirectoryFiles) + { + SortDescriptions = + { + new SortDescription(nameof(FileItem.IsLooseFilesContainer), ListSortDirection.Ascending), + new SortDescription(nameof(FileItem.Name), ListSortDirection.Ascending) + } + }; } public void Add(IAesVfsReader reader) @@ -124,6 +150,25 @@ public class GameDirectoryViewModel : ViewModel Application.Current.Dispatcher.Invoke(() => DirectoryFiles.Add(fileItem)); } + public void AddLooseFiles(int fileCount) + { + if (fileCount < 1) + return; + + Application.Current.Dispatcher.Invoke(() => + { + var looseFilesContainer = DirectoryFiles.FirstOrDefault(x => x.IsLooseFilesContainer); + if (looseFilesContainer is not null) + { + looseFilesContainer.FileCount += fileCount; + } + else + { + DirectoryFiles.Add(new FileItem("Loose Files", fileCount, 0, true)); + } + }); + } + public void Verify(IAesVfsReader reader) { if (DirectoryFiles.FirstOrDefault(x => x.Name == reader.Name) is not { } file) return; @@ -138,4 +183,7 @@ public class GameDirectoryViewModel : ViewModel if (DirectoryFiles.FirstOrDefault(x => x.Name == reader.Name) is not { } file) return; file.IsEnabled = false; } + + [GeneratedRegex(@"^(?!global|pakchunk.+(optional|ondemand)\-).+(pak|utoc)$", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.Singleline | RegexOptions.CultureInvariant)] + private static partial Regex ArchivesRegex(); } diff --git a/FModel/Views/Resources/Resources.xaml b/FModel/Views/Resources/Resources.xaml index c33eb148..712fd2f0 100644 --- a/FModel/Views/Resources/Resources.xaml +++ b/FModel/Views/Resources/Resources.xaml @@ -98,17 +98,35 @@ - @@ -119,50 +137,97 @@ - - + + - - - - + + + + - - + + - - + + + + + + - + - - - + + - - + + - - + + - - + +