diff --git a/CUE4Parse b/CUE4Parse index c43d6707c..76a4c53af 160000 --- a/CUE4Parse +++ b/CUE4Parse @@ -1 +1 @@ -Subproject commit c43d6707cbde0305e2970d7c527e2ccd26f393c7 +Subproject commit 76a4c53afe1f26c24a84973f5228a6ef475de8b7 diff --git a/FModel/App.xaml b/FModel/App.xaml index 603f409d6..55a4af7de 100644 --- a/FModel/App.xaml +++ b/FModel/App.xaml @@ -20,7 +20,7 @@ - + #206BD4 diff --git a/FModel/App.xaml.cs b/FModel/App.xaml.cs index 31da19b97..2dd04be64 100644 --- a/FModel/App.xaml.cs +++ b/FModel/App.xaml.cs @@ -11,7 +11,9 @@ using FModel.Framework; using FModel.Services; using FModel.Settings; +using FModel.Views.Snooper; using Newtonsoft.Json; +using Serilog.Events; using Serilog.Sinks.SystemConsole.Themes; using MessageBox = AdonisUI.Controls.MessageBox; using MessageBoxImage = AdonisUI.Controls.MessageBoxImage; @@ -110,25 +112,39 @@ protected override void OnStartup(StartupEventArgs e) Directory.CreateDirectory(Path.Combine(UserSettings.Default.OutputDirectory, "Logs")); Directory.CreateDirectory(Path.Combine(UserSettings.Default.OutputDirectory, ".data")); - const string template = "{Timestamp:yyyy-MM-dd HH:mm:ss} [{Level:u3}] {Enriched}: {Message:lj}{NewLine}{Exception}"; +#if DEBUG + var filePath = Path.Combine(UserSettings.Default.OutputDirectory, "Logs", $"FModel-Debug-Log-{DateTime.Now:yyyy-MM-dd}.log"); +#else + var filePath = Path.Combine(UserSettings.Default.OutputDirectory, "Logs", $"FModel-Log-{DateTime.Now:yyyy-MM-dd}.log"); +#endif + const string template1 = "{Timestamp:yyyy-MM-dd HH:mm:ss} [{Level:u3}] {Enriched}: {Message:lj}{NewLine}{Exception}"; + const string template2 = "{Timestamp:yyyy-MM-dd HH:mm:ss} [{Level:u3}] [{ClassName}] {ObjectPath}: {Message:lj}{NewLine}{Exception}"; Log.Logger = new LoggerConfiguration() #if DEBUG .Enrich.With() .MinimumLevel.Verbose() - .WriteTo.Console(outputTemplate: template, theme: AnsiConsoleTheme.Literate) - .WriteTo.File(outputTemplate: template, - path: Path.Combine(UserSettings.Default.OutputDirectory, "Logs", $"FModel-Debug-Log-{DateTime.Now:yyyy-MM-dd}.log")) #else .Enrich.With() - .WriteTo.File(outputTemplate: template, - path: Path.Combine(UserSettings.Default.OutputDirectory, "Logs", $"FModel-Log-{DateTime.Now:yyyy-MM-dd}.log")) #endif + .WriteTo.Logger(lc => lc + .Filter.ByExcluding(IsConversionLibrary) + .WriteTo.Console(outputTemplate: template1, theme: AnsiConsoleTheme.Literate) + .WriteTo.File(outputTemplate: template1, path: filePath)) + .WriteTo.Logger(lc => lc + .Filter.ByIncludingOnly(IsConversionLibrary) + .WriteTo.Console(outputTemplate: template2, theme: AnsiConsoleTheme.Literate) + .WriteTo.File(outputTemplate: template2, path: filePath)) + .MinimumLevel.Override("CUE4Parse_Conversion", LogEventLevel.Verbose).WriteTo.Sink(ImGuiSink.Instance) .CreateLogger(); Log.Information("Version {Version} ({CommitId})", Constants.APP_VERSION, Constants.APP_COMMIT_ID); Log.Information("{OS}", GetOperatingSystemProductName()); Log.Information("{RuntimeVer}", RuntimeInformation.FrameworkDescription); Log.Information("Culture {SysLang}", CultureInfo.CurrentCulture); + + static bool IsConversionLibrary(LogEvent e) => + e.Properties.TryGetValue("SourceContext", out var sc) && + sc.ToString().Contains("CUE4Parse_Conversion"); } private void AppExit(object sender, ExitEventArgs e) diff --git a/FModel/Creator/Bases/FN/BaseIcon.cs b/FModel/Creator/Bases/FN/BaseIcon.cs index edf73fce2..cd57824c9 100644 --- a/FModel/Creator/Bases/FN/BaseIcon.cs +++ b/FModel/Creator/Bases/FN/BaseIcon.cs @@ -56,8 +56,13 @@ public void ParseForReward(bool isUsingDisplayAsset) Preview = Utils.GetBitmap(otherPreview); else if (Object.TryGetValue(out UMaterialInstanceConstant materialInstancePreview, "EventCalloutImage")) Preview = Utils.GetBitmap(materialInstancePreview); - else if (Object.TryGetValue(out FStructFallback brush, "IconBrush") && brush.TryGetValue(out UTexture2D res, "ResourceObject")) + else if (Object.TryGetValue(out FStructFallback brush, "IconBrush", "BuildingSymbolNormal") && brush.TryGetValue(out UTexture2D res, "ResourceObject")) Preview = Utils.GetBitmap(res); + else if (Object.TryGetValue(out FStructFallback mission, "MissionIcons", "PopupWidgetData")) + { + if (mission.TryGetValue(out FStructFallback brushsize, "Brush_XL", "Brush_L", "Brush_M", "Brush_S", "Brush_XS", "Brush_XXS", "AvailableIcon", "UnavailableIcon") && brushsize.TryGetValue(out UTexture2D res2, "ResourceObject")) + Preview = Utils.GetBitmap(res2); + } } // text diff --git a/FModel/Creator/CreatorPackage.cs b/FModel/Creator/CreatorPackage.cs index 4369fdd4c..82f814270 100644 --- a/FModel/Creator/CreatorPackage.cs +++ b/FModel/Creator/CreatorPackage.cs @@ -51,6 +51,11 @@ public bool TryConstructCreator([MaybeNullWhen(false)] out UCreator creator) case "CosmeticShoesItemDefinition": case "CosmeticCompanionItemDefinition": case "CosmeticCompanionReactFXItemDefinition": + case "MagpieEntitlementRewardDefinition": + case "FortDeferredItemGrantDefinition": + case "BattleLabDeviceItemDefinition": + case "PiggybackDanceItemDefinition": + case "MyTownBuildingDefinitionData": case "AthenaPickaxeItemDefinition": case "AthenaGadgetItemDefinition": case "AthenaGliderItemDefinition": @@ -64,6 +69,7 @@ public bool TryConstructCreator([MaybeNullWhen(false)] out UCreator creator) case "FortTokenType": case "FortAbilityKit": case "FortWorkerType": + case "FortMissionInfo": case "RewardGraphToken": case "JunoKnowledgeBundle": case "FortBannerTokenType": diff --git a/FModel/Enums.cs b/FModel/Enums.cs index dfbeeecec..ff8bc027b 100644 --- a/FModel/Enums.cs +++ b/FModel/Enums.cs @@ -109,6 +109,7 @@ public enum EBulkType Audio = 1 << 5, Code = 1 << 6, Raw = 1 << 7, + Worlds = 1 << 8, } public enum EAssetCategory : uint @@ -171,3 +172,9 @@ public enum EUnluacMode Decompile, Disassemble, } + +public enum EExplorerViewMode +{ + Grid, + List +} diff --git a/FModel/Extensions/LogEventExtensions.cs b/FModel/Extensions/LogEventExtensions.cs new file mode 100644 index 000000000..620d67980 --- /dev/null +++ b/FModel/Extensions/LogEventExtensions.cs @@ -0,0 +1,11 @@ +using Serilog.Events; + +namespace FModel.Extensions; + +public static class LogEventExtensions +{ + public static string GetContext(this LogEvent log, string propertyName) + { + return log.Properties.TryGetValue(propertyName, out var value) ? value.ToString().Trim('"') : string.Empty; + } +} diff --git a/FModel/FModel.csproj b/FModel/FModel.csproj index af9923094..1bfc1c33e 100644 --- a/FModel/FModel.csproj +++ b/FModel/FModel.csproj @@ -121,6 +121,9 @@ + + + @@ -149,6 +152,9 @@ + + + diff --git a/FModel/Framework/ImGuiController.cs b/FModel/Framework/ImGuiController.cs index b62fde212..0768de97f 100644 --- a/FModel/Framework/ImGuiController.cs +++ b/FModel/Framework/ImGuiController.cs @@ -66,18 +66,69 @@ public ImGuiController(int width, int height) var iniFileNamePtr = Marshal.StringToCoTaskMemUTF8(Path.Combine(UserSettings.Default.OutputDirectory, ".data", "imgui.ini")); io.NativePtr->IniFilename = (byte*)iniFileNamePtr; } - - // If not found, Fallback to default ImGui Font - var normalPath = @"C:\Windows\Fonts\segoeui.ttf"; - var boldPath = @"C:\Windows\Fonts\segoeuib.ttf"; - var semiBoldPath = @"C:\Windows\Fonts\seguisb.ttf"; - - if (File.Exists(normalPath)) - FontNormal = io.Fonts.AddFontFromFileTTF(normalPath, 16 * DpiScale); - if (File.Exists(boldPath)) - FontBold = io.Fonts.AddFontFromFileTTF(boldPath, 16 * DpiScale); - if (File.Exists(semiBoldPath)) - FontSemiBold = io.Fonts.AddFontFromFileTTF(semiBoldPath, 16 * DpiScale); + + var assembly = System.Reflection.Assembly.GetExecutingAssembly(); + var assemblyName = assembly.GetName().Name; + byte[] LoadFont(string name) + { + using var stream = assembly.GetManifestResourceStream($"{assemblyName}.Resources.{name}") + ?? throw new FileNotFoundException($"Embedded font '{name}' not found."); + using var ms = new MemoryStream(); + stream.CopyTo(ms); + return ms.ToArray(); + } + + var faSolid = LoadFont("fa-solid-900.otf"); + var faRegular = LoadFont("fa-regular-400.otf"); + var faBrands = LoadFont("fa-brands-400.otf"); + + unsafe + { + // FA5 icons live in E000–F8FF (brands/regular/solid) + var iconRanges = stackalloc ushort[] { 0xe000, 0xf8ff, 0 }; + + var cfg = ImGuiNative.ImFontConfig_ImFontConfig(); + cfg->MergeMode = 1; + cfg->PixelSnapH = 1; + cfg->GlyphMinAdvanceX = 16f; + // FontDataOwnedByAtlas = 0 because we manage the GCHandle lifetime ourselves + cfg->FontDataOwnedByAtlas = 0; + + void MergeFontAwesome(byte[] solid, byte[] regular, byte[] brands) + { + fixed (byte* pSolid = solid) + fixed (byte* pRegular = regular) + fixed (byte* pBrands = brands) + { + io.Fonts.AddFontFromMemoryTTF((IntPtr)pSolid, solid.Length, 14, (IntPtr)cfg, (IntPtr)iconRanges); + io.Fonts.AddFontFromMemoryTTF((IntPtr)pRegular, regular.Length, 14, (IntPtr)cfg, (IntPtr)iconRanges); + io.Fonts.AddFontFromMemoryTTF((IntPtr)pBrands, brands.Length, 14, (IntPtr)cfg, (IntPtr)iconRanges); + } + } + + // If not found, Fallback to default ImGui Font + var normalPath = @"C:\Windows\Fonts\segoeui.ttf"; + var boldPath = @"C:\Windows\Fonts\segoeuib.ttf"; + var semiBoldPath = @"C:\Windows\Fonts\seguisb.ttf"; + + if (File.Exists(normalPath)) + { + FontNormal = io.Fonts.AddFontFromFileTTF(normalPath, 16 * DpiScale); + MergeFontAwesome(faSolid, faRegular, faBrands); + } + if (File.Exists(boldPath)) + { + FontBold = io.Fonts.AddFontFromFileTTF(boldPath, 16 * DpiScale); + MergeFontAwesome(faSolid, faRegular, faBrands); + } + if (File.Exists(semiBoldPath)) + { + FontSemiBold = io.Fonts.AddFontFromFileTTF(semiBoldPath, 16 * DpiScale); + MergeFontAwesome(faSolid, faRegular, faBrands); + } + + ImGuiNative.ImFontConfig_destroy(cfg); + } io.Fonts.AddFontDefault(); io.Fonts.Build(); // Build font atlas diff --git a/FModel/Framework/ViewModel.cs b/FModel/Framework/ViewModel.cs index dbaf1c031..6c4eecabc 100644 --- a/FModel/Framework/ViewModel.cs +++ b/FModel/Framework/ViewModel.cs @@ -19,12 +19,12 @@ public string this[string propertyName] if (string.IsNullOrEmpty(propertyName)) return Error; - return _validationErrors.ContainsKey(propertyName) ? string.Join(Environment.NewLine, _validationErrors[propertyName]) : string.Empty; + return _validationErrors.TryGetValue(propertyName, out IList validationError) ? string.Join(Environment.NewLine, validationError) : string.Empty; } } [JsonIgnore] public string Error => string.Join(Environment.NewLine, GetAllErrors()); - [JsonIgnore] public bool HasErrors => _validationErrors.Any(); + [JsonIgnore] public virtual bool HasErrors => _validationErrors.Count != 0; public IEnumerable GetErrors(string propertyName) { @@ -49,8 +49,7 @@ public void AddValidationError(string propertyName, string errorMessage) public void ClearValidationErrors(string propertyName) { - if (_validationErrors.ContainsKey(propertyName)) - _validationErrors.Remove(propertyName); + _validationErrors.Remove(propertyName); } public event PropertyChangedEventHandler PropertyChanged; @@ -72,4 +71,4 @@ protected virtual bool SetProperty(ref T storage, T value, [CallerMemberName] RaisePropertyChanged(propertyName); return true; } -} \ No newline at end of file +} diff --git a/FModel/Helper.cs b/FModel/Helper.cs index 05f1530ba..62d7c6e03 100644 --- a/FModel/Helper.cs +++ b/FModel/Helper.cs @@ -39,7 +39,7 @@ public static void OpenWindow(string windowName, Action action) where T : Win else { var w = GetOpenedWindow(windowName); - if (windowName == "Search For Packages") w.WindowState = WindowState.Normal; + if (w.WindowState == WindowState.Minimized) w.WindowState = WindowState.Normal; w.Focus(); } } diff --git a/FModel/MainWindow.xaml b/FModel/MainWindow.xaml index 46d2ddb52..10bb6d219 100644 --- a/FModel/MainWindow.xaml +++ b/FModel/MainWindow.xaml @@ -2,6 +2,7 @@ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:FModel" + xmlns:viewModels="clr-namespace:FModel.ViewModels" xmlns:controls="clr-namespace:FModel.Views.Resources.Controls" xmlns:inputs="clr-namespace:FModel.Views.Resources.Controls.Inputs" xmlns:converters="clr-namespace:FModel.Views.Resources.Converters" @@ -24,6 +25,28 @@ + + + + + + + + + + + + + + + + + + + @@ -588,7 +691,9 @@ Command="{Binding MenuCommand}" CommandParameter="ToolBox_Open_Output_Directory" Focusable="False"> - + + + @@ -698,23 +803,82 @@ - + - - - - - - + + - - + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + diff --git a/FModel/MainWindow.xaml.cs b/FModel/MainWindow.xaml.cs index fbed15d40..25e828934 100644 --- a/FModel/MainWindow.xaml.cs +++ b/FModel/MainWindow.xaml.cs @@ -54,8 +54,10 @@ public MainWindow() InitializeComponent(); AssetsExplorer.ItemContainerGenerator.StatusChanged += ItemContainerGenerator_StatusChanged; + AssetsListExplorer.ItemContainerGenerator.StatusChanged += ItemContainerGenerator_StatusChanged; AssetsListName.ItemContainerGenerator.StatusChanged += ItemContainerGenerator_StatusChanged; AssetsExplorer.SelectionChanged += (_, e) => SyncSelection(AssetsListName, e); + AssetsListExplorer.SelectionChanged += (_, e) => SyncSelection(AssetsListName, e); AssetsListName.SelectionChanged += (_, e) => SyncSelection(AssetsExplorer, e); FLogger.Logger = LogRtbName; @@ -376,4 +378,15 @@ private async void OnFoldersPreviewKeyDown(object sender, KeyEventArgs e) childFolder.IsExpanded = true; childFolder.IsSelected = true; } + + private CustomPopupPlacement[] OnQueueToastCustomPopupPlacement(Size popupSize, Size targetSize, Point offset) + { + return + [ + new CustomPopupPlacement( + new Point((targetSize.Width - popupSize.Width) / 2, -popupSize.Height - 10), + PopupPrimaryAxis.Horizontal + ) + ]; + } } diff --git a/FModel/Resources/fa-brands-400.otf b/FModel/Resources/fa-brands-400.otf new file mode 100644 index 000000000..9e0133231 Binary files /dev/null and b/FModel/Resources/fa-brands-400.otf differ diff --git a/FModel/Resources/fa-regular-400.otf b/FModel/Resources/fa-regular-400.otf new file mode 100644 index 000000000..7cf3bee75 Binary files /dev/null and b/FModel/Resources/fa-regular-400.otf differ diff --git a/FModel/Resources/fa-solid-900.otf b/FModel/Resources/fa-solid-900.otf new file mode 100644 index 000000000..b00559ccf Binary files /dev/null and b/FModel/Resources/fa-solid-900.otf differ diff --git a/FModel/Settings/UserSettings.cs b/FModel/Settings/UserSettings.cs index 0f48e7e89..9fc0b922b 100644 --- a/FModel/Settings/UserSettings.cs +++ b/FModel/Settings/UserSettings.cs @@ -4,14 +4,10 @@ using System.Windows; using System.Windows.Input; using CUE4Parse.UE4.Assets.Exports.Material; -using CUE4Parse.UE4.Assets.Exports.Nanite; -using CUE4Parse.UE4.Lua.unluac; using CUE4Parse.UE4.Versions; -using CUE4Parse_Conversion; -using CUE4Parse_Conversion.Animations; -using CUE4Parse_Conversion.Meshes; -using CUE4Parse_Conversion.Textures; -using CUE4Parse_Conversion.UEFormat.Enums; +using CUE4Parse_Conversion.Options; +using CUE4Parse_Conversion.Writers.UEFormat.Enums; +using CUE4Parse.UE4.Lua.unluac; using FModel.Extensions.Themes; using FModel.Framework; using FModel.ViewModels; @@ -58,26 +54,23 @@ public static bool IsEndpointValid(EEndpointType type, out EndpointSettings endp return endpoint.Overwrite || endpoint.IsValid; } - [JsonIgnore] - public ExporterOptions ExportOptions => new() + public static ExportOptions GetExportOptions() { - LodFormat = Default.LodExportFormat, - MeshFormat = Default.MeshExportFormat, - NaniteMeshFormat = Default.NaniteMeshExportFormat, - AnimFormat = Default.MeshExportFormat switch - { - EMeshFormat.UEFormat => EAnimFormat.UEFormat, - _ => EAnimFormat.ActorX - }, - MaterialFormat = Default.MaterialExportFormat, - TextureFormat = Default.TextureExportFormat, - SocketFormat = Default.SocketExportFormat, - CompressionFormat = Default.CompressionFormat, - Platform = Default.CurrentDir.TexturePlatform, - ExportMorphTargets = Default.SaveMorphTargets, - ExportMaterials = Default.SaveEmbeddedMaterials, - ExportHdrTexturesAsHdr = Default.SaveHdrTexturesAsHdr - }; + return new ExportOptions( + Default.MeshExportFormat, + Default.NaniteMeshExportFormat, + Default.MeshQuality, + Default.CurrentDir.TexturePlatform, + Default.TextureExportFormat, + Default.TextureQuality, + Default.SaveHdrTexturesAsHdr, + Default.MaterialExportFormat, + Default.SaveEmbeddedMaterials, + Default.SaveMorphTargets, + Default.SocketExportFormat, + Default.CompressionFormat + ); + } private bool _showChangelog = true; public bool ShowChangelog @@ -282,6 +275,13 @@ public bool ConvertAudioOnBulkExport set => SetProperty(ref _convertAudioOnBulkExport, value); } + private bool _mergeEditorOnlyDataExports = false; + public bool MergeEditorOnlyDataExports + { + get => _mergeEditorOnlyDataExports; + set => SetProperty(ref _mergeEditorOnlyDataExports, value); + } + private bool _decompileLua; public bool DecompileLua { @@ -402,6 +402,13 @@ public Hotkey AddAudio set => SetProperty(ref _addAudio, value); } + private Hotkey _removeAudio = new(Key.X); + public Hotkey RemoveAudio + { + get => _removeAudio; + set => SetProperty(ref _removeAudio, value); + } + private Hotkey _playPauseAudio = new(Key.K); public Hotkey PlayPauseAudio { @@ -430,15 +437,22 @@ public EMeshFormat MeshExportFormat set => SetProperty(ref _meshExportFormat, value); } - private ENaniteMeshFormat _naniteMeshExportFormat = ENaniteMeshFormat.OnlyNaniteLOD; + private ENaniteMeshFormat _naniteMeshExportFormat = ENaniteMeshFormat.NaniteOnly; public ENaniteMeshFormat NaniteMeshExportFormat { get => _naniteMeshExportFormat; set => SetProperty(ref _naniteMeshExportFormat, value); } - private EMaterialFormat _materialExportFormat = EMaterialFormat.FirstLayer; - public EMaterialFormat MaterialExportFormat + private EMeshQuality _meshQuality = EMeshQuality.Highest; + public EMeshQuality MeshQuality + { + get => _meshQuality; + set => SetProperty(ref _meshQuality, value); + } + + private EMaterialDepth _materialExportFormat = EMaterialDepth.TopLayerOnly; + public EMaterialDepth MaterialExportFormat { get => _materialExportFormat; set => SetProperty(ref _materialExportFormat, value); @@ -451,6 +465,13 @@ public ETextureFormat TextureExportFormat set => SetProperty(ref _textureExportFormat, value); } + private int _textureQuality = 100; + public int TextureQuality + { + get => _textureQuality; + set => SetProperty(ref _textureQuality, value); + } + private ESocketFormat _socketExportFormat = ESocketFormat.Bone; public ESocketFormat SocketExportFormat { @@ -465,13 +486,6 @@ public EFileCompressionFormat CompressionFormat set => SetProperty(ref _compressionFormat, value); } - private ELodFormat _lodExportFormat = ELodFormat.FirstLod; - public ELodFormat LodExportFormat - { - get => _lodExportFormat; - set => SetProperty(ref _lodExportFormat, value); - } - private bool _showSkybox = true; public bool ShowSkybox { @@ -583,5 +597,13 @@ public bool PreviewTexturesAssetExplorer get => _previewTexturesAssetExplorer; set => SetProperty(ref _previewTexturesAssetExplorer, value); } + + private EExplorerViewMode _explorerViewMode = EExplorerViewMode.Grid; + + public EExplorerViewMode ExplorerViewMode + { + get => _explorerViewMode; + set => SetProperty(ref _explorerViewMode, value); + } } } diff --git a/FModel/ViewModels/ApiEndpoints/GitHubApiEndpoint.cs b/FModel/ViewModels/ApiEndpoints/GitHubApiEndpoint.cs index 4fd507317..f9165130a 100644 --- a/FModel/ViewModels/ApiEndpoints/GitHubApiEndpoint.cs +++ b/FModel/ViewModels/ApiEndpoints/GitHubApiEndpoint.cs @@ -1,3 +1,4 @@ +using System; using System.Threading.Tasks; using FModel.Framework; using FModel.ViewModels.ApiEndpoints.Models; @@ -26,7 +27,7 @@ public async Task GetReleaseAsync(string tag) public async Task GetUserAsync(string username) { - var request = new FRestRequest($"https://api.github.com/users/{username}"); + var request = new FRestRequest($"https://api.github.com/users/{Uri.EscapeDataString(username)}"); var response = await _client.ExecuteAsync(request).ConfigureAwait(false); return response.Data; } diff --git a/FModel/ViewModels/AudioPlayerViewModel.cs b/FModel/ViewModels/AudioPlayerViewModel.cs index 997b986ad..ee9f2af9e 100644 --- a/FModel/ViewModels/AudioPlayerViewModel.cs +++ b/FModel/ViewModels/AudioPlayerViewModel.cs @@ -239,6 +239,20 @@ public void Load() }); } + public void Unload() + { + Application.Current.Dispatcher.Invoke(() => + { + _waveSource = null; + + PlayedFile = new AudioFile(-1, "No audio file"); + Spectrum = null; + + RaiseSourceEvent(ESourceEventType.Clearing); + ClearSoundOut(); + }); + } + public void AddToPlaylist(byte[] data, string filePath) { Application.Current.Dispatcher.Invoke(() => @@ -270,11 +284,30 @@ public void Remove() if (_audioFiles.Count < 1) return; Application.Current.Dispatcher.Invoke(() => { + var removedPlaying = false; + if (PlayedFile.Id == SelectedAudioFile.Id) + { + removedPlaying = true; + Stop(); + } + _audioFiles.RemoveAt(SelectedAudioFile.Id); for (var i = 0; i < _audioFiles.Count; i++) { _audioFiles[i].Id = i; } + + if (_audioFiles.Count < 1) + { + Unload(); + return; + } + + SelectedAudioFile = SelectedAudioFile.Id >= _audioFiles.Count ? _audioFiles.Last() : _audioFiles[SelectedAudioFile.Id]; + + if (!removedPlaying) return; + Load(); + Play(); }); } @@ -526,6 +559,11 @@ private void LoadSoundOut() _soundOut.Volume = UserSettings.Default.AudioPlayerVolume / 100; } + private void ClearSoundOut() + { + _soundOut = null; + } + private IEnumerable EnumerateDevices() { using var deviceEnumerator = new MMDeviceEnumerator(); diff --git a/FModel/ViewModels/CUE4ParseViewModel.cs b/FModel/ViewModels/CUE4ParseViewModel.cs index caf7527a5..2d4923d73 100644 --- a/FModel/ViewModels/CUE4ParseViewModel.cs +++ b/FModel/ViewModels/CUE4ParseViewModel.cs @@ -44,7 +44,6 @@ using CUE4Parse.UE4.Assets.Exports.Texture; using CUE4Parse.UE4.Assets.Exports.Verse; using CUE4Parse.UE4.Assets.Exports.Wwise; -using CUE4Parse.UE4.Assets.Objects; using CUE4Parse.UE4.BinaryConfig; using CUE4Parse.UE4.CriWare; using CUE4Parse.UE4.CriWare.Readers; @@ -63,7 +62,7 @@ using CUE4Parse.UE4.Versions; using CUE4Parse.UE4.Wwise; using CUE4Parse.Utils; -using CUE4Parse_Conversion; +using CUE4Parse_Conversion.Exporters; using CUE4Parse_Conversion.Sounds; using CUE4Parse.MappingsProvider.Jmap; using CUE4Parse.MappingsProvider.Usmap; @@ -218,6 +217,7 @@ public CUE4ParseViewModel() Provider.ReadScriptData = UserSettings.Default.ReadScriptData; Provider.ReadShaderMaps = UserSettings.Default.ReadShaderMaps; Provider.ReadNaniteData = true; + PropertyUtil.SearchPropertyInTemplate = true; // search template properties when looking for a prop via GetOrDefault and cie GameDirectory = new GameDirectoryViewModel(); AssetsFolder = new AssetsFolderViewModel(); @@ -622,7 +622,7 @@ public void ExportFolder(CancellationToken cancellationToken, TreeItem folder) Parallel.ForEach(folder.AssetsList.Assets, entry => { cancellationToken.ThrowIfCancellationRequested(); - ExportData(entry.Asset, false); + ExportData(entry.Asset); }); foreach (var f in folder.Folders) ExportFolder(cancellationToken, f); @@ -631,27 +631,6 @@ public void ExportFolder(CancellationToken cancellationToken, TreeItem folder) public void ExtractFolder(CancellationToken cancellationToken, TreeItem folder, EBulkType bulk) => BulkFolder(cancellationToken, folder, asset => Extract(cancellationToken, asset, TabControl.HasNoTabs, bulk)); - 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 CodeFolder(CancellationToken cancellationToken, TreeItem folder) - => BulkFolder(cancellationToken, folder, asset => Extract(cancellationToken, asset, TabControl.HasNoTabs, EBulkType.Code | EBulkType.Auto)); - public void Extract(CancellationToken cancellationToken, GameFile entry, bool addNewTab = false, EBulkType bulk = EBulkType.None) { ApplicationService.ApplicationView.IsAssetsExplorerVisible = false; @@ -676,7 +655,34 @@ public void Extract(CancellationToken cancellationToken, GameFile entry, bool ad if (saveProperties || updateUi) { - TabControl.SelectedTab.SetDocumentText(JsonConvert.SerializeObject(result.GetDisplayData(saveProperties), Formatting.Indented), saveProperties, updateUi); + var displayData = result.GetDisplayData(saveProperties); + + if (UserSettings.Default.MergeEditorOnlyDataExports && Provider.TryLoadPackage(entry.Path.SubstringBefore('.') + ".o.uasset", out var editorAsset)) + { + var pkg = Provider.LoadPackage(entry.Path); + var exports = pkg.GetExports().ToArray(); + var finalExports = new List(exports); + var editorOnlyDataExports = new HashSet(); + + foreach (var export in exports) + { + var editorData = editorAsset.GetExportOrNull(export.Name + "EditorOnlyData"); + if (editorData == null) + continue; + + export.Properties.AddRange(editorData.Properties); + editorOnlyDataExports.Add(editorData); + } + + if (editorOnlyDataExports.Count > 0) + { + finalExports.AddRange(editorAsset.GetExports().Where(editorExport => !editorOnlyDataExports.Contains(editorExport))); + } + + displayData = finalExports; + } + + TabControl.SelectedTab.SetDocumentText(JsonConvert.SerializeObject(displayData, Formatting.Indented), saveProperties, updateUi); if (saveProperties) break; // do not search for viewable exports if we are dealing with jsons } @@ -814,6 +820,7 @@ public void Extract(CancellationToken cancellationToken, GameFile entry, bool ad } case "ebd" when Provider.Versions.Game is EGame.GAME_ArcRaiders: case "json": + case "Json": { var data = Provider.SaveAsset(entry); using var stream = new MemoryStream(data) { Position = 0 }; @@ -1537,10 +1544,11 @@ public void ExtractAndScroll(CancellationToken cancellationToken, string fullPat 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): + // case UMaterialInterface when HasFlag(bulk, EBulkType.Materials): + case UAnimationAsset when HasFlag(bulk, EBulkType.Animations): + case UWorld when HasFlag(bulk, EBulkType.Worlds): { - SaveExport(pointer.Object.Value, updateUi); + SaveExport(pointer.Object.Value); return true; } default: @@ -1697,64 +1705,27 @@ private void SaveAndPlaySound(CancellationToken cancellationToken, string fullPa }); } - private void SaveExport(UObject export, bool updateUi = true) + private void SaveExport(UObject export) { - 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)) + try { - Interlocked.Increment(ref ExportedCount); - Log.Information("Successfully saved {FilePath}", savedFilePath); - if (updateUi) - { - FLogger.Append(ELog.Information, () => - { - FLogger.Text("Successfully saved ", Constants.WHITE); - FLogger.Link(label, savedFilePath, true); - }); - } + ExportSessionViewModel.Instance.Session.Add(export); } - else + catch (Exception e) { - Interlocked.Increment(ref FailedExportCount); - Log.Error("{FileName} could not be saved", export.Name); - FLogger.Append(ELog.Error, () => FLogger.Text($"Could not save '{export.Name}'", Constants.WHITE, true)); + Log.Error(e, "Could not add to export session"); } } - private readonly object _rawData = new (); - public void ExportData(GameFile entry, bool updateUi = true) + public void ExportData(GameFile entry) { - if (Provider.TrySavePackage(entry, out var assets)) + try { - 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); - } - }); - - Interlocked.Increment(ref ExportedCount); - 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); - }); - } + ExportSessionViewModel.Instance.Session.Add(new RawDataExporter(entry, Provider)); } - else + catch (Exception e) { - Interlocked.Increment(ref FailedExportCount); - 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)); + Log.Error(e, "Could not add to export session"); } } diff --git a/FModel/ViewModels/Commands/MenuCommand.cs b/FModel/ViewModels/Commands/MenuCommand.cs index fe223f882..06e17a1a4 100644 --- a/FModel/ViewModels/Commands/MenuCommand.cs +++ b/FModel/ViewModels/Commands/MenuCommand.cs @@ -40,6 +40,9 @@ public override async void Execute(ApplicationViewModel contextViewModel, object case "Views_3dViewer": contextViewModel.CUE4Parse.SnooperViewer.Run(); break; + case "Views_ExportSession": + Helper.OpenWindow("Export Session", () => new ExportSessionWindow().Show()); + break; case "Views_AudioPlayer": Helper.OpenWindow("Audio Player", () => new AudioPlayer().Show()); break; diff --git a/FModel/ViewModels/Commands/RightClickMenuCommand.cs b/FModel/ViewModels/Commands/RightClickMenuCommand.cs index b490957c0..42e4e8a37 100644 --- a/FModel/ViewModels/Commands/RightClickMenuCommand.cs +++ b/FModel/ViewModels/Commands/RightClickMenuCommand.cs @@ -67,6 +67,7 @@ public override async void Execute(ApplicationViewModel contextViewModel, object "Save_Properties" => (EAction.Export, EShowAssetType.None, EBulkType.Properties), "Save_Textures" => (EAction.Export, EShowAssetType.None, EBulkType.Textures), "Save_Models" => (EAction.Export, EShowAssetType.None, EBulkType.Meshes), + "Save_Worlds" => (EAction.Export, EShowAssetType.None, EBulkType.Worlds), "Save_Animations" => (EAction.Export, EShowAssetType.None, EBulkType.Animations), "Save_Audio" => (EAction.Export, EShowAssetType.None, EBulkType.Audio), "Save_Code" => (EAction.Export, EShowAssetType.None, EBulkType.Code), @@ -108,6 +109,7 @@ await _threadWorkerView.Begin(cancellationToken => EBulkType.Properties => (UserSettings.Default.PropertiesDirectory, "json files"), EBulkType.Textures => (UserSettings.Default.TextureDirectory, "textures"), EBulkType.Meshes => (UserSettings.Default.ModelDirectory, "models"), + EBulkType.Worlds => (UserSettings.Default.ModelDirectory, "worlds"), EBulkType.Animations => (UserSettings.Default.ModelDirectory, "animations"), EBulkType.Audio => (UserSettings.Default.AudioDirectory, "audio files"), EBulkType.Code => (UserSettings.Default.CodeDirectory, "code files"), @@ -126,16 +128,17 @@ await _threadWorkerView.Begin(cancellationToken => foreach (var folder in folders) { cancellationToken.ThrowIfCancellationRequested(); + var queuedBefore = ExportSessionViewModel.Instance.Session.TotalQueued; folderAction(folder); var path = Path.Combine(dirType, UserSettings.Default.KeepDirectoryStructure ? folder.PathAtThisPoint : folder.PathAtThisPoint.SubstringAfterLast('/')).Replace('\\', '/'); - LogExport(contextViewModel, folder.PathAtThisPoint, path, dirType, filetype); + LogExport(contextViewModel, folder.PathAtThisPoint, path, dirType, filetype, queuedBefore); } - Action fileAction = bulktype switch + Action fileAction = bulktype switch { - EBulkType.Raw => (entry, _, update) => contextViewModel.CUE4Parse.ExportData(entry, !update), - _ => (entry, bulk, update) => contextViewModel.CUE4Parse.Extract(cancellationToken, entry, false, bulk), + EBulkType.Raw => (entry, _) => contextViewModel.CUE4Parse.ExportData(entry), + _ => (entry, bulk) => contextViewModel.CUE4Parse.Extract(cancellationToken, entry, false, bulk), }; foreach (var group in assetsGroups) @@ -144,24 +147,26 @@ await _threadWorkerView.Begin(cancellationToken => var list = group.ToArray(); var update = list.Length > 1; var bulk = bulktype | (update ? EBulkType.Auto : EBulkType.None); + var queuedBefore = ExportSessionViewModel.Instance.Session.TotalQueued; foreach (var entry in list) { Thread.Yield(); cancellationToken.ThrowIfCancellationRequested(); - fileAction(entry, bulk, update); + fileAction(entry, bulk); } if (update) { var path = Path.Combine(dirType, UserSettings.Default.KeepDirectoryStructure ? directory : directory.SubstringAfterLast('/')).Replace('\\', '/'); - LogExport(contextViewModel, directory, path, dirType, filetype); + LogExport(contextViewModel, directory, path, dirType, filetype, queuedBefore); } } }); } - private void LogExport(ApplicationViewModel contextViewModel, string directory, string path, string basePath, string fileType) + private void LogExport(ApplicationViewModel contextViewModel, string directory, string path, string basePath, string fileType, int queuedBefore = 0) { + var queuedDelta = ExportSessionViewModel.Instance.Session.TotalQueued - queuedBefore; if (contextViewModel.CUE4Parse.ExportedCount > 0) { FLogger.Append(ELog.Information, () => @@ -170,6 +175,13 @@ private void LogExport(ApplicationViewModel contextViewModel, string directory, FLogger.Link(directory, Path.Exists(path) ? path : basePath, true); }); } + else if (queuedDelta > 0) + { + FLogger.Append(ELog.Information, () => + { + FLogger.Text($"Queued {queuedDelta} {fileType} for export from {directory}", Constants.WHITE, true); + }); + } else if (contextViewModel.CUE4Parse.FailedExportCount == 0) { // Not an error because folder simply might not contain type of asset user is trying to save diff --git a/FModel/ViewModels/Commands/TabCommand.cs b/FModel/ViewModels/Commands/TabCommand.cs index a622d5e48..d77895374 100644 --- a/FModel/ViewModels/Commands/TabCommand.cs +++ b/FModel/ViewModels/Commands/TabCommand.cs @@ -31,9 +31,15 @@ public override async void Execute(TabItem tabViewModel, object parameter) case "Close_Other_Tabs": _applicationView.CUE4Parse.TabControl.RemoveOtherTabs(tabViewModel); break; + case "Assets_Show_Metadata": + _applicationView.CUE4Parse.ShowMetadata(tabViewModel.Entry); + break; case "Find_References": _applicationView.CUE4Parse.FindReferences(tabViewModel.Entry); break; + case "Assets_Decompile": + _applicationView.CUE4Parse.Decompile(tabViewModel.Entry); + break; case "Save_Data": await _threadWorkerView.Begin(_ => _applicationView.CUE4Parse.ExportData(tabViewModel.Entry)); break; @@ -55,6 +61,12 @@ await _threadWorkerView.Begin(cancellationToken => _applicationView.CUE4Parse.Extract(cancellationToken, tabViewModel.Entry, false, EBulkType.Meshes); }); break; + case "Save_Worlds": + await _threadWorkerView.Begin(cancellationToken => + { + _applicationView.CUE4Parse.Extract(cancellationToken, tabViewModel.Entry, false, EBulkType.Worlds); + }); + break; case "Save_Animations": await _threadWorkerView.Begin(cancellationToken => { @@ -77,9 +89,21 @@ await _threadWorkerView.Begin(cancellationToken => }.Show(); }); break; - case "Copy_Asset_Path": + case "File_Path": Clipboard.SetText(tabViewModel.Entry.Path); break; + case "File_Name": + Clipboard.SetText(tabViewModel.Entry.Name); + break; + case "Directory_Path": + Clipboard.SetText(tabViewModel.Entry.Directory); + break; + case "File_Path_No_Extension": + Clipboard.SetText(tabViewModel.Entry.PathWithoutExtension); + break; + case "File_Name_No_Extension": + Clipboard.SetText(tabViewModel.Entry.NameWithoutExtension); + break; } } } diff --git a/FModel/ViewModels/ExportOptionsViewModel.cs b/FModel/ViewModels/ExportOptionsViewModel.cs new file mode 100644 index 000000000..2f9d4b663 --- /dev/null +++ b/FModel/ViewModels/ExportOptionsViewModel.cs @@ -0,0 +1,218 @@ +using System; +using System.Collections.Generic; +using System.Windows.Threading; +using CUE4Parse.UE4.Assets.Exports.Material; +using CUE4Parse.UE4.Assets.Exports.Texture; +using CUE4Parse_Conversion.Options; +using CUE4Parse_Conversion.Writers.UEFormat.Enums; +using FModel.Framework; +using FModel.Settings; + +namespace FModel.ViewModels; + +public class ExportOptionsViewModel : ViewModel +{ + public bool OverrideOptions + { + get; + set => SetProperty(ref field, value); + } + + private DispatcherTimer? _feedbackTimer; + public string? FeedbackMessage + { + get; + private set + { + if (!SetProperty(ref field, value)) return; + RaisePropertyChanged(nameof(HasFeedback)); + _feedbackTimer?.Stop(); + if (string.IsNullOrWhiteSpace(value)) return; + + if (_feedbackTimer == null) + { + _feedbackTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(2) }; +#pragma warning disable CA2011 + _feedbackTimer.Tick += (_, _) => FeedbackMessage = null; +#pragma warning restore CA2011 + } + _feedbackTimer.Start(); + } + } + public bool HasFeedback => FeedbackMessage != null; + + public string OutputDirectory + { + get; + set => SetProperty(ref field, value); + } + + public IEnumerable MeshFormats { get; } = Enum.GetValues(); + public EMeshFormat SelectedMeshFormat + { + get; + set + { + if (!SetProperty(ref field, value)) return; + RaisePropertyChanged(nameof(SocketSettingsEnabled)); + RaisePropertyChanged(nameof(CompressionSettingsEnabled)); + RaisePropertyChanged(nameof(TextureFormatsEnabled)); + + if (value == EMeshFormat.USD) + { + _preUsdTextureFormat = SelectedTextureFormat; + SelectedTextureFormat = ETextureFormat.Png; + } + else if (_preUsdTextureFormat.HasValue) + { + SelectedTextureFormat = _preUsdTextureFormat.Value; + _preUsdTextureFormat = null; + } + } + } + + public IEnumerable NaniteMeshFormats { get; } = Enum.GetValues(); + public ENaniteMeshFormat SelectedNaniteMeshFormat + { + get; + set + { + if (!SetProperty(ref field, value)) return; + RaisePropertyChanged(nameof(ShowNaniteWarning)); + } + } + + public bool ShowNaniteWarning => SelectedNaniteMeshFormat != ENaniteMeshFormat.NoNanite; + + public IEnumerable MeshQualities { get; } = Enum.GetValues(); + public EMeshQuality SelectedMeshQuality + { + get; + set => SetProperty(ref field, value); + } + + public IEnumerable SocketFormats { get; } = Enum.GetValues(); + public ESocketFormat SelectedSocketFormat + { + get; + set => SetProperty(ref field, value); + } + + public bool SocketSettingsEnabled => SelectedMeshFormat == EMeshFormat.ActorX; + + public IEnumerable CompressionFormats { get; } = Enum.GetValues(); + public EFileCompressionFormat SelectedCompressionFormat + { + get; + set => SetProperty(ref field, value); + } + + public bool CompressionSettingsEnabled => SelectedMeshFormat == EMeshFormat.UEFormat; + + public IEnumerable MaterialDepths { get; } = Enum.GetValues(); + public EMaterialDepth SelectedMaterialDepth + { + get; + set => SetProperty(ref field, value); + } + public bool ExportMaterials + { + get; + set => SetProperty(ref field, value); + } + + public IEnumerable TexturePlatforms { get; } = Enum.GetValues(); + public ETexturePlatform SelectedTexturePlatform + { + get; + set => SetProperty(ref field, value); + } + + public IEnumerable TextureFormats { get; } = Enum.GetValues(); + public ETextureFormat SelectedTextureFormat + { + get; + set => SetProperty(ref field, value); + } + + private ETextureFormat? _preUsdTextureFormat; + public bool TextureFormatsEnabled => SelectedMeshFormat != EMeshFormat.USD; + + public bool ExportHdrTexturesAsHdr + { + get; + set => SetProperty(ref field, value); + } + public int TextureQuality + { + get; + set => SetProperty(ref field, value); + } + + public bool ExportMorphTargets + { + get; + set => SetProperty(ref field, value); + } + + public ExportOptionsViewModel() + { + ResetToUserDefaults(); + } + + public void ResetToUserDefaults() + { + OutputDirectory = UserSettings.Default.ModelDirectory; + SelectedMeshFormat = UserSettings.Default.MeshExportFormat; + SelectedNaniteMeshFormat = UserSettings.Default.NaniteMeshExportFormat; + SelectedMeshQuality = UserSettings.Default.MeshQuality; + SelectedSocketFormat = UserSettings.Default.SocketExportFormat; + SelectedCompressionFormat = UserSettings.Default.CompressionFormat; + SelectedMaterialDepth = UserSettings.Default.MaterialExportFormat; + ExportMaterials = UserSettings.Default.SaveEmbeddedMaterials; + SelectedTexturePlatform = UserSettings.Default.CurrentDir.TexturePlatform; + SelectedTextureFormat = UserSettings.Default.TextureExportFormat; + ExportHdrTexturesAsHdr = UserSettings.Default.SaveHdrTexturesAsHdr; + ExportMorphTargets = UserSettings.Default.SaveMorphTargets; + TextureQuality = UserSettings.Default.TextureQuality; + + OverrideOptions = false; + FeedbackMessage = "Reset to defaults"; + } + + public void SaveAsUserDefaults() + { + UserSettings.Default.ModelDirectory = OutputDirectory; + UserSettings.Default.MeshExportFormat = SelectedMeshFormat; + UserSettings.Default.NaniteMeshExportFormat = SelectedNaniteMeshFormat; + UserSettings.Default.MeshQuality = SelectedMeshQuality; + UserSettings.Default.SocketExportFormat = SelectedSocketFormat; + UserSettings.Default.CompressionFormat = SelectedCompressionFormat; + UserSettings.Default.MaterialExportFormat = SelectedMaterialDepth; + UserSettings.Default.SaveEmbeddedMaterials = ExportMaterials; + UserSettings.Default.CurrentDir.TexturePlatform = SelectedTexturePlatform; + UserSettings.Default.TextureExportFormat = SelectedTextureFormat; + UserSettings.Default.SaveHdrTexturesAsHdr = ExportHdrTexturesAsHdr; + UserSettings.Default.SaveMorphTargets = ExportMorphTargets; + UserSettings.Default.TextureQuality = TextureQuality; + UserSettings.Save(); + + OverrideOptions = false; + FeedbackMessage = "Saved as default"; + } + + public ExportOptions BuildOptions() => new( + SelectedMeshFormat, + SelectedNaniteMeshFormat, + SelectedMeshQuality, + SelectedTexturePlatform, + SelectedTextureFormat, + TextureQuality, + ExportHdrTexturesAsHdr, + SelectedMaterialDepth, + ExportMaterials, + ExportMorphTargets, + SelectedSocketFormat, + SelectedCompressionFormat + ); +} diff --git a/FModel/ViewModels/ExportSessionViewModel.cs b/FModel/ViewModels/ExportSessionViewModel.cs new file mode 100644 index 000000000..fbeb8f1ca --- /dev/null +++ b/FModel/ViewModels/ExportSessionViewModel.cs @@ -0,0 +1,405 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Threading; +using CUE4Parse_Conversion; +using CUE4Parse_Conversion.Options; +using CUE4Parse.Utils; +using FModel.Extensions; +using FModel.Framework; +using FModel.Settings; +using FModel.Views; +using FModel.Views.Snooper; +using Serilog.Events; + +namespace FModel.ViewModels; + +public class ExportSessionViewModel : ViewModel +{ + public static ExportSessionViewModel Instance { get; } = new(); + + private DispatcherTimer? _toastTimer; + public bool ShowQueueToast + { + get; + set + { + if (!SetProperty(ref field, value)) return; + if (!value) + { + _toastTimer?.Stop(); + return; + } + + if (_toastTimer == null) + { + _toastTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(5) }; + _toastTimer.Tick += (_, _) => + { + field = false; + RaisePropertyChanged(nameof(ShowQueueToast)); + _toastTimer.Stop(); + }; + } + + _toastTimer.Stop(); + _toastTimer.Start(); + } + } + + private ExportSession? _session; + public ExportSession Session + { + get + { + if (_session != null) return _session; + _session = new ExportSession((args, ct) => + { + Application.Current.Dispatcher.Invoke(() => + { + var window = new StreamingLevelFilterWindow(new StreamingLevelFilterViewModel(args)); + _stopwatch.Stop(); + window.ShowDialog(); + _stopwatch.Start(); + }, DispatcherPriority.Normal, ct); + }); + _session.PropertyChanged += OnSessionPropertyChanged; + return _session; + } + } + + public ExportOptionsViewModel Options { get; } = new(); + + public bool IsRunning + { + get; + private set + { + if (!SetProperty(ref field, value)) return; + RaisePropertyChanged(nameof(CanExport)); + } + } + public bool IsFinished + { + get; + private set => SetProperty(ref field, value); + } + public bool CanExport => !IsRunning && Session.TotalQueued > 0; + + public int CompletedCount + { + get; + private set => SetProperty(ref field, value); + } + public int SucceededCount + { + get; + private set => SetProperty(ref field, value); + } + public int FailedCount + { + get; + private set => SetProperty(ref field, value); + } + public string? CurrentItemName + { + get; + private set => SetProperty(ref field, value); + } + public TimeSpan ElapsedTime + { + get; + private set => SetProperty(ref field, value); + } + public TimeSpan? EtaTime + { + get; + private set => SetProperty(ref field, value); + } + public bool IsCanceled + { + get; + private set => SetProperty(ref field, value); + } + public double ProgressValue + { + get; + private set => SetProperty(ref field, value); + } + + public ObservableCollection ClassGroups { get; } = []; + + private CancellationTokenSource? _cts; + private readonly Stopwatch _stopwatch = new(); + private readonly ConcurrentQueue _pendingLogs = new(); + private DispatcherTimer? _uiTimer; + + private ExportSessionViewModel() + { + ImGuiSink.Instance.OnExporterLogEvent += OnLogEvent; + } + + private void OnLogEvent(LogEvent log) + { + _pendingLogs.Enqueue(log); + Application.Current?.Dispatcher.InvokeAsync(DrainLogs); + } + + private int _previousCount; + private void OnSessionPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName != nameof(ExportSession.TotalQueued)) return; + + var count = _session?.TotalQueued ?? 0; + Application.Current?.Dispatcher.InvokeAsync(() => + { + if (count > 0 && _previousCount == 0) + { + ClearExportHistory(); + } + + ShowQueueToast = count switch + { + > 0 when _previousCount == 0 => true, + 0 => false, + _ => ShowQueueToast + }; + _previousCount = count; + RaisePropertyChanged(nameof(CanExport)); + }); + } + + public async Task ExportAsync() + { + if (IsRunning || Session.TotalQueued == 0) return; + + IsRunning = true; + IsFinished = false; + IsCanceled = false; + CompletedCount = 0; + SucceededCount = 0; + FailedCount = 0; + _stopwatch.Restart(); + + _cts = new CancellationTokenSource(); + StartUiTimer(); + + string exportDirectory; + ExportOptions exportOptions; + if (Options.OverrideOptions) + { + exportDirectory = Options.OutputDirectory; + exportOptions = Options.BuildOptions(); + } + else + { + exportDirectory = UserSettings.Default.ModelDirectory; + exportOptions = UserSettings.GetExportOptions(); + } + + var progress = new Progress(p => + { + Application.Current?.Dispatcher.InvokeAsync(() => + { + CompletedCount = p.Completed; + CurrentItemName = p.LastResult?.ObjectPath; + if (p.LastResult != null) + { + if (p.LastResult.Success) SucceededCount++; + else FailedCount++; + } + ProgressValue = p.Total > 0 ? (double)p.Completed / p.Total : 0; + }); + }); + + try + { + await Session.RunAsync(exportDirectory, exportOptions, progress, _cts.Token).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + Application.Current?.Dispatcher.InvokeAsync(() => IsCanceled = true); + } + finally + { + _stopwatch.Stop(); + Application.Current?.Dispatcher.InvokeAsync(() => + { + StopUiTimer(); + IsRunning = false; + IsFinished = !IsCanceled; + UpdateElapsedAndEta(); + }); + } + } + + public void CancelExport() + { + _cts?.Cancel(); + } + + public void ClearQueue() + { + _session?.Clear(); + ClearExportHistory(); + } + + private void StartUiTimer() + { + _uiTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(100) }; + _uiTimer.Tick += (_, _) => UpdateElapsedAndEta(); + _uiTimer.Start(); + } + + private void StopUiTimer() + { + _uiTimer?.Stop(); + _uiTimer = null; + } + + private void UpdateElapsedAndEta() + { + ElapsedTime = _stopwatch.Elapsed; + + var remaining = Session.TotalQueued; + if (IsRunning && remaining > 0 && CompletedCount > 1 && ElapsedTime.TotalSeconds > 0) + { + var rate = CompletedCount / ElapsedTime.TotalSeconds; + if (rate > 0) + { + EtaTime = TimeSpan.FromSeconds(remaining / rate); + return; + } + } + EtaTime = null; + } + + private void ClearExportHistory() + { + CompletedCount = 0; + SucceededCount = 0; + FailedCount = 0; + ProgressValue = 0; + ElapsedTime = TimeSpan.Zero; + EtaTime = null; + CurrentItemName = null; + IsFinished = false; + IsCanceled = false; + ClassGroups.Clear(); + } + + private void DrainLogs() + { + while (_pendingLogs.TryDequeue(out var log)) + { + var className = log.GetContext("ClassName"); + var objectPath = log.GetContext("ObjectPath"); + var filePath = log.GetContext("FilePath"); + + var cg = FindOrCreateClass(className); + var og = FindOrCreateObject(cg, objectPath); + if (log.Level >= LogEventLevel.Error) + { + og.ErrorCount++; + cg.ErrorCount++; + } + if (og.FirstFilePath == null && !string.IsNullOrEmpty(filePath)) + og.FirstFilePath = filePath; + og.Entries.Add(new LogEntryViewModel(log)); + } + } + + private ClassGroupViewModel FindOrCreateClass(string name) + { + var cg = ClassGroups.FirstOrDefault(c => c.Name == name); + if (cg != null) return cg; + cg = new ClassGroupViewModel(name); + ClassGroups.Add(cg); + return cg; + } + + private static ObjectGroupViewModel FindOrCreateObject(ClassGroupViewModel cg, string path) + { + var og = cg.Objects.FirstOrDefault(o => o.Path == path); + if (og != null) return og; + og = new ObjectGroupViewModel(path); + cg.Objects.Add(og); + return og; + } +} + +public class ClassGroupViewModel(string name) : ViewModel +{ + public string Name { get; } = name; + public ObservableCollection Objects { get; } = []; + + public bool IsExpanded + { + get; + set => SetProperty(ref field, value); + } + + public int ErrorCount + { + get; + set + { + SetProperty(ref field, value); + RaisePropertyChanged(nameof(HasErrors)); + } + } + public override bool HasErrors => ErrorCount > 0; +} + +public class ObjectGroupViewModel(string path) : ViewModel +{ + public string Path { get; } = path; + public string Name { get; } = path.SubstringAfterLast('.'); + public ObservableCollection Entries { get; } = []; + + public bool IsExpanded + { + get; + set => SetProperty(ref field, value); + } + + public int ErrorCount + { + get; + set + { + SetProperty(ref field, value); + RaisePropertyChanged(nameof(HasErrors)); + } + } + public override bool HasErrors => ErrorCount > 0; + + public string? FirstFilePath + { + get; + set + { + SetProperty(ref field, value); + RaisePropertyChanged(nameof(HasFilePath)); + } + } + public bool HasFilePath => FirstFilePath != null; +} + +public class LogEntryViewModel(LogEvent log) +{ + public LogEventLevel Level { get; } = log.Level; + public DateTimeOffset Timestamp { get; } = log.Timestamp; + public string Message { get; } = log.Exception switch + { + NullReferenceException or ArgumentException => log.RenderMessage(), + _ => log.Exception?.Message ?? log.RenderMessage() + }; + public Exception? Exception { get; } = log.Exception; +} diff --git a/FModel/ViewModels/GameFileViewModel.cs b/FModel/ViewModels/GameFileViewModel.cs index 6bac2c927..f8841805f 100644 --- a/FModel/ViewModels/GameFileViewModel.cs +++ b/FModel/ViewModels/GameFileViewModel.cs @@ -170,7 +170,7 @@ private Task ResolveByPackageAsync(EResolveCompute resolve) if (Asset.Extension is "umap") { AssetCategory = EAssetCategory.World; - AssetActions = EBulkType.Meshes | EBulkType.Textures | EBulkType.Audio | EBulkType.Code; + AssetActions = EBulkType.Worlds | EBulkType.Textures | EBulkType.Audio | EBulkType.Code; ResolvedAssetType = "World"; Resolved |= EResolveCompute.Preview; return Task.CompletedTask; diff --git a/FModel/ViewModels/GameSelectorViewModel.cs b/FModel/ViewModels/GameSelectorViewModel.cs index 369945b27..6447e2d78 100644 --- a/FModel/ViewModels/GameSelectorViewModel.cs +++ b/FModel/ViewModels/GameSelectorViewModel.cs @@ -134,11 +134,27 @@ private bool TryDetectUeVersion(string gameDirectory, out EGame ueVersion, [Mayb } } - var crashReportClientExe = Path.Combine(projectDir, "..", "Engine", "Binaries", "Win64", "CrashReportClient.exe"); - if (File.Exists(crashReportClientExe) && TryGetUeVersionFromExe(crashReportClientExe, out ueVersion)) + var projectEngineBinariesDir = Path.Combine(projectDir, "..", "Engine", "Binaries", "Win64"); + + if (Directory.Exists(projectEngineBinariesDir)) { - Log.Information("Detected UE version {UeVersion} from \"{Exe}\"", ueVersion, crashReportClientExe); - return true; + var crashReportClientExe = Path.Combine(projectEngineBinariesDir, "CrashReportClient.exe"); + if (File.Exists(crashReportClientExe) && TryGetUeVersionFromExe(crashReportClientExe, out ueVersion)) + { + Log.Information("Detected UE version {UeVersion} from \"{Exe}\"", ueVersion, crashReportClientExe); + return true; + } + if (Directory.GetFiles(projectEngineBinariesDir, "*-Win64-Shipping.exe") is { Length: > 0 } shipping) + { + foreach (var exe in shipping) + { + if (TryGetUeVersionFromExe(exe, out ueVersion)) + { + Log.Information("Detected UE version {UeVersion} from \"{Exe}\"", ueVersion, exe); + return true; + } + } + } } ueVersion = EGame.GAME_UE4_LATEST; diff --git a/FModel/ViewModels/SettingsViewModel.cs b/FModel/ViewModels/SettingsViewModel.cs index 1441cd2a7..fd5a97cf1 100644 --- a/FModel/ViewModels/SettingsViewModel.cs +++ b/FModel/ViewModels/SettingsViewModel.cs @@ -2,20 +2,16 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; -using CUE4Parse.UE4.Assets.Exports.Material; -using CUE4Parse.UE4.Assets.Exports.Nanite; using CUE4Parse.UE4.Assets.Exports.Texture; using CUE4Parse.UE4.Objects.Core.Serialization; using CUE4Parse.UE4.Versions; -using CUE4Parse_Conversion.Meshes; -using CUE4Parse_Conversion.Textures; -using CUE4Parse_Conversion.UEFormat.Enums; -using FModel.Extensions; +using CUE4Parse_Conversion.Options; +using CUE4Parse_Conversion.Writers.UEFormat.Enums; +using CUE4Parse.UE4.Assets.Exports.Material; using FModel.Extensions.Themes; using FModel.Framework; using FModel.Services; using FModel.Settings; -using ICSharpCode.AvalonEdit.Highlighting; namespace FModel.ViewModels; @@ -30,13 +26,6 @@ public bool UseCustomOutputFolders set => SetProperty(ref _useCustomOutputFolders, value); } - private ETexturePlatform _selectedUePlatform; - public ETexturePlatform SelectedUePlatform - { - get => _selectedUePlatform; - set => SetProperty(ref _selectedUePlatform, value); - } - private EGame _selectedUeGame; public EGame SelectedUeGame { @@ -114,60 +103,6 @@ public EIconStyle SelectedCosmeticStyle set => SetProperty(ref _selectedCosmeticStyle, value); } - private EMeshFormat _selectedMeshExportFormat; - public EMeshFormat SelectedMeshExportFormat - { - get => _selectedMeshExportFormat; - set - { - SetProperty(ref _selectedMeshExportFormat, value); - RaisePropertyChanged(nameof(SocketSettingsEnabled)); - RaisePropertyChanged(nameof(CompressionSettingsEnabled)); - } - } - - private ESocketFormat _selectedSocketExportFormat; - public ESocketFormat SelectedSocketExportFormat - { - get => _selectedSocketExportFormat; - set => SetProperty(ref _selectedSocketExportFormat, value); - } - - private EFileCompressionFormat _selectedCompressionFormat; - public EFileCompressionFormat SelectedCompressionFormat - { - get => _selectedCompressionFormat; - set => SetProperty(ref _selectedCompressionFormat, value); - } - - private ELodFormat _selectedLodExportFormat; - public ELodFormat SelectedLodExportFormat - { - get => _selectedLodExportFormat; - set => SetProperty(ref _selectedLodExportFormat, value); - } - - private ENaniteMeshFormat _selectedNaniteMeshExportFormat; - public ENaniteMeshFormat SelectedNaniteMeshExportFormat - { - get => _selectedNaniteMeshExportFormat; - set => SetProperty(ref _selectedNaniteMeshExportFormat, value); - } - - private EMaterialFormat _selectedMaterialExportFormat; - public EMaterialFormat SelectedMaterialExportFormat - { - get => _selectedMaterialExportFormat; - set => SetProperty(ref _selectedMaterialExportFormat, value); - } - - private ETextureFormat _selectedTextureExportFormat; - public ETextureFormat SelectedTextureExportFormat - { - get => _selectedTextureExportFormat; - set => SetProperty(ref _selectedTextureExportFormat, value); - } - private EJsonHighlightTheme _selectedJsonHighlightTheme; public EJsonHighlightTheme SelectedJsonHighlightTheme { @@ -189,8 +124,7 @@ public string UnluacOpcodeMap set => SetProperty(ref _unluacOpcodeMap, value); } - public bool SocketSettingsEnabled => SelectedMeshExportFormat == EMeshFormat.ActorX; - public bool CompressionSettingsEnabled => SelectedMeshExportFormat == EMeshFormat.UEFormat; + public ExportOptionsViewModel Options { get; } = new(); public ReadOnlyObservableCollection UeGames { get; private set; } public ReadOnlyObservableCollection AssetLanguages { get; private set; } @@ -198,14 +132,6 @@ public string UnluacOpcodeMap public ReadOnlyObservableCollection DiscordRpcs { get; private set; } public ReadOnlyObservableCollection CompressedAudios { get; private set; } public ReadOnlyObservableCollection CosmeticStyles { get; private set; } - public ReadOnlyObservableCollection MeshExportFormats { get; private set; } - public ReadOnlyObservableCollection SocketExportFormats { get; private set; } - public ReadOnlyObservableCollection CompressionFormats { get; private set; } - public ReadOnlyObservableCollection LodExportFormats { get; private set; } - public ReadOnlyObservableCollection NaniteMeshExportFormats { get; private set; } - public ReadOnlyObservableCollection MaterialExportFormats { get; private set; } - public ReadOnlyObservableCollection TextureExportFormats { get; private set; } - public ReadOnlyObservableCollection Platforms { get; private set; } public ReadOnlyObservableCollection JsonHighlightThemes { get; private set; } private string _outputSnapshot; @@ -216,7 +142,6 @@ public string UnluacOpcodeMap private string _codeSnapshot; private string _modelSnapshot; private string _gameSnapshot; - private ETexturePlatform _uePlatformSnapshot; private EGame _ueGameSnapshot; private IList _customVersionsSnapshot; private IDictionary _optionsSnapshot; @@ -224,13 +149,6 @@ public string UnluacOpcodeMap private ELanguage _assetLanguageSnapshot; private ECompressedAudio _compressedAudioSnapshot; private EIconStyle _cosmeticStyleSnapshot; - private EMeshFormat _meshExportFormatSnapshot; - private ESocketFormat _socketExportFormatSnapshot; - private EFileCompressionFormat _compressionFormatSnapshot; - private ELodFormat _lodExportFormatSnapshot; - private ENaniteMeshFormat _naniteMeshExportFormatSnapshot; - private EMaterialFormat _materialExportFormatSnapshot; - private ETextureFormat _textureExportFormatSnapshot; private EJsonHighlightTheme _jsonHighlightThemeSnapshot; private bool _mappingsUpdate = false; @@ -250,7 +168,6 @@ public void Initialize() _codeSnapshot = UserSettings.Default.CodeDirectory; _modelSnapshot = UserSettings.Default.ModelDirectory; _gameSnapshot = UserSettings.Default.GameDirectory; - _uePlatformSnapshot = UserSettings.Default.CurrentDir.TexturePlatform; _ueGameSnapshot = UserSettings.Default.CurrentDir.UeVersion; _customVersionsSnapshot = UserSettings.Default.CurrentDir.Versioning.CustomVersions; _optionsSnapshot = UserSettings.Default.CurrentDir.Versioning.Options; @@ -269,16 +186,8 @@ public void Initialize() _assetLanguageSnapshot = UserSettings.Default.AssetLanguage; _compressedAudioSnapshot = UserSettings.Default.CompressedAudioMode; _cosmeticStyleSnapshot = UserSettings.Default.CosmeticStyle; - _meshExportFormatSnapshot = UserSettings.Default.MeshExportFormat; - _socketExportFormatSnapshot = UserSettings.Default.SocketExportFormat; - _compressionFormatSnapshot = UserSettings.Default.CompressionFormat; - _lodExportFormatSnapshot = UserSettings.Default.LodExportFormat; - _naniteMeshExportFormatSnapshot = UserSettings.Default.NaniteMeshExportFormat; - _materialExportFormatSnapshot = UserSettings.Default.MaterialExportFormat; - _textureExportFormatSnapshot = UserSettings.Default.TextureExportFormat; _jsonHighlightThemeSnapshot = UserSettings.Default.JsonHighlightTheme; - SelectedUePlatform = _uePlatformSnapshot; SelectedUeGame = _ueGameSnapshot; SelectedCustomVersions = _customVersionsSnapshot; SelectedOptions = _optionsSnapshot; @@ -286,13 +195,6 @@ public void Initialize() SelectedAssetLanguage = _assetLanguageSnapshot; SelectedCompressedAudio = _compressedAudioSnapshot; SelectedCosmeticStyle = _cosmeticStyleSnapshot; - SelectedMeshExportFormat = _meshExportFormatSnapshot; - SelectedSocketExportFormat = _socketExportFormatSnapshot; - SelectedCompressionFormat = _selectedCompressionFormat; - SelectedLodExportFormat = _lodExportFormatSnapshot; - SelectedNaniteMeshExportFormat = _naniteMeshExportFormatSnapshot; - SelectedMaterialExportFormat = _materialExportFormatSnapshot; - SelectedTextureExportFormat = _textureExportFormatSnapshot; CriwareDecryptionKey = _criwareDecryptionKey; UnluacOpcodeMap = _unluacOpcodeMap; SelectedJsonHighlightTheme = _jsonHighlightThemeSnapshot; @@ -305,14 +207,6 @@ public void Initialize() DiscordRpcs = new ReadOnlyObservableCollection(new ObservableCollection(EnumerateDiscordRpcs())); CompressedAudios = new ReadOnlyObservableCollection(new ObservableCollection(EnumerateCompressedAudios())); CosmeticStyles = new ReadOnlyObservableCollection(new ObservableCollection(EnumerateCosmeticStyles())); - MeshExportFormats = new ReadOnlyObservableCollection(new ObservableCollection(EnumerateMeshExportFormat())); - SocketExportFormats = new ReadOnlyObservableCollection(new ObservableCollection(EnumerateSocketExportFormat())); - CompressionFormats = new ReadOnlyObservableCollection(new ObservableCollection(EnumerateCompressionFormat())); - LodExportFormats = new ReadOnlyObservableCollection(new ObservableCollection(EnumerateLodExportFormat())); - NaniteMeshExportFormats = new ReadOnlyObservableCollection(new ObservableCollection(EnumerateNaniteMeshExportFormat())); - MaterialExportFormats = new ReadOnlyObservableCollection(new ObservableCollection(EnumerateMaterialExportFormat())); - TextureExportFormats = new ReadOnlyObservableCollection(new ObservableCollection(EnumerateTextureExportFormat())); - Platforms = new ReadOnlyObservableCollection(new ObservableCollection(EnumerateUePlatforms())); JsonHighlightThemes = new ReadOnlyObservableCollection(new ObservableCollection(EnumerateJsonHighlightThemes())); } @@ -327,13 +221,12 @@ public bool Save(out List whatShouldIDo) whatShouldIDo.Add(SettingsOut.ReloadMappings); if (_ueGameSnapshot != SelectedUeGame || _customVersionsSnapshot != SelectedCustomVersions || - _uePlatformSnapshot != SelectedUePlatform || _optionsSnapshot != SelectedOptions || // combobox + _optionsSnapshot != SelectedOptions || // combobox _mapStructTypesSnapshot != SelectedMapStructTypes || _gameSnapshot != UserSettings.Default.GameDirectory) // textbox restart = true; UserSettings.Default.CurrentDir.UeVersion = SelectedUeGame; - UserSettings.Default.CurrentDir.TexturePlatform = SelectedUePlatform; UserSettings.Default.CurrentDir.Versioning.CustomVersions = SelectedCustomVersions; UserSettings.Default.CurrentDir.Versioning.Options = SelectedOptions; UserSettings.Default.CurrentDir.Versioning.MapStructTypes = SelectedMapStructTypes; @@ -343,17 +236,12 @@ public bool Save(out List whatShouldIDo) UserSettings.Default.AssetLanguage = SelectedAssetLanguage; UserSettings.Default.CompressedAudioMode = SelectedCompressedAudio; UserSettings.Default.CosmeticStyle = SelectedCosmeticStyle; - UserSettings.Default.MeshExportFormat = SelectedMeshExportFormat; - UserSettings.Default.SocketExportFormat = SelectedSocketExportFormat; - UserSettings.Default.CompressionFormat = SelectedCompressionFormat; - UserSettings.Default.LodExportFormat = SelectedLodExportFormat; - UserSettings.Default.NaniteMeshExportFormat = SelectedNaniteMeshExportFormat; - UserSettings.Default.MaterialExportFormat = SelectedMaterialExportFormat; - UserSettings.Default.TextureExportFormat = SelectedTextureExportFormat; UserSettings.Default.AesReload = SelectedAesReload; UserSettings.Default.DiscordRpc = SelectedDiscordRpc; UserSettings.Default.JsonHighlightTheme = SelectedJsonHighlightTheme; + Options.SaveAsUserDefaults(); + if (SelectedDiscordRpc == EDiscordRpc.Never) _discordHandler.Shutdown(); @@ -370,13 +258,5 @@ private IEnumerable EnumerateUeGames() private IEnumerable EnumerateDiscordRpcs() => Enum.GetValues(); private IEnumerable EnumerateCompressedAudios() => Enum.GetValues(); private IEnumerable EnumerateCosmeticStyles() => Enum.GetValues(); - private IEnumerable EnumerateMeshExportFormat() => Enum.GetValues(); - private IEnumerable EnumerateSocketExportFormat() => Enum.GetValues(); - private IEnumerable EnumerateCompressionFormat() => Enum.GetValues(); - private IEnumerable EnumerateLodExportFormat() => Enum.GetValues(); - private IEnumerable EnumerateNaniteMeshExportFormat() => Enum.GetValues(); - private IEnumerable EnumerateMaterialExportFormat() => Enum.GetValues(); - private IEnumerable EnumerateTextureExportFormat() => Enum.GetValues(); - private IEnumerable EnumerateUePlatforms() => Enum.GetValues(); private IEnumerable EnumerateJsonHighlightThemes() => Enum.GetValues(); } diff --git a/FModel/ViewModels/StreamingLevelFilterViewModel.cs b/FModel/ViewModels/StreamingLevelFilterViewModel.cs new file mode 100644 index 000000000..a77fc448a --- /dev/null +++ b/FModel/ViewModels/StreamingLevelFilterViewModel.cs @@ -0,0 +1,192 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Linq; +using System.Runtime.CompilerServices; +using CUE4Parse_Conversion; +using CUE4Parse_Conversion.Dto; + +namespace FModel.ViewModels; + +public class StreamingLevelFilterViewModel +{ + public string WorldName { get; } + public int TotalCount { get; } + public List Children { get; } = []; + + public StreamingLevelFilterViewModel(StreamingLevelFilterArgs args) + { + WorldName = args.WorldName; + + foreach (var actor in args.Actors) + { + Children.Add(new ActorNodeVm(actor)); + } + + var worldLevels = new ActorNodeVm("Streaming Levels") { IsExpanded = true }; + foreach (var level in args.StreamingLevels) + { + worldLevels.Children.Add(new StreamingLevelNodeVm(level)); + } + if (worldLevels.Children.Count > 0) Children.Add(worldLevels); + + TotalCount = CountLevels(args.Actors) + args.StreamingLevels.Count; + } + + public void SkipAll() + { + foreach (var child in Children) + { + child.IsChecked = false; + } + } + + private int CountLevels(IReadOnlyList actors) + { + var n = 0; + foreach (var a in actors) n += CountFromActor(a); + return n; + } + + private int CountFromActor(ActorDto actor) + { + var n = actor.StreamingLevels?.Count ?? 0; + if (actor.RootComponent is { } comp) n += CountFromComponent(comp); + return n; + } + + private int CountFromComponent(SceneComponentDto comp) + { + var n = 0; + foreach (var a in comp.AttachedActors) n += CountFromActor(a); + foreach (var c in comp.Children) n += CountFromComponent(c); + return n; + } +} + +public class ActorNodeVm : TreeNodeVm +{ + public override string Name { get; } + public bool IsExpanded { get; set; } + public ObservableCollection Children { get; } = []; + + private static int _batch; + + public ActorNodeVm(string name) + { + Name = name; + Children.CollectionChanged += (_, e) => + { + if (e.NewItems is null) return; + + foreach (TreeNodeVm child in e.NewItems) + { + child.PropertyChanged += (_, args) => + { + if (args.PropertyName == nameof(IsChecked) && _batch == 0) + OnPropertyChanged(nameof(IsChecked)); + }; + } + }; + } + + public ActorNodeVm(ActorDto actor) : this(actor.Name) + { + if (actor.StreamingLevels is { Count: > 0 } streamingLevels) + foreach (var level in streamingLevels) + Children.Add(new StreamingLevelNodeVm(level)); + + CollectFromComponent(actor.RootComponent); + } + + private ActorNodeVm(SceneComponentDto component) : this(component.Name) + { + CollectFromComponent(component); + } + + private void NotifyIntermediates() + { + foreach (var child in Children.OfType()) + { + child.NotifyIntermediates(); + child.OnPropertyChanged(nameof(IsChecked)); + } + } + + private void CollectFromComponent(SceneComponentDto? comp) + { + if (comp is null) return; + foreach (var actor in comp.AttachedActors) + Children.Add(new ActorNodeVm(actor)); + foreach (var component in comp.Children) + Children.Add(new ActorNodeVm(component)); + } + + private IEnumerable AllLevels() + { + foreach (var child in Children) + { + switch (child) + { + case StreamingLevelNodeVm sl: + yield return sl; + break; + case ActorNodeVm actor: + { + foreach (var l in actor.AllLevels()) + { + yield return l; + } + break; + } + } + } + } + + public bool? IsChecked + { + get + { + var all = AllLevels().ToList(); + if (all.Count == 0) return false; + + var trueCount = all.Count(l => l.IsChecked); + if (trueCount == all.Count) return true; + if (trueCount == 0) return false; + return null; + } + set + { + var v = value ?? false; + _batch++; + foreach (var l in AllLevels()) + l.IsChecked = v; + NotifyIntermediates(); + _batch--; + OnPropertyChanged(); + } + } +} + +public class StreamingLevelNodeVm(StreamingLevel level) : TreeNodeVm +{ + public override string Name { get; } = level.World.Name; + + public bool IsChecked + { + get => level.IsPersistent; + set + { + level.IsPersistent = value; + OnPropertyChanged(); + } + } +} + +public abstract class TreeNodeVm : INotifyPropertyChanged +{ + public abstract string Name { get; } + + public event PropertyChangedEventHandler? PropertyChanged; + protected void OnPropertyChanged([CallerMemberName] string? name = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); +} diff --git a/FModel/ViewModels/TabControlViewModel.cs b/FModel/ViewModels/TabControlViewModel.cs index dbeb44239..f3d70befd 100644 --- a/FModel/ViewModels/TabControlViewModel.cs +++ b/FModel/ViewModels/TabControlViewModel.cs @@ -5,6 +5,7 @@ using System.Threading; using System.Windows; using System.Windows.Media.Imaging; +using CUE4Parse_Conversion.Options; using CUE4Parse.FileProvider.Objects; using CUE4Parse.UE4.Assets.Exports.Texture; using CUE4Parse.Utils; @@ -113,7 +114,7 @@ private void SetImage(CTexture bitmap) else { ImageBuffer = imageData; - ExportName += "." + (NoAlpha ? "jpg" : "png"); + ExportName += "." + (NoAlpha || UserSettings.Default.TextureExportFormat == ETextureFormat.Jpeg ? "jpg" : "png"); } using var stream = new MemoryStream(imageData); diff --git a/FModel/ViewModels/UpdateViewModel.cs b/FModel/ViewModels/UpdateViewModel.cs index 26acdb88a..60ae9129a 100644 --- a/FModel/ViewModels/UpdateViewModel.cs +++ b/FModel/ViewModels/UpdateViewModel.cs @@ -63,7 +63,7 @@ private Task LoadCoAuthors() var coAuthorMap = new Dictionary>(); foreach (var commit in Commits) { - if (!commit.Commit.Message.Contains("Co-authored-by")) + if (!commit.Commit.Message.Contains("Co-authored-by", StringComparison.OrdinalIgnoreCase)) continue; var regex = GetCoAuthorRegex(); @@ -111,7 +111,7 @@ private Task LoadCoAuthors() foreach (var (commit, usernames) in coAuthorMap) { var coAuthors = usernames - .Where(username => authorCache.ContainsKey(username)) + .Where(authorCache.ContainsKey) .Select(username => authorCache[username]) .ToArray(); diff --git a/FModel/Views/AudioPlayer.xaml.cs b/FModel/Views/AudioPlayer.xaml.cs index 332b61015..f65660cea 100644 --- a/FModel/Views/AudioPlayer.xaml.cs +++ b/FModel/Views/AudioPlayer.xaml.cs @@ -75,6 +75,8 @@ private void OnPreviewKeyDown(object sender, KeyEventArgs e) _applicationView.AudioPlayer.Previous(); else if (UserSettings.Default.NextAudio.IsTriggered(e.Key)) _applicationView.AudioPlayer.Next(); + else if (UserSettings.Default.RemoveAudio.IsTriggered(e.Key)) + _applicationView.AudioPlayer.Remove(); } private void OnAudioFileMouseDoubleClick(object sender, MouseButtonEventArgs e) diff --git a/FModel/Views/ExportSessionWindow.xaml b/FModel/Views/ExportSessionWindow.xaml new file mode 100644 index 000000000..8e3f6b9df --- /dev/null +++ b/FModel/Views/ExportSessionWindow.xaml @@ -0,0 +1,534 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +