using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Net.Http; using System.Net.Http.Headers; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using System.Windows; using AdonisUI.Controls; using CUE4Parse; using CUE4Parse.Compression; using CUE4Parse.Encryption.Aes; using CUE4Parse.FileProvider; using CUE4Parse.FileProvider.Objects; using CUE4Parse.FileProvider.Vfs; using CUE4Parse.GameTypes.Aion2.Objects; using CUE4Parse.GameTypes.AshEchoes.FileProvider; using CUE4Parse.GameTypes.SMG.UE4.Assets.Exports.Wwise; using CUE4Parse.GameTypes.KRD.Assets.Exports; using CUE4Parse.MappingsProvider; using CUE4Parse.UE4.AssetRegistry; using CUE4Parse.UE4.Assets; using CUE4Parse.UE4.Assets.Exports; using CUE4Parse.UE4.Assets.Exports.Animation; using CUE4Parse.UE4.Assets.Exports.CriWare; using CUE4Parse.UE4.Assets.Exports.Fmod; using CUE4Parse.UE4.Assets.Exports.Material; using CUE4Parse.UE4.Assets.Exports.SkeletalMesh; using CUE4Parse.UE4.Assets.Exports.Sound; using CUE4Parse.UE4.Assets.Exports.StaticMesh; using CUE4Parse.UE4.Assets.Exports.Texture; using CUE4Parse.UE4.Assets.Exports.Verse; using CUE4Parse.UE4.Assets.Exports.Wwise; using CUE4Parse.UE4.BinaryConfig; using CUE4Parse.UE4.CriWare; using CUE4Parse.UE4.CriWare.Readers; using CUE4Parse.UE4.FMod; using CUE4Parse.UE4.IO; using CUE4Parse.UE4.Localization; using CUE4Parse.UE4.Objects.Core.Serialization; using CUE4Parse.UE4.Objects.Engine; using CUE4Parse.UE4.Objects.UObject; using CUE4Parse.UE4.Objects.UObject.Editor; using CUE4Parse.UE4.Oodle.Objects; using CUE4Parse.UE4.Readers; using CUE4Parse.UE4.Shaders; using CUE4Parse.UE4.Versions; using CUE4Parse.UE4.Wwise; using CUE4Parse.Utils; using CUE4Parse_Conversion; using CUE4Parse_Conversion.Sounds; using EpicManifestParser; using EpicManifestParser.UE; using EpicManifestParser.ZlibngDotNetDecompressor; using FModel.Creator; using FModel.Extensions; using FModel.Framework; using FModel.Services; using FModel.Settings; using FModel.Views; using FModel.Views.Resources.Controls; using FModel.Views.Snooper; using Newtonsoft.Json; using Newtonsoft.Json.Converters; using OpenTK.Windowing.Common; using OpenTK.Windowing.Desktop; using Serilog; using SkiaSharp; using Svg.Skia; using UE4Config.Parsing; using Application = System.Windows.Application; using FGuid = CUE4Parse.UE4.Objects.Core.Misc.FGuid; namespace FModel.ViewModels; public class CUE4ParseViewModel : ViewModel { private ThreadWorkerViewModel _threadWorkerView => ApplicationService.ThreadWorkerView; private ApiEndpointViewModel _apiEndpointView => ApplicationService.ApiEndpointView; private readonly Regex _fnLiveRegex = new(@"^FortniteGame[/\\]Content[/\\]Paks[/\\]", RegexOptions.Compiled | RegexOptions.Singleline | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); private bool _modelIsOverwritingMaterial; public bool ModelIsOverwritingMaterial { get => _modelIsOverwritingMaterial; set => SetProperty(ref _modelIsOverwritingMaterial, value); } private bool _modelIsWaitingAnimation; public bool ModelIsWaitingAnimation { get => _modelIsWaitingAnimation; set => SetProperty(ref _modelIsWaitingAnimation, value); } public bool IsSnooperOpen => _snooper is { Exists: true, IsVisible: true }; private Snooper _snooper; public Snooper SnooperViewer { get { if (_snooper != null) return _snooper; return Application.Current.Dispatcher.Invoke(delegate { var scale = ImGuiController.GetDpiScale(); var htz = Snooper.GetMaxRefreshFrequency(); return _snooper = new Snooper( new GameWindowSettings { UpdateFrequency = htz }, new NativeWindowSettings { ClientSize = new OpenTK.Mathematics.Vector2i( Convert.ToInt32(SystemParameters.MaximizedPrimaryScreenWidth * .75 * scale), Convert.ToInt32(SystemParameters.MaximizedPrimaryScreenHeight * .85 * scale)), NumberOfSamples = Constants.SAMPLES_COUNT, WindowBorder = WindowBorder.Resizable, Flags = ContextFlags.ForwardCompatible, Profile = ContextProfile.Core, Vsync = VSyncMode.Adaptive, APIVersion = new Version(4, 6), StartVisible = false, StartFocused = false, Title = "3D Viewer" }); }); } } public AbstractVfsFileProvider Provider { get; } public GameDirectoryViewModel GameDirectory { get; } public AssetsFolderViewModel AssetsFolder { get; } public SearchViewModel SearchVm { get; } public SearchViewModel RefVm { get; } public TabControlViewModel TabControl { get; } public ConfigIni IoStoreOnDemand { get; } private Lazy _wwiseProviderLazy; public WwiseProvider WwiseProvider => _wwiseProviderLazy.Value; private Lazy _fmodProviderLazy; public FModProvider FmodProvider => _fmodProviderLazy?.Value; private Lazy _criWareProviderLazy; public CriWareProvider CriWareProvider => _criWareProviderLazy?.Value; public ConcurrentBag UnknownExtensions = []; public CUE4ParseViewModel() { var currentDir = UserSettings.Default.CurrentDir; var gameDirectory = currentDir.GameDirectory; var versionContainer = new VersionContainer( game: currentDir.UeVersion, platform: currentDir.TexturePlatform, customVersions: new FCustomVersionContainer(currentDir.Versioning.CustomVersions), optionOverrides: currentDir.Versioning.Options, mapStructTypesOverrides: currentDir.Versioning.MapStructTypes); var pathComparer = StringComparer.OrdinalIgnoreCase; switch (gameDirectory) { case Constants._FN_LIVE_TRIGGER: { Provider = new StreamedFileProvider("FortniteLive", versionContainer, pathComparer); break; } case Constants._VAL_LIVE_TRIGGER: { Provider = new StreamedFileProvider("ValorantLive", versionContainer, pathComparer); break; } default: { var project = gameDirectory.SubstringBeforeLast(gameDirectory.Contains("eFootball") ? "\\pak" : "\\Content").SubstringAfterLast("\\"); Provider = project switch { "StateOfDecay2" => new DefaultFileProvider(new DirectoryInfo(gameDirectory), [ new(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + "\\StateOfDecay2\\Saved\\Paks"), new(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + "\\StateOfDecay2\\Saved\\DisabledPaks") ], SearchOption.AllDirectories, versionContainer, pathComparer), "eFootball" => new DefaultFileProvider(new DirectoryInfo(gameDirectory), [ new(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData) + "\\KONAMI\\eFootball\\ST\\Download") ], SearchOption.AllDirectories, versionContainer, pathComparer), _ when versionContainer.Game is EGame.GAME_AshEchoes => new AEDefaultFileProvider(gameDirectory, SearchOption.AllDirectories, versionContainer, pathComparer), _ when versionContainer.Game is EGame.GAME_BlackStigma => new DefaultFileProvider(gameDirectory, SearchOption.AllDirectories, versionContainer, StringComparer.Ordinal), _ => new DefaultFileProvider(gameDirectory, SearchOption.AllDirectories, versionContainer, pathComparer) }; break; } } Provider.ReadScriptData = UserSettings.Default.ReadScriptData; Provider.ReadShaderMaps = UserSettings.Default.ReadShaderMaps; Provider.ReadNaniteData = true; GameDirectory = new GameDirectoryViewModel(); AssetsFolder = new AssetsFolderViewModel(); SearchVm = new SearchViewModel(); RefVm = new SearchViewModel(); TabControl = new TabControlViewModel(); IoStoreOnDemand = new ConfigIni(nameof(IoStoreOnDemand)); } public async Task Initialize() { await _threadWorkerView.Begin(cancellationToken => { switch (Provider) { case StreamedFileProvider p: switch (p.LiveGame) { case "FortniteLive": { var manifestInfo = _apiEndpointView.EpicApi.GetManifest(cancellationToken); if (manifestInfo is null) { throw new FileLoadException("Could not load latest Fortnite manifest, you may have to switch to your local installation."); } var cacheDir = Directory.CreateDirectory(Path.Combine(UserSettings.Default.OutputDirectory, ".data")).FullName; var manifestOptions = new ManifestParseOptions { ChunkCacheDirectory = cacheDir, ManifestCacheDirectory = cacheDir, ChunkBaseUrl = "http://download.epicgames.com/Builds/Fortnite/CloudDir/", Decompressor = ManifestZlibngDotNetDecompressor.Decompress, DecompressorState = ZlibHelper.Instance, CacheChunksAsIs = false }; var startTs = Stopwatch.GetTimestamp(); FBuildPatchAppManifest manifest; try { (manifest, _) = manifestInfo.DownloadAndParseAsync(manifestOptions, cancellationToken: cancellationToken, elementManifestPredicate: static x => x.Uri.Host == "download.epicgames.com" ).GetAwaiter().GetResult(); } catch (HttpRequestException ex) { Log.Error("Failed to download manifest ({ManifestUri})", ex.Data["ManifestUri"]?.ToString() ?? ""); throw; } if (manifest.TryFindFile("Cloud/IoStoreOnDemand.ini", out var ioStoreOnDemandFile)) { IoStoreOnDemand.Read(new StreamReader(ioStoreOnDemandFile.GetStream())); } Parallel.ForEach(manifest.Files.Where(x => _fnLiveRegex.IsMatch(x.FileName)), fileManifest => { p.RegisterVfs(fileManifest.FileName, [fileManifest.GetStream()], it => new FRandomAccessStreamArchive(it, manifest.FindFile(it)!.GetStream(), p.Versions)); }); var elapsedTime = Stopwatch.GetElapsedTime(startTs); FLogger.Append(ELog.Information, () => FLogger.Text($"Fortnite [LIVE] has been loaded successfully in {elapsedTime.TotalMilliseconds:F1}ms", Constants.WHITE, true)); break; } case "ValorantLive": { var manifest = _apiEndpointView.ValorantApi.GetManifest(cancellationToken); if (manifest == null) { throw new Exception("Could not load latest Valorant manifest, you may have to switch to your local installation."); } Parallel.ForEach(manifest.Paks, pak => { p.RegisterVfs(pak.GetFullName(), [pak.GetStream(manifest)]); }); FLogger.Append(ELog.Information, () => FLogger.Text($"Valorant '{manifest.Header.GameVersion}' has been loaded successfully", Constants.WHITE, true)); break; } } break; case DefaultFileProvider: { var ioStoreOnDemandPath = Path.Combine(UserSettings.Default.GameDirectory, "..\\..\\..\\Cloud\\IoStoreOnDemand.ini"); if (File.Exists(ioStoreOnDemandPath)) { using var s = new StreamReader(ioStoreOnDemandPath); IoStoreOnDemand.Read(s); } break; } } Provider.Initialize(); _wwiseProviderLazy = new Lazy(() => new WwiseProvider(Provider, UserSettings.Default.GameDirectory, UserSettings.Default.WwiseMaxBnkPrefetch)); _fmodProviderLazy = new Lazy(() => new FModProvider(Provider, UserSettings.Default.GameDirectory)); _criWareProviderLazy = new Lazy(() => new CriWareProvider(Provider, UserSettings.Default.GameDirectory)); Log.Information($"{Provider.Versions.Game} ({Provider.Versions.Platform}) | Archives: x{Provider.UnloadedVfs.Count} | AES: x{Provider.RequiredKeys.Count} | Loose Files: x{Provider.Files.Count}"); }); } /// /// load virtual files system from GameDirectory /// /// public void LoadVfs(IEnumerable> aesKeys) { Provider.SubmitKeys(aesKeys); Provider.PostMount(); var aesMax = Provider.RequiredKeys.Count + Provider.Keys.Count; var archiveMax = Provider.UnloadedVfs.Count + Provider.MountedVfs.Count; Log.Information($"Project: {Provider.ProjectName} | Mounted: {Provider.MountedVfs.Count}/{archiveMax} | AES: {Provider.Keys.Count}/{aesMax} | Files: x{Provider.Files.Count}"); } public void ClearProvider() { if (Provider == null) return; AssetsFolder.Folders.Clear(); SearchVm.SearchResults.Clear(); Helper.CloseWindow("Search For Packages"); Provider.UnloadNonStreamedVfs(); GC.Collect(); } public async Task RefreshAes() { // game directory dependent, we don't have the provider game name yet since we don't have aes keys // except when this comes from the AES Manager if (!UserSettings.IsEndpointValid(EEndpointType.Aes, out var endpoint)) return; await _threadWorkerView.Begin(cancellationToken => { // deprecated values if (endpoint.Url == "https://fortnitecentral.genxgames.gg/api/v1/aes") endpoint.Url = "https://uedb.dev/svc/api/v1/fortnite/aes"; var aes = _apiEndpointView.DynamicApi.GetAesKeys(cancellationToken, endpoint.Url, endpoint.Path); if (aes is not { IsValid: true }) return; UserSettings.Default.CurrentDir.AesKeys = aes; }); } public async Task InitInformation() { await _threadWorkerView.Begin(cancellationToken => { var info = _apiEndpointView.FModelApi.GetNews(cancellationToken, Provider.ProjectName); if (info == null) return; FLogger.Append(ELog.None, () => { for (var i = 0; i < info.Messages.Length; i++) { FLogger.Text(info.Messages[i], info.Colors[i], bool.Parse(info.NewLines[i])); } }); }); } public Task InitMappings(bool force = false) { if (!UserSettings.IsEndpointValid(EEndpointType.Mapping, out var endpoint)) { Provider.MappingsContainer = null; return Task.CompletedTask; } return Task.Run(() => { var l = ELog.Information; if (endpoint.Overwrite && File.Exists(endpoint.FilePath)) { Provider.MappingsContainer = new FileUsmapTypeMappingsProvider(endpoint.FilePath); } else if (endpoint.IsValid) { // deprecated values if (endpoint.Path == "$.[?(@.meta.compressionMethod=='Oodle')].['url','fileName']") endpoint.Path = "$.[0].['url','fileName']"; if (endpoint.Url == "https://fortnitecentral.genxgames.gg/api/v1/mappings") { endpoint.Url = "https://uedb.dev/svc/api/v1/fortnite/mappings"; endpoint.Path = "$.mappings.ZStandard"; } var mappingsFolder = Path.Combine(UserSettings.Default.OutputDirectory, ".data"); var mappings = _apiEndpointView.DynamicApi.GetMappings(CancellationToken.None, endpoint.Url, endpoint.Path); if (mappings is { Length: > 0 }) { foreach (var mapping in mappings) { if (!mapping.IsValid) continue; var mappingPath = Path.Combine(mappingsFolder, mapping.FileName); if (force || !File.Exists(mappingPath) || new FileInfo(mappingPath).Length == 0) { _apiEndpointView.DownloadFile(mapping.Url, mappingPath); } Provider.MappingsContainer = new FileUsmapTypeMappingsProvider(mappingPath); break; } } if (Provider.MappingsContainer == null) { var latestUsmaps = new DirectoryInfo(mappingsFolder).GetFiles("*_oo.usmap"); if (latestUsmaps.Length <= 0) return; var latestUsmapInfo = latestUsmaps.OrderBy(f => f.LastWriteTime).Last(); Provider.MappingsContainer = new FileUsmapTypeMappingsProvider(latestUsmapInfo.FullName); l = ELog.Warning; } } if (Provider.MappingsContainer is FileUsmapTypeMappingsProvider m) { Log.Information($"Mappings pulled from '{m.FileName}'"); FLogger.Append(l, () => FLogger.Text($"Mappings pulled from '{m.FileName}'", Constants.WHITE, true)); } }); } public Task VerifyConsoleVariables() { if (Provider.Versions["StripAdditiveRefPose"]) { FLogger.Append(ELog.Warning, () => FLogger.Text("Additive animations have their reference pose stripped, which will lead to inaccurate preview and export", Constants.WHITE, true)); } if (Provider.Versions.Game is EGame.GAME_UE4_LATEST or EGame.GAME_UE5_LATEST && !Provider.ProjectName.Equals("FortniteGame", StringComparison.OrdinalIgnoreCase)) // ignore fortnite globally { FLogger.Append(ELog.Warning, () => FLogger.Text($"Experimental UE version selected, likely unsuitable for '{Provider.GameDisplayName ?? Provider.ProjectName}'", Constants.WHITE, true)); } return Task.CompletedTask; } public Task VerifyOnDemandArchives() { // only local fortnite if (Provider is not DefaultFileProvider || !Provider.ProjectName.Equals("FortniteGame", StringComparison.OrdinalIgnoreCase)) return Task.CompletedTask; // scuffed but working var persistentDownloadDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "FortniteGame/Saved/PersistentDownloadDir"); var iasFileInfo = new FileInfo(Path.Combine(persistentDownloadDir, "ias", "ias.cache.0")); if (!iasFileInfo.Exists || iasFileInfo.Length == 0) return Task.CompletedTask; return Task.Run(async () => { var inst = new List(); IoStoreOnDemand.FindPropertyInstructions("Endpoint", "TocPath", inst); if (inst.Count <= 0) return; var ioStoreOnDemandPath = Path.Combine(UserSettings.Default.GameDirectory, "..\\..\\..\\Cloud", inst[0].Value.SubstringAfterLast("/").SubstringBefore("\"")); if (!File.Exists(ioStoreOnDemandPath)) return; await _apiEndpointView.EpicApi.VerifyAuth(CancellationToken.None); await Provider.RegisterVfs(new IoChunkToc(ioStoreOnDemandPath), new IoStoreOnDemandOptions { ChunkBaseUri = new Uri("https://download.epicgames.com/ias/fortnite/", UriKind.Absolute), ChunkCacheDirectory = Directory.CreateDirectory(Path.Combine(UserSettings.Default.OutputDirectory, ".data")), Authorization = new AuthenticationHeaderValue("Bearer", UserSettings.Default.LastAuthResponse.AccessToken), Timeout = TimeSpan.FromSeconds(30) }); var onDemandCount = await Provider.MountAsync(); FLogger.Append(ELog.Information, () => FLogger.Text($"{onDemandCount} on-demand archive{(onDemandCount > 1 ? "s" : "")} streamed via epicgames.com", Constants.WHITE, true)); }); } public int LocalizedResourcesCount { get; set; } public bool LocalResourcesDone { get; set; } public bool HotfixedResourcesDone { get; set; } public async Task LoadLocalizedResources() { var snapshot = LocalizedResourcesCount; await Task.WhenAll(LoadGameLocalizedResources(), LoadHotfixedLocalizedResources()).ConfigureAwait(false); LocalizedResourcesCount = Provider.Internationalization.Count; if (snapshot != LocalizedResourcesCount) { FLogger.Append(ELog.Information, () => FLogger.Text($"{LocalizedResourcesCount} localized resources loaded for '{UserSettings.Default.AssetLanguage.GetDescription()}'", Constants.WHITE, true)); Utils.Typefaces = new Typefaces(this); } } private Task LoadGameLocalizedResources() { if (LocalResourcesDone) return Task.CompletedTask; return Task.Run(() => { LocalResourcesDone = Provider.TryChangeCulture(Provider.GetLanguageCode(UserSettings.Default.AssetLanguage)); }); } private Task LoadHotfixedLocalizedResources() { if (!Provider.ProjectName.Equals("fortnitegame", StringComparison.OrdinalIgnoreCase) || HotfixedResourcesDone) return Task.CompletedTask; return Task.Run(() => { var hotfixes = ApplicationService.ApiEndpointView.CentralApi.GetHotfixes(CancellationToken.None, Provider.GetLanguageCode(UserSettings.Default.AssetLanguage)); if (hotfixes == null) return; Provider.Internationalization.Override(hotfixes); HotfixedResourcesDone = true; }); } private int _virtualPathCount { get; set; } public Task LoadVirtualPaths() { if (_virtualPathCount > 0) return Task.CompletedTask; return Task.Run(() => { _virtualPathCount = Provider.LoadVirtualPaths(UserSettings.Default.CurrentDir.UeVersion.GetVersion()); if (_virtualPathCount > 0) { FLogger.Append(ELog.Information, () => FLogger.Text($"{_virtualPathCount} virtual paths loaded", Constants.WHITE, true)); } else { FLogger.Append(ELog.Warning, () => FLogger.Text("Could not load virtual paths, plugin manifest may not exist", Constants.WHITE, true)); } }); } public void ExtractSelected(CancellationToken cancellationToken, IEnumerable assetItems) { foreach (var entry in assetItems) { Thread.Yield(); cancellationToken.ThrowIfCancellationRequested(); Extract(cancellationToken, entry, TabControl.HasNoTabs); } } private void BulkFolder(CancellationToken cancellationToken, TreeItem folder, Action action) { foreach (var entry in folder.AssetsList.Assets) { Thread.Yield(); cancellationToken.ThrowIfCancellationRequested(); try { action(entry.Asset); } catch { // ignore } } foreach (var f in folder.Folders) BulkFolder(cancellationToken, f, action); } public void ExportFolder(CancellationToken cancellationToken, TreeItem folder) { Parallel.ForEach(folder.AssetsList.Assets, entry => { cancellationToken.ThrowIfCancellationRequested(); ExportData(entry.Asset, false); }); foreach (var f in folder.Folders) ExportFolder(cancellationToken, f); } public void ExtractFolder(CancellationToken cancellationToken, TreeItem folder) => BulkFolder(cancellationToken, folder, asset => Extract(cancellationToken, asset, TabControl.HasNoTabs)); public void SaveFolder(CancellationToken cancellationToken, TreeItem folder) => BulkFolder(cancellationToken, folder, asset => Extract(cancellationToken, asset, TabControl.HasNoTabs, EBulkType.Properties | EBulkType.Auto)); public void TextureFolder(CancellationToken cancellationToken, TreeItem folder) => BulkFolder(cancellationToken, folder, asset => Extract(cancellationToken, asset, TabControl.HasNoTabs, EBulkType.Textures | EBulkType.Auto)); public void ModelFolder(CancellationToken cancellationToken, TreeItem folder) => BulkFolder(cancellationToken, folder, asset => Extract(cancellationToken, asset, TabControl.HasNoTabs, EBulkType.Meshes | EBulkType.Auto)); public void AnimationFolder(CancellationToken cancellationToken, TreeItem folder) => BulkFolder(cancellationToken, folder, asset => Extract(cancellationToken, asset, TabControl.HasNoTabs, EBulkType.Animations | EBulkType.Auto)); public void AudioFolder(CancellationToken cancellationToken, TreeItem folder) => BulkFolder(cancellationToken, folder, asset => Extract(cancellationToken, asset, TabControl.HasNoTabs, EBulkType.Audio | EBulkType.Auto)); public void Extract(CancellationToken cancellationToken, GameFile entry, bool addNewTab = false, EBulkType bulk = EBulkType.None) { ApplicationService.ApplicationView.IsAssetsExplorerVisible = false; Log.Information("User DOUBLE-CLICKED to extract '{FullPath}'", entry.Path); if (addNewTab && TabControl.CanAddTabs) TabControl.AddTab(entry); else TabControl.SelectedTab.SoftReset(entry); TabControl.SelectedTab.Highlighter = AvalonExtensions.HighlighterSelector(entry.Extension); var updateUi = !HasFlag(bulk, EBulkType.Auto); var saveProperties = HasFlag(bulk, EBulkType.Properties); var saveTextures = HasFlag(bulk, EBulkType.Textures); var saveAudio = HasFlag(bulk, EBulkType.Audio); switch (entry.Extension) { case "uasset": case "umap": { var result = Provider.GetLoadPackageResult(entry); TabControl.SelectedTab.TitleExtra = result.TabTitleExtra; if (saveProperties || updateUi) { TabControl.SelectedTab.SetDocumentText(JsonConvert.SerializeObject(result.GetDisplayData(saveProperties), Formatting.Indented), saveProperties, updateUi); if (saveProperties) break; // do not search for viewable exports if we are dealing with jsons } for (var i = result.InclusiveStart; i < result.ExclusiveEnd; i++) { if (CheckExport(cancellationToken, result.Package, i, bulk)) break; } break; } case "ini" when entry.Name.Contains("BinaryConfig"): { var ar = entry.CreateReader(); var configCache = new FConfigCacheIni(ar); TabControl.SelectedTab.Highlighter = AvalonExtensions.HighlighterSelector("json"); TabControl.SelectedTab.SetDocumentText(JsonConvert.SerializeObject(configCache, Formatting.Indented), saveProperties, updateUi); break; } case "dat" when Provider.ProjectName.Equals("Aion2", StringComparison.OrdinalIgnoreCase): { ProcessAion2DatFile(entry, updateUi, saveProperties); break; } case "upluginmanifest": case "code-workspace": case "projectstore": case "uefnproject": case "uproject": case "manifest": case "uplugin": case "archive": case "dnearchive": // Banishers: Ghosts of New Eden case "gitignore": case "LICENSE": case "playstats": // Dispatch case "template": case "stUMeta": // LIS: Double Exposure case "vmodule": case "glslfx": case "cptake": case "uparam": // Steel Hunters case "spi1d": case "verse": case "html": case "json5": case "json": case "uref": case "cube": case "usda": case "ocio": case "data" when Provider.ProjectName is "OakGame": case "ini": case "txt": case "log": case "lsd": // Days Gone case "bat": case "dat": case "cfg": case "ddr": case "ide": case "ipl": case "zon": case "xml": case "css": case "csv": case "pem": case "tps": case "tgc": // State of Decay 2 case "cpp": case "apx": case "udn": case "doc": case "lua": case "vdf": case "yml": case "js": case "po": case "md": case "h": // Uncharted Waters Origin case "crn": case "uwt": case "wvh": case "bf": case "bl": case "bm": case "br": { var data = Provider.SaveAsset(entry); using var stream = new MemoryStream(data) { Position = 0 }; using var reader = new StreamReader(stream); TabControl.SelectedTab.SetDocumentText(reader.ReadToEnd(), saveProperties, updateUi); break; } case "locmeta": { var archive = entry.CreateReader(); var metadata = new FTextLocalizationMetaDataResource(archive); TabControl.SelectedTab.SetDocumentText(JsonConvert.SerializeObject(metadata, Formatting.Indented), saveProperties, updateUi); break; } case "locres": { var archive = entry.CreateReader(); var locres = new FTextLocalizationResource(archive); TabControl.SelectedTab.SetDocumentText(JsonConvert.SerializeObject(locres, Formatting.Indented), saveProperties, updateUi); break; } case "bin" when entry.Name.Contains("AssetRegistry", StringComparison.OrdinalIgnoreCase): { var archive = entry.CreateReader(); var registry = new FAssetRegistryState(archive); TabControl.SelectedTab.SetDocumentText(JsonConvert.SerializeObject(registry, Formatting.Indented), saveProperties, updateUi); break; } case "bin" when entry.Name.Contains("GlobalShaderCache", StringComparison.OrdinalIgnoreCase): { var archive = entry.CreateReader(); var registry = new FGlobalShaderCache(archive); TabControl.SelectedTab.SetDocumentText(JsonConvert.SerializeObject(registry, Formatting.Indented), saveProperties, updateUi); break; } case "bank": { var archive = entry.CreateReader(); if (!FModProvider.TryLoadBank(archive, entry.NameWithoutExtension, out var fmodReader)) { Log.Error($"Failed to load FMOD bank {entry.Path}"); break; } TabControl.SelectedTab.SetDocumentText(JsonConvert.SerializeObject(fmodReader, Formatting.Indented, converters: [new FmodSoundBankConverter(), new StringEnumConverter()]), saveProperties, updateUi); var extractedSounds = FmodProvider.ExtractBankSounds(fmodReader); var directory = Path.GetDirectoryName(entry.Path) ?? "/FMOD/Desktop/"; foreach (var sound in extractedSounds) { SaveAndPlaySound(Path.Combine(directory, sound.Name), sound.Extension, sound.Data, saveAudio); } break; } case "bnk": case "pck": { var archive = entry.CreateReader(); var wwise = new WwiseReader(archive); TabControl.SelectedTab.SetDocumentText(JsonConvert.SerializeObject(wwise, Formatting.Indented), saveProperties, updateUi); var medias = WwiseProvider.ExtractBankSounds(wwise); foreach (var media in medias) { SaveAndPlaySound(media.OutputPath, media.Extension, media.Data, saveAudio); } break; } case "awb": { var archive = entry.CreateReader(); var awbReader = new AwbReader(archive); TabControl.SelectedTab.SetDocumentText(JsonConvert.SerializeObject(awbReader, Formatting.Indented), saveProperties, updateUi); var directory = Path.GetDirectoryName(archive.Name) ?? "/Criware/"; var extractedSounds = CriWareProvider.ExtractCriWareSounds(awbReader, archive.Name); foreach (var sound in extractedSounds) { SaveAndPlaySound(Path.Combine(directory, sound.Name), sound.Extension, sound.Data, saveAudio); } break; } case "acb": { var archive = entry.CreateReader(); var acbReader = new AcbReader(archive); TabControl.SelectedTab.SetDocumentText(JsonConvert.SerializeObject(acbReader, Formatting.Indented), saveProperties, updateUi); var directory = Path.GetDirectoryName(archive.Name) ?? "/Criware/"; var extractedSounds = CriWareProvider.ExtractCriWareSounds(acbReader, archive.Name); foreach (var sound in extractedSounds) { SaveAndPlaySound(Path.Combine(directory, sound.Name), sound.Extension, sound.Data, saveAudio); } break; } case "xvag": case "flac": case "at9": case "wem": case "wav": case "WAV": case "ogg": // todo: CSCore.MediaFoundation.MediaFoundationException The byte stream type of the given URL is unsupported. case "aif": { var data = Provider.SaveAsset(entry); SaveAndPlaySound(entry.PathWithoutExtension, entry.Extension, data, saveAudio); break; } case "udic": { var archive = entry.CreateReader(); var header = new FOodleDictionaryArchive(archive).Header; TabControl.SelectedTab.SetDocumentText(JsonConvert.SerializeObject(header, Formatting.Indented), saveProperties, updateUi); break; } case "png": case "jpg": case "bmp": { var data = Provider.SaveAsset(entry); using var stream = new MemoryStream(data) { Position = 0 }; TabControl.SelectedTab.AddImage(entry.NameWithoutExtension, false, SKBitmap.Decode(stream), saveTextures, updateUi); break; } case "svg": { var data = Provider.SaveAsset(entry); using var stream = new MemoryStream(data) { Position = 0 }; var svg = new SKSvg(); svg.Load(stream); int size = 512; var bitmap = new SKBitmap(size, size); using var canvas = new SKCanvas(bitmap); canvas.Clear(SKColors.Transparent); if (svg.Picture == null) break; var bounds = svg.Picture.CullRect; float scale = Math.Min(size / bounds.Width, size / bounds.Height); canvas.Scale(scale); canvas.Translate(-bounds.Left, -bounds.Top); canvas.DrawPicture(svg.Picture); TabControl.SelectedTab.AddImage(entry.NameWithoutExtension, false, bitmap, saveTextures, updateUi); break; } case "ufont": case "otf": case "ttf": FLogger.Append(ELog.Warning, () => FLogger.Text($"Export '{entry.Name}' raw data and change its extension if you want it to be an installable font file", Constants.WHITE, true)); break; case "ushaderbytecode": case "ushadercode": { var archive = entry.CreateReader(); var ar = new FShaderCodeArchive(archive); TabControl.SelectedTab.SetDocumentText(JsonConvert.SerializeObject(ar, Formatting.Indented), saveProperties, updateUi); break; } case "upipelinecache": { var archive = entry.CreateReader(); var ar = new FPipelineCacheFile(archive); TabControl.SelectedTab.SetDocumentText(JsonConvert.SerializeObject(ar, Formatting.Indented), saveProperties, updateUi); break; } case "stinfo": { var archive = entry.CreateReader(); var ar = new FShaderTypeHashes(archive); TabControl.SelectedTab.SetDocumentText(JsonConvert.SerializeObject(ar, Formatting.Indented), saveProperties, updateUi); break; } case "res": // just skip case "luac": // compiled lua case "bytes": // wuthering waves break; default: { Log.Warning($"The package '{entry.Name}' is of an unknown type."); if (!UnknownExtensions.Contains(entry.Extension)) { UnknownExtensions.Add(entry.Extension); FLogger.Append(ELog.Warning, () => FLogger.Text($"There are some packages with an unknown type {entry.Extension}. Check Log file for a full list.", Constants.WHITE, true)); } break; } } void ProcessAion2DatFile(GameFile entry, bool updateUi, bool saveProperties) { TabControl.SelectedTab.Highlighter = AvalonExtensions.HighlighterSelector("json"); if (entry.NameWithoutExtension.EndsWith("_MapEvent")) { var data = Provider.SaveAsset(entry); FAion2DatFileArchive.DecryptData(data); using var stream = new MemoryStream(data) { Position = 0 }; using var reader = new StreamReader(stream); TabControl.SelectedTab.SetDocumentText(reader.ReadToEnd(), saveProperties, updateUi); } else if (entry.NameWithoutExtension.Equals("L10NString")) { var l10nData = new FAion2L10NFile(entry); TabControl.SelectedTab.SetDocumentText(JsonConvert.SerializeObject(l10nData, Formatting.Indented), saveProperties, updateUi); } else { FAion2DataFile datfile = entry.NameWithoutExtension switch { "MapDataHierarchy" => new FAion2MapHierarchyFile(entry), "MapData" => new FAion2MapDataFile(entry, Provider), _ when entry.Directory.EndsWith("Data/WorldMap", StringComparison.OrdinalIgnoreCase) => new FAion2MapDataFile(entry, Provider), _ => new FAion2DataTableFile(entry, Provider) }; TabControl.SelectedTab.SetDocumentText(JsonConvert.SerializeObject(datfile, Formatting.Indented), saveProperties, updateUi); } } } public void ExtractAndScroll(CancellationToken cancellationToken, string fullPath, string objectName, string parentExportType) { Log.Information("User CTRL-CLICKED to extract '{FullPath}'", fullPath); var entry = Provider[fullPath]; TabControl.AddTab(entry, parentExportType); TabControl.SelectedTab.ScrollTrigger = objectName; var result = Provider.GetLoadPackageResult(entry, objectName); TabControl.SelectedTab.TitleExtra = result.TabTitleExtra; TabControl.SelectedTab.Highlighter = AvalonExtensions.HighlighterSelector(""); // json TabControl.SelectedTab.SetDocumentText(JsonConvert.SerializeObject(result.GetDisplayData(), Formatting.Indented), false, false); for (var i = result.InclusiveStart; i < result.ExclusiveEnd; i++) { if (CheckExport(cancellationToken, result.Package, i)) break; } } private bool CheckExport(CancellationToken cancellationToken, IPackage pkg, int index, EBulkType bulk = EBulkType.None) // return true once you want to stop searching for exports { var isNone = bulk == EBulkType.None; var updateUi = !HasFlag(bulk, EBulkType.Auto); var saveTextures = HasFlag(bulk, EBulkType.Textures); var saveAudio = HasFlag(bulk, EBulkType.Audio); var pointer = new FPackageIndex(pkg, index + 1).ResolvedObject; if (pointer?.Object is null) return false; var dummy = ((AbstractUePackage) pkg).ConstructObject(pointer.Class?.Object?.Value as UStruct, pkg); switch (dummy) { case UVerseDigest when isNone && pointer.Object.Value is UVerseDigest verseDigest: { if (!TabControl.CanAddTabs) return false; TabControl.AddTab($"{verseDigest.ProjectName}.verse"); TabControl.SelectedTab.Highlighter = AvalonExtensions.HighlighterSelector("verse"); TabControl.SelectedTab.SetDocumentText(verseDigest.ReadableCode, false, false); return true; } case UTexture when (isNone || saveTextures) && pointer.Object.Value is UTexture texture: { TabControl.SelectedTab.AddImage(texture, saveTextures, updateUi); return false; } case USvgAsset when (isNone || saveTextures) && pointer.Object.Value is USvgAsset svgasset: { const int size = 512; var data = svgasset.GetOrDefault("SvgData"); var sourceFile = svgasset.GetOrDefault("SourceFile"); using var stream = new MemoryStream(data) { Position = 0 }; var svg = new SKSvg(); svg.Load(stream); if (svg.Picture == null) return false; var b = svg.Picture.CullRect; float s = Math.Min(size / b.Width, size / b.Height); var bitmap = new SKBitmap(size, size); using var canvas = new SKCanvas(bitmap); using var paint = new SKPaint { IsAntialias = true, FilterQuality = SKFilterQuality.Medium }; canvas.Scale(s); canvas.Translate(-b.Left, -b.Top); canvas.DrawPicture(svg.Picture, paint); if (saveTextures) { var fileName = sourceFile.SubstringAfterLast('/'); var path = Path.Combine(UserSettings.Default.TextureDirectory, UserSettings.Default.KeepDirectoryStructure ? TabControl.SelectedTab.Entry.Directory : "", fileName!).Replace('\\', '/'); Directory.CreateDirectory(path.SubstringBeforeLast('/')); using var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read); fs.Write(data, 0, data.Length); if (File.Exists(path)) { Log.Information("{FileName} successfully saved", fileName); if (updateUi) { FLogger.Append(ELog.Information, () => { FLogger.Text("Successfully saved ", Constants.WHITE); FLogger.Link(fileName, path, true); }); } } else { Log.Error("{FileName} could not be saved", fileName); if (updateUi) FLogger.Append(ELog.Error, () => FLogger.Text($"Could not save '{fileName}'", Constants.WHITE, true)); } } TabControl.SelectedTab.AddImage(sourceFile.SubstringAfterLast('/'), false, bitmap, false, updateUi); return false; } // The Dark Pictures Anthology: House of Ashes case UExternalSource when (isNone || saveAudio) && pointer.Object.Value is UExternalSource externalSource: { var audioName = Path.GetFileNameWithoutExtension(externalSource.ExternalSourcePath); SaveAndPlaySound(audioName, "wem", externalSource.Data?.WemFile ?? [], saveAudio); return false; } case UAkAudioEvent when (isNone || saveAudio) && pointer.Object.Value is UAkAudioEvent audioEvent: { var extractedSounds = WwiseProvider.ExtractAudioEventSounds(audioEvent); foreach (var sound in extractedSounds) { SaveAndPlaySound(sound.OutputPath, sound.Extension, sound.Data, saveAudio); } return false; } case UFMODEvent when (isNone || saveAudio) && pointer.Object.Value is UFMODEvent fmodEvent: { var extractedSounds = FmodProvider.ExtractEventSounds(fmodEvent); var directory = Path.GetDirectoryName(fmodEvent.Owner?.Name) ?? "/FMOD/Desktop/"; foreach (var sound in extractedSounds) { SaveAndPlaySound(Path.Combine(directory, sound.Name), sound.Extension, sound.Data, saveAudio); } return false; } case UFMODBank when (isNone || saveAudio) && pointer.Object.Value is UFMODBank fmodBank: { var extractedSounds = FmodProvider.ExtractBankSounds(fmodBank); var directory = Path.GetDirectoryName(fmodBank.Owner?.Name) ?? "/FMOD/Desktop/"; foreach (var sound in extractedSounds) { SaveAndPlaySound(Path.Combine(directory, sound.Name), sound.Extension, sound.Data, saveAudio); } return false; } case USoundAtomCueSheet or UAtomCueSheet or USoundAtomCue or UAtomWaveBank when (isNone || saveAudio) && pointer.Object.Value is UObject atomObject: { var extractedSounds = atomObject switch { USoundAtomCueSheet cueSheet => CriWareProvider.ExtractCriWareSounds(cueSheet), UAtomCueSheet cueSheet => CriWareProvider.ExtractCriWareSounds(cueSheet), USoundAtomCue cue => CriWareProvider.ExtractCriWareSounds(cue), UAtomWaveBank awb => CriWareProvider.ExtractCriWareSounds(awb), _ => [] }; var directory = Path.GetDirectoryName(atomObject.Owner?.Name) ?? "/Criware/"; directory = Path.GetDirectoryName(atomObject.Owner.Provider.FixPath(directory)); foreach (var sound in extractedSounds) { SaveAndPlaySound(Path.Combine(directory, sound.Name).Replace("\\", "/"), sound.Extension, sound.Data, saveAudio); } return false; } case UAkMediaAssetData when isNone || saveAudio: case USoundWave when isNone || saveAudio: { var shouldDecompress = UserSettings.Default.CompressedAudioMode == ECompressedAudio.PlayDecompressed; pointer.Object.Value.Decode(shouldDecompress, out var audioFormat, out var data); var hasAf = !string.IsNullOrEmpty(audioFormat); if (data == null || !hasAf) { if (hasAf) FLogger.Append(ELog.Warning, () => FLogger.Text($"Unsupported audio format '{audioFormat}'", Constants.WHITE, true)); return false; } SaveAndPlaySound(TabControl.SelectedTab.Entry.PathWithoutExtension.Replace('\\', '/'), audioFormat, data, saveAudio); return false; } case UWorld when isNone && UserSettings.Default.PreviewWorlds: case UBlueprintGeneratedClass when isNone && UserSettings.Default.PreviewWorlds && TabControl.SelectedTab.ParentExportType switch { "JunoBuildInstructionsItemDefinition" => true, "JunoBuildingSetAccountItemDefinition" => true, "JunoBuildingPropAccountItemDefinition" => true, _ => false }: case UPaperSprite when isNone && UserSettings.Default.PreviewMaterials: case UStaticMesh when isNone && UserSettings.Default.PreviewStaticMeshes: case USkeletalMesh when isNone && UserSettings.Default.PreviewSkeletalMeshes: case USkeleton when isNone && UserSettings.Default.SaveSkeletonAsMesh: case UMaterialInstance when isNone && UserSettings.Default.PreviewMaterials && !ModelIsOverwritingMaterial && !(Provider.ProjectName.Equals("FortniteGame", StringComparison.OrdinalIgnoreCase) && (pkg.Name.Contains("/MI_OfferImages/", StringComparison.OrdinalIgnoreCase) || pkg.Name.Contains("/RenderSwitch_Materials/", StringComparison.OrdinalIgnoreCase) || pkg.Name.Contains("/MI_BPTile/", StringComparison.OrdinalIgnoreCase))): { if (SnooperViewer.TryLoadExport(cancellationToken, dummy, pointer.Object)) SnooperViewer.Run(); return true; } case UMaterialInstance when isNone && ModelIsOverwritingMaterial && pointer.Object.Value is UMaterialInstance m: { SnooperViewer.Renderer.Swap(m); SnooperViewer.Run(); return true; } case UAnimSequenceBase when isNone && UserSettings.Default.PreviewAnimations || ModelIsWaitingAnimation: { // animate all animations using their specified skeleton or when we explicitly asked for a loaded model to be animated (ignoring whether we wanted to preview animations) SnooperViewer.Renderer.Animate(pointer.Object.Value); SnooperViewer.Run(); return true; } case UStaticMesh when HasFlag(bulk, EBulkType.Meshes): case USkeletalMesh when HasFlag(bulk, EBulkType.Meshes): case USkeleton when UserSettings.Default.SaveSkeletonAsMesh && HasFlag(bulk, EBulkType.Meshes): // case UMaterialInstance when HasFlag(bulk, EBulkType.Materials): // read the fucking json case UAnimSequenceBase when HasFlag(bulk, EBulkType.Animations): { SaveExport(pointer.Object.Value, updateUi); return true; } default: { if (!isNone && !saveTextures) return false; using var cPackage = new CreatorPackage(pkg.Name, dummy.ExportType, pointer.Object, UserSettings.Default.CosmeticStyle); if (!cPackage.TryConstructCreator(out var creator)) return false; creator.ParseForInfo(); TabControl.SelectedTab.AddImage(pointer.Object.Value.Name, false, creator.Draw(), saveTextures, updateUi); return true; } } } public void ShowMetadata(GameFile entry) { ApplicationService.ApplicationView.IsAssetsExplorerVisible = false; var package = Provider.LoadPackage(entry); if (TabControl.CanAddTabs) TabControl.AddTab(entry); else TabControl.SelectedTab.SoftReset(entry); TabControl.SelectedTab.TitleExtra = "Metadata"; TabControl.SelectedTab.Highlighter = AvalonExtensions.HighlighterSelector(""); TabControl.SelectedTab.SetDocumentText(JsonConvert.SerializeObject(package, Formatting.Indented), false, false); } public void FindReferences(GameFile entry) { var refs = Provider.ScanForPackageRefs(entry); Application.Current.Dispatcher.Invoke(delegate { var refView = Helper.GetWindow("Search For Packages", () => new SearchView().Show()); refView.ChangeCollection(ESearchViewTab.RefView, refs, entry); refView.FocusTab(ESearchViewTab.RefView); }); } public void Decompile(GameFile entry) { ApplicationService.ApplicationView.IsAssetsExplorerVisible = false; if (TabControl.CanAddTabs) TabControl.AddTab(entry); else TabControl.SelectedTab.SoftReset(entry); TabControl.SelectedTab.TitleExtra = "Decompiled"; TabControl.SelectedTab.Highlighter = AvalonExtensions.HighlighterSelector("cpp"); UClassCookedMetaData cookedMetaData = null; try { var editorPkg = Provider.LoadPackage(entry.Path.Replace(".uasset", ".o.uasset")); cookedMetaData = editorPkg.GetExport("CookedClassMetaData"); } catch { // ignored } var cppList = new List(); var pkg = Provider.LoadPackage(entry); for (var i = 0; i < pkg.ExportMapLength; i++) { var pointer = new FPackageIndex(pkg, i + 1).ResolvedObject; if (pointer?.Object is null && pointer.Class?.Object?.Value is null) continue; var dummy = ((AbstractUePackage) pkg).ConstructObject(pointer.Class?.Object?.Value as UStruct, pkg); if (dummy is not UClass || pointer.Object.Value is not UClass blueprint) continue; cppList.Add(blueprint.DecompileBlueprintToPseudo(cookedMetaData)); } var cpp = cppList.Count > 1 ? string.Join("\n\n", cppList) : cppList.FirstOrDefault() ?? string.Empty; if (entry.Path.Contains("_Verse.uasset")) { cpp = Regex.Replace(cpp, "__verse_0x[a-fA-F0-9]{8}_", ""); // UnmangleCasedName } cpp = Regex.Replace(cpp, @"CallFunc_([A-Za-z0-9_]+)_ReturnValue", "$1"); TabControl.SelectedTab.SetDocumentText(cpp, false, false); } private void SaveAndPlaySound(string fullPath, string ext, byte[] data, bool isBulk) { if (fullPath.StartsWith('/')) fullPath = fullPath[1..]; var savedAudioPath = Path.Combine(UserSettings.Default.AudioDirectory, UserSettings.Default.KeepDirectoryStructure ? fullPath : fullPath.SubstringAfterLast('/')).Replace('\\', '/') + $".{ext.ToLowerInvariant()}"; if (isBulk) { Directory.CreateDirectory(savedAudioPath.SubstringBeforeLast('/')); using var stream = new FileStream(savedAudioPath, FileMode.Create, FileAccess.Write); using var writer = new BinaryWriter(stream); writer.Write(data); writer.Flush(); return; } // TODO // since we are currently in a thread, the audio player's lifetime (memory-wise) will keep the current thread up and running until fmodel itself closes // the solution would be to kill the current thread at this line and then open the audio player without "Application.Current.Dispatcher.Invoke" // but the ThreadWorkerViewModel is an idiot and doesn't understand we want to kill the current thread inside the current thread and continue the code Application.Current.Dispatcher.Invoke(delegate { var audioPlayer = Helper.GetWindow("Audio Player", () => new AudioPlayer().Show()); audioPlayer.Load(data, savedAudioPath); }); } private void SaveExport(UObject export, bool updateUi = true) { var toSave = new Exporter(export, UserSettings.Default.ExportOptions); var toSaveDirectory = new DirectoryInfo(UserSettings.Default.ModelDirectory); if (toSave.TryWriteToDir(toSaveDirectory, out var label, out var savedFilePath)) { Log.Information("Successfully saved {FilePath}", savedFilePath); if (updateUi) { FLogger.Append(ELog.Information, () => { FLogger.Text("Successfully saved ", Constants.WHITE); FLogger.Link(label, savedFilePath, true); }); } } else { Log.Error("{FileName} could not be saved", export.Name); FLogger.Append(ELog.Error, () => FLogger.Text($"Could not save '{export.Name}'", Constants.WHITE, true)); } } private readonly object _rawData = new (); public void ExportData(GameFile entry, bool updateUi = true) { if (Provider.TrySavePackage(entry, out var assets)) { string path = UserSettings.Default.RawDataDirectory; Parallel.ForEach(assets, kvp => { lock (_rawData) { path = Path.Combine(UserSettings.Default.RawDataDirectory, UserSettings.Default.KeepDirectoryStructure ? kvp.Key : kvp.Key.SubstringAfterLast('/')).Replace('\\', '/'); Directory.CreateDirectory(path.SubstringBeforeLast('/')); File.WriteAllBytes(path, kvp.Value); } }); Log.Information("{FileName} successfully exported", entry.Name); if (updateUi) { FLogger.Append(ELog.Information, () => { FLogger.Text("Successfully exported ", Constants.WHITE); FLogger.Link(entry.Name, path, true); }); } } else { Log.Error("{FileName} could not be exported", entry.Name); if (updateUi) FLogger.Append(ELog.Error, () => FLogger.Text($"Could not export '{entry.Name}'", Constants.WHITE, true)); } } private static bool HasFlag(EBulkType a, EBulkType b) { return (a & b) == b; } }