diff --git a/FModel/Creator/Bases/FN/BaseCommunity.cs b/FModel/Creator/Bases/FN/BaseCommunity.cs index 4e63ac13..bd4b673c 100644 --- a/FModel/Creator/Bases/FN/BaseCommunity.cs +++ b/FModel/Creator/Bases/FN/BaseCommunity.cs @@ -1,5 +1,7 @@ +using System.Linq; using CUE4Parse.GameTypes.FN.Enums; using CUE4Parse.UE4.Assets.Exports; +using CUE4Parse.UE4.Assets.Objects; using CUE4Parse.UE4.Objects.GameplayTags; using CUE4Parse.UE4.Objects.UObject; using CUE4Parse.UE4.Versions; @@ -32,10 +34,27 @@ public class BaseCommunity : BaseIcon { ParseForReward(UserSettings.Default.CosmeticDisplayAsset); - if (Object.TryGetValue(out FPackageIndex series, "Series") && Utils.TryGetPackageIndexExport(series, out UObject export)) - _rarityName = export.Name; + if (Object.TryGetValue(out FPackageIndex series, "Series")) + { + _rarityName = series.Name; + } + else if (Object.TryGetValue(out FInstancedStruct[] dataList, "DataList") && + dataList.FirstOrDefault(d => d.NonConstStruct?.TryGetValue(out FPackageIndex _, "Series") == true) is { } dl) + { + _rarityName = dl.NonConstStruct?.Get("Series").Name; + } + else if (Object.TryGetValue(out FStructFallback componentContainer, "ComponentContainer") && + componentContainer.TryGetValue(out FPackageIndex[] components, "Components") && + components.FirstOrDefault(c => c.Name.Contains("Series")) is { } seriesDef && + seriesDef.TryLoad(out var seriesDefObj) && seriesDefObj is not null && + seriesDefObj.TryGetValue(out series, "Series")) + { + _rarityName = series.Name; + } else + { _rarityName = Object.GetOrDefault("Rarity", EFortRarity.Uncommon).GetDescription(); + } if (Object.TryGetValue(out FGameplayTagContainer gameplayTags, "GameplayTags")) CheckGameplayTags(gameplayTags); diff --git a/FModel/Creator/Bases/FN/BaseIcon.cs b/FModel/Creator/Bases/FN/BaseIcon.cs index fbe98810..9c5f2d30 100644 --- a/FModel/Creator/Bases/FN/BaseIcon.cs +++ b/FModel/Creator/Bases/FN/BaseIcon.cs @@ -1,315 +1,317 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Windows; -using CUE4Parse.GameTypes.FN.Enums; -using CUE4Parse.UE4.Assets.Exports; -using CUE4Parse.UE4.Assets.Exports.Engine; -using CUE4Parse.UE4.Assets.Exports.Material; -using CUE4Parse.UE4.Assets.Exports.Texture; -using CUE4Parse.UE4.Assets.Objects; -using CUE4Parse.UE4.Objects.Core.i18N; -using CUE4Parse.UE4.Objects.Core.Math; -using CUE4Parse.UE4.Objects.GameplayTags; -using CUE4Parse.UE4.Objects.UObject; -using CUE4Parse_Conversion.Textures; -using FModel.Settings; -using SkiaSharp; - -namespace FModel.Creator.Bases.FN; - -public class BaseIcon : UCreator -{ - public SKBitmap SeriesBackground { get; protected set; } - protected string ShortDescription { get; set; } - protected string CosmeticSource { get; set; } - protected Dictionary UserFacingFlags { get; set; } - - public BaseIcon(UObject uObject, EIconStyle style) : base(uObject, style) { } - - public void ParseForReward(bool isUsingDisplayAsset) - { - // rarity - if (Object.TryGetValue(out FPackageIndex series, "Series")) GetSeries(series); - else if (Object.TryGetValue(out FStructFallback componentContainer, "ComponentContainer")) GetSeries(componentContainer); - else GetRarity(Object.GetOrDefault("Rarity", EFortRarity.Uncommon)); // default is uncommon - - // preview - if (isUsingDisplayAsset && Utils.TryGetDisplayAsset(Object, out var preview)) - Preview = preview; - else if (Object.TryGetValue(out FPackageIndex itemDefinition, "HeroDefinition", "WeaponDefinition")) - Preview = Utils.GetBitmap(itemDefinition); - else if (Object.TryGetValue(out FSoftObjectPath largePreview, "LargePreviewImage", "EntryListIcon", "SmallPreviewImage", "BundleImage", "ItemDisplayAsset", "LargeIcon", "ToastIcon", "SmallIcon")) - Preview = Utils.GetBitmap(largePreview); - else if (Object.TryGetValue(out string s, "LargePreviewImage") && !string.IsNullOrEmpty(s)) - Preview = Utils.GetBitmap(s); - else if (Object.TryGetValue(out FPackageIndex otherPreview, "SmallPreviewImage", "ToastIcon", "access_item")) - Preview = Utils.GetBitmap(otherPreview); - else if (Object.TryGetValue(out UMaterialInstanceConstant materialInstancePreview, "EventCalloutImage")) - Preview = Utils.GetBitmap(materialInstancePreview); - else if (Object.TryGetValue(out FStructFallback brush, "IconBrush") && brush.TryGetValue(out UTexture2D res, "ResourceObject")) - Preview = Utils.GetBitmap(res); - - // text - if (Object.TryGetValue(out FText displayName, "DisplayName", "ItemName", "BundleName", "DefaultHeaderText", "UIDisplayName", "EntryName", "EventCalloutTitle")) - DisplayName = displayName.Text; - if (Object.TryGetValue(out FText description, "Description", "ItemDescription", "BundleDescription", "GeneralDescription", "DefaultBodyText", "UIDescription", "UIDisplayDescription", "EntryDescription", "EventCalloutDescription")) - Description = description.Text; - else if (Object.TryGetValue(out FText[] descriptions, "Description")) - Description = string.Join('\n', descriptions.Select(x => x.Text)); - if (Object.TryGetValue(out FText shortDescription, "ShortDescription", "UIDisplaySubName")) - ShortDescription = shortDescription.Text; - else if (Object.ExportType.Equals("AthenaItemWrapDefinition", StringComparison.OrdinalIgnoreCase)) - ShortDescription = Utils.GetLocalizedResource("Fort.Cosmetics", "ItemWrapShortDescription", "Wrap"); - - // Only works on non-cataba designs - if (Object.TryGetValue(out FStructFallback eventArrowColor, "EventArrowColor") && - eventArrowColor.TryGetValue(out FLinearColor specifiedArrowColor, "SpecifiedColor") && - Object.TryGetValue(out FStructFallback eventArrowShadowColor, "EventArrowShadowColor") && - eventArrowShadowColor.TryGetValue(out FLinearColor specifiedShadowColor, "SpecifiedColor")) - { - Background = new[] { SKColor.Parse(specifiedArrowColor.Hex), SKColor.Parse(specifiedShadowColor.Hex) }; - Border = new[] { SKColor.Parse(specifiedShadowColor.Hex), SKColor.Parse(specifiedArrowColor.Hex) }; - } - - Description = Utils.RemoveHtmlTags(Description); - } - - public override void ParseForInfo() - { - ParseForReward(UserSettings.Default.CosmeticDisplayAsset); - - if (Object.TryGetValue(out FGameplayTagContainer gameplayTags, "GameplayTags")) - CheckGameplayTags(gameplayTags); - if (Object.TryGetValue(out FPackageIndex cosmeticItem, "cosmetic_item")) - CosmeticSource = cosmeticItem.Name; - } - - protected void Draw(SKCanvas c) - { - switch (Style) - { - case EIconStyle.NoBackground: - DrawPreview(c); - break; - case EIconStyle.NoText: - DrawBackground(c); - DrawPreview(c); - DrawUserFacingFlags(c); - break; - default: - DrawBackground(c); - DrawPreview(c); - DrawTextBackground(c); - DrawDisplayName(c); - DrawDescription(c); - DrawToBottom(c, SKTextAlign.Right, CosmeticSource); - if (Description != ShortDescription) - DrawToBottom(c, SKTextAlign.Left, ShortDescription); - DrawUserFacingFlags(c); - break; - } - } - - public override SKBitmap[] Draw() - { - var ret = new SKBitmap(Width, Height, SKColorType.Rgba8888, SKAlphaType.Premul); - using var c = new SKCanvas(ret); - - Draw(c); - - return new[] { ret }; - } - - private void GetSeries(FPackageIndex s) - { - if (!Utils.TryGetPackageIndexExport(s, out UObject export)) return; - - GetSeries(export); - } - - private void GetSeries(FStructFallback s) - { - if (!s.TryGetValue(out FPackageIndex[] components, "Components")) return; - - foreach (var component in components) - { - if (!component.TryLoad(out var componentObj) || - !componentObj!.TryGetValue(out UObject componentSeriesDef, "Series")) - continue; - - GetSeries(componentSeriesDef); - break; - } - } - - protected void GetSeries(UObject uObject) - { - if (uObject is UTexture2D texture2D) - { - SeriesBackground = texture2D.Decode(); - return; - } - - if (uObject.TryGetValue(out FSoftObjectPath backgroundTexture, "BackgroundTexture")) - { - SeriesBackground = Utils.GetBitmap(backgroundTexture); - } - - if (uObject.TryGetValue(out FStructFallback colors, "Colors") && - colors.TryGetValue(out FLinearColor color1, "Color1") && - colors.TryGetValue(out FLinearColor color2, "Color2") && - colors.TryGetValue(out FLinearColor color3, "Color3")) - { - Background = new[] { SKColor.Parse(color1.Hex), SKColor.Parse(color3.Hex) }; - Border = new[] { SKColor.Parse(color2.Hex), SKColor.Parse(color1.Hex) }; - } - - if (uObject.Name.Equals("PlatformSeries") && - uObject.TryGetValue(out FSoftObjectPath itemCardMaterial, "ItemCardMaterial") && - Utils.TryLoadObject(itemCardMaterial.AssetPathName.Text, out UMaterialInstanceConstant material)) - { - foreach (var vectorParameter in material.VectorParameterValues) - { - if (vectorParameter.ParameterValue == null || !vectorParameter.ParameterInfo.Name.Text.Equals("ColorCircuitBackground")) - continue; - - Background[0] = SKColor.Parse(vectorParameter.ParameterValue.Value.Hex); - } - } - } - - private void GetRarity(EFortRarity r) - { - if (!Utils.TryLoadObject("FortniteGame/Content/Balance/RarityData.RarityData", out UObject export)) return; - - if (export.GetByIndex((int) r) is { } data && - data.TryGetValue(out FLinearColor color1, "Color1") && - data.TryGetValue(out FLinearColor color2, "Color2") && - data.TryGetValue(out FLinearColor color3, "Color3")) - { - Background = new[] { SKColor.Parse(color1.Hex), SKColor.Parse(color3.Hex) }; - Border = new[] { SKColor.Parse(color2.Hex), SKColor.Parse(color1.Hex) }; - } - } - - protected string GetCosmeticSet(string setName) - { - if (!Utils.TryLoadObject("FortniteGame/Content/Athena/Items/Cosmetics/Metadata/CosmeticSets.CosmeticSets", out UDataTable cosmeticSets)) - return string.Empty; - - if (!cosmeticSets.TryGetDataTableRow(setName, StringComparison.OrdinalIgnoreCase, out var uObject)) - return string.Empty; - - var name = string.Empty; - if (uObject.TryGetValue(out FText displayName, "DisplayName")) - name = displayName.Text; - - var format = Utils.GetLocalizedResource("Fort.Cosmetics", "CosmeticItemDescription_SetMembership_NotRich", "\nPart of the {0} set."); - return string.Format(format, name); - } - - protected (int, int) GetInternalSID(int number) - { - static int GetSeasonsInChapter(int chapter) => chapter switch - { - 1 => 10, - 2 => 8, - 3 => 4, - 4 => 5, - _ => 10 - }; - - var chapterIdx = 0; - var seasonIdx = 0; - while (number > 0) - { - var seasonsInChapter = GetSeasonsInChapter(++chapterIdx); - if (number > seasonsInChapter) - number -= seasonsInChapter; - else - { - seasonIdx = number; - number = 0; - } - } - return (chapterIdx, seasonIdx); - } - - protected string GetCosmeticSeason(string seasonNumber) - { - var s = seasonNumber["Cosmetics.Filter.Season.".Length..]; - var initial = int.Parse(s); - (int chapterIdx, int seasonIdx) = GetInternalSID(initial); - - var season = Utils.GetLocalizedResource("AthenaSeasonItemDefinitionInternal", "SeasonTextFormat", "Season {0}"); - var introduced = Utils.GetLocalizedResource("Fort.Cosmetics", "CosmeticItemDescription_Season", "\nIntroduced in {0}."); - if (initial <= 10) return Utils.RemoveHtmlTags(string.Format(introduced, string.Format(season, s))); - - var chapter = Utils.GetLocalizedResource("AthenaSeasonItemDefinitionInternal", "ChapterTextFormat", "Chapter {0}"); - var chapterFormat = Utils.GetLocalizedResource("AthenaSeasonItemDefinitionInternal", "ChapterSeasonTextFormat", "{0}, {1}"); - var d = string.Format(chapterFormat, string.Format(chapter, chapterIdx), string.Format(season, seasonIdx)); - return Utils.RemoveHtmlTags(string.Format(introduced, d)); - } - - private void CheckGameplayTags(FGameplayTagContainer gameplayTags) - { - if (gameplayTags.TryGetGameplayTag("Cosmetics.Source.", out var source)) - CosmeticSource = source.Text["Cosmetics.Source.".Length..]; - else if (gameplayTags.TryGetGameplayTag("Athena.ItemAction.", out var action)) - CosmeticSource = action.Text["Athena.ItemAction.".Length..]; - - if (gameplayTags.TryGetGameplayTag("Cosmetics.Set.", out var set)) - Description += GetCosmeticSet(set.Text); - if (gameplayTags.TryGetGameplayTag("Cosmetics.Filter.Season.", out var season)) - Description += GetCosmeticSeason(season.Text); - - GetUserFacingFlags(gameplayTags.GetAllGameplayTags( - "Cosmetics.UserFacingFlags.", "Homebase.Class.", "NPC.CharacterType.Survivor.Defender.")); - } - - protected void GetUserFacingFlags(IList userFacingFlags) - { - if (userFacingFlags.Count < 1 || !Utils.TryLoadObject("FortniteGame/Content/Items/ItemCategories.ItemCategories", out UObject itemCategories)) - return; - - if (!itemCategories.TryGetValue(out FStructFallback[] tertiaryCategories, "TertiaryCategories")) - return; - - UserFacingFlags = new Dictionary(userFacingFlags.Count); - foreach (var flag in userFacingFlags) - { - if (flag.Equals("Cosmetics.UserFacingFlags.HasUpgradeQuests", StringComparison.OrdinalIgnoreCase)) - { - if (Object.ExportType.Equals("AthenaPetCarrierItemDefinition", StringComparison.OrdinalIgnoreCase)) - UserFacingFlags[flag] = SKBitmap.Decode(Application.GetResourceStream(new Uri("pack://application:,,,/Resources/T-Icon-Pets-64.png"))?.Stream); - else UserFacingFlags[flag] = SKBitmap.Decode(Application.GetResourceStream(new Uri("pack://application:,,,/Resources/T-Icon-Quests-64.png"))?.Stream); - } - else - { - foreach (var category in tertiaryCategories) - { - if (category.TryGetValue(out FGameplayTagContainer tagContainer, "TagContainer") && tagContainer.TryGetGameplayTag(flag, out _) && - category.TryGetValue(out FStructFallback categoryBrush, "CategoryBrush") && categoryBrush.TryGetValue(out FStructFallback brushXxs, "Brush_XXS") && - brushXxs.TryGetValue(out FPackageIndex resourceObject, "ResourceObject") && Utils.TryGetPackageIndexExport(resourceObject, out UTexture2D texture)) - { - UserFacingFlags[flag] = Utils.GetBitmap(texture); - } - } - } - } - } - - private void DrawUserFacingFlags(SKCanvas c) - { - if (UserFacingFlags == null) return; - - const int size = 25; - var x = Margin * (int) 2.5; - foreach (var flag in UserFacingFlags.Values.Where(flag => flag != null)) - { - c.DrawBitmap(flag.Resize(size), new SKPoint(x, Margin * (int) 2.5), ImagePaint); - x += size; - } - } -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Windows; +using CUE4Parse.GameTypes.FN.Enums; +using CUE4Parse.UE4.Assets.Exports; +using CUE4Parse.UE4.Assets.Exports.Engine; +using CUE4Parse.UE4.Assets.Exports.Material; +using CUE4Parse.UE4.Assets.Exports.Texture; +using CUE4Parse.UE4.Assets.Objects; +using CUE4Parse.UE4.Objects.Core.i18N; +using CUE4Parse.UE4.Objects.Core.Math; +using CUE4Parse.UE4.Objects.GameplayTags; +using CUE4Parse.UE4.Objects.UObject; +using CUE4Parse_Conversion.Textures; +using FModel.Settings; +using SkiaSharp; + +namespace FModel.Creator.Bases.FN; + +public class BaseIcon : UCreator +{ + public SKBitmap SeriesBackground { get; protected set; } + protected string ShortDescription { get; set; } + protected string CosmeticSource { get; set; } + protected Dictionary UserFacingFlags { get; set; } + + public BaseIcon(UObject uObject, EIconStyle style) : base(uObject, style) { } + + public void ParseForReward(bool isUsingDisplayAsset) + { + // rarity + if (Object.TryGetValue(out FPackageIndex series, "Series")) GetSeries(series); + else if (Object.TryGetValue(out FInstancedStruct[] dataList, "DataList")) GetSeries(dataList); + else if (Object.TryGetValue(out FStructFallback componentContainer, "ComponentContainer")) GetSeries(componentContainer); + else GetRarity(Object.GetOrDefault("Rarity", EFortRarity.Uncommon)); // default is uncommon + + // preview + if (isUsingDisplayAsset && Utils.TryGetDisplayAsset(Object, out var preview)) + Preview = preview; + else if (Object.TryGetValue(out FPackageIndex itemDefinition, "HeroDefinition", "WeaponDefinition")) + Preview = Utils.GetBitmap(itemDefinition); + else if (Object.TryGetValue(out FSoftObjectPath largePreview, "LargePreviewImage", "EntryListIcon", "SmallPreviewImage", "BundleImage", "ItemDisplayAsset", "LargeIcon", "ToastIcon", "SmallIcon")) + Preview = Utils.GetBitmap(largePreview); + else if (Object.TryGetValue(out string s, "LargePreviewImage") && !string.IsNullOrEmpty(s)) + Preview = Utils.GetBitmap(s); + else if (Object.TryGetValue(out FPackageIndex otherPreview, "SmallPreviewImage", "ToastIcon", "access_item")) + Preview = Utils.GetBitmap(otherPreview); + else if (Object.TryGetValue(out UMaterialInstanceConstant materialInstancePreview, "EventCalloutImage")) + Preview = Utils.GetBitmap(materialInstancePreview); + else if (Object.TryGetValue(out FStructFallback brush, "IconBrush") && brush.TryGetValue(out UTexture2D res, "ResourceObject")) + Preview = Utils.GetBitmap(res); + + // text + if (Object.TryGetValue(out FText displayName, "DisplayName", "ItemName", "BundleName", "DefaultHeaderText", "UIDisplayName", "EntryName", "EventCalloutTitle")) + DisplayName = displayName.Text; + if (Object.TryGetValue(out FText description, "Description", "ItemDescription", "BundleDescription", "GeneralDescription", "DefaultBodyText", "UIDescription", "UIDisplayDescription", "EntryDescription", "EventCalloutDescription")) + Description = description.Text; + else if (Object.TryGetValue(out FText[] descriptions, "Description")) + Description = string.Join('\n', descriptions.Select(x => x.Text)); + if (Object.TryGetValue(out FText shortDescription, "ShortDescription", "UIDisplaySubName")) + ShortDescription = shortDescription.Text; + else if (Object.ExportType.Equals("AthenaItemWrapDefinition", StringComparison.OrdinalIgnoreCase)) + ShortDescription = Utils.GetLocalizedResource("Fort.Cosmetics", "ItemWrapShortDescription", "Wrap"); + + // Only works on non-cataba designs + if (Object.TryGetValue(out FStructFallback eventArrowColor, "EventArrowColor") && + eventArrowColor.TryGetValue(out FLinearColor specifiedArrowColor, "SpecifiedColor") && + Object.TryGetValue(out FStructFallback eventArrowShadowColor, "EventArrowShadowColor") && + eventArrowShadowColor.TryGetValue(out FLinearColor specifiedShadowColor, "SpecifiedColor")) + { + Background = new[] { SKColor.Parse(specifiedArrowColor.Hex), SKColor.Parse(specifiedShadowColor.Hex) }; + Border = new[] { SKColor.Parse(specifiedShadowColor.Hex), SKColor.Parse(specifiedArrowColor.Hex) }; + } + + Description = Utils.RemoveHtmlTags(Description); + } + + public override void ParseForInfo() + { + ParseForReward(UserSettings.Default.CosmeticDisplayAsset); + + if (Object.TryGetValue(out FGameplayTagContainer gameplayTags, "GameplayTags")) + CheckGameplayTags(gameplayTags); + if (Object.TryGetValue(out FPackageIndex cosmeticItem, "cosmetic_item")) + CosmeticSource = cosmeticItem.Name; + } + + protected void Draw(SKCanvas c) + { + switch (Style) + { + case EIconStyle.NoBackground: + DrawPreview(c); + break; + case EIconStyle.NoText: + DrawBackground(c); + DrawPreview(c); + DrawUserFacingFlags(c); + break; + default: + DrawBackground(c); + DrawPreview(c); + DrawTextBackground(c); + DrawDisplayName(c); + DrawDescription(c); + DrawToBottom(c, SKTextAlign.Right, CosmeticSource); + if (Description != ShortDescription) + DrawToBottom(c, SKTextAlign.Left, ShortDescription); + DrawUserFacingFlags(c); + break; + } + } + + public override SKBitmap[] Draw() + { + var ret = new SKBitmap(Width, Height, SKColorType.Rgba8888, SKAlphaType.Premul); + using var c = new SKCanvas(ret); + + Draw(c); + + return new[] { ret }; + } + + private void GetSeries(FPackageIndex s) + { + if (!Utils.TryGetPackageIndexExport(s, out UObject export)) return; + + GetSeries(export); + } + + private void GetSeries(FInstancedStruct[] s) + { + if (s.FirstOrDefault(d => d.NonConstStruct?.TryGetValue(out FPackageIndex _, "Series") == true) is { } dl) + GetSeries(dl.NonConstStruct?.Get("Series")); + } + + private void GetSeries(FStructFallback s) + { + if (!s.TryGetValue(out FPackageIndex[] components, "Components")) return; + if (components.FirstOrDefault(c => c.Name.Contains("Series")) is not { } seriesDef || + !seriesDef.TryLoad(out var seriesDefObj) || seriesDefObj is null || + !seriesDefObj.TryGetValue(out UObject series, "Series")) return; + + GetSeries(series); + } + + protected void GetSeries(UObject uObject) + { + if (uObject is UTexture2D texture2D) + { + SeriesBackground = texture2D.Decode(); + return; + } + + if (uObject.TryGetValue(out FSoftObjectPath backgroundTexture, "BackgroundTexture")) + { + SeriesBackground = Utils.GetBitmap(backgroundTexture); + } + + if (uObject.TryGetValue(out FStructFallback colors, "Colors") && + colors.TryGetValue(out FLinearColor color1, "Color1") && + colors.TryGetValue(out FLinearColor color2, "Color2") && + colors.TryGetValue(out FLinearColor color3, "Color3")) + { + Background = new[] { SKColor.Parse(color1.Hex), SKColor.Parse(color3.Hex) }; + Border = new[] { SKColor.Parse(color2.Hex), SKColor.Parse(color1.Hex) }; + } + + if (uObject.Name.Equals("PlatformSeries") && + uObject.TryGetValue(out FSoftObjectPath itemCardMaterial, "ItemCardMaterial") && + Utils.TryLoadObject(itemCardMaterial.AssetPathName.Text, out UMaterialInstanceConstant material)) + { + foreach (var vectorParameter in material.VectorParameterValues) + { + if (vectorParameter.ParameterValue == null || !vectorParameter.ParameterInfo.Name.Text.Equals("ColorCircuitBackground")) + continue; + + Background[0] = SKColor.Parse(vectorParameter.ParameterValue.Value.Hex); + } + } + } + + private void GetRarity(EFortRarity r) + { + if (!Utils.TryLoadObject("FortniteGame/Content/Balance/RarityData.RarityData", out UObject export)) return; + + if (export.GetByIndex((int) r) is { } data && + data.TryGetValue(out FLinearColor color1, "Color1") && + data.TryGetValue(out FLinearColor color2, "Color2") && + data.TryGetValue(out FLinearColor color3, "Color3")) + { + Background = new[] { SKColor.Parse(color1.Hex), SKColor.Parse(color3.Hex) }; + Border = new[] { SKColor.Parse(color2.Hex), SKColor.Parse(color1.Hex) }; + } + } + + protected string GetCosmeticSet(string setName) + { + if (!Utils.TryLoadObject("FortniteGame/Content/Athena/Items/Cosmetics/Metadata/CosmeticSets.CosmeticSets", out UDataTable cosmeticSets)) + return string.Empty; + + if (!cosmeticSets.TryGetDataTableRow(setName, StringComparison.OrdinalIgnoreCase, out var uObject)) + return string.Empty; + + var name = string.Empty; + if (uObject.TryGetValue(out FText displayName, "DisplayName")) + name = displayName.Text; + + var format = Utils.GetLocalizedResource("Fort.Cosmetics", "CosmeticItemDescription_SetMembership_NotRich", "\nPart of the {0} set."); + return string.Format(format, name); + } + + protected (int, int) GetInternalSID(int number) + { + static int GetSeasonsInChapter(int chapter) => chapter switch + { + 1 => 10, + 2 => 8, + 3 => 4, + 4 => 5, + _ => 10 + }; + + var chapterIdx = 0; + var seasonIdx = 0; + while (number > 0) + { + var seasonsInChapter = GetSeasonsInChapter(++chapterIdx); + if (number > seasonsInChapter) + number -= seasonsInChapter; + else + { + seasonIdx = number; + number = 0; + } + } + return (chapterIdx, seasonIdx); + } + + protected string GetCosmeticSeason(string seasonNumber) + { + var s = seasonNumber["Cosmetics.Filter.Season.".Length..]; + var initial = int.Parse(s); + (int chapterIdx, int seasonIdx) = GetInternalSID(initial); + + var season = Utils.GetLocalizedResource("AthenaSeasonItemDefinitionInternal", "SeasonTextFormat", "Season {0}"); + var introduced = Utils.GetLocalizedResource("Fort.Cosmetics", "CosmeticItemDescription_Season", "\nIntroduced in {0}."); + if (initial <= 10) return Utils.RemoveHtmlTags(string.Format(introduced, string.Format(season, s))); + + var chapter = Utils.GetLocalizedResource("AthenaSeasonItemDefinitionInternal", "ChapterTextFormat", "Chapter {0}"); + var chapterFormat = Utils.GetLocalizedResource("AthenaSeasonItemDefinitionInternal", "ChapterSeasonTextFormat", "{0}, {1}"); + var d = string.Format(chapterFormat, string.Format(chapter, chapterIdx), string.Format(season, seasonIdx)); + return Utils.RemoveHtmlTags(string.Format(introduced, d)); + } + + private void CheckGameplayTags(FGameplayTagContainer gameplayTags) + { + if (gameplayTags.TryGetGameplayTag("Cosmetics.Source.", out var source)) + CosmeticSource = source.Text["Cosmetics.Source.".Length..]; + else if (gameplayTags.TryGetGameplayTag("Athena.ItemAction.", out var action)) + CosmeticSource = action.Text["Athena.ItemAction.".Length..]; + + if (gameplayTags.TryGetGameplayTag("Cosmetics.Set.", out var set)) + Description += GetCosmeticSet(set.Text); + if (gameplayTags.TryGetGameplayTag("Cosmetics.Filter.Season.", out var season)) + Description += GetCosmeticSeason(season.Text); + + GetUserFacingFlags(gameplayTags.GetAllGameplayTags( + "Cosmetics.UserFacingFlags.", "Homebase.Class.", "NPC.CharacterType.Survivor.Defender.")); + } + + protected void GetUserFacingFlags(IList userFacingFlags) + { + if (userFacingFlags.Count < 1 || !Utils.TryLoadObject("FortniteGame/Content/Items/ItemCategories.ItemCategories", out UObject itemCategories)) + return; + + if (!itemCategories.TryGetValue(out FStructFallback[] tertiaryCategories, "TertiaryCategories")) + return; + + UserFacingFlags = new Dictionary(userFacingFlags.Count); + foreach (var flag in userFacingFlags) + { + if (flag.Equals("Cosmetics.UserFacingFlags.HasUpgradeQuests", StringComparison.OrdinalIgnoreCase)) + { + if (Object.ExportType.Equals("AthenaPetCarrierItemDefinition", StringComparison.OrdinalIgnoreCase)) + UserFacingFlags[flag] = SKBitmap.Decode(Application.GetResourceStream(new Uri("pack://application:,,,/Resources/T-Icon-Pets-64.png"))?.Stream); + else UserFacingFlags[flag] = SKBitmap.Decode(Application.GetResourceStream(new Uri("pack://application:,,,/Resources/T-Icon-Quests-64.png"))?.Stream); + } + else + { + foreach (var category in tertiaryCategories) + { + if (category.TryGetValue(out FGameplayTagContainer tagContainer, "TagContainer") && tagContainer.TryGetGameplayTag(flag, out _) && + category.TryGetValue(out FStructFallback categoryBrush, "CategoryBrush") && categoryBrush.TryGetValue(out FStructFallback brushXxs, "Brush_XXS") && + brushXxs.TryGetValue(out FPackageIndex resourceObject, "ResourceObject") && Utils.TryGetPackageIndexExport(resourceObject, out UTexture2D texture)) + { + UserFacingFlags[flag] = Utils.GetBitmap(texture); + } + } + } + } + } + + private void DrawUserFacingFlags(SKCanvas c) + { + if (UserFacingFlags == null) return; + + const int size = 25; + var x = Margin * (int) 2.5; + foreach (var flag in UserFacingFlags.Values.Where(flag => flag != null)) + { + c.DrawBitmap(flag.Resize(size), new SKPoint(x, Margin * (int) 2.5), ImagePaint); + x += size; + } + } +} diff --git a/FModel/ViewModels/CUE4ParseViewModel.cs b/FModel/ViewModels/CUE4ParseViewModel.cs index c7dd23ca..109ade2d 100644 --- a/FModel/ViewModels/CUE4ParseViewModel.cs +++ b/FModel/ViewModels/CUE4ParseViewModel.cs @@ -1,1003 +1,1005 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Net.Http.Headers; -using System.Text.RegularExpressions; -using System.Threading; -using System.Threading.Tasks; -using System.Windows; -using AdonisUI.Controls; -using CUE4Parse.Compression; -using CUE4Parse.Encryption.Aes; -using CUE4Parse.FileProvider; -using CUE4Parse.FileProvider.Vfs; -using CUE4Parse.MappingsProvider; -using CUE4Parse.UE4.AssetRegistry; -using CUE4Parse.UE4.Assets.Exports; -using CUE4Parse.UE4.Assets.Exports.Animation; -using CUE4Parse.UE4.Assets.Exports.Material; -using CUE4Parse.UE4.Assets.Exports.SkeletalMesh; -using CUE4Parse.UE4.Assets.Exports.Sound; -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.IO; -using CUE4Parse.UE4.Localization; -using CUE4Parse.UE4.Objects.Core.Serialization; -using CUE4Parse.UE4.Objects.Engine; -using CUE4Parse.UE4.Oodle.Objects; -using CUE4Parse.UE4.Readers; -using CUE4Parse.UE4.Shaders; -using CUE4Parse.UE4.Versions; -using CUE4Parse.UE4.Wwise; -using CUE4Parse_Conversion; -using CUE4Parse_Conversion.Sounds; -using CUE4Parse.GameTypes.UDWN.Encryption.Aes; -using CUE4Parse.GameTypes.DBD.Encryption.Aes; -using CUE4Parse.GameTypes.PAXDEI.Encryption.Aes; -using CUE4Parse.GameTypes.NetEase.MAR.Encryption.Aes; -using CUE4Parse.GameTypes.FSR.Encryption.Aes; -using EpicManifestParser; -using FModel.Creator; -using FModel.Extensions; -using FModel.Framework; -using FModel.Services; -using FModel.Settings; -using FModel.Views; -using FModel.Views.Resources.Controls; -using FModel.Views.Snooper; -using Newtonsoft.Json; -using Ookii.Dialogs.Wpf; -using OpenTK.Windowing.Common; -using OpenTK.Windowing.Desktop; -using Serilog; -using SkiaSharp; -using UE4Config.Parsing; -using Application = System.Windows.Application; - -namespace FModel.ViewModels; - -public class CUE4ParseViewModel : ViewModel -{ - private ThreadWorkerViewModel _threadWorkerView => ApplicationService.ThreadWorkerView; - private ApiEndpointViewModel _apiEndpointView => ApplicationService.ApiEndpointView; - 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 _fnLive = new(@"^FortniteGame(/|\\)Content(/|\\)Paks(/|\\)", - RegexOptions.Compiled | RegexOptions.Singleline | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); - - private string _internalGameName; - public string InternalGameName - { - get => _internalGameName; - set => SetProperty(ref _internalGameName, value); - } - - private bool _modelIsOverwritingMaterial; - public bool ModelIsOverwritingMaterial - { - get => _modelIsOverwritingMaterial; - set => SetProperty(ref _modelIsOverwritingMaterial, value); - } - - private bool _modelIsWaitingAnimation; - public bool ModelIsWaitingAnimation - { - get => _modelIsWaitingAnimation; - set => SetProperty(ref _modelIsWaitingAnimation, value); - } - - public bool IsSnooperOpen => _snooper is { Exists: true, IsVisible: true }; - private Snooper _snooper; - public Snooper SnooperViewer - { - get - { - if (_snooper != null) return _snooper; - - return Application.Current.Dispatcher.Invoke(delegate - { - var scale = ImGuiController.GetDpiScale(); - var htz = Snooper.GetMaxRefreshFrequency(); - return _snooper = new Snooper( - new GameWindowSettings { UpdateFrequency = htz }, - new NativeWindowSettings - { - ClientSize = new OpenTK.Mathematics.Vector2i( - Convert.ToInt32(SystemParameters.MaximizedPrimaryScreenWidth * .75 * scale), - Convert.ToInt32(SystemParameters.MaximizedPrimaryScreenHeight * .85 * scale)), - NumberOfSamples = Constants.SAMPLES_COUNT, - WindowBorder = WindowBorder.Resizable, - Flags = ContextFlags.ForwardCompatible, - Profile = ContextProfile.Core, - Vsync = VSyncMode.Adaptive, - APIVersion = new Version(4, 6), - StartVisible = false, - StartFocused = false, - Title = "3D Viewer" - }); - }); - } - } - - public AbstractVfsFileProvider Provider { get; } - public GameDirectoryViewModel GameDirectory { get; } - public AssetsFolderViewModel AssetsFolder { get; } - public SearchViewModel SearchVm { get; } - public TabControlViewModel TabControl { get; } - public ConfigIni IoStoreOnDemand { get; } - - public CUE4ParseViewModel() - { - var currentDir = UserSettings.Default.CurrentDir; - var gameDirectory = currentDir.GameDirectory; - var versionContainer = new VersionContainer( - game: currentDir.UeVersion, platform: currentDir.TexturePlatform, - customVersions: new FCustomVersionContainer(currentDir.Versioning.CustomVersions), - optionOverrides: currentDir.Versioning.Options, - mapStructTypesOverrides: currentDir.Versioning.MapStructTypes); - - switch (gameDirectory) - { - case Constants._FN_LIVE_TRIGGER: - { - InternalGameName = "FortniteGame"; - Provider = new StreamedFileProvider("FortniteLive", true, versionContainer); - break; - } - case Constants._VAL_LIVE_TRIGGER: - { - InternalGameName = "ShooterGame"; - Provider = new StreamedFileProvider("ValorantLive", true, versionContainer); - break; - } - default: - { - InternalGameName = gameDirectory.SubstringBeforeLast(gameDirectory.Contains("eFootball") ? "\\pak" : "\\Content").SubstringAfterLast("\\"); - Provider = InternalGameName switch - { - "StateOfDecay2" => new DefaultFileProvider(new DirectoryInfo(gameDirectory), - new DirectoryInfo[] - { - new(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + "\\StateOfDecay2\\Saved\\Paks"), - new(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + "\\StateOfDecay2\\Saved\\DisabledPaks") - }, SearchOption.AllDirectories, true, versionContainer), - "eFootball" => new DefaultFileProvider(new DirectoryInfo(gameDirectory), - new DirectoryInfo[] - { - new(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData) + "\\KONAMI\\eFootball\\ST\\Download") - }, SearchOption.AllDirectories, true, versionContainer), - _ => new DefaultFileProvider(gameDirectory, SearchOption.AllDirectories, true, versionContainer) - }; - - break; - } - } - Provider.ReadScriptData = UserSettings.Default.ReadScriptData; - Provider.CustomEncryption = Provider.Versions.Game switch - { - EGame.GAME_MarvelRivals => MarvelAes.MarvelDecrypt, - EGame.GAME_Undawn => ToaaAes.ToaaDecrypt, - EGame.GAME_DeadByDaylight => DBDAes.DbDDecrypt, - EGame.GAME_PaxDei => PaxDeiAes.PaxDeiDecrypt, - EGame.GAME_3on3FreeStyleRebound => FreeStyleReboundAes.FSRDecrypt, - _ => Provider.CustomEncryption - }; - - GameDirectory = new GameDirectoryViewModel(); - AssetsFolder = new AssetsFolderViewModel(); - SearchVm = new SearchViewModel(); - TabControl = new TabControlViewModel(); - IoStoreOnDemand = new ConfigIni(nameof(IoStoreOnDemand)); - } - - public async Task Initialize() - { - await _threadWorkerView.Begin(cancellationToken => - { - switch (Provider) - { - case StreamedFileProvider p: - switch (p.LiveGame) - { - case "FortniteLive": - { - var manifestInfo = _apiEndpointView.EpicApi.GetManifest(cancellationToken); - if (manifestInfo is null) - { - 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, - ChunkBaseUrl = "http://epicgames-download1.akamaized.net/Builds/Fortnite/CloudDir/", - Zlibng = ZlibHelper.Instance - }; - - var startTs = Stopwatch.GetTimestamp(); - var (manifest, _) = manifestInfo.DownloadAndParseAsync(manifestOptions, - cancellationToken: cancellationToken).GetAwaiter().GetResult(); - var parseTime = Stopwatch.GetElapsedTime(startTs); - const bool cacheChunksAsIs = false; - - foreach (var fileManifest in manifest.FileManifestList) - { - if (fileManifest.FileName.Equals("Cloud/IoStoreOnDemand.ini", StringComparison.OrdinalIgnoreCase)) - { - IoStoreOnDemand.Read(new StreamReader(fileManifest.GetStream(cacheChunksAsIs))); - continue; - } - if (!_fnLive.IsMatch(fileManifest.FileName)) continue; - - p.RegisterVfs(fileManifest.FileName, [fileManifest.GetStream(cacheChunksAsIs)] - , it => new FStreamArchive(it, manifest.FileManifestList.First(x => x.FileName.Equals(it)).GetStream(cacheChunksAsIs), p.Versions)); - } - - FLogger.Append(ELog.Information, () => - FLogger.Text($"Fortnite [LIVE] has been loaded successfully in {parseTime.TotalMilliseconds}ms", Constants.WHITE, true)); - break; - } - case "ValorantLive": - { - var manifestInfo = _apiEndpointView.ValorantApi.GetManifest(cancellationToken); - if (manifestInfo == null) - { - throw new Exception("Could not load latest Valorant manifest, you may have to switch to your local installation."); - } - - for (var i = 0; i < manifestInfo.Paks.Length; i++) - { - p.RegisterVfs(manifestInfo.Paks[i].GetFullName(), [manifestInfo.GetPakStream(i)]); - } - - FLogger.Append(ELog.Information, () => - FLogger.Text($"Valorant '{manifestInfo.Header.GameVersion}' has been loaded successfully", Constants.WHITE, true)); - break; - } - } - - break; - case DefaultFileProvider: - { - var ioStoreOnDemandPath = Path.Combine(UserSettings.Default.GameDirectory, "..\\..\\..\\Cloud\\IoStoreOnDemand.ini"); - if (File.Exists(ioStoreOnDemandPath)) - { - using var s = new StreamReader(ioStoreOnDemandPath); - IoStoreOnDemand.Read(s); - } - break; - } - } - - Provider.Initialize(); - Log.Information($"{Provider.Versions.Game} ({Provider.Versions.Platform}) | Archives: x{Provider.UnloadedVfs.Count} | AES: x{Provider.RequiredKeys.Count}"); - - foreach (var vfs in Provider.UnloadedVfs) // push files from the provider to the ui - { - cancellationToken.ThrowIfCancellationRequested(); - if (!_hiddenArchives.IsMatch(vfs.Name)) continue; - - GameDirectory.Add(vfs); - } - }); - } - - /// - /// load virtual files system from GameDirectory - /// - /// - public void LoadVfs(CancellationToken token, IEnumerable aesKeys) - { - GameDirectory.DeactivateAll(); - - // load files using UnloadedVfs to include non-encrypted vfs - foreach (var key in aesKeys) - { - token.ThrowIfCancellationRequested(); // cancel if needed - - var k = key.Key.Trim(); - if (k.Length != 66) k = Constants.ZERO_64_CHAR; - Provider.SubmitKey(key.Guid, new FAesKey(k)); - } - Provider.PostMount(); - - // files in MountedVfs will be enabled - foreach (var file in GameDirectory.DirectoryFiles) - { - token.ThrowIfCancellationRequested(); - if (Provider.MountedVfs.FirstOrDefault(x => x.Name == file.Name) is not { } vfs) - { - if (Provider.UnloadedVfs.FirstOrDefault(x => x.Name == file.Name) is IoStoreReader store) - file.FileCount = (int) store.TocResource.Header.TocEntryCount - 1; - - continue; - } - - file.IsEnabled = true; - file.MountPoint = vfs.MountPoint; - file.FileCount = vfs.FileCount; - } - - InternalGameName = Provider.InternalGameName; - - var aesMax = Provider.RequiredKeys.Count + Provider.Keys.Count; - var archiveMax = Provider.UnloadedVfs.Count + Provider.MountedVfs.Count; - Log.Information($"Project: {InternalGameName} | Mounted: {Provider.MountedVfs.Count}/{archiveMax} | AES: {Provider.Keys.Count}/{aesMax}"); - } - - public void ClearProvider() - { - if (Provider == null) return; - - AssetsFolder.Folders.Clear(); - SearchVm.SearchResults.Clear(); - Helper.CloseWindow("Search View"); - Provider.UnloadNonStreamedVfs(); - GC.Collect(); - } - - public async Task RefreshAes() - { - // game directory dependent, we don't have the provider game name yet since we don't have aes keys - // except when this comes from the AES Manager - if (!UserSettings.IsEndpointValid(EEndpointType.Aes, out var endpoint)) - return; - - await _threadWorkerView.Begin(cancellationToken => - { - var aes = _apiEndpointView.DynamicApi.GetAesKeys(cancellationToken, endpoint.Url, endpoint.Path); - if (aes is not { IsValid: true }) return; - - UserSettings.Default.CurrentDir.AesKeys = aes; - }); - } - - public async Task InitInformation() - { - await _threadWorkerView.Begin(cancellationToken => - { - var info = _apiEndpointView.FModelApi.GetNews(cancellationToken, Provider.InternalGameName); - if (info == null) return; - - FLogger.Append(ELog.None, () => - { - for (var i = 0; i < info.Messages.Length; i++) - { - FLogger.Text(info.Messages[i], info.Colors[i], bool.Parse(info.NewLines[i])); - } - }); - }); - } - - public Task InitMappings(bool force = false) - { - if (!UserSettings.IsEndpointValid(EEndpointType.Mapping, out var endpoint)) - { - Provider.MappingsContainer = null; - return Task.CompletedTask; - } - - return Task.Run(() => - { - var l = ELog.Information; - if (endpoint.Overwrite && File.Exists(endpoint.FilePath)) - { - Provider.MappingsContainer = new FileUsmapTypeMappingsProvider(endpoint.FilePath); - } - else if (endpoint.IsValid) - { - var mappingsFolder = Path.Combine(UserSettings.Default.OutputDirectory, ".data"); - if (endpoint.Path == "$.[?(@.meta.compressionMethod=='Oodle')].['url','fileName']") endpoint.Path = "$.[0].['url','fileName']"; - var mappings = _apiEndpointView.DynamicApi.GetMappings(default, endpoint.Url, endpoint.Path); - if (mappings is { Length: > 0 }) - { - foreach (var mapping in mappings) - { - if (!mapping.IsValid) continue; - - var mappingPath = Path.Combine(mappingsFolder, mapping.FileName); - if (force || !File.Exists(mappingPath)) - { - _apiEndpointView.DownloadFile(mapping.Url, mappingPath); - } - - Provider.MappingsContainer = new FileUsmapTypeMappingsProvider(mappingPath); - break; - } - } - - if (Provider.MappingsContainer == null) - { - var latestUsmaps = new DirectoryInfo(mappingsFolder).GetFiles("*_oo.usmap"); - if (latestUsmaps.Length <= 0) return; - - var latestUsmapInfo = latestUsmaps.OrderBy(f => f.LastWriteTime).Last(); - Provider.MappingsContainer = new FileUsmapTypeMappingsProvider(latestUsmapInfo.FullName); - l = ELog.Warning; - } - } - - if (Provider.MappingsContainer is FileUsmapTypeMappingsProvider m) - { - Log.Information($"Mappings pulled from '{m.FileName}'"); - FLogger.Append(l, () => FLogger.Text($"Mappings pulled from '{m.FileName}'", Constants.WHITE, true)); - } - }); - } - - public Task VerifyConsoleVariables() - { - if (Provider.Versions["StripAdditiveRefPose"]) - { - FLogger.Append(ELog.Warning, () => - FLogger.Text("Additive animations have their reference pose stripped, which will lead to inaccurate preview and export", Constants.WHITE, true)); - } - - return Task.CompletedTask; - } - - public Task VerifyOnDemandArchives() - { - // only local fortnite - if (Provider is not DefaultFileProvider || !Provider.InternalGameName.Equals("FortniteGame", StringComparison.OrdinalIgnoreCase)) - return Task.CompletedTask; - - // scuffed but working - var persistentDownloadDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "FortniteGame/Saved/PersistentDownloadDir"); - var iasFileInfo = new FileInfo(Path.Combine(persistentDownloadDir, "ias", "ias.cache.0")); - if (!iasFileInfo.Exists || iasFileInfo.Length == 0) - return Task.CompletedTask; - - return Task.Run(async () => - { - var inst = new List(); - IoStoreOnDemand.FindPropertyInstructions("Endpoint", "TocPath", inst); - if (inst.Count <= 0) return; - - var ioStoreOnDemandPath = Path.Combine(UserSettings.Default.GameDirectory, "..\\..\\..\\Cloud", inst[0].Value.SubstringAfterLast("/").SubstringBefore("\"")); - if (!File.Exists(ioStoreOnDemandPath)) return; - - await _apiEndpointView.EpicApi.VerifyAuth(default); - await Provider.RegisterVfs(new IoChunkToc(ioStoreOnDemandPath), new IoStoreOnDemandOptions - { - ChunkBaseUri = new Uri("https://download.epicgames.com/ias/fortnite/", UriKind.Absolute), - ChunkCacheDirectory = Directory.CreateDirectory(Path.Combine(UserSettings.Default.OutputDirectory, ".data")), - Authorization = new AuthenticationHeaderValue("Bearer", UserSettings.Default.LastAuthResponse.AccessToken), - Timeout = TimeSpan.FromSeconds(30) - }); - var onDemandCount = await Provider.MountAsync(); - FLogger.Append(ELog.Information, () => - FLogger.Text($"{onDemandCount} on-demand archive{(onDemandCount > 1 ? "s" : "")} streamed via epicgames.com", Constants.WHITE, true)); - }); - } - - public int LocalizedResourcesCount { get; set; } - public bool LocalResourcesDone { get; set; } - public bool HotfixedResourcesDone { get; set; } - public async Task LoadLocalizedResources() - { - var snapshot = LocalizedResourcesCount; - await Task.WhenAll(LoadGameLocalizedResources(), LoadHotfixedLocalizedResources()).ConfigureAwait(false); - if (snapshot != LocalizedResourcesCount) - { - FLogger.Append(ELog.Information, () => - FLogger.Text($"{LocalizedResourcesCount} localized resources loaded for '{UserSettings.Default.AssetLanguage.GetDescription()}'", Constants.WHITE, true)); - Utils.Typefaces = new Typefaces(this); - } - } - private Task LoadGameLocalizedResources() - { - if (LocalResourcesDone) return Task.CompletedTask; - return Task.Run(() => - { - LocalizedResourcesCount += Provider.LoadLocalization(UserSettings.Default.AssetLanguage); - LocalResourcesDone = true; - }); - } - private Task LoadHotfixedLocalizedResources() - { - if (!Provider.InternalGameName.Equals("fortnitegame", StringComparison.OrdinalIgnoreCase) || HotfixedResourcesDone) return Task.CompletedTask; - return Task.Run(() => - { - var hotfixes = ApplicationService.ApiEndpointView.CentralApi.GetHotfixes(default, Provider.GetLanguageCode(UserSettings.Default.AssetLanguage)); - if (hotfixes == null) return; - - HotfixedResourcesDone = true; - foreach (var entries in hotfixes) - { - if (!Provider.LocalizedResources.ContainsKey(entries.Key)) - Provider.LocalizedResources[entries.Key] = new Dictionary(); - - foreach (var keyValue in entries.Value) - { - Provider.LocalizedResources[entries.Key][keyValue.Key] = keyValue.Value; - LocalizedResourcesCount++; - } - } - }); - } - - private int _virtualPathCount { get; set; } - public Task LoadVirtualPaths() - { - if (_virtualPathCount > 0) return Task.CompletedTask; - return Task.Run(() => - { - _virtualPathCount = Provider.LoadVirtualPaths(UserSettings.Default.CurrentDir.UeVersion.GetVersion()); - if (_virtualPathCount > 0) - { - FLogger.Append(ELog.Information, () => - FLogger.Text($"{_virtualPathCount} virtual paths loaded", Constants.WHITE, true)); - } - else - { - FLogger.Append(ELog.Warning, () => - FLogger.Text("Could not load virtual paths, plugin manifest may not exist", Constants.WHITE, true)); - } - }); - } - - public void ExtractSelected(CancellationToken cancellationToken, IEnumerable assetItems) - { - foreach (var asset in assetItems) - { - Thread.Yield(); - cancellationToken.ThrowIfCancellationRequested(); - Extract(cancellationToken, asset.FullPath, TabControl.HasNoTabs); - } - } - - private void BulkFolder(CancellationToken cancellationToken, TreeItem folder, Action action) - { - foreach (var asset in folder.AssetsList.Assets) - { - Thread.Yield(); - cancellationToken.ThrowIfCancellationRequested(); - try - { - action(asset); - } - catch - { - // ignore - } - } - - foreach (var f in folder.Folders) BulkFolder(cancellationToken, f, action); - } - - public void ExportFolder(CancellationToken cancellationToken, TreeItem folder) - { - Parallel.ForEach(folder.AssetsList.Assets, asset => - { - cancellationToken.ThrowIfCancellationRequested(); - ExportData(asset.FullPath, false); - }); - - foreach (var f in folder.Folders) ExportFolder(cancellationToken, f); - } - - public void ExtractFolder(CancellationToken cancellationToken, TreeItem folder) - => BulkFolder(cancellationToken, folder, asset => Extract(cancellationToken, asset.FullPath, TabControl.HasNoTabs)); - - public void SaveFolder(CancellationToken cancellationToken, TreeItem folder) - => BulkFolder(cancellationToken, folder, asset => Extract(cancellationToken, asset.FullPath, TabControl.HasNoTabs, EBulkType.Properties | EBulkType.Auto)); - - public void TextureFolder(CancellationToken cancellationToken, TreeItem folder) - => BulkFolder(cancellationToken, folder, asset => Extract(cancellationToken, asset.FullPath, TabControl.HasNoTabs, EBulkType.Textures | EBulkType.Auto)); - - public void ModelFolder(CancellationToken cancellationToken, TreeItem folder) - => BulkFolder(cancellationToken, folder, asset => Extract(cancellationToken, asset.FullPath, TabControl.HasNoTabs, EBulkType.Meshes | EBulkType.Auto)); - - public void AnimationFolder(CancellationToken cancellationToken, TreeItem folder) - => BulkFolder(cancellationToken, folder, asset => Extract(cancellationToken, asset.FullPath, TabControl.HasNoTabs, EBulkType.Animations | EBulkType.Auto)); - - public void Extract(CancellationToken cancellationToken, string fullPath, bool addNewTab = false, EBulkType bulk = EBulkType.None) - { - Log.Information("User DOUBLE-CLICKED to extract '{FullPath}'", fullPath); - - var directory = fullPath.SubstringBeforeLast('/'); - var fileName = fullPath.SubstringAfterLast('/'); - var ext = fullPath.SubstringAfterLast('.').ToLower(); - - if (addNewTab && TabControl.CanAddTabs) TabControl.AddTab(fileName, directory); - else TabControl.SelectedTab.SoftReset(fileName, directory); - TabControl.SelectedTab.Highlighter = AvalonExtensions.HighlighterSelector(ext); - - var updateUi = !HasFlag(bulk, EBulkType.Auto); - var saveProperties = HasFlag(bulk, EBulkType.Properties); - var saveTextures = HasFlag(bulk, EBulkType.Textures); - switch (ext) - { - case "uasset": - case "umap": - { - var exports = Provider.LoadAllObjects(fullPath); - TabControl.SelectedTab.SetDocumentText(JsonConvert.SerializeObject(exports, Formatting.Indented), saveProperties, updateUi); - if (HasFlag(bulk, EBulkType.Properties)) break; // do not search for viewable exports if we are dealing with jsons - - foreach (var e in exports) - { - if (CheckExport(cancellationToken, e, bulk)) - break; - } - - break; - } - case "upluginmanifest": - case "uproject": - case "manifest": - case "uplugin": - case "archive": - case "vmodule": - case "verse": - case "html": - case "json": - case "ini": - case "txt": - case "log": - case "bat": - case "dat": - case "cfg": - case "ide": - case "ipl": - case "zon": - case "xml": - case "css": - case "csv": - case "pem": - case "tps": - case "lua": - case "js": - case "po": - case "h": - { - if (Provider.TrySaveAsset(fullPath, out var data)) - { - using var stream = new MemoryStream(data) { Position = 0 }; - using var reader = new StreamReader(stream); - - TabControl.SelectedTab.SetDocumentText(reader.ReadToEnd(), saveProperties, updateUi); - } - - break; - } - case "locmeta": - { - if (Provider.TryCreateReader(fullPath, out var archive)) - { - var metadata = new FTextLocalizationMetaDataResource(archive); - TabControl.SelectedTab.SetDocumentText(JsonConvert.SerializeObject(metadata, Formatting.Indented), saveProperties, updateUi); - } - - break; - } - case "locres": - { - if (Provider.TryCreateReader(fullPath, out var archive)) - { - var locres = new FTextLocalizationResource(archive); - TabControl.SelectedTab.SetDocumentText(JsonConvert.SerializeObject(locres, Formatting.Indented), saveProperties, updateUi); - } - - break; - } - case "bin" when fileName.Contains("AssetRegistry", StringComparison.OrdinalIgnoreCase): - { - if (Provider.TryCreateReader(fullPath, out var archive)) - { - var registry = new FAssetRegistryState(archive); - TabControl.SelectedTab.SetDocumentText(JsonConvert.SerializeObject(registry, Formatting.Indented), saveProperties, updateUi); - } - - break; - } - case "bnk": - case "pck": - { - if (Provider.TryCreateReader(fullPath, out var archive)) - { - var wwise = new WwiseReader(archive); - TabControl.SelectedTab.SetDocumentText(JsonConvert.SerializeObject(wwise, Formatting.Indented), saveProperties, updateUi); - foreach (var (name, data) in wwise.WwiseEncodedMedias) - { - SaveAndPlaySound(fullPath.SubstringBeforeWithLast("/") + name, "WEM", data); - } - } - - break; - } - case "wem": - { - if (Provider.TrySaveAsset(fullPath, out var input)) - SaveAndPlaySound(fullPath, "WEM", input); - - break; - } - case "udic": - { - if (Provider.TryCreateReader(fullPath, out var archive)) - { - var header = new FOodleDictionaryArchive(archive).Header; - TabControl.SelectedTab.SetDocumentText(JsonConvert.SerializeObject(header, Formatting.Indented), saveProperties, updateUi); - } - - break; - } - case "png": - case "jpg": - case "bmp": - { - if (Provider.TrySaveAsset(fullPath, out var data)) - { - using var stream = new MemoryStream(data) { Position = 0 }; - TabControl.SelectedTab.AddImage(fileName.SubstringBeforeLast("."), false, SKBitmap.Decode(stream), saveTextures, updateUi); - } - - break; - } - case "svg": - { - if (Provider.TrySaveAsset(fullPath, out var data)) - { - using var stream = new MemoryStream(data) { Position = 0 }; - var svg = new SkiaSharp.Extended.Svg.SKSvg(new SKSize(512, 512)); - svg.Load(stream); - - var bitmap = new SKBitmap(512, 512); - using (var canvas = new SKCanvas(bitmap)) - using (var paint = new SKPaint { IsAntialias = true, FilterQuality = SKFilterQuality.Medium }) - { - canvas.DrawPicture(svg.Picture, paint); - } - - TabControl.SelectedTab.AddImage(fileName.SubstringBeforeLast("."), false, bitmap, saveTextures, updateUi); - } - - break; - } - case "ufont": - case "otf": - case "ttf": - FLogger.Append(ELog.Warning, () => - FLogger.Text($"Export '{fileName}' raw data and change its extension if you want it to be an installable font file", Constants.WHITE, true)); - break; - case "ushaderbytecode": - case "ushadercode": - { - if (Provider.TryCreateReader(fullPath, out var archive)) - { - var ar = new FShaderCodeArchive(archive); - TabControl.SelectedTab.SetDocumentText(JsonConvert.SerializeObject(ar, Formatting.Indented), saveProperties, updateUi); - } - - break; - } - default: - { - FLogger.Append(ELog.Warning, () => - FLogger.Text($"The package '{fileName}' is of an unknown type.", Constants.WHITE, true)); - break; - } - } - } - - public void ExtractAndScroll(CancellationToken cancellationToken, string fullPath, string objectName, string parentExportType) - { - Log.Information("User CTRL-CLICKED to extract '{FullPath}'", fullPath); - TabControl.AddTab(fullPath.SubstringAfterLast('/'), fullPath.SubstringBeforeLast('/'), parentExportType); - TabControl.SelectedTab.ScrollTrigger = objectName; - - var exports = Provider.LoadAllObjects(fullPath); - TabControl.SelectedTab.Highlighter = AvalonExtensions.HighlighterSelector(""); // json - TabControl.SelectedTab.SetDocumentText(JsonConvert.SerializeObject(exports, Formatting.Indented), false, false); - - foreach (var e in exports) - { - if (CheckExport(cancellationToken, e)) - break; - } - } - - private bool CheckExport(CancellationToken cancellationToken, UObject export, EBulkType bulk = EBulkType.None) // return true once you wanna stop searching for exports - { - var isNone = bulk == EBulkType.None; - var updateUi = !HasFlag(bulk, EBulkType.Auto); - var saveTextures = HasFlag(bulk, EBulkType.Textures); - switch (export) - { - case UVerseDigest verseDigest when isNone: - { - if (!TabControl.CanAddTabs) return false; - - TabControl.AddTab($"{verseDigest.ProjectName}.verse"); - TabControl.SelectedTab.Highlighter = AvalonExtensions.HighlighterSelector("verse"); - TabControl.SelectedTab.SetDocumentText(verseDigest.ReadableCode, false, false); - return true; - } - case UTexture texture when isNone || saveTextures: - { - TabControl.SelectedTab.AddImage(texture, saveTextures, updateUi); - return false; - } - case UAkMediaAssetData when isNone: - case USoundWave when isNone: - { - var shouldDecompress = UserSettings.Default.CompressedAudioMode == ECompressedAudio.PlayDecompressed; - export.Decode(shouldDecompress, out var audioFormat, out var data); - var hasAf = !string.IsNullOrEmpty(audioFormat); - if (data == null || !hasAf || export.Owner == null) - { - if (hasAf) FLogger.Append(ELog.Warning, () => FLogger.Text($"Unsupported audio format '{audioFormat}'", Constants.WHITE, true)); - return false; - } - - SaveAndPlaySound(Path.Combine(TabControl.SelectedTab.Directory, TabControl.SelectedTab.Header.SubstringBeforeLast('.')).Replace('\\', '/'), audioFormat, data); - return false; - } - case UWorld when isNone && UserSettings.Default.PreviewWorlds: - case UBlueprintGeneratedClass when isNone && UserSettings.Default.PreviewWorlds && TabControl.SelectedTab.ParentExportType switch - { - "JunoBuildInstructionsItemDefinition" => true, - "JunoBuildingSetAccountItemDefinition" => true, - "JunoBuildingPropAccountItemDefinition" => true, - _ => false - }: - case UStaticMesh when isNone && UserSettings.Default.PreviewStaticMeshes: - case USkeletalMesh when isNone && UserSettings.Default.PreviewSkeletalMeshes: - case USkeleton when isNone && UserSettings.Default.SaveSkeletonAsMesh: - case UMaterialInstance when isNone && UserSettings.Default.PreviewMaterials && !ModelIsOverwritingMaterial && - !(Provider.InternalGameName.Equals("FortniteGame", StringComparison.OrdinalIgnoreCase) && export.Owner != null && - (export.Owner.Name.Contains("/MI_OfferImages/", StringComparison.OrdinalIgnoreCase) || - export.Owner.Name.EndsWith($"/RenderSwitch_Materials/{export.Name}", StringComparison.OrdinalIgnoreCase) || - export.Owner.Name.EndsWith($"/MI_BPTile/{export.Name}", StringComparison.OrdinalIgnoreCase))): - { - if (SnooperViewer.TryLoadExport(cancellationToken, export)) - SnooperViewer.Run(); - return true; - } - case UMaterialInstance m when isNone && ModelIsOverwritingMaterial: - { - SnooperViewer.Renderer.Swap(m); - SnooperViewer.Run(); - return true; - } - case UAnimSequence when isNone && ModelIsWaitingAnimation: - case UAnimMontage when isNone && ModelIsWaitingAnimation: - case UAnimComposite when isNone && ModelIsWaitingAnimation: - { - SnooperViewer.Renderer.Animate(export); - SnooperViewer.Run(); - return true; - } - case UStaticMesh when HasFlag(bulk, EBulkType.Meshes): - case USkeletalMesh when HasFlag(bulk, EBulkType.Meshes): - case USkeleton when UserSettings.Default.SaveSkeletonAsMesh && HasFlag(bulk, EBulkType.Meshes): - // case UMaterialInstance when HasFlag(bulk, EBulkType.Materials): // read the fucking json - case UAnimSequence when HasFlag(bulk, EBulkType.Animations): - case UAnimMontage when HasFlag(bulk, EBulkType.Animations): - case UAnimComposite when HasFlag(bulk, EBulkType.Animations): - { - SaveExport(export, HasFlag(bulk, EBulkType.Auto)); - return true; - } - default: - { - if (!isNone && !saveTextures) return false; - - using var package = new CreatorPackage(export, UserSettings.Default.CosmeticStyle); - if (!package.TryConstructCreator(out var creator)) - return false; - - creator.ParseForInfo(); - TabControl.SelectedTab.AddImage(export.Name, false, creator.Draw(), saveTextures, updateUi); - return true; - - } - } - } - - private void SaveAndPlaySound(string fullPath, string ext, byte[] data) - { - if (fullPath.StartsWith("/")) fullPath = fullPath[1..]; - var savedAudioPath = Path.Combine(UserSettings.Default.AudioDirectory, - UserSettings.Default.KeepDirectoryStructure ? fullPath : fullPath.SubstringAfterLast('/')).Replace('\\', '/') + $".{ext.ToLower()}"; - - if (!UserSettings.Default.IsAutoOpenSounds) - { - Directory.CreateDirectory(savedAudioPath.SubstringBeforeLast('/')); - using var stream = new FileStream(savedAudioPath, FileMode.Create, FileAccess.Write); - using var writer = new BinaryWriter(stream); - writer.Write(data); - writer.Flush(); - return; - } - - // TODO - // since we are currently in a thread, the audio player's lifetime (memory-wise) will keep the current thread up and running until fmodel itself closes - // the solution would be to kill the current thread at this line and then open the audio player without "Application.Current.Dispatcher.Invoke" - // but the ThreadWorkerViewModel is an idiot and doesn't understand we want to kill the current thread inside the current thread and continue the code - Application.Current.Dispatcher.Invoke(delegate - { - var audioPlayer = Helper.GetWindow("Audio Player", () => new AudioPlayer().Show()); - audioPlayer.Load(data, savedAudioPath); - }); - } - - private void SaveExport(UObject export, bool auto) - { - var toSave = new Exporter(export, UserSettings.Default.ExportOptions); - - string dir; - if (!auto) - { - var folderBrowser = new VistaFolderBrowserDialog(); - if (folderBrowser.ShowDialog() == true) - dir = folderBrowser.SelectedPath; - else return; - } - else dir = UserSettings.Default.ModelDirectory; - - var toSaveDirectory = new DirectoryInfo(dir); - if (toSave.TryWriteToDir(toSaveDirectory, out var label, out var savedFilePath)) - { - Log.Information("Successfully saved {FilePath}", savedFilePath); - FLogger.Append(ELog.Information, () => - { - FLogger.Text("Successfully saved ", Constants.WHITE); - FLogger.Link(label, savedFilePath, true); - }); - } - else - { - Log.Error("{FileName} could not be saved", export.Name); - FLogger.Append(ELog.Error, () => FLogger.Text($"Could not save '{export.Name}'", Constants.WHITE, true)); - } - } - - private readonly object _rawData = new (); - public void ExportData(string fullPath, bool updateUi = true) - { - var fileName = fullPath.SubstringAfterLast('/'); - if (Provider.TrySavePackage(fullPath, out var assets)) - { - string path = UserSettings.Default.RawDataDirectory; - Parallel.ForEach(assets, kvp => - { - lock (_rawData) - { - path = Path.Combine(UserSettings.Default.RawDataDirectory, UserSettings.Default.KeepDirectoryStructure ? kvp.Key : kvp.Key.SubstringAfterLast('/')).Replace('\\', '/'); - Directory.CreateDirectory(path.SubstringBeforeLast('/')); - File.WriteAllBytes(path, kvp.Value); - } - }); - - Log.Information("{FileName} successfully exported", fileName); - if (updateUi) - { - FLogger.Append(ELog.Information, () => - { - FLogger.Text("Successfully exported ", Constants.WHITE); - FLogger.Link(fileName, path, true); - }); - } - } - else - { - Log.Error("{FileName} could not be exported", fileName); - if (updateUi) - FLogger.Append(ELog.Error, () => FLogger.Text($"Could not export '{fileName}'", Constants.WHITE, true)); - } - } - - private static bool HasFlag(EBulkType a, EBulkType b) - { - return (a & b) == b; - } -} +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net.Http.Headers; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using System.Windows; +using AdonisUI.Controls; +using CUE4Parse.Compression; +using CUE4Parse.Encryption.Aes; +using CUE4Parse.FileProvider; +using CUE4Parse.FileProvider.Vfs; +using CUE4Parse.MappingsProvider; +using CUE4Parse.UE4.AssetRegistry; +using CUE4Parse.UE4.Assets.Exports; +using CUE4Parse.UE4.Assets.Exports.Animation; +using CUE4Parse.UE4.Assets.Exports.Material; +using CUE4Parse.UE4.Assets.Exports.SkeletalMesh; +using CUE4Parse.UE4.Assets.Exports.Sound; +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.IO; +using CUE4Parse.UE4.Localization; +using CUE4Parse.UE4.Objects.Core.Serialization; +using CUE4Parse.UE4.Objects.Engine; +using CUE4Parse.UE4.Oodle.Objects; +using CUE4Parse.UE4.Readers; +using CUE4Parse.UE4.Shaders; +using CUE4Parse.UE4.Versions; +using CUE4Parse.UE4.Wwise; +using CUE4Parse_Conversion; +using CUE4Parse_Conversion.Sounds; +using CUE4Parse.GameTypes.UDWN.Encryption.Aes; +using CUE4Parse.GameTypes.DBD.Encryption.Aes; +using CUE4Parse.GameTypes.DreamStar.Encryption.Aes; +using CUE4Parse.GameTypes.PAXDEI.Encryption.Aes; +using CUE4Parse.GameTypes.NetEase.MAR.Encryption.Aes; +using CUE4Parse.GameTypes.FSR.Encryption.Aes; +using EpicManifestParser; +using FModel.Creator; +using FModel.Extensions; +using FModel.Framework; +using FModel.Services; +using FModel.Settings; +using FModel.Views; +using FModel.Views.Resources.Controls; +using FModel.Views.Snooper; +using Newtonsoft.Json; +using Ookii.Dialogs.Wpf; +using OpenTK.Windowing.Common; +using OpenTK.Windowing.Desktop; +using Serilog; +using SkiaSharp; +using UE4Config.Parsing; +using Application = System.Windows.Application; + +namespace FModel.ViewModels; + +public class CUE4ParseViewModel : ViewModel +{ + private ThreadWorkerViewModel _threadWorkerView => ApplicationService.ThreadWorkerView; + private ApiEndpointViewModel _apiEndpointView => ApplicationService.ApiEndpointView; + 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 _fnLive = new(@"^FortniteGame(/|\\)Content(/|\\)Paks(/|\\)", + RegexOptions.Compiled | RegexOptions.Singleline | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); + + private string _internalGameName; + public string InternalGameName + { + get => _internalGameName; + set => SetProperty(ref _internalGameName, value); + } + + private bool _modelIsOverwritingMaterial; + public bool ModelIsOverwritingMaterial + { + get => _modelIsOverwritingMaterial; + set => SetProperty(ref _modelIsOverwritingMaterial, value); + } + + private bool _modelIsWaitingAnimation; + public bool ModelIsWaitingAnimation + { + get => _modelIsWaitingAnimation; + set => SetProperty(ref _modelIsWaitingAnimation, value); + } + + public bool IsSnooperOpen => _snooper is { Exists: true, IsVisible: true }; + private Snooper _snooper; + public Snooper SnooperViewer + { + get + { + if (_snooper != null) return _snooper; + + return Application.Current.Dispatcher.Invoke(delegate + { + var scale = ImGuiController.GetDpiScale(); + var htz = Snooper.GetMaxRefreshFrequency(); + return _snooper = new Snooper( + new GameWindowSettings { UpdateFrequency = htz }, + new NativeWindowSettings + { + ClientSize = new OpenTK.Mathematics.Vector2i( + Convert.ToInt32(SystemParameters.MaximizedPrimaryScreenWidth * .75 * scale), + Convert.ToInt32(SystemParameters.MaximizedPrimaryScreenHeight * .85 * scale)), + NumberOfSamples = Constants.SAMPLES_COUNT, + WindowBorder = WindowBorder.Resizable, + Flags = ContextFlags.ForwardCompatible, + Profile = ContextProfile.Core, + Vsync = VSyncMode.Adaptive, + APIVersion = new Version(4, 6), + StartVisible = false, + StartFocused = false, + Title = "3D Viewer" + }); + }); + } + } + + public AbstractVfsFileProvider Provider { get; } + public GameDirectoryViewModel GameDirectory { get; } + public AssetsFolderViewModel AssetsFolder { get; } + public SearchViewModel SearchVm { get; } + public TabControlViewModel TabControl { get; } + public ConfigIni IoStoreOnDemand { get; } + + public CUE4ParseViewModel() + { + var currentDir = UserSettings.Default.CurrentDir; + var gameDirectory = currentDir.GameDirectory; + var versionContainer = new VersionContainer( + game: currentDir.UeVersion, platform: currentDir.TexturePlatform, + customVersions: new FCustomVersionContainer(currentDir.Versioning.CustomVersions), + optionOverrides: currentDir.Versioning.Options, + mapStructTypesOverrides: currentDir.Versioning.MapStructTypes); + + switch (gameDirectory) + { + case Constants._FN_LIVE_TRIGGER: + { + InternalGameName = "FortniteGame"; + Provider = new StreamedFileProvider("FortniteLive", true, versionContainer); + break; + } + case Constants._VAL_LIVE_TRIGGER: + { + InternalGameName = "ShooterGame"; + Provider = new StreamedFileProvider("ValorantLive", true, versionContainer); + break; + } + default: + { + InternalGameName = gameDirectory.SubstringBeforeLast(gameDirectory.Contains("eFootball") ? "\\pak" : "\\Content").SubstringAfterLast("\\"); + Provider = InternalGameName switch + { + "StateOfDecay2" => new DefaultFileProvider(new DirectoryInfo(gameDirectory), + new DirectoryInfo[] + { + new(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + "\\StateOfDecay2\\Saved\\Paks"), + new(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + "\\StateOfDecay2\\Saved\\DisabledPaks") + }, SearchOption.AllDirectories, true, versionContainer), + "eFootball" => new DefaultFileProvider(new DirectoryInfo(gameDirectory), + new DirectoryInfo[] + { + new(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData) + "\\KONAMI\\eFootball\\ST\\Download") + }, SearchOption.AllDirectories, true, versionContainer), + _ => new DefaultFileProvider(gameDirectory, SearchOption.AllDirectories, true, versionContainer) + }; + + break; + } + } + Provider.ReadScriptData = UserSettings.Default.ReadScriptData; + Provider.CustomEncryption = Provider.Versions.Game switch + { + EGame.GAME_MarvelRivals => MarvelAes.MarvelDecrypt, + EGame.GAME_Undawn => ToaaAes.ToaaDecrypt, + EGame.GAME_DeadByDaylight => DBDAes.DbDDecrypt, + EGame.GAME_PaxDei => PaxDeiAes.PaxDeiDecrypt, + EGame.GAME_3on3FreeStyleRebound => FreeStyleReboundAes.FSRDecrypt, + EGame.GAME_DreamStar => DreamStarAes.DreamStarDecrypt, + _ => Provider.CustomEncryption + }; + + GameDirectory = new GameDirectoryViewModel(); + AssetsFolder = new AssetsFolderViewModel(); + SearchVm = new SearchViewModel(); + TabControl = new TabControlViewModel(); + IoStoreOnDemand = new ConfigIni(nameof(IoStoreOnDemand)); + } + + public async Task Initialize() + { + await _threadWorkerView.Begin(cancellationToken => + { + switch (Provider) + { + case StreamedFileProvider p: + switch (p.LiveGame) + { + case "FortniteLive": + { + var manifestInfo = _apiEndpointView.EpicApi.GetManifest(cancellationToken); + if (manifestInfo is null) + { + 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, + ChunkBaseUrl = "http://epicgames-download1.akamaized.net/Builds/Fortnite/CloudDir/", + Zlibng = ZlibHelper.Instance + }; + + var startTs = Stopwatch.GetTimestamp(); + var (manifest, _) = manifestInfo.DownloadAndParseAsync(manifestOptions, + cancellationToken: cancellationToken).GetAwaiter().GetResult(); + var parseTime = Stopwatch.GetElapsedTime(startTs); + const bool cacheChunksAsIs = false; + + foreach (var fileManifest in manifest.FileManifestList) + { + if (fileManifest.FileName.Equals("Cloud/IoStoreOnDemand.ini", StringComparison.OrdinalIgnoreCase)) + { + IoStoreOnDemand.Read(new StreamReader(fileManifest.GetStream(cacheChunksAsIs))); + continue; + } + if (!_fnLive.IsMatch(fileManifest.FileName)) continue; + + p.RegisterVfs(fileManifest.FileName, [fileManifest.GetStream(cacheChunksAsIs)] + , it => new FStreamArchive(it, manifest.FileManifestList.First(x => x.FileName.Equals(it)).GetStream(cacheChunksAsIs), p.Versions)); + } + + FLogger.Append(ELog.Information, () => + FLogger.Text($"Fortnite [LIVE] has been loaded successfully in {parseTime.TotalMilliseconds}ms", Constants.WHITE, true)); + break; + } + case "ValorantLive": + { + var manifestInfo = _apiEndpointView.ValorantApi.GetManifest(cancellationToken); + if (manifestInfo == null) + { + throw new Exception("Could not load latest Valorant manifest, you may have to switch to your local installation."); + } + + for (var i = 0; i < manifestInfo.Paks.Length; i++) + { + p.RegisterVfs(manifestInfo.Paks[i].GetFullName(), [manifestInfo.GetPakStream(i)]); + } + + FLogger.Append(ELog.Information, () => + FLogger.Text($"Valorant '{manifestInfo.Header.GameVersion}' has been loaded successfully", Constants.WHITE, true)); + break; + } + } + + break; + case DefaultFileProvider: + { + var ioStoreOnDemandPath = Path.Combine(UserSettings.Default.GameDirectory, "..\\..\\..\\Cloud\\IoStoreOnDemand.ini"); + if (File.Exists(ioStoreOnDemandPath)) + { + using var s = new StreamReader(ioStoreOnDemandPath); + IoStoreOnDemand.Read(s); + } + break; + } + } + + Provider.Initialize(); + Log.Information($"{Provider.Versions.Game} ({Provider.Versions.Platform}) | Archives: x{Provider.UnloadedVfs.Count} | AES: x{Provider.RequiredKeys.Count}"); + + foreach (var vfs in Provider.UnloadedVfs) // push files from the provider to the ui + { + cancellationToken.ThrowIfCancellationRequested(); + if (!_hiddenArchives.IsMatch(vfs.Name)) continue; + + GameDirectory.Add(vfs); + } + }); + } + + /// + /// load virtual files system from GameDirectory + /// + /// + public void LoadVfs(CancellationToken token, IEnumerable aesKeys) + { + GameDirectory.DeactivateAll(); + + // load files using UnloadedVfs to include non-encrypted vfs + foreach (var key in aesKeys) + { + token.ThrowIfCancellationRequested(); // cancel if needed + + var k = key.Key.Trim(); + if (k.Length != 66) k = Constants.ZERO_64_CHAR; + Provider.SubmitKey(key.Guid, new FAesKey(k)); + } + Provider.PostMount(); + + // files in MountedVfs will be enabled + foreach (var file in GameDirectory.DirectoryFiles) + { + token.ThrowIfCancellationRequested(); + if (Provider.MountedVfs.FirstOrDefault(x => x.Name == file.Name) is not { } vfs) + { + if (Provider.UnloadedVfs.FirstOrDefault(x => x.Name == file.Name) is IoStoreReader store) + file.FileCount = (int) store.TocResource.Header.TocEntryCount - 1; + + continue; + } + + file.IsEnabled = true; + file.MountPoint = vfs.MountPoint; + file.FileCount = vfs.FileCount; + } + + InternalGameName = Provider.InternalGameName; + + var aesMax = Provider.RequiredKeys.Count + Provider.Keys.Count; + var archiveMax = Provider.UnloadedVfs.Count + Provider.MountedVfs.Count; + Log.Information($"Project: {InternalGameName} | Mounted: {Provider.MountedVfs.Count}/{archiveMax} | AES: {Provider.Keys.Count}/{aesMax}"); + } + + public void ClearProvider() + { + if (Provider == null) return; + + AssetsFolder.Folders.Clear(); + SearchVm.SearchResults.Clear(); + Helper.CloseWindow("Search View"); + Provider.UnloadNonStreamedVfs(); + GC.Collect(); + } + + public async Task RefreshAes() + { + // game directory dependent, we don't have the provider game name yet since we don't have aes keys + // except when this comes from the AES Manager + if (!UserSettings.IsEndpointValid(EEndpointType.Aes, out var endpoint)) + return; + + await _threadWorkerView.Begin(cancellationToken => + { + var aes = _apiEndpointView.DynamicApi.GetAesKeys(cancellationToken, endpoint.Url, endpoint.Path); + if (aes is not { IsValid: true }) return; + + UserSettings.Default.CurrentDir.AesKeys = aes; + }); + } + + public async Task InitInformation() + { + await _threadWorkerView.Begin(cancellationToken => + { + var info = _apiEndpointView.FModelApi.GetNews(cancellationToken, Provider.InternalGameName); + if (info == null) return; + + FLogger.Append(ELog.None, () => + { + for (var i = 0; i < info.Messages.Length; i++) + { + FLogger.Text(info.Messages[i], info.Colors[i], bool.Parse(info.NewLines[i])); + } + }); + }); + } + + public Task InitMappings(bool force = false) + { + if (!UserSettings.IsEndpointValid(EEndpointType.Mapping, out var endpoint)) + { + Provider.MappingsContainer = null; + return Task.CompletedTask; + } + + return Task.Run(() => + { + var l = ELog.Information; + if (endpoint.Overwrite && File.Exists(endpoint.FilePath)) + { + Provider.MappingsContainer = new FileUsmapTypeMappingsProvider(endpoint.FilePath); + } + else if (endpoint.IsValid) + { + var mappingsFolder = Path.Combine(UserSettings.Default.OutputDirectory, ".data"); + if (endpoint.Path == "$.[?(@.meta.compressionMethod=='Oodle')].['url','fileName']") endpoint.Path = "$.[0].['url','fileName']"; + var mappings = _apiEndpointView.DynamicApi.GetMappings(default, endpoint.Url, endpoint.Path); + if (mappings is { Length: > 0 }) + { + foreach (var mapping in mappings) + { + if (!mapping.IsValid) continue; + + var mappingPath = Path.Combine(mappingsFolder, mapping.FileName); + if (force || !File.Exists(mappingPath)) + { + _apiEndpointView.DownloadFile(mapping.Url, mappingPath); + } + + Provider.MappingsContainer = new FileUsmapTypeMappingsProvider(mappingPath); + break; + } + } + + if (Provider.MappingsContainer == null) + { + var latestUsmaps = new DirectoryInfo(mappingsFolder).GetFiles("*_oo.usmap"); + if (latestUsmaps.Length <= 0) return; + + var latestUsmapInfo = latestUsmaps.OrderBy(f => f.LastWriteTime).Last(); + Provider.MappingsContainer = new FileUsmapTypeMappingsProvider(latestUsmapInfo.FullName); + l = ELog.Warning; + } + } + + if (Provider.MappingsContainer is FileUsmapTypeMappingsProvider m) + { + Log.Information($"Mappings pulled from '{m.FileName}'"); + FLogger.Append(l, () => FLogger.Text($"Mappings pulled from '{m.FileName}'", Constants.WHITE, true)); + } + }); + } + + public Task VerifyConsoleVariables() + { + if (Provider.Versions["StripAdditiveRefPose"]) + { + FLogger.Append(ELog.Warning, () => + FLogger.Text("Additive animations have their reference pose stripped, which will lead to inaccurate preview and export", Constants.WHITE, true)); + } + + return Task.CompletedTask; + } + + public Task VerifyOnDemandArchives() + { + // only local fortnite + if (Provider is not DefaultFileProvider || !Provider.InternalGameName.Equals("FortniteGame", StringComparison.OrdinalIgnoreCase)) + return Task.CompletedTask; + + // scuffed but working + var persistentDownloadDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "FortniteGame/Saved/PersistentDownloadDir"); + var iasFileInfo = new FileInfo(Path.Combine(persistentDownloadDir, "ias", "ias.cache.0")); + if (!iasFileInfo.Exists || iasFileInfo.Length == 0) + return Task.CompletedTask; + + return Task.Run(async () => + { + var inst = new List(); + IoStoreOnDemand.FindPropertyInstructions("Endpoint", "TocPath", inst); + if (inst.Count <= 0) return; + + var ioStoreOnDemandPath = Path.Combine(UserSettings.Default.GameDirectory, "..\\..\\..\\Cloud", inst[0].Value.SubstringAfterLast("/").SubstringBefore("\"")); + if (!File.Exists(ioStoreOnDemandPath)) return; + + await _apiEndpointView.EpicApi.VerifyAuth(default); + await Provider.RegisterVfs(new IoChunkToc(ioStoreOnDemandPath), new IoStoreOnDemandOptions + { + ChunkBaseUri = new Uri("https://download.epicgames.com/ias/fortnite/", UriKind.Absolute), + ChunkCacheDirectory = Directory.CreateDirectory(Path.Combine(UserSettings.Default.OutputDirectory, ".data")), + Authorization = new AuthenticationHeaderValue("Bearer", UserSettings.Default.LastAuthResponse.AccessToken), + Timeout = TimeSpan.FromSeconds(30) + }); + var onDemandCount = await Provider.MountAsync(); + FLogger.Append(ELog.Information, () => + FLogger.Text($"{onDemandCount} on-demand archive{(onDemandCount > 1 ? "s" : "")} streamed via epicgames.com", Constants.WHITE, true)); + }); + } + + public int LocalizedResourcesCount { get; set; } + public bool LocalResourcesDone { get; set; } + public bool HotfixedResourcesDone { get; set; } + public async Task LoadLocalizedResources() + { + var snapshot = LocalizedResourcesCount; + await Task.WhenAll(LoadGameLocalizedResources(), LoadHotfixedLocalizedResources()).ConfigureAwait(false); + if (snapshot != LocalizedResourcesCount) + { + FLogger.Append(ELog.Information, () => + FLogger.Text($"{LocalizedResourcesCount} localized resources loaded for '{UserSettings.Default.AssetLanguage.GetDescription()}'", Constants.WHITE, true)); + Utils.Typefaces = new Typefaces(this); + } + } + private Task LoadGameLocalizedResources() + { + if (LocalResourcesDone) return Task.CompletedTask; + return Task.Run(() => + { + LocalizedResourcesCount += Provider.LoadLocalization(UserSettings.Default.AssetLanguage); + LocalResourcesDone = true; + }); + } + private Task LoadHotfixedLocalizedResources() + { + if (!Provider.InternalGameName.Equals("fortnitegame", StringComparison.OrdinalIgnoreCase) || HotfixedResourcesDone) return Task.CompletedTask; + return Task.Run(() => + { + var hotfixes = ApplicationService.ApiEndpointView.CentralApi.GetHotfixes(default, Provider.GetLanguageCode(UserSettings.Default.AssetLanguage)); + if (hotfixes == null) return; + + HotfixedResourcesDone = true; + foreach (var entries in hotfixes) + { + if (!Provider.LocalizedResources.ContainsKey(entries.Key)) + Provider.LocalizedResources[entries.Key] = new Dictionary(); + + foreach (var keyValue in entries.Value) + { + Provider.LocalizedResources[entries.Key][keyValue.Key] = keyValue.Value; + LocalizedResourcesCount++; + } + } + }); + } + + private int _virtualPathCount { get; set; } + public Task LoadVirtualPaths() + { + if (_virtualPathCount > 0) return Task.CompletedTask; + return Task.Run(() => + { + _virtualPathCount = Provider.LoadVirtualPaths(UserSettings.Default.CurrentDir.UeVersion.GetVersion()); + if (_virtualPathCount > 0) + { + FLogger.Append(ELog.Information, () => + FLogger.Text($"{_virtualPathCount} virtual paths loaded", Constants.WHITE, true)); + } + else + { + FLogger.Append(ELog.Warning, () => + FLogger.Text("Could not load virtual paths, plugin manifest may not exist", Constants.WHITE, true)); + } + }); + } + + public void ExtractSelected(CancellationToken cancellationToken, IEnumerable assetItems) + { + foreach (var asset in assetItems) + { + Thread.Yield(); + cancellationToken.ThrowIfCancellationRequested(); + Extract(cancellationToken, asset.FullPath, TabControl.HasNoTabs); + } + } + + private void BulkFolder(CancellationToken cancellationToken, TreeItem folder, Action action) + { + foreach (var asset in folder.AssetsList.Assets) + { + Thread.Yield(); + cancellationToken.ThrowIfCancellationRequested(); + try + { + action(asset); + } + catch + { + // ignore + } + } + + foreach (var f in folder.Folders) BulkFolder(cancellationToken, f, action); + } + + public void ExportFolder(CancellationToken cancellationToken, TreeItem folder) + { + Parallel.ForEach(folder.AssetsList.Assets, asset => + { + cancellationToken.ThrowIfCancellationRequested(); + ExportData(asset.FullPath, false); + }); + + foreach (var f in folder.Folders) ExportFolder(cancellationToken, f); + } + + public void ExtractFolder(CancellationToken cancellationToken, TreeItem folder) + => BulkFolder(cancellationToken, folder, asset => Extract(cancellationToken, asset.FullPath, TabControl.HasNoTabs)); + + public void SaveFolder(CancellationToken cancellationToken, TreeItem folder) + => BulkFolder(cancellationToken, folder, asset => Extract(cancellationToken, asset.FullPath, TabControl.HasNoTabs, EBulkType.Properties | EBulkType.Auto)); + + public void TextureFolder(CancellationToken cancellationToken, TreeItem folder) + => BulkFolder(cancellationToken, folder, asset => Extract(cancellationToken, asset.FullPath, TabControl.HasNoTabs, EBulkType.Textures | EBulkType.Auto)); + + public void ModelFolder(CancellationToken cancellationToken, TreeItem folder) + => BulkFolder(cancellationToken, folder, asset => Extract(cancellationToken, asset.FullPath, TabControl.HasNoTabs, EBulkType.Meshes | EBulkType.Auto)); + + public void AnimationFolder(CancellationToken cancellationToken, TreeItem folder) + => BulkFolder(cancellationToken, folder, asset => Extract(cancellationToken, asset.FullPath, TabControl.HasNoTabs, EBulkType.Animations | EBulkType.Auto)); + + public void Extract(CancellationToken cancellationToken, string fullPath, bool addNewTab = false, EBulkType bulk = EBulkType.None) + { + Log.Information("User DOUBLE-CLICKED to extract '{FullPath}'", fullPath); + + var directory = fullPath.SubstringBeforeLast('/'); + var fileName = fullPath.SubstringAfterLast('/'); + var ext = fullPath.SubstringAfterLast('.').ToLower(); + + if (addNewTab && TabControl.CanAddTabs) TabControl.AddTab(fileName, directory); + else TabControl.SelectedTab.SoftReset(fileName, directory); + TabControl.SelectedTab.Highlighter = AvalonExtensions.HighlighterSelector(ext); + + var updateUi = !HasFlag(bulk, EBulkType.Auto); + var saveProperties = HasFlag(bulk, EBulkType.Properties); + var saveTextures = HasFlag(bulk, EBulkType.Textures); + switch (ext) + { + case "uasset": + case "umap": + { + var exports = Provider.LoadAllObjects(fullPath); + TabControl.SelectedTab.SetDocumentText(JsonConvert.SerializeObject(exports, Formatting.Indented), saveProperties, updateUi); + if (HasFlag(bulk, EBulkType.Properties)) break; // do not search for viewable exports if we are dealing with jsons + + foreach (var e in exports) + { + if (CheckExport(cancellationToken, e, bulk)) + break; + } + + break; + } + case "upluginmanifest": + case "uproject": + case "manifest": + case "uplugin": + case "archive": + case "vmodule": + case "verse": + case "html": + case "json": + case "ini": + case "txt": + case "log": + case "bat": + case "dat": + case "cfg": + case "ide": + case "ipl": + case "zon": + case "xml": + case "css": + case "csv": + case "pem": + case "tps": + case "lua": + case "js": + case "po": + case "h": + { + if (Provider.TrySaveAsset(fullPath, out var data)) + { + using var stream = new MemoryStream(data) { Position = 0 }; + using var reader = new StreamReader(stream); + + TabControl.SelectedTab.SetDocumentText(reader.ReadToEnd(), saveProperties, updateUi); + } + + break; + } + case "locmeta": + { + if (Provider.TryCreateReader(fullPath, out var archive)) + { + var metadata = new FTextLocalizationMetaDataResource(archive); + TabControl.SelectedTab.SetDocumentText(JsonConvert.SerializeObject(metadata, Formatting.Indented), saveProperties, updateUi); + } + + break; + } + case "locres": + { + if (Provider.TryCreateReader(fullPath, out var archive)) + { + var locres = new FTextLocalizationResource(archive); + TabControl.SelectedTab.SetDocumentText(JsonConvert.SerializeObject(locres, Formatting.Indented), saveProperties, updateUi); + } + + break; + } + case "bin" when fileName.Contains("AssetRegistry", StringComparison.OrdinalIgnoreCase): + { + if (Provider.TryCreateReader(fullPath, out var archive)) + { + var registry = new FAssetRegistryState(archive); + TabControl.SelectedTab.SetDocumentText(JsonConvert.SerializeObject(registry, Formatting.Indented), saveProperties, updateUi); + } + + break; + } + case "bnk": + case "pck": + { + if (Provider.TryCreateReader(fullPath, out var archive)) + { + var wwise = new WwiseReader(archive); + TabControl.SelectedTab.SetDocumentText(JsonConvert.SerializeObject(wwise, Formatting.Indented), saveProperties, updateUi); + foreach (var (name, data) in wwise.WwiseEncodedMedias) + { + SaveAndPlaySound(fullPath.SubstringBeforeWithLast("/") + name, "WEM", data); + } + } + + break; + } + case "wem": + { + if (Provider.TrySaveAsset(fullPath, out var input)) + SaveAndPlaySound(fullPath, "WEM", input); + + break; + } + case "udic": + { + if (Provider.TryCreateReader(fullPath, out var archive)) + { + var header = new FOodleDictionaryArchive(archive).Header; + TabControl.SelectedTab.SetDocumentText(JsonConvert.SerializeObject(header, Formatting.Indented), saveProperties, updateUi); + } + + break; + } + case "png": + case "jpg": + case "bmp": + { + if (Provider.TrySaveAsset(fullPath, out var data)) + { + using var stream = new MemoryStream(data) { Position = 0 }; + TabControl.SelectedTab.AddImage(fileName.SubstringBeforeLast("."), false, SKBitmap.Decode(stream), saveTextures, updateUi); + } + + break; + } + case "svg": + { + if (Provider.TrySaveAsset(fullPath, out var data)) + { + using var stream = new MemoryStream(data) { Position = 0 }; + var svg = new SkiaSharp.Extended.Svg.SKSvg(new SKSize(512, 512)); + svg.Load(stream); + + var bitmap = new SKBitmap(512, 512); + using (var canvas = new SKCanvas(bitmap)) + using (var paint = new SKPaint { IsAntialias = true, FilterQuality = SKFilterQuality.Medium }) + { + canvas.DrawPicture(svg.Picture, paint); + } + + TabControl.SelectedTab.AddImage(fileName.SubstringBeforeLast("."), false, bitmap, saveTextures, updateUi); + } + + break; + } + case "ufont": + case "otf": + case "ttf": + FLogger.Append(ELog.Warning, () => + FLogger.Text($"Export '{fileName}' raw data and change its extension if you want it to be an installable font file", Constants.WHITE, true)); + break; + case "ushaderbytecode": + case "ushadercode": + { + if (Provider.TryCreateReader(fullPath, out var archive)) + { + var ar = new FShaderCodeArchive(archive); + TabControl.SelectedTab.SetDocumentText(JsonConvert.SerializeObject(ar, Formatting.Indented), saveProperties, updateUi); + } + + break; + } + default: + { + FLogger.Append(ELog.Warning, () => + FLogger.Text($"The package '{fileName}' is of an unknown type.", Constants.WHITE, true)); + break; + } + } + } + + public void ExtractAndScroll(CancellationToken cancellationToken, string fullPath, string objectName, string parentExportType) + { + Log.Information("User CTRL-CLICKED to extract '{FullPath}'", fullPath); + TabControl.AddTab(fullPath.SubstringAfterLast('/'), fullPath.SubstringBeforeLast('/'), parentExportType); + TabControl.SelectedTab.ScrollTrigger = objectName; + + var exports = Provider.LoadAllObjects(fullPath); + TabControl.SelectedTab.Highlighter = AvalonExtensions.HighlighterSelector(""); // json + TabControl.SelectedTab.SetDocumentText(JsonConvert.SerializeObject(exports, Formatting.Indented), false, false); + + foreach (var e in exports) + { + if (CheckExport(cancellationToken, e)) + break; + } + } + + private bool CheckExport(CancellationToken cancellationToken, UObject export, EBulkType bulk = EBulkType.None) // return true once you wanna stop searching for exports + { + var isNone = bulk == EBulkType.None; + var updateUi = !HasFlag(bulk, EBulkType.Auto); + var saveTextures = HasFlag(bulk, EBulkType.Textures); + switch (export) + { + case UVerseDigest verseDigest when isNone: + { + if (!TabControl.CanAddTabs) return false; + + TabControl.AddTab($"{verseDigest.ProjectName}.verse"); + TabControl.SelectedTab.Highlighter = AvalonExtensions.HighlighterSelector("verse"); + TabControl.SelectedTab.SetDocumentText(verseDigest.ReadableCode, false, false); + return true; + } + case UTexture texture when isNone || saveTextures: + { + TabControl.SelectedTab.AddImage(texture, saveTextures, updateUi); + return false; + } + case UAkMediaAssetData when isNone: + case USoundWave when isNone: + { + var shouldDecompress = UserSettings.Default.CompressedAudioMode == ECompressedAudio.PlayDecompressed; + export.Decode(shouldDecompress, out var audioFormat, out var data); + var hasAf = !string.IsNullOrEmpty(audioFormat); + if (data == null || !hasAf || export.Owner == null) + { + if (hasAf) FLogger.Append(ELog.Warning, () => FLogger.Text($"Unsupported audio format '{audioFormat}'", Constants.WHITE, true)); + return false; + } + + SaveAndPlaySound(Path.Combine(TabControl.SelectedTab.Directory, TabControl.SelectedTab.Header.SubstringBeforeLast('.')).Replace('\\', '/'), audioFormat, data); + return false; + } + case UWorld when isNone && UserSettings.Default.PreviewWorlds: + case UBlueprintGeneratedClass when isNone && UserSettings.Default.PreviewWorlds && TabControl.SelectedTab.ParentExportType switch + { + "JunoBuildInstructionsItemDefinition" => true, + "JunoBuildingSetAccountItemDefinition" => true, + "JunoBuildingPropAccountItemDefinition" => true, + _ => false + }: + case UStaticMesh when isNone && UserSettings.Default.PreviewStaticMeshes: + case USkeletalMesh when isNone && UserSettings.Default.PreviewSkeletalMeshes: + case USkeleton when isNone && UserSettings.Default.SaveSkeletonAsMesh: + case UMaterialInstance when isNone && UserSettings.Default.PreviewMaterials && !ModelIsOverwritingMaterial && + !(Provider.InternalGameName.Equals("FortniteGame", StringComparison.OrdinalIgnoreCase) && export.Owner != null && + (export.Owner.Name.Contains("/MI_OfferImages/", StringComparison.OrdinalIgnoreCase) || + export.Owner.Name.EndsWith($"/RenderSwitch_Materials/{export.Name}", StringComparison.OrdinalIgnoreCase) || + export.Owner.Name.EndsWith($"/MI_BPTile/{export.Name}", StringComparison.OrdinalIgnoreCase))): + { + if (SnooperViewer.TryLoadExport(cancellationToken, export)) + SnooperViewer.Run(); + return true; + } + case UMaterialInstance m when isNone && ModelIsOverwritingMaterial: + { + SnooperViewer.Renderer.Swap(m); + SnooperViewer.Run(); + return true; + } + case UAnimSequence when isNone && ModelIsWaitingAnimation: + case UAnimMontage when isNone && ModelIsWaitingAnimation: + case UAnimComposite when isNone && ModelIsWaitingAnimation: + { + SnooperViewer.Renderer.Animate(export); + SnooperViewer.Run(); + return true; + } + case UStaticMesh when HasFlag(bulk, EBulkType.Meshes): + case USkeletalMesh when HasFlag(bulk, EBulkType.Meshes): + case USkeleton when UserSettings.Default.SaveSkeletonAsMesh && HasFlag(bulk, EBulkType.Meshes): + // case UMaterialInstance when HasFlag(bulk, EBulkType.Materials): // read the fucking json + case UAnimSequence when HasFlag(bulk, EBulkType.Animations): + case UAnimMontage when HasFlag(bulk, EBulkType.Animations): + case UAnimComposite when HasFlag(bulk, EBulkType.Animations): + { + SaveExport(export, HasFlag(bulk, EBulkType.Auto)); + return true; + } + default: + { + if (!isNone && !saveTextures) return false; + + using var package = new CreatorPackage(export, UserSettings.Default.CosmeticStyle); + if (!package.TryConstructCreator(out var creator)) + return false; + + creator.ParseForInfo(); + TabControl.SelectedTab.AddImage(export.Name, false, creator.Draw(), saveTextures, updateUi); + return true; + + } + } + } + + private void SaveAndPlaySound(string fullPath, string ext, byte[] data) + { + if (fullPath.StartsWith("/")) fullPath = fullPath[1..]; + var savedAudioPath = Path.Combine(UserSettings.Default.AudioDirectory, + UserSettings.Default.KeepDirectoryStructure ? fullPath : fullPath.SubstringAfterLast('/')).Replace('\\', '/') + $".{ext.ToLower()}"; + + if (!UserSettings.Default.IsAutoOpenSounds) + { + Directory.CreateDirectory(savedAudioPath.SubstringBeforeLast('/')); + using var stream = new FileStream(savedAudioPath, FileMode.Create, FileAccess.Write); + using var writer = new BinaryWriter(stream); + writer.Write(data); + writer.Flush(); + return; + } + + // TODO + // since we are currently in a thread, the audio player's lifetime (memory-wise) will keep the current thread up and running until fmodel itself closes + // the solution would be to kill the current thread at this line and then open the audio player without "Application.Current.Dispatcher.Invoke" + // but the ThreadWorkerViewModel is an idiot and doesn't understand we want to kill the current thread inside the current thread and continue the code + Application.Current.Dispatcher.Invoke(delegate + { + var audioPlayer = Helper.GetWindow("Audio Player", () => new AudioPlayer().Show()); + audioPlayer.Load(data, savedAudioPath); + }); + } + + private void SaveExport(UObject export, bool auto) + { + var toSave = new Exporter(export, UserSettings.Default.ExportOptions); + + string dir; + if (!auto) + { + var folderBrowser = new VistaFolderBrowserDialog(); + if (folderBrowser.ShowDialog() == true) + dir = folderBrowser.SelectedPath; + else return; + } + else dir = UserSettings.Default.ModelDirectory; + + var toSaveDirectory = new DirectoryInfo(dir); + if (toSave.TryWriteToDir(toSaveDirectory, out var label, out var savedFilePath)) + { + Log.Information("Successfully saved {FilePath}", savedFilePath); + FLogger.Append(ELog.Information, () => + { + FLogger.Text("Successfully saved ", Constants.WHITE); + FLogger.Link(label, savedFilePath, true); + }); + } + else + { + Log.Error("{FileName} could not be saved", export.Name); + FLogger.Append(ELog.Error, () => FLogger.Text($"Could not save '{export.Name}'", Constants.WHITE, true)); + } + } + + private readonly object _rawData = new (); + public void ExportData(string fullPath, bool updateUi = true) + { + var fileName = fullPath.SubstringAfterLast('/'); + if (Provider.TrySavePackage(fullPath, out var assets)) + { + string path = UserSettings.Default.RawDataDirectory; + Parallel.ForEach(assets, kvp => + { + lock (_rawData) + { + path = Path.Combine(UserSettings.Default.RawDataDirectory, UserSettings.Default.KeepDirectoryStructure ? kvp.Key : kvp.Key.SubstringAfterLast('/')).Replace('\\', '/'); + Directory.CreateDirectory(path.SubstringBeforeLast('/')); + File.WriteAllBytes(path, kvp.Value); + } + }); + + Log.Information("{FileName} successfully exported", fileName); + if (updateUi) + { + FLogger.Append(ELog.Information, () => + { + FLogger.Text("Successfully exported ", Constants.WHITE); + FLogger.Link(fileName, path, true); + }); + } + } + else + { + Log.Error("{FileName} could not be exported", fileName); + if (updateUi) + FLogger.Append(ELog.Error, () => FLogger.Text($"Could not export '{fileName}'", Constants.WHITE, true)); + } + } + + private static bool HasFlag(EBulkType a, EBulkType b) + { + return (a & b) == b; + } +}