using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Threading; using AdonisUI.Controls; using CUE4Parse.UE4.Readers; using CUE4Parse.UE4.VirtualFileSystem; using FModel.Extensions; using FModel.Framework; using FModel.Services; using FModel.Settings; using FModel.Views.Resources.Controls; using K4os.Compression.LZ4.Streams; using Microsoft.Win32; namespace FModel.ViewModels.Commands; /// /// this will always load all files no matter the loading mode /// however what this does is filtering what to show to the user /// public class LoadCommand : ViewModelCommand { private const uint _IS_LZ4 = 0x184D2204u; private ThreadWorkerViewModel _threadWorkerView => ApplicationService.ThreadWorkerView; private ApplicationViewModel _applicationView => ApplicationService.ApplicationView; private DiscordHandler _discordHandler => DiscordService.DiscordHandler; public LoadCommand(LoadingModesViewModel contextViewModel) : base(contextViewModel) { } public override async void Execute(LoadingModesViewModel contextViewModel, object parameter) { if (_applicationView.CUE4Parse.GameDirectory.HasNoFile) return; if (_applicationView.CUE4Parse.Provider.Files.Count <= 0) { FLogger.AppendError(); FLogger.AppendText("An encrypted archive has been found. In order to decrypt it, please specify a working AES encryption key", Constants.WHITE, true); return; } if (_applicationView.CUE4Parse.Game == FGame.FortniteGame && _applicationView.CUE4Parse.Provider.MappingsContainer == null) { FLogger.AppendError(); FLogger.AppendText("Mappings could not get pulled, extracting packages might not work properly. If so, either press F12, restart, or come back later.", Constants.WHITE, true); } #if DEBUG var loadingTime = Stopwatch.StartNew(); #endif _applicationView.CUE4Parse.AssetsFolder.Folders.Clear(); _applicationView.CUE4Parse.SearchVm.SearchResults.Clear(); MainWindow.YesWeCats.LeftTabControl.SelectedIndex = 1; // folders tab await _applicationView.CUE4Parse.LoadLocalizedResources(); // load locres if not already loaded await _applicationView.CUE4Parse.LoadVirtualPaths(); // load virtual paths if not already loaded Helper.CloseWindow("Search View"); // close search window if opened await _threadWorkerView.Begin(cancellationToken => { // filter what to show switch (UserSettings.Default.LoadingMode) { case ELoadingMode.Single: case ELoadingMode.Multiple: { var l = (IList) parameter; if (l.Count < 1) return; var directoryFilesToShow = l.Cast(); FilterDirectoryFilesToDisplay(cancellationToken, directoryFilesToShow); break; } case ELoadingMode.All: { FilterDirectoryFilesToDisplay(cancellationToken, null); break; } case ELoadingMode.AllButNew: case ELoadingMode.AllButModified: { FilterNewOrModifiedFilesToDisplay(cancellationToken); break; } default: throw new ArgumentOutOfRangeException(); } _discordHandler.UpdatePresence(_applicationView.CUE4Parse); }); #if DEBUG loadingTime.Stop(); FLogger.AppendDebug(); FLogger.AppendText($"{_applicationView.CUE4Parse.SearchVm.SearchResults.Count} packages, {_applicationView.CUE4Parse.LocalizedResourcesCount} localized resources, and {_applicationView.CUE4Parse.VirtualPathCount} virtual paths loaded in {loadingTime.Elapsed.TotalSeconds.ToString("F3", CultureInfo.InvariantCulture)} seconds", Constants.WHITE, true); #endif } private void FilterDirectoryFilesToDisplay(CancellationToken cancellationToken, IEnumerable directoryFiles) { HashSet filter; if (directoryFiles == null) filter = null; else { filter = new HashSet(); foreach (var directoryFile in directoryFiles) { if (!directoryFile.IsEnabled) continue; filter.Add(directoryFile.Name); } } var hasFilter = filter != null && filter.Count != 0; var entries = new List(); foreach (var asset in _applicationView.CUE4Parse.Provider.Files.Values) { cancellationToken.ThrowIfCancellationRequested(); // cancel if needed if (asset is not VfsEntry entry || entry.Path.EndsWith(".uexp") || entry.Path.EndsWith(".ubulk") || entry.Path.EndsWith(".uptnl")) continue; if (hasFilter) { if (filter.Contains(entry.Vfs.Name)) { entries.Add(entry); _applicationView.Status.UpdateStatusLabel(entry.Vfs.Name); } } else { entries.Add(entry); _applicationView.Status.UpdateStatusLabel(entry.Vfs.Name); } } _applicationView.Status.UpdateStatusLabel("Folders & Packages"); _applicationView.CUE4Parse.AssetsFolder.BulkPopulate(entries); } private void FilterNewOrModifiedFilesToDisplay(CancellationToken cancellationToken) { var openFileDialog = new OpenFileDialog { Title = "Select a backup file older than your current game version", InitialDirectory = Path.Combine(UserSettings.Default.OutputDirectory, "Backups"), Filter = "FBKP Files (*.fbkp)|*.fbkp|All Files (*.*)|*.*", Multiselect = false }; if (!openFileDialog.ShowDialog().GetValueOrDefault()) return; FLogger.AppendInformation(); FLogger.AppendText($"Backup file older than current game is '{openFileDialog.FileName.SubstringAfterLast("\\")}'", Constants.WHITE, true); using var fileStream = new FileStream(openFileDialog.FileName, FileMode.Open); using var memoryStream = new MemoryStream(); if (fileStream.ReadUInt32() == _IS_LZ4) { fileStream.Position -= 4; using var compressionStream = LZ4Stream.Decode(fileStream); compressionStream.CopyTo(memoryStream); } else fileStream.CopyTo(memoryStream); memoryStream.Position = 0; using var archive = new FStreamArchive(fileStream.Name, memoryStream); var entries = new List(); var mode = UserSettings.Default.LoadingMode; switch (mode) { case ELoadingMode.AllButNew: { var paths = new Dictionary(); while (archive.Position < archive.Length) { cancellationToken.ThrowIfCancellationRequested(); archive.Position += 29; paths[archive.ReadString().ToLower()[1..]] = 0; archive.Position += 4; } foreach (var (key, value) in _applicationView.CUE4Parse.Provider.Files) { cancellationToken.ThrowIfCancellationRequested(); if (value is not VfsEntry entry || paths.ContainsKey(key) || entry.Path.EndsWith(".uexp") || entry.Path.EndsWith(".ubulk") || entry.Path.EndsWith(".uptnl")) continue; entries.Add(entry); _applicationView.Status.UpdateStatusLabel(entry.Vfs.Name); } break; } case ELoadingMode.AllButModified: { while (archive.Position < archive.Length) { cancellationToken.ThrowIfCancellationRequested(); archive.Position += 16; var uncompressedSize = archive.Read(); var isEncrypted = archive.ReadFlag(); archive.Position += 4; var fullPath = archive.ReadString().ToLower()[1..]; archive.Position += 4; if (fullPath.EndsWith(".uexp") || fullPath.EndsWith(".ubulk") || fullPath.EndsWith(".uptnl") || !_applicationView.CUE4Parse.Provider.Files.TryGetValue(fullPath, out var asset) || asset is not VfsEntry entry || entry.Size == uncompressedSize && entry.IsEncrypted == isEncrypted) continue; entries.Add(entry); _applicationView.Status.UpdateStatusLabel(entry.Vfs.Name); } break; } } _applicationView.Status.UpdateStatusLabel($"{mode.ToString()[6..]} Folders & Packages"); _applicationView.CUE4Parse.AssetsFolder.BulkPopulate(entries); } }