diff --git a/FModel/App.xaml.cs b/FModel/App.xaml.cs index c434def65..31da19b97 100644 --- a/FModel/App.xaml.cs +++ b/FModel/App.xaml.cs @@ -92,6 +92,12 @@ protected override void OnStartup(StartupEventArgs e) UserSettings.Default.AudioDirectory = Path.Combine(UserSettings.Default.OutputDirectory, "Exports"); } + if (!Directory.Exists(UserSettings.Default.CodeDirectory)) + { + createMe = true; + UserSettings.Default.CodeDirectory = Path.Combine(UserSettings.Default.OutputDirectory, "Exports"); + } + if (!Directory.Exists(UserSettings.Default.ModelDirectory)) { createMe = true; diff --git a/FModel/Creator/Bases/FN/BaseAssembledMesh.cs b/FModel/Creator/Bases/FN/BaseAssembledMesh.cs new file mode 100644 index 000000000..830b90bfc --- /dev/null +++ b/FModel/Creator/Bases/FN/BaseAssembledMesh.cs @@ -0,0 +1,47 @@ +using CUE4Parse.UE4.Assets.Exports; +using CUE4Parse.UE4.Assets.Objects; +using CUE4Parse.UE4.Objects.UObject; +using SkiaSharp; + +namespace FModel.Creator.Bases.FN; + +public class BaseAssembledMesh : UCreator +{ + public BaseAssembledMesh(UObject uObject, EIconStyle style) : base(uObject, style) + { + + } + + public override void ParseForInfo() + { + if (Object.TryGetValue(out FInstancedStruct[] additionalData, "AdditionalData")) + { + foreach (var data in additionalData) + { + if (data.NonConstStruct?.TryGetValue(out FSoftObjectPath largePreview, "LargePreviewImage", "SmallPreviewImage") == true) + { + Preview = Utils.GetBitmap(largePreview); + } + } + } + } + + public override SKBitmap[] Draw() + { + var ret = new SKBitmap(Width, Height, SKColorType.Rgba8888, SKAlphaType.Premul); + using var c = new SKCanvas(ret); + + switch (Style) + { + case EIconStyle.NoBackground: + DrawPreview(c); + break; + default: + DrawBackground(c); + DrawPreview(c); + break; + } + + return new[] { ret }; + } +} diff --git a/FModel/Creator/Bases/FN/BaseIconStats.cs b/FModel/Creator/Bases/FN/BaseIconStats.cs index 13d2bfe1d..d495d17d3 100644 --- a/FModel/Creator/Bases/FN/BaseIconStats.cs +++ b/FModel/Creator/Bases/FN/BaseIconStats.cs @@ -87,6 +87,7 @@ public override void ParseForInfo() weaponRowValue.TryGetValue(out float dmgPb, "DmgPB"); //Damage at point blank weaponRowValue.TryGetValue(out float mdpc, "MaxDamagePerCartridge"); //Max damage a weapon can do in a single hit, usually used for shotguns weaponRowValue.TryGetValue(out float dmgCritical, "DamageZone_Critical"); //Headshot multiplier + weaponRowValue.TryGetValue(out float envDmgPb, "EnvDmgPB"); //Structure damage at point blank weaponRowValue.TryGetValue(out int clipSize, "ClipSize"); //Item magazine size weaponRowValue.TryGetValue(out float firingRate, "FiringRate"); //Item firing rate, value is shots per second weaponRowValue.TryGetValue(out float swingTime, "SwingTime"); //Item swing rate, value is swing per second @@ -115,6 +116,15 @@ public override void ParseForInfo() _statistics.Add(new IconStat(Utils.GetLocalizedResource("", "0DEF2455463B008C4499FEA03D149EDF", "Headshot Damage"), dmgPb * dmgCritical * multiplier, 160)); } } + { + var envdmgmultiplier = bpc != 0f ? bpc : 1; + if (envDmgPb != 0f) + + { + _statistics.Add(new IconStat(Utils.GetLocalizedResource("", "11AF67134E0F4E27E5E588806AB475BE", "Structure Damage"), envDmgPb * envdmgmultiplier, 160)); + } + } + if (clipSize > 999f || clipSize == 0f) { _statistics.Add(new IconStat(Utils.GetLocalizedResource("", "068239DD4327B36124498C9C5F61C038", "Magazine Size"), Utils.GetLocalizedResource("", "0FAE8E5445029F2AA209ADB0FE49B23C", "Infinite"), -1)); diff --git a/FModel/Creator/CreatorPackage.cs b/FModel/Creator/CreatorPackage.cs index 0fe8c317b..b2b122848 100644 --- a/FModel/Creator/CreatorPackage.cs +++ b/FModel/Creator/CreatorPackage.cs @@ -100,12 +100,14 @@ public bool TryConstructCreator([MaybeNullWhen(false)] out UCreator creator) case "FortCodeTokenItemDefinition": case "FortSchematicItemDefinition": case "FortAlterableItemDefinition": + case "SproutHousingItemDefinition": case "SparksKeyboardItemDefinition": case "FortWorldMultiItemDefinition": case "FortAlterationItemDefinition": case "FortExpeditionItemDefinition": case "FortIngredientItemDefinition": case "FortConsumableItemDefinition": + case "SproutBuildingItemDefinition": case "StWFortAccoladeItemDefinition": case "FortAccountBuffItemDefinition": case "FortFOBCoreDecoItemDefinition": @@ -163,6 +165,9 @@ public bool TryConstructCreator([MaybeNullWhen(false)] out UCreator creator) case "JunoAthenaDanceItemOverrideDefinition": creator = new BaseJuno(_object.Value, _style); return true; + case "AssembledMeshSchema": + creator = new BaseAssembledMesh(_object.Value, _style); + return true; case "FortTandemCharacterData": creator = new BaseTandem(_object.Value, _style); return true; diff --git a/FModel/Resources/Cpp.xshd b/FModel/Resources/Cpp.xshd index f61b12642..89c827804 100644 --- a/FModel/Resources/Cpp.xshd +++ b/FModel/Resources/Cpp.xshd @@ -18,6 +18,7 @@ + (\/\/.*|\/\*[\s\S]*?\*\/) @@ -44,10 +45,19 @@ Int16 Int32 Int64 + int8 + int16 + int32 + int64 uint + UInt8 UInt16 UInt32 UInt64 + uint8 + uint16 + uint32 + uint64 float double bool @@ -83,6 +93,7 @@ inline constexpr default + && @@ -120,8 +131,6 @@ [\[\]\{\}] - (\/\/.*|\/\*[\s\S]*?\*\/) - \b[A-Za-z_][A-Za-z0-9_]*\b(?=<) diff --git a/FModel/Settings/UserSettings.cs b/FModel/Settings/UserSettings.cs index 441042640..bca1427de 100644 --- a/FModel/Settings/UserSettings.cs +++ b/FModel/Settings/UserSettings.cs @@ -119,6 +119,13 @@ public string AudioDirectory set => SetProperty(ref _audioDirectory, value); } + private string _codeDirectory; + public string CodeDirectory + { + get => _codeDirectory; + set => SetProperty(ref _codeDirectory, value); + } + private string _modelDirectory; public string ModelDirectory { diff --git a/FModel/ViewModels/ApiEndpoints/DillyApiEndpoints.cs b/FModel/ViewModels/ApiEndpoints/DillyApiEndpoints.cs index 926061bc1..630c08167 100644 --- a/FModel/ViewModels/ApiEndpoints/DillyApiEndpoints.cs +++ b/FModel/ViewModels/ApiEndpoints/DillyApiEndpoints.cs @@ -11,6 +11,7 @@ namespace FModel.ViewModels.ApiEndpoints; public class DillyApiEndpoint : AbstractApiProvider { private Backup[] _backups; + private ManifestInfoDilly[] _manifests; public DillyApiEndpoint(RestClient client) : base(client) { } @@ -27,6 +28,19 @@ public Backup[] GetBackups(CancellationToken token) return _backups ??= GetBackupsAsync(token).GetAwaiter().GetResult(); } + public async Task GetManifestsAsync(CancellationToken token) + { + var request = new FRestRequest($"https://export-service-new.dillyapis.com/v1/manifests"); + var response = await _client.ExecuteAsync(request, token).ConfigureAwait(false); + Log.Information("[{Method}] [{Status}({StatusCode})] '{Resource}'", request.Method, response.StatusDescription, (int) response.StatusCode, response.ResponseUri?.OriginalString); + return response.Data; + } + + public ManifestInfoDilly[] GetManifests(CancellationToken token) + { + return _manifests ??= GetManifestsAsync(token).GetAwaiter().GetResult(); + } + public async Task>> GetHotfixesAsync(CancellationToken token, string language = "en") { var request = new FRestRequest("https://api.fortniteapi.com/v1/cloudstorage/hotfixes") diff --git a/FModel/ViewModels/ApiEndpoints/Models/FModelResponse.cs b/FModel/ViewModels/ApiEndpoints/Models/FModelResponse.cs index 67c6f6a4a..598f070c3 100644 --- a/FModel/ViewModels/ApiEndpoints/Models/FModelResponse.cs +++ b/FModel/ViewModels/ApiEndpoints/Models/FModelResponse.cs @@ -23,6 +23,13 @@ public class Backup [J] public string Url { get; private set; } } +[DebuggerDisplay("{" + nameof(AppName) + "}")] +public class ManifestInfoDilly +{ + [J] public string AppName { get; private set; } + [J] public string DownloadUrl { get; private set; } +} + public class Donator { [J] public string Username { get; private set; } diff --git a/FModel/ViewModels/ApplicationViewModel.cs b/FModel/ViewModels/ApplicationViewModel.cs index 51aa8bfe3..42b24d90b 100644 --- a/FModel/ViewModels/ApplicationViewModel.cs +++ b/FModel/ViewModels/ApplicationViewModel.cs @@ -104,7 +104,7 @@ public ApplicationViewModel() if (UserSettings.Default.CurrentDir is null) { //If no game is selected, many things will break before a shutdown request is processed in the normal way. - //A hard exit is preferable to an unhandled expection in this case + //A hard exit is preferable to an unhandled exception in this case Environment.Exit(0); } @@ -126,7 +126,6 @@ public ApplicationViewModel() if (sender is not IAesVfsReader reader) return; CUE4Parse.GameDirectory.Disable(reader); }; - CustomDirectories = new CustomDirectoriesViewModel(); SettingsView = new SettingsViewModel(); AesManager = new AesManagerViewModel(CUE4Parse); diff --git a/FModel/ViewModels/CUE4ParseViewModel.cs b/FModel/ViewModels/CUE4ParseViewModel.cs index 3fa86b0fb..3896d5825 100644 --- a/FModel/ViewModels/CUE4ParseViewModel.cs +++ b/FModel/ViewModels/CUE4ParseViewModel.cs @@ -225,14 +225,12 @@ public CUE4ParseViewModel() public async Task Initialize() { - await _apiEndpointView.EpicApi.VerifyAuth(CancellationToken.None); await _threadWorkerView.Begin(cancellationToken => { Provider.OnDemandOptions = new IoStoreOnDemandOptions { ChunkHostUri = new Uri("https://download.epicgames.com/", UriKind.Absolute), ChunkCacheDirectory = Directory.CreateDirectory(Path.Combine(UserSettings.Default.OutputDirectory, ".data")), - Authorization = new AuthenticationHeaderValue("Bearer", UserSettings.Default.LastAuthResponse.AccessToken), Timeout = TimeSpan.FromSeconds(30) }; @@ -287,6 +285,20 @@ await _threadWorkerView.Begin(cancellationToken => it => new FRandomAccessStreamArchive(it, manifest.FindFile(it)!.GetStream(), p.Versions)); }); + var manifests = _apiEndpointView.DillyApi.GetManifests(cancellationToken); + var downloadUrl = manifests.First(x => x.AppName == "Fortnite_Studio").DownloadUrl; + + using var client = new HttpClient(); + var manifestBytes = client.GetByteArrayAsync(downloadUrl).GetAwaiter().GetResult(); + + var uefnManifest = FBuildPatchAppManifest.Deserialize(manifestBytes, manifestOptions); + + Parallel.ForEach(uefnManifest.Files.Where(x => _fnLiveRegex.IsMatch(x.FileName)), fileManifest => + { + p.RegisterVfs(fileManifest.FileName, [fileManifest.GetStream()], + it => new FRandomAccessStreamArchive(it, uefnManifest.FindFile(it)!.GetStream(), p.Versions)); + }); + var elapsedTime = Stopwatch.GetElapsedTime(startTs); FLogger.Append(ELog.Information, () => FLogger.Text($"Fortnite [LIVE] has been loaded successfully in {elapsedTime.TotalMilliseconds:F1}ms", Constants.WHITE, true)); @@ -622,6 +634,9 @@ public void AnimationFolder(CancellationToken cancellationToken, TreeItem folder 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; @@ -635,6 +650,7 @@ public void Extract(CancellationToken cancellationToken, GameFile entry, bool ad var saveProperties = HasFlag(bulk, EBulkType.Properties); var saveTextures = HasFlag(bulk, EBulkType.Textures); var saveAudio = HasFlag(bulk, EBulkType.Audio); + var saveDecompiled = HasFlag(bulk, EBulkType.Code); switch (entry.Extension) { case "uasset": @@ -649,6 +665,13 @@ public void Extract(CancellationToken cancellationToken, GameFile entry, bool ad if (saveProperties) break; // do not search for viewable exports if we are dealing with jsons } + if (saveDecompiled) + { + if (Decompile(entry, false)) + TabControl.SelectedTab.SaveDecompiled(updateUi); + break; + } + for (var i = result.InclusiveStart; i < result.ExclusiveEnd; i++) { if (CheckExport(cancellationToken, result.Package, i, bulk)) @@ -1363,11 +1386,13 @@ public void FindReferences(GameFile entry) } - public void Decompile(GameFile entry) + public bool Decompile(GameFile entry, bool AddTab = true) { - ApplicationService.ApplicationView.IsAssetsExplorerVisible = false; - - if (TabControl.CanAddTabs) TabControl.AddTab(entry); + if (TabControl.CanAddTabs && AddTab) + { + ApplicationService.ApplicationView.IsAssetsExplorerVisible = false; + TabControl.AddTab(entry); + } else TabControl.SelectedTab.SoftReset(entry); TabControl.SelectedTab.TitleExtra = "Decompiled"; @@ -1405,9 +1430,11 @@ public void Decompile(GameFile entry) cpp = Regex.Replace(cpp, "__verse_0x[a-fA-F0-9]{8}_", ""); // UnmangleCasedName } cpp = Regex.Replace(cpp, @"CallFunc_([A-Za-z0-9_]+)_ReturnValue", "$1"); - + cpp = Regex.Replace(cpp, @"K2Node_DynamicCast_([A-Za-z0-9_]+)", "$1"); + cpp = Regex.Replace(cpp, @"K2Node_([A-Za-z0-9_]+)", "$1"); TabControl.SelectedTab.SetDocumentText(cpp, false, false); + return cpp.Length > 0; } private void SaveAndPlaySound(CancellationToken cancellationToken, string fullPath, string ext, byte[] data, bool saveAudio, bool updateUi) diff --git a/FModel/ViewModels/Commands/RightClickMenuCommand.cs b/FModel/ViewModels/Commands/RightClickMenuCommand.cs index 6731918da..b490957c0 100644 --- a/FModel/ViewModels/Commands/RightClickMenuCommand.cs +++ b/FModel/ViewModels/Commands/RightClickMenuCommand.cs @@ -69,6 +69,7 @@ public override async void Execute(ApplicationViewModel contextViewModel, object "Save_Models" => (EAction.Export, EShowAssetType.None, EBulkType.Meshes), "Save_Animations" => (EAction.Export, EShowAssetType.None, EBulkType.Animations), "Save_Audio" => (EAction.Export, EShowAssetType.None, EBulkType.Audio), + "Save_Code" => (EAction.Export, EShowAssetType.None, EBulkType.Code), _ => throw new ArgumentOutOfRangeException("Unsupported asset action."), }; @@ -109,6 +110,7 @@ await _threadWorkerView.Begin(cancellationToken => EBulkType.Meshes => (UserSettings.Default.ModelDirectory, "models"), EBulkType.Animations => (UserSettings.Default.ModelDirectory, "animations"), EBulkType.Audio => (UserSettings.Default.AudioDirectory, "audio files"), + EBulkType.Code => (UserSettings.Default.CodeDirectory, "code files"), _ => (null, null), }; diff --git a/FModel/ViewModels/SettingsViewModel.cs b/FModel/ViewModels/SettingsViewModel.cs index becbf3a2b..626f4227d 100644 --- a/FModel/ViewModels/SettingsViewModel.cs +++ b/FModel/ViewModels/SettingsViewModel.cs @@ -195,6 +195,7 @@ public ulong CriwareDecryptionKey private string _propertiesSnapshot; private string _textureSnapshot; private string _audioSnapshot; + private string _codeSnapshot; private string _modelSnapshot; private string _gameSnapshot; private ETexturePlatform _uePlatformSnapshot; @@ -227,6 +228,7 @@ public void Initialize() _propertiesSnapshot = UserSettings.Default.PropertiesDirectory; _textureSnapshot = UserSettings.Default.TextureDirectory; _audioSnapshot = UserSettings.Default.AudioDirectory; + _codeSnapshot = UserSettings.Default.CodeDirectory; _modelSnapshot = UserSettings.Default.ModelDirectory; _gameSnapshot = UserSettings.Default.GameDirectory; _uePlatformSnapshot = UserSettings.Default.CurrentDir.TexturePlatform; @@ -303,12 +305,6 @@ public bool Save(out List whatShouldIDo) if (_ueGameSnapshot != SelectedUeGame || _customVersionsSnapshot != SelectedCustomVersions || _uePlatformSnapshot != SelectedUePlatform || _optionsSnapshot != SelectedOptions || // combobox _mapStructTypesSnapshot != SelectedMapStructTypes || - _outputSnapshot != UserSettings.Default.OutputDirectory || // textbox - _rawDataSnapshot != UserSettings.Default.RawDataDirectory || // textbox - _propertiesSnapshot != UserSettings.Default.PropertiesDirectory || // textbox - _textureSnapshot != UserSettings.Default.TextureDirectory || // textbox - _audioSnapshot != UserSettings.Default.AudioDirectory || // textbox - _modelSnapshot != UserSettings.Default.ModelDirectory || // textbox _gameSnapshot != UserSettings.Default.GameDirectory) // textbox restart = true; diff --git a/FModel/ViewModels/TabControlViewModel.cs b/FModel/ViewModels/TabControlViewModel.cs index 748dd1ae0..dbeb44239 100644 --- a/FModel/ViewModels/TabControlViewModel.cs +++ b/FModel/ViewModels/TabControlViewModel.cs @@ -409,7 +409,17 @@ public void SaveProperty(bool updateUi) Application.Current.Dispatcher.Invoke(() => File.WriteAllText(directory, Document.Text)); SaveCheck(directory, fileName, updateUi); } + public void SaveDecompiled(bool updateUi) + { + var fileName = Path.ChangeExtension(Entry.Name, ".cpp"); + var directory = Path.Combine(UserSettings.Default.PropertiesDirectory, + UserSettings.Default.KeepDirectoryStructure ? Entry.Directory : "", fileName).Replace('\\', '/'); + + Directory.CreateDirectory(directory.SubstringBeforeLast('/')); + Application.Current.Dispatcher.Invoke(() => File.WriteAllText(directory, Document.Text)); + SaveCheck(directory, fileName, updateUi); + } private void SaveCheck(string path, string fileName, bool updateUi) { if (File.Exists(path)) diff --git a/FModel/ViewModels/UpdateViewModel.cs b/FModel/ViewModels/UpdateViewModel.cs index adbc79bd7..26acdb88a 100644 --- a/FModel/ViewModels/UpdateViewModel.cs +++ b/FModel/ViewModels/UpdateViewModel.cs @@ -81,6 +81,9 @@ private Task LoadCoAuthors() if (username.Equals("Asval", StringComparison.OrdinalIgnoreCase)) { username = "4sval"; // found out the hard way co-authored usernames can't be trusted + } else if (username.Equals("Krowe Moh", StringComparison.OrdinalIgnoreCase)) + { + username = "Krowe-moh"; } coAuthorMap[commit].Add(username); @@ -101,7 +104,7 @@ private Task LoadCoAuthors() } catch { - // + // Ignore } } diff --git a/FModel/Views/Resources/Controls/ContextMenus/FolderContextMenu.xaml b/FModel/Views/Resources/Controls/ContextMenus/FolderContextMenu.xaml index ca5a58715..26aa40745 100644 --- a/FModel/Views/Resources/Controls/ContextMenus/FolderContextMenu.xaml +++ b/FModel/Views/Resources/Controls/ContextMenus/FolderContextMenu.xaml @@ -65,6 +65,26 @@ + + + + + + + + + + + + + + + diff --git a/FModel/Views/Resources/Controls/EndpointEditor.xaml.cs b/FModel/Views/Resources/Controls/EndpointEditor.xaml.cs index 215010d13..f875d2630 100644 --- a/FModel/Views/Resources/Controls/EndpointEditor.xaml.cs +++ b/FModel/Views/Resources/Controls/EndpointEditor.xaml.cs @@ -93,7 +93,7 @@ private void OnSyntax(object sender, RoutedEventArgs e) private void OnEvaluator(object sender, RoutedEventArgs e) { - Process.Start(new ProcessStartInfo { FileName = "https://jsonpath.herokuapp.com/", UseShellExecute = true }); + Process.Start(new ProcessStartInfo { FileName = "https://jsonpath.com/", UseShellExecute = true }); } } diff --git a/FModel/Views/Resources/Converters/FileNameWithoutExtensionConverter.cs b/FModel/Views/Resources/Converters/FileNameWithoutExtensionConverter.cs new file mode 100644 index 000000000..c1fac76f5 --- /dev/null +++ b/FModel/Views/Resources/Converters/FileNameWithoutExtensionConverter.cs @@ -0,0 +1,18 @@ +using System; +using System.Globalization; +using System.IO; +using System.Windows.Data; + +namespace FModel.Views.Resources.Converters +{ + public class FileNameWithoutExtensionConverter : IValueConverter + { + public static readonly FileNameWithoutExtensionConverter Instance = new(); + + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + => value is string s ? Path.GetFileNameWithoutExtension(s) : value; + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + => throw new NotImplementedException(); + } +} diff --git a/FModel/Views/Resources/Converters/StringToGameConverter.cs b/FModel/Views/Resources/Converters/StringToGameConverter.cs index 701813359..dbd01a964 100644 --- a/FModel/Views/Resources/Converters/StringToGameConverter.cs +++ b/FModel/Views/Resources/Converters/StringToGameConverter.cs @@ -1,7 +1,6 @@ using System; using System.Globalization; using System.Windows.Data; -using FModel.Extensions; namespace FModel.Views.Resources.Converters; diff --git a/FModel/Views/Resources/Converters/TextToRefreshConverter.cs b/FModel/Views/Resources/Converters/TextToRefreshConverter.cs new file mode 100644 index 000000000..0f3f8ec01 --- /dev/null +++ b/FModel/Views/Resources/Converters/TextToRefreshConverter.cs @@ -0,0 +1,23 @@ +using System; +using System.Globalization; +using System.Windows.Data; + +namespace FModel.Views.Resources.Converters; + +public class TextToRefreshConverter : IValueConverter +{ + public static readonly TextToRefreshConverter Instance = new(); + + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is DateTime dt && dt != DateTime.MaxValue) + return $"Next Refresh: {dt:MMM d, yyyy}"; + + return "Next Refresh: Never"; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} diff --git a/FModel/Views/Resources/Resources.xaml b/FModel/Views/Resources/Resources.xaml index 715a5b1f9..c33eb148f 100644 --- a/FModel/Views/Resources/Resources.xaml +++ b/FModel/Views/Resources/Resources.xaml @@ -771,9 +771,15 @@ - + + + + diff --git a/FModel/Views/SettingsView.xaml b/FModel/Views/SettingsView.xaml index 965eef136..b38b62d5d 100644 --- a/FModel/Views/SettingsView.xaml +++ b/FModel/Views/SettingsView.xaml @@ -695,7 +695,7 @@ -