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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/FModel/Views/ExportSessionWindow.xaml.cs b/FModel/Views/ExportSessionWindow.xaml.cs
new file mode 100644
index 000000000..59966603d
--- /dev/null
+++ b/FModel/Views/ExportSessionWindow.xaml.cs
@@ -0,0 +1,67 @@
+using System.Diagnostics;
+using System.IO;
+using System.Windows;
+using System.Windows.Controls;
+using FModel.ViewModels;
+
+namespace FModel.Views;
+
+public partial class ExportSessionWindow
+{
+ public ExportSessionWindow()
+ {
+ InitializeComponent();
+ }
+
+ private async void OnExportClick(object sender, RoutedEventArgs e)
+ {
+ if (sender is Button { DataContext: ExportSessionViewModel { CanExport: true } viewModel })
+ await viewModel.ExportAsync();
+ }
+
+ private void OnOkClick(object sender, RoutedEventArgs e)
+ {
+ Close();
+ }
+
+ private void OnCancelClick(object sender, RoutedEventArgs e)
+ {
+ if (sender is Button { DataContext: ExportSessionViewModel viewModel })
+ viewModel.CancelExport();
+ }
+
+ private void OnClearQueueClick(object sender, RoutedEventArgs e)
+ {
+ if (sender is Button { DataContext: ExportSessionViewModel viewModel })
+ viewModel.ClearQueue();
+ }
+
+ private void OnMakeDefaultOptions(object sender, RoutedEventArgs e)
+ {
+ if (sender is Button { DataContext: ExportSessionViewModel viewModel })
+ viewModel.Options.SaveAsUserDefaults();
+ }
+
+ private void OnResetOptions(object sender, RoutedEventArgs e)
+ {
+ if (sender is Button { DataContext: ExportSessionViewModel viewModel })
+ viewModel.Options.ResetToUserDefaults();
+ }
+
+ private void OnOpenInExplorerClick(object sender, RoutedEventArgs e)
+ {
+ if (sender is not Button { Tag: string path }) return;
+
+ try
+ {
+ if (File.Exists(path) || Directory.Exists(path))
+ {
+ Process.Start("explorer.exe", $"/select,\"{path}\"");
+ }
+ }
+ catch
+ {
+ //
+ }
+ }
+}
diff --git a/FModel/Views/Resources/Controls/Aup/SourceEventArgs.cs b/FModel/Views/Resources/Controls/Aup/SourceEventArgs.cs
index 102546ea5..964638b7b 100644
--- a/FModel/Views/Resources/Controls/Aup/SourceEventArgs.cs
+++ b/FModel/Views/Resources/Controls/Aup/SourceEventArgs.cs
@@ -4,7 +4,8 @@ namespace FModel.Views.Resources.Controls.Aup;
public enum ESourceEventType
{
- Loading
+ Loading,
+ Clearing
}
public class SourceEventArgs : EventArgs
@@ -15,4 +16,4 @@ public SourceEventArgs(ESourceEventType e)
{
Event = e;
}
-}
\ No newline at end of file
+}
diff --git a/FModel/Views/Resources/Controls/Aup/SpectrumAnalyzer.cs b/FModel/Views/Resources/Controls/Aup/SpectrumAnalyzer.cs
index b1f5a6cee..20898e574 100644
--- a/FModel/Views/Resources/Controls/Aup/SpectrumAnalyzer.cs
+++ b/FModel/Views/Resources/Controls/Aup/SpectrumAnalyzer.cs
@@ -121,7 +121,7 @@ private void OnSourceChanged(ISource oldValue, ISource newValue)
private void OnSourceEvent(object sender, SourceEventArgs e)
{
- if (e.Event != ESourceEventType.Loading) return;
+ if (e is not { Event: ESourceEventType.Loading } g) return;
_spectrumProvider = Source.Spectrum;
UpdateFrequencyMapping();
}
diff --git a/FModel/Views/Resources/Controls/Aup/Timeclock.cs b/FModel/Views/Resources/Controls/Aup/Timeclock.cs
index eab86f969..681a56186 100644
--- a/FModel/Views/Resources/Controls/Aup/Timeclock.cs
+++ b/FModel/Views/Resources/Controls/Aup/Timeclock.cs
@@ -116,9 +116,9 @@ private void OnSourceChanged(ISource oldValue, ISource newValue)
private void OnSourceEvent(object sender, SourceEventArgs e)
{
- if (Source == null) return;
+ if (e is not { Event: ESourceEventType.Loading } || Source == null) return;
Label = Source.PlayedFile.FileName;
- Dispatcher.BeginInvoke((Action) CalculateTime);
+ Dispatcher.BeginInvoke(CalculateTime);
}
private void OnSourcePropertyChangedEvent(object sender, SourcePropertyChangedEventArgs e)
diff --git a/FModel/Views/Resources/Controls/Aup/Timeline.cs b/FModel/Views/Resources/Controls/Aup/Timeline.cs
index 07500fe0f..b77593fea 100644
--- a/FModel/Views/Resources/Controls/Aup/Timeline.cs
+++ b/FModel/Views/Resources/Controls/Aup/Timeline.cs
@@ -100,8 +100,8 @@ private void OnSourceChanged(ISource oldValue, ISource newValue)
private void OnSourceEvent(object sender, SourceEventArgs e)
{
- if (Source == null) return;
- Dispatcher.BeginInvoke((Action) UpdateTimeline);
+ if (e is not { Event: ESourceEventType.Loading } || Source == null) return;
+ Dispatcher.BeginInvoke(UpdateTimeline);
}
private void OnSourcePropertyChangedEvent(object sender, SourcePropertyChangedEventArgs e)
@@ -347,4 +347,4 @@ private void UpdateTimeline()
}
}
}
-}
\ No newline at end of file
+}
diff --git a/FModel/Views/Resources/Controls/ContextMenus/FileContextMenu.xaml b/FModel/Views/Resources/Controls/ContextMenus/FileContextMenu.xaml
index 28196a502..afba4a085 100644
--- a/FModel/Views/Resources/Controls/ContextMenus/FileContextMenu.xaml
+++ b/FModel/Views/Resources/Controls/ContextMenus/FileContextMenu.xaml
@@ -110,146 +110,189 @@
-