diff --git a/CUE4Parse b/CUE4Parse index bf3c0908..a098f0b6 160000 --- a/CUE4Parse +++ b/CUE4Parse @@ -1 +1 @@ -Subproject commit bf3c09087c2a4d7d812582e268ec0d275a0827fd +Subproject commit a098f0b6f87372e95d42701216159961eb691948 diff --git a/FModel/App.xaml.cs b/FModel/App.xaml.cs index 31da19b9..36a1b8c1 100644 --- a/FModel/App.xaml.cs +++ b/FModel/App.xaml.cs @@ -108,7 +108,7 @@ public partial class App Directory.CreateDirectory(Path.Combine(UserSettings.Default.OutputDirectory, "Backups")); if (createMe) Directory.CreateDirectory(Path.Combine(UserSettings.Default.OutputDirectory, "Exports")); Directory.CreateDirectory(Path.Combine(UserSettings.Default.OutputDirectory, "Logs")); - Directory.CreateDirectory(Path.Combine(UserSettings.Default.OutputDirectory, ".data")); + CacheManager.EnsureDirectories(); const string template = "{Timestamp:yyyy-MM-dd HH:mm:ss} [{Level:u3}] {Enriched}: {Message:lj}{NewLine}{Exception}"; Log.Logger = new LoggerConfiguration() @@ -125,6 +125,7 @@ public partial class App #endif .CreateLogger(); + CacheManager.MigrateLegacyFiles(); Log.Information("Version {Version} ({CommitId})", Constants.APP_VERSION, Constants.APP_COMMIT_ID); Log.Information("{OS}", GetOperatingSystemProductName()); Log.Information("{RuntimeVer}", RuntimeInformation.FrameworkDescription); 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/Framework/CacheManager.cs b/FModel/Framework/CacheManager.cs new file mode 100644 index 00000000..55b55478 --- /dev/null +++ b/FModel/Framework/CacheManager.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Generic; +using System.IO; +using FModel.Settings; +using Serilog; + +namespace FModel.Framework; + +public static class CacheManager +{ + public static string DataDirectory => + Directory.CreateDirectory(Path.Combine(UserSettings.Default.OutputDirectory, ".data")).FullName; + + public static string ChunksDirectory => + Directory.CreateDirectory(Path.Combine(DataDirectory, "chunks")).FullName; + + public static string ManifestsDirectory => + Directory.CreateDirectory(Path.Combine(DataDirectory, "manifests")).FullName; + + public static string MappingsDirectory => + Directory.CreateDirectory(Path.Combine(DataDirectory, "mappings")).FullName; + + public static void EnsureDirectories() + { + _ = ChunksDirectory; + _ = ManifestsDirectory; + _ = MappingsDirectory; + } + + public static void MigrateLegacyFiles() + { + EnsureDirectories(); + + var movedFiles = 0; + var skippedFiles = 0; + var sources = new HashSet(StringComparer.OrdinalIgnoreCase) + { + Path.GetFullPath(UserSettings.Default.OutputDirectory), + Path.GetFullPath(DataDirectory) + }; + + foreach (var sourceDirectory in sources) + { + foreach (var file in new DirectoryInfo(sourceDirectory).EnumerateFiles("*", SearchOption.TopDirectoryOnly)) + { + var destinationDirectory = GetDestinationDirectory(file.Name); + if (destinationDirectory is null) continue; + + var destinationPath = Path.Combine(destinationDirectory, file.Name); + if (File.Exists(destinationPath)) + { + skippedFiles++; + continue; + } + + try + { + File.Move(file.FullName, destinationPath); + UpdateMappingEndpointPath(file.FullName, destinationPath); + movedFiles++; + } + catch (Exception e) when (e is IOException or UnauthorizedAccessException) + { + Log.Warning(e, "Could not migrate cache file '{CacheFile}'", file.FullName); + } + } + } + + if (movedFiles > 0) + Log.Information("Migrated {CacheFileCount} cached file(s) into dedicated cache directories", movedFiles); + if (skippedFiles > 0) + Log.Warning("Skipped {CacheFileCount} cache migration(s) because the destination file already exists", skippedFiles); + } + + private static string GetDestinationDirectory(string fileName) + { + if (fileName.EndsWith(".jmap.gz", StringComparison.OrdinalIgnoreCase)) + return MappingsDirectory; + + return Path.GetExtension(fileName).ToLowerInvariant() switch + { + ".chunk" or ".iochunk" or ".iopart" or ".utoc" => ChunksDirectory, + ".manifest" => ManifestsDirectory, + ".usmap" or ".jmap" => MappingsDirectory, + _ => null + }; + } + + private static void UpdateMappingEndpointPath(string oldPath, string newPath) + { + if (!newPath.StartsWith(MappingsDirectory, StringComparison.OrdinalIgnoreCase)) return; + + if (UserSettings.Default.PerDirectory is null) return; + + foreach (var directorySettings in UserSettings.Default.PerDirectory.Values) + { + if (directorySettings.Endpoints is null) continue; + + foreach (var endpoint in directorySettings.Endpoints) + { + if (endpoint is not null && string.Equals(endpoint.FilePath, oldPath, StringComparison.OrdinalIgnoreCase)) + endpoint.FilePath = newPath; + } + } + } +} diff --git a/FModel/Settings/UserSettings.cs b/FModel/Settings/UserSettings.cs index 42991de9..0f48e7e8 100644 --- a/FModel/Settings/UserSettings.cs +++ b/FModel/Settings/UserSettings.cs @@ -5,13 +5,14 @@ using System.Windows; using System.Windows.Input; using CUE4Parse.UE4.Assets.Exports.Material; using CUE4Parse.UE4.Assets.Exports.Nanite; +using CUE4Parse.UE4.Lua.unluac; using CUE4Parse.UE4.Versions; using CUE4Parse_Conversion; using CUE4Parse_Conversion.Animations; using CUE4Parse_Conversion.Meshes; using CUE4Parse_Conversion.Textures; using CUE4Parse_Conversion.UEFormat.Enums; -using CUE4Parse.UE4.Lua.unluac; +using FModel.Extensions.Themes; using FModel.Framework; using FModel.ViewModels; using FModel.ViewModels.ApiEndpoints.Models; @@ -311,6 +312,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/ApiEndpoints/ValorantApiEndpoint.cs b/FModel/ViewModels/ApiEndpoints/ValorantApiEndpoint.cs index 2a450517..7cc15660 100644 --- a/FModel/ViewModels/ApiEndpoints/ValorantApiEndpoint.cs +++ b/FModel/ViewModels/ApiEndpoints/ValorantApiEndpoint.cs @@ -78,7 +78,7 @@ public class VManifest public async ValueTask PrefetchChunk(VChunk chunk, CancellationToken cancellationToken) { - var chunkPath = Path.Combine(UserSettings.Default.OutputDirectory, ".data", $"{chunk.Id}.chunk"); + var chunkPath = Path.Combine(CacheManager.ChunksDirectory, $"{chunk.Id}.chunk"); if (File.Exists(chunkPath)) return; using var response = await _client.GetAsync(chunk.GetUrl(), cancellationToken).ConfigureAwait(false); @@ -91,7 +91,7 @@ public class VManifest public async Task GetChunkBytes(VChunk chunk, CancellationToken cancellationToken) { - var chunkPath = Path.Combine(UserSettings.Default.OutputDirectory, ".data", $"{chunk.Id}.chunk"); + var chunkPath = Path.Combine(CacheManager.ChunksDirectory, $"{chunk.Id}.chunk"); byte[] chunkBytes; if (File.Exists(chunkPath)) diff --git a/FModel/ViewModels/ApplicationViewModel.cs b/FModel/ViewModels/ApplicationViewModel.cs index 93f746c0..242379f9 100644 --- a/FModel/ViewModels/ApplicationViewModel.cs +++ b/FModel/ViewModels/ApplicationViewModel.cs @@ -22,6 +22,7 @@ using FModel.Views.Resources.Controls; using MessageBox = AdonisUI.Controls.MessageBox; using MessageBoxButton = AdonisUI.Controls.MessageBoxButton; using MessageBoxImage = AdonisUI.Controls.MessageBoxImage; +using System.Runtime.Intrinsics.X86; namespace FModel.ViewModels; @@ -292,6 +293,9 @@ public class ApplicationViewModel : ViewModel public static async Task InitOodle() { + if (!Avx2.IsSupported) + return; + var oodlePath = Path.Combine(UserSettings.Default.OutputDirectory, ".data", OodleHelper.OODLE_NAME_OLD); if (!File.Exists(oodlePath)) { diff --git a/FModel/ViewModels/AssetsFolderViewModel.cs b/FModel/ViewModels/AssetsFolderViewModel.cs index 8a4bfb22..70f6eeaf 100644 --- a/FModel/ViewModels/AssetsFolderViewModel.cs +++ b/FModel/ViewModels/AssetsFolderViewModel.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; @@ -218,31 +219,56 @@ 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; + } + + var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData).Replace(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); foreach (var entry in entries) { TreeItem lastNode = null; TreeItem parentItem = null; - var folders = entry.Path.Split('/', StringSplitOptions.RemoveEmptyEntries); + + var path = entry.Path; + if (path.StartsWith(localAppData, StringComparison.OrdinalIgnoreCase)) + path = path[localAppData.Length..].TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + + var folders = path.Split('/', StringSplitOptions.RemoveEmptyEntries); 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(); @@ -262,13 +288,16 @@ public class AssetsFolderViewModel lastNode?.AssetsList.Add(entry); } + Folders.AddRange(treeItems); + if (treeItems.Count > 0) { + // Select after publishing the collection. Selecting a detached TreeItem lets WPF + // auto-select the first root (usually the synthetic "Content" bucket) instead. var projectName = ApplicationService.ApplicationView.CUE4Parse.Provider.ProjectName; (treeItems.FirstOrDefault(x => x.Header.Equals(projectName, StringComparison.OrdinalIgnoreCase)) ?? treeItems[0]).IsSelected = true; } - Folders.AddRange(treeItems); ApplicationService.ApplicationView.CUE4Parse.SearchVm.ChangeCollection(entries); foreach (var folder in Folders) diff --git a/FModel/ViewModels/CUE4ParseViewModel.cs b/FModel/ViewModels/CUE4ParseViewModel.cs index f20cd939..07772cc3 100644 --- a/FModel/ViewModels/CUE4ParseViewModel.cs +++ b/FModel/ViewModels/CUE4ParseViewModel.cs @@ -27,10 +27,14 @@ using CUE4Parse.GameTypes.DFHO.Assets.Objects; using CUE4Parse.GameTypes.HonorOfKings.FileProvider; using CUE4Parse.GameTypes.KRD.Assets.Exports; using CUE4Parse.GameTypes.LegoBatman.Assets; +using CUE4Parse.GameTypes.LordOfMysteries.FileProvider; using CUE4Parse.GameTypes.RocoKingdomWorld.Assets.Objects; using CUE4Parse.GameTypes.SMG.UE4.Assets.Exports.Wwise; using CUE4Parse.GameTypes.SquareEnix.UE4.Assets.Exports; +using CUE4Parse.GameTypes.Theia.FileProvider; using CUE4Parse.MappingsProvider; +using CUE4Parse.MappingsProvider.Jmap; +using CUE4Parse.MappingsProvider.Usmap; using CUE4Parse.UE4.AssetRegistry; using CUE4Parse.UE4.Assets; using CUE4Parse.UE4.Assets.Exports; @@ -44,11 +48,11 @@ using CUE4Parse.UE4.Assets.Exports.StaticMesh; using CUE4Parse.UE4.Assets.Exports.Texture; using CUE4Parse.UE4.Assets.Exports.Verse; using CUE4Parse.UE4.Assets.Exports.Wwise; -using CUE4Parse.UE4.Assets.Objects; using CUE4Parse.UE4.BinaryConfig; using CUE4Parse.UE4.CriWare; using CUE4Parse.UE4.CriWare.Readers; using CUE4Parse.UE4.FMod; +using CUE4Parse.UE4.GameFeatures; using CUE4Parse.UE4.IO; using CUE4Parse.UE4.Localization; using CUE4Parse.UE4.Lua.unluac; @@ -198,9 +202,15 @@ public class CUE4ParseViewModel : ViewModel [ new(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData) + "\\KONAMI\\eFootball\\ST\\Download") ], SearchOption.AllDirectories, versionContainer, pathComparer), + "DeadByDaylight" => new DefaultFileProvider(new DirectoryInfo(gameDirectory), + [ + new(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + "\\DeadByDaylight\\Saved\\PersistentDownloadDir\\DynamicContent") + ], SearchOption.AllDirectories, versionContainer, pathComparer), _ when versionContainer.Game is EGame.GAME_AshEchoes => new AEDefaultFileProvider(gameDirectory, SearchOption.AllDirectories, versionContainer, pathComparer), _ when versionContainer.Game is EGame.GAME_BlackStigma => new DefaultFileProvider(gameDirectory, SearchOption.AllDirectories, versionContainer, StringComparer.Ordinal), _ when versionContainer.Game is EGame.GAME_HonorofKingsWorld => new HoKWDefaultFileProvider(gameDirectory, SearchOption.AllDirectories, versionContainer, pathComparer), + _ when versionContainer.Game is EGame.GAME_LordOfMysteries => new LoMDefaultFileProvider(gameDirectory, SearchOption.AllDirectories, versionContainer, pathComparer), + _ when versionContainer.Game is EGame.GAME_ArcRaiders => new TheiaFileProvider(gameDirectory, SearchOption.AllDirectories, versionContainer, pathComparer), _ => new DefaultFileProvider(gameDirectory, SearchOption.AllDirectories, versionContainer, pathComparer) }; @@ -227,7 +237,7 @@ public class CUE4ParseViewModel : ViewModel Provider.OnDemandOptions = new IoStoreOnDemandOptions { ChunkHostUri = new Uri("https://egdownload.fastly-edge.com/", UriKind.Absolute), - ChunkCacheDirectory = Directory.CreateDirectory(Path.Combine(UserSettings.Default.OutputDirectory, ".data")), + ChunkCacheDirectory = new DirectoryInfo(CacheManager.ChunksDirectory), DownloaderClient = _chunkClient }; @@ -244,11 +254,10 @@ public class CUE4ParseViewModel : ViewModel throw new FileLoadException("Could not load latest Fortnite manifest, you may have to switch to your local installation."); } - var cacheDir = Directory.CreateDirectory(Path.Combine(UserSettings.Default.OutputDirectory, ".data")).FullName; var manifestOptions = new ManifestParseOptions { - ChunkCacheDirectory = cacheDir, - ManifestCacheDirectory = cacheDir, + ChunkCacheDirectory = CacheManager.ChunksDirectory, + ManifestCacheDirectory = CacheManager.ManifestsDirectory, ChunkBaseUrl = "https://egdownload.fastly-edge.com/Builds/Fortnite/CloudDir/", Decompressor = Compression.Decompressor, Client = _chunkClient, @@ -276,25 +285,16 @@ public class CUE4ParseViewModel : ViewModel IoStoreOnDemand.Read(new StreamReader(ioStoreOnDemandFile.GetStream())); } - Parallel.ForEach(manifest.Files.Where(x => _fnLiveRegex.IsMatch(x.FileName)), fileManifest => - { - p.RegisterVfs(fileManifest.FileName, [fileManifest.GetStream()], - it => new FRandomAccessStreamArchive(it, manifest.FindFile(it)!.GetStream(), p.Versions)); - }); + RegisterFortniteLiveArchives(p, manifest, cancellationToken); 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 manifestBytes = _chunkClient.GetByteArrayAsync(downloadUrl, cancellationToken).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)); - }); + RegisterFortniteLiveArchives(p, uefnManifest, cancellationToken); var elapsedTime = Stopwatch.GetElapsedTime(startTs); FLogger.Append(ELog.Information, () => @@ -334,6 +334,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)); @@ -341,6 +342,36 @@ public class CUE4ParseViewModel : ViewModel }); } + private void RegisterFortniteLiveArchives(StreamedFileProvider provider, FBuildPatchAppManifest manifest, + CancellationToken cancellationToken) + { + var archiveFiles = manifest.Files.Where(x => + _fnLiveRegex.IsMatch(x.FileName) && + (x.FileName.EndsWith(".pak", StringComparison.OrdinalIgnoreCase) || + x.FileName.EndsWith(".utoc", StringComparison.OrdinalIgnoreCase) || + x.FileName.EndsWith(".uondemandtoc", StringComparison.OrdinalIgnoreCase))).ToList(); + var parallelOptions = new ParallelOptions { CancellationToken = cancellationToken }; + + Parallel.ForEach(archiveFiles.Where(x => !x.FileName.EndsWith(".uondemandtoc", StringComparison.OrdinalIgnoreCase)), + parallelOptions, fileManifest => + { + provider.RegisterVfs(fileManifest.FileName, [fileManifest.GetStream()], + it => new FRandomAccessStreamArchive(it, manifest.FindFile(it)!.GetStream(), provider.Versions)); + }); + + // V2 on-demand TOCs are large and span many BuildPatch chunks. Reading them through CUE4Parse's synchronous + // archive interface downloads those chunks one at a time. Materialize each TOC through EpicManifestParser's + // parallel path first, then register it normally so the on-demand containers remain available. + foreach (var fileManifest in archiveFiles.Where(x => x.FileName.EndsWith(".uondemandtoc", StringComparison.OrdinalIgnoreCase))) + { + cancellationToken.ThrowIfCancellationRequested(); + using var stream = fileManifest.GetStream(); + var data = stream.SaveBytesAsync(8, cancellationToken).GetAwaiter().GetResult(); + using var archive = new FByteArchive(fileManifest.FileName, data, provider.Versions); + provider.RegisterVfs(new IoChunkToc(archive)); + } + } + /// /// load virtual files system from GameDirectory /// @@ -437,7 +468,7 @@ public class CUE4ParseViewModel : ViewModel endpoint.Path = "$.mappings.ZStandard"; } - var mappingsFolder = Path.Combine(UserSettings.Default.OutputDirectory, ".data"); + var mappingsFolder = CacheManager.MappingsDirectory; var mappings = _apiEndpointView.DynamicApi.GetMappings(CancellationToken.None, endpoint.Url, endpoint.Path); if (mappings is { Length: > 0 }) { @@ -848,6 +879,13 @@ public class CUE4ParseViewModel : ViewModel break; } + case "bin" when entry.Name.Contains("GameFeatureVersePaths", StringComparison.OrdinalIgnoreCase): + { + var archive = entry.CreateReader(); + var versePathLookup = new FGameFeatureVersePathLookup(archive); + TabControl.SelectedTab.SetDocumentText(JsonConvert.SerializeObject(versePathLookup, Formatting.Indented), saveProperties, updateUi); + break; + } case "bank": { var archive = entry.CreateReader(); @@ -1093,6 +1131,11 @@ public class CUE4ParseViewModel : ViewModel var l10nData = new FAion2L10NFile(entry, Provider); TabControl.SelectedTab.SetDocumentText(JsonConvert.SerializeObject(l10nData, Formatting.Indented), saveProperties, updateUi); } + else if (entry.NameWithoutExtension.Equals("key_manifest")) + { + var keymanifest = new FAion2KeyManifestFile(entry, Provider); + TabControl.SelectedTab.SetDocumentText(JsonConvert.SerializeObject(keymanifest, Formatting.Indented), saveProperties, updateUi); + } else { FAion2DataFile datfile = entry.NameWithoutExtension switch @@ -1217,7 +1260,7 @@ public class CUE4ParseViewModel : ViewModel { if (!TabControl.CanAddTabs) return false; - TabControl.AddTab($"{verseDigest.ProjectName}.verse"); + TabControl.AddTab($"{verseDigest.Name}.verse"); TabControl.SelectedTab.Highlighter = AvalonExtensions.HighlighterSelector("verse"); TabControl.SelectedTab.SetDocumentText(verseDigest.ReadableCode, false, false); return true; @@ -1601,7 +1644,7 @@ public class CUE4ParseViewModel : ViewModel if (dummy is not UClass || pointer.Object.Value is not UClass blueprint) continue; - cppList.Add(blueprint.DecompileBlueprintToPseudo(pkg.Mappings, cookedMetaData)); + cppList.Add(blueprint.DecompileBlueprintToPseudo(cookedMetaData)); } if (cppList.Count == 0) return false; diff --git a/FModel/ViewModels/Commands/LoadCommand.cs b/FModel/ViewModels/Commands/LoadCommand.cs index fc4ad6cb..c29a695e 100644 --- a/FModel/ViewModels/Commands/LoadCommand.cs +++ b/FModel/ViewModels/Commands/LoadCommand.cs @@ -59,50 +59,53 @@ public class LoadCommand : ViewModelCommand _applicationView.IsAssetsExplorerVisible = true; Helper.CloseWindow("Search For Packages"); // close search window if opened - await Task.WhenAll( - _applicationView.CUE4Parse.LoadLocalizedResources(), // load locres if not already loaded, - _applicationView.CUE4Parse.LoadVirtualPaths(), // load virtual paths if not already loaded - _threadWorkerView.Begin(cancellationToken => + // Populate the package tree before doing supplemental reads. For streamed providers those + // reads may download BuildPatch chunks and must not delay or hide an otherwise valid tree. + await _threadWorkerView.Begin(cancellationToken => + { + // filter what to show + _applicationView.Status.UpdateStatusLabel("Packages", "Filtering"); + switch (UserSettings.Default.LoadingMode) { - // filter what to show - _applicationView.Status.UpdateStatusLabel("Packages", "Filtering"); - switch (UserSettings.Default.LoadingMode) + case ELoadingMode.Multiple: { - case ELoadingMode.Multiple: + var l = (IList) parameter; + if (l.Count == 0) { - var l = (IList) parameter; - if (l.Count == 0) - { - UserSettings.Default.LoadingMode = ELoadingMode.All; - goto case ELoadingMode.All; - } + UserSettings.Default.LoadingMode = ELoadingMode.All; + goto case ELoadingMode.All; + } - var directoryFilesToShow = l.Cast(); - FilterDirectoryFilesToDisplay(cancellationToken, directoryFilesToShow); - break; - } - case ELoadingMode.All: - { - FilterDirectoryFilesToDisplay(cancellationToken, null); - break; - } - case ELoadingMode.AllButNew: - case ELoadingMode.AllButModified: - { - FilterNewOrModifiedFilesToDisplay(cancellationToken); - break; - } - case ELoadingMode.AllButPatched: - { - FilterPacthedFilesToDisplay(cancellationToken); - break; - } - default: throw new ArgumentOutOfRangeException(); + var directoryFilesToShow = l.Cast(); + FilterDirectoryFilesToDisplay(cancellationToken, directoryFilesToShow); + break; } + case ELoadingMode.All: + { + FilterDirectoryFilesToDisplay(cancellationToken, null); + break; + } + case ELoadingMode.AllButNew: + case ELoadingMode.AllButModified: + { + FilterNewOrModifiedFilesToDisplay(cancellationToken); + break; + } + case ELoadingMode.AllButPatched: + { + FilterPacthedFilesToDisplay(cancellationToken); + break; + } + default: throw new ArgumentOutOfRangeException(); + } - _discordHandler.UpdatePresence(_applicationView.CUE4Parse); - }) - ).ConfigureAwait(false); + _discordHandler.UpdatePresence(_applicationView.CUE4Parse); + }).ConfigureAwait(false); + + // These enrich later package reads but are not required to display the archive contents. + // Run them sequentially to avoid competing BuildPatch download bursts on Fortnite Live. + await _applicationView.CUE4Parse.LoadVirtualPaths().ConfigureAwait(false); + await _applicationView.CUE4Parse.LoadLocalizedResources().ConfigureAwait(false); #if DEBUG loadingTime.Stop(); FLogger.Append(ELog.Debug, () => @@ -113,6 +116,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 +124,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 +142,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/GameFileViewModel.cs b/FModel/ViewModels/GameFileViewModel.cs index 6bac2c92..ea33dac9 100644 --- a/FModel/ViewModels/GameFileViewModel.cs +++ b/FModel/ViewModels/GameFileViewModel.cs @@ -366,6 +366,7 @@ public class GameFileViewModel(GameFile asset) : ViewModel case "stinfo": case "ushaderbytecode": case "upipelinecache": + case "dxbc": AssetCategory = EAssetCategory.ByteCode; break; case "wav": diff --git a/FModel/ViewModels/SettingsViewModel.cs b/FModel/ViewModels/SettingsViewModel.cs index 0d6ace36..1441cd2a 100644 --- a/FModel/ViewModels/SettingsViewModel.cs +++ b/FModel/ViewModels/SettingsViewModel.cs @@ -10,9 +10,12 @@ using CUE4Parse.UE4.Versions; using CUE4Parse_Conversion.Meshes; using CUE4Parse_Conversion.Textures; using CUE4Parse_Conversion.UEFormat.Enums; +using FModel.Extensions; +using FModel.Extensions.Themes; using FModel.Framework; using FModel.Services; using FModel.Settings; +using ICSharpCode.AvalonEdit.Highlighting; namespace FModel.ViewModels; @@ -165,6 +168,13 @@ public class SettingsViewModel : ViewModel set => SetProperty(ref _selectedTextureExportFormat, value); } + private EJsonHighlightTheme _selectedJsonHighlightTheme; + public EJsonHighlightTheme SelectedJsonHighlightTheme + { + get => _selectedJsonHighlightTheme; + set => SetProperty(ref _selectedJsonHighlightTheme, value); + } + private ulong _criwareDecryptionKey; public ulong CriwareDecryptionKey { @@ -196,6 +206,7 @@ public class SettingsViewModel : ViewModel public ReadOnlyObservableCollection MaterialExportFormats { get; private set; } public ReadOnlyObservableCollection TextureExportFormats { get; private set; } public ReadOnlyObservableCollection Platforms { get; private set; } + public ReadOnlyObservableCollection JsonHighlightThemes { get; private set; } private string _outputSnapshot; private string _rawDataSnapshot; @@ -220,6 +231,7 @@ public class SettingsViewModel : ViewModel private ENaniteMeshFormat _naniteMeshExportFormatSnapshot; private EMaterialFormat _materialExportFormatSnapshot; private ETextureFormat _textureExportFormatSnapshot; + private EJsonHighlightTheme _jsonHighlightThemeSnapshot; private bool _mappingsUpdate = false; @@ -264,6 +276,7 @@ public class SettingsViewModel : ViewModel _naniteMeshExportFormatSnapshot = UserSettings.Default.NaniteMeshExportFormat; _materialExportFormatSnapshot = UserSettings.Default.MaterialExportFormat; _textureExportFormatSnapshot = UserSettings.Default.TextureExportFormat; + _jsonHighlightThemeSnapshot = UserSettings.Default.JsonHighlightTheme; SelectedUePlatform = _uePlatformSnapshot; SelectedUeGame = _ueGameSnapshot; @@ -282,6 +295,7 @@ public class SettingsViewModel : ViewModel SelectedTextureExportFormat = _textureExportFormatSnapshot; CriwareDecryptionKey = _criwareDecryptionKey; UnluacOpcodeMap = _unluacOpcodeMap; + SelectedJsonHighlightTheme = _jsonHighlightThemeSnapshot; SelectedAesReload = UserSettings.Default.AesReload; SelectedDiscordRpc = UserSettings.Default.DiscordRpc; @@ -299,6 +313,7 @@ public class SettingsViewModel : ViewModel MaterialExportFormats = new ReadOnlyObservableCollection(new ObservableCollection(EnumerateMaterialExportFormat())); TextureExportFormats = new ReadOnlyObservableCollection(new ObservableCollection(EnumerateTextureExportFormat())); Platforms = new ReadOnlyObservableCollection(new ObservableCollection(EnumerateUePlatforms())); + JsonHighlightThemes = new ReadOnlyObservableCollection(new ObservableCollection(EnumerateJsonHighlightThemes())); } public bool Save(out List whatShouldIDo) @@ -337,6 +352,7 @@ public class SettingsViewModel : ViewModel UserSettings.Default.TextureExportFormat = SelectedTextureExportFormat; UserSettings.Default.AesReload = SelectedAesReload; UserSettings.Default.DiscordRpc = SelectedDiscordRpc; + UserSettings.Default.JsonHighlightTheme = SelectedJsonHighlightTheme; if (SelectedDiscordRpc == EDiscordRpc.Never) _discordHandler.Shutdown(); @@ -362,4 +378,5 @@ public class SettingsViewModel : ViewModel private IEnumerable EnumerateMaterialExportFormat() => Enum.GetValues(); private IEnumerable EnumerateTextureExportFormat() => Enum.GetValues(); private IEnumerable EnumerateUePlatforms() => Enum.GetValues(); + private IEnumerable EnumerateJsonHighlightThemes() => Enum.GetValues(); } diff --git a/FModel/Views/Resources/Icons.xaml b/FModel/Views/Resources/Icons.xaml index b46d3ece..f248fc1f 100644 --- a/FModel/Views/Resources/Icons.xaml +++ b/FModel/Views/Resources/Icons.xaml @@ -102,7 +102,8 @@ M231.822 132.778c1.2-5.152 6.352-8.357 11.505-7.158 5.153 1.2 8.357 6.352 7.158 11.505l-20.773 88.972c-1.199 5.153-6.352 8.358-11.504 7.158-5.153-1.199-8.358-6.352-7.158-11.505l20.772-88.972zM21.123 259.247h16.714V52.395C37.837 23.601 61.438 0 90.232 0h230.505a7.998 7.998 0 016.39 3.183l96.876 104.809a7.937 7.937 0 012.118 5.411h.041v145.844h16.714c11.618 0 21.124 9.525 21.124 21.124v163.863c0 11.599-9.526 21.124-21.124 21.124h-17.058c-3.001 25.877-25.473 46.505-52.051 46.505H90.232c-26.72 0-49.082-20.526-52.055-46.505H21.123C9.526 465.358 0 455.853 0 444.234V280.371c0-11.619 9.506-21.124 21.123-21.124zm32.731 0h356.292V136.525h-32.219v-.033h-.124c-19.111-.302-34.068-5.373-44.736-14.486-11.073-9.458-17.282-22.969-18.52-39.763l-.075-1.098V16.016H90.232c-20.032 0-36.378 16.346-36.378 36.379v206.852zm355.795 206.111H54.344c2.862 17.157 17.981 30.488 35.888 30.488h283.535c17.803 0 32.993-13.394 35.882-30.488zM330.467 30.272V81.11c.913 12.412 5.265 22.192 12.97 28.775 7.822 6.682 19.34 10.418 34.49 10.676v-.03h32.219v-4.052l-79.679-86.207zM199.303 204.095c3.972 3.477 4.377 9.521.9 13.493-3.478 3.973-9.521 4.377-13.494.9l-36.354-31.854c-3.972-3.477-4.377-9.521-.9-13.493.295-.336.606-.644.933-.927l36.321-31.826c3.973-3.478 10.016-3.073 13.494.899 3.477 3.973 3.072 10.016-.9 13.494l-28.14 24.656 28.14 24.658zm77.99 14.393c-3.972 3.477-10.016 3.073-13.493-.9-3.478-3.972-3.073-10.016.899-13.493l28.14-24.658-28.14-24.656c-3.972-3.478-4.377-9.521-.899-13.494 3.477-3.972 9.521-4.377 13.493-.899l36.318 31.826c.328.283.639.591.933.927 3.477 3.972 3.073 10.016-.899 13.493l-36.352 31.854zM120.405 400.89l3.805-19.455c8.333 2.083 15.827 3.123 22.484 3.123 6.66 0 12.024-.27 16.101-.815v-9.755l-12.228-1.086c-11.051-.999-18.635-3.648-22.759-7.951-4.121-4.302-6.181-10.665-6.181-19.089 0-11.592 2.514-19.565 7.54-23.911 5.029-4.35 13.565-6.523 25.612-6.523 12.047 0 22.915 1.134 32.61 3.398l-3.398 18.978c-8.423-1.358-15.171-2.038-20.244-2.038s-9.375.226-12.908.68v9.416l9.783.952c11.865 1.177 20.063 4.008 24.591 8.492 4.531 4.484 6.795 10.71 6.795 18.682 0 5.706-.77 10.528-2.311 14.47-1.541 3.94-3.373 6.927-5.502 8.966-2.129 2.038-5.141 3.601-9.037 4.687-3.892 1.087-7.312 1.745-10.255 1.97-2.944.228-6.864.341-11.755.341-11.775 0-22.688-1.177-32.743-3.532zm128.036-1.631l-11.548 3.125c-15.219 0-25.414-3.926-30.707-11.684-2.674-3.923-4.621-8.266-5.708-13.111-1.087-4.844-1.628-10.712-1.628-17.595 0-15.491 2.899-26.789 8.695-33.9 5.797-7.109 16.212-10.666 31.249-10.666 15.036 0 25.498 3.579 31.386 10.734 5.887 7.156 8.83 18.432 8.83 33.832 0 11.502-2.399 20.786-7.199 27.851l9.782 5.708-7.067 15.623-26.085-4.754v-5.163zm-20.382-18.072h11.279c3.714 0 6.407-.429 8.083-1.29 1.675-.861 2.514-2.83 2.514-5.909v-35.325h-11.414c-3.62 0-6.272.429-7.947 1.29-1.675.861-2.515 2.834-2.515 5.909v35.325zm115.536 21.197h-54.349v-84.917h27.175v63.179h27.174v21.738z M19.14,7.5A2.86,2.86 0 0,1 22,10.36V14.14A2.86,2.86 0 0,1 19.14,17H12C12,17.39 12.32,17.96 12.71,17.96H17V19.64A2.86,2.86 0 0,1 14.14,22.5H9.86A2.86,2.86 0 0,1 7,19.64V15.89C7,14.31 8.28,13.04 9.86,13.04H15.11C16.69,13.04 17.96,11.76 17.96,10.18V7.5H19.14M14.86,19.29C14.46,19.29 14.14,19.59 14.14,20.18C14.14,20.77 14.46,20.89 14.86,20.89A0.71,0.71 0 0,0 15.57,20.18C15.57,19.59 15.25,19.29 14.86,19.29M4.86,17.5C3.28,17.5 2,16.22 2,14.64V10.86C2,9.28 3.28,8 4.86,8H12C12,7.61 11.68,7.04 11.29,7.04H7V5.36C7,3.78 8.28,2.5 9.86,2.5H14.14C15.72,2.5 17,3.78 17,5.36V9.11C17,10.69 15.72,11.96 14.14,11.96H8.89C7.31,11.96 6.04,13.24 6.04,14.82V17.5H4.86M9.14,5.71C9.54,5.71 9.86,5.41 9.86,4.82C9.86,4.23 9.54,4.11 9.14,4.11C8.75,4.11 8.43,4.23 8.43,4.82C8.43,5.41 8.75,5.71 9.14,5.71Z M7878 15350 c-162 -15 -335 -63 -471 -131 -146 -74 -5492 -3082 -5567 -3133 -41 -28 -120 -96 -175 -151 -189 -189 -307 -411 -362 -682 -17 -84 -18 -245 -18 -3253 0 -3008 1 -3169 18 -3253 50 -249 159 -466 323 -645 64 -71 188 -173 269 -224 102 -64 5469 -3075 5535 -3106 367 -170 814 -166 1165 10 75 38 5370 3012 5482 3079 326 196 543 505 620 886 17 84 18 245 18 3253 0 3008 -1 3169 -18 3253 -55 271 -173 493 -362 682 -55 55 -134 123 -175 151 -75 51 -5421 3059 -5568 3133 -203 102 -479 152 -714 131z m591 -3534 c526 -70 1018 -235 1456 -489 298 -172 642 -446 866 -689 117 -126 259 -298 259 -312 -1 -6 -301 -184 -667 -396 -366 -212 -680 -393 -697 -403 l-31 -18 -85 82 c-583 568 -1404 778 -2184 559 -501 -141 -952 -471 -1245 -910 -249 -373 -374 -786 -374 -1240 0 -372 82 -709 250 -1030 337 -644 947 -1075 1678 -1185 133 -21 439 -23 570 -6 499 66 958 288 1304 631 l84 82 32 -18 c892 -514 1364 -791 1365 -800 1 -32 -300 -365 -462 -511 -583 -528 -1275 -856 -2043 -967 -200 -29 -382 -39 -638 -33 -277 6 -411 20 -672 72 -1512 301 -2719 1521 -3009 3040 -52 276 -61 378 -61 725 0 355 9 453 66 747 255 1307 1201 2410 2461 2867 312 113 669 190 1018 220 118 10 641 -2 759 -18z m3371 -2536 l0 -320 320 0 320 0 0 320 0 320 320 0 320 0 0 -320 0 -320 320 0 320 0 0 -320 0 -320 -320 0 -320 0 0 -320 0 -320 320 0 320 0 0 -320 0 -320 -320 0 -320 0 0 -320 0 -320 -320 0 -320 0 0 320 0 320 -320 0 -320 0 0 -320 0 -320 -320 0 -320 0 0 320 0 320 -320 0 -320 0 0 320 0 320 320 0 320 0 0 320 0 320 -320 0 -320 0 0 320 0 320 320 0 320 0 0 320 0 320 320 0 320 0 0 -320z M11840 8000 l0 -320 320 0 320 0 0 320 0 320 -320 0 -320 0 0 -320z - + 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 M609 180.1c-19.4 2.3-38.2 7.8-56.5 16.8-53.8 26.5-89.2 74.9-98.7 135.1-1.1 7-1.3 133.1-1.3 739 0 693.9.1 731 1.8 741.4 12.8 79.4 70.7 137.8 149.2 150.6 11.8 2 20.6 2 563.5 2s551.7 0 563.5-2c57.2-9.3 104-43 130.8-94.1 9.1-17.2 14.6-33.9 18.4-55.4 1.6-9.4 1.8-40.6 2-554l.3-544-268.2-268.2L1345.5 179l-364.5.1c-200.5.1-367.9.5-372 1m893 378.4L1747.5 804H1256V558.5c0-135 .1-245.5.3-245.5.1 0 110.7 110.5 245.7 245.5m-792.1 532c24.5 1.3 47 2.9 49.8 3.4 7.1 1.3 12.8 5.5 22.5 16.4 6.4 7.3 9.2 11.5 13.6 20.7 9.2 19.4 21.4 47.2 44.4 101.5 27.5 65.1 39.7 92.3 55.5 124 6.9 13.7 16.1 32.9 20.5 42.5 8.2 17.9 15.4 31.9 18.3 35.5 1.5 1.8 1.5-7.2.9-109-.6-108.7-.7-111.1-2.7-116.5-3.1-8.3-10-14.5-24.7-22-10.1-5.1-11.9-6.4-10.9-7.6 1.6-2 5.8-1.8 21.3 1.1 19.3 3.6 39.1 4.5 97.7 4.5 42.1 0 51.9.3 53.4 1.4 1.8 1.3 1.8 1.4-.1 1.9-1 .3-3.9.8-6.4 1.2s-8.5 1.7-13.5 3c-4.9 1.3-12.1 2.6-16 3-7.9.8-14.3 3.2-18 6.9l-2.5 2.5v123.7c0 121.8 0 123.8 2 125.9 1.4 1.5 2.1 4.1 2.6 9.2.8 8.1 2.2 10.1 12.6 17.2 9.3 6.5 9.3 6.5-26.7 7.3-17.6.4-45.3.7-61.5.5-29.2-.2-29.5-.2-29.8-2.3-.2-1.7.7-2.5 5.5-4.5 3.2-1.3 7.5-4.1 9.8-6.4 4.5-4.5 8.3-11 7.2-12.2-1.3-1.2-72.8-1.6-90.2-.5-13.5.9-16.2.8-17.5-.4-1.9-1.9-.9-2.7 8-6.4 9.8-4.1 11-5.2 11-11 0-3.9-3.7-13.2-22.3-56.1l-22.3-51.4-4.5-.6c-13.1-2-52.1-3.1-69.3-2-14.4.9-38.5 3.6-39.3 4.4-.1.1-5.6 13.5-12.3 29.7s-16 38.7-20.7 50c-7.4 17.9-8.5 21.1-8.1 25.1.6 5.6 3.9 10.3 10.2 14.3 4.8 3 5.8 5.1 2.9 5.8-1 .2-10.6 0-21.3-.5-21.7-.9-83.9-.2-108.4 1.3-22.1 1.4-22.5.3-1.7-5 17.9-4.6 33.3-9.6 42.4-13.7l7.7-3.6 6.9-13.1c3.8-7.2 14.2-27.5 23.1-45.2 17.9-35.6 19-38.9 15.5-47.9-1-2.7-1.4-5.6-1.1-7.1.4-1.5 8.1-10.1 17.5-19.8 9.2-9.4 17.6-18.6 18.5-20.4s6.4-15.7 12.2-30.8c5.7-15.1 17.9-46.7 27.1-70.2l16.6-42.7-9.3-18.2c-5.1-10-10.7-19.8-12.5-21.6-6-6.1-16.6-9.9-32-11.4-7.8-.8-8-.9-8.3-3.6s-.3-2.7 5.9-2.7c3.5 0 26.3 1.1 50.8 2.5m999.3 20.4c3 5.2 4.8 10.9 9.7 31 8.8 35.5 11.8 55.6 14.2 93.1 1.5 24.1.6 87-1.5 108-1.8 17.4-4.4 37.5-5.3 40.5-.3 1.3-.2 1.7.5 1 1.2-1.2 11.9-33.8 16.2-49.5 1.8-6.3 3.7-12.4 4.2-13.4 3.4-6.4 2.2 14.4-2 34.9-1.3 6.3-1.5 9.6-.9 10.7 2.1 3.3-9.4 55.8-18.3 83.3-5.5 17.2-15.6 42.7-18.1 45.9-1.1 1.5-2.6 2.3-3.4 1.9-.8-.3-5.7-6.9-10.9-14.7-17.4-26.2-33.3-45.2-60.7-72.1-16.4-16.1-29.1-27.5-45.6-41-4-3.2-7.5-6.5-7.8-7.3-1.2-3.2 5.5.3 19.9 10.4 8.1 5.8 15 10.3 15.2 10.2.4-.5-17.3-14.6-37.1-29.4-43.5-32.7-81.4-58.4-86.1-58.4-1.3 0-1.4 9.9-.8 86.7.4 61.2 1 87.5 1.8 89.3 1.4 3.1 7.5 7.2 14 9.5 6.9 2.4 7.6 3.2 4.6 5.5-2.4 1.9-5 2-74.2 2-45.3 0-71.8-.4-71.8-1 0-.9 3.2-1.9 12-3.9 9.2-2.1 22.3-6.2 26.9-8.6 7.6-3.8 8.9-8.7 10.1-38 1.3-29.8 2.5-203 1.5-217.8-.7-11.3-.9-12-3.8-15.3-1.6-1.9-4.3-4.2-6-5-5.1-2.7-24.7-6.3-42.9-7.9-9.7-.8-18.4-1.8-19.2-2.1-1.4-.5-2.2-2-1.3-2.6.3-.2 73.8-1.9 90.7-2.1 12.6-.2 13.9-.4 19.5-3 4.8-2.3 8.6-3.1 19.5-4.2 18.5-1.9 34.2-2.3 34.7-.9.3 1-2.3 4.6-10.9 14.7l-3.3 3.9-.3 20.2-.3 20.2 5.7 8c17.5 24.5 49.6 55.6 93.9 91 20 16.1 58.7 45.6 59.1 45.1.7-.7 6.5-38.1 8.5-55.7 3-25 3.3-66.8.6-84-2.5-16.2-9.6-43-11.4-43-.2 0-8.1 7.2-17.5 16.1-9.3 8.8-17.2 15.8-17.5 15.5s2.7-4.5 6.8-9.3c13-15.6 35.1-41.7 37.2-44.1 1.1-1.2 1.8-2.2 1.4-2.2-.3 0-16.9 17-36.8 37.7-37.3 38.8-41.5 43-42.6 41.9-.8-.8-4.1 3.1 54-63.3 28.4-32.6 51.7-59.6 51.7-59.9s-3.2 2.3-7 5.7c-14.3 12.9-15.9 13.8-7.5 4.4 16.1-18.1 32.8-35.6 33.9-35.2.6.2 2.8 3.2 4.8 6.6m-578.4 94.3c-.3.7-5 4.4-10.5 8.3-13.5 9.4-17.7 12.9-31.6 26.4-8.1 8-13.5 14.2-17.6 20.5-6.8 10.7-11.5 18.6-10.9 18.6.3 0 2-2.1 3.8-4.8 5.1-7.1 19.5-21.6 28.5-28.7 24.4-19.1 53.7-31.3 89-37.2 13.7-2.2 53.7-2.5 68.5-.5 45.5 6.4 82.4 22.6 109.1 48 26 24.7 40.4 54.7 45 93.7 1.7 14.2.6 42.3-2.1 54.9-8.5 39.6-25.4 68.8-53.5 92.6-15.5 13-29.1 20.9-45.7 26.5-18.4 6.1-35.4 7.7-56 5.3l-10.3-1.3 10.5-.6c11.4-.8 27.2-3.2 31.1-4.9 2.3-.9 2.2-1-.6-.5-15.1 2.4-38.9 3.2-48.9 1.5-11.5-1.9-10.1-4 6.3-9.9 6.2-2.3 15.4-6.2 20.4-8.8 56.5-29.2 84.7-97.9 69.1-168.3-11.5-51.5-43.4-85-89.3-93.6-11.4-2.1-32.9-2.2-44.2 0-40.6 7.7-70 34.9-84.5 78.1-5.8 17.5-7.7 31.8-7.1 54 .6 21.3 2.2 32.3 7.3 49.2 11.8 39 34.4 64.6 78.4 88.7 4.7 2.6 9.6 5.8 11 7.1 2.3 2.4 2.4 2.6.7 4.2-1.6 1.6-3.8 1.8-17 1.7-45.3-.2-82.1-18.2-112.6-55-4.2-5.1-9.6-12.4-12.1-16.2-2.5-3.9-4.9-7.1-5.4-7.1-.5-.1-.6 3.7-.3 8.5l.5 8.5-2.4-2.2c-3-2.8-10.5-17.2-14.3-27.7-7.2-19.5-12.1-46.9-12.1-67.8 0-30.8 5.7-56.3 18.5-81.9 16.2-32.6 43.1-58.4 79-75.7 10.3-4.9 13-5.7 12.3-3.6 M731 1231.2c-6.2 15.5-15.9 40.1-21.6 54.5-5.7 14.5-10.4 26.7-10.4 27.2 0 1.5 7.9 2 39 2.7 26.5.5 51.1-.3 52.6-1.8.6-.6-46.1-108.6-47.5-110.1-.5-.5-5.9 11.9-12.1 27.5 diff --git a/FModel/Views/Resources/Resources.xaml b/FModel/Views/Resources/Resources.xaml index c33eb148..201841ef 100644 --- a/FModel/Views/Resources/Resources.xaml +++ b/FModel/Views/Resources/Resources.xaml @@ -98,17 +98,33 @@ - @@ -119,50 +135,97 @@ - - + + - - - - + + + + - - + + - - + + + + + + - + - - - + + - - + + - - + + - - + + diff --git a/FModel/Views/SettingsView.xaml b/FModel/Views/SettingsView.xaml index c7e8a1bf..1df9bdb3 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'}" @@ -611,7 +612,7 @@ HotKey="{Binding NextAudio, Source={x:Static local:Settings.UserSettings.Default}, Mode=TwoWay}" /> - + @@ -703,6 +704,65 @@ ToolTip="Paste a custom opcode map." /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -780,7 +840,7 @@ - + @@ -788,7 +848,7 @@ - + @@ -802,6 +862,25 @@ + + + + + + + + + + + + diff --git a/FModel/Views/SettingsView.xaml.cs b/FModel/Views/SettingsView.xaml.cs index 74e428ef..015729b5 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() { @@ -115,8 +120,8 @@ public partial class SettingsView var openFileDialog = new OpenFileDialog { Title = "Select a mapping file", - InitialDirectory = Path.Combine(UserSettings.Default.OutputDirectory, ".data"), - Filter = "USMAP Files (*.usmap)|*.usmap|All Files (*.*)|*.*" + InitialDirectory = CacheManager.MappingsDirectory, + Filter = "USMAP Files (*.usmap, *.jmap, *.jmap.gz)|*.usmap;*.jmap;*.jmap.gz|All Files (*.*)|*.*" }; if (!openFileDialog.ShowDialog().GetValueOrDefault()) @@ -292,4 +297,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(); + } }