diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8e6f8404..b80429a5 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -21,16 +21,16 @@ jobs: - name: Fetch Submodules Recursively run: git submodule update --init --recursive - - name: .NET 6 Setup + - name: .NET 8 Setup uses: actions/setup-dotnet@v2 with: - dotnet-version: '6.0.x' + dotnet-version: '8.0.x' - name: .NET Restore run: dotnet restore FModel - name: .NET Publish - run: dotnet publish FModel -c Release --no-self-contained -r win-x64 -f net6.0-windows -o "./FModel/bin/Publish/" -p:PublishReadyToRun=false -p:PublishSingleFile=true -p:DebugType=None -p:GenerateDocumentationFile=false -p:DebugSymbols=false -p:AssemblyVersion=${{ github.event.inputs.appVersion }} -p:FileVersion=${{ github.event.inputs.appVersion }} + run: dotnet publish FModel -c Release --no-self-contained -r win-x64 -f net8.0-windows -o "./FModel/bin/Publish/" -p:PublishReadyToRun=false -p:PublishSingleFile=true -p:DebugType=None -p:GenerateDocumentationFile=false -p:DebugSymbols=false -p:AssemblyVersion=${{ github.event.inputs.appVersion }} -p:FileVersion=${{ github.event.inputs.appVersion }} - name: ZIP File uses: papeloto/action-zip@v1 diff --git a/.github/workflows/qa.yml b/.github/workflows/qa.yml new file mode 100644 index 00000000..ead5c624 --- /dev/null +++ b/.github/workflows/qa.yml @@ -0,0 +1,65 @@ +name: FModel QA Builder + +on: + push: + branches: [ dev ] + +jobs: + build: + runs-on: windows-latest + + steps: + - name: GIT Checkout + uses: actions/checkout@v4 + with: + submodules: 'recursive' + + - name: .NET 8 Setup + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.0.x' + + - name: .NET Restore + run: dotnet restore FModel + + - name: .NET Publish + run: dotnet publish "./FModel/FModel.csproj" -c Release --no-restore --no-self-contained -r win-x64 -f net8.0-windows -o "./FModel/bin/Publish/" -p:PublishReadyToRun=false -p:PublishSingleFile=true -p:DebugType=None -p:GenerateDocumentationFile=false -p:DebugSymbols=false + + - name: ZIP File + uses: thedoctor0/zip-release@0.7.6 + with: + type: zip + filename: ${{ github.sha }}.zip # will end up in working directory not the Publish folder + path: ./FModel/bin/Publish/FModel.exe + + - name: Edit QA Artifact + uses: ncipollo/release-action@v1.14.0 + with: + token: ${{ secrets.GITHUB_TOKEN }} + name: 'FModel QA Testing' + body: 'Dev builds' + tag: 'qa' + artifacts: ${{ github.sha }}.zip + prerelease: true + allowUpdates: true + + - name: Get Version + id: package_version + uses: kzrnm/get-net-sdk-project-versions-action@v2 + with: + proj-path: ./FModel/FModel.csproj + + - name: FModel Auth + id: fmodel_auth + uses: fjogeleit/http-request-action@v1.15.5 + with: + url: "https://api.fmodel.app/v1/oauth/token" + data: '{"username": "${{ secrets.API_USERNAME }}", "password": "${{ secrets.API_PASSWORD }}"}' + + - name: FModel Deploy Build + uses: fjogeleit/http-request-action@v1.15.5 + with: + url: "https://api.fmodel.app/v1/infos/${{ secrets.QA_ID }}" + method: "PATCH" + bearerToken: ${{ fromJson(steps.fmodel_auth.outputs.response).accessToken }} + data: '{"version": "${{ steps.package_version.outputs.version }}-dev+${{ github.sha }}", "downloadUrl": "https://github.com/4sval/FModel/releases/download/qa/${{ github.sha }}.zip"}' diff --git a/.gitmodules b/.gitmodules index 959570f6..2221fc41 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,3 @@ [submodule "CUE4Parse"] path = CUE4Parse url = https://github.com/FabianFG/CUE4Parse -[submodule "EpicManifestParser"] - path = EpicManifestParser - url = https://github.com/FModel/EpicManifestParser \ No newline at end of file diff --git a/CUE4Parse b/CUE4Parse index d816fe61..4e955153 160000 --- a/CUE4Parse +++ b/CUE4Parse @@ -1 +1 @@ -Subproject commit d816fe61ac8e5798d1584ea2f9871acfca0ca429 +Subproject commit 4e955153559be8dc156d15fc93ff8c1016d3ebfe diff --git a/EpicManifestParser b/EpicManifestParser deleted file mode 160000 index 21df8a55..00000000 --- a/EpicManifestParser +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 21df8a55d474f14148a35bc943e06f3fdc20c997 diff --git a/FModel/App.xaml.cs b/FModel/App.xaml.cs index 8491c467..e5161508 100644 --- a/FModel/App.xaml.cs +++ b/FModel/App.xaml.cs @@ -41,8 +41,6 @@ protected override void OnStartup(StartupEventArgs e) { UserSettings.Default = JsonConvert.DeserializeObject( File.ReadAllText(UserSettings.FilePath), JsonNetSerializer.SerializerSettings); - - /*if (UserSettings.Default.ShowChangelog) */MigrateV1Games(); } catch { @@ -52,7 +50,14 @@ protected override void OnStartup(StartupEventArgs e) var createMe = false; if (!Directory.Exists(UserSettings.Default.OutputDirectory)) { - UserSettings.Default.OutputDirectory = Path.Combine(Directory.GetCurrentDirectory(), "Output"); + var currentDir = Directory.GetCurrentDirectory(); + var dirInfo = new DirectoryInfo(currentDir); + if (dirInfo.Attributes.HasFlag(FileAttributes.Archive)) + throw new Exception("FModel cannot be run from an archive file. Please extract it and try again."); + if (dirInfo.Attributes.HasFlag(FileAttributes.ReadOnly)) + throw new Exception("FModel cannot be run from a read-only directory. Please move it to a writable location."); + + UserSettings.Default.OutputDirectory = Path.Combine(currentDir, "Output"); } if (!Directory.Exists(UserSettings.Default.RawDataDirectory)) @@ -143,17 +148,6 @@ private void OnUnhandledException(object sender, DispatcherUnhandledExceptionEve e.Handled = true; } - private void MigrateV1Games() - { - foreach ((var gameDir, var setting) in UserSettings.Default.ManualGames) - { - if (!Directory.Exists(gameDir)) continue; - UserSettings.Default.PerDirectory[gameDir] = - DirectorySettings.Default(setting.GameName, setting.GameDirectory, true, setting.OverridedGame, setting.AesKeys?.MainKey); - } - UserSettings.Default.ManualGames.Clear(); - } - private string GetOperatingSystemProductName() { var productName = string.Empty; diff --git a/FModel/Creator/Bases/FN/BaseBundle.cs b/FModel/Creator/Bases/FN/BaseBundle.cs index ea5114e8..9f4df8ce 100644 --- a/FModel/Creator/Bases/FN/BaseBundle.cs +++ b/FModel/Creator/Bases/FN/BaseBundle.cs @@ -26,7 +26,7 @@ public override void ParseForInfo() { _quests = new List(); - if (Object.TryGetValue(out FText displayName, "DisplayName")) + if (Object.TryGetValue(out FText displayName, "DisplayName", "ItemName")) DisplayName = displayName.Text.ToUpperInvariant(); if (Object.TryGetValue(out FStructFallback[] quests, "QuestInfos")) // prout :) 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 override void ParseForInfo() { 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 b47109e8..9c5f2d30 100644 --- a/FModel/Creator/Bases/FN/BaseIcon.cs +++ b/FModel/Creator/Bases/FN/BaseIcon.cs @@ -1,298 +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 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", "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", "DefaultHeaderText", "UIDisplayName", "EntryName", "EventCalloutTitle")) - DisplayName = displayName.Text; - if (Object.TryGetValue(out FText description, "Description", "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); - } - - 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, - _ => 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/Creator/Bases/FN/BaseItemAccessToken.cs b/FModel/Creator/Bases/FN/BaseItemAccessToken.cs index 3df2e490..455a1fc5 100644 --- a/FModel/Creator/Bases/FN/BaseItemAccessToken.cs +++ b/FModel/Creator/Bases/FN/BaseItemAccessToken.cs @@ -29,12 +29,12 @@ public override void ParseForInfo() _icon.ParseForReward(false); } - if (Object.TryGetValue(out FText displayName, "DisplayName") && displayName.Text != "TBD") + if (Object.TryGetValue(out FText displayName, "DisplayName", "ItemName") && displayName.Text != "TBD") DisplayName = displayName.Text; else DisplayName = _icon?.DisplayName; - Description = Object.TryGetValue(out FText description, "Description") ? description.Text : _icon?.Description; + Description = Object.TryGetValue(out FText description, "Description", "ItemDescription") ? description.Text : _icon?.Description; if (Object.TryGetValue(out FText unlockDescription, "UnlockDescription")) _unlockedDescription = unlockDescription.Text; } diff --git a/FModel/Creator/Bases/FN/BaseJuno.cs b/FModel/Creator/Bases/FN/BaseJuno.cs new file mode 100644 index 00000000..bcabb7bf --- /dev/null +++ b/FModel/Creator/Bases/FN/BaseJuno.cs @@ -0,0 +1,49 @@ +using CUE4Parse.UE4.Assets.Exports; +using CUE4Parse.UE4.Assets.Objects; +using CUE4Parse.UE4.Objects.UObject; +using SkiaSharp; + +namespace FModel.Creator.Bases.FN; + +public class BaseJuno : BaseIcon +{ + private BaseIcon _character; + + public BaseJuno(UObject uObject, EIconStyle style) : base(uObject, style) + { + + } + + public override void ParseForInfo() + { + if (Object.TryGetValue(out FSoftObjectPath baseCid, "BaseAthenaCharacterItemDefinition") && + Utils.TryLoadObject(baseCid.AssetPathName.Text, out UObject cid)) + { + _character = new BaseIcon(cid, Style); + _character.ParseForInfo(); + + if (Object.TryGetValue(out FSoftObjectPath assembledMeshSchema, "AssembledMeshSchema", "LowDetailsAssembledMeshSchema") && + Utils.TryLoadObject(assembledMeshSchema.AssetPathName.Text, out UObject meshSchema) && + meshSchema.TryGetValue(out FInstancedStruct[] additionalData, "AdditionalData")) + { + foreach (var data in additionalData) + { + if (data.NonConstStruct?.TryGetValue(out FSoftObjectPath largePreview, "LargePreviewImage", "SmallPreviewImage") ?? false) + { + _character.Preview = Utils.GetBitmap(largePreview); + break; + } + } + } + } + + if (Object.TryGetValue(out FSoftObjectPath baseEid, "BaseAthenaDanceItemDefinition") && + Utils.TryLoadObject(baseEid.AssetPathName.Text, out UObject eid)) + { + _character = new BaseIcon(eid, Style); + _character.ParseForInfo(); + } + } + + public override SKBitmap[] Draw() => _character.Draw(); +} diff --git a/FModel/Creator/Bases/FN/BaseMtxOffer.cs b/FModel/Creator/Bases/FN/BaseMtxOffer.cs index 68e64762..7f62c89e 100644 --- a/FModel/Creator/Bases/FN/BaseMtxOffer.cs +++ b/FModel/Creator/Bases/FN/BaseMtxOffer.cs @@ -17,10 +17,9 @@ public BaseMtxOffer(UObject uObject, EIconStyle style) : base(uObject, style) public override void ParseForInfo() { - if (Object.TryGetValue(out FStructFallback typeImage, "DetailsImage", "TileImage") && - typeImage.TryGetValue(out FPackageIndex resource, "ResourceObject")) + if (Object.TryGetValue(out FSoftObjectPath image, "SoftDetailsImage", "SoftTileImage")) { - Preview = Utils.GetBitmap(resource); + Preview = Utils.GetBitmap(image); } if (Object.TryGetValue(out FStructFallback gradient, "Gradient") && @@ -81,4 +80,4 @@ public override SKBitmap[] Draw() return new[] { ret }; } -} \ No newline at end of file +} diff --git a/FModel/Creator/Bases/FN/BaseOfferDisplayData.cs b/FModel/Creator/Bases/FN/BaseOfferDisplayData.cs index b1a1808e..250a6ab3 100644 --- a/FModel/Creator/Bases/FN/BaseOfferDisplayData.cs +++ b/FModel/Creator/Bases/FN/BaseOfferDisplayData.cs @@ -1,5 +1,7 @@ using CUE4Parse.UE4.Assets.Exports; using CUE4Parse.UE4.Assets.Exports.Material; +using CUE4Parse.UE4.Assets.Objects; +using CUE4Parse.UE4.Objects.UObject; using SkiaSharp; namespace FModel.Creator.Bases.FN; @@ -14,13 +16,16 @@ public BaseOfferDisplayData(UObject uObject, EIconStyle style) : base(uObject, s public override void ParseForInfo() { - if (!Object.TryGetValue(out UMaterialInterface[] presentations, "Presentations")) + if (!Object.TryGetValue(out FStructFallback[] contextualPresentations, "ContextualPresentations")) return; - _offerImages = new BaseMaterialInstance[presentations.Length]; + _offerImages = new BaseMaterialInstance[contextualPresentations.Length]; for (var i = 0; i < _offerImages.Length; i++) { - var offerImage = new BaseMaterialInstance(presentations[i], Style); + if (!contextualPresentations[i].TryGetValue(out FSoftObjectPath material, "Material") || + !material.TryLoad(out UMaterialInterface presentation)) continue; + + var offerImage = new BaseMaterialInstance(presentation, Style); offerImage.ParseForInfo(); _offerImages[i] = offerImage; } @@ -36,4 +41,4 @@ public override SKBitmap[] Draw() return ret; } -} \ No newline at end of file +} diff --git a/FModel/Creator/Bases/FN/BaseQuest.cs b/FModel/Creator/Bases/FN/BaseQuest.cs index 4c0364b2..fd0c862b 100644 --- a/FModel/Creator/Bases/FN/BaseQuest.cs +++ b/FModel/Creator/Bases/FN/BaseQuest.cs @@ -69,13 +69,19 @@ public override void ParseForInfo() } else { - Description = ShortDescription; - if (Object.TryGetValue(out FText completionText, "CompletionText")) - Description += "\n" + completionText.Text; - if (Object.TryGetValue(out FSoftObjectPath tandemCharacterData, "TandemCharacterData") && + if (!string.IsNullOrEmpty(ShortDescription)) + Description = ShortDescription; + if (string.IsNullOrEmpty(DisplayName) && !string.IsNullOrEmpty(Description)) + DisplayName = Description; + if (DisplayName == Description) + Description = string.Empty; + + if ((Object.TryGetValue(out FSoftObjectPath icon, "QuestGiverWidgetIcon", "NotificationIconOverride") && + Utils.TryLoadObject(icon.AssetPathName.Text, out UObject iconObject)) || + (Object.TryGetValue(out FSoftObjectPath tandemCharacterData, "TandemCharacterData") && Utils.TryLoadObject(tandemCharacterData.AssetPathName.Text, out UObject uObject) && uObject.TryGetValue(out FSoftObjectPath tandemIcon, "EntryListIcon", "ToastIcon") && - Utils.TryLoadObject(tandemIcon.AssetPathName.Text, out UObject iconObject)) + Utils.TryLoadObject(tandemIcon.AssetPathName.Text, out iconObject))) { Preview = iconObject switch { @@ -127,9 +133,16 @@ public override void ParseForInfo() } } - if (_reward == null && Object.TryGetValue(out UDataTable rewardsTable, "RewardsTable")) + if (_reward == null) { - if (rewardsTable.TryGetDataTableRow("Default", StringComparison.InvariantCulture, out var row)) + FName rowName = null; + if (Object.TryGetValue(out UDataTable rewardsTable, "RewardsTable")) + rowName = new FName("Default"); + else if (Object.TryGetValue(out FStructFallback[] rewardTableRows, "IndividualRewardTableRows") && + rewardTableRows.Length > 0 && rewardTableRows[0].TryGetValue(out rowName, "RowName") && + rewardTableRows[0].TryGetValue(out rewardsTable, "DataTable")) {} + + if (rewardsTable != null && rowName != null && rewardsTable.TryGetDataTableRow(rowName.Text, StringComparison.InvariantCulture, out var row)) { if (row.TryGetValue(out FName templateId, "TemplateId") && row.TryGetValue(out int quantity, "Quantity")) diff --git a/FModel/Creator/Bases/FN/BaseSeason.cs b/FModel/Creator/Bases/FN/BaseSeason.cs index c97163b5..1ee32067 100644 --- a/FModel/Creator/Bases/FN/BaseSeason.cs +++ b/FModel/Creator/Bases/FN/BaseSeason.cs @@ -36,7 +36,7 @@ public override void ParseForInfo() { _bookXpSchedule = Array.Empty(); - if (Object.TryGetValue(out FText displayName, "DisplayName")) + if (Object.TryGetValue(out FText displayName, "DisplayName", "ItemName")) DisplayName = displayName.Text.ToUpperInvariant(); if (Object.TryGetValue(out FStructFallback seasonFirstWinRewards, "SeasonFirstWinRewards") && diff --git a/FModel/Creator/Bases/FN/Reward.cs b/FModel/Creator/Bases/FN/Reward.cs index 0e5aad43..fe650617 100644 --- a/FModel/Creator/Bases/FN/Reward.cs +++ b/FModel/Creator/Bases/FN/Reward.cs @@ -64,7 +64,7 @@ public void DrawQuest(SKCanvas c, SKRect rect) _rewardPaint.TextSize = 50; if (HasReward()) { - c.DrawBitmap(_theReward.Preview.Resize((int) rect.Height), new SKPoint(rect.Left, rect.Top), _rewardPaint); + c.DrawBitmap((_theReward.Preview ?? _theReward.DefaultPreview).Resize((int) rect.Height), new SKPoint(rect.Left, rect.Top), _rewardPaint); _rewardPaint.Color = _theReward.Border[0]; _rewardPaint.Typeface = _rewardQuantity.StartsWith("x") ? Utils.Typefaces.BundleNumber : Utils.Typefaces.Bundle; @@ -88,7 +88,7 @@ public void DrawQuest(SKCanvas c, SKRect rect) public void DrawSeasonWin(SKCanvas c, int size) { if (!HasReward()) return; - c.DrawBitmap(_theReward.Preview.Resize(size), new SKPoint(0, 0), _rewardPaint); + c.DrawBitmap((_theReward.Preview ?? _theReward.DefaultPreview).Resize(size), new SKPoint(0, 0), _rewardPaint); } public void DrawSeason(SKCanvas c, int x, int y, int areaSize) @@ -115,33 +115,33 @@ private void GetReward(string trigger) { switch (trigger.ToLower()) { - case "athenabattlestar": - _theReward = new BaseIcon(null, EIconStyle.Default); - _theReward.Border[0] = SKColor.Parse("FFDB67"); - _theReward.Background[0] = SKColor.Parse("8F4A20"); - _theReward.Preview = Utils.GetBitmap("FortniteGame/Content/Athena/UI/Frontend/Art/T_UI_BP_BattleStar_L.T_UI_BP_BattleStar_L"); - break; - case "athenaseasonalxp": - _theReward = new BaseIcon(null, EIconStyle.Default); - _theReward.Border[0] = SKColor.Parse("E6FDB1"); - _theReward.Background[0] = SKColor.Parse("51830F"); - _theReward.Preview = Utils.GetBitmap("FortniteGame/Content/UI/Foundation/Textures/Icons/Items/T-FNBR-XPUncommon-L.T-FNBR-XPUncommon-L"); - break; - case "mtxgiveaway": - _theReward = new BaseIcon(null, EIconStyle.Default); - _theReward.Border[0] = SKColor.Parse("DCE6FF"); - _theReward.Background[0] = SKColor.Parse("64A0AF"); - _theReward.Preview = Utils.GetBitmap("FortniteGame/Content/UI/Foundation/Textures/Icons/Items/T-Items-MTX.T-Items-MTX"); - break; + // case "athenabattlestar": + // _theReward = new BaseIcon(null, EIconStyle.Default); + // _theReward.Border[0] = SKColor.Parse("FFDB67"); + // _theReward.Background[0] = SKColor.Parse("8F4A20"); + // _theReward.Preview = Utils.GetBitmap("FortniteGame/Content/Athena/UI/Frontend/Art/T_UI_BP_BattleStar_L.T_UI_BP_BattleStar_L"); + // break; + // case "athenaseasonalxp": + // _theReward = new BaseIcon(null, EIconStyle.Default); + // _theReward.Border[0] = SKColor.Parse("E6FDB1"); + // _theReward.Background[0] = SKColor.Parse("51830F"); + // _theReward.Preview = Utils.GetBitmap("FortniteGame/Content/UI/Foundation/Textures/Icons/Items/T-FNBR-XPUncommon-L.T-FNBR-XPUncommon-L"); + // break; + // case "mtxgiveaway": + // _theReward = new BaseIcon(null, EIconStyle.Default); + // _theReward.Border[0] = SKColor.Parse("DCE6FF"); + // _theReward.Background[0] = SKColor.Parse("64A0AF"); + // _theReward.Preview = Utils.GetBitmap("FortniteGame/Content/UI/Foundation/Textures/Icons/Items/T-Items-MTX.T-Items-MTX"); + // break; default: { - var path = Utils.GetFullPath($"FortniteGame/Content/Athena/.*?/{trigger}.*"); // path has no objectname and its needed so we push the trigger again as the objectname + var path = Utils.GetFullPath($"FortniteGame/(?:Content/Athena|Content/Items|Plugins/GameFeatures)/.*?/{trigger}.uasset"); // path has no objectname and its needed so we push the trigger again as the objectname if (!string.IsNullOrWhiteSpace(path) && Utils.TryLoadObject(path.Replace("uasset", trigger), out UObject d)) { _theReward = new BaseIcon(d, EIconStyle.Default); _theReward.ParseForReward(false); _theReward.Border[0] = SKColors.White; - _rewardQuantity = _theReward.DisplayName; + _rewardQuantity = $"{_theReward.DisplayName} ({_rewardQuantity})"; } break; diff --git a/FModel/Creator/CreatorPackage.cs b/FModel/Creator/CreatorPackage.cs index 1ff20af0..4a10cde2 100644 --- a/FModel/Creator/CreatorPackage.cs +++ b/FModel/Creator/CreatorPackage.cs @@ -1,218 +1,243 @@ -using System; -using System.Runtime.CompilerServices; -using CUE4Parse.UE4.Assets.Exports; -using FModel.Creator.Bases; -using FModel.Creator.Bases.BB; -using FModel.Creator.Bases.FN; -using FModel.Creator.Bases.MV; -using FModel.Creator.Bases.SB; - -namespace FModel.Creator; - -public class CreatorPackage : IDisposable -{ - private UObject _object; - private EIconStyle _style; - - public CreatorPackage(UObject uObject, EIconStyle style) - { - _object = uObject; - _style = style; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public UCreator ConstructCreator() - { - TryConstructCreator(out var creator); - return creator; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool TryConstructCreator(out UCreator creator) - { - switch (_object.ExportType) - { - // Fortnite - case "FortCreativeWeaponMeleeItemDefinition": - case "AthenaConsumableEmoteItemDefinition": - case "AthenaSkyDiveContrailItemDefinition": - case "AthenaLoadingScreenItemDefinition": - case "AthenaVictoryPoseItemDefinition": - case "AthenaPetCarrierItemDefinition": - case "AthenaMusicPackItemDefinition": - case "AthenaBattleBusItemDefinition": - case "AthenaCharacterItemDefinition": - case "AthenaMapMarkerItemDefinition": - case "AthenaBackpackItemDefinition": - case "AthenaPickaxeItemDefinition": - case "AthenaGadgetItemDefinition": - case "AthenaGliderItemDefinition": - case "AthenaSprayItemDefinition": - case "AthenaDanceItemDefinition": - case "AthenaEmojiItemDefinition": - case "AthenaItemWrapDefinition": - case "AthenaToyItemDefinition": - case "FortHeroType": - case "FortTokenType": - case "FortAbilityKit": - case "FortWorkerType": - case "RewardGraphToken": - case "FortBannerTokenType": - case "FortVariantTokenType": - case "FortDecoItemDefinition": - case "FortStatItemDefinition": - case "FortAmmoItemDefinition": - case "FortEmoteItemDefinition": - case "FortBadgeItemDefinition": - case "FortAwardItemDefinition": - case "FortGadgetItemDefinition": - case "AthenaCharmItemDefinition": - case "FortPlaysetItemDefinition": - case "FortGiftBoxItemDefinition": - case "FortOutpostItemDefinition": - case "FortVehicleItemDefinition": - case "FortCardPackItemDefinition": - case "FortDefenderItemDefinition": - case "FortCurrencyItemDefinition": - case "FortResourceItemDefinition": - case "FortBackpackItemDefinition": - case "FortEventQuestMapDataAsset": - case "FortWeaponModItemDefinition": - case "FortCodeTokenItemDefinition": - case "FortSchematicItemDefinition": - case "FortWorldMultiItemDefinition": - case "FortAlterationItemDefinition": - case "FortExpeditionItemDefinition": - case "FortIngredientItemDefinition": - case "FortAccountBuffItemDefinition": - case "FortWeaponMeleeItemDefinition": - case "FortPlayerPerksItemDefinition": - case "FortPlaysetPropItemDefinition": - case "FortHomebaseNodeItemDefinition": - case "FortNeverPersistItemDefinition": - case "FortPlayerAugmentItemDefinition": - case "RadioContentSourceItemDefinition": - case "FortPlaysetGrenadeItemDefinition": - case "FortPersonalVehicleItemDefinition": - case "FortGameplayModifierItemDefinition": - case "FortHardcoreModifierItemDefinition": - case "FortConsumableAccountItemDefinition": - case "FortConversionControlItemDefinition": - case "FortAccountBuffCreditItemDefinition": - case "FortEventCurrencyItemDefinitionRedir": - case "FortPersistentResourceItemDefinition": - case "FortHomebaseBannerIconItemDefinition": - case "FortCampaignHeroLoadoutItemDefinition": - case "FortConditionalResourceItemDefinition": - case "FortChallengeBundleScheduleDefinition": - case "FortWeaponMeleeDualWieldItemDefinition": - case "FortDailyRewardScheduleTokenDefinition": - case "FortCreativeWeaponRangedItemDefinition": - case "FortCreativeRealEstatePlotItemDefinition": - case "AthenaDanceItemDefinition_AdHocSquadsJoin_C": - case "StWFortAccoladeItemDefinition": - case "FortSmartBuildingItemDefinition": - creator = _style switch - { - EIconStyle.Cataba => new BaseCommunity(_object, _style, "Cataba"), - _ => new BaseIcon(_object, _style) - }; - return true; - case "FortTandemCharacterData": - creator = new BaseTandem(_object, _style); - return true; - case "FortTrapItemDefinition": - case "FortSpyTechItemDefinition": - case "FortAccoladeItemDefinition": - case "FortContextTrapItemDefinition": - case "FortWeaponRangedItemDefinition": - case "Daybreak_LevelExitVehicle_PartItemDefinition_C": - creator = new BaseIconStats(_object, _style); - return true; - case "FortItemSeriesDefinition": - creator = new BaseSeries(_object, _style); - return true; - case "MaterialInstanceConstant" - when _object.Owner != null && - (_object.Owner.Name.EndsWith($"/MI_OfferImages/{_object.Name}", StringComparison.OrdinalIgnoreCase) || - _object.Owner.Name.EndsWith($"/RenderSwitch_Materials/{_object.Name}", StringComparison.OrdinalIgnoreCase) || - _object.Owner.Name.EndsWith($"/MI_BPTile/{_object.Name}", StringComparison.OrdinalIgnoreCase)): - creator = new BaseMaterialInstance(_object, _style); - return true; - case "AthenaItemShopOfferDisplayData": - creator = new BaseOfferDisplayData(_object, _style); - return true; - case "FortMtxOfferData": - creator = new BaseMtxOffer(_object, _style); - return true; - case "FortPlaylistAthena": - creator = new BasePlaylist(_object, _style); - return true; - case "FortFeatItemDefinition": - case "FortQuestItemDefinition": - case "FortQuestItemDefinition_Athena": - case "FortQuestItemDefinition_Campaign": - case "AthenaDailyQuestDefinition": - case "FortUrgentQuestItemDefinition": - creator = new Bases.FN.BaseQuest(_object, _style); - return true; - case "FortCompendiumItemDefinition": - case "FortChallengeBundleItemDefinition": - creator = new BaseBundle(_object, _style); - return true; - // case "AthenaSeasonItemDefinition": - // creator = new BaseSeason(_object, _style); - // return true; - case "FortItemAccessTokenType": - creator = new BaseItemAccessToken(_object, _style); - return true; - case "FortCreativeOption": - case "PlaylistUserOptionEnum": - case "PlaylistUserOptionBool": - case "PlaylistUserOptionString": - case "PlaylistUserOptionIntEnum": - case "PlaylistUserOptionIntRange": - case "PlaylistUserOptionColorEnum": - case "PlaylistUserOptionFloatEnum": - case "PlaylistUserOptionFloatRange": - case "PlaylistUserTintedIconIntEnum": - case "PlaylistUserOptionPrimaryAsset": - case "PlaylistUserOptionCollisionProfileEnum": - creator = new BaseUserControl(_object, _style); - return true; - // PandaGame - case "CharacterData": - creator = new BaseFighter(_object, _style); - return true; - case "PerkGroup": - creator = new BasePerkGroup(_object, _style); - return true; - case "StatTrackingBundleData": - case "HydraSyncedDataAsset": - case "AnnouncerPackData": - case "CharacterGiftData": - case "ProfileIconData": - case "RingOutVfxData": - case "BannerData": - case "EmoteData": - case "TauntData": - case "SkinData": - case "PerkData": - creator = new BasePandaIcon(_object, _style); - return true; - case "QuestData": - creator = new Bases.MV.BaseQuest(_object, _style); - return true; - default: - creator = null; - return false; - } - } - - public override string ToString() => $"{_object.ExportType} | {_style}"; - - public void Dispose() - { - _object = null; - } -} +using System; +using System.Runtime.CompilerServices; +using CUE4Parse.UE4.Assets.Exports; +using FModel.Creator.Bases; +using FModel.Creator.Bases.BB; +using FModel.Creator.Bases.FN; +using FModel.Creator.Bases.MV; +using FModel.Creator.Bases.SB; + +namespace FModel.Creator; + +public class CreatorPackage : IDisposable +{ + private UObject _object; + private EIconStyle _style; + + public CreatorPackage(UObject uObject, EIconStyle style) + { + _object = uObject; + _style = style; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public UCreator ConstructCreator() + { + TryConstructCreator(out var creator); + return creator; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryConstructCreator(out UCreator creator) + { + switch (_object.ExportType) + { + // Fortnite + case "FortCreativeWeaponMeleeItemDefinition": + case "AthenaConsumableEmoteItemDefinition": + case "AthenaSkyDiveContrailItemDefinition": + case "AthenaLoadingScreenItemDefinition": + case "AthenaVictoryPoseItemDefinition": + case "AthenaPetCarrierItemDefinition": + case "AthenaMusicPackItemDefinition": + case "AthenaBattleBusItemDefinition": + case "AthenaCharacterItemDefinition": + case "AthenaMapMarkerItemDefinition": + case "AthenaBackpackItemDefinition": + case "AthenaPickaxeItemDefinition": + case "AthenaGadgetItemDefinition": + case "AthenaGliderItemDefinition": + case "AthenaSprayItemDefinition": + case "AthenaDanceItemDefinition": + case "AthenaEmojiItemDefinition": + case "AthenaItemWrapDefinition": + case "AthenaToyItemDefinition": + case "FortHeroType": + case "FortTokenType": + case "FortAbilityKit": + case "FortWorkerType": + case "RewardGraphToken": + case "JunoKnowledgeBundle": + case "FortBannerTokenType": + case "FortVariantTokenType": + case "FortDecoItemDefinition": + case "FortStatItemDefinition": + case "FortAmmoItemDefinition": + case "FortEmoteItemDefinition": + case "FortBadgeItemDefinition": + case "SparksMicItemDefinition": + case "FortAwardItemDefinition": + case "SparksAuraItemDefinition": + case "SparksDrumItemDefinition": + case "SparksBassItemDefinition": + case "FortGadgetItemDefinition": + case "AthenaCharmItemDefinition": + case "FortPlaysetItemDefinition": + case "FortGiftBoxItemDefinition": + case "FortOutpostItemDefinition": + case "FortVehicleItemDefinition": + case "SparksGuitarItemDefinition": + case "FortCardPackItemDefinition": + case "FortDefenderItemDefinition": + case "FortCurrencyItemDefinition": + case "FortResourceItemDefinition": + case "FortBackpackItemDefinition": + case "FortEventQuestMapDataAsset": + case "FortWeaponModItemDefinition": + case "FortCodeTokenItemDefinition": + case "FortSchematicItemDefinition": + case "SparksKeyboardItemDefinition": + case "FortWorldMultiItemDefinition": + case "FortAlterationItemDefinition": + case "FortExpeditionItemDefinition": + case "FortIngredientItemDefinition": + case "StWFortAccoladeItemDefinition": + case "FortAccountBuffItemDefinition": + case "FortWeaponMeleeItemDefinition": + case "FortPlayerPerksItemDefinition": + case "FortPlaysetPropItemDefinition": + case "JunoRecipeBundleItemDefinition": + case "FortHomebaseNodeItemDefinition": + case "FortNeverPersistItemDefinition": + case "FortPlayerAugmentItemDefinition": + case "FortSmartBuildingItemDefinition": + case "FortWeaponModItemDefinitionOptic": + case "RadioContentSourceItemDefinition": + case "FortPlaysetGrenadeItemDefinition": + case "JunoWeaponCreatureItemDefinition": + case "FortPersonalVehicleItemDefinition": + case "FortGameplayModifierItemDefinition": + case "FortHardcoreModifierItemDefinition": + case "FortWeaponModItemDefinitionMagazine": + case "FortConsumableAccountItemDefinition": + case "FortConversionControlItemDefinition": + case "FortAccountBuffCreditItemDefinition": + case "JunoBuildInstructionsItemDefinition": + case "JunoBuildingSetAccountItemDefinition": + case "FortEventCurrencyItemDefinitionRedir": + case "FortPersistentResourceItemDefinition": + case "FortWeaponMeleeOffhandItemDefinition": + case "FortHomebaseBannerIconItemDefinition": + case "JunoBuildingPropAccountItemDefinition": + case "FortCampaignHeroLoadoutItemDefinition": + case "FortConditionalResourceItemDefinition": + case "FortChallengeBundleScheduleDefinition": + case "FortWeaponMeleeDualWieldItemDefinition": + case "FortDailyRewardScheduleTokenDefinition": + case "FortCreativeWeaponRangedItemDefinition": + case "FortVehicleCosmeticsItemDefinition_Body": + case "FortVehicleCosmeticsItemDefinition_Skin": + case "FortVehicleCosmeticsItemDefinition_Wheel": + case "FortCreativeRealEstatePlotItemDefinition": + case "FortVehicleCosmeticsItemDefinition_Booster": + case "AthenaDanceItemDefinition_AdHocSquadsJoin_C": + case "FortVehicleCosmeticsItemDefinition_DriftSmoke": + case "FortVehicleCosmeticsItemDefinition_EngineAudio": + creator = _style switch + { + EIconStyle.Cataba => new BaseCommunity(_object, _style, "Cataba"), + _ => new BaseIcon(_object, _style) + }; + return true; + case "JunoAthenaCharacterItemOverrideDefinition": + case "JunoAthenaDanceItemOverrideDefinition": + creator = new BaseJuno(_object, _style); + return true; + case "FortTandemCharacterData": + creator = new BaseTandem(_object, _style); + return true; + case "FortTrapItemDefinition": + case "FortSpyTechItemDefinition": + case "FortAccoladeItemDefinition": + case "FortContextTrapItemDefinition": + case "FortWeaponRangedItemDefinition": + case "Daybreak_LevelExitVehicle_PartItemDefinition_C": + creator = new BaseIconStats(_object, _style); + return true; + case "FortItemSeriesDefinition": + creator = new BaseSeries(_object, _style); + return true; + case "MaterialInstanceConstant" + when _object.Owner != null && + (_object.Owner.Name.Contains("/MI_OfferImages/", StringComparison.OrdinalIgnoreCase) || + _object.Owner.Name.EndsWith($"/RenderSwitch_Materials/{_object.Name}", StringComparison.OrdinalIgnoreCase) || + _object.Owner.Name.EndsWith($"/MI_BPTile/{_object.Name}", StringComparison.OrdinalIgnoreCase)): + creator = new BaseMaterialInstance(_object, _style); + return true; + case "AthenaItemShopOfferDisplayData": + creator = new BaseOfferDisplayData(_object, _style); + return true; + case "FortMtxOfferData": + creator = new BaseMtxOffer(_object, _style); + return true; + case "FortPlaylistAthena": + creator = new BasePlaylist(_object, _style); + return true; + case "FortFeatItemDefinition": + case "FortQuestItemDefinition": + case "FortQuestItemDefinition_Athena": + case "FortQuestItemDefinition_Campaign": + case "AthenaDailyQuestDefinition": + case "FortUrgentQuestItemDefinition": + creator = new Bases.FN.BaseQuest(_object, _style); + return true; + case "FortCompendiumItemDefinition": + case "FortChallengeBundleItemDefinition": + creator = new BaseBundle(_object, _style); + return true; + // case "AthenaSeasonItemDefinition": + // creator = new BaseSeason(_object, _style); + // return true; + case "FortItemAccessTokenType": + creator = new BaseItemAccessToken(_object, _style); + return true; + case "FortCreativeOption": + case "PlaylistUserOptionEnum": + case "PlaylistUserOptionBool": + case "PlaylistUserOptionString": + case "PlaylistUserOptionIntEnum": + case "PlaylistUserOptionIntRange": + case "PlaylistUserOptionColorEnum": + case "PlaylistUserOptionFloatEnum": + case "PlaylistUserOptionFloatRange": + case "PlaylistUserTintedIconIntEnum": + case "PlaylistUserOptionPrimaryAsset": + case "PlaylistUserOptionCollisionProfileEnum": + creator = new BaseUserControl(_object, _style); + return true; + // PandaGame + case "CharacterData": + creator = new BaseFighter(_object, _style); + return true; + case "PerkGroup": + creator = new BasePerkGroup(_object, _style); + return true; + case "StatTrackingBundleData": + case "HydraSyncedDataAsset": + case "AnnouncerPackData": + case "CharacterGiftData": + case "ProfileIconData": + case "RingOutVfxData": + case "BannerData": + case "EmoteData": + case "TauntData": + case "SkinData": + case "PerkData": + creator = new BasePandaIcon(_object, _style); + return true; + case "QuestData": + creator = new Bases.MV.BaseQuest(_object, _style); + return true; + default: + creator = null; + return false; + } + } + + public override string ToString() => $"{_object.ExportType} | {_style}"; + + public void Dispose() + { + _object = null; + } +} diff --git a/FModel/Enums.cs b/FModel/Enums.cs index 77c81c0e..8231f7b6 100644 --- a/FModel/Enums.cs +++ b/FModel/Enums.cs @@ -54,8 +54,6 @@ public enum EDiscordRpc public enum ELoadingMode { - [Description("Single")] - Single, [Description("Multiple")] Multiple, [Description("All")] @@ -71,7 +69,9 @@ public enum EUpdateMode [Description("Stable")] Stable, [Description("Beta")] - Beta + Beta, + [Description("QA Testing")] + Qa } public enum ECompressedAudio diff --git a/FModel/Extensions/EnumExtensions.cs b/FModel/Extensions/EnumExtensions.cs index 3c3fa9a9..9b764f02 100644 --- a/FModel/Extensions/EnumExtensions.cs +++ b/FModel/Extensions/EnumExtensions.cs @@ -11,8 +11,21 @@ public static string GetDescription(this Enum value) { var fi = value.GetType().GetField(value.ToString()); if (fi == null) return $"{value} ({value:D})"; + var attributes = (DescriptionAttribute[]) fi.GetCustomAttributes(typeof(DescriptionAttribute), false); - return attributes.Length > 0 ? attributes[0].Description : $"{value} ({value:D})"; + if (attributes.Length > 0) return attributes[0].Description; + + + var suffix = $"{value:D}"; + var current = Convert.ToInt32(suffix); + var target = current & ~0xF; + if (current != target) + { + var values = Enum.GetValues(value.GetType()); + var index = Array.IndexOf(values, value); + suffix = values.GetValue(index - (current - target))?.ToString(); + } + return $"{value} ({suffix})"; } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -43,4 +56,4 @@ public static T Previous(this Enum value) var i = Array.IndexOf(values, value) - 1; return i == -1 ? (T) values.GetValue(values.Length - 1) : (T) values.GetValue(i); } -} \ No newline at end of file +} diff --git a/FModel/Extensions/StringExtensions.cs b/FModel/Extensions/StringExtensions.cs index 67dec4ba..b632865d 100644 --- a/FModel/Extensions/StringExtensions.cs +++ b/FModel/Extensions/StringExtensions.cs @@ -2,6 +2,7 @@ using System.IO; using System.Runtime.CompilerServices; using System.Text.RegularExpressions; +using ICSharpCode.AvalonEdit.Document; namespace FModel.Extensions; @@ -94,7 +95,7 @@ public static string SubstringAfterLast(this string s, string delimiter, StringC } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int GetLineNumber(this string s, string lineToFind) + public static int GetNameLineNumber(this string s, string lineToFind) { if (int.TryParse(lineToFind, out var index)) return s.GetLineNumber(index); @@ -113,6 +114,25 @@ public static int GetLineNumber(this string s, string lineToFind) return 1; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string GetParentExportType(this TextDocument doc, int startOffset) + { + var line = doc.GetLineByOffset(startOffset); + var lineNumber = line.LineNumber - 1; + + while (doc.GetText(line.Offset, line.Length) is { } content) + { + if (content.StartsWith(" \"Type\": \"", StringComparison.OrdinalIgnoreCase)) + return content.Split("\"")[3]; + + lineNumber--; + if (lineNumber < 1) break; + line = doc.GetLineByNumber(lineNumber); + } + + return string.Empty; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int GetKismetLineNumber(this string s, string input) { diff --git a/FModel/FModel.csproj b/FModel/FModel.csproj index e8971df2..1094a287 100644 --- a/FModel/FModel.csproj +++ b/FModel/FModel.csproj @@ -2,12 +2,12 @@ WinExe - net6.0-windows + net8.0-windows true FModel.ico - 4.4.3.4 - 4.4.3.4 - 4.4.3.4 + 4.4.3.6 + 4.4.3.6 + 4.4.3.6 false true win-x64 @@ -45,6 +45,12 @@ + + + + + + @@ -110,6 +116,7 @@ + @@ -135,34 +142,34 @@ + - + - - + + + - - + - + - - + + - @@ -182,6 +189,12 @@ + + + + + + diff --git a/FModel/FModel.sln b/FModel/FModel.sln index 52492681..238d1b59 100644 --- a/FModel/FModel.sln +++ b/FModel/FModel.sln @@ -9,8 +9,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CUE4Parse", "..\CUE4Parse\C EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CUE4Parse-Conversion", "..\CUE4Parse\CUE4Parse-Conversion\CUE4Parse-Conversion.csproj", "{D0E1E8F7-F56D-469A-8E24-C2439B9FFD83}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EpicManifestParser", "..\EpicManifestParser\src\EpicManifestParser\EpicManifestParser.csproj", "{D4958A8B-815B-421D-A988-2A4E8E2B582D}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -29,10 +27,6 @@ Global {D0E1E8F7-F56D-469A-8E24-C2439B9FFD83}.Debug|Any CPU.Build.0 = Debug|Any CPU {D0E1E8F7-F56D-469A-8E24-C2439B9FFD83}.Release|Any CPU.ActiveCfg = Release|Any CPU {D0E1E8F7-F56D-469A-8E24-C2439B9FFD83}.Release|Any CPU.Build.0 = Release|Any CPU - {D4958A8B-815B-421D-A988-2A4E8E2B582D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D4958A8B-815B-421D-A988-2A4E8E2B582D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D4958A8B-815B-421D-A988-2A4E8E2B582D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D4958A8B-815B-421D-A988-2A4E8E2B582D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/FModel/Framework/FStatus.cs b/FModel/Framework/FStatus.cs index 28421deb..a702dccb 100644 --- a/FModel/Framework/FStatus.cs +++ b/FModel/Framework/FStatus.cs @@ -38,8 +38,8 @@ public void SetStatus(EStatusKind kind, string label = "") UpdateStatusLabel(label); } - public void UpdateStatusLabel(string label) + public void UpdateStatusLabel(string label, string prefix = null) { - Label = Kind == EStatusKind.Loading ? $"{Kind} {label}".Trim() : Kind.ToString(); + Label = Kind == EStatusKind.Loading ? $"{prefix ?? Kind.ToString()} {label}".Trim() : Kind.ToString(); } } diff --git a/FModel/Framework/ImGuiController.cs b/FModel/Framework/ImGuiController.cs index 3c099c7b..8e1e2c91 100644 --- a/FModel/Framework/ImGuiController.cs +++ b/FModel/Framework/ImGuiController.cs @@ -1,10 +1,13 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.IO; using System.Numerics; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using System.Windows; using System.Windows.Forms; +using FModel.Settings; using ImGuiNET; using OpenTK.Graphics.OpenGL4; using OpenTK.Windowing.Desktop; @@ -34,7 +37,6 @@ public class ImGuiController : IDisposable private int _windowWidth; private int _windowHeight; - // private string _iniPath; public ImFontPtr FontNormal; public ImFontPtr FontBold; @@ -49,7 +51,6 @@ public ImGuiController(int width, int height) { _windowWidth = width; _windowHeight = height; - // _iniPath = Path.Combine(UserSettings.Default.OutputDirectory, ".data", "imgui.ini"); int major = GL.GetInteger(GetPName.MajorVersion); int minor = GL.GetInteger(GetPName.MinorVersion); @@ -58,9 +59,13 @@ public ImGuiController(int width, int height) IntPtr context = ImGui.CreateContext(); ImGui.SetCurrentContext(context); - // ImGui.LoadIniSettingsFromDisk(_iniPath); var io = ImGui.GetIO(); + unsafe + { + var iniFileNamePtr = Marshal.StringToCoTaskMemUTF8(Path.Combine(UserSettings.Default.OutputDirectory, ".data", "imgui.ini")); + io.NativePtr->IniFilename = (byte*)iniFileNamePtr; + } FontNormal = io.Fonts.AddFontFromFileTTF("C:\\Windows\\Fonts\\segoeui.ttf", 16 * DpiScale); FontBold = io.Fonts.AddFontFromFileTTF("C:\\Windows\\Fonts\\segoeuib.ttf", 16 * DpiScale); FontSemiBold = io.Fonts.AddFontFromFileTTF("C:\\Windows\\Fonts\\seguisb.ttf", 16 * DpiScale); @@ -71,7 +76,6 @@ public ImGuiController(int width, int height) io.Fonts.Flags |= ImFontAtlasFlags.NoBakedLines; CreateDeviceResources(); - SetKeyMappings(); SetPerFrameImGuiData(1f / 60f); @@ -271,8 +275,8 @@ private void UpdateImGuiInput(GameWindow wnd) foreach (Keys key in Enum.GetValues(typeof(Keys))) { - if (key == Keys.Unknown || io.KeyMap[(int) key] == -1) continue; - io.AddKeyEvent((ImGuiKey) io.KeyMap[(int) key], kState.IsKeyDown(key)); + if (key == Keys.Unknown) continue; + io.AddKeyEvent(TranslateKey(key), kState.IsKeyDown(key)); } foreach (var c in PressedChars) @@ -292,115 +296,6 @@ public void PressChar(char keyChar) PressedChars.Add(keyChar); } - private static void SetKeyMappings() - { - ImGuiIOPtr io = ImGui.GetIO(); - io.KeyMap[(int)ImGuiKey.LeftShift] = (int)Keys.LeftShift; - io.KeyMap[(int)ImGuiKey.RightShift] = (int)Keys.RightShift; - io.KeyMap[(int)ImGuiKey.LeftCtrl] = (int)Keys.LeftControl; - io.KeyMap[(int)ImGuiKey.RightCtrl] = (int)Keys.RightControl; - io.KeyMap[(int)ImGuiKey.LeftAlt] = (int)Keys.LeftAlt; - io.KeyMap[(int)ImGuiKey.RightAlt] = (int)Keys.RightAlt; - io.KeyMap[(int)ImGuiKey.LeftSuper] = (int)Keys.LeftSuper; - io.KeyMap[(int)ImGuiKey.RightSuper] = (int)Keys.RightSuper; - io.KeyMap[(int)ImGuiKey.Menu] = (int)Keys.Menu; - io.KeyMap[(int)ImGuiKey.UpArrow] = (int)Keys.Up; - io.KeyMap[(int)ImGuiKey.DownArrow] = (int)Keys.Down; - io.KeyMap[(int)ImGuiKey.LeftArrow] = (int)Keys.Left; - io.KeyMap[(int)ImGuiKey.RightArrow] = (int)Keys.Right; - io.KeyMap[(int)ImGuiKey.Enter] = (int)Keys.Enter; - io.KeyMap[(int)ImGuiKey.Escape] = (int)Keys.Escape; - io.KeyMap[(int)ImGuiKey.Space] = (int)Keys.Space; - io.KeyMap[(int)ImGuiKey.Tab] = (int)Keys.Tab; - io.KeyMap[(int)ImGuiKey.Backspace] = (int)Keys.Backspace; - io.KeyMap[(int)ImGuiKey.Insert] = (int)Keys.Insert; - io.KeyMap[(int)ImGuiKey.Delete] = (int)Keys.Delete; - io.KeyMap[(int)ImGuiKey.PageUp] = (int)Keys.PageUp; - io.KeyMap[(int)ImGuiKey.PageDown] = (int)Keys.PageDown; - io.KeyMap[(int)ImGuiKey.Home] = (int)Keys.Home; - io.KeyMap[(int)ImGuiKey.End] = (int)Keys.End; - io.KeyMap[(int)ImGuiKey.CapsLock] = (int)Keys.CapsLock; - io.KeyMap[(int)ImGuiKey.ScrollLock] = (int)Keys.ScrollLock; - io.KeyMap[(int)ImGuiKey.PrintScreen] = (int)Keys.PrintScreen; - io.KeyMap[(int)ImGuiKey.Pause] = (int)Keys.Pause; - io.KeyMap[(int)ImGuiKey.NumLock] = (int)Keys.NumLock; - io.KeyMap[(int)ImGuiKey.KeypadDivide] = (int)Keys.KeyPadDivide; - io.KeyMap[(int)ImGuiKey.KeypadMultiply] = (int)Keys.KeyPadMultiply; - io.KeyMap[(int)ImGuiKey.KeypadSubtract] = (int)Keys.KeyPadSubtract; - io.KeyMap[(int)ImGuiKey.KeypadAdd] = (int)Keys.KeyPadAdd; - io.KeyMap[(int)ImGuiKey.KeypadDecimal] = (int)Keys.KeyPadDecimal; - io.KeyMap[(int)ImGuiKey.KeypadEnter] = (int)Keys.KeyPadEnter; - io.KeyMap[(int)ImGuiKey.GraveAccent] = (int)Keys.GraveAccent; - io.KeyMap[(int)ImGuiKey.Minus] = (int)Keys.Minus; - io.KeyMap[(int)ImGuiKey.Equal] = (int)Keys.Equal; - io.KeyMap[(int)ImGuiKey.LeftBracket] = (int)Keys.LeftBracket; - io.KeyMap[(int)ImGuiKey.RightBracket] = (int)Keys.RightBracket; - io.KeyMap[(int)ImGuiKey.Semicolon] = (int)Keys.Semicolon; - io.KeyMap[(int)ImGuiKey.Apostrophe] = (int)Keys.Apostrophe; - io.KeyMap[(int)ImGuiKey.Comma] = (int)Keys.Comma; - io.KeyMap[(int)ImGuiKey.Period] = (int)Keys.Period; - io.KeyMap[(int)ImGuiKey.Slash] = (int)Keys.Slash; - io.KeyMap[(int)ImGuiKey.Backslash] = (int)Keys.Backslash; - io.KeyMap[(int)ImGuiKey.F1] = (int)Keys.F1; - io.KeyMap[(int)ImGuiKey.F2] = (int)Keys.F2; - io.KeyMap[(int)ImGuiKey.F3] = (int)Keys.F3; - io.KeyMap[(int)ImGuiKey.F4] = (int)Keys.F4; - io.KeyMap[(int)ImGuiKey.F5] = (int)Keys.F5; - io.KeyMap[(int)ImGuiKey.F6] = (int)Keys.F6; - io.KeyMap[(int)ImGuiKey.F7] = (int)Keys.F7; - io.KeyMap[(int)ImGuiKey.F8] = (int)Keys.F8; - io.KeyMap[(int)ImGuiKey.F9] = (int)Keys.F9; - io.KeyMap[(int)ImGuiKey.F10] = (int)Keys.F10; - io.KeyMap[(int)ImGuiKey.F11] = (int)Keys.F11; - io.KeyMap[(int)ImGuiKey.F12] = (int)Keys.F12; - io.KeyMap[(int)ImGuiKey.Keypad0] = (int)Keys.KeyPad0; - io.KeyMap[(int)ImGuiKey.Keypad1] = (int)Keys.KeyPad1; - io.KeyMap[(int)ImGuiKey.Keypad2] = (int)Keys.KeyPad2; - io.KeyMap[(int)ImGuiKey.Keypad3] = (int)Keys.KeyPad3; - io.KeyMap[(int)ImGuiKey.Keypad4] = (int)Keys.KeyPad4; - io.KeyMap[(int)ImGuiKey.Keypad5] = (int)Keys.KeyPad5; - io.KeyMap[(int)ImGuiKey.Keypad6] = (int)Keys.KeyPad6; - io.KeyMap[(int)ImGuiKey.Keypad7] = (int)Keys.KeyPad7; - io.KeyMap[(int)ImGuiKey.Keypad8] = (int)Keys.KeyPad8; - io.KeyMap[(int)ImGuiKey.Keypad9] = (int)Keys.KeyPad9; - io.KeyMap[(int)ImGuiKey._0] = (int)Keys.D0; - io.KeyMap[(int)ImGuiKey._1] = (int)Keys.D1; - io.KeyMap[(int)ImGuiKey._2] = (int)Keys.D2; - io.KeyMap[(int)ImGuiKey._3] = (int)Keys.D3; - io.KeyMap[(int)ImGuiKey._4] = (int)Keys.D4; - io.KeyMap[(int)ImGuiKey._5] = (int)Keys.D5; - io.KeyMap[(int)ImGuiKey._6] = (int)Keys.D6; - io.KeyMap[(int)ImGuiKey._7] = (int)Keys.D7; - io.KeyMap[(int)ImGuiKey._8] = (int)Keys.D8; - io.KeyMap[(int)ImGuiKey._9] = (int)Keys.D9; - io.KeyMap[(int)ImGuiKey.A] = (int)Keys.A; - io.KeyMap[(int)ImGuiKey.B] = (int)Keys.B; - io.KeyMap[(int)ImGuiKey.C] = (int)Keys.C; - io.KeyMap[(int)ImGuiKey.D] = (int)Keys.D; - io.KeyMap[(int)ImGuiKey.E] = (int)Keys.E; - io.KeyMap[(int)ImGuiKey.F] = (int)Keys.F; - io.KeyMap[(int)ImGuiKey.G] = (int)Keys.G; - io.KeyMap[(int)ImGuiKey.H] = (int)Keys.H; - io.KeyMap[(int)ImGuiKey.I] = (int)Keys.I; - io.KeyMap[(int)ImGuiKey.J] = (int)Keys.J; - io.KeyMap[(int)ImGuiKey.K] = (int)Keys.K; - io.KeyMap[(int)ImGuiKey.L] = (int)Keys.L; - io.KeyMap[(int)ImGuiKey.M] = (int)Keys.M; - io.KeyMap[(int)ImGuiKey.N] = (int)Keys.N; - io.KeyMap[(int)ImGuiKey.O] = (int)Keys.O; - io.KeyMap[(int)ImGuiKey.P] = (int)Keys.P; - io.KeyMap[(int)ImGuiKey.Q] = (int)Keys.Q; - io.KeyMap[(int)ImGuiKey.R] = (int)Keys.R; - io.KeyMap[(int)ImGuiKey.S] = (int)Keys.S; - io.KeyMap[(int)ImGuiKey.T] = (int)Keys.T; - io.KeyMap[(int)ImGuiKey.U] = (int)Keys.U; - io.KeyMap[(int)ImGuiKey.V] = (int)Keys.V; - io.KeyMap[(int)ImGuiKey.W] = (int)Keys.W; - io.KeyMap[(int)ImGuiKey.X] = (int)Keys.X; - io.KeyMap[(int)ImGuiKey.Y] = (int)Keys.Y; - io.KeyMap[(int)ImGuiKey.Z] = (int)Keys.Z; - } - private void RenderImDrawData(ImDrawDataPtr draw_data) { if (draw_data.CmdListsCount == 0) @@ -440,7 +335,7 @@ private void RenderImDrawData(ImDrawDataPtr draw_data) GL.BindBuffer(BufferTarget.ArrayBuffer, _vertexBuffer); for (int i = 0; i < draw_data.CmdListsCount; i++) { - ImDrawListPtr cmd_list = draw_data.CmdListsRange[i]; + ImDrawListPtr cmd_list = draw_data.CmdLists[i]; int vertexSize = cmd_list.VtxBuffer.Size * Unsafe.SizeOf(); if (vertexSize > _vertexBufferSize) @@ -490,7 +385,7 @@ private void RenderImDrawData(ImDrawDataPtr draw_data) // Render command lists for (int n = 0; n < draw_data.CmdListsCount; n++) { - ImDrawListPtr cmd_list = draw_data.CmdListsRange[n]; + ImDrawListPtr cmd_list = draw_data.CmdLists[n]; GL.BufferSubData(BufferTarget.ArrayBuffer, IntPtr.Zero, cmd_list.VtxBuffer.Size * Unsafe.SizeOf(), cmd_list.VtxBuffer.Data); CheckGLError($"Data Vert {n}"); @@ -643,4 +538,71 @@ public static float GetDpiScale() { return Math.Max((float)(Screen.PrimaryScreen.Bounds.Width / SystemParameters.PrimaryScreenWidth), (float)(Screen.PrimaryScreen.Bounds.Height / SystemParameters.PrimaryScreenHeight)); } + + public static ImGuiKey TranslateKey(Keys key) + { + if (key is >= Keys.D0 and <= Keys.D9) + return key - Keys.D0 + ImGuiKey._0; + + if (key is >= Keys.A and <= Keys.Z) + return key - Keys.A + ImGuiKey.A; + + if (key is >= Keys.KeyPad0 and <= Keys.KeyPad9) + return key - Keys.KeyPad0 + ImGuiKey.Keypad0; + + if (key is >= Keys.F1 and <= Keys.F24) + return key - Keys.F1 + ImGuiKey.F24; + + return key switch + { + Keys.Tab => ImGuiKey.Tab, + Keys.Left => ImGuiKey.LeftArrow, + Keys.Right => ImGuiKey.RightArrow, + Keys.Up => ImGuiKey.UpArrow, + Keys.Down => ImGuiKey.DownArrow, + Keys.PageUp => ImGuiKey.PageUp, + Keys.PageDown => ImGuiKey.PageDown, + Keys.Home => ImGuiKey.Home, + Keys.End => ImGuiKey.End, + Keys.Insert => ImGuiKey.Insert, + Keys.Delete => ImGuiKey.Delete, + Keys.Backspace => ImGuiKey.Backspace, + Keys.Space => ImGuiKey.Space, + Keys.Enter => ImGuiKey.Enter, + Keys.Escape => ImGuiKey.Escape, + Keys.Apostrophe => ImGuiKey.Apostrophe, + Keys.Comma => ImGuiKey.Comma, + Keys.Minus => ImGuiKey.Minus, + Keys.Period => ImGuiKey.Period, + Keys.Slash => ImGuiKey.Slash, + Keys.Semicolon => ImGuiKey.Semicolon, + Keys.Equal => ImGuiKey.Equal, + Keys.LeftBracket => ImGuiKey.LeftBracket, + Keys.Backslash => ImGuiKey.Backslash, + Keys.RightBracket => ImGuiKey.RightBracket, + Keys.GraveAccent => ImGuiKey.GraveAccent, + Keys.CapsLock => ImGuiKey.CapsLock, + Keys.ScrollLock => ImGuiKey.ScrollLock, + Keys.NumLock => ImGuiKey.NumLock, + Keys.PrintScreen => ImGuiKey.PrintScreen, + Keys.Pause => ImGuiKey.Pause, + Keys.KeyPadDecimal => ImGuiKey.KeypadDecimal, + Keys.KeyPadDivide => ImGuiKey.KeypadDivide, + Keys.KeyPadMultiply => ImGuiKey.KeypadMultiply, + Keys.KeyPadSubtract => ImGuiKey.KeypadSubtract, + Keys.KeyPadAdd => ImGuiKey.KeypadAdd, + Keys.KeyPadEnter => ImGuiKey.KeypadEnter, + Keys.KeyPadEqual => ImGuiKey.KeypadEqual, + Keys.LeftShift => ImGuiKey.LeftShift, + Keys.LeftControl => ImGuiKey.LeftCtrl, + Keys.LeftAlt => ImGuiKey.LeftAlt, + Keys.LeftSuper => ImGuiKey.LeftSuper, + Keys.RightShift => ImGuiKey.RightShift, + Keys.RightControl => ImGuiKey.RightCtrl, + Keys.RightAlt => ImGuiKey.RightAlt, + Keys.RightSuper => ImGuiKey.RightSuper, + Keys.Menu => ImGuiKey.Menu, + _ => ImGuiKey.None + }; + } } diff --git a/FModel/Helper.cs b/FModel/Helper.cs index 67cd659f..c7a43c50 100644 --- a/FModel/Helper.cs +++ b/FModel/Helper.cs @@ -1,30 +1,32 @@ -using System; +using System; using System.Linq; -using System.Runtime.InteropServices; +using System.Runtime.CompilerServices; using System.Windows; namespace FModel; public static class Helper { - [StructLayout(LayoutKind.Explicit)] - private struct NanUnion - { - [FieldOffset(0)] - internal double DoubleValue; - [FieldOffset(0)] - internal readonly ulong UlongValue; - } - public static string FixKey(string key) { if (string.IsNullOrEmpty(key)) return string.Empty; - if (key.StartsWith("0x")) - key = key[2..]; + var keySpan = key.AsSpan().Trim(); + if (keySpan.Length > sizeof(char) * (2 /* 0x */ + 32 /* FAES = 256 bit */)) // maybe strictly check for length? + return string.Empty; // bullshit key + + Span resultSpan = stackalloc char[keySpan.Length + 2 /* pad for 0x */]; + keySpan.ToUpperInvariant(resultSpan[2..]); + + if (resultSpan[2..].StartsWith("0X")) + resultSpan = resultSpan[2..]; + else + resultSpan[0] = '0'; + + resultSpan[1] = 'x'; - return "0x" + key.ToUpper().Trim(); + return new string(resultSpan); } public static void OpenWindow(string windowName, Action action) where T : Window @@ -74,9 +76,9 @@ private static Window GetOpenedWindow(string name) where T : Window public static bool IsNaN(double value) { - var t = new NanUnion { DoubleValue = value }; - var exp = t.UlongValue & 0xfff0000000000000; - var man = t.UlongValue & 0x000fffffffffffff; + var ulongValue = Unsafe.As(ref value); + var exp = ulongValue & 0xfff0000000000000; + var man = ulongValue & 0x000fffffffffffff; return exp is 0x7ff0000000000000 or 0xfff0000000000000 && man != 0; } @@ -96,13 +98,17 @@ public static bool AreVirtuallyEqual(double d1, double d2) return -d < n && d > n; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static float DegreesToRadians(float degrees) { - return MathF.PI / 180f * degrees; + const float ratio = MathF.PI / 180f; + return ratio * degrees; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static float RadiansToDegrees(float radians) { - return radians* 180f / MathF.PI; + const float ratio = 180f / MathF.PI; + return radians * ratio; } } diff --git a/FModel/MainWindow.xaml b/FModel/MainWindow.xaml index b9952ffd..b574a4f2 100644 --- a/FModel/MainWindow.xaml +++ b/FModel/MainWindow.xaml @@ -321,16 +321,16 @@ - - - - - - - - - - + + + + + + + + + + @@ -358,7 +358,7 @@ - + @@ -367,7 +367,7 @@ - + @@ -532,7 +532,7 @@ - + @@ -547,7 +547,7 @@ - + diff --git a/FModel/MainWindow.xaml.cs b/FModel/MainWindow.xaml.cs index 449f04a0..7714cd4e 100644 --- a/FModel/MainWindow.xaml.cs +++ b/FModel/MainWindow.xaml.cs @@ -47,7 +47,7 @@ private async void OnLoaded(object sender, RoutedEventArgs e) { var newOrUpdated = UserSettings.Default.ShowChangelog; #if !DEBUG - ApplicationService.ApiEndpointView.FModelApi.CheckForUpdates(UserSettings.Default.UpdateMode); + ApplicationService.ApiEndpointView.FModelApi.CheckForUpdates(UserSettings.Default.UpdateMode, true); #endif switch (UserSettings.Default.AesReload) @@ -61,6 +61,8 @@ private async void OnLoaded(object sender, RoutedEventArgs e) break; } + await ApplicationViewModel.InitOodle(); + await ApplicationViewModel.InitZlib(); await _applicationView.CUE4Parse.Initialize(); await _applicationView.AesManager.InitAes(); await _applicationView.UpdateProvider(true); @@ -69,12 +71,10 @@ private async void OnLoaded(object sender, RoutedEventArgs e) #endif await Task.WhenAll( _applicationView.CUE4Parse.VerifyConsoleVariables(), - _applicationView.CUE4Parse.VerifyVirtualCache(), - _applicationView.CUE4Parse.VerifyContentBuildManifest(), + _applicationView.CUE4Parse.VerifyOnDemandArchives(), _applicationView.CUE4Parse.InitMappings(), - _applicationView.InitImGuiSettings(newOrUpdated), - _applicationView.InitVgmStream(), - _applicationView.InitOodle(), + ApplicationViewModel.InitVgmStream(), + ApplicationViewModel.InitImGuiSettings(newOrUpdated), Task.Run(() => { if (UserSettings.Default.DiscordRpc == EDiscordRpc.Always) @@ -83,12 +83,12 @@ await Task.WhenAll( ).ConfigureAwait(false); #if DEBUG - await _threadWorkerView.Begin(cancellationToken => - _applicationView.CUE4Parse.Extract(cancellationToken, - "fortnitegame/Content/Characters/Player/Female/Medium/Bodies/F_Med_Soldier_01/Meshes/F_Med_Soldier_01.uasset")); - await _threadWorkerView.Begin(cancellationToken => - _applicationView.CUE4Parse.Extract(cancellationToken, - "fortnitegame/Content/Animation/Game/MainPlayer/Emotes/Cowbell/Cowbell_CMM_Loop_M.uasset")); + // await _threadWorkerView.Begin(cancellationToken => + // _applicationView.CUE4Parse.Extract(cancellationToken, + // "fortnitegame/Content/Characters/Player/Female/Large/Bodies/F_LRG_BunnyBR/Meshes/F_LRG_BunnyBR.uasset")); + // await _threadWorkerView.Begin(cancellationToken => + // _applicationView.CUE4Parse.Extract(cancellationToken, + // "FortniteGame/Content/Environments/Helios/Props/GlacierHotel/GlacierHotel_Globe_A/Meshes/SM_GlacierHotel_Globe_A.uasset")); #endif } diff --git a/FModel/Resources/collision.vert b/FModel/Resources/collision.vert new file mode 100644 index 00000000..b2036c3c --- /dev/null +++ b/FModel/Resources/collision.vert @@ -0,0 +1,20 @@ +#version 460 core + +layout (location = 0) in vec3 vPos; + +uniform mat4 uView; +uniform mat4 uProjection; +uniform mat4 uInstanceMatrix; +uniform mat4 uCollisionMatrix; +uniform float uScaleDown; + +out vec3 fPos; +out vec3 fColor; + +void main() +{ + gl_PointSize = 7.5f; + gl_Position = uProjection * uView * uInstanceMatrix * uCollisionMatrix * vec4(vPos.xzy * uScaleDown, 1.0); + fPos = vec3(uInstanceMatrix * uCollisionMatrix * vec4(vPos.xzy * uScaleDown, 1.0)); + fColor = vec3(1.0); +} diff --git a/FModel/Resources/cube.png b/FModel/Resources/cube.png new file mode 100644 index 00000000..00766874 Binary files /dev/null and b/FModel/Resources/cube.png differ diff --git a/FModel/Resources/cube_off.png b/FModel/Resources/cube_off.png new file mode 100644 index 00000000..eae850fa Binary files /dev/null and b/FModel/Resources/cube_off.png differ diff --git a/FModel/Resources/default.frag b/FModel/Resources/default.frag index 29b9f84d..e41a6b9b 100644 --- a/FModel/Resources/default.frag +++ b/FModel/Resources/default.frag @@ -8,7 +8,7 @@ in vec3 fPos; in vec3 fNormal; in vec3 fTangent; in vec2 fTexCoords; -in float fTexLayer; +flat in int fTexLayer; in vec4 fColor; struct Texture @@ -89,6 +89,7 @@ uniform Parameters uParameters; uniform Light uLights[MAX_LIGHT_COUNT]; uniform int uNumLights; uniform int uUvCount; +uniform float uOpacity; uniform bool uHasVertexColors; uniform vec3 uSectionColor; uniform bool bVertexColors[6]; @@ -98,7 +99,7 @@ out vec4 FragColor; int LayerToIndex() { - return clamp(int(fTexLayer), 0, uUvCount - 1); + return clamp(fTexLayer, 0, uUvCount - 1); } vec4 SamplerToVector(sampler2D s, vec2 coords) @@ -148,6 +149,11 @@ vec3 CalcLight(int layer, vec3 normals, vec3 position, vec3 color, float attenua { vec3 fLambert = SamplerToVector(uParameters.Diffuse[layer].Sampler).rgb * uParameters.Diffuse[layer].Color.rgb; vec3 specular_masks = SamplerToVector(uParameters.SpecularMasks[layer].Sampler).rgb; + float cavity = specular_masks.g; + if (uParameters.HasAo) + { + cavity = SamplerToVector(uParameters.Ao.Sampler).g; + } float roughness = mix(uParameters.RoughnessMin, uParameters.RoughnessMax, specular_masks.b); vec3 l = normalize(uViewPos - fPos); @@ -165,7 +171,7 @@ vec3 CalcLight(int layer, vec3 normals, vec3 position, vec3 color, float attenua vec3 kS = f; vec3 kD = 1.0 - kS; - kD *= 1.0 - specular_masks.g; + kD *= 1.0 - cavity; vec3 specBrdfNom = ggxDistribution(roughness, nDotH) * geomSmith(roughness, nDotL) * geomSmith(roughness, nDotV) * f; float specBrdfDenom = 4.0 * nDotV * nDotL + 0.0001; @@ -212,30 +218,32 @@ vec3 CalcSpotLight(int layer, vec3 normals, Light light) void main() { + int layer = LayerToIndex(); + vec3 normals = ComputeNormals(layer); + vec3 lightDir = normalize(uViewPos - fPos); + float diffuseFactor = max(dot(normals, lightDir), 0.4); + if (bVertexColors[1]) { - FragColor = vec4(uSectionColor, 1.0); + FragColor = vec4(diffuseFactor * uSectionColor, uOpacity); } else if (bVertexColors[2] && uHasVertexColors) { - FragColor = fColor; + FragColor = vec4(diffuseFactor * fColor.rgb, fColor.a); } else if (bVertexColors[3]) { - int layer = LayerToIndex(); - vec3 normals = ComputeNormals(layer); - FragColor = vec4(normals, 1.0); + FragColor = vec4(normals, uOpacity); } else if (bVertexColors[4]) { - FragColor = SamplerToVector(uParameters.Diffuse[0].Sampler); + vec4 diffuse = SamplerToVector(uParameters.Diffuse[0].Sampler); + FragColor = vec4(diffuseFactor * diffuse.rgb, diffuse.a); } else { - int layer = LayerToIndex(); - vec3 normals = ComputeNormals(layer); vec4 diffuse = SamplerToVector(uParameters.Diffuse[layer].Sampler); - vec3 result = uParameters.Diffuse[layer].Color.rgb * diffuse.rgb; + vec3 result = diffuseFactor * diffuse.rgb * uParameters.Diffuse[layer].Color.rgb; if (uParameters.HasAo) { @@ -245,7 +253,7 @@ void main() vec3 color = uParameters.Ao.ColorBoost.Color * uParameters.Ao.ColorBoost.Exponent; result = mix(result, result * color, m.b); } - result = vec3(uParameters.Ao.AmbientOcclusion) * result * m.r; + result *= m.r; } vec2 coords = fTexCoords; @@ -263,7 +271,7 @@ void main() } { - result += CalcLight(layer, normals, uViewPos, vec3(0.75), 1.0, false); + result += CalcLight(layer, normals, uViewPos, vec3(1.0), 1.0, false); vec3 lights = vec3(uNumLights > 0 ? 0 : 1); for (int i = 0; i < uNumLights; i++) @@ -281,6 +289,6 @@ void main() } result = result / (result + vec3(1.0)); - FragColor = vec4(pow(result, vec3(1.0 / 2.2)), 1.0); + FragColor = vec4(pow(result, vec3(1.0 / 2.2)), uOpacity); } } diff --git a/FModel/Resources/default.vert b/FModel/Resources/default.vert index 3d530a5b..9f396843 100644 --- a/FModel/Resources/default.vert +++ b/FModel/Resources/default.vert @@ -4,10 +4,10 @@ layout (location = 1) in vec3 vPos; layout (location = 2) in vec3 vNormal; layout (location = 3) in vec3 vTangent; layout (location = 4) in vec2 vTexCoords; -layout (location = 5) in float vTexLayer; -layout (location = 6) in vec4 vColor; -layout (location = 7) in vec4 vBoneIds; -layout (location = 8) in vec4 vBoneWeights; +layout (location = 5) in int vTexLayer; +layout (location = 6) in float vColor; +layout (location = 7) in vec4 vBoneInfluence; +layout (location = 8) in vec4 vBoneInfluenceExtra; layout (location = 9) in mat4 vInstanceMatrix; layout (location = 13) in vec3 vMorphTargetPos; layout (location = 14) in vec3 vMorphTargetTangent; @@ -30,9 +30,23 @@ out vec3 fPos; out vec3 fNormal; out vec3 fTangent; out vec2 fTexCoords; -out float fTexLayer; +flat out int fTexLayer; out vec4 fColor; +vec4 unpackARGB(int color) +{ + float a = float((color >> 24) & 0xFF); + float r = float((color >> 16) & 0xFF); + float g = float((color >> 8) & 0xFF); + float b = float((color >> 0) & 0xFF); + return vec4(r, g, b, a); +} + +vec2 unpackBoneIDsAndWeights(int packedData) +{ + return vec2(float((packedData >> 16) & 0xFFFF), float(packedData & 0xFFFF)); +} + void main() { vec4 bindPos = vec4(mix(vPos, vMorphTargetPos, uMorphTime), 1.0); @@ -44,19 +58,28 @@ void main() vec4 finalTangent = vec4(0.0); if (uIsAnimated) { - for(int i = 0 ; i < 4; i++) + vec4 boneInfluences[2]; + boneInfluences[0] = vBoneInfluence; + boneInfluences[1] = vBoneInfluenceExtra; + for (int i = 0; i < 2; i++) { - int boneIndex = int(vBoneIds[i]); - if(boneIndex < 0) break; + for(int j = 0 ; j < 4; j++) + { + vec2 boneInfluence = unpackBoneIDsAndWeights(int(boneInfluences[i][j])); + int boneIndex = int(boneInfluence.x); + float weight = boneInfluence.y; - mat4 boneMatrix = uFinalBonesMatrix[boneIndex] * inverse(uRestBonesMatrix[boneIndex]); - mat4 inverseBoneMatrix = transpose(inverse(boneMatrix)); - float weight = vBoneWeights[i]; + mat4 boneMatrix = uFinalBonesMatrix[boneIndex] * inverse(uRestBonesMatrix[boneIndex]); + mat4 inverseBoneMatrix = transpose(inverse(boneMatrix)); - finalPos += boneMatrix * bindPos * weight; - finalNormal += inverseBoneMatrix * bindNormal * weight; - finalTangent += inverseBoneMatrix * bindTangent * weight; + finalPos += boneMatrix * bindPos * weight; + finalNormal += inverseBoneMatrix * bindNormal * weight; + finalTangent += inverseBoneMatrix * bindTangent * weight; + } } + finalPos = normalize(finalPos); + finalNormal = normalize(finalNormal); + finalTangent = normalize(finalTangent); } else { @@ -72,5 +95,5 @@ void main() fTangent = vec3(transpose(inverse(vInstanceMatrix)) * finalTangent); fTexCoords = vTexCoords; fTexLayer = vTexLayer; - fColor = vColor; + fColor = unpackARGB(int(vColor)) / 255.0; } diff --git a/FModel/Resources/light.png b/FModel/Resources/light.png new file mode 100644 index 00000000..cdce6934 Binary files /dev/null and b/FModel/Resources/light.png differ diff --git a/FModel/Resources/light_off.png b/FModel/Resources/light_off.png new file mode 100644 index 00000000..5e2d5890 Binary files /dev/null and b/FModel/Resources/light_off.png differ diff --git a/FModel/Resources/outline.vert b/FModel/Resources/outline.vert index ffb85c6c..396b6b14 100644 --- a/FModel/Resources/outline.vert +++ b/FModel/Resources/outline.vert @@ -2,8 +2,8 @@ layout (location = 1) in vec3 vPos; layout (location = 2) in vec3 vNormal; -layout (location = 7) in vec4 vBoneIds; -layout (location = 8) in vec4 vBoneWeights; +layout (location = 7) in vec4 vBoneInfluence; +layout (location = 8) in vec4 vBoneInfluenceExtra; layout (location = 9) in mat4 vInstanceMatrix; layout (location = 13) in vec3 vMorphTargetPos; @@ -22,6 +22,11 @@ uniform mat4 uProjection; uniform float uMorphTime; uniform bool uIsAnimated; +vec2 unpackBoneIDsAndWeights(int packedData) +{ + return vec2(float((packedData >> 16) & 0xFFFF), float(packedData & 0xFFFF)); +} + void main() { vec4 bindPos = vec4(mix(vPos, vMorphTargetPos, uMorphTime), 1.0); @@ -31,17 +36,25 @@ void main() vec4 finalNormal = vec4(0.0); if (uIsAnimated) { - for(int i = 0 ; i < 4; i++) + vec4 boneInfluences[2]; + boneInfluences[0] = vBoneInfluence; + boneInfluences[1] = vBoneInfluenceExtra; + for(int i = 0 ; i < 2; i++) { - int boneIndex = int(vBoneIds[i]); - if(boneIndex < 0) break; + for(int j = 0; j < 4; j++) + { + vec2 boneInfluence = unpackBoneIDsAndWeights(int(boneInfluences[i][j])); + int boneIndex = int(boneInfluence.x); + float weight = boneInfluence.y; - mat4 boneMatrix = uFinalBonesMatrix[boneIndex] * inverse(uRestBonesMatrix[boneIndex]); - float weight = vBoneWeights[i]; + mat4 boneMatrix = uFinalBonesMatrix[boneIndex] * inverse(uRestBonesMatrix[boneIndex]); - finalPos += boneMatrix * bindPos * weight; - finalNormal += transpose(inverse(boneMatrix)) * bindNormal * weight; + finalPos += boneMatrix * bindPos * weight; + finalNormal += transpose(inverse(boneMatrix)) * bindNormal * weight; + } } + finalPos = normalize(finalPos); + finalNormal = normalize(finalNormal); } else { @@ -49,10 +62,10 @@ void main() finalNormal = bindNormal; } - finalPos = vInstanceMatrix * finalPos; - float scaleFactor = distance(vec3(finalPos), uViewPos) * 0.0035; - vec4 nor = transpose(inverse(vInstanceMatrix)) * normalize(finalNormal) * scaleFactor; + vec4 worldPos = vInstanceMatrix * finalPos; + float scaleFactor = length(uViewPos - worldPos.xyz) * 0.0035; + vec4 nor = transpose(inverse(vInstanceMatrix)) * finalNormal * scaleFactor; finalPos.xyz += nor.xyz; - gl_Position = uProjection * uView * finalPos; + gl_Position = uProjection * uView * vInstanceMatrix * finalPos; } diff --git a/FModel/Resources/picking.vert b/FModel/Resources/picking.vert index 7d267c0c..15194254 100644 --- a/FModel/Resources/picking.vert +++ b/FModel/Resources/picking.vert @@ -1,8 +1,8 @@ #version 460 core layout (location = 1) in vec3 vPos; -layout (location = 7) in vec4 vBoneIds; -layout (location = 8) in vec4 vBoneWeights; +layout (location = 7) in vec4 vBoneInfluence; +layout (location = 8) in vec4 vBoneInfluenceExtra; layout (location = 9) in mat4 vInstanceMatrix; layout (location = 13) in vec3 vMorphTargetPos; @@ -20,6 +20,11 @@ uniform mat4 uProjection; uniform float uMorphTime; uniform bool uIsAnimated; +vec2 unpackBoneIDsAndWeights(int packedData) +{ + return vec2(float((packedData >> 16) & 0xFFFF), float(packedData & 0xFFFF)); +} + void main() { vec4 bindPos = vec4(mix(vPos, vMorphTargetPos, uMorphTime), 1.0); @@ -27,12 +32,19 @@ void main() vec4 finalPos = vec4(0.0); if (uIsAnimated) { - for(int i = 0 ; i < 4; i++) + vec4 boneInfluences[2]; + boneInfluences[0] = vBoneInfluence; + boneInfluences[1] = vBoneInfluenceExtra; + for(int i = 0 ; i < 2; i++) { - int boneIndex = int(vBoneIds[i]); - if(boneIndex < 0) break; + for(int j = 0; j < 4; j++) + { + vec2 boneInfluence = unpackBoneIDsAndWeights(int(boneInfluences[i][j])); + int boneIndex = int(boneInfluence.x); + float weight = boneInfluence.y; - finalPos += uFinalBonesMatrix[boneIndex] * inverse(uRestBonesMatrix[boneIndex]) * bindPos * vBoneWeights[i]; + finalPos += uFinalBonesMatrix[boneIndex] * inverse(uRestBonesMatrix[boneIndex]) * bindPos * weight; + } } } else finalPos = bindPos; diff --git a/FModel/Resources/square.png b/FModel/Resources/square.png new file mode 100644 index 00000000..1edb68e8 Binary files /dev/null and b/FModel/Resources/square.png differ diff --git a/FModel/Resources/square_off.png b/FModel/Resources/square_off.png new file mode 100644 index 00000000..cd743702 Binary files /dev/null and b/FModel/Resources/square_off.png differ diff --git a/FModel/Settings/CustomDirectory.cs b/FModel/Settings/CustomDirectory.cs index 471cb93f..14b4a388 100644 --- a/FModel/Settings/CustomDirectory.cs +++ b/FModel/Settings/CustomDirectory.cs @@ -13,9 +13,9 @@ public static IList Default(string gameName) case "Fortnite [LIVE]": return new List { - new("Cosmetics", "FortniteGame/Content/Athena/Items/Cosmetics/"), - new("Emotes [AUDIO]", "FortniteGame/Content/Athena/Sounds/Emotes/"), - new("Music Packs [AUDIO]", "FortniteGame/Content/Athena/Sounds/MusicPacks/"), + new("Cosmetics", "FortniteGame/Plugins/GameFeatures/BRCosmetics/Content/Athena/Items/Cosmetics/"), + new("Emotes [AUDIO]", "FortniteGame/Plugins/GameFeatures/BRCosmetics/Content/Athena/Sounds/Emotes/"), + new("Music Packs [AUDIO]", "FortniteGame/Plugins/GameFeatures/BRCosmetics/Content/Athena/Sounds/MusicPacks/"), new("Weapons", "FortniteGame/Content/Athena/Items/Weapons/"), new("Strings", "FortniteGame/Content/Localization/") }; diff --git a/FModel/Settings/EndpointSettings.cs b/FModel/Settings/EndpointSettings.cs index 24f1960b..a84c6867 100644 --- a/FModel/Settings/EndpointSettings.cs +++ b/FModel/Settings/EndpointSettings.cs @@ -1,110 +1,109 @@ -using System.Linq; -using FModel.Framework; -using FModel.ViewModels.ApiEndpoints; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace FModel.Settings; - -public class EndpointSettings : ViewModel -{ - public static EndpointSettings[] Default(string gameName) - { - switch (gameName) - { - case "Fortnite": - case "Fortnite [LIVE]": - return new EndpointSettings[] - { - new("https://fortnitecentral.genxgames.gg/api/v1/aes", "$.['mainKey','dynamicKeys']"), - // new("https://fortnitecentral.genxgames.gg/api/v1/mappings", "$.[?(@.meta.compressionMethod=='Oodle')].['url','fileName']") - new("https://fortnitecentral.genxgames.gg/api/v1/mappings", "$.[0].['url','fileName']") // just get the first available, not just oodle! (Unfortunately not default except when resetting settings) - }; - default: - return new EndpointSettings[] { new(), new() }; - } - } - - private string _url; - public string Url - { - get => _url; - set => SetProperty(ref _url, value); - } - - private string _path; - public string Path - { - get => _path; - set => SetProperty(ref _path, value); - } - - private bool _overwrite; - public bool Overwrite - { - get => _overwrite; - set => SetProperty(ref _overwrite, value); - } - - private string _filePath; - public string FilePath - { - get => _filePath; - set => SetProperty(ref _filePath, value); - } - - private bool _isValid; - public bool IsValid - { - get => _isValid; - set - { - SetProperty(ref _isValid, value); - RaisePropertyChanged(nameof(Label)); - } - } - - [JsonIgnore] - public string Label => IsValid ? - "Your endpoint configuration is valid! Please, avoid any unnecessary modifications!" : - "Your endpoint configuration DOES NOT seem to be valid yet! Please, test it out!"; - - public EndpointSettings() {} - public EndpointSettings(string url, string path) - { - Url = url; - Path = path; - IsValid = !string.IsNullOrEmpty(url) && !string.IsNullOrEmpty(path); // be careful with this - } - - public void TryValidate(DynamicApiEndpoint endpoint, EEndpointType type, out JToken response) - { - response = null; - if (string.IsNullOrEmpty(Url) || string.IsNullOrEmpty(Path)) - { - IsValid = false; - } - else switch (type) - { - case EEndpointType.Aes: - { - var r = endpoint.GetAesKeys(default, Url, Path); - response = JToken.FromObject(r); - IsValid = r.IsValid; - break; - } - case EEndpointType.Mapping: - { - var r = endpoint.GetMappings(default, Url, Path); - response = JToken.FromObject(r); - IsValid = r.Any(x => x.IsValid); - break; - } - default: - { - IsValid = false; - break; - } - } - } -} +using System.Linq; +using FModel.Framework; +using FModel.ViewModels.ApiEndpoints; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace FModel.Settings; + +public class EndpointSettings : ViewModel +{ + public static EndpointSettings[] Default(string gameName) + { + switch (gameName) + { + case "Fortnite": + case "Fortnite [LIVE]": + return new EndpointSettings[] + { + new("https://fortnitecentral.genxgames.gg/api/v1/aes", "$.['mainKey','dynamicKeys']"), + new("https://fortnitecentral.genxgames.gg/api/v1/mappings", "$.[0].['url','fileName']") // just get the first available, not just oodle! (Unfortunately not default except when resetting settings) + }; + default: + return new EndpointSettings[] { new(), new() }; + } + } + + private string _url; + public string Url + { + get => _url; + set => SetProperty(ref _url, value); + } + + private string _path; + public string Path + { + get => _path; + set => SetProperty(ref _path, value); + } + + private bool _overwrite; + public bool Overwrite + { + get => _overwrite; + set => SetProperty(ref _overwrite, value); + } + + private string _filePath; + public string FilePath + { + get => _filePath; + set => SetProperty(ref _filePath, value); + } + + private bool _isValid; + public bool IsValid + { + get => _isValid; + set + { + SetProperty(ref _isValid, value); + RaisePropertyChanged(nameof(Label)); + } + } + + [JsonIgnore] + public string Label => IsValid ? + "Your endpoint configuration is valid! Please, avoid any unnecessary modifications!" : + "Your endpoint configuration DOES NOT seem to be valid yet! Please, test it out!"; + + public EndpointSettings() {} + public EndpointSettings(string url, string path) + { + Url = url; + Path = path; + IsValid = !string.IsNullOrEmpty(url) && !string.IsNullOrEmpty(path); // be careful with this + } + + public void TryValidate(DynamicApiEndpoint endpoint, EEndpointType type, out JToken response) + { + response = null; + if (string.IsNullOrEmpty(Url) || string.IsNullOrEmpty(Path)) + { + IsValid = false; + } + else switch (type) + { + case EEndpointType.Aes: + { + var r = endpoint.GetAesKeys(default, Url, Path); + response = JToken.FromObject(r); + IsValid = r.IsValid; + break; + } + case EEndpointType.Mapping: + { + var r = endpoint.GetMappings(default, Url, Path); + response = JToken.FromObject(r); + IsValid = r.Any(x => x.IsValid); + break; + } + default: + { + IsValid = false; + break; + } + } + } +} \ No newline at end of file diff --git a/FModel/Settings/UserSettings.cs b/FModel/Settings/UserSettings.cs index 5dc4eb85..b5eb8e3e 100644 --- a/FModel/Settings/UserSettings.cs +++ b/FModel/Settings/UserSettings.cs @@ -3,9 +3,12 @@ using System.IO; using System.Windows; using System.Windows.Input; +using CUE4Parse_Conversion; +using CUE4Parse_Conversion.Animations; using CUE4Parse.UE4.Versions; using CUE4Parse_Conversion.Meshes; using CUE4Parse_Conversion.Textures; +using CUE4Parse_Conversion.UEFormat.Enums; using CUE4Parse.UE4.Assets.Exports.Material; using FModel.Framework; using FModel.ViewModels; @@ -47,6 +50,25 @@ public static bool IsEndpointValid(EEndpointType type, out EndpointSettings endp return endpoint.Overwrite || endpoint.IsValid; } + [JsonIgnore] + public ExporterOptions ExportOptions => new() + { + LodFormat = Default.LodExportFormat, + MeshFormat = Default.MeshExportFormat, + AnimFormat = Default.MeshExportFormat switch + { + EMeshFormat.UEFormat => EAnimFormat.UEFormat, + _ => EAnimFormat.ActorX + }, + MaterialFormat = Default.MaterialExportFormat, + TextureFormat = Default.TextureExportFormat, + SocketFormat = Default.SocketExportFormat, + CompressionFormat = Default.CompressionFormat, + Platform = Default.CurrentDir.TexturePlatform, + ExportMorphTargets = Default.SaveMorphTargets, + ExportMaterials = Default.SaveEmbeddedMaterials + }; + private bool _showChangelog = true; public bool ShowChangelog { @@ -159,6 +181,13 @@ public EUpdateMode UpdateMode set => SetProperty(ref _updateMode, value); } + private string _commitHash = Constants.APP_VERSION; + public string CommitHash + { + get => _commitHash; + set => SetProperty(ref _commitHash, value); + } + private bool _keepDirectoryStructure = true; public bool KeepDirectoryStructure { @@ -231,6 +260,8 @@ public IDictionary PerDirectory [JsonIgnore] public DirectorySettings CurrentDir { get; set; } + [JsonIgnore] + public string ShortCommitHash => CommitHash[..7]; /// /// TO DELETEEEEEEEEEEEEE @@ -347,6 +378,13 @@ public ESocketFormat SocketExportFormat set => SetProperty(ref _socketExportFormat, value); } + private EFileCompressionFormat _compressionFormat = EFileCompressionFormat.ZSTD; + public EFileCompressionFormat CompressionFormat + { + get => _compressionFormat; + set => SetProperty(ref _compressionFormat, value); + } + private ELodFormat _lodExportFormat = ELodFormat.FirstLod; public ELodFormat LodExportFormat { diff --git a/FModel/ViewModels/AesManagerViewModel.cs b/FModel/ViewModels/AesManagerViewModel.cs index a3fa522d..c581d3cd 100644 --- a/FModel/ViewModels/AesManagerViewModel.cs +++ b/FModel/ViewModels/AesManagerViewModel.cs @@ -8,7 +8,6 @@ using FModel.Services; using FModel.Settings; using FModel.ViewModels.ApiEndpoints.Models; -using Serilog; namespace FModel.ViewModels; diff --git a/FModel/ViewModels/ApiEndpoints/EpicApiEndpoint.cs b/FModel/ViewModels/ApiEndpoints/EpicApiEndpoint.cs index e09390a2..6b1eeea2 100644 --- a/FModel/ViewModels/ApiEndpoints/EpicApiEndpoint.cs +++ b/FModel/ViewModels/ApiEndpoints/EpicApiEndpoint.cs @@ -1,10 +1,14 @@ using System.Threading; using System.Threading.Tasks; -using EpicManifestParser.Objects; + +using EpicManifestParser.Api; + using FModel.Framework; using FModel.Settings; using FModel.ViewModels.ApiEndpoints.Models; + using RestSharp; + using Serilog; namespace FModel.ViewModels.ApiEndpoints; @@ -14,29 +18,26 @@ public class EpicApiEndpoint : AbstractApiProvider private const string _OAUTH_URL = "https://account-public-service-prod03.ol.epicgames.com/account/api/oauth/token"; private const string _BASIC_TOKEN = "basic MzQ0NmNkNzI2OTRjNGE0NDg1ZDgxYjc3YWRiYjIxNDE6OTIwOWQ0YTVlMjVhNDU3ZmI5YjA3NDg5ZDMxM2I0MWE="; private const string _APP_URL = "https://launcher-public-service-prod06.ol.epicgames.com/launcher/api/public/assets/v2/platform/Windows/namespace/fn/catalogItem/4fe75bbc5a674f4f9b356b5c90567da5/app/Fortnite/label/Live"; - private const string _CBM_URL = "https://launcher-public-service-prod06.ol.epicgames.com/launcher/api/public/assets/Windows/5cb97847cee34581afdbc445400e2f77/FortniteContentBuilds"; public EpicApiEndpoint(RestClient client) : base(client) { } public async Task GetManifestAsync(CancellationToken token) { - if (await IsExpired().ConfigureAwait(false)) - { - var auth = await GetAuthAsync(token).ConfigureAwait(false); - if (auth != null) - { - UserSettings.Default.LastAuthResponse = auth; - } - } + await VerifyAuth(token).ConfigureAwait(false); var request = new FRestRequest(_APP_URL); request.AddHeader("Authorization", $"bearer {UserSettings.Default.LastAuthResponse.AccessToken}"); var response = await _client.ExecuteAsync(request, token).ConfigureAwait(false); Log.Information("[{Method}] [{Status}({StatusCode})] '{Resource}'", request.Method, response.StatusDescription, (int) response.StatusCode, response.ResponseUri?.OriginalString); - return response.IsSuccessful ? new ManifestInfo(response.Content) : null; + return response.IsSuccessful ? ManifestInfo.Deserialize(response.RawBytes) : null; } - public async Task GetContentBuildManifestAsync(CancellationToken token, string label) + public ManifestInfo GetManifest(CancellationToken token) + { + return GetManifestAsync(token).GetAwaiter().GetResult(); + } + + public async Task VerifyAuth(CancellationToken token) { if (await IsExpired().ConfigureAwait(false)) { @@ -46,23 +47,6 @@ public async Task GetContentBuildManifestAsync(Cancell UserSettings.Default.LastAuthResponse = auth; } } - - var request = new FRestRequest(_CBM_URL); - request.AddHeader("Authorization", $"bearer {UserSettings.Default.LastAuthResponse.AccessToken}"); - request.AddQueryParameter("label", label); - var response = await _client.ExecuteAsync(request, token).ConfigureAwait(false); - Log.Information("[{Method}] [{Status}({StatusCode})] '{Resource}'", request.Method, response.StatusDescription, (int) response.StatusCode, response.ResponseUri?.OriginalString); - return response.IsSuccessful ? new ContentBuildManifestInfo(response.Content) : null; - } - - public ManifestInfo GetManifest(CancellationToken token) - { - return GetManifestAsync(token).GetAwaiter().GetResult(); - } - - public ContentBuildManifestInfo GetContentBuildManifest(CancellationToken token, string label) - { - return GetContentBuildManifestAsync(token, label).GetAwaiter().GetResult(); } private async Task GetAuthAsync(CancellationToken token) diff --git a/FModel/ViewModels/ApiEndpoints/FModelApiEndpoint.cs b/FModel/ViewModels/ApiEndpoints/FModelApiEndpoint.cs index 0e1b1f85..1fbe18f0 100644 --- a/FModel/ViewModels/ApiEndpoints/FModelApiEndpoint.cs +++ b/FModel/ViewModels/ApiEndpoints/FModelApiEndpoint.cs @@ -116,10 +116,13 @@ public CommunityDesign GetDesign(string designName) return communityDesign; } - public void CheckForUpdates(EUpdateMode updateMode) + public void CheckForUpdates(EUpdateMode updateMode, bool launch = false) { - AutoUpdater.ParseUpdateInfoEvent += ParseUpdateInfoEvent; - AutoUpdater.CheckForUpdateEvent += CheckForUpdateEvent; + if (launch) + { + AutoUpdater.ParseUpdateInfoEvent += ParseUpdateInfoEvent; + AutoUpdater.CheckForUpdateEvent += CheckForUpdateEvent; + } AutoUpdater.Start($"https://api.fmodel.app/v1/infos/{updateMode}"); } @@ -130,9 +133,14 @@ private void ParseUpdateInfoEvent(ParseUpdateInfoEventArgs args) { args.UpdateInfo = new UpdateInfoEventArgs { - CurrentVersion = _infos.Version, + CurrentVersion = _infos.Version.SubstringBefore('-'), ChangelogURL = _infos.ChangelogUrl, - DownloadURL = _infos.DownloadUrl + DownloadURL = _infos.DownloadUrl, + Mandatory = new CustomMandatory + { + Value = UserSettings.Default.UpdateMode == EUpdateMode.Qa, + CommitHash = _infos.Version.SubstringAfter('+') + } }; } } @@ -141,8 +149,10 @@ private void CheckForUpdateEvent(UpdateInfoEventArgs args) { if (args is { CurrentVersion: { } }) { + var qa = (CustomMandatory) args.Mandatory; var currentVersion = new System.Version(args.CurrentVersion); - if (currentVersion == args.InstalledVersion) + if ((qa.Value && qa.CommitHash == UserSettings.Default.CommitHash) || // qa branch : same commit id + (!qa.Value && currentVersion == args.InstalledVersion && args.CurrentVersion == UserSettings.Default.CommitHash)) // stable - beta branch : same version + commit id = version { if (UserSettings.Default.ShowChangelog) ShowChangelog(args); @@ -152,7 +162,7 @@ private void CheckForUpdateEvent(UpdateInfoEventArgs args) var downgrade = currentVersion < args.InstalledVersion; var messageBox = new MessageBoxModel { - Text = $"The latest version of FModel {UserSettings.Default.UpdateMode} is {args.CurrentVersion}. You are using version {args.InstalledVersion}. Do you want to {(downgrade ? "downgrade" : "update")} the application now?", + Text = $"The latest version of FModel {UserSettings.Default.UpdateMode.GetDescription()} is {(qa.Value ? qa.ShortCommitHash : args.CurrentVersion)}. You are using version {(qa.Value ? UserSettings.Default.ShortCommitHash : args.InstalledVersion)}. Do you want to {(downgrade ? "downgrade" : "update")} the application now?", Caption = $"{(downgrade ? "Downgrade" : "Update")} Available", Icon = MessageBoxImage.Question, Buttons = MessageBoxButtons.YesNo(), @@ -166,7 +176,8 @@ private void CheckForUpdateEvent(UpdateInfoEventArgs args) { if (AutoUpdater.DownloadUpdate(args)) { - UserSettings.Default.ShowChangelog = true; + UserSettings.Default.ShowChangelog = currentVersion != args.InstalledVersion; + UserSettings.Default.CommitHash = qa.CommitHash; Application.Current.Shutdown(); } } @@ -196,3 +207,9 @@ private void ShowChangelog(UpdateInfoEventArgs args) UserSettings.Default.ShowChangelog = false; } } + +public class CustomMandatory : Mandatory +{ + public string CommitHash { get; set; } + public string ShortCommitHash => CommitHash[..7]; +} diff --git a/FModel/ViewModels/ApiEndpoints/ValorantApiEndpoint.cs b/FModel/ViewModels/ApiEndpoints/ValorantApiEndpoint.cs index 67c51f96..8c827867 100644 --- a/FModel/ViewModels/ApiEndpoints/ValorantApiEndpoint.cs +++ b/FModel/ViewModels/ApiEndpoints/ValorantApiEndpoint.cs @@ -1,8 +1,3 @@ -using CUE4Parse.UE4.Exceptions; -using CUE4Parse.UE4.Readers; -using FModel.Settings; -using Ionic.Zlib; -using RestSharp; using System; using System.Collections.Generic; using System.IO; @@ -13,7 +8,15 @@ using System.Text; using System.Threading; using System.Threading.Tasks; + +using CUE4Parse.Compression; +using CUE4Parse.UE4.Exceptions; +using CUE4Parse.UE4.Readers; + using FModel.Framework; +using FModel.Settings; + +using RestSharp; namespace FModel.ViewModels.ApiEndpoints; @@ -40,26 +43,22 @@ public class VManifest public readonly VChunk[] Chunks; public readonly VPak[] Paks; - public VManifest(byte[] data) : this(new FByteArchive("CompressedValorantManifest", data)) - { - } - + public VManifest(byte[] data) : this(new FByteArchive("CompressedValorantManifest", data)) { } private VManifest(FArchive Ar) { using (Ar) { Header = new VHeader(Ar); var compressedBuffer = Ar.ReadBytes((int) Header.CompressedSize); - var uncompressedBuffer = ZlibStream.UncompressBuffer(compressedBuffer); - if (uncompressedBuffer.Length != Header.UncompressedSize) - throw new ParserException(Ar, $"Decompression failed, {uncompressedBuffer.Length} != {Header.UncompressedSize}"); + var uncompressedBuffer = new byte[(int)Header.UncompressedSize]; + ZlibHelper.Decompress(compressedBuffer, 0, compressedBuffer.Length, uncompressedBuffer, 0, uncompressedBuffer.Length); - using var manifest = new FByteArchive("UncompressedValorantManifest", uncompressedBuffer); - Chunks = manifest.ReadArray((int) Header.ChunkCount); - Paks = manifest.ReadArray((int) Header.PakCount, () => new VPak(manifest)); + var manifestAr = new FByteArchive("UncompressedValorantManifest", uncompressedBuffer); + Chunks = manifestAr.ReadArray((int) Header.ChunkCount); + Paks = manifestAr.ReadArray((int) Header.PakCount, () => new VPak(manifestAr)); - if (manifest.Position != manifest.Length) - throw new ParserException(manifest, $"Parsing failed, {manifest.Position} != {manifest.Length}"); + if (manifestAr.Position != manifestAr.Length) + throw new ParserException(manifestAr, $"Parsing failed, {manifestAr.Position} != {manifestAr.Length}"); } _client = new HttpClient(new HttpClientHandler diff --git a/FModel/ViewModels/ApplicationViewModel.cs b/FModel/ViewModels/ApplicationViewModel.cs index dd044e67..3e5bef8f 100644 --- a/FModel/ViewModels/ApplicationViewModel.cs +++ b/FModel/ViewModels/ApplicationViewModel.cs @@ -1,3 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Threading.Tasks; +using System.Windows; +using CUE4Parse.Compression; +using CUE4Parse.Encryption.Aes; +using CUE4Parse.UE4.Objects.Core.Misc; +using CUE4Parse.UE4.VirtualFileSystem; using FModel.Extensions; using FModel.Framework; using FModel.Services; @@ -5,17 +17,9 @@ using FModel.ViewModels.Commands; using FModel.Views; using FModel.Views.Resources.Controls; -using Ionic.Zip; -using Oodle.NET; -using System; -using System.Diagnostics; -using System.IO; -using System.Threading.Tasks; -using System.Windows; using MessageBox = AdonisUI.Controls.MessageBox; using MessageBoxButton = AdonisUI.Controls.MessageBoxButton; using MessageBoxImage = AdonisUI.Controls.MessageBoxImage; -using OodleCUE4 = CUE4Parse.Compression.Oodle; namespace FModel.ViewModels; @@ -46,7 +50,7 @@ public FStatus Status public CopyCommand CopyCommand => _copyCommand ??= new CopyCommand(this); private CopyCommand _copyCommand; - public string InitialWindowTitle => $"FModel {UserSettings.Default.UpdateMode}"; + public string InitialWindowTitle => $"FModel {UserSettings.Default.UpdateMode.GetDescription()}"; public string GameDisplayName => CUE4Parse.Provider.GameDisplayName ?? "Unknown"; public string TitleExtra => $"({UserSettings.Default.CurrentDir.UeVersion}){(Build != EBuildKind.Release ? $" ({Build})" : "")}"; @@ -56,7 +60,6 @@ public FStatus Status public SettingsViewModel SettingsView { get; } public AesManagerViewModel AesManager { get; } public AudioPlayerViewModel AudioPlayer { get; } - private OodleCompressor _oodle; public ApplicationViewModel() { @@ -79,6 +82,24 @@ public ApplicationViewModel() } CUE4Parse = new CUE4ParseViewModel(); + CUE4Parse.Provider.VfsRegistered += (sender, count) => + { + if (sender is not IAesVfsReader reader) return; + Status.UpdateStatusLabel($"{count} Archives ({reader.Name})", "Registered"); + CUE4Parse.GameDirectory.Add(reader); + }; + CUE4Parse.Provider.VfsMounted += (sender, count) => + { + if (sender is not IAesVfsReader reader) return; + Status.UpdateStatusLabel($"{count:N0} Packages ({reader.Name})", "Mounted"); + CUE4Parse.GameDirectory.Verify(reader); + }; + CUE4Parse.Provider.VfsUnmounted += (sender, _) => + { + if (sender is not IAesVfsReader reader) return; + CUE4Parse.GameDirectory.Disable(reader); + }; + CustomDirectories = new CustomDirectoriesViewModel(); SettingsView = new SettingsViewModel(); AesManager = new AesManagerViewModel(CUE4Parse); @@ -156,14 +177,23 @@ public async Task UpdateProvider(bool isLaunch) CUE4Parse.ClearProvider(); await ApplicationService.ThreadWorkerView.Begin(cancellationToken => { - CUE4Parse.LoadVfs(cancellationToken, AesManager.AesKeys); - CUE4Parse.Provider.LoadIniConfigs(); + // TODO: refactor after release, select updated keys only + var aes = AesManager.AesKeys.Select(x => + { + cancellationToken.ThrowIfCancellationRequested(); // cancel if needed + + var k = x.Key.Trim(); + if (k.Length != 66) k = Constants.ZERO_64_CHAR; + return new KeyValuePair(x.Guid, new FAesKey(k)); + }); + + CUE4Parse.LoadVfs(aes); AesManager.SetAesKeys(); }); RaisePropertyChanged(nameof(GameDisplayName)); } - public async Task InitVgmStream() + public static async Task InitVgmStream() { var vgmZipFilePath = Path.Combine(UserSettings.Default.OutputDirectory, ".data", "vgmstream-win.zip"); if (File.Exists(vgmZipFilePath)) return; @@ -171,9 +201,17 @@ public async Task InitVgmStream() await ApplicationService.ApiEndpointView.DownloadFileAsync("https://github.com/vgmstream/vgmstream/releases/latest/download/vgmstream-win.zip", vgmZipFilePath); if (new FileInfo(vgmZipFilePath).Length > 0) { - var zip = ZipFile.Read(vgmZipFilePath); - var zipDir = vgmZipFilePath.SubstringBeforeLast("\\"); - foreach (var e in zip) e.Extract(zipDir, ExtractExistingFileAction.OverwriteSilently); + var zipDir = Path.GetDirectoryName(vgmZipFilePath)!; + await using var zipFs = File.OpenRead(vgmZipFilePath); + using var zip = new ZipArchive(zipFs, ZipArchiveMode.Read); + + foreach (var entry in zip.Entries) + { + var entryPath = Path.Combine(zipDir, entry.FullName); + await using var entryFs = File.OpenRead(entryPath); + await using var entryStream = entry.Open(); + await entryStream.CopyToAsync(entryFs); + } } else { @@ -181,43 +219,44 @@ public async Task InitVgmStream() } } - public async Task InitOodle() + public static async Task InitImGuiSettings(bool forceDownload) { - var dataDir = Directory.CreateDirectory(Path.Combine(UserSettings.Default.OutputDirectory, ".data")); - var oodlePath = Path.Combine(dataDir.FullName, OodleCUE4.OODLE_DLL_NAME); + var imgui = "imgui.ini"; + var imguiPath = Path.Combine(UserSettings.Default.OutputDirectory, ".data", imgui); - if (File.Exists(OodleCUE4.OODLE_DLL_NAME)) + if (File.Exists(imgui)) File.Move(imgui, imguiPath, true); + if (File.Exists(imguiPath) && !forceDownload) return; + + await ApplicationService.ApiEndpointView.DownloadFileAsync($"https://cdn.fmodel.app/d/configurations/{imgui}", imguiPath); + if (new FileInfo(imguiPath).Length == 0) { - File.Move(OodleCUE4.OODLE_DLL_NAME, oodlePath, true); + FLogger.Append(ELog.Error, () => FLogger.Text("Could not download ImGui settings", Constants.WHITE, true)); } - else if (!File.Exists(oodlePath)) + } + + public static async ValueTask InitOodle() + { + var oodlePath = Path.Combine(UserSettings.Default.OutputDirectory, ".data", OodleHelper.OODLE_DLL_NAME); + if (File.Exists(OodleHelper.OODLE_DLL_NAME)) { - var result = await OodleCUE4.DownloadOodleDll(oodlePath); - if (!result) return; + File.Move(OodleHelper.OODLE_DLL_NAME, oodlePath, true); } - - if (File.Exists("oo2core_8_win64.dll")) - File.Delete("oo2core_8_win64.dll"); - - _oodle = new OodleCompressor(oodlePath); - - unsafe + else if (!File.Exists(oodlePath)) { - OodleCUE4.DecompressFunc = (bufferPtr, bufferSize, outputPtr, outputSize, a, b, c, d, e, f, g, h, i, threadModule) => - _oodle.Decompress(new IntPtr(bufferPtr), bufferSize, new IntPtr(outputPtr), outputSize, - (OodleLZ_FuzzSafe) a, (OodleLZ_CheckCRC) b, (OodleLZ_Verbosity) c, d, e, f, g, h, i, (OodleLZ_Decode_ThreadPhase) threadModule); + await OodleHelper.DownloadOodleDllAsync(oodlePath); } + + OodleHelper.Initialize(oodlePath); } - public async Task InitImGuiSettings(bool forceDownload) + public static async ValueTask InitZlib() { - var imgui = Path.Combine(/*UserSettings.Default.OutputDirectory, ".data", */"imgui.ini"); - if (File.Exists(imgui) && !forceDownload) return; - - await ApplicationService.ApiEndpointView.DownloadFileAsync("https://cdn.fmodel.app/d/configurations/imgui.ini", imgui); - if (new FileInfo(imgui).Length == 0) + var zlibPath = Path.Combine(UserSettings.Default.OutputDirectory, ".data", ZlibHelper.DLL_NAME); + if (!File.Exists(zlibPath)) { - FLogger.Append(ELog.Error, () => FLogger.Text("Could not download ImGui settings", Constants.WHITE, true)); + await ZlibHelper.DownloadDllAsync(zlibPath); } + + ZlibHelper.Initialize(zlibPath); } } diff --git a/FModel/ViewModels/AudioPlayerViewModel.cs b/FModel/ViewModels/AudioPlayerViewModel.cs index 4aedde73..e6c099db 100644 --- a/FModel/ViewModels/AudioPlayerViewModel.cs +++ b/FModel/ViewModels/AudioPlayerViewModel.cs @@ -552,14 +552,26 @@ private bool ConvertIfNeeded() case "opus": case "wem": case "at9": + case "raw": { - if (TryConvert(out var wavFilePath) && !string.IsNullOrEmpty(wavFilePath)) + if (TryConvert(out var wavFilePath)) { var newAudio = new AudioFile(SelectedAudioFile.Id, new FileInfo(wavFilePath)); Replace(newAudio); return true; } + return false; + } + case "binka": + { + if (TryDecode(out var rawFilePath)) + { + var newAudio = new AudioFile(SelectedAudioFile.Id, new FileInfo(rawFilePath)); + Replace(newAudio); + return true; + } + return false; } } @@ -567,7 +579,8 @@ private bool ConvertIfNeeded() return true; } - private bool TryConvert(out string wavFilePath) + private bool TryConvert(out string wavFilePath) => TryConvert(SelectedAudioFile.FilePath, SelectedAudioFile.Data, out wavFilePath); + private bool TryConvert(string inputFilePath, byte[] inputFileData, out string wavFilePath) { wavFilePath = string.Empty; var vgmFilePath = Path.Combine(UserSettings.Default.OutputDirectory, ".data", "test.exe"); @@ -577,22 +590,46 @@ private bool TryConvert(out string wavFilePath) if (!File.Exists(vgmFilePath)) return false; } - Directory.CreateDirectory(SelectedAudioFile.FilePath.SubstringBeforeLast("/")); - File.WriteAllBytes(SelectedAudioFile.FilePath, SelectedAudioFile.Data); + Directory.CreateDirectory(inputFilePath.SubstringBeforeLast("/")); + File.WriteAllBytes(inputFilePath, inputFileData); - wavFilePath = Path.ChangeExtension(SelectedAudioFile.FilePath, ".wav"); + wavFilePath = Path.ChangeExtension(inputFilePath, ".wav"); var vgmProcess = Process.Start(new ProcessStartInfo { FileName = vgmFilePath, - Arguments = $"-o \"{wavFilePath}\" \"{SelectedAudioFile.FilePath}\"", + Arguments = $"-o \"{wavFilePath}\" \"{inputFilePath}\"", UseShellExecute = false, - RedirectStandardOutput = true, - RedirectStandardError = true, CreateNoWindow = true }); - vgmProcess?.WaitForExit(); + vgmProcess?.WaitForExit(5000); - File.Delete(SelectedAudioFile.FilePath); + File.Delete(inputFilePath); return vgmProcess?.ExitCode == 0 && File.Exists(wavFilePath); } + + private bool TryDecode(out string rawFilePath) + { + rawFilePath = string.Empty; + var binkadecPath = Path.Combine(UserSettings.Default.OutputDirectory, ".data", "binkadec.exe"); + if (!File.Exists(binkadecPath)) + { + return false; + } + + Directory.CreateDirectory(SelectedAudioFile.FilePath.SubstringBeforeLast("/")); + File.WriteAllBytes(SelectedAudioFile.FilePath, SelectedAudioFile.Data); + + rawFilePath = Path.ChangeExtension(SelectedAudioFile.FilePath, ".wav"); + var binkadecProcess = Process.Start(new ProcessStartInfo + { + FileName = binkadecPath, + Arguments = $"-i \"{SelectedAudioFile.FilePath}\" -o \"{rawFilePath}\"", + UseShellExecute = false, + CreateNoWindow = true + }); + binkadecProcess?.WaitForExit(5000); + + File.Delete(SelectedAudioFile.FilePath); + return binkadecProcess?.ExitCode == 0 && File.Exists(rawFilePath); + } } diff --git a/FModel/ViewModels/CUE4ParseViewModel.cs b/FModel/ViewModels/CUE4ParseViewModel.cs index 473ad1fb..18ebeb30 100644 --- a/FModel/ViewModels/CUE4ParseViewModel.cs +++ b/FModel/ViewModels/CUE4ParseViewModel.cs @@ -1,1059 +1,966 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text.RegularExpressions; -using System.Threading; -using System.Threading.Tasks; -using System.Windows; -using AdonisUI.Controls; -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.Verse; -using CUE4Parse.UE4.Assets.Exports.Sound; -using CUE4Parse.UE4.Assets.Exports.StaticMesh; -using CUE4Parse.UE4.Assets.Exports.Texture; -using CUE4Parse.UE4.Assets.Exports.Wwise; -using CUE4Parse.UE4.IO; -using CUE4Parse.UE4.Localization; -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.FileProvider.Objects; -using CUE4Parse.UE4.Objects.Core.Serialization; -using EpicManifestParser.Objects; -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 - { - Size = 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 BuildInfo { 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; - - GameDirectory = new GameDirectoryViewModel(); - AssetsFolder = new AssetsFolderViewModel(); - SearchVm = new SearchViewModel(); - TabControl = new TabControlViewModel(); - BuildInfo = new ConfigIni(nameof(BuildInfo)); - } - - 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 == null) - { - throw new Exception("Could not load latest Fortnite manifest, you may have to switch to your local installation."); - } - - byte[] manifestData; - var chunksDir = Directory.CreateDirectory(Path.Combine(UserSettings.Default.OutputDirectory, ".data")); - var manifestPath = Path.Combine(chunksDir.FullName, manifestInfo.FileName); - if (File.Exists(manifestPath)) - { - manifestData = File.ReadAllBytes(manifestPath); - } - else - { - manifestData = manifestInfo.DownloadManifestData(); - File.WriteAllBytes(manifestPath, manifestData); - } - - var manifest = new Manifest(manifestData, new ManifestOptions - { - ChunkBaseUri = new Uri("http://epicgames-download1.akamaized.net/Builds/Fortnite/CloudDir/ChunksV4/", UriKind.Absolute), - ChunkCacheDirectory = chunksDir - }); - - foreach (var fileManifest in manifest.FileManifests) - { - if (fileManifest.Name.Equals("Cloud/BuildInfo.ini", StringComparison.OrdinalIgnoreCase)) - { - BuildInfo.Read(new StreamReader(fileManifest.GetStream())); - continue; - } - if (!_fnLive.IsMatch(fileManifest.Name)) continue; - - p.RegisterVfs(fileManifest.Name, new Stream[] { fileManifest.GetStream() } - , it => new FStreamArchive(it, manifest.FileManifests.First(x => x.Name.Equals(it)).GetStream(), p.Versions)); - } - - FLogger.Append(ELog.Information, () => - FLogger.Text($"Fortnite has been loaded successfully in {manifest.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(), new[] { 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 buildInfoPath = Path.Combine(UserSettings.Default.GameDirectory, "..\\..\\..\\Cloud\\BuildInfo.ini"); - if (File.Exists(buildInfoPath)) BuildInfo.Read(new StringReader(File.ReadAllText(buildInfoPath))); - break; - } - - Provider.Initialize(); - - 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)); - } - - // 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.Info.TocEntryCount - 1; - - continue; - } - - file.IsEnabled = true; - file.MountPoint = vfs.MountPoint; - file.FileCount = vfs.FileCount; - } - - InternalGameName = Provider.InternalGameName; - } - - 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"); - 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)); - } - }); - } - - private bool _cvaVerifDone { get; set; } - public Task VerifyConsoleVariables() - { - if (_cvaVerifDone) - return Task.CompletedTask; - - return Task.Run(() => - { - foreach (var token in Provider.DefaultEngine.Sections.FirstOrDefault(s => s.Name == "ConsoleVariables")?.Tokens ?? new List()) - { - if (token is not InstructionToken it) continue; - var boolValue = it.Value.Equals("1"); - - switch (it.Key) - { - case "a.StripAdditiveRefPose" when boolValue: - FLogger.Append(ELog.Warning, () => - FLogger.Text("Additive animations have their reference pose stripped, which will lead to inaccurate preview and export", Constants.WHITE, true)); - continue; - case "r.StaticMesh.KeepMobileMinLODSettingOnDesktop": - case "r.SkeletalMesh.KeepMobileMinLODSettingOnDesktop": - Provider.Versions[it.Key[2..]] = boolValue; - continue; - } - } - - _cvaVerifDone = true; - }); - } - - private int _vfcCount { get; set; } - public Task VerifyVirtualCache() - { - if (Provider is StreamedFileProvider { LiveGame: "FortniteLive" } || _vfcCount > 0) - return Task.CompletedTask; - - return Task.Run(() => - { - _vfcCount = Provider.LoadVirtualCache(); - if (_vfcCount > 0) - FLogger.Append(ELog.Information, - () => FLogger.Text($"{_vfcCount} cached packages loaded", Constants.WHITE, true)); - }); - } - - public Task VerifyContentBuildManifest() - { - if (Provider is not DefaultFileProvider || !Provider.InternalGameName.Equals("FortniteGame", StringComparison.OrdinalIgnoreCase)) - return Task.CompletedTask; - - var persistentDownloadDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "FortniteGame/Saved/PersistentDownloadDir"); - var vfcMetadata = Path.Combine(persistentDownloadDir, "VFC", "vfc.meta"); - if (!File.Exists(vfcMetadata)) - return Task.CompletedTask; - - // load if local fortnite with ondemand disabled - // VFC folder is created at launch if ondemand - // VFC folder is deleted at launch if not ondemand anymore - return Task.Run(() => - { - var inst = new List(); - BuildInfo.FindPropertyInstructions("Content", "Label", inst); - if (inst.Count <= 0) return; - - var manifestInfo = _apiEndpointView.EpicApi.GetContentBuildManifest(default, inst[0].Value); - var manifestDir = new DirectoryInfo(Path.Combine(persistentDownloadDir, "ManifestCache")); - var manifestPath = Path.Combine(manifestDir.FullName, manifestInfo?.FileName ?? ""); - - byte[] manifestData; - if (File.Exists(manifestPath)) - { - manifestData = File.ReadAllBytes(manifestPath); - } - else if (manifestInfo != null) - { - manifestData = manifestInfo.DownloadManifestData(); - File.WriteAllBytes(manifestPath, manifestData); - } - else if (manifestDir.Exists && manifestDir.GetFiles("*.manifest") is { Length: > 0} cachedManifests) - { - manifestData = File.ReadAllBytes(cachedManifests[0].FullName); - } - else return; - - var manifest = new Manifest(manifestData, new ManifestOptions - { - ChunkBaseUri = new Uri("http://epicgames-download1.akamaized.net/Builds/Fortnite/Content/CloudDir/ChunksV4/", UriKind.Absolute), - ChunkCacheDirectory = Directory.CreateDirectory(Path.Combine(UserSettings.Default.OutputDirectory, ".data")) - }); - - var onDemandFiles = new Dictionary(); - foreach (var fileManifest in manifest.FileManifests) - { - if (Provider.Files.TryGetValue(fileManifest.Name, out _)) continue; - - var onDemandFile = new StreamedGameFile(fileManifest.Name, fileManifest.GetStream(), Provider.Versions); - if (Provider.IsCaseInsensitive) onDemandFiles[onDemandFile.Path.ToLowerInvariant()] = onDemandFile; - else onDemandFiles[onDemandFile.Path] = onDemandFile; - } - - (Provider.Files as FileProviderDictionary)?.AddFiles(onDemandFiles); - if (onDemandFiles.Count > 0) - FLogger.Append(ELog.Information, - () => FLogger.Text($"{onDemandFiles.Count} streamed packages loaded", Constants.WHITE, true)); -#if DEBUG - - var missing = manifest.FileManifests.Count - onDemandFiles.Count; - if (missing > 0) - FLogger.Append(ELog.Debug, - () => FLogger.Text($"{missing} packages were already loaded by regular archives", Constants.WHITE, true)); -#endif - }); - } - - 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.Header = fileName; - TabControl.SelectedTab.Directory = directory; - } - - var updateUi = !HasFlag(bulk, EBulkType.Auto); - var saveProperties = HasFlag(bulk, EBulkType.Properties); - var saveTextures = HasFlag(bulk, EBulkType.Textures); - TabControl.SelectedTab.ClearImages(); - TabControl.SelectedTab.ResetDocumentText(); - TabControl.SelectedTab.ScrollTrigger = null; - TabControl.SelectedTab.Highlighter = AvalonExtensions.HighlighterSelector(ext); - 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 "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) - { - Log.Information("User CTRL-CLICKED to extract '{FullPath}'", fullPath); - TabControl.AddTab(fullPath.SubstringAfterLast('/'), fullPath.SubstringBeforeLast('/')); - 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 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.EndsWith($"/MI_OfferImages/{export.Name}", 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 exportOptions = new ExporterOptions - { - LodFormat = UserSettings.Default.LodExportFormat, - MeshFormat = UserSettings.Default.MeshExportFormat, - MaterialFormat = UserSettings.Default.MaterialExportFormat, - TextureFormat = UserSettings.Default.TextureExportFormat, - SocketFormat = UserSettings.Default.SocketExportFormat, - Platform = UserSettings.Default.CurrentDir.TexturePlatform, - ExportMorphTargets = UserSettings.Default.SaveMorphTargets, - ExportMaterials = UserSettings.Default.SaveEmbeddedMaterials - }; - var toSave = new Exporter(export, 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 CUE4Parse.UE4.Objects.Core.Misc; +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 _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(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(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}"); + }); + } + + /// + /// load virtual files system from GameDirectory + /// + /// + public void LoadVfs(IEnumerable> aesKeys) + { + Provider.SubmitKeys(aesKeys); + Provider.PostMount(); + 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; + } +} diff --git a/FModel/ViewModels/Commands/LoadCommand.cs b/FModel/ViewModels/Commands/LoadCommand.cs index f11c9e4a..3dfa13b1 100644 --- a/FModel/ViewModels/Commands/LoadCommand.cs +++ b/FModel/ViewModels/Commands/LoadCommand.cs @@ -38,7 +38,7 @@ public LoadCommand(LoadingModesViewModel contextViewModel) : base(contextViewMod public override async void Execute(LoadingModesViewModel contextViewModel, object parameter) { if (_applicationView.CUE4Parse.GameDirectory.HasNoFile) return; - if (_applicationView.CUE4Parse.Provider.Files.Count <= 0) + if (_applicationView.CUE4Parse.Provider.Keys.Count == 0 && _applicationView.CUE4Parse.Provider.RequiredKeys.Count > 0) { FLogger.Append(ELog.Error, () => FLogger.Text("An encrypted archive has been found. In order to decrypt it, please specify a working AES encryption key", Constants.WHITE, true)); @@ -61,7 +61,6 @@ await Task.WhenAll( // filter what to show switch (UserSettings.Default.LoadingMode) { - case ELoadingMode.Single: case ELoadingMode.Multiple: { var l = (IList) parameter; diff --git a/FModel/ViewModels/GameDirectoryViewModel.cs b/FModel/ViewModels/GameDirectoryViewModel.cs index 714318f2..16b11656 100644 --- a/FModel/ViewModels/GameDirectoryViewModel.cs +++ b/FModel/ViewModels/GameDirectoryViewModel.cs @@ -1,8 +1,11 @@ using FModel.Framework; using System.Collections.ObjectModel; using System.ComponentModel; +using System.Linq; +using System.Text.RegularExpressions; using System.Windows; using System.Windows.Data; +using CUE4Parse.UE4.IO; using CUE4Parse.UE4.Objects.Core.Misc; using CUE4Parse.UE4.VirtualFileSystem; @@ -72,6 +75,17 @@ public FileItem(string name, long length) Length = length; } + public FileItem(IAesVfsReader reader) + { + Name = reader.Name; + Length = reader.Length; + Guid = reader.EncryptionKeyGuid; + IsEncrypted = reader.IsEncrypted; + IsEnabled = false; + Key = string.Empty; + FileCount = reader is IoStoreReader storeReader ? (int) storeReader.TocResource.Header.TocEntryCount - 1 : 0; + } + public override string ToString() { return $"{Name} | {Key}"; @@ -84,31 +98,35 @@ public class GameDirectoryViewModel : ViewModel public readonly ObservableCollection DirectoryFiles; public ICollectionView DirectoryFilesView { get; } + private readonly Regex _hiddenArchives = new(@"^(?!global|pakchunk.+(optional|ondemand)\-).+(pak|utoc)$", // should be universal + RegexOptions.Compiled | RegexOptions.Singleline | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); + public GameDirectoryViewModel() { DirectoryFiles = new ObservableCollection(); DirectoryFilesView = new ListCollectionView(DirectoryFiles) { SortDescriptions = { new SortDescription("Name", ListSortDirection.Ascending) } }; } - public void DeactivateAll() + public void Add(IAesVfsReader reader) { - foreach (var file in DirectoryFiles) - { - file.IsEnabled = false; - } + if (!_hiddenArchives.IsMatch(reader.Name)) return; + + var fileItem = new FileItem(reader); + Application.Current.Dispatcher.Invoke(() => DirectoryFiles.Add(fileItem)); } - public void Add(IAesVfsReader reader) + public void Verify(IAesVfsReader reader) + { + if (DirectoryFiles.FirstOrDefault(x => x.Name == reader.Name) is not { } file) return; + + file.IsEnabled = true; + file.MountPoint = reader.MountPoint; + file.FileCount = reader.FileCount; + } + + public void Disable(IAesVfsReader reader) { - Application.Current.Dispatcher.Invoke(() => - { - DirectoryFiles.Add(new FileItem(reader.Name, reader.Length) - { - Guid = reader.EncryptionKeyGuid, - IsEncrypted = reader.IsEncrypted, - IsEnabled = false, - Key = string.Empty - }); - }); + if (DirectoryFiles.FirstOrDefault(x => x.Name == reader.Name) is not { } file) return; + file.IsEnabled = false; } } diff --git a/FModel/ViewModels/GameSelectorViewModel.cs b/FModel/ViewModels/GameSelectorViewModel.cs index f1d40228..273248b2 100644 --- a/FModel/ViewModels/GameSelectorViewModel.cs +++ b/FModel/ViewModels/GameSelectorViewModel.cs @@ -33,16 +33,28 @@ public class DetectedGame public IList CustomDirectories { get; set; } } + private bool _useCustomEGames; + public bool UseCustomEGames + { + get => _useCustomEGames; + set => SetProperty(ref _useCustomEGames, value); + } + private DirectorySettings _selectedDirectory; public DirectorySettings SelectedDirectory { get => _selectedDirectory; - set => SetProperty(ref _selectedDirectory, value); + set + { + SetProperty(ref _selectedDirectory, value); + if (_selectedDirectory != null) UseCustomEGames = EnumerateUeGames().ElementAt(1).Contains(_selectedDirectory.UeVersion); + } } private readonly ObservableCollection _detectedDirectories; public ReadOnlyObservableCollection DetectedDirectories { get; } - public ReadOnlyObservableCollection UeVersions { get; } + public ReadOnlyObservableCollection UeGames { get; } + public ReadOnlyObservableCollection CustomUeGames { get; } public GameSelectorViewModel(string gameDirectory) { @@ -61,7 +73,9 @@ public GameSelectorViewModel(string gameDirectory) else SelectedDirectory = DetectedDirectories.FirstOrDefault(); - UeVersions = new ReadOnlyObservableCollection(new ObservableCollection(EnumerateUeGames())); + var ueGames = EnumerateUeGames().ToArray(); + UeGames = new ReadOnlyObservableCollection(new ObservableCollection(ueGames[0])); + CustomUeGames = new ReadOnlyObservableCollection(new ObservableCollection(ueGames[1])); } public void AddUndetectedDir(string gameDirectory) => AddUndetectedDir(gameDirectory.SubstringAfterLast('\\'), gameDirectory); @@ -80,15 +94,15 @@ public void DeleteSelectedGame() SelectedDirectory = DetectedDirectories.Last(); } - private IEnumerable EnumerateUeGames() + private IEnumerable> EnumerateUeGames() => Enum.GetValues() .GroupBy(value => (int)value) .Select(group => group.First()) - .OrderBy(value => (int)value == ((int)value & ~0xF)); + .GroupBy(value => (int)value == ((int)value & ~0xF)); private IEnumerable EnumerateDetectedGames() { - yield return GetUnrealEngineGame("Fortnite", "\\FortniteGame\\Content\\Paks", EGame.GAME_UE5_3); - yield return DirectorySettings.Default("Fortnite [LIVE]", Constants._FN_LIVE_TRIGGER, ue: EGame.GAME_UE5_3); + yield return GetUnrealEngineGame("Fortnite", "\\FortniteGame\\Content\\Paks", EGame.GAME_UE5_5); + yield return DirectorySettings.Default("Fortnite [LIVE]", Constants._FN_LIVE_TRIGGER, ue: EGame.GAME_UE5_5); yield return GetUnrealEngineGame("Pewee", "\\RogueCompany\\Content\\Paks", EGame.GAME_RogueCompany); yield return GetUnrealEngineGame("Rosemallow", "\\Indiana\\Content\\Paks", EGame.GAME_UE4_21); yield return GetUnrealEngineGame("Catnip", "\\OakGame\\Content\\Paks", EGame.GAME_Borderlands3); diff --git a/FModel/ViewModels/SettingsViewModel.cs b/FModel/ViewModels/SettingsViewModel.cs index 4d5f2aa4..e76c59b5 100644 --- a/FModel/ViewModels/SettingsViewModel.cs +++ b/FModel/ViewModels/SettingsViewModel.cs @@ -2,11 +2,13 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; +using System.Windows; using CUE4Parse.UE4.Assets.Exports.Texture; using CUE4Parse.UE4.Objects.Core.Serialization; using CUE4Parse.UE4.Versions; using CUE4Parse_Conversion.Meshes; using CUE4Parse_Conversion.Textures; +using CUE4Parse_Conversion.UEFormat.Enums; using CUE4Parse.UE4.Assets.Exports.Material; using FModel.Framework; using FModel.Services; @@ -25,6 +27,13 @@ public bool UseCustomOutputFolders set => SetProperty(ref _useCustomOutputFolders, value); } + private bool _useCustomEGames; + public bool UseCustomEGames + { + get => _useCustomEGames; + set => SetProperty(ref _useCustomEGames, value); + } + private EUpdateMode _selectedUpdateMode; public EUpdateMode SelectedUpdateMode { @@ -120,7 +129,12 @@ public EIconStyle SelectedCosmeticStyle public EMeshFormat SelectedMeshExportFormat { get => _selectedMeshExportFormat; - set => SetProperty(ref _selectedMeshExportFormat, value); + set + { + SetProperty(ref _selectedMeshExportFormat, value); + RaisePropertyChanged(nameof(SocketSettingsEnabled)); + RaisePropertyChanged(nameof(CompressionSettingsEnabled)); + } } private ESocketFormat _selectedSocketExportFormat; @@ -130,6 +144,13 @@ public ESocketFormat SelectedSocketExportFormat set => SetProperty(ref _selectedSocketExportFormat, value); } + private EFileCompressionFormat _selectedCompressionFormat; + public EFileCompressionFormat SelectedCompressionFormat + { + get => _selectedCompressionFormat; + set => SetProperty(ref _selectedCompressionFormat, value); + } + private ELodFormat _selectedLodExportFormat; public ELodFormat SelectedLodExportFormat { @@ -151,8 +172,12 @@ public ETextureFormat SelectedTextureExportFormat set => SetProperty(ref _selectedTextureExportFormat, value); } + public bool SocketSettingsEnabled => SelectedMeshExportFormat == EMeshFormat.ActorX; + public bool CompressionSettingsEnabled => SelectedMeshExportFormat == EMeshFormat.UEFormat; + public ReadOnlyObservableCollection UpdateModes { get; private set; } public ReadOnlyObservableCollection UeGames { get; private set; } + public ReadOnlyObservableCollection CustomUeGames { get; private set; } public ReadOnlyObservableCollection AssetLanguages { get; private set; } public ReadOnlyObservableCollection AesReloads { get; private set; } public ReadOnlyObservableCollection DiscordRpcs { get; private set; } @@ -160,6 +185,7 @@ public ETextureFormat SelectedTextureExportFormat public ReadOnlyObservableCollection CosmeticStyles { get; private set; } public ReadOnlyObservableCollection MeshExportFormats { get; private set; } public ReadOnlyObservableCollection SocketExportFormats { get; private set; } + public ReadOnlyObservableCollection CompressionFormats { get; private set; } public ReadOnlyObservableCollection LodExportFormats { get; private set; } public ReadOnlyObservableCollection MaterialExportFormats { get; private set; } public ReadOnlyObservableCollection TextureExportFormats { get; private set; } @@ -183,6 +209,7 @@ public ETextureFormat SelectedTextureExportFormat private EIconStyle _cosmeticStyleSnapshot; private EMeshFormat _meshExportFormatSnapshot; private ESocketFormat _socketExportFormatSnapshot; + private EFileCompressionFormat _compressionFormatSnapshot; private ELodFormat _lodExportFormatSnapshot; private EMaterialFormat _materialExportFormatSnapshot; private ETextureFormat _textureExportFormatSnapshot; @@ -223,6 +250,7 @@ public void Initialize() _cosmeticStyleSnapshot = UserSettings.Default.CosmeticStyle; _meshExportFormatSnapshot = UserSettings.Default.MeshExportFormat; _socketExportFormatSnapshot = UserSettings.Default.SocketExportFormat; + _compressionFormatSnapshot = UserSettings.Default.CompressionFormat; _lodExportFormatSnapshot = UserSettings.Default.LodExportFormat; _materialExportFormatSnapshot = UserSettings.Default.MaterialExportFormat; _textureExportFormatSnapshot = UserSettings.Default.TextureExportFormat; @@ -238,14 +266,19 @@ public void Initialize() SelectedCosmeticStyle = _cosmeticStyleSnapshot; SelectedMeshExportFormat = _meshExportFormatSnapshot; SelectedSocketExportFormat = _socketExportFormatSnapshot; + SelectedCompressionFormat = _selectedCompressionFormat; SelectedLodExportFormat = _lodExportFormatSnapshot; SelectedMaterialExportFormat = _materialExportFormatSnapshot; SelectedTextureExportFormat = _textureExportFormatSnapshot; SelectedAesReload = UserSettings.Default.AesReload; SelectedDiscordRpc = UserSettings.Default.DiscordRpc; + var ueGames = EnumerateUeGames().ToArray(); + UseCustomEGames = ueGames[1].Contains(SelectedUeGame); + UpdateModes = new ReadOnlyObservableCollection(new ObservableCollection(EnumerateUpdateModes())); - UeGames = new ReadOnlyObservableCollection(new ObservableCollection(EnumerateUeGames())); + UeGames = new ReadOnlyObservableCollection(new ObservableCollection(ueGames[0])); + CustomUeGames = new ReadOnlyObservableCollection(new ObservableCollection(ueGames[1])); AssetLanguages = new ReadOnlyObservableCollection(new ObservableCollection(EnumerateAssetLanguages())); AesReloads = new ReadOnlyObservableCollection(new ObservableCollection(EnumerateAesReloads())); DiscordRpcs = new ReadOnlyObservableCollection(new ObservableCollection(EnumerateDiscordRpcs())); @@ -253,6 +286,7 @@ public void Initialize() CosmeticStyles = new ReadOnlyObservableCollection(new ObservableCollection(EnumerateCosmeticStyles())); MeshExportFormats = new ReadOnlyObservableCollection(new ObservableCollection(EnumerateMeshExportFormat())); SocketExportFormats = new ReadOnlyObservableCollection(new ObservableCollection(EnumerateSocketExportFormat())); + CompressionFormats = new ReadOnlyObservableCollection(new ObservableCollection(EnumerateCompressionFormat())); LodExportFormats = new ReadOnlyObservableCollection(new ObservableCollection(EnumerateLodExportFormat())); MaterialExportFormats = new ReadOnlyObservableCollection(new ObservableCollection(EnumerateMaterialExportFormat())); TextureExportFormats = new ReadOnlyObservableCollection(new ObservableCollection(EnumerateTextureExportFormat())); @@ -295,6 +329,7 @@ public bool Save(out List whatShouldIDo) UserSettings.Default.CosmeticStyle = SelectedCosmeticStyle; UserSettings.Default.MeshExportFormat = SelectedMeshExportFormat; UserSettings.Default.SocketExportFormat = SelectedSocketExportFormat; + UserSettings.Default.CompressionFormat = SelectedCompressionFormat; UserSettings.Default.LodExportFormat = SelectedLodExportFormat; UserSettings.Default.MaterialExportFormat = SelectedMaterialExportFormat; UserSettings.Default.TextureExportFormat = SelectedTextureExportFormat; @@ -308,11 +343,11 @@ public bool Save(out List whatShouldIDo) } private IEnumerable EnumerateUpdateModes() => Enum.GetValues(); - private IEnumerable EnumerateUeGames() + private IEnumerable> EnumerateUeGames() => Enum.GetValues() .GroupBy(value => (int)value) .Select(group => group.First()) - .OrderBy(value => (int)value == ((int)value & ~0xF)); + .GroupBy(value => (int)value == ((int)value & ~0xF)); private IEnumerable EnumerateAssetLanguages() => Enum.GetValues(); private IEnumerable EnumerateAesReloads() => Enum.GetValues(); private IEnumerable EnumerateDiscordRpcs() => Enum.GetValues(); @@ -320,6 +355,7 @@ private IEnumerable EnumerateUeGames() private IEnumerable EnumerateCosmeticStyles() => Enum.GetValues(); private IEnumerable EnumerateMeshExportFormat() => Enum.GetValues(); private IEnumerable EnumerateSocketExportFormat() => Enum.GetValues(); + private IEnumerable EnumerateCompressionFormat() => Enum.GetValues(); private IEnumerable EnumerateLodExportFormat() => Enum.GetValues(); private IEnumerable EnumerateMaterialExportFormat() => Enum.GetValues(); private IEnumerable EnumerateTextureExportFormat() => Enum.GetValues(); diff --git a/FModel/ViewModels/TabControlViewModel.cs b/FModel/ViewModels/TabControlViewModel.cs index 461dd34e..27924c4d 100644 --- a/FModel/ViewModels/TabControlViewModel.cs +++ b/FModel/ViewModels/TabControlViewModel.cs @@ -86,6 +86,8 @@ private void SetImage(SKBitmap bitmap) public class TabItem : ViewModel { + public string ParentExportType { get; private set; } + private string _header; public string Header { @@ -211,25 +213,41 @@ public TabImage SelectedImage private GoToCommand _goToCommand; public GoToCommand GoToCommand => _goToCommand ??= new GoToCommand(null); - public TabItem(string header, string directory) + public TabItem(string header, string directory, string parentExportType) { Header = header; Directory = directory; + ParentExportType = parentExportType; _images = new ObservableCollection(); } - public void ClearImages() + public void SoftReset(string header, string directory) { + Header = header; + Directory = directory; + ParentExportType = string.Empty; + ScrollTrigger = null; Application.Current.Dispatcher.Invoke(() => { _images.Clear(); SelectedImage = null; RaisePropertyChanged("HasMultipleImages"); + + Document ??= new TextDocument(); + Document.Text = string.Empty; }); } public void AddImage(UTexture texture, bool save, bool updateUi) - => AddImage(texture.Name, texture.RenderNearestNeighbor, texture.Decode(UserSettings.Default.CurrentDir.TexturePlatform), save, updateUi); + { + var img = texture.Decode(UserSettings.Default.CurrentDir.TexturePlatform); + if (texture is UTextureCube) + { + img = img?.ToPanorama(); + } + + AddImage(texture.Name, texture.RenderNearestNeighbor, img, save, updateUi); + } public void AddImage(string name, bool rnn, SKBitmap[] img, bool save, bool updateUi) { @@ -266,15 +284,6 @@ public void SetDocumentText(string text, bool save, bool updateUi) }); } - public void ResetDocumentText() - { - Application.Current.Dispatcher.Invoke(() => - { - Document ??= new TextDocument(); - Document.Text = string.Empty; - }); - } - public void SaveImage() => SaveImage(SelectedImage, true); private void SaveImage(TabImage image, bool updateUi) { @@ -360,12 +369,13 @@ public TabControlViewModel() SelectedTab = TabsItems.FirstOrDefault(); } - public void AddTab(string header = null, string directory = null) + public void AddTab(string header = null, string directory = null, string parentExportType = null) { if (!CanAddTabs) return; var h = header ?? "New Tab"; var d = directory ?? string.Empty; + var p = parentExportType ?? string.Empty; if (SelectedTab is { Header : "New Tab" }) { SelectedTab.Header = h; @@ -375,7 +385,7 @@ public void AddTab(string header = null, string directory = null) Application.Current.Dispatcher.Invoke(() => { - _tabItems.Add(new TabItem(h, d)); + _tabItems.Add(new TabItem(h, d, p)); SelectedTab = _tabItems.Last(); }); } @@ -437,6 +447,6 @@ public void RemoveAllTabs() private static IEnumerable EnumerateTabs() { - yield return new TabItem("New Tab", string.Empty); + yield return new TabItem("New Tab", string.Empty, string.Empty); } } diff --git a/FModel/Views/DirectorySelector.xaml b/FModel/Views/DirectorySelector.xaml index cf3d3c1a..badcfc23 100644 --- a/FModel/Views/DirectorySelector.xaml +++ b/FModel/Views/DirectorySelector.xaml @@ -61,14 +61,27 @@ - + + + + @@ -83,7 +96,9 @@ + + diff --git a/FModel/Views/DirectorySelector.xaml.cs b/FModel/Views/DirectorySelector.xaml.cs index 8b51717f..9605352f 100644 --- a/FModel/Views/DirectorySelector.xaml.cs +++ b/FModel/Views/DirectorySelector.xaml.cs @@ -1,6 +1,7 @@ using FModel.ViewModels; using Ookii.Dialogs.Wpf; using System.Windows; +using CUE4Parse.Utils; namespace FModel.Views; @@ -38,6 +39,7 @@ private void OnBrowseManualDirectories(object sender, RoutedEventArgs e) var folderBrowser = new VistaFolderBrowserDialog {ShowNewFolderButton = false}; if (folderBrowser.ShowDialog() == true) { + HelloMyNameIsGame.Text = folderBrowser.SelectedPath.SubstringAfterLast('\\'); HelloGameMyNameIsDirectory.Text = folderBrowser.SelectedPath; } } diff --git a/FModel/Views/ImageMerger.xaml.cs b/FModel/Views/ImageMerger.xaml.cs index a8cc6a99..6190973d 100644 --- a/FModel/Views/ImageMerger.xaml.cs +++ b/FModel/Views/ImageMerger.xaml.cs @@ -142,7 +142,7 @@ private async void OnImageAdd(object sender, RoutedEventArgs e) var fileBrowser = new OpenFileDialog { Title = "Add image(s)", - InitialDirectory = $"{UserSettings.Default.OutputDirectory}\\Exports", + InitialDirectory = Path.Combine(UserSettings.Default.OutputDirectory, "Exports"), Multiselect = true, Filter = "Image Files (*.png,*.bmp,*.jpg,*.jpeg,*.jfif,*.jpe,*.tiff,*.tif)|*.png;*.bmp;*.jpg;*.jpeg;*.jfif;*.jpe;*.tiff;*.tif|All Files (*.*)|*.*" }; diff --git a/FModel/Views/Resources/Controls/Aed/GamePathElementGenerator.cs b/FModel/Views/Resources/Controls/Aed/GamePathElementGenerator.cs index 914309e8..dbf83c0a 100644 --- a/FModel/Views/Resources/Controls/Aed/GamePathElementGenerator.cs +++ b/FModel/Views/Resources/Controls/Aed/GamePathElementGenerator.cs @@ -1,4 +1,5 @@ using System.Text.RegularExpressions; +using FModel.Extensions; using ICSharpCode.AvalonEdit.Rendering; namespace FModel.Views.Resources.Controls; @@ -29,8 +30,10 @@ public override int GetFirstInterestedOffset(int startOffset) public override VisualLineElement ConstructElement(int offset) { var m = FindMatch(offset); - if (!m.Success || m.Index != 0) return null; + if (!m.Success || m.Index != 0 || + !m.Groups.TryGetValue("target", out var g)) return null; - return m.Groups.TryGetValue("target", out var g) ? new GamePathVisualLineText(g.Value, CurrentContext.VisualLine, g.Length + g.Index + 1) : null; + var parentExportType = CurrentContext.Document.GetParentExportType(offset); + return new GamePathVisualLineText(g.Value, parentExportType, CurrentContext.VisualLine, g.Length + g.Index + 1); } -} \ No newline at end of file +} diff --git a/FModel/Views/Resources/Controls/Aed/GamePathVisualLineText.cs b/FModel/Views/Resources/Controls/Aed/GamePathVisualLineText.cs index 426891a0..d0dedb59 100644 --- a/FModel/Views/Resources/Controls/Aed/GamePathVisualLineText.cs +++ b/FModel/Views/Resources/Controls/Aed/GamePathVisualLineText.cs @@ -16,14 +16,16 @@ public class GamePathVisualLineText : VisualLineText private ThreadWorkerViewModel _threadWorkerView => ApplicationService.ThreadWorkerView; private ApplicationViewModel _applicationView => ApplicationService.ApplicationView; - public delegate void GamePathOnClick(string gamePath); + public delegate void GamePathOnClick(string gamePath, string parentExportType); public event GamePathOnClick OnGamePathClicked; private readonly string _gamePath; + private readonly string _parentExportType; - public GamePathVisualLineText(string gamePath, VisualLine parentVisualLine, int length) : base(parentVisualLine, length) + public GamePathVisualLineText(string gamePath, string parentExportType, VisualLine parentVisualLine, int length) : base(parentVisualLine, length) { _gamePath = gamePath; + _parentExportType = parentExportType; } public override TextRun CreateTextRun(int startVisualColumn, ITextRunConstructionContext context) @@ -56,14 +58,14 @@ protected override void OnMouseDown(MouseButtonEventArgs e) if (e.Handled || OnGamePathClicked == null) return; - OnGamePathClicked(_gamePath); + OnGamePathClicked(_gamePath, _parentExportType); e.Handled = true; } protected override VisualLineText CreateInstance(int length) { - var a = new GamePathVisualLineText(_gamePath, ParentVisualLine, length); - a.OnGamePathClicked += async gamePath => + var a = new GamePathVisualLineText(_gamePath, _parentExportType, ParentVisualLine, length); + a.OnGamePathClicked += async (gamePath, parentExportType) => { var obj = gamePath.SubstringAfterLast('.'); var package = gamePath.SubstringBeforeLast('.'); @@ -80,17 +82,17 @@ protected override VisualLineText CreateInstance(int length) } else { - lineNumber = a.ParentVisualLine.Document.Text.GetLineNumber(obj); + lineNumber = a.ParentVisualLine.Document.Text.GetNameLineNumber(obj); line = a.ParentVisualLine.Document.GetLineByNumber(lineNumber); } - + AvalonEditor.YesWeEditor.Select(line.Offset, line.Length); AvalonEditor.YesWeEditor.ScrollToLine(lineNumber); } else { await _threadWorkerView.Begin(cancellationToken => - _applicationView.CUE4Parse.ExtractAndScroll(cancellationToken, fullPath, obj)); + _applicationView.CUE4Parse.ExtractAndScroll(cancellationToken, fullPath, obj, parentExportType)); } }; return a; diff --git a/FModel/Views/Resources/Controls/AvalonEditor.xaml.cs b/FModel/Views/Resources/Controls/AvalonEditor.xaml.cs index 04e5ed5e..f09fa3f8 100644 --- a/FModel/Views/Resources/Controls/AvalonEditor.xaml.cs +++ b/FModel/Views/Resources/Controls/AvalonEditor.xaml.cs @@ -126,7 +126,7 @@ private void OnTextChanged(object sender, EventArgs e) if (!tabItem.ShouldScroll) return; - var lineNumber = avalonEditor.Document.Text.GetLineNumber(tabItem.ScrollTrigger); + var lineNumber = avalonEditor.Document.Text.GetNameLineNumber(tabItem.ScrollTrigger); var line = avalonEditor.Document.GetLineByNumber(lineNumber); avalonEditor.Select(line.Offset, line.Length); avalonEditor.ScrollToLine(lineNumber); @@ -223,10 +223,9 @@ private void OnCloseClick(object sender, RoutedEventArgs e) private void OnTabClose(object sender, EventArgs eventArgs) { - if (eventArgs is not TabControlViewModel.TabEventArgs e || e.TabToRemove.Document == null) + if (eventArgs is not TabControlViewModel.TabEventArgs e || e.TabToRemove.Document?.FileName is not { } fileName) return; - var fileName = e.TabToRemove.Document.FileName; if (_savedCarets.ContainsKey(fileName)) _savedCarets.Remove(fileName); } diff --git a/FModel/Views/Resources/Resources.xaml b/FModel/Views/Resources/Resources.xaml index 61f071b9..4cd0164e 100644 --- a/FModel/Views/Resources/Resources.xaml +++ b/FModel/Views/Resources/Resources.xaml @@ -80,6 +80,31 @@ + + + + + @@ -330,7 +345,7 @@