diff --git a/CUE4Parse b/CUE4Parse index cd9fa6d9..25d982ec 160000 --- a/CUE4Parse +++ b/CUE4Parse @@ -1 +1 @@ -Subproject commit cd9fa6d9c7f521370e40045d9515e982fc486bc0 +Subproject commit 25d982ecf40620038525ebd7adbd07ed644df2fe diff --git a/FModel/MainWindow.xaml.cs b/FModel/MainWindow.xaml.cs index 36342c3b..3024cc91 100644 --- a/FModel/MainWindow.xaml.cs +++ b/FModel/MainWindow.xaml.cs @@ -109,6 +109,14 @@ public partial class MainWindow ApplicationViewModel.InitZlib() ); + var auxiliaryInitialization = Task.WhenAll( + _applicationView.CUE4Parse.InitMappings(), + ApplicationViewModel.InitDetex(), + ApplicationViewModel.InitVgmStream(), + ApplicationViewModel.InitImGuiSettings(newOrUpdated), + UserSettings.Default.DecompileLua ? ApplicationViewModel.InitUnluac() : Task.CompletedTask + ); + await _applicationView.CUE4Parse.Initialize(); await _applicationView.AesManager.InitAes(); await _applicationView.UpdateProvider(true); @@ -119,16 +127,12 @@ public partial class MainWindow await Task.WhenAll( _applicationView.CUE4Parse.VerifyConsoleVariables(), _applicationView.CUE4Parse.VerifyOnDemandArchives(), - _applicationView.CUE4Parse.InitMappings(), - ApplicationViewModel.InitDetex(), - ApplicationViewModel.InitVgmStream(), - ApplicationViewModel.InitImGuiSettings(newOrUpdated), + auxiliaryInitialization, Task.Run(() => { if (UserSettings.Default.DiscordRpc == EDiscordRpc.Always) _discordHandler.Initialize(_applicationView.GameDisplayName); - }), - UserSettings.Default.DecompileLua ? ApplicationViewModel.InitUnluac() : Task.CompletedTask + }) ).ConfigureAwait(false); #if DEBUG diff --git a/FModel/ViewModels/ApplicationViewModel.cs b/FModel/ViewModels/ApplicationViewModel.cs index 3120ddd4..700c09b2 100644 --- a/FModel/ViewModels/ApplicationViewModel.cs +++ b/FModel/ViewModels/ApplicationViewModel.cs @@ -6,6 +6,7 @@ using System.IO.Compression; using System.Linq; using System.Threading.Tasks; using System.Windows; +using System.Windows.Threading; using CUE4Parse_Conversion.Textures.BC; using CUE4Parse.Compression; using CUE4Parse.Encryption.Aes; @@ -27,6 +28,10 @@ namespace FModel.ViewModels; public class ApplicationViewModel : ViewModel { + private readonly object _providerStatusLock = new(); + private (string Label, string Prefix)? _pendingProviderStatus; + private bool _providerStatusScheduled; + private EBuildKind _build; public EBuildKind Build { @@ -115,13 +120,13 @@ public class ApplicationViewModel : ViewModel CUE4Parse.Provider.VfsRegistered += (sender, count) => { if (sender is not IAesVfsReader reader) return; - Status.UpdateStatusLabel($"{count} Archives ({reader.Name})", "Registered"); + QueueProviderStatus($"{count} Archives ({reader.Name})", "Registered"); CUE4Parse.GameDirectory.Add(reader); }; CUE4Parse.Provider.VfsMounted += (sender, count) => { if (sender is not IAesVfsReader reader) return; - Status.UpdateStatusLabel($"{count:N0} Packages ({reader.Name})", "Mounted"); + QueueProviderStatus($"{count:N0} Packages ({reader.Name})", "Mounted"); CUE4Parse.GameDirectory.Verify(reader); }; CUE4Parse.Provider.VfsUnmounted += (sender, _) => @@ -137,6 +142,34 @@ public class ApplicationViewModel : ViewModel Status.SetStatus(EStatusKind.Ready); } + private void QueueProviderStatus(string label, string prefix) + { + lock (_providerStatusLock) + { + _pendingProviderStatus = (label, prefix); + if (_providerStatusScheduled) + return; + + _providerStatusScheduled = true; + } + + _ = Application.Current.Dispatcher.BeginInvoke(PublishProviderStatus, DispatcherPriority.Background); + } + + private void PublishProviderStatus() + { + (string Label, string Prefix)? update; + lock (_providerStatusLock) + { + update = _pendingProviderStatus; + _pendingProviderStatus = null; + _providerStatusScheduled = false; + } + + if (update is { } status) + Status.UpdateStatusLabel(status.Label, status.Prefix); + } + public DirectorySettings AvoidEmptyGameDirectory(bool bAlreadyLaunched) { var gameDirectory = UserSettings.Default.GameDirectory; diff --git a/FModel/ViewModels/CUE4ParseViewModel.cs b/FModel/ViewModels/CUE4ParseViewModel.cs index 8c445f32..162345ec 100644 --- a/FModel/ViewModels/CUE4ParseViewModel.cs +++ b/FModel/ViewModels/CUE4ParseViewModel.cs @@ -334,6 +334,7 @@ public class CUE4ParseViewModel : ViewModel Provider.Initialize(); GameDirectory.AddLooseFiles(Provider.LooseFileCount); + GameDirectory.FlushPendingChanges(); _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)); @@ -379,6 +380,7 @@ public class CUE4ParseViewModel : ViewModel { Provider.SubmitKeys(aesKeys); Provider.PostMount(); + GameDirectory.FlushPendingChanges(); var aesMax = Provider.RequiredKeys.Count + Provider.Keys.Count; var archiveMax = Provider.UnloadedVfs.Count + Provider.MountedVfs.Count; diff --git a/FModel/ViewModels/GameDirectoryViewModel.cs b/FModel/ViewModels/GameDirectoryViewModel.cs index 1d358e6c..d4760433 100644 --- a/FModel/ViewModels/GameDirectoryViewModel.cs +++ b/FModel/ViewModels/GameDirectoryViewModel.cs @@ -1,10 +1,12 @@ using FModel.Framework; -using System.Collections.ObjectModel; +using System.Collections.Concurrent; +using System.Collections.Generic; using System.ComponentModel; -using System.Linq; using System.Text.RegularExpressions; +using System.Threading; using System.Windows; using System.Windows.Data; +using System.Windows.Threading; using CUE4Parse.Compression; using CUE4Parse.UE4.IO; using CUE4Parse.UE4.Objects.Core.Misc; @@ -123,11 +125,19 @@ public class FileItem : ViewModel public partial class GameDirectoryViewModel : ViewModel { - public readonly ObservableCollection DirectoryFiles; + private readonly record struct FileItemUpdate(FileItem File, bool IsEnabled, string MountPoint, + int FileCount, bool HasMountInfo); + + public readonly RangeObservableCollection DirectoryFiles; public ICollectionView DirectoryFilesView { get; } private readonly Regex _hiddenArchives = ArchivesRegex(); + private readonly ConcurrentDictionary _filesByReader = + new(ReferenceEqualityComparer.Instance); + private readonly ConcurrentQueue _pendingAdditions = new(); + private readonly ConcurrentQueue _pendingUpdates = new(); + private int _publishScheduled; public GameDirectoryViewModel() { @@ -147,7 +157,11 @@ public partial class GameDirectoryViewModel : ViewModel if (!_hiddenArchives.IsMatch(reader.Name)) return; var fileItem = new FileItem(reader); - Application.Current.Dispatcher.Invoke(() => DirectoryFiles.Add(fileItem)); + if (!_filesByReader.TryAdd(reader, fileItem)) + return; + + _pendingAdditions.Enqueue(fileItem); + SchedulePublish(); } public void AddLooseFiles(int fileCount) @@ -155,33 +169,64 @@ public partial class GameDirectoryViewModel : ViewModel 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)); - } - }); + _pendingAdditions.Enqueue(new FileItem("Loose Files", fileCount, 0, true)); + SchedulePublish(); } public void Verify(IAesVfsReader reader) { - if (DirectoryFiles.FirstOrDefault(x => x.Name == reader.Name) is not { } file) return; + if (!_filesByReader.TryGetValue(reader, out var file)) return; - file.IsEnabled = true; - file.MountPoint = reader.MountPoint; - file.FileCount = reader.FileCount; + _pendingUpdates.Enqueue(new FileItemUpdate(file, true, reader.MountPoint, reader.FileCount, true)); + SchedulePublish(); } public void Disable(IAesVfsReader reader) { - if (DirectoryFiles.FirstOrDefault(x => x.Name == reader.Name) is not { } file) return; - file.IsEnabled = false; + if (!_filesByReader.TryGetValue(reader, out var file)) return; + + _pendingUpdates.Enqueue(new FileItemUpdate(file, false, string.Empty, 0, false)); + SchedulePublish(); + } + + public void FlushPendingChanges() + { + if (Application.Current.Dispatcher.CheckAccess()) + PublishPendingChanges(); + else + Application.Current.Dispatcher.Invoke(PublishPendingChanges); + } + + private void SchedulePublish() + { + if (Interlocked.CompareExchange(ref _publishScheduled, 1, 0) != 0) + return; + + _ = Application.Current.Dispatcher.BeginInvoke(PublishPendingChanges, DispatcherPriority.Background); + } + + private void PublishPendingChanges() + { + var additions = new List(); + while (_pendingAdditions.TryDequeue(out var file)) + additions.Add(file); + + if (additions.Count > 0) + DirectoryFiles.AddRange(additions); + + while (_pendingUpdates.TryDequeue(out var update)) + { + update.File.IsEnabled = update.IsEnabled; + if (!update.HasMountInfo) + continue; + + update.File.MountPoint = update.MountPoint; + update.File.FileCount = update.FileCount; + } + + Interlocked.Exchange(ref _publishScheduled, 0); + if (!_pendingAdditions.IsEmpty || !_pendingUpdates.IsEmpty) + SchedulePublish(); } [GeneratedRegex(@"^(?!global|pakchunk.+(optional|ondemand)\-).+(pak|utoc)$", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.Singleline | RegexOptions.CultureInvariant)] diff --git a/FModel/Views/Resources/Resources.xaml b/FModel/Views/Resources/Resources.xaml index 876e6440..7cb4b638 100644 --- a/FModel/Views/Resources/Resources.xaml +++ b/FModel/Views/Resources/Resources.xaml @@ -105,6 +105,12 @@ Value="{Binding CUE4Parse.GameDirectory.DirectoryFilesView, IsAsync=True}" /> + + +