diff --git a/CUE4Parse b/CUE4Parse index 3b2a6867..22dbf59c 160000 --- a/CUE4Parse +++ b/CUE4Parse @@ -1 +1 @@ -Subproject commit 3b2a686787bb08c28940271709897957b759a6f4 +Subproject commit 22dbf59c942fbc8d544fc2ee51bb96577fae247b diff --git a/FModel/Extensions/AvalonExtensions.cs b/FModel/Extensions/AvalonExtensions.cs index bb5b7db6..6cc7968a 100644 --- a/FModel/Extensions/AvalonExtensions.cs +++ b/FModel/Extensions/AvalonExtensions.cs @@ -1,11 +1,19 @@ using System.Reflection; using System.Runtime.CompilerServices; using System.Xml; +using FModel.Extensions.Themes; using ICSharpCode.AvalonEdit.Highlighting; using ICSharpCode.AvalonEdit.Highlighting.Xshd; namespace FModel.Extensions; +public sealed record HighlightColor +( + string Foreground, + bool Bold = false, + bool Italic = false +); + public static class AvalonExtensions { private static readonly IHighlightingDefinition _jsonHighlighter = LoadHighlighter("Json.xshd"); @@ -53,6 +61,7 @@ public static class AvalonExtensions case "po": return null; default: + _jsonHighlighter.ApplyJsonTheme(Settings.UserSettings.Default.JsonHighlightTheme); return _jsonHighlighter; } } diff --git a/FModel/Extensions/Themes/HighlightingThemeExtensions.cs b/FModel/Extensions/Themes/HighlightingThemeExtensions.cs new file mode 100644 index 00000000..8edbac2d --- /dev/null +++ b/FModel/Extensions/Themes/HighlightingThemeExtensions.cs @@ -0,0 +1,34 @@ +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; + +public static class HighlightingThemeExtensions +{ + public static void ApplyJsonTheme(this IHighlightingDefinition highlighter, EJsonHighlightTheme theme) + { + var palette = Get(theme); + + Apply(highlighter, "FieldName", palette.FieldName); + Apply(highlighter, "String", palette.String); + Apply(highlighter, "Number", palette.Number); + Apply(highlighter, "Bool", palette.Bool); + Apply(highlighter, "Null", palette.Null); + Apply(highlighter, "Punctuation", palette.Punctuation); + Apply(highlighter, "Escape", palette.Escape); + } + + private static void Apply(IHighlightingDefinition highlighter, string name, HighlightColor color) + { + var namedColor = highlighter.GetNamedColor(name); + if (namedColor is null) + return; + + namedColor.Foreground = new SimpleHighlightingBrush((Color) ColorConverter.ConvertFromString(color.Foreground)); + namedColor.FontWeight = color.Bold ? FontWeights.Bold : null; + namedColor.FontStyle = color.Italic ? FontStyles.Italic : null; + } +} diff --git a/FModel/Extensions/Themes/JsonHighlightThemes.cs b/FModel/Extensions/Themes/JsonHighlightThemes.cs new file mode 100644 index 00000000..66329d6d --- /dev/null +++ b/FModel/Extensions/Themes/JsonHighlightThemes.cs @@ -0,0 +1,203 @@ +using System.ComponentModel; + +namespace FModel.Extensions.Themes; + +public enum EJsonHighlightTheme +{ + [Description("Default")] + Default, + [Description("Mint Lavender")] + MintLavender, + [Description("Soft Blue")] + SoftBlueGreen, + [Description("Purple Cyan")] + PurpleCyan, + [Description("Neutral Warm")] + NeutralWarm, + [Description("Nord")] + Nord, + [Description("Mocha")] + Mocha, + [Description("Tokyo Night")] + TokyoNight, + [Description("One Dark")] + OneDark, + [Description("Gruvbox Dark")] + GruvboxDark, + [Description("Rosé Pine")] + RosePine, + [Description("Monokai")] + Monokai, + [Description("Oceanic")] + Oceanic, + [Description("Forest")] + Forest, + [Description("Amber")] + Amber, + [Description("Iceberg")] + Iceberg +} + +public static class JsonHighlightThemes +{ + public sealed record JsonHighlightPalette( + HighlightColor FieldName, + HighlightColor String, + HighlightColor Number, + HighlightColor Bool, + HighlightColor Null, + HighlightColor Punctuation, + HighlightColor Escape + ); + + public static JsonHighlightPalette Get(EJsonHighlightTheme theme) => theme switch + { + EJsonHighlightTheme.MintLavender => new JsonHighlightPalette( + FieldName: new HighlightColor("#CBA6F7"), + String: new HighlightColor("#8FE3CF"), + Number: new HighlightColor("#FFD166"), + Bool: new HighlightColor("#FF9CAC", Bold: true), + Null: new HighlightColor("#A7B0C0", Italic: true), + Punctuation: new HighlightColor("#D7DEE9"), + Escape: new HighlightColor("#5DD9C1", Bold: true) + ), + EJsonHighlightTheme.SoftBlueGreen => new JsonHighlightPalette( + FieldName: new HighlightColor("#7CC7FF"), + String: new HighlightColor("#A6E3A1"), + Number: new HighlightColor("#FFA36C"), + Bool: new HighlightColor("#FF6B8A", Bold: true), + Null: new HighlightColor("#8FA3BF", Italic: true), + Punctuation: new HighlightColor("#C7D0DD"), + Escape: new HighlightColor("#66E3D4", Bold: true) + ), + EJsonHighlightTheme.PurpleCyan => new JsonHighlightPalette( + FieldName: new HighlightColor("#BD93F9"), + String: new HighlightColor("#7EE7F2"), + Number: new HighlightColor("#FFB86C"), + Bool: new HighlightColor("#FF6B8B", Bold: true), + Null: new HighlightColor("#9AA7B8", Italic: true), + Punctuation: new HighlightColor("#CBD5E1"), + Escape: new HighlightColor("#5DE4C7", Bold: true) + ), + + EJsonHighlightTheme.NeutralWarm => new JsonHighlightPalette( + FieldName: new HighlightColor("#F8C291"), + String: new HighlightColor("#A3E635"), + Number: new HighlightColor("#FB923C"), + Bool: new HighlightColor("#F87171", Bold: true), + Null: new HighlightColor("#9CA3AF", Italic: true), + Punctuation: new HighlightColor("#D1D5DB"), + Escape: new HighlightColor("#67E8F9", Bold: true) + ), + + EJsonHighlightTheme.Nord => new JsonHighlightPalette( + FieldName: new HighlightColor("#88C0D0"), + String: new HighlightColor("#A3BE8C"), + Number: new HighlightColor("#D08770"), + Bool: new HighlightColor("#BF616A", Bold: true), + Null: new HighlightColor("#81A1C1", Italic: true), + Punctuation: new HighlightColor("#D8DEE9"), + Escape: new HighlightColor("#8FBCBB", Bold: true) + ), + EJsonHighlightTheme.Default => new JsonHighlightPalette( + FieldName: new HighlightColor("#FFCB6B"), + String: new HighlightColor("#C3E88D"), + Number: new HighlightColor("#F78C6C"), + Bool: new HighlightColor("#61AFEF", Bold: true), + Null: new HighlightColor("#7F848E", Italic: true), + Punctuation: new HighlightColor("#89DDFF"), + Escape: new HighlightColor("#4DD0E1", Bold: true) + ), + EJsonHighlightTheme.Mocha => new JsonHighlightPalette( + FieldName: new HighlightColor("#CBA6F7"), + String: new HighlightColor("#A6E3A1"), + Number: new HighlightColor("#FAB387"), + Bool: new HighlightColor("#F38BA8", Bold: true), + Null: new HighlightColor("#9399B2", Italic: true), + Punctuation: new HighlightColor("#CDD6F4"), + Escape: new HighlightColor("#94E2D5", Bold: true) + ), + EJsonHighlightTheme.TokyoNight => new JsonHighlightPalette( + FieldName: new HighlightColor("#7AA2F7"), + String: new HighlightColor("#9ECE6A"), + Number: new HighlightColor("#FF9E64"), + Bool: new HighlightColor("#F7768E", Bold: true), + Null: new HighlightColor("#565F89", Italic: true), + Punctuation: new HighlightColor("#A9B1D6"), + Escape: new HighlightColor("#7DCFFF", Bold: true) + ), + EJsonHighlightTheme.OneDark => new JsonHighlightPalette( + FieldName: new HighlightColor("#E5C07B"), + String: new HighlightColor("#98C379"), + Number: new HighlightColor("#D19A66"), + Bool: new HighlightColor("#61AFEF", Bold: true), + Null: new HighlightColor("#5C6370", Italic: true), + Punctuation: new HighlightColor("#ABB2BF"), + Escape: new HighlightColor("#56B6C2", Bold: true) + ), + EJsonHighlightTheme.GruvboxDark => new JsonHighlightPalette( + FieldName: new HighlightColor("#FABD2F"), + String: new HighlightColor("#B8BB26"), + Number: new HighlightColor("#FE8019"), + Bool: new HighlightColor("#FB4934", Bold: true), + Null: new HighlightColor("#928374", Italic: true), + Punctuation: new HighlightColor("#EBDBB2"), + Escape: new HighlightColor("#8EC07C", Bold: true) + ), + EJsonHighlightTheme.RosePine => new JsonHighlightPalette( + FieldName: new HighlightColor("#C4A7E7"), + String: new HighlightColor("#9CCFD8"), + Number: new HighlightColor("#F6C177"), + Bool: new HighlightColor("#EB6F92", Bold: true), + Null: new HighlightColor("#6E6A86", Italic: true), + Punctuation: new HighlightColor("#E0DEF4"), + Escape: new HighlightColor("#31748F", Bold: true) + ), + EJsonHighlightTheme.Monokai => new JsonHighlightPalette( + FieldName: new HighlightColor("#A6E22E"), + String: new HighlightColor("#E6DB74"), + Number: new HighlightColor("#AE81FF"), + Bool: new HighlightColor("#F92672", Bold: true), + Null: new HighlightColor("#75715E", Italic: true), + Punctuation: new HighlightColor("#F8F8F2"), + Escape: new HighlightColor("#66D9EF", Bold: true) + ), + EJsonHighlightTheme.Oceanic => new JsonHighlightPalette( + FieldName: new HighlightColor("#5FB3B3"), + String: new HighlightColor("#99C794"), + Number: new HighlightColor("#F99157"), + Bool: new HighlightColor("#EC5F67", Bold: true), + Null: new HighlightColor("#65737E", Italic: true), + Punctuation: new HighlightColor("#D8DEE9"), + Escape: new HighlightColor("#6699CC", Bold: true) + ), + EJsonHighlightTheme.Forest => new JsonHighlightPalette( + FieldName: new HighlightColor("#86EFAC"), + String: new HighlightColor("#B4D455"), + Number: new HighlightColor("#FACC15"), + Bool: new HighlightColor("#FB7185", Bold: true), + Null: new HighlightColor("#6B7280", Italic: true), + Punctuation: new HighlightColor("#D1FAE5"), + Escape: new HighlightColor("#34D399", Bold: true) + ), + EJsonHighlightTheme.Amber => new JsonHighlightPalette( + FieldName: new HighlightColor("#FBBF24"), + String: new HighlightColor("#D9F99D"), + Number: new HighlightColor("#FDBA74"), + Bool: new HighlightColor("#F87171", Bold: true), + Null: new HighlightColor("#A8A29E", Italic: true), + Punctuation: new HighlightColor("#FDE68A"), + Escape: new HighlightColor("#67E8F9", Bold: true) + ), + EJsonHighlightTheme.Iceberg => new JsonHighlightPalette( + FieldName: new HighlightColor("#84A0C6"), + String: new HighlightColor("#B4BE82"), + Number: new HighlightColor("#E2A478"), + Bool: new HighlightColor("#E27878", Bold: true), + Null: new HighlightColor("#6B7089", Italic: true), + Punctuation: new HighlightColor("#D2D4DE"), + Escape: new HighlightColor("#89B8C2", Bold: true) + ), + _ => Get(EJsonHighlightTheme.Default) + }; +} diff --git a/FModel/Settings/UserSettings.cs b/FModel/Settings/UserSettings.cs index 63b0fd63..091ca394 100644 --- a/FModel/Settings/UserSettings.cs +++ b/FModel/Settings/UserSettings.cs @@ -8,6 +8,7 @@ using CUE4Parse.UE4.Versions; 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; using FModel.ViewModels.ApiEndpoints.Models; @@ -304,6 +305,13 @@ namespace FModel.Settings } } + private EJsonHighlightTheme _jsonHighlightTheme; + public EJsonHighlightTheme JsonHighlightTheme + { + get => _jsonHighlightTheme; + set => SetProperty(ref _jsonHighlightTheme, value); + } + private IDictionary _perDirectory = new Dictionary(); public IDictionary PerDirectory { diff --git a/FModel/ViewModels/AssetsFolderViewModel.cs b/FModel/ViewModels/AssetsFolderViewModel.cs index 8a4bfb22..d5af8e98 100644 --- a/FModel/ViewModels/AssetsFolderViewModel.cs +++ b/FModel/ViewModels/AssetsFolderViewModel.cs @@ -218,6 +218,17 @@ public class AssetsFolderViewModel var treeItems = new RangeObservableCollection(); treeItems.SetSuppressionState(true); + static TreeItem FindByHeaderOrNull(IReadOnlyList list, string header) + { + for (var i = 0; i < list.Count; i++) + { + if (list[i].Header == header) + return list[i]; + } + + return null; + } + foreach (var entry in entries) { TreeItem lastNode = null; @@ -226,23 +237,31 @@ public class AssetsFolderViewModel var builder = new StringBuilder(64); var parentNode = treeItems; + if (folders.Length <= 1) + { + var rootNode = FindByHeaderOrNull(treeItems, "Content"); + if (rootNode == null) + { + rootNode = new TreeItem("Content", entry, "Content") + { + Parent = null + }; + + rootNode.Folders.SetSuppressionState(true); + rootNode.AssetsList.Assets.SetSuppressionState(true); + treeItems.Add(rootNode); + } + + rootNode.AssetsList.Add(entry); + continue; + } + for (var i = 0; i < folders.Length - 1; i++) { var folder = folders[i]; builder.Append(folder).Append('/'); lastNode = FindByHeaderOrNull(parentNode, folder); - static TreeItem FindByHeaderOrNull(IReadOnlyList list, string header) - { - for (var i = 0; i < list.Count; i++) - { - if (list[i].Header == header) - return list[i]; - } - - return null; - } - if (lastNode == null) { var nodePath = builder.ToString(); diff --git a/FModel/ViewModels/CUE4ParseViewModel.cs b/FModel/ViewModels/CUE4ParseViewModel.cs index 2401fd7a..99afa0d5 100644 --- a/FModel/ViewModels/CUE4ParseViewModel.cs +++ b/FModel/ViewModels/CUE4ParseViewModel.cs @@ -212,6 +212,7 @@ public class CUE4ParseViewModel : ViewModel 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(); @@ -335,6 +336,7 @@ public class CUE4ParseViewModel : ViewModel } Provider.Initialize(); + GameDirectory.AddLooseFiles(Provider.LooseFileCount); _wwiseProviderLazy = new Lazy(() => new WwiseProvider(Provider, UserSettings.Default.GameDirectory)); _fmodProviderLazy = new Lazy(() => new FModProvider(Provider, UserSettings.Default.GameDirectory)); _criWareProviderLazy = new Lazy(() => new CriWareProvider(Provider, UserSettings.Default.GameDirectory)); diff --git a/FModel/ViewModels/Commands/LoadCommand.cs b/FModel/ViewModels/Commands/LoadCommand.cs index fc4ad6cb..f476ea24 100644 --- a/FModel/ViewModels/Commands/LoadCommand.cs +++ b/FModel/ViewModels/Commands/LoadCommand.cs @@ -113,6 +113,7 @@ public class LoadCommand : ViewModelCommand private void FilterDirectoryFilesToDisplay(CancellationToken cancellationToken, IEnumerable directoryFiles) { HashSet filter; + var includeLooseFiles = false; if (directoryFiles == null) filter = null; else { @@ -120,11 +121,17 @@ public class LoadCommand : ViewModelCommand foreach (var directoryFile in directoryFiles) { if (!directoryFile.IsEnabled) continue; + if (directoryFile.IsLooseFilesContainer) + { + includeLooseFiles = true; + continue; + } filter.Add(directoryFile.Name); } } var hasFilter = filter != null && filter.Count != 0; + var hasSelection = hasFilter || includeLooseFiles; var entries = new List(); foreach (var asset in _applicationView.CUE4Parse.Provider.Files.Values) @@ -132,12 +139,16 @@ public class LoadCommand : ViewModelCommand cancellationToken.ThrowIfCancellationRequested(); // cancel if needed if (asset.IsUePackagePayload) continue; - if (hasFilter) + if (hasSelection) { if (asset is VfsEntry entry && filter.Contains(entry.Vfs.Name)) { entries.Add(asset); } + else if (includeLooseFiles && asset is OsGameFile) + { + entries.Add(asset); + } } else { diff --git a/FModel/ViewModels/GameDirectoryViewModel.cs b/FModel/ViewModels/GameDirectoryViewModel.cs index 730b7895..1d358e6c 100644 --- a/FModel/ViewModels/GameDirectoryViewModel.cs +++ b/FModel/ViewModels/GameDirectoryViewModel.cs @@ -56,6 +56,13 @@ public class FileItem : ViewModel set => SetProperty(ref _isEnabled, value); } + private bool _isLooseFilesContainer; + public bool IsLooseFilesContainer + { + get => _isLooseFilesContainer; + set => SetProperty(ref _isLooseFilesContainer, value); + } + private string _key; public string Key { @@ -83,6 +90,18 @@ public class FileItem : ViewModel Length = length; } + public FileItem(string name, int fileCount, long length, bool isLooseFile) + { + Name = name; + Length = length; + FileCount = fileCount; + IsLooseFilesContainer = isLooseFile; + IsEnabled = true; + Key = string.Empty; + MountPoint = string.Empty; + CompressionMethods = []; + } + public FileItem(IAesVfsReader reader) { Name = reader.Name; @@ -90,6 +109,7 @@ public class FileItem : ViewModel Guid = reader.EncryptionKeyGuid; IsEncrypted = reader.IsEncrypted; IsEnabled = false; + IsLooseFilesContainer = false; Key = string.Empty; FileCount = reader is IoStoreReader storeReader ? (int) storeReader.TocResource.Header.TocEntryCount - 1 : 0; CompressionMethods = reader.CompressionMethods; @@ -101,19 +121,25 @@ public class FileItem : ViewModel } } -public class GameDirectoryViewModel : ViewModel +public partial class GameDirectoryViewModel : ViewModel { - public bool HasNoFile => DirectoryFiles.Count < 1; public readonly ObservableCollection DirectoryFiles; + public ICollectionView DirectoryFilesView { get; } - private readonly Regex _hiddenArchives = new(@"^(?!global|pakchunk.+(optional|ondemand)\-).+(pak|utoc)$", // should be universal - RegexOptions.Compiled | RegexOptions.Singleline | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); + private readonly Regex _hiddenArchives = ArchivesRegex(); public GameDirectoryViewModel() { - DirectoryFiles = new ObservableCollection(); - DirectoryFilesView = new ListCollectionView(DirectoryFiles) { SortDescriptions = { new SortDescription("Name", ListSortDirection.Ascending) } }; + DirectoryFiles = []; + DirectoryFilesView = new ListCollectionView(DirectoryFiles) + { + SortDescriptions = + { + new SortDescription(nameof(FileItem.IsLooseFilesContainer), ListSortDirection.Ascending), + new SortDescription(nameof(FileItem.Name), ListSortDirection.Ascending) + } + }; } public void Add(IAesVfsReader reader) @@ -124,6 +150,25 @@ public class GameDirectoryViewModel : ViewModel Application.Current.Dispatcher.Invoke(() => DirectoryFiles.Add(fileItem)); } + public void AddLooseFiles(int fileCount) + { + if (fileCount < 1) + return; + + Application.Current.Dispatcher.Invoke(() => + { + var looseFilesContainer = DirectoryFiles.FirstOrDefault(x => x.IsLooseFilesContainer); + if (looseFilesContainer is not null) + { + looseFilesContainer.FileCount += fileCount; + } + else + { + DirectoryFiles.Add(new FileItem("Loose Files", fileCount, 0, true)); + } + }); + } + public void Verify(IAesVfsReader reader) { if (DirectoryFiles.FirstOrDefault(x => x.Name == reader.Name) is not { } file) return; @@ -138,4 +183,7 @@ public class GameDirectoryViewModel : ViewModel if (DirectoryFiles.FirstOrDefault(x => x.Name == reader.Name) is not { } file) return; file.IsEnabled = false; } + + [GeneratedRegex(@"^(?!global|pakchunk.+(optional|ondemand)\-).+(pak|utoc)$", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.Singleline | RegexOptions.CultureInvariant)] + private static partial Regex ArchivesRegex(); } diff --git a/FModel/ViewModels/SettingsViewModel.cs b/FModel/ViewModels/SettingsViewModel.cs index a6e8e395..fd5a97cf 100644 --- a/FModel/ViewModels/SettingsViewModel.cs +++ b/FModel/ViewModels/SettingsViewModel.cs @@ -8,6 +8,7 @@ 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; using FModel.Settings; @@ -102,6 +103,13 @@ public class SettingsViewModel : ViewModel set => SetProperty(ref _selectedCosmeticStyle, value); } + private EJsonHighlightTheme _selectedJsonHighlightTheme; + public EJsonHighlightTheme SelectedJsonHighlightTheme + { + get => _selectedJsonHighlightTheme; + set => SetProperty(ref _selectedJsonHighlightTheme, value); + } + private ulong _criwareDecryptionKey; public ulong CriwareDecryptionKey { @@ -124,6 +132,7 @@ public class SettingsViewModel : ViewModel public ReadOnlyObservableCollection DiscordRpcs { get; private set; } public ReadOnlyObservableCollection CompressedAudios { get; private set; } public ReadOnlyObservableCollection CosmeticStyles { get; private set; } + public ReadOnlyObservableCollection JsonHighlightThemes { get; private set; } private string _outputSnapshot; private string _rawDataSnapshot; @@ -140,6 +149,7 @@ public class SettingsViewModel : ViewModel private ELanguage _assetLanguageSnapshot; private ECompressedAudio _compressedAudioSnapshot; private EIconStyle _cosmeticStyleSnapshot; + private EJsonHighlightTheme _jsonHighlightThemeSnapshot; private bool _mappingsUpdate = false; @@ -176,6 +186,7 @@ public class SettingsViewModel : ViewModel _assetLanguageSnapshot = UserSettings.Default.AssetLanguage; _compressedAudioSnapshot = UserSettings.Default.CompressedAudioMode; _cosmeticStyleSnapshot = UserSettings.Default.CosmeticStyle; + _jsonHighlightThemeSnapshot = UserSettings.Default.JsonHighlightTheme; SelectedUeGame = _ueGameSnapshot; SelectedCustomVersions = _customVersionsSnapshot; @@ -186,6 +197,7 @@ public class SettingsViewModel : ViewModel SelectedCosmeticStyle = _cosmeticStyleSnapshot; CriwareDecryptionKey = _criwareDecryptionKey; UnluacOpcodeMap = _unluacOpcodeMap; + SelectedJsonHighlightTheme = _jsonHighlightThemeSnapshot; SelectedAesReload = UserSettings.Default.AesReload; SelectedDiscordRpc = UserSettings.Default.DiscordRpc; @@ -195,6 +207,7 @@ public class SettingsViewModel : ViewModel DiscordRpcs = new ReadOnlyObservableCollection(new ObservableCollection(EnumerateDiscordRpcs())); CompressedAudios = new ReadOnlyObservableCollection(new ObservableCollection(EnumerateCompressedAudios())); CosmeticStyles = new ReadOnlyObservableCollection(new ObservableCollection(EnumerateCosmeticStyles())); + JsonHighlightThemes = new ReadOnlyObservableCollection(new ObservableCollection(EnumerateJsonHighlightThemes())); } public bool Save(out List whatShouldIDo) @@ -225,6 +238,7 @@ public class SettingsViewModel : ViewModel UserSettings.Default.CosmeticStyle = SelectedCosmeticStyle; UserSettings.Default.AesReload = SelectedAesReload; UserSettings.Default.DiscordRpc = SelectedDiscordRpc; + UserSettings.Default.JsonHighlightTheme = SelectedJsonHighlightTheme; Options.SaveAsUserDefaults(); @@ -244,4 +258,5 @@ public class SettingsViewModel : ViewModel private IEnumerable EnumerateDiscordRpcs() => Enum.GetValues(); private IEnumerable EnumerateCompressedAudios() => Enum.GetValues(); private IEnumerable EnumerateCosmeticStyles() => Enum.GetValues(); + private IEnumerable EnumerateJsonHighlightThemes() => Enum.GetValues(); } diff --git a/FModel/Views/Resources/Icons.xaml b/FModel/Views/Resources/Icons.xaml index 9afc0f6c..2de5598e 100644 --- a/FModel/Views/Resources/Icons.xaml +++ b/FModel/Views/Resources/Icons.xaml @@ -110,6 +110,7 @@ M3.6 15h16.8 M11.5 3a17 17 0 0 0 0 18 M12.5 3a17 17 0 0 1 0 18 + M17.5,12A1.5,1.5 0 0,1 16,10.5A1.5,1.5 0 0,1 17.5,9A1.5,1.5 0 0,1 19,10.5A1.5,1.5 0 0,1 17.5,12M14.5,8A1.5,1.5 0 0,1 13,6.5A1.5,1.5 0 0,1 14.5,5A1.5,1.5 0 0,1 16,6.5A1.5,1.5 0 0,1 14.5,8M9.5,8A1.5,1.5 0 0,1 8,6.5A1.5,1.5 0 0,1 9.5,5A1.5,1.5 0 0,1 11,6.5A1.5,1.5 0 0,1 9.5,8M6.5,12A1.5,1.5 0 0,1 5,10.5A1.5,1.5 0 0,1 6.5,9A1.5,1.5 0 0,1 8,10.5A1.5,1.5 0 0,1 6.5,12M12,3A9,9 0 0,0 3,12A9,9 0 0,0 12,21A1.5,1.5 0 0,0 13.5,19.5C13.5,19.11 13.35,18.76 13.11,18.5C12.88,18.23 12.73,17.88 12.73,17.5A1.5,1.5 0 0,1 14.23,16H16A5,5 0 0,0 21,11C21,6.58 16.97,3 12,3Z M13,9V3.5L18.5,9M6,2C4.89,2 4,2.89 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2H6 ZM12,11A3,3 0 1,0 12,17A3,3 0 0,0 12,11 ZM12,12.5L14,16H13L12,14.5L11,16H10L12,12.5Z diff --git a/FModel/Views/Resources/Resources.xaml b/FModel/Views/Resources/Resources.xaml index 70f44324..4d29a5c4 100644 --- a/FModel/Views/Resources/Resources.xaml +++ b/FModel/Views/Resources/Resources.xaml @@ -101,14 +101,17 @@ @@ -119,14 +122,26 @@ - - + + - - + Width="16" Height="16" HorizontalAlignment="Center" Margin="0 0 4 0" + VerticalAlignment="Center" RenderOptions.BitmapScalingMode="HighQuality" /> + + + @@ -135,6 +150,10 @@ + + + + diff --git a/FModel/Views/SettingsView.xaml b/FModel/Views/SettingsView.xaml index c9e71a9c..36bf65a1 100644 --- a/FModel/Views/SettingsView.xaml +++ b/FModel/Views/SettingsView.xaml @@ -8,6 +8,7 @@ xmlns:adonisUi="clr-namespace:AdonisUI;assembly=AdonisUI" xmlns:adonisControls="clr-namespace:AdonisUI.Controls;assembly=AdonisUI" xmlns:adonisExtensions="clr-namespace:AdonisUI.Extensions;assembly=AdonisUI" + xmlns:avalonEdit="http://icsharpcode.net/sharpdevelop/avalonedit" WindowStartupLocation="CenterScreen" ResizeMode="NoResize" IconVisibility="Collapsed" SizeToContent="Height" MinHeight="{Binding Source={x:Static SystemParameters.MaximizedPrimaryScreenHeight}, Converter={converters:RatioConverter}, ConverterParameter='0.10'}" Width="{Binding Source={x:Static SystemParameters.MaximizedPrimaryScreenWidth}, Converter={converters:RatioConverter}, ConverterParameter='0.45'}" @@ -470,7 +471,7 @@ HotKey="{Binding RemoveAudio, Source={x:Static local:Settings.UserSettings.Default}, Mode=TwoWay}" /> - + @@ -562,6 +563,65 @@ ToolTip="Paste a custom opcode map." /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -652,7 +712,7 @@ - + @@ -660,7 +720,7 @@ - + @@ -674,6 +734,25 @@ + + + + + + + + + + + + diff --git a/FModel/Views/SettingsView.xaml.cs b/FModel/Views/SettingsView.xaml.cs index 3423136d..1fb6ee31 100644 --- a/FModel/Views/SettingsView.xaml.cs +++ b/FModel/Views/SettingsView.xaml.cs @@ -7,10 +7,14 @@ using System.Windows; using System.Windows.Controls; using System.Windows.Documents; using CUE4Parse.UE4.Lua.unluac; +using FModel.Extensions; +using FModel.Extensions.Themes; +using FModel.Framework; using FModel.Services; using FModel.Settings; using FModel.ViewModels; using FModel.Views.Resources.Controls; +using ICSharpCode.AvalonEdit; using Microsoft.Win32; using Ookii.Dialogs.Wpf; @@ -19,6 +23,7 @@ namespace FModel.Views; public partial class SettingsView { private ApplicationViewModel _applicationView => ApplicationService.ApplicationView; + private SettingsViewModel _settingsView => _applicationView.SettingsView; public SettingsView() { @@ -294,4 +299,64 @@ public partial class SettingsView UserSettings.Default.UnluacFlags = isChecked ? (current | flag) : (current & ~flag); } + + private const string JsonThemePreviewText = + """ + { + "title": "This is an example JSON", + "environment": "production", + "enabled": true, + "version": 4, + "scale": 0.92, + "features": { + "previewAssets": true, + "autoSave": false, + "maxRecentFiles": 12 + }, + "export": { + "rootDirectory": "C:\\Exports\\Assets", + "keepDirectoryStructure": true, + "formats": [ + "json", + "png", + "wav" + ] + }, + "paths": [ + "/Game/Characters/Hero", + "/Game/UI/Widgets", + "/Game/Audio/Music" + ], + "metadata": { + "lastOpened": "2026-06-20T14:30:00Z", + "experimental": false, + "fallbackTheme": null, + "escapeExample": "Line one\nLine two\tTabbed", + "accentColor": "#FFC857" + } + } + """; + + private void OnJsonThemePreviewLoaded(object sender, RoutedEventArgs e) + { + if (sender is not TextEditor editor) + return; + + editor.SyntaxHighlighting = AvalonExtensions.HighlighterSelector("json"); + editor.Text = JsonThemePreviewText; + ApplyJsonThemePreview(editor); + } + + private void OnJsonHighlightThemeChanged(object sender, SelectionChangedEventArgs e) + { + if (sender is FrameworkElement { Tag: TextEditor editor }) + Dispatcher.BeginInvoke(() => ApplyJsonThemePreview(editor)); + } + + private void ApplyJsonThemePreview(TextEditor editor) + { + editor.SyntaxHighlighting ??= AvalonExtensions.HighlighterSelector("json"); + editor.SyntaxHighlighting.ApplyJsonTheme(_applicationView.SettingsView.SelectedJsonHighlightTheme); + editor.TextArea.TextView.Redraw(); + } } diff --git a/FModel/Views/Snooper/Models/SkeletalModel.cs b/FModel/Views/Snooper/Models/SkeletalModel.cs index 1e4c34fc..4f9fcd2d 100644 --- a/FModel/Views/Snooper/Models/SkeletalModel.cs +++ b/FModel/Views/Snooper/Models/SkeletalModel.cs @@ -99,7 +99,7 @@ public class SkeletalModel : UModel foreach (var morph in export.MorphTargets) { - if (!morph.TryLoad(out UMorphTarget morphTarget) || morphTarget.MorphLODModels.Length < 1 || + if (!morph.TryLoad(out UMorphTarget morphTarget) || morphTarget.MorphLODModels.Length <= skeletalMesh.LODs[LodLevel].SourceLodIndex || morphTarget.MorphLODModels[skeletalMesh.LODs[LodLevel].SourceLodIndex].Vertices.Length < 1) continue; diff --git a/FModel/Views/Snooper/Options.cs b/FModel/Views/Snooper/Options.cs index 916c943c..875cf896 100644 --- a/FModel/Views/Snooper/Options.cs +++ b/FModel/Views/Snooper/Options.cs @@ -248,7 +248,7 @@ public class Options { return _game switch { - "LIESOFP" or "CODEVEIN2" or "HIGHONLIFE2" => true, + "LIESOFP" or "CODEVEIN2" or "HIGHONLIFE2" or "MORTALSHELL2" => true, _ => false, }; }