diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f0389930b..09f4d0d28 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -30,7 +30,7 @@ jobs: run: dotnet restore FModel - name: .NET Publish - run: dotnet publish FModel -c Release --no-self-contained -r win-x64 -f net8.0-windows -o "./FModel/bin/Publish/" -p:PublishReadyToRun=false -p:PublishSingleFile=true -p:DebugType=None -p:GenerateDocumentationFile=false -p:DebugSymbols=false -p:AssemblyVersion=${{ github.event.inputs.appVersion }} -p:FileVersion=${{ github.event.inputs.appVersion }} + run: dotnet publish FModel -c Release --no-self-contained -r win-x64 -f net10.0-windows -o "./FModel/bin/Publish/" -p:PublishReadyToRun=false -p:PublishSingleFile=true -p:DebugType=None -p:GenerateDocumentationFile=false -p:DebugSymbols=false -p:AssemblyVersion=${{ github.event.inputs.appVersion }} -p:FileVersion=${{ github.event.inputs.appVersion }} - name: ZIP File uses: papeloto/action-zip@v1.2 diff --git a/CUE4Parse b/CUE4Parse index c99a1d6df..bfd4e2cf1 160000 --- a/CUE4Parse +++ b/CUE4Parse @@ -1 +1 @@ -Subproject commit c99a1d6dfc269281c2a7bf4dc36f8664dd8791e6 +Subproject commit bfd4e2cf1fb2bc6ef83b5e5b0e1e870e732ffccb diff --git a/FModel/App.xaml.cs b/FModel/App.xaml.cs index 5cd426a36..5000293d6 100644 --- a/FModel/App.xaml.cs +++ b/FModel/App.xaml.cs @@ -1,18 +1,17 @@ -using AdonisUI.Controls; -using Microsoft.Win32; -using Serilog; using System; using System.Globalization; using System.IO; using System.Runtime.InteropServices; using System.Windows; using System.Windows.Threading; -using CUE4Parse; +using AdonisUI.Controls; using FModel.Framework; using FModel.Services; using FModel.Settings; using FModel.Views.Snooper; +using Microsoft.Win32; using Newtonsoft.Json; +using Serilog; using Serilog.Events; using Serilog.Sinks.SystemConsole.Themes; using MessageBox = AdonisUI.Controls.MessageBox; diff --git a/FModel/Creator/Bases/FN/BaseBundle.cs b/FModel/Creator/Bases/FN/BaseBundle.cs index 90c9de2b0..eab425aa3 100644 --- a/FModel/Creator/Bases/FN/BaseBundle.cs +++ b/FModel/Creator/Bases/FN/BaseBundle.cs @@ -4,9 +4,7 @@ using CUE4Parse.UE4.Objects.Core.i18N; using CUE4Parse.UE4.Objects.UObject; using CUE4Parse.Utils; -using FModel.Framework; using SkiaSharp; -using SkiaSharp.HarfBuzz; namespace FModel.Creator.Bases.FN; diff --git a/FModel/Creator/Bases/FN/BaseIcon.cs b/FModel/Creator/Bases/FN/BaseIcon.cs index cd57824c9..d669aa1fc 100644 --- a/FModel/Creator/Bases/FN/BaseIcon.cs +++ b/FModel/Creator/Bases/FN/BaseIcon.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Windows; +using CUE4Parse_Conversion.Textures; using CUE4Parse.GameTypes.FN.Enums; using CUE4Parse.UE4.Assets.Exports; using CUE4Parse.UE4.Assets.Exports.Engine; @@ -12,7 +13,6 @@ using CUE4Parse.UE4.Objects.Core.Math; using CUE4Parse.UE4.Objects.GameplayTags; using CUE4Parse.UE4.Objects.UObject; -using CUE4Parse_Conversion.Textures; using FModel.Settings; using SkiaSharp; diff --git a/FModel/Creator/Bases/FN/BaseQuest.cs b/FModel/Creator/Bases/FN/BaseQuest.cs index 6d2e9b876..b1ea74076 100644 --- a/FModel/Creator/Bases/FN/BaseQuest.cs +++ b/FModel/Creator/Bases/FN/BaseQuest.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using CUE4Parse.UE4.Assets.Exports; using CUE4Parse.UE4.Assets.Exports.Engine; using CUE4Parse.UE4.Assets.Exports.Material; @@ -9,9 +8,7 @@ using CUE4Parse.UE4.Objects.Core.i18N; using CUE4Parse.UE4.Objects.UObject; using CUE4Parse.Utils; -using FModel.Framework; using SkiaSharp; -using SkiaSharp.HarfBuzz; namespace FModel.Creator.Bases.FN; diff --git a/FModel/Creator/Bases/FN/BaseUserControl.cs b/FModel/Creator/Bases/FN/BaseUserControl.cs index a4e5bd860..1cc063d47 100644 --- a/FModel/Creator/Bases/FN/BaseUserControl.cs +++ b/FModel/Creator/Bases/FN/BaseUserControl.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Globalization; using CUE4Parse.UE4.Assets.Exports; using CUE4Parse.UE4.Assets.Objects; using CUE4Parse.UE4.Objects.Core.i18N; diff --git a/FModel/Creator/Bases/MV/BaseFighter.cs b/FModel/Creator/Bases/MV/BaseFighter.cs index 8dd0ca3b7..ec6374037 100644 --- a/FModel/Creator/Bases/MV/BaseFighter.cs +++ b/FModel/Creator/Bases/MV/BaseFighter.cs @@ -1,7 +1,7 @@ using System; -using System.Linq; using System.Collections.Generic; using System.ComponentModel; +using System.Linq; using CUE4Parse.UE4.Assets.Exports; using CUE4Parse.UE4.Assets.Exports.Engine; using CUE4Parse.UE4.Assets.Exports.Material; @@ -9,7 +9,6 @@ using CUE4Parse.UE4.Objects.Core.i18N; using CUE4Parse.UE4.Objects.Core.Math; using CUE4Parse.UE4.Objects.UObject; -using FModel.Extensions; using SkiaSharp; namespace FModel.Creator.Bases.MV; diff --git a/FModel/Creator/CreatorPackage.cs b/FModel/Creator/CreatorPackage.cs index 82f814270..667516b26 100644 --- a/FModel/Creator/CreatorPackage.cs +++ b/FModel/Creator/CreatorPackage.cs @@ -5,6 +5,7 @@ using FModel.Creator.Bases; using FModel.Creator.Bases.FN; using FModel.Creator.Bases.MV; +using BaseQuest = FModel.Creator.Bases.FN.BaseQuest; namespace FModel.Creator; @@ -214,7 +215,7 @@ when _pkgName.Contains("/MI_OfferImages/", StringComparison.OrdinalIgnoreCase) | case "FortQuestItemDefinition_Campaign": case "AthenaDailyQuestDefinition": case "FortUrgentQuestItemDefinition": - creator = new Bases.FN.BaseQuest(_object.Value, _style); + creator = new BaseQuest(_object.Value, _style); return true; case "FortCompendiumItemDefinition": case "FortCompendiumBundleDefinition": diff --git a/FModel/Creator/Utils.cs b/FModel/Creator/Utils.cs index 3d44c40e8..8d88db494 100644 --- a/FModel/Creator/Utils.cs +++ b/FModel/Creator/Utils.cs @@ -5,16 +5,16 @@ using System.Linq; using System.Text; using System.Text.RegularExpressions; +using CUE4Parse_Conversion.Textures; using CUE4Parse.UE4.Assets.Exports; using CUE4Parse.UE4.Assets.Exports.Material; using CUE4Parse.UE4.Assets.Exports.Texture; +using CUE4Parse.UE4.Assets.Objects; using CUE4Parse.UE4.Objects.UObject; using CUE4Parse.UE4.Versions; -using CUE4Parse_Conversion.Textures; -using CUE4Parse.UE4.Assets.Objects; using CUE4Parse.Utils; -using FModel.Framework; using FModel.Extensions; +using FModel.Framework; using FModel.Services; using FModel.Settings; using FModel.ViewModels; diff --git a/FModel/Extensions/AvalonExtensions.cs b/FModel/Extensions/AvalonExtensions.cs index 6cc7968ab..0faf65344 100644 --- a/FModel/Extensions/AvalonExtensions.cs +++ b/FModel/Extensions/AvalonExtensions.cs @@ -2,6 +2,7 @@ using System.Runtime.CompilerServices; using System.Xml; using FModel.Extensions.Themes; +using FModel.Settings; using ICSharpCode.AvalonEdit.Highlighting; using ICSharpCode.AvalonEdit.Highlighting.Xshd; @@ -61,7 +62,7 @@ public static IHighlightingDefinition HighlighterSelector(string ext) case "po": return null; default: - _jsonHighlighter.ApplyJsonTheme(Settings.UserSettings.Default.JsonHighlightTheme); + _jsonHighlighter.ApplyJsonTheme(UserSettings.Default.JsonHighlightTheme); return _jsonHighlighter; } } diff --git a/FModel/Extensions/ClipboardExtensions.cs b/FModel/Extensions/ClipboardExtensions.cs index 571e91567..34bf83505 100644 --- a/FModel/Extensions/ClipboardExtensions.cs +++ b/FModel/Extensions/ClipboardExtensions.cs @@ -1,4 +1,3 @@ -using SkiaSharp; using System; using System.Drawing; using System.Drawing.Imaging; @@ -6,6 +5,7 @@ using System.Runtime.CompilerServices; using System.Text; using System.Windows; +using SkiaSharp; namespace FModel.Extensions; @@ -195,4 +195,4 @@ private static string GenerateHTMLFragment(string html) return sb.ToString(); } -} \ No newline at end of file +} diff --git a/FModel/Extensions/Themes/HighlightingThemeExtensions.cs b/FModel/Extensions/Themes/HighlightingThemeExtensions.cs index 8edbac2d2..d8782102b 100644 --- a/FModel/Extensions/Themes/HighlightingThemeExtensions.cs +++ b/FModel/Extensions/Themes/HighlightingThemeExtensions.cs @@ -1,7 +1,6 @@ using System.Windows; using System.Windows.Media; using ICSharpCode.AvalonEdit.Highlighting; -using static FModel.Extensions.AvalonExtensions; using static FModel.Extensions.Themes.JsonHighlightThemes; namespace FModel.Extensions.Themes; diff --git a/FModel/FModel.csproj b/FModel/FModel.csproj index 1bfc1c33e..71000b161 100644 --- a/FModel/FModel.csproj +++ b/FModel/FModel.csproj @@ -6,24 +6,20 @@ true FModel.ico 4.4.4.0 - 4.4.4.0 - 4.4.4.0 false true - win-x64 x64 true - true + enable + en + en-US + NU1701 FModel.App - - 1701;1702;NU1701 - - - + true - NU1701 + true @@ -31,127 +27,9 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + @@ -166,6 +44,7 @@ + @@ -185,88 +64,9 @@ - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/FModel/Framework/ImGuiController.cs b/FModel/Framework/ImGuiController.cs index 0768de97f..7514537a5 100644 --- a/FModel/Framework/ImGuiController.cs +++ b/FModel/Framework/ImGuiController.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; -using System.Numerics; +using System.Reflection; using System.Runtime.InteropServices; using System.Windows; using System.Windows.Forms; @@ -10,10 +10,12 @@ using ImGuiNET; using ImGuizmoNET; using OpenTK.Graphics.OpenGL4; +using OpenTK.Mathematics; using OpenTK.Windowing.Desktop; using OpenTK.Windowing.GraphicsLibraryFramework; using ErrorCode = OpenTK.Graphics.OpenGL4.ErrorCode; using Keys = OpenTK.Windowing.GraphicsLibraryFramework.Keys; +using Vector2 = System.Numerics.Vector2; namespace FModel.Framework; @@ -67,7 +69,7 @@ public ImGuiController(int width, int height) io.NativePtr->IniFilename = (byte*)iniFileNamePtr; } - var assembly = System.Reflection.Assembly.GetExecutingAssembly(); + var assembly = Assembly.GetExecutingAssembly(); var assemblyName = assembly.GetName().Name; byte[] LoadFont(string name) { @@ -421,7 +423,7 @@ private void RenderImDrawData(ImDrawDataPtr draw_data) // Setup orthographic projection matrix into our constant buffer ImGuiIOPtr io = ImGui.GetIO(); - var mvp = OpenTK.Mathematics.Matrix4.CreateOrthographicOffCenter( + var mvp = Matrix4.CreateOrthographicOffCenter( 0.0f, io.DisplaySize.X, io.DisplaySize.Y, diff --git a/FModel/Framework/SerilogEnricher.cs b/FModel/Framework/SerilogEnricher.cs index 3567d5d0d..adbc9c518 100644 --- a/FModel/Framework/SerilogEnricher.cs +++ b/FModel/Framework/SerilogEnricher.cs @@ -1,6 +1,6 @@ -using System; using System.Diagnostics; using System.Reflection; +using Serilog; using Serilog.Core; using Serilog.Events; @@ -14,7 +14,7 @@ protected bool TryGetCaller(out MethodBase method) { method = null; - var serilogAssembly = typeof(Serilog.Log).Assembly; + var serilogAssembly = typeof(Log).Assembly; var stack = new StackTrace(3); foreach (var frame in stack.GetFrames()) diff --git a/FModel/MainWindow.xaml b/FModel/MainWindow.xaml index 6c63f0d73..176566575 100644 --- a/FModel/MainWindow.xaml +++ b/FModel/MainWindow.xaml @@ -295,6 +295,16 @@ + + + @@ -307,14 +317,83 @@ - - + + + + + + + + + + + - - - - + + + + + + + + + + + + + + + + + + + + + + @@ -384,8 +463,7 @@ Style="{StaticResource AssetsFolderTreeView}" SelectedItemChanged="OnAssetsTreeSelectedItemChanged" PreviewKeyDown="OnFoldersPreviewKeyDown" - PreviewMouseDoubleClick="OnAssetsTreeMouseDoubleClick"> - + PreviewMouseDoubleClick="OnAssetsTreeMouseDoubleClick" /> @@ -405,12 +483,98 @@ - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -455,17 +619,128 @@ - - - + + + + + + + + + + - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/FModel/MainWindow.xaml.cs b/FModel/MainWindow.xaml.cs index 2609d0a57..693bf6ad4 100644 --- a/FModel/MainWindow.xaml.cs +++ b/FModel/MainWindow.xaml.cs @@ -1,7 +1,6 @@ using System; using System.Collections; using System.ComponentModel; -using System.IO; using System.Linq; using System.Threading.Tasks; using System.Windows; @@ -120,6 +119,7 @@ await Task.WhenAll( await Task.WhenAll( _applicationView.CUE4Parse.VerifyConsoleVariables(), _applicationView.CUE4Parse.VerifyOnDemandArchives(), + _applicationView.CUE4Parse.VerifyCloudArchives(), _applicationView.CUE4Parse.InitMappings(), ApplicationViewModel.InitDetex(), ApplicationViewModel.InitVgmStream(), @@ -131,12 +131,6 @@ await Task.WhenAll( }), UserSettings.Default.DecompileLua ? ApplicationViewModel.InitUnluac() : Task.CompletedTask ).ConfigureAwait(false); - -#if DEBUG - // await _threadWorkerView.Begin(cancellationToken => - // _applicationView.CUE4Parse.Extract(cancellationToken, - // _applicationView.CUE4Parse.Provider["Marvel/Content/Marvel/Wwise/Assets/Events/Music/music_new/event/Entry.uasset"])); -#endif } private void OnGridSplitterDoubleClick(object sender, MouseButtonEventArgs e) @@ -146,7 +140,7 @@ private void OnGridSplitterDoubleClick(object sender, MouseButtonEventArgs e) private void OnWindowKeyDown(object sender, KeyEventArgs e) { - if (e.OriginalSource is TextBox || e.OriginalSource is TextArea && Keyboard.Modifiers.HasFlag(ModifierKeys.Control)) + if (e.OriginalSource is TextBox || (e.OriginalSource is TextArea && Keyboard.Modifiers.HasFlag(ModifierKeys.Control))) return; if (_threadWorkerView.CanBeCanceled && e.Key == Key.Escape) diff --git a/FModel/Settings/UserSettings.cs b/FModel/Settings/UserSettings.cs index aff57f4ce..0dbf6c0bf 100644 --- a/FModel/Settings/UserSettings.cs +++ b/FModel/Settings/UserSettings.cs @@ -3,11 +3,11 @@ using System.IO; using System.Windows; using System.Windows.Input; -using CUE4Parse.UE4.Assets.Exports.Material; -using CUE4Parse.UE4.Versions; using CUE4Parse_Conversion.Options; using CUE4Parse_Conversion.Writers.UEFormat.Enums; +using CUE4Parse.UE4.Assets.Exports.Material; using CUE4Parse.UE4.Lua.unluac; +using CUE4Parse.UE4.Versions; using FModel.Extensions.Themes; using FModel.Framework; using FModel.ViewModels; diff --git a/FModel/ViewModels/ApiEndpoints/EpicApiEndpoint.cs b/FModel/ViewModels/ApiEndpoints/EpicApiEndpoint.cs index 57a8c57aa..542400b58 100644 --- a/FModel/ViewModels/ApiEndpoints/EpicApiEndpoint.cs +++ b/FModel/ViewModels/ApiEndpoints/EpicApiEndpoint.cs @@ -1,14 +1,10 @@ using System.Threading; using System.Threading.Tasks; - using EpicManifestParser.Api; - using FModel.Framework; using FModel.Settings; using FModel.ViewModels.ApiEndpoints.Models; - using RestSharp; - using Serilog; namespace FModel.ViewModels.ApiEndpoints; diff --git a/FModel/ViewModels/ApiEndpoints/FModelApiEndpoint.cs b/FModel/ViewModels/ApiEndpoints/FModelApiEndpoint.cs index db2882ed6..a3ceb9536 100644 --- a/FModel/ViewModels/ApiEndpoints/FModelApiEndpoint.cs +++ b/FModel/ViewModels/ApiEndpoints/FModelApiEndpoint.cs @@ -1,9 +1,9 @@ using System; -using AdonisUI.Controls; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using System.Windows; +using AdonisUI.Controls; using AutoUpdaterDotNET; using CUE4Parse.Utils; using FModel.Extensions; @@ -18,6 +18,7 @@ using MessageBox = AdonisUI.Controls.MessageBox; using MessageBoxButton = AdonisUI.Controls.MessageBoxButton; using MessageBoxImage = AdonisUI.Controls.MessageBoxImage; +using Version = System.Version; namespace FModel.ViewModels.ApiEndpoints; @@ -139,7 +140,7 @@ private void CheckForUpdateEvent(UpdateInfoEventArgs args) return; } - var currentVersion = new System.Version(args.CurrentVersion); + var currentVersion = new Version(args.CurrentVersion); UserSettings.Default.ShowChangelog = currentVersion != args.InstalledVersion; const string message = "A new update is available!"; diff --git a/FModel/ViewModels/ApiEndpoints/FortniteApiEndpoint.cs b/FModel/ViewModels/ApiEndpoints/FortniteApiEndpoint.cs index 34028ba8e..87f59a90d 100644 --- a/FModel/ViewModels/ApiEndpoints/FortniteApiEndpoint.cs +++ b/FModel/ViewModels/ApiEndpoints/FortniteApiEndpoint.cs @@ -1,8 +1,8 @@ using System; -using FModel.ViewModels.ApiEndpoints.Models; -using RestSharp; using System.Threading.Tasks; using FModel.Framework; +using FModel.ViewModels.ApiEndpoints.Models; +using RestSharp; namespace FModel.ViewModels.ApiEndpoints; diff --git a/FModel/ViewModels/ApiEndpoints/Models/FModelResponse.cs b/FModel/ViewModels/ApiEndpoints/Models/FModelResponse.cs index 598f070c3..d9fd084f6 100644 --- a/FModel/ViewModels/ApiEndpoints/Models/FModelResponse.cs +++ b/FModel/ViewModels/ApiEndpoints/Models/FModelResponse.cs @@ -30,6 +30,11 @@ public class ManifestInfoDilly [J] public string DownloadUrl { get; private set; } } +public class CloudContent +{ + public string ManifestPath { get; private set; } +} + public class Donator { [J] public string Username { get; private set; } diff --git a/FModel/ViewModels/ApiEndpoints/ValorantApiEndpoint.cs b/FModel/ViewModels/ApiEndpoints/ValorantApiEndpoint.cs index 7cc156609..095d728e7 100644 --- a/FModel/ViewModels/ApiEndpoints/ValorantApiEndpoint.cs +++ b/FModel/ViewModels/ApiEndpoints/ValorantApiEndpoint.cs @@ -8,13 +8,10 @@ using System.Text; using System.Threading; using System.Threading.Tasks; - using CUE4Parse.Compression; using CUE4Parse.UE4.Exceptions; using CUE4Parse.UE4.Readers; - using FModel.Framework; -using FModel.Settings; using OffiUtils; using RestSharp; diff --git a/FModel/ViewModels/ApplicationViewModel.cs b/FModel/ViewModels/ApplicationViewModel.cs index 242379f93..55483f300 100644 --- a/FModel/ViewModels/ApplicationViewModel.cs +++ b/FModel/ViewModels/ApplicationViewModel.cs @@ -4,6 +4,7 @@ using System.IO; using System.IO.Compression; using System.Linq; +using System.Runtime.Intrinsics.X86; using System.Threading.Tasks; using System.Windows; using CUE4Parse_Conversion.Textures.BC; @@ -22,7 +23,6 @@ using MessageBox = AdonisUI.Controls.MessageBox; using MessageBoxButton = AdonisUI.Controls.MessageBoxButton; using MessageBoxImage = AdonisUI.Controls.MessageBoxImage; -using System.Runtime.Intrinsics.X86; namespace FModel.ViewModels; diff --git a/FModel/ViewModels/AssetsFolderViewModel.cs b/FModel/ViewModels/AssetsFolderViewModel.cs index 845a930a0..fb257b563 100644 --- a/FModel/ViewModels/AssetsFolderViewModel.cs +++ b/FModel/ViewModels/AssetsFolderViewModel.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.ComponentModel; -using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; diff --git a/FModel/ViewModels/AudioPlayerViewModel.cs b/FModel/ViewModels/AudioPlayerViewModel.cs index ee9f2af9e..e014351f0 100644 --- a/FModel/ViewModels/AudioPlayerViewModel.cs +++ b/FModel/ViewModels/AudioPlayerViewModel.cs @@ -191,7 +191,14 @@ public AudioFile SelectedAudioFile public MMDevice SelectedAudioDevice { get => _selectedAudioDevice; - set => SetProperty(ref _selectedAudioDevice, value); + set + { + if (SetProperty(ref _selectedAudioDevice, value)) + { + UserSettings.Default.AudioDeviceId = value?.DeviceID; + UserSettings.Save(); + } + } } private AudioCommand _audioCommand; @@ -213,13 +220,21 @@ public AudioPlayerViewModel() var audioDevices = new ObservableCollection(EnumerateDevices()); AudioDevicesView = new ListCollectionView(audioDevices) { SortDescriptions = { new SortDescription("FriendlyName", ListSortDirection.Ascending) } }; - SelectedAudioDevice ??= audioDevices.FirstOrDefault(); + SelectedAudioDevice = audioDevices.FirstOrDefault(x => x.DeviceID == UserSettings.Default.AudioDeviceId) ?? GetDefaultAudioDevice() ?? audioDevices.FirstOrDefault(); + } + + private MMDevice GetDefaultAudioDevice() + { + using var enumerator = new MMDeviceEnumerator(); + return enumerator.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia); } public void Load() { Application.Current.Dispatcher.Invoke(() => { + _sourceTimer ??= new Timer(TimerTick, null, 0, 10); + if (!ConvertIfNeeded()) return; @@ -508,11 +523,10 @@ public void Dispose() _waveSource = null; } - if (_soundOut != null) - { - _soundOut.Dispose(); - _soundOut = null; - } + ClearSoundOut(); + + _sourceTimer?.Dispose(); + _sourceTimer = null; if (Spectrum != null) Spectrum = null; @@ -527,6 +541,7 @@ public void Dispose() }); } + [DebuggerHidden] private void TimerTick(object state) { if (_waveSource == null || _soundOut == null) return; @@ -554,6 +569,8 @@ private void TimerTick(object state) private void LoadSoundOut() { if (_waveSource == null) return; + + ClearSoundOut(); _soundOut = new WasapiOut(true, AudioClientShareMode.Shared, 100, ThreadPriority.Highest) { Device = SelectedAudioDevice }; _soundOut.Initialize(_waveSource.ToSampleSource().ToWaveSource(16)); _soundOut.Volume = UserSettings.Default.AudioPlayerVolume / 100; @@ -561,6 +578,10 @@ private void LoadSoundOut() private void ClearSoundOut() { + if (_soundOut == null) return; + + _soundOut.Stop(); + _soundOut.Dispose(); _soundOut = null; } @@ -748,17 +769,33 @@ private static bool TryConvertToWav(string inputFilePath, byte[] inputFileData, UseShellExecute = false, CreateNoWindow = true }); - process?.WaitForExit(5000); + using (process) + { + var exited = process != null && process.WaitForExit(5000); + if (!exited) + { + try + { + process?.Kill(); + process?.WaitForExit(2000); + Log.Warning("Audio process timed out and was killed"); + } + catch + { + // Ignore + } + } - File.Delete(tempfile); + File.Delete(tempfile); - var success = process?.ExitCode == 0 && File.Exists(tempWavFilePath); - if (success) - { - File.Move(tempWavFilePath, wavFilePath, true); - } + var success = exited && process.ExitCode == 0 && File.Exists(tempWavFilePath); + if (success) + { + File.Move(tempWavFilePath, wavFilePath, true); + } - return success; + return success; + } } private static string TryGetVgmstreamPath() diff --git a/FModel/ViewModels/CUE4ParseViewModel.cs b/FModel/ViewModels/CUE4ParseViewModel.cs index 528e01fda..63b23a45e 100644 --- a/FModel/ViewModels/CUE4ParseViewModel.cs +++ b/FModel/ViewModels/CUE4ParseViewModel.cs @@ -11,6 +11,8 @@ using System.Threading.Tasks; using System.Windows; using AdonisUI.Controls; +using CUE4Parse_Conversion.Exporters; +using CUE4Parse_Conversion.Sounds; using CUE4Parse; using CUE4Parse.Compression; using CUE4Parse.Encryption.Aes; @@ -67,8 +69,6 @@ using CUE4Parse.UE4.Versions; using CUE4Parse.UE4.Wwise; using CUE4Parse.Utils; -using CUE4Parse_Conversion.Exporters; -using CUE4Parse_Conversion.Sounds; using EpicManifestParser; using EpicManifestParser.UE; using FModel.Creator; @@ -76,11 +76,13 @@ using FModel.Framework; using FModel.Services; using FModel.Settings; +using FModel.ViewModels.ApiEndpoints.Models; using FModel.Views; using FModel.Views.Resources.Controls; using FModel.Views.Snooper; using Newtonsoft.Json; using Newtonsoft.Json.Converters; +using OpenTK.Mathematics; using OpenTK.Windowing.Common; using OpenTK.Windowing.Desktop; using Serilog; @@ -90,6 +92,7 @@ using static CUE4Parse.UE4.Versions.EGame; using Application = System.Windows.Application; using FGuid = CUE4Parse.UE4.Objects.Core.Misc.FGuid; +using Version = System.Version; namespace FModel.ViewModels; @@ -132,7 +135,7 @@ public Snooper SnooperViewer new GameWindowSettings { UpdateFrequency = htz }, new NativeWindowSettings { - ClientSize = new OpenTK.Mathematics.Vector2i( + ClientSize = new Vector2i( Convert.ToInt32(SystemParameters.MaximizedPrimaryScreenWidth * .75 * scale), Convert.ToInt32(SystemParameters.MaximizedPrimaryScreenHeight * .85 * scale)), NumberOfSamples = Constants.SAMPLES_COUNT, @@ -374,6 +377,21 @@ private void RegisterFortniteLiveArchives(StreamedFileProvider provider, FBuildP } } + private void RegisterArchivesFromManifest(DefaultFileProvider provider, FBuildPatchAppManifest manifest) + { + var archiveFiles = manifest.Files.Where(x => + _fnLiveRegex.IsMatch(x.FileName) && + (x.FileName.EndsWith(".pak", StringComparison.OrdinalIgnoreCase) || + x.FileName.EndsWith(".utoc", StringComparison.OrdinalIgnoreCase))).ToList(); + + Parallel.ForEach(archiveFiles, fileManifest => + { + provider.RegisterVfs(fileManifest.FileName, [fileManifest.GetStream()], + it => new FRandomAccessStreamArchive(it, manifest.FindFile(it)!.GetStream(), provider.Versions)); + }); + } + + /// /// load virtual files system from GameDirectory /// @@ -553,6 +571,47 @@ public Task VerifyOnDemandArchives() }); } + public Task VerifyCloudArchives() + { + if (Provider is not DefaultFileProvider p || !Provider.ProjectName.Equals("FortniteGame", StringComparison.OrdinalIgnoreCase)) + return Task.CompletedTask; + + var cloudContentPath = Path.Combine(UserSettings.Default.GameDirectory, "..\\..\\..\\Cloud\\cloudcontent.json"); + if (!File.Exists(cloudContentPath)) + return Task.CompletedTask; + + return Task.Run(async () => + { + var startTs = Stopwatch.GetTimestamp(); + + var cloudContent = JsonConvert.DeserializeObject(await File.ReadAllTextAsync(cloudContentPath)); + if (cloudContent is null || string.IsNullOrEmpty(cloudContent.ManifestPath)) + return; + + var manifestBytes = await _chunkClient.GetByteArrayAsync("https://egdownload.fastly-edge.com/" + cloudContent.ManifestPath); + + var manifestOptions = new ManifestParseOptions + { + ChunkCacheDirectory = CacheManager.ChunksDirectory, + ManifestCacheDirectory = CacheManager.ManifestsDirectory, + ChunkBaseUrl = "https://egdownload.fastly-edge.com/Builds/Fortnite/CloudDir/", + Decompressor = Compression.Decompressor, + Client = _chunkClient, + CacheChunksAsIs = false + }; + + var contentManifest = FBuildPatchAppManifest.Deserialize(manifestBytes, manifestOptions); + + RegisterArchivesFromManifest(p, contentManifest); + + var cloudCount = await Provider.MountAsync(); + var elapsedTime = Stopwatch.GetElapsedTime(startTs); + + FLogger.Append(ELog.Information, () => + FLogger.Text($"{cloudCount} cloud archive{(cloudCount > 1 ? "s" : "")} streamed via epicgames.com in {elapsedTime.TotalMilliseconds:F1}ms", Constants.WHITE, true)); + }); + } + public int LocalizedResourcesCount { get; set; } public bool LocalResourcesDone { get; set; } public bool HotfixedResourcesDone { get; set; } @@ -644,9 +703,8 @@ private void BulkFolder(CancellationToken cancellationToken, TreeItem folder, Ac public void ExportFolder(CancellationToken cancellationToken, TreeItem folder) { - Parallel.ForEach(folder.AssetsList.Assets, entry => + Parallel.ForEach(folder.AssetsList.Assets, new ParallelOptions { CancellationToken = cancellationToken }, entry => { - cancellationToken.ThrowIfCancellationRequested(); ExportData(entry.Asset); }); diff --git a/FModel/ViewModels/Commands/CopyCommand.cs b/FModel/ViewModels/Commands/CopyCommand.cs index 97177db3d..fa1126141 100644 --- a/FModel/ViewModels/Commands/CopyCommand.cs +++ b/FModel/ViewModels/Commands/CopyCommand.cs @@ -1,5 +1,4 @@ using System.Collections; -using System.Collections.Generic; using System.Linq; using System.Text; using System.Windows; diff --git a/FModel/ViewModels/Commands/ImageCommand.cs b/FModel/ViewModels/Commands/ImageCommand.cs index df4b7735a..9d5083875 100644 --- a/FModel/ViewModels/Commands/ImageCommand.cs +++ b/FModel/ViewModels/Commands/ImageCommand.cs @@ -1,9 +1,9 @@ +using System.Windows; +using System.Windows.Media; using AdonisUI.Controls; using FModel.Extensions; using FModel.Framework; using FModel.Views.Resources.Controls; -using System.Windows; -using System.Windows.Media; using FModel.Views.Resources.Converters; namespace FModel.ViewModels.Commands; diff --git a/FModel/ViewModels/Commands/LoadCommand.cs b/FModel/ViewModels/Commands/LoadCommand.cs index c29a695ee..77bce9ad1 100644 --- a/FModel/ViewModels/Commands/LoadCommand.cs +++ b/FModel/ViewModels/Commands/LoadCommand.cs @@ -6,7 +6,6 @@ using System.IO; using System.Linq; using System.Threading; -using System.Threading.Tasks; using AdonisUI.Controls; using CUE4Parse.FileProvider.Objects; using CUE4Parse.UE4.Readers; @@ -176,7 +175,7 @@ private void FilterNewOrModifiedFilesToDisplay(CancellationToken cancellationTok if (!openFileDialog.ShowDialog().GetValueOrDefault()) return; FLogger.Append(ELog.Information, () => - FLogger.Text($"Backup file older than current game is '{openFileDialog.FileName.SubstringAfterLast("\\")}'", Constants.WHITE, true)); + FLogger.Text($"Loaded old backup file '{openFileDialog.FileName.SubstringAfterLast("\\")}'", Constants.WHITE, true)); var mode = UserSettings.Default.LoadingMode; var entries = ParseBackup(openFileDialog.FileName, mode, cancellationToken); diff --git a/FModel/ViewModels/Commands/RightClickMenuCommand.cs b/FModel/ViewModels/Commands/RightClickMenuCommand.cs index 7ad31b6fd..3eb37a3c8 100644 --- a/FModel/ViewModels/Commands/RightClickMenuCommand.cs +++ b/FModel/ViewModels/Commands/RightClickMenuCommand.cs @@ -1,6 +1,5 @@ using System; using System.Collections; -using System.Data; using System.IO; using System.Linq; using System.Threading; diff --git a/FModel/ViewModels/ExportOptionsViewModel.cs b/FModel/ViewModels/ExportOptionsViewModel.cs index c2f808bcc..058837161 100644 --- a/FModel/ViewModels/ExportOptionsViewModel.cs +++ b/FModel/ViewModels/ExportOptionsViewModel.cs @@ -1,10 +1,10 @@ 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 CUE4Parse.UE4.Assets.Exports.Material; +using CUE4Parse.UE4.Assets.Exports.Texture; using FModel.Framework; using FModel.Settings; diff --git a/FModel/ViewModels/GameDirectoryViewModel.cs b/FModel/ViewModels/GameDirectoryViewModel.cs index 1d358e6c6..588b4f5bf 100644 --- a/FModel/ViewModels/GameDirectoryViewModel.cs +++ b/FModel/ViewModels/GameDirectoryViewModel.cs @@ -1,4 +1,3 @@ -using FModel.Framework; using System.Collections.ObjectModel; using System.ComponentModel; using System.Linq; @@ -9,6 +8,7 @@ using CUE4Parse.UE4.IO; using CUE4Parse.UE4.Objects.Core.Misc; using CUE4Parse.UE4.VirtualFileSystem; +using FModel.Framework; namespace FModel.ViewModels; diff --git a/FModel/ViewModels/GameFileViewModel.cs b/FModel/ViewModels/GameFileViewModel.cs index 57a9456c3..25e986b4c 100644 --- a/FModel/ViewModels/GameFileViewModel.cs +++ b/FModel/ViewModels/GameFileViewModel.cs @@ -5,7 +5,7 @@ using System.Windows; using System.Windows.Media; using System.Windows.Media.Imaging; - +using CUE4Parse_Conversion.Textures; using CUE4Parse.FileProvider.Objects; using CUE4Parse.GameTypes.Borderlands3.Assets.Exports; using CUE4Parse.GameTypes.Borderlands4.Assets.Exports; @@ -51,17 +51,11 @@ using CUE4Parse.UE4.Objects.UObject.Editor; using CUE4Parse.UE4.Versions; using CUE4Parse.Utils; - -using CUE4Parse_Conversion.Textures; - using FModel.Framework; using FModel.Services; using FModel.Settings; - using Serilog; - using SkiaSharp; - using Svg.Skia; namespace FModel.ViewModels; diff --git a/FModel/ViewModels/GameSelectorViewModel.cs b/FModel/ViewModels/GameSelectorViewModel.cs index 6447e2d78..e142bbfcc 100644 --- a/FModel/ViewModels/GameSelectorViewModel.cs +++ b/FModel/ViewModels/GameSelectorViewModel.cs @@ -1,6 +1,3 @@ -using FModel.Framework; -using Newtonsoft.Json; -using Serilog; using System; using System.Collections.Generic; using System.Collections.ObjectModel; @@ -12,9 +9,12 @@ using CUE4Parse.UE4.Objects.Core.Serialization; using CUE4Parse.UE4.Versions; using CUE4Parse.Utils; +using FModel.Framework; using FModel.Settings; using FModel.ViewModels.ApiEndpoints.Models; using Microsoft.Win32; +using Newtonsoft.Json; +using Serilog; namespace FModel.ViewModels; @@ -45,10 +45,15 @@ public DirectorySettings SelectedDirectory private readonly ObservableCollection _detectedDirectories; public ReadOnlyObservableCollection DetectedDirectories { get; } public ReadOnlyObservableCollection UeGames { get; } - + private readonly LauncherInstalled _launcherInstalled; public GameSelectorViewModel(string gameDirectory) { - _detectedDirectories = new ObservableCollection(EnumerateDetectedGames().Where(x => x != null)); + _launcherInstalled = GetDriveLauncherInstalls("ProgramData\\Epic\\UnrealEngineLauncher\\LauncherInstalled.dat"); + _detectedDirectories = new ObservableCollection(EnumerateDetectedGames() + .Where(x => x != null) + .GroupBy(x => x.GameDirectory, StringComparer.OrdinalIgnoreCase) + .Select(g => g.First())); + foreach (var dir in UserSettings.Default.PerDirectory.Values.Where(x => x.IsManual)) { _detectedDirectories.Add((DirectorySettings) dir.Clone()); @@ -92,6 +97,13 @@ private bool TryDetectUeVersion(string gameDirectory, out EGame ueVersion, [Mayb Log.Warning("Selected directory \"{GameDirectory}\" does not end with \"Paks\". Looking in \"{PaksDir}\" instead.", targetGameDir, paksDir); targetGameDir = paksDir; } + else + { + // Log.Warning("No Paks folder found under \"{GameDirectory}\".", gameDirectory); + ueVersion = EGame.GAME_UE4_LATEST; + newGameDirectory = targetGameDir; + return false; + } if (Directory.GetFiles(gameDirectory, "*.exe") is { Length: 1 } exe && TryGetUeVersionFromExe(exe[0], out ueVersion)) { @@ -189,6 +201,11 @@ public void DeleteSelectedGame() SelectedDirectory = DetectedDirectories.Last(); } + private static bool IsUuidNamespace(string ns) + { + return Guid.TryParseExact(ns, "N", out _); + } + private IEnumerable EnumerateUeGames() => Enum.GetValues() .GroupBy(value => (int)value) @@ -218,12 +235,32 @@ private IEnumerable EnumerateDetectedGames() yield return GetRockstarGamesGame("GTA San Andreas - Definitive Edition", "\\Gameface\\Content\\Paks", EGame.GAME_GTATheTrilogyDefinitiveEdition); yield return GetRockstarGamesGame("GTA Vice City - Definitive Edition", "\\Gameface\\Content\\Paks", EGame.GAME_GTATheTrilogyDefinitiveEdition); yield return GetLevelInfiniteGame("tof_launcher", "\\Hotta\\Content\\Paks", EGame.GAME_TowerOfFantasy); + + foreach (var install in (_launcherInstalled?.InstallationList ?? []).Take(30)) // First 30 games only if a user has hundreds, it will be slow. + { + if (!IsUuidNamespace(install.NamespaceId) || !IsUuidNamespace(install.AppName)) // No official Epic games apps / apps with no name (needs api calls) + continue; + + if (!Directory.Exists(install.InstallLocation)) + continue; + + if (!TryDetectUeVersion(install.InstallLocation, out var ueVersion, out var detectedDir)) + continue; + + yield return DirectorySettings.Default(install.AppName, detectedDir ?? install.InstallLocation, ue: ueVersion); + } + + foreach (var game in SteamDetection.GetSteamGames().Take(30)) + { + if (!TryDetectUeVersion(game.GameRoot, out var ueVersion, out var detectedDir)) + continue; + + yield return DirectorySettings.Default(game.Name, detectedDir ?? game.GameRoot, ue: ueVersion); + } } - private LauncherInstalled _launcherInstalled; private DirectorySettings GetUnrealEngineGame(string gameName, string pakDirectory, EGame ueVersion) { - _launcherInstalled ??= GetDriveLauncherInstalls("ProgramData\\Epic\\UnrealEngineLauncher\\LauncherInstalled.dat"); if (_launcherInstalled?.InstallationList != null) { foreach (var installationList in _launcherInstalled.InstallationList) @@ -342,6 +379,7 @@ private class LauncherInstalled private class Installation { public string InstallLocation; + public string NamespaceId; public string AppName; public string AppVersion; } @@ -395,6 +433,7 @@ static SteamDetection() public static AppInfo GetSteamGameById(int id) => _steamApps.FirstOrDefault(app => app.Id == id.ToString()); + public static IEnumerable GetSteamApps() => _steamApps; private static List GetSteamApps(IEnumerable steamLibs) { var apps = new List(); @@ -409,6 +448,17 @@ private static List GetSteamApps(IEnumerable steamLibs) return apps; } + public static IEnumerable GetSteamGames() + { + foreach (var app in GetSteamApps()) + { + if (!Directory.Exists(app.GameRoot)) + continue; + + yield return app; + } + } + private static AppInfo GetAppInfo(string appMetaFile) { var fileDataLines = File.ReadAllLines(appMetaFile); diff --git a/FModel/ViewModels/SearchViewModel.cs b/FModel/ViewModels/SearchViewModel.cs index 17b904f2c..feed6372a 100644 --- a/FModel/ViewModels/SearchViewModel.cs +++ b/FModel/ViewModels/SearchViewModel.cs @@ -2,8 +2,9 @@ using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; +using System.Threading; using System.Threading.Tasks; -using System.Windows.Data; +using System.Windows; using CUE4Parse.FileProvider.Objects; using CUE4Parse.UE4.VirtualFileSystem; using FModel.Framework; @@ -47,7 +48,7 @@ public ESortSizeMode CurrentSortSizeMode set => SetProperty(ref _currentSortSizeMode, value); } - private int _resultsCount = 0; + private int _resultsCount; public int ResultsCount { get => _resultsCount; @@ -62,30 +63,24 @@ public GameFile RefFile } public RangeObservableCollection SearchResults { get; } - public ListCollectionView SearchResultsView { get; } + private List _allEntries = new(); + private CancellationTokenSource _updateCts = new(); public SearchViewModel() { - SearchResults = []; - SearchResultsView = new ListCollectionView(SearchResults) - { - Filter = e => ItemFilter(e, FilterText.Trim().Split(' ', StringSplitOptions.RemoveEmptyEntries)), - }; - ResultsCount = SearchResultsView.Count; + SearchResults = new RangeObservableCollection(); } - public void RefreshFilter() + public void ChangeCollection(IEnumerable files, GameFile refFile = null) { - SearchResultsView.Refresh(); - ResultsCount = SearchResultsView.Count; + _allEntries = files?.ToList() ?? new List(); + RefFile = refFile; + _ = UpdateResultsAsync(); } - public void ChangeCollection(IEnumerable files, GameFile refFile = null) + public async Task RefreshFilter() { - SearchResults.Clear(); - SearchResults.AddRange(files); - RefFile = refFile; - ResultsCount = SearchResultsView.Count; + await UpdateResultsAsync(); } public async Task CycleSortSizeMode() @@ -96,50 +91,110 @@ public async Task CycleSortSizeMode() ESortSizeMode.Descending => ESortSizeMode.Ascending, _ => ESortSizeMode.None }; + await UpdateResultsAsync(); + } - var sorted = await Task.Run(() => + private async Task UpdateResultsAsync() + { + _updateCts.Cancel(); + var cts = new CancellationTokenSource(); + _updateCts = cts; + var token = cts.Token; + + string filterText = FilterText; + bool regex = HasRegexEnabled; + bool matchCase = HasMatchCaseEnabled; + ESortSizeMode sortMode = CurrentSortSizeMode; + List allEntries = _allEntries; + + try { - var archiveDict = SearchResults - .OfType() - .Select(f => f.Vfs.Name) - .Distinct() - .Select((name, idx) => (name, idx)) - .ToDictionary(x => x.name, x => x.idx); - - var keyed = SearchResults.Select(f => + var filtered = await Task.Run(() => { - int archiveKey = f is VfsEntry ve && archiveDict.TryGetValue(ve.Vfs.Name, out var key) ? key : -1; - return (File: f, f.Size, ArchiveKey: archiveKey); - }); + return FilterAndSort(allEntries, filterText, regex, matchCase, sortMode, token); + }, token).ConfigureAwait(false); - return CurrentSortSizeMode switch + if (cts != _updateCts) + return; + + await Application.Current.Dispatcher.InvokeAsync(() => { - ESortSizeMode.Ascending => keyed - .OrderBy(x => x.Size).ThenBy(x => x.ArchiveKey) - .Select(x => x.File).ToList(), - ESortSizeMode.Descending => keyed - .OrderByDescending(x => x.Size).ThenBy(x => x.ArchiveKey) - .Select(x => x.File).ToList(), - _ => keyed - .OrderBy(x => x.ArchiveKey).ThenBy(x => x.File.Path, StringComparer.OrdinalIgnoreCase) - .Select(x => x.File).ToList() - }; - }); - - SearchResults.Clear(); - SearchResults.AddRange(sorted); + SearchResults.Clear(); + SearchResults.AddRange(filtered); + ResultsCount = SearchResults.Count; + }); + } + catch (OperationCanceledException) + { + // Ignore + } } - private bool ItemFilter(object item, IEnumerable filters) + private static List FilterAndSort( + List entries, + string filterText, + bool regex, + bool matchCase, + ESortSizeMode sortMode, + CancellationToken token) { - if (item is not GameFile entry) - return true; - - if (!HasRegexEnabled) - return filters.All(x => entry.Path.Contains(x, HasMatchCaseEnabled ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase)); + if (entries.Count == 0) + return new List(); - var o = RegexOptions.None; - if (!HasMatchCaseEnabled) o |= RegexOptions.IgnoreCase; - return new Regex(FilterText, o).Match(entry.Path).Success; + IEnumerable filtered = entries; + if (!string.IsNullOrWhiteSpace(filterText)) + { + if (regex) + { + var options = RegexOptions.None; + if (!matchCase) options |= RegexOptions.IgnoreCase; + var regexObj = new Regex(filterText, options | RegexOptions.Compiled); + filtered = entries.Where(f => regexObj.IsMatch(f.Path)); + } + else + { + var filters = filterText.Trim().Split(' ', StringSplitOptions.RemoveEmptyEntries); + var comparison = matchCase ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase; + filtered = entries.Where(f => filters.All(x => f.Path.Contains(x, comparison))); + } + } + + var sortedList = filtered.ToList(); + if (token.IsCancellationRequested) + return sortedList; + + var archiveDict = sortedList + .OfType() + .Select(f => f.Vfs.Name) + .Distinct() + .Select((name, idx) => (name, idx)) + .ToDictionary(x => x.name, x => x.idx); + + int GetArchiveIndex(GameFile f) => + f is VfsEntry ve && archiveDict.TryGetValue(ve.Vfs.Name, out var idx) ? idx : -1; + + switch (sortMode) + { + case ESortSizeMode.Ascending: + sortedList = sortedList + .OrderBy(f => f.Size) + .ThenBy(f => GetArchiveIndex(f)) + .ToList(); + break; + case ESortSizeMode.Descending: + sortedList = sortedList + .OrderByDescending(f => f.Size) + .ThenBy(f => GetArchiveIndex(f)) + .ToList(); + break; + default: + sortedList = sortedList + .OrderBy(f => GetArchiveIndex(f)) + .ThenBy(f => f.Path, StringComparer.OrdinalIgnoreCase) + .ToList(); + break; + } + + return sortedList; } } diff --git a/FModel/ViewModels/SettingsViewModel.cs b/FModel/ViewModels/SettingsViewModel.cs index 0b91ff261..30e693561 100644 --- a/FModel/ViewModels/SettingsViewModel.cs +++ b/FModel/ViewModels/SettingsViewModel.cs @@ -2,12 +2,8 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; -using CUE4Parse.UE4.Assets.Exports.Texture; using CUE4Parse.UE4.Objects.Core.Serialization; using CUE4Parse.UE4.Versions; -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; diff --git a/FModel/ViewModels/TabControlViewModel.cs b/FModel/ViewModels/TabControlViewModel.cs index a2b7ddf73..581a02407 100644 --- a/FModel/ViewModels/TabControlViewModel.cs +++ b/FModel/ViewModels/TabControlViewModel.cs @@ -6,10 +6,10 @@ using System.Windows; using System.Windows.Media.Imaging; using CUE4Parse_Conversion.Options; +using CUE4Parse_Conversion.Textures; using CUE4Parse.FileProvider.Objects; using CUE4Parse.UE4.Assets.Exports.Texture; using CUE4Parse.Utils; -using CUE4Parse_Conversion.Textures; using FModel.Extensions; using FModel.Framework; using FModel.Services; @@ -43,6 +43,23 @@ public TabImage(string name, bool rnn, CTexture img) SetImage(img); } + private static readonly string[] KnownImageExtensions = [".png", ".jpg", ".jpeg", ".jpe", ".jfif", ".bmp", ".tif", ".tiff", ".webp", ".dds", ".hdr", ".exr"]; + + // prevents T_Slop.png.jpg + private void SetExportExtension(string extension) + { + var name = ExportName; + foreach (var ext in KnownImageExtensions) + { + if (name.EndsWith(ext, StringComparison.OrdinalIgnoreCase)) + { + name = name[..^ext.Length]; + break; + } + } + ExportName = $"{name}.{extension}"; + } + private BitmapImage _image; public BitmapImage Image { @@ -82,7 +99,7 @@ private void SetImage(SKBitmap bitmap) } _bmp = bitmap; - ExportName += "." + (NoAlpha ? "jpg" : "png"); + SetExportExtension(NoAlpha ? "jpg" : "png"); using var data = _bmp.Encode(NoAlpha ? SKEncodedImageFormat.Jpeg : SKEncodedImageFormat.Png, 100); using var stream = new MemoryStream(ImageBuffer = data.ToArray(), false); var image = new BitmapImage(); @@ -109,12 +126,12 @@ private void SetImage(CTexture bitmap) if (PixelFormatUtils.IsHDR(bitmap.PixelFormat) || (UserSettings.Default.TextureExportFormat != ETextureFormat.Jpeg && UserSettings.Default.TextureExportFormat != ETextureFormat.Png)) { ImageBuffer = bitmap.Encode(UserSettings.Default.TextureExportFormat, UserSettings.Default.SaveHdrTexturesAsHdr, out var ext); - ExportName += "." + ext; + SetExportExtension(ext); } else { ImageBuffer = imageData; - ExportName += "." + (NoAlpha || UserSettings.Default.TextureExportFormat == ETextureFormat.Jpeg ? "jpg" : "png"); + SetExportExtension(NoAlpha || UserSettings.Default.TextureExportFormat == ETextureFormat.Jpeg ? "jpg" : "png"); } using var stream = new MemoryStream(imageData); @@ -245,6 +262,7 @@ public TabImage SelectedImage if (_selectedImage == value) return; SetProperty(ref _selectedImage, value); RaisePropertyChanged("HasImage"); + RaisePropertyChanged("ShowExportName"); RaisePropertyChanged("Page"); } } @@ -252,6 +270,10 @@ public TabImage SelectedImage public string Header => $"{Entry.Name}{(string.IsNullOrEmpty(TitleExtra) ? "" : $" ({TitleExtra})")}"; public bool HasImage => SelectedImage != null; + public bool ShowExportName => HasImage && !string.Equals( + Path.GetFileNameWithoutExtension(SelectedImage.ExportName), + Path.GetFileNameWithoutExtension(Entry.Name), + StringComparison.OrdinalIgnoreCase); public bool HasMultipleImages => _images.Count > 1; public string Page => $"{_images.IndexOf(_selectedImage) + 1} / {_images.Count}"; diff --git a/FModel/Views/AesManager.xaml b/FModel/Views/AesManager.xaml index 9948d4b8a..43d424263 100644 --- a/FModel/Views/AesManager.xaml +++ b/FModel/Views/AesManager.xaml @@ -74,8 +74,7 @@ HorizontalAlignment="Right" VerticalAlignment="Bottom" Content="OK" Click="OnClick" /> + Visibility="{Binding Converter={x:Static converters:EndpointToTypeConverter.Instance}, ConverterParameter={x:Static local:EEndpointType.Aes}}" /> diff --git a/FModel/Views/AudioPlayer.xaml.cs b/FModel/Views/AudioPlayer.xaml.cs index f65660cea..ca9c40dd6 100644 --- a/FModel/Views/AudioPlayer.xaml.cs +++ b/FModel/Views/AudioPlayer.xaml.cs @@ -36,11 +36,10 @@ private void OnClosing(object sender, CancelEventArgs e) private void OnDeviceSwap(object sender, SelectionChangedEventArgs e) { - if (sender is not ComboBox { SelectedItem: MMDevice selectedDevice }) - return; - - UserSettings.Default.AudioDeviceId = selectedDevice.DeviceID; - _applicationView.AudioPlayer.Device(); + if (sender is ComboBox { SelectedItem: MMDevice selectedDevice }) + { + _applicationView.AudioPlayer.Device(); + } } private void OnVolumeChange(object sender, RoutedEventArgs e) diff --git a/FModel/Views/DirectorySelector.xaml.cs b/FModel/Views/DirectorySelector.xaml.cs index 8b0be1431..b12210ed8 100644 --- a/FModel/Views/DirectorySelector.xaml.cs +++ b/FModel/Views/DirectorySelector.xaml.cs @@ -1,6 +1,6 @@ +using System.Windows; using FModel.ViewModels; using Ookii.Dialogs.Wpf; -using System.Windows; namespace FModel.Views; diff --git a/FModel/Views/ExportSessionWindow.xaml b/FModel/Views/ExportSessionWindow.xaml index 61a6f26b6..e0ff579f3 100644 --- a/FModel/Views/ExportSessionWindow.xaml +++ b/FModel/Views/ExportSessionWindow.xaml @@ -13,7 +13,6 @@ xmlns:converters="clr-namespace:FModel.Views.Resources.Converters" xmlns:controls="clr-namespace:FModel.Views.Resources.Controls" xmlns:serilog="clr-namespace:Serilog.Events;assembly=Serilog" - xmlns:c4pMeshes="clr-namespace:CUE4Parse_Conversion.Options;assembly=CUE4Parse-Conversion" DataContext="{x:Static vm:ExportSessionViewModel.Instance}" WindowStartupLocation="CenterScreen" ResizeMode="NoResize" IconVisibility="Collapsed" diff --git a/FModel/Views/ImageMerger.xaml.cs b/FModel/Views/ImageMerger.xaml.cs index 6a3a41897..5544fae9f 100644 --- a/FModel/Views/ImageMerger.xaml.cs +++ b/FModel/Views/ImageMerger.xaml.cs @@ -1,10 +1,3 @@ -using AdonisUI.Controls; -using FModel.Extensions; -using FModel.Settings; -using FModel.Views.Resources.Controls; -using Microsoft.Win32; -using Serilog; -using SkiaSharp; using System; using System.Collections.Generic; using System.Drawing.Imaging; @@ -16,6 +9,14 @@ using System.Windows.Controls.Primitives; using System.Windows.Input; using System.Windows.Media.Imaging; +using AdonisUI.Controls; +using FModel.Extensions; +using FModel.Settings; +using FModel.Views.Resources.Controls; +using Microsoft.Win32; +using Serilog; +using SkiaSharp; +using Image = System.Drawing.Image; namespace FModel.Views; @@ -43,6 +44,13 @@ private async void Click_DrawPreview(object sender, MouseButtonEventArgs e) private async Task DrawPreview() { + if (ImagesListBox.Items.Count == 0) + { + ImagePreview.Source = null; + _imageBuffer = null; + return; + } + AddButton.IsEnabled = false; UpButton.IsEnabled = false; DownButton.IsEnabled = false; @@ -59,21 +67,36 @@ private async Task DrawPreview() for (var i = 0; i < images.Length; i++) { var item = (ListBoxItem) ImagesListBox.Items[i]; - var ms = new MemoryStream(); - var stream = new FileStream(item.ContentStringFormat, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); - if (item.ContentStringFormat.EndsWith(".tif")) + SKBitmap image; + await using (var ms = new MemoryStream()) { - await using var tmp = new MemoryStream(); - await stream.CopyToAsync(tmp); - System.Drawing.Image.FromStream(tmp).Save(ms, ImageFormat.Png); + using (var stream = new FileStream(item.ContentStringFormat, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) + { + if (item.ContentStringFormat.EndsWith(".tif")) + { + await using var tmp = new MemoryStream(); + await stream.CopyToAsync(tmp); + tmp.Position = 0; + using var drawing = Image.FromStream(tmp); + drawing.Save(ms, ImageFormat.Png); + } + else + { + await stream.CopyToAsync(ms); + } + } + + image = SKBitmap.Decode(ms.ToArray()); } - else + + if (image == null) { - await stream.CopyToAsync(ms); + Log.Warning("Image merger skipped an undecodable file: {File}", item.ContentStringFormat); + images[i] = null; + continue; } - var image = SKBitmap.Decode(ms.ToArray()); positions[i] = new SKPoint(curW, curH); images[i] = image; @@ -108,6 +131,9 @@ await Task.Run(() => for (var i = 0; i < images.Length; i++) { + if (images[i] == null) + continue; + using (images[i]) { canvas.DrawBitmap(images[i], positions[i], new SKPaint { FilterQuality = SKFilterQuality.High, IsAntialias = true }); @@ -185,7 +211,6 @@ private async void ModifyItemInList(object sender, RoutedEventArgs e) } } - ImagesListBox.SelectedItems.Add(indices); if (reloadImage) { await DrawPreview().ConfigureAwait(false); @@ -223,6 +248,13 @@ private async void ModifyItemInList(object sender, RoutedEventArgs e) ImagesListBox.Items.Remove(ImagesListBox.SelectedItems[i]); } + if (ImagesListBox.Items.Count == 0) + { + ImagePreview.Source = null; + _imageBuffer = null; + break; + } + await DrawPreview().ConfigureAwait(false); break; diff --git a/FModel/Views/Resources/Controls/Aed/GamePathElementGenerator.cs b/FModel/Views/Resources/Controls/Aed/GamePathElementGenerator.cs index 3b6a7efe8..7a1c80dfa 100644 --- a/FModel/Views/Resources/Controls/Aed/GamePathElementGenerator.cs +++ b/FModel/Views/Resources/Controls/Aed/GamePathElementGenerator.cs @@ -1,5 +1,4 @@ using System.Text.RegularExpressions; -using FModel.Extensions; using ICSharpCode.AvalonEdit.Rendering; namespace FModel.Views.Resources.Controls; diff --git a/FModel/Views/Resources/Controls/Aed/GamePathVisualLineText.cs b/FModel/Views/Resources/Controls/Aed/GamePathVisualLineText.cs index 51d6a5d1c..6cacbaf08 100644 --- a/FModel/Views/Resources/Controls/Aed/GamePathVisualLineText.cs +++ b/FModel/Views/Resources/Controls/Aed/GamePathVisualLineText.cs @@ -1,5 +1,4 @@ using System; -using System.Text.RegularExpressions; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.TextFormatting; @@ -7,7 +6,6 @@ using FModel.Extensions; using FModel.Services; using FModel.ViewModels; -using ICSharpCode.AvalonEdit.Document; using ICSharpCode.AvalonEdit.Rendering; namespace FModel.Views.Resources.Controls; diff --git a/FModel/Views/Resources/Controls/Aed/JumpVisualLineText.cs b/FModel/Views/Resources/Controls/Aed/JumpVisualLineText.cs index 41034e3ee..7c11876f4 100644 --- a/FModel/Views/Resources/Controls/Aed/JumpVisualLineText.cs +++ b/FModel/Views/Resources/Controls/Aed/JumpVisualLineText.cs @@ -3,8 +3,6 @@ using System.Windows.Media; using System.Windows.Media.TextFormatting; using FModel.Extensions; -using FModel.Services; -using FModel.ViewModels; using ICSharpCode.AvalonEdit.Rendering; namespace FModel.Views.Resources.Controls; diff --git a/FModel/Views/Resources/Controls/AvalonEditor.xaml.cs b/FModel/Views/Resources/Controls/AvalonEditor.xaml.cs index 9aebc30cc..0dbf569dd 100644 --- a/FModel/Views/Resources/Controls/AvalonEditor.xaml.cs +++ b/FModel/Views/Resources/Controls/AvalonEditor.xaml.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Text.RegularExpressions; using System.Windows; +using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using CUE4Parse.Utils; @@ -11,6 +12,7 @@ using FModel.ViewModels; using ICSharpCode.AvalonEdit; using SkiaSharp; +using TabItem = FModel.ViewModels.TabItem; namespace FModel.Views.Resources.Controls; @@ -20,10 +22,10 @@ namespace FModel.Views.Resources.Controls; public partial class AvalonEditor { public static TextEditor YesWeEditor; - public static System.Windows.Controls.TextBox YesWeSearch; + public static TextBox YesWeSearch; private readonly Regex _hexColorRegex = new("\"Hex\": \"(?'target'[0-9A-Fa-f]{3,8})\"$", RegexOptions.Compiled | RegexOptions.Singleline | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); - private readonly System.Windows.Controls.ToolTip _toolTip = new(); + private readonly ToolTip _toolTip = new(); private readonly Dictionary> _savedCarets = new(); private NavigationList _caretsOffsets { @@ -226,7 +228,15 @@ private void OnCloseClick(object sender, RoutedEventArgs e) private void OnTabClose(object sender, EventArgs eventArgs) { - if (eventArgs is not TabControlViewModel.TabEventArgs e || e.TabToRemove.Document?.FileName is not { } fileName) + if (eventArgs is not TabControlViewModel.TabEventArgs e) + return; + + if (ReferenceEquals(e.TabToRemove, DataContext)) + { + ApplicationService.ApplicationView.CUE4Parse.TabControl.OnTabRemove -= OnTabClose; + } + + if (e.TabToRemove.Document?.FileName is not { } fileName) return; if (_savedCarets.ContainsKey(fileName)) diff --git a/FModel/Views/Resources/Controls/ContextMenus/FolderContextMenu.xaml.cs b/FModel/Views/Resources/Controls/ContextMenus/FolderContextMenu.xaml.cs index b1e1c8c97..ec348f777 100644 --- a/FModel/Views/Resources/Controls/ContextMenus/FolderContextMenu.xaml.cs +++ b/FModel/Views/Resources/Controls/ContextMenus/FolderContextMenu.xaml.cs @@ -2,7 +2,6 @@ using System.Linq; using System.Windows; using System.Windows.Controls; -using System.Windows.Media; using FModel.Extensions; using FModel.Services; using FModel.Settings; diff --git a/FModel/Views/Resources/Controls/DropOverlay.xaml b/FModel/Views/Resources/Controls/DropOverlay.xaml index 2a5a0dce7..41ef200f8 100644 --- a/FModel/Views/Resources/Controls/DropOverlay.xaml +++ b/FModel/Views/Resources/Controls/DropOverlay.xaml @@ -3,7 +3,6 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" - xmlns:local="clr-namespace:FModel.Views.Resources.Controls" xmlns:adonisUi="clr-namespace:AdonisUI;assembly=AdonisUI" Visibility="Collapsed" Background="Transparent" diff --git a/FModel/Views/Resources/Controls/DropOverlay.xaml.cs b/FModel/Views/Resources/Controls/DropOverlay.xaml.cs index faacc4129..f0214a99a 100644 --- a/FModel/Views/Resources/Controls/DropOverlay.xaml.cs +++ b/FModel/Views/Resources/Controls/DropOverlay.xaml.cs @@ -4,7 +4,6 @@ using System.Runtime.CompilerServices; using System.Windows; using System.Windows.Controls; -using System.Windows.Threading; using FModel.Services; using FModel.Settings; using FModel.ViewModels; diff --git a/FModel/Views/Resources/Controls/Explorer/ListExplorer/FolderRow.xaml b/FModel/Views/Resources/Controls/Explorer/ListExplorer/FolderRow.xaml index deaa89577..92bbb63f0 100644 --- a/FModel/Views/Resources/Controls/Explorer/ListExplorer/FolderRow.xaml +++ b/FModel/Views/Resources/Controls/Explorer/ListExplorer/FolderRow.xaml @@ -4,7 +4,6 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:vm="clr-namespace:FModel.ViewModels" - xmlns:converters="clr-namespace:FModel.Views.Resources.Converters" xmlns:adonisUi="clr-namespace:AdonisUI;assembly=AdonisUI" mc:Ignorable="d" d:DesignHeight="32" diff --git a/FModel/Views/Resources/Controls/PropertiesPopout.xaml.cs b/FModel/Views/Resources/Controls/PropertiesPopout.xaml.cs index ed4b3ff7c..738695e9b 100644 --- a/FModel/Views/Resources/Controls/PropertiesPopout.xaml.cs +++ b/FModel/Views/Resources/Controls/PropertiesPopout.xaml.cs @@ -1,12 +1,12 @@ using System; using System.Text.RegularExpressions; +using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; -using CUE4Parse.Utils; -using FModel.ViewModels; using ICSharpCode.AvalonEdit; using ICSharpCode.AvalonEdit.Document; using SkiaSharp; +using TabItem = FModel.ViewModels.TabItem; namespace FModel.Views.Resources.Controls; @@ -14,7 +14,7 @@ public partial class PropertiesPopout { private readonly Regex _hexColorRegex = new("\"Hex\": \"(?'target'[0-9A-Fa-f]{3,8})\"$", RegexOptions.Compiled | RegexOptions.Singleline | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); - private readonly System.Windows.Controls.ToolTip _toolTip = new(); + private readonly ToolTip _toolTip = new(); private JsonFoldingStrategies _manager; public PropertiesPopout(TabItem contextViewModel) diff --git a/FModel/Views/Resources/Converters/EnumToStringConverter.cs b/FModel/Views/Resources/Converters/EnumToStringConverter.cs index d317f4e61..56c6bf6f2 100644 --- a/FModel/Views/Resources/Converters/EnumToStringConverter.cs +++ b/FModel/Views/Resources/Converters/EnumToStringConverter.cs @@ -1,7 +1,7 @@ -using FModel.Extensions; using System; using System.Globalization; using System.Windows.Data; +using FModel.Extensions; namespace FModel.Views.Resources.Converters; @@ -27,4 +27,4 @@ public object ConvertBack(object value, Type targetType, object parameter, Cultu { throw new NotImplementedException(); } -} \ No newline at end of file +} diff --git a/FModel/Views/Resources/Converters/SizeToStringConverter.cs b/FModel/Views/Resources/Converters/SizeToStringConverter.cs index a256e092b..ab90588a3 100644 --- a/FModel/Views/Resources/Converters/SizeToStringConverter.cs +++ b/FModel/Views/Resources/Converters/SizeToStringConverter.cs @@ -1,7 +1,7 @@ -using FModel.Extensions; using System; using System.Globalization; using System.Windows.Data; +using FModel.Extensions; namespace FModel.Views.Resources.Converters; @@ -18,4 +18,4 @@ public object ConvertBack(object value, Type targetType, object parameter, Cultu { throw new NotImplementedException(); } -} \ No newline at end of file +} diff --git a/FModel/Views/Resources/Resources.xaml b/FModel/Views/Resources/Resources.xaml index 53cefe2bd..b3d4aba42 100644 --- a/FModel/Views/Resources/Resources.xaml +++ b/FModel/Views/Resources/Resources.xaml @@ -792,7 +792,7 @@ + Visibility="{Binding SelectedItem.ShowExportName, ElementName=TabControlName, Converter={StaticResource BoolToVisibilityConverter}}" /> diff --git a/FModel/Views/SearchView.xaml b/FModel/Views/SearchView.xaml index 01d300358..84daee836 100644 --- a/FModel/Views/SearchView.xaml +++ b/FModel/Views/SearchView.xaml @@ -7,6 +7,7 @@ xmlns:adonisExtensions="clr-namespace:AdonisUI.Extensions;assembly=AdonisUI" WindowStartupLocation="CenterScreen" ResizeMode="CanResize" ShowInTaskbar="True" IconVisibility="Collapsed" KeyDown="OnWindowKeyDown" StateChanged="OnStateChanged" + Closed="OnWindowClosed" Height="{Binding Source={x:Static SystemParameters.MaximizedPrimaryScreenHeight}, Converter={converters:RatioConverter}, ConverterParameter='0.75'}" Width="{Binding Source={x:Static SystemParameters.MaximizedPrimaryScreenWidth}, Converter={converters:RatioConverter}, ConverterParameter='0.65'}"> @@ -38,14 +39,14 @@ - + @@ -91,7 +92,7 @@ + ScrollViewer.CanContentScroll="True" ItemsSource="{Binding SearchTab.SearchResults, IsAsync=True}"> @@ -506,7 +507,7 @@ VirtualizingPanel.IsVirtualizing="True" VirtualizingPanel.VirtualizationMode="Recycling" ScrollViewer.CanContentScroll="True" - ItemsSource="{Binding RefTab.SearchResultsView, IsAsync=True}"> + ItemsSource="{Binding RefTab.SearchResults, IsAsync=True}">