diff --git a/CUE4Parse b/CUE4Parse index 7afcbb32..c99a1d6d 160000 --- a/CUE4Parse +++ b/CUE4Parse @@ -1 +1 @@ -Subproject commit 7afcbb323c9fd9445d5452856b18b1d732d2dccd +Subproject commit c99a1d6dfc269281c2a7bf4dc36f8664dd8791e6 diff --git a/FModel/App.xaml.cs b/FModel/App.xaml.cs index fb282ea6..5cd426a3 100644 --- a/FModel/App.xaml.cs +++ b/FModel/App.xaml.cs @@ -129,11 +129,11 @@ public partial class App .WriteTo.Logger(lc => lc .Filter.ByExcluding(IsConversionLibrary) .WriteTo.Console(outputTemplate: template1, theme: AnsiConsoleTheme.Literate) - .WriteTo.File(outputTemplate: template1, path: filePath)) + .WriteTo.File(outputTemplate: template1, path: filePath, shared: true)) .WriteTo.Logger(lc => lc .Filter.ByIncludingOnly(IsConversionLibrary) .WriteTo.Console(outputTemplate: template2, theme: AnsiConsoleTheme.Literate) - .WriteTo.File(outputTemplate: template2, path: filePath)) + .WriteTo.File(outputTemplate: template2, path: filePath, shared: true)) .MinimumLevel.Override("CUE4Parse_Conversion", LogEventLevel.Verbose).WriteTo.Sink(ImGuiSink.Instance) .CreateLogger(); diff --git a/FModel/Enums.cs b/FModel/Enums.cs index ff8bc027..24de8230 100644 --- a/FModel/Enums.cs +++ b/FModel/Enums.cs @@ -130,6 +130,7 @@ public enum EAssetCategory : uint SkeletalMesh = Mesh + 2, CustomizableObject = Mesh + 3, NaniteDisplacedMesh = Mesh + 4, + GeometryCollection = Mesh + 5, Texture = AssetCategoryExtensions.CategoryBase + (3 << 16), Materials = AssetCategoryExtensions.CategoryBase + (4 << 16), Material = Materials + 1, @@ -165,6 +166,7 @@ public enum EAssetCategory : uint RocoKingdomWorld = GameSpecific + 3, DeltaForce = GameSpecific + 4, LegoBatman = GameSpecific + 5, + ArcSys = GameSpecific + 6, } public enum EUnluacMode diff --git a/FModel/Extensions/VisualTreeExtensions.cs b/FModel/Extensions/VisualTreeExtensions.cs new file mode 100644 index 00000000..c898336f --- /dev/null +++ b/FModel/Extensions/VisualTreeExtensions.cs @@ -0,0 +1,18 @@ +using System.Windows; +using System.Windows.Media; + +namespace FModel.Extensions; + +public static class VisualTreeExtensions +{ + public static T FindAncestor(this DependencyObject current) where T : DependencyObject + { + while (current != null) + { + if (current is T t) + return t; + current = VisualTreeHelper.GetParent(current); + } + return null; + } +} diff --git a/FModel/Framework/Hotkey.cs b/FModel/Framework/Hotkey.cs index cc984965..8bed78aa 100644 --- a/FModel/Framework/Hotkey.cs +++ b/FModel/Framework/Hotkey.cs @@ -27,11 +27,13 @@ public class Hotkey : ViewModel public bool IsTriggered(Key e) { - return e == Key && Keyboard.Modifiers.HasFlag(Modifiers); + return Key != Key.None && e == Key && Keyboard.Modifiers.HasFlag(Modifiers); } public override string ToString() { + if (Key == Key.None) return string.Empty; + var str = new StringBuilder(); if (Modifiers.HasFlag(ModifierKeys.Control)) @@ -46,4 +48,4 @@ public class Hotkey : ViewModel str.Append(Key); return str.ToString(); } -} \ No newline at end of file +} diff --git a/FModel/MainWindow.xaml.cs b/FModel/MainWindow.xaml.cs index 25e82893..2609d0a5 100644 --- a/FModel/MainWindow.xaml.cs +++ b/FModel/MainWindow.xaml.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.ComponentModel; using System.IO; using System.Linq; @@ -7,6 +8,7 @@ using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Input; +using FModel.Extensions; using FModel.Services; using FModel.Settings; using FModel.ViewModels; @@ -179,6 +181,22 @@ public partial class MainWindow _ => CategoriesSelector.SelectedIndex }; } + else if (_applicationView.Status.IsReady && UserSettings.Default.ExportData.IsTriggered(e.Key)) + OnExportHotkey("Save_Data"); + else if (_applicationView.Status.IsReady && UserSettings.Default.ExportProperties.IsTriggered(e.Key)) + OnExportHotkey("Save_Properties"); + else if (_applicationView.Status.IsReady && UserSettings.Default.ExportTextures.IsTriggered(e.Key)) + OnExportHotkey("Save_Textures"); + else if (_applicationView.Status.IsReady && UserSettings.Default.ExportModels.IsTriggered(e.Key)) + OnExportHotkey("Save_Models"); + else if (_applicationView.Status.IsReady && UserSettings.Default.ExportWorlds.IsTriggered(e.Key)) + OnExportHotkey("Save_Worlds"); + else if (_applicationView.Status.IsReady && UserSettings.Default.ExportAnimations.IsTriggered(e.Key)) + OnExportHotkey("Save_Animations"); + else if (_applicationView.Status.IsReady && UserSettings.Default.ExportAudio.IsTriggered(e.Key)) + OnExportHotkey("Save_Audio"); + else if (_applicationView.Status.IsReady && UserSettings.Default.ExportCode.IsTriggered(e.Key)) + OnExportHotkey("Save_Code"); else if (_applicationView.Status.IsReady && UserSettings.Default.FeaturePreviewNewAssetExplorer && UserSettings.Default.SwitchAssetExplorer.IsTriggered(e.Key)) _applicationView.IsAssetsExplorerVisible = !_applicationView.IsAssetsExplorerVisible; else if (UserSettings.Default.AssetAddTab.IsTriggered(e.Key)) @@ -379,6 +397,22 @@ public partial class MainWindow childFolder.IsSelected = true; } + private void OnExportHotkey(string trigger) + { + if (!_applicationView.Status.IsReady || Keyboard.FocusedElement is not DependencyObject focused) + return; + + IList selection = focused.FindAncestor() == AssetsFolderName + ? new[] { AssetsFolderName.SelectedItem } + : focused.FindAncestor()?.SelectedItems; + + var exportable = selection?.OfType().Where(static item => item is TreeItem or GameFileViewModel).ToArray() ?? []; + if (exportable.Length == 0) + return; + + _applicationView.RightClickMenuCommand.Execute(new object[] { trigger, exportable }); + } + private CustomPopupPlacement[] OnQueueToastCustomPopupPlacement(Size popupSize, Size targetSize, Point offset) { return diff --git a/FModel/Settings/UserSettings.cs b/FModel/Settings/UserSettings.cs index f937b6d2..aff57f4c 100644 --- a/FModel/Settings/UserSettings.cs +++ b/FModel/Settings/UserSettings.cs @@ -64,12 +64,12 @@ public sealed class UserSettings : ViewModel Default.TextureExportFormat, Default.TextureQuality, Default.SaveHdrTexturesAsHdr, + Default.ExportAllTextureMips, Default.MaterialExportFormat, Default.SaveEmbeddedMaterials, Default.SaveMorphTargets, Default.SocketExportFormat, - Default.CompressionFormat, - Default.ExportAllTextureMips + Default.CompressionFormat ); } @@ -431,6 +431,62 @@ public sealed class UserSettings : ViewModel set => SetProperty(ref _nextAudio, value); } + private Hotkey _exportData = new(Key.None); + public Hotkey ExportData + { + get => _exportData; + set => SetProperty(ref _exportData, value); + } + + private Hotkey _exportProperties = new(Key.None); + public Hotkey ExportProperties + { + get => _exportProperties; + set => SetProperty(ref _exportProperties, value); + } + + private Hotkey _exportTextures = new(Key.None); + public Hotkey ExportTextures + { + get => _exportTextures; + set => SetProperty(ref _exportTextures, value); + } + + private Hotkey _exportModels = new(Key.None); + public Hotkey ExportModels + { + get => _exportModels; + set => SetProperty(ref _exportModels, value); + } + + private Hotkey _exportWorlds = new(Key.None); + public Hotkey ExportWorlds + { + get => _exportWorlds; + set => SetProperty(ref _exportWorlds, value); + } + + private Hotkey _exportAnimations = new(Key.None); + public Hotkey ExportAnimations + { + get => _exportAnimations; + set => SetProperty(ref _exportAnimations, value); + } + + private Hotkey _exportAudio = new(Key.None); + public Hotkey ExportAudio + { + get => _exportAudio; + set => SetProperty(ref _exportAudio, value); + } + + private Hotkey _exportCode = new(Key.None); + public Hotkey ExportCode + { + get => _exportCode; + set => SetProperty(ref _exportCode, value); + } + private EMeshFormat _meshExportFormat = EMeshFormat.UEFormat; public EMeshFormat MeshExportFormat { diff --git a/FModel/ViewModels/CUE4ParseViewModel.cs b/FModel/ViewModels/CUE4ParseViewModel.cs index fd589621..528e01fd 100644 --- a/FModel/ViewModels/CUE4ParseViewModel.cs +++ b/FModel/ViewModels/CUE4ParseViewModel.cs @@ -48,6 +48,7 @@ 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.Assets.Objects; using CUE4Parse.UE4.BinaryConfig; using CUE4Parse.UE4.CriWare; using CUE4Parse.UE4.CriWare.Readers; @@ -86,6 +87,7 @@ using Serilog; using SkiaSharp; using Svg.Skia; using UE4Config.Parsing; +using static CUE4Parse.UE4.Versions.EGame; using Application = System.Windows.Application; using FGuid = CUE4Parse.UE4.Objects.Core.Misc.FGuid; @@ -192,24 +194,24 @@ public class CUE4ParseViewModel : ViewModel { Provider = versionContainer.Game switch { - EGame.GAME_StateOfDecay2 => new DefaultFileProvider(new DirectoryInfo(gameDirectory), + GAME_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), - EGame.GAME_eFootball => new DefaultFileProvider(new DirectoryInfo(gameDirectory), + GAME_eFootball => new DefaultFileProvider(new DirectoryInfo(gameDirectory), [ new(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData) + "\\KONAMI\\eFootball\\ST\\Download") ], SearchOption.AllDirectories, versionContainer, pathComparer), - EGame.GAME_DeadByDaylight => new DefaultFileProvider(new DirectoryInfo(gameDirectory), + GAME_DeadByDaylight => new DefaultFileProvider(new DirectoryInfo(gameDirectory), [ new(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + "\\DeadByDaylight\\Saved\\PersistentDownloadDir\\DynamicContent") ], SearchOption.AllDirectories, versionContainer, pathComparer), - EGame.GAME_AshEchoes => new AEDefaultFileProvider(gameDirectory, SearchOption.AllDirectories, versionContainer, pathComparer), - EGame.GAME_BlackStigma => new DefaultFileProvider(gameDirectory, SearchOption.AllDirectories, versionContainer, StringComparer.Ordinal), - EGame.GAME_HonorofKingsWorld => new HoKWDefaultFileProvider(gameDirectory, SearchOption.AllDirectories, versionContainer, pathComparer), - EGame.GAME_LordOfMysteries => new LoMDefaultFileProvider(gameDirectory, SearchOption.AllDirectories, versionContainer, pathComparer), - EGame.GAME_ArcRaiders => new TheiaFileProvider(gameDirectory, SearchOption.AllDirectories, versionContainer, pathComparer), + GAME_AshEchoes => new AEDefaultFileProvider(gameDirectory, SearchOption.AllDirectories, versionContainer, pathComparer), + GAME_BlackStigma => new DefaultFileProvider(gameDirectory, SearchOption.AllDirectories, versionContainer, StringComparer.Ordinal), + GAME_HonorofKingsWorld => new HoKWDefaultFileProvider(gameDirectory, SearchOption.AllDirectories, versionContainer, pathComparer), + GAME_LordOfMysteries => new LoMDefaultFileProvider(gameDirectory, SearchOption.AllDirectories, versionContainer, pathComparer), + GAME_ArcRaiders or GAME_Highguard or GAME_MARVELTokonFightingSouls => new TheiaFileProvider(gameDirectory, SearchOption.AllDirectories, versionContainer, pathComparer), _ => new DefaultFileProvider(gameDirectory, SearchOption.AllDirectories, versionContainer, pathComparer) }; @@ -514,7 +516,7 @@ public class CUE4ParseViewModel : ViewModel 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 + if (Provider.Versions.Game is GAME_UE4_LATEST or 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)); @@ -734,17 +736,17 @@ public class CUE4ParseViewModel : ViewModel break; } - case "dat" when Provider.Versions.Game is EGame.GAME_Aion2: + case "dat" when Provider.Versions.Game is GAME_Aion2: { ProcessAion2DatFile(entry, updateUi, saveProperties); break; } - case "bytes" when Provider.Versions.Game is EGame.GAME_RocoKingdomWorld: + case "bytes" when Provider.Versions.Game is GAME_RocoKingdomWorld: { ProcessRocoBinFile(entry, updateUi, saveProperties); break; } - case "dbc" when Provider.Versions.Game is EGame.GAME_AshesOfCreation: + case "dbc" when Provider.Versions.Game is GAME_AshesOfCreation: { ProcessCacheDBFile(entry, updateUi, saveProperties); break; @@ -820,8 +822,8 @@ public class CUE4ParseViewModel : ViewModel case "py": case "md": case "h": - case "non" when Provider.Versions.Game is EGame.GAME_RocoKingdomWorld: - case "cam" when Provider.Versions.Game is EGame.GAME_RocoKingdomWorld: + case "non" when Provider.Versions.Game is GAME_RocoKingdomWorld: + case "cam" when Provider.Versions.Game is GAME_RocoKingdomWorld: // Uncharted Waters Origin case "crn": case "uwt": @@ -841,7 +843,7 @@ public class CUE4ParseViewModel : ViewModel break; } - case "ebd" when Provider.Versions.Game is EGame.GAME_ArcRaiders: + case "ebd" when Provider.Versions.Game is GAME_ArcRaiders: case "json": case "Json": { @@ -982,7 +984,7 @@ public class CUE4ParseViewModel : ViewModel break; } - case "ustbin" when Provider.Versions.Game is EGame.GAME_DeltaForce: + case "ustbin" when Provider.Versions.Game is GAME_DeltaForce: { var archive = entry.CreateReader(); var ustbin = new FDeltaStringTable(archive); @@ -1453,6 +1455,11 @@ public class CUE4ParseViewModel : ViewModel SaveAndPlaySound(cancellationToken, outputPath, audioFormat, data, saveAudio, updateUi); } + if (Provider.Versions.Game is GAME_GearsofWarEDay && akMediaAsset.CustomGameData is FByteBulkData bulkData) + { + var shouldDecompress = UserSettings.Default.CompressedAudioMode is ECompressedAudio.PlayDecompressed; + SaveAndPlaySound(cancellationToken, outputPath, "WEM", bulkData.Data, saveAudio, updateUi); + } return false; } case UAkAudioEventData when (isNone || saveAudio) && pointer.Object.Value is UAkAudioEventData akAudioEventData: @@ -1470,6 +1477,12 @@ public class CUE4ParseViewModel : ViewModel SaveAndPlaySound(cancellationToken, outputPath, audioFormat, data, saveAudio, updateUi); } + if (Provider.Versions.Game is GAME_GearsofWarEDay && akMediaAsset.CustomGameData is FByteBulkData bulkData) + { + var audioName = akMediaAsset.MediaName ?? $"{akAudioEventData.Outer.Name} ({akMediaAsset.ID})"; + var outputPath = Path.Combine(TabControl.SelectedTab.Entry.PathWithoutExtension.Replace('\\', '/').SubstringBeforeLast('/'), audioName); + SaveAndPlaySound(cancellationToken, outputPath, "WEM", bulkData.Data, saveAudio, updateUi); + } } } return false; @@ -1487,7 +1500,7 @@ public class CUE4ParseViewModel : ViewModel // Borderlands 4 case UFaceFXAnimSet when (isNone || saveAudio) && pointer.Object.Value is UFaceFXAnimSet faceFXAnimSet: { - if (Provider.Versions.Game is not EGame.GAME_Borderlands4) + if (Provider.Versions.Game is not GAME_Borderlands4) return false; var ownerDirectory = WwiseProvider.GetOwnerDirectory(faceFXAnimSet); diff --git a/FModel/ViewModels/ExportOptionsViewModel.cs b/FModel/ViewModels/ExportOptionsViewModel.cs index 5f41ed1b..c2f808bc 100644 --- a/FModel/ViewModels/ExportOptionsViewModel.cs +++ b/FModel/ViewModels/ExportOptionsViewModel.cs @@ -61,7 +61,6 @@ public class ExportOptionsViewModel : ViewModel set { if (!SetProperty(ref field, value)) return; - RaisePropertyChanged(nameof(SocketSettingsEnabled)); RaisePropertyChanged(nameof(CompressionSettingsEnabled)); RaisePropertyChanged(nameof(TextureFormatsEnabled)); @@ -105,8 +104,6 @@ public class ExportOptionsViewModel : ViewModel set => SetProperty(ref field, value); } - public bool SocketSettingsEnabled => SelectedMeshFormat == EMeshFormat.ActorX; - public IEnumerable CompressionFormats { get; } = Enum.GetValues(); public EFileCompressionFormat SelectedCompressionFormat { @@ -228,11 +225,11 @@ public class ExportOptionsViewModel : ViewModel SelectedTextureFormat, TextureQuality, ExportHdrTexturesAsHdr, + ExportAllTextureMips, SelectedMaterialDepth, ExportMaterials, ExportMorphTargets, SelectedSocketFormat, - SelectedCompressionFormat, - ExportAllTextureMips + SelectedCompressionFormat ); } diff --git a/FModel/ViewModels/ExportSessionViewModel.cs b/FModel/ViewModels/ExportSessionViewModel.cs index 6d225ef1..d877fd40 100644 --- a/FModel/ViewModels/ExportSessionViewModel.cs +++ b/FModel/ViewModels/ExportSessionViewModel.cs @@ -1,8 +1,10 @@ using System; using System.Collections.Concurrent; +using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; using System.Diagnostics; +using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -15,6 +17,7 @@ using FModel.Extensions; using FModel.Framework; using FModel.Settings; using FModel.Views; +using FModel.Views.Resources.Controls; using FModel.Views.Snooper; using Serilog.Events; @@ -175,9 +178,9 @@ public class ExportSessionViewModel : ViewModel }); } - public async Task ExportAsync() + public async Task> ExportAsync() { - if (IsRunning || Session.TotalQueued == 0) return; + if (IsRunning || Session.TotalQueued == 0) return null; IsRunning = true; IsFinished = false; @@ -218,9 +221,10 @@ public class ExportSessionViewModel : ViewModel }); }); + IReadOnlyList results = null; try { - await Session.RunAsync(exportDirectory, exportOptions, progress, _cts.Token).ConfigureAwait(false); + results = await Session.RunAsync(exportDirectory, exportOptions, progress, _cts.Token).ConfigureAwait(false); } catch (OperationCanceledException) { @@ -237,14 +241,86 @@ public class ExportSessionViewModel : ViewModel UpdateElapsedAndEta(); }); } + return results; } public async Task ExportAutomaticallyAsync() { - if (UserSettings.Default.ExportImmediately) + if (!UserSettings.Default.ExportImmediately) return; + + var results = await ExportAsync(); + if (results is { Count: > 0 }) LogSummary(results); + } + + private static void LogSummary(IReadOnlyList results) + { + if (results.Count == 1) { - await ExportAsync(); + var result = results[0]; + switch (result.Success) + { + case true when result.DiskFilePaths is { Count: > 0 } files: + FLogger.Append(ELog.Information, () => + { + FLogger.Text("Successfully exported ", Constants.WHITE); + FLogger.Link(Path.GetFileName(files[0]), files[0], true); + }); + break; + case false when result.Error is { } exception: + FLogger.Append(exception); + break; + } + return; } + + var failed = 0; + var groups = new Dictionary(StringComparer.OrdinalIgnoreCase); + + foreach (var result in results) + { + if (!result.Success) + { + failed++; + continue; + } + + if (result.DiskFilePaths == null) continue; + + foreach (var file in result.DiskFilePaths) + { + var extension = Path.GetExtension(file).TrimStart('.'); + var source = SplitDirectory(result.ObjectPath); + var directory = SplitDirectory(file); + groups[extension] = groups.TryGetValue(extension, out var group) + ? (group.Count + 1, CommonPrefix(group.Source, source), CommonPrefix(group.Directory, directory)) + : (1, source, directory); + } + } + + foreach (var (extension, group) in groups) + { + var source = string.Join('/', group.Source); + var directory = string.Join(Path.DirectorySeparatorChar, group.Directory); + FLogger.Append(ELog.Information, () => + { + FLogger.Text($"Successfully exported {group.Count} {extension} from ", Constants.WHITE); + if (directory.Length > 0) FLogger.Link(source, directory, true); + else FLogger.Text(source, Constants.WHITE, true); + }); + } + + if (failed > 0) + { + FLogger.Append(ELog.Error, () => FLogger.Text($"Failed to export {failed} asset{(failed == 1 ? "" : "s")}, open the Export Session window for more details.", Constants.WHITE, true)); + } + } + + private static string[] SplitDirectory(string path) => Path.GetDirectoryName(path)?.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) ?? []; + private static string[] CommonPrefix(string[] a, string[] b) + { + var i = 0; + while (i < a.Length && i < b.Length && string.Equals(a[i], b[i], StringComparison.OrdinalIgnoreCase)) i++; + return i == a.Length ? a : a[..i]; } public void CancelExport() @@ -384,6 +460,7 @@ public class ClassGroupViewModel(string name) : ViewModel public class ObjectGroupViewModel(string path) : ViewModel { public string Path { get; } = path; + public string Directory { get; } = path.SubstringBeforeLast('/'); public string Name { get; } = path.SubstringAfterLast('.'); public ObservableCollection Entries { get; } = []; @@ -426,4 +503,29 @@ public class LogEntryViewModel(LogEvent log) _ => log.Exception?.Message ?? log.RenderMessage() }; public Exception? Exception { get; } = log.Exception; + public IReadOnlyList ExceptionDetails { get; } = + log.Exception is { } exception ? [new ExceptionDetailsViewModel(exception)] : []; +} + +public class ExceptionDetailsViewModel +{ + public string Header { get; } + public string Details { get; } + + public ExceptionDetailsViewModel(Exception exception) + { + var text = exception.ToString().ReplaceLineEndings("\n"); + var newline = text.IndexOf('\n'); + + if (newline < 0) + { + Header = text; + Details = string.Empty; + } + else + { + Header = text[..newline]; + Details = text[(newline + 1)..]; + } + } } diff --git a/FModel/ViewModels/GameFileViewModel.cs b/FModel/ViewModels/GameFileViewModel.cs index f01ac7bf..57a9456c 100644 --- a/FModel/ViewModels/GameFileViewModel.cs +++ b/FModel/ViewModels/GameFileViewModel.cs @@ -11,6 +11,7 @@ using CUE4Parse.GameTypes.Borderlands3.Assets.Exports; using CUE4Parse.GameTypes.Borderlands4.Assets.Exports; using CUE4Parse.GameTypes.FN.Assets.Exports.DataAssets; using CUE4Parse.GameTypes.LegoBatman.Assets; +using CUE4Parse.GameTypes.RED.Assets.Exports; using CUE4Parse.GameTypes.SMG.UE4.Assets.Exports.Wwise; using CUE4Parse.GameTypes.SMG.UE4.Assets.Objects; using CUE4Parse.GameTypes.SquareEnix.UE4.Assets.Exports; @@ -26,6 +27,7 @@ using CUE4Parse.UE4.Assets.Exports.Engine.Font; using CUE4Parse.UE4.Assets.Exports.Fmod; using CUE4Parse.UE4.Assets.Exports.FMod; using CUE4Parse.UE4.Assets.Exports.Foliage; +using CUE4Parse.UE4.Assets.Exports.GeometryCollection; using CUE4Parse.UE4.Assets.Exports.Internationalization; using CUE4Parse.UE4.Assets.Exports.LevelSequence; using CUE4Parse.UE4.Assets.Exports.Material; @@ -217,6 +219,7 @@ public class GameFileViewModel(GameFile asset) : ViewModel USkeletalMesh => (EAssetCategory.SkeletalMesh, EBulkType.Meshes), UCustomizableObject => (EAssetCategory.CustomizableObject, EBulkType.None), UNaniteDisplacedMesh => (EAssetCategory.NaniteDisplacedMesh, EBulkType.None), + UGeometryCollection => (EAssetCategory.GeometryCollection, EBulkType.None), UTexture => (EAssetCategory.Texture, EBulkType.Textures), @@ -265,6 +268,7 @@ public class GameFileViewModel(GameFile asset) : ViewModel UGbxGraphAsset or UDialogScriptData or UDialogPerformanceData when GameVersion is EGame.GAME_Borderlands4 or EGame.GAME_Borderlands3 => (EAssetCategory.Borderlands, EBulkType.Audio), // Borderlands 4; Borderlands 3; UFaceFXAnimSet when GameVersion is EGame.GAME_Borderlands4 => (EAssetCategory.Borderlands, EBulkType.Audio), // Borderlands 4; UWubAudioEvent or UWubDialogueEvent when GameVersion is EGame.GAME_LEGOBatmanLegacyoftheDarkKnight => (EAssetCategory.LegoBatman, EBulkType.Audio), // Lego Batman: Legacy of the Dark Knight; + UREDBinaryObject => (EAssetCategory.ArcSys, EBulkType.None), // Arc System Works games; _ => (EAssetCategory.All, EBulkType.None), }; diff --git a/FModel/ViewModels/TabControlViewModel.cs b/FModel/ViewModels/TabControlViewModel.cs index 481e52be..a2b7ddf7 100644 --- a/FModel/ViewModels/TabControlViewModel.cs +++ b/FModel/ViewModels/TabControlViewModel.cs @@ -517,8 +517,16 @@ public class TabControlViewModel : ViewModel } public event EventHandler OnTabRemove; - public void GoLeftTab() => SelectedTab = _tabItems.Previous(SelectedTab); - public void GoRightTab() => SelectedTab = _tabItems.Next(SelectedTab); + public void GoLeftTab() + { + if (_tabItems.Count > 0) + SelectedTab = _tabItems.Previous(SelectedTab); + } + public void GoRightTab() + { + if (_tabItems.Count > 0) + SelectedTab = _tabItems.Next(SelectedTab); + } public void RemoveOtherTabs(TabItem tab) { diff --git a/FModel/Views/ExportSessionWindow.xaml b/FModel/Views/ExportSessionWindow.xaml index bf1cb9af..61a6f26b 100644 --- a/FModel/Views/ExportSessionWindow.xaml +++ b/FModel/Views/ExportSessionWindow.xaml @@ -131,8 +131,25 @@ + + - + @@ -182,7 +199,6 @@ - @@ -196,11 +212,19 @@ - + + + + + - -