diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8c5302d2..14fbb2ab 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,7 +1,12 @@ -name: Artifact Generator +name: FModel Builder on: workflow_dispatch: + inputs: + appVersion: + description: 'FModel Version And Release Tag' + required: true + default: '4.0.X.X' jobs: build: @@ -12,7 +17,6 @@ jobs: uses: actions/checkout@v2 with: submodules: 'true' - token: ${{ secrets.PAT_TOKEN }} - name: .NET 5 Setup uses: actions/setup-dotnet@v1 @@ -23,10 +27,19 @@ jobs: run: dotnet restore FModel - name: .NET Publish - run: dotnet publish FModel -c Release -f net5.0-windows -o "./FModel/bin/Publish/" -p:PublishReadyToRun=true -p:PublishSingleFile=true -p:DebugType=None -p:GenerateDocumentationFile=false -p:DebugSymbols=false --no-self-contained -r win-x64 + run: dotnet publish FModel -c Release -f net5.0-windows -o "./FModel/bin/Publish/" -p:PublishReadyToRun=true -p:PublishSingleFile=true -p:DebugType=None -p:GenerateDocumentationFile=false -p:DebugSymbols=false -p:AssemblyVersion=${{ github.event.inputs.appVersion }} -p:FileVersion=${{ github.event.inputs.appVersion }} --no-self-contained -r win-x64 - - name: EXE Upload - uses: actions/upload-artifact@v2 + - name: ZIP File + uses: papeloto/action-zip@v1 with: - name: FModel - path: D:\a\FModel\FModel\FModel\bin\Publish\ + files: ./FModel/bin/Publish/FModel.exe + dest: FModel.zip # will end up in working directory not the Publish folder + + - name: GIT Release + uses: marvinpinto/action-automatic-releases@latest + with: + title: "FModel v${{ github.event.inputs.appVersion }}" + automatic_release_tag: ${{ github.event.inputs.appVersion }} + repo_token: ${{ secrets.GITHUB_TOKEN }} + prerelease: false + files: FModel.zip \ No newline at end of file diff --git a/CUE4Parse b/CUE4Parse index 70d41700..a520ada3 160000 --- a/CUE4Parse +++ b/CUE4Parse @@ -1 +1 @@ -Subproject commit 70d417009d729260486d905eb5d10f45c78cfec7 +Subproject commit a520ada39b9736d06db1df3321c7695873b23c44 diff --git a/FModel/App.xaml.cs b/FModel/App.xaml.cs index c0692264..6d8751ec 100644 --- a/FModel/App.xaml.cs +++ b/FModel/App.xaml.cs @@ -23,8 +23,14 @@ namespace FModel /// public partial class App { + [DllImport("kernel32.dll")] + private static extern bool AttachConsole(int dwProcessId); + protected override void OnStartup(StartupEventArgs e) { +#if DEBUG + AttachConsole(-1); +#endif base.OnStartup(e); try diff --git a/FModel/Constants.cs b/FModel/Constants.cs index 2a4603da..a33827d5 100644 --- a/FModel/Constants.cs +++ b/FModel/Constants.cs @@ -1,5 +1,4 @@ -using System; -using System.Reflection; +using System.Reflection; using CUE4Parse.UE4.Objects.Core.Misc; namespace FModel @@ -23,41 +22,5 @@ namespace FModel public const string _FN_LIVE_TRIGGER = "fortnite-live.manifest"; public const string _VAL_LIVE_TRIGGER = "valorant-live.manifest"; - - public static string GetRandomColor() - { - return _randomColors[_random.Next(0, 255)]; - } - - private static readonly Random _random = new(Environment.TickCount); - private static readonly string[] _randomColors = - { - "F44336", "FFEBEE", "FFCDD2", "EF9A9A", "E57373", "EF5350", "E53935", "D32F2F", "C62828", "B71C1C", - "FF8A80", "FF5252", "FF1744", "D50000", "FCE4EC", "F8BBD0", "F48FB1", "F06292", "EC407A", "E91E63", - "D81B60", "C2185B", "AD1457", "880E4F", "FF80AB", "FF4081", "F50057", "C51162", "F3E5F5", "E1BEE7", - "CE93D8", "BA68C8", "AB47BC", "9C27B0", "8E24AA", "7B1FA2", "6A1B9A", "4A148C", "EA80FC", "E040FB", - "D500F9", "AA00FF", "EDE7F6", "D1C4E9", "B39DDB", "9575CD", "7E57C2", "673AB7", "5E35B1", "512DA8", - "4527A0", "311B92", "B388FF", "7C4DFF", "651FFF", "6200EA", "E8EAF6", "C5CAE9", "9FA8DA", "7986CB", - "5C6BC0", "3F51B5", "3949AB", "303F9F", "283593", "1A237E", "8C9EFF", "536DFE", "3D5AFE", "304FFE", - "E3F2FD", "BBDEFB", "90CAF9", "64B5F6", "42A5F5", "2196F3", "1E88E5", "1976D2", "1565C0", "0D47A1", - "82B1FF", "448AFF", "2979FF", "2962FF", "E1F5FE", "B3E5FC", "81D4FA", "4FC3F7", "29B6F6", "03A9F4", - "039BE5", "0288D1", "0277BD", "01579B", "80D8FF", "40C4FF", "00B0FF", "0091EA", "E0F7FA", "B2EBF2", - "80DEEA", "4DD0E1", "26C6DA", "00BCD4", "00ACC1", "0097A7", "00838F", "006064", "84FFFF", "18FFFF", - "00E5FF", "00B8D4", "E0F2F1", "B2DFDB", "80CBC4", "4DB6AC", "26A69A", "009688", "00897B", "00796B", - "00695C", "004D40", "A7FFEB", "64FFDA", "1DE9B6", "00BFA5", "E8F5E9", "C8E6C9", "A5D6A7", "81C784", - "66BB6A", "4CAF50", "43A047", "388E3C", "2E7D32", "1B5E20", "B9F6CA", "69F0AE", "00E676", "00C853", - "F1F8E9", "DCEDC8", "C5E1A5", "AED581", "9CCC65", "8BC34A", "7CB342", "689F38", "558B2F", "33691E", - "CCFF90", "B2FF59", "76FF03", "64DD17", "F9FBE7", "F0F4C3", "E6EE9C", "DCE775", "D4E157", "CDDC39", - "C0CA33", "AFB42B", "9E9D24", "827717", "F4FF81", "EEFF41", "C6FF00", "AEEA00", "FFFDE7", "FFF9C4", - "FFF59D", "FFF176", "FFEE58", "FFEB3B", "FDD835", "FBC02D", "F9A825", "F57F17", "FFFF8D", "FFFF00", - "FFEA00", "FFD600", "FFF8E1", "FFECB3", "FFE082", "FFD54F", "FFCA28", "FFC107", "FFB300", "FFA000", - "FF8F00", "FF6F00", "FFE57F", "FFD740", "FFC400", "FFAB00", "FFF3E0", "FFE0B2", "FFCC80", "FFB74D", - "FFA726", "FF9800", "FB8C00", "F57C00", "EF6C00", "E65100", "FFD180", "FFAB40", "FF9100", "FF6D00", - "FBE9E7", "FFCCBC", "FFAB91", "FF8A65", "FF7043", "FF5722", "F4511E", "E64A19", "D84315", "BF360C", - "FF9E80", "FF6E40", "FF3D00", "DD2C00", "EFEBE9", "D7CCC8", "BCAAA4", "A1887F", "8D6E63", "795548", - "6D4C41", "5D4037", "4E342E", "3E2723", "FAFAFA", "F5F5F5", "EEEEEE", "E0E0E0", "BDBDBD", "9E9E9E", - "757575", "616161", "424242", "212121", "ECEFF1", "CFD8DC", "B0BEC5", "90A4AE", "78909C", "607D8B", - "546E7A", "455A64", "37474F", "263238", "000000", - }; } } \ No newline at end of file diff --git a/FModel/Creator/Bases/FN/BaseCommunity.cs b/FModel/Creator/Bases/FN/BaseCommunity.cs index 2f139665..78c2fe30 100644 --- a/FModel/Creator/Bases/FN/BaseCommunity.cs +++ b/FModel/Creator/Bases/FN/BaseCommunity.cs @@ -88,8 +88,8 @@ namespace FModel.Creator.Bases.FN if (_design.DrawSeason && gameplayTags.TryGetGameplayTag("Cosmetics.Filter.Season.", out var season)) _season = GetCosmeticSeason(season.Text, _design.DrawSeasonShort); - GetUserFacingFlags(gameplayTags.GetAllGameplayTags( - "Cosmetics.UserFacingFlags.", "Homebase.Class.", "NPC.CharacterType.Survivor.Defender.")); + var triggers = _design.GameplayTags.DrawCustomOnly ? new[] {"Cosmetics.UserFacingFlags."} : new[] {"Cosmetics.UserFacingFlags.", "Homebase.Class.", "NPC.CharacterType.Survivor.Defender."}; + GetUserFacingFlags(gameplayTags.GetAllGameplayTags(triggers)); } private string GetCosmeticSet(string setName, bool bShort) @@ -290,7 +290,7 @@ namespace FModel.Creator.Bases.FN private void DrawUserFacingFlags(SKCanvas c, bool customOnly) { - if (UserFacingFlags == null || UserFacingFlags.Length < 1) return; + if (UserFacingFlags == null || UserFacingFlags.Count < 1) return; if (customOnly) { c.DrawBitmap(_design.GameplayTags.Custom, 0, 0, ImagePaint); diff --git a/FModel/Creator/Bases/FN/BaseIcon.cs b/FModel/Creator/Bases/FN/BaseIcon.cs index be861db1..6ecfce2b 100644 --- a/FModel/Creator/Bases/FN/BaseIcon.cs +++ b/FModel/Creator/Bases/FN/BaseIcon.cs @@ -23,7 +23,7 @@ namespace FModel.Creator.Bases.FN public SKBitmap SeriesBackground { get; protected set; } protected string ShortDescription { get; set; } protected string CosmeticSource { get; set; } - protected SKBitmap[] UserFacingFlags { get; set; } + protected Dictionary UserFacingFlags { get; set; } public BaseIcon(UObject uObject, EIconStyle style) : base(uObject, style) { @@ -269,24 +269,24 @@ namespace FModel.Creator.Bases.FN if (!itemCategories.TryGetValue(out FStructFallback[] tertiaryCategories, "TertiaryCategories")) return; - UserFacingFlags = new SKBitmap[userFacingFlags.Count]; - for (var i = 0; i < UserFacingFlags.Length; i++) + UserFacingFlags = new Dictionary(userFacingFlags.Count); + foreach (var flag in userFacingFlags) { - if (userFacingFlags[i].Equals("Cosmetics.UserFacingFlags.HasUpgradeQuests", StringComparison.OrdinalIgnoreCase)) + if (flag.Equals("Cosmetics.UserFacingFlags.HasUpgradeQuests", StringComparison.OrdinalIgnoreCase)) { if (Object.ExportType.Equals("AthenaPetCarrierItemDefinition", StringComparison.OrdinalIgnoreCase)) - UserFacingFlags[i] = SKBitmap.Decode(Application.GetResourceStream(new Uri("pack://application:,,,/Resources/T-Icon-Pets-64.png"))?.Stream); - else UserFacingFlags[i] = SKBitmap.Decode(Application.GetResourceStream(new Uri("pack://application:,,,/Resources/T-Icon-Quests-64.png"))?.Stream); + 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(userFacingFlags[i], out _) && + 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[i] = Utils.GetBitmap(texture); + UserFacingFlags[flag] = Utils.GetBitmap(texture); } } } @@ -299,7 +299,7 @@ namespace FModel.Creator.Bases.FN const int size = 25; var x = Margin * (int) 2.5; - foreach (var flag in UserFacingFlags) + foreach (var flag in UserFacingFlags.Values) { if (flag == null) continue; diff --git a/FModel/Creator/Bases/FN/BaseMaterialInstance.cs b/FModel/Creator/Bases/FN/BaseMaterialInstance.cs index 1578d40b..31f211ae 100644 --- a/FModel/Creator/Bases/FN/BaseMaterialInstance.cs +++ b/FModel/Creator/Bases/FN/BaseMaterialInstance.cs @@ -19,6 +19,7 @@ namespace FModel.Creator.Bases.FN { if (Object is not UMaterialInstanceConstant material) return; + texture_finding: foreach (var textureParameter in material.TextureParameterValues) // get texture from base material { if (textureParameter.ParameterValue is not UTexture2D texture || Preview != null) continue; @@ -45,6 +46,9 @@ namespace FModel.Creator.Bases.FN if (material == null) return; } + if (Preview == null) + goto texture_finding; + foreach (var vectorParameter in material.VectorParameterValues) { if (vectorParameter.ParameterValue == null) continue; diff --git a/FModel/Creator/Bases/FN/BaseQuest.cs b/FModel/Creator/Bases/FN/BaseQuest.cs index 1baafaeb..b4a1df51 100644 --- a/FModel/Creator/Bases/FN/BaseQuest.cs +++ b/FModel/Creator/Bases/FN/BaseQuest.cs @@ -114,7 +114,7 @@ namespace FModel.Creator.Bases.FN } else if (!_unauthorizedReward.Contains(name.Text)) { - _reward = new Reward(quantity, primaryAssetName); + _reward = new Reward(quantity, $"{name}:{primaryAssetName}"); } } } @@ -256,4 +256,4 @@ namespace FModel.Creator.Bases.FN _reward.DrawQuest(c, new SKRect(Height, outY + 25, Width - 20, y + Height - 25)); } } -} \ No newline at end of file +} diff --git a/FModel/Creator/Bases/FN/BaseSeason.cs b/FModel/Creator/Bases/FN/BaseSeason.cs index c39be25c..a4f9a3cd 100644 --- a/FModel/Creator/Bases/FN/BaseSeason.cs +++ b/FModel/Creator/Bases/FN/BaseSeason.cs @@ -1,5 +1,5 @@ using System; -using System.Collections.Generic; +using System.Linq; using CUE4Parse.UE4.Assets.Exports; using CUE4Parse.UE4.Assets.Objects; using CUE4Parse.UE4.Objects.Core.i18N; @@ -10,10 +10,17 @@ using SkiaSharp.HarfBuzz; namespace FModel.Creator.Bases.FN { + public class Page + { + public int LevelsNeededForUnlock; + public int RewardsNeededForUnlock; + public Reward[] RewardEntryList; + } + public class BaseSeason : UCreator { private Reward _firstWinReward; - private Dictionary> _bookXpSchedule; + private Page[] _bookXpSchedule; private const int _headerHeight = 150; // keep the list because rewards are ordered by least to most important // we only care about the most but we also have filters so we can't just take the last reward @@ -27,8 +34,8 @@ namespace FModel.Creator.Bases.FN public override void ParseForInfo() { - _bookXpSchedule = new Dictionary>(); - + _bookXpSchedule = Array.Empty(); + if (Object.TryGetValue(out FText displayName, "DisplayName")) DisplayName = displayName.Text.ToUpperInvariant(); @@ -45,56 +52,48 @@ namespace FModel.Creator.Bases.FN } } - var freeLevels = Array.Empty(); - var paidLevels = Array.Empty(); - if (Object.TryGetValue(out FPackageIndex[] additionalSeasonData, "AdditionalSeasonData") && - additionalSeasonData.Length > 0 && Utils.TryGetPackageIndexExport(additionalSeasonData[0], out UObject data) && - data.TryGetValue(out FStructFallback battlePassXpScheduleFree, "BattlePassXpScheduleFree") && - battlePassXpScheduleFree.TryGetValue(out freeLevels, "Levels") && - data.TryGetValue(out FStructFallback battlePassXpSchedulePaid, "BattlePassXpSchedulePaid") && - battlePassXpSchedulePaid.TryGetValue(out paidLevels, "Levels")) + if (Object.TryGetValue(out FPackageIndex[] additionalSeasonData, "AdditionalSeasonData")) { - // we got them boys - } - else if (Object.TryGetValue(out FStructFallback bookXpScheduleFree, "BookXpScheduleFree") && - bookXpScheduleFree.TryGetValue(out freeLevels, "Levels") && - Object.TryGetValue(out FStructFallback bookXpSchedulePaid, "BookXpSchedulePaid") && - bookXpSchedulePaid.TryGetValue(out paidLevels, "Levels")) - { - // we got them boys - } - - for (var i = 0; i < freeLevels.Length; i++) - { - _bookXpSchedule[i] = new List(); - if (!freeLevels[i].TryGetValue(out rewards, "Rewards")) continue; - - foreach (var reward in rewards) + foreach (var data in additionalSeasonData) { - if (!reward.TryGetValue(out FSoftObjectPath itemDefinition, "ItemDefinition") || - itemDefinition.AssetPathName.Text.Contains("/Items/Tokens/") || - !Utils.TryLoadObject(itemDefinition.AssetPathName.Text, out UObject uObject)) continue; - - _bookXpSchedule[i].Add(new Reward(uObject)); + if (!Utils.TryGetPackageIndexExport(data, out UObject packageIndex) || + !packageIndex.TryGetValue(out FStructFallback[] pageList, "PageList")) continue; + + var i = 0; + _bookXpSchedule = new Page[pageList.Length]; + foreach (var page in pageList) + { + if (!page.TryGetValue(out int levelsNeededForUnlock, "LevelsNeededForUnlock") || + !page.TryGetValue(out int rewardsNeededForUnlock, "RewardsNeededForUnlock") || + !page.TryGetValue(out FPackageIndex[] rewardEntryList, "RewardEntryList")) + continue; + + var p = new Page + { + LevelsNeededForUnlock = levelsNeededForUnlock, + RewardsNeededForUnlock = rewardsNeededForUnlock, + RewardEntryList = new Reward[rewardEntryList.Length] + }; + + for (var j = 0; j < p.RewardEntryList.Length; j++) + { + if (!Utils.TryGetPackageIndexExport(rewardEntryList[j], out packageIndex) || + !packageIndex.TryGetValue(out FStructFallback battlePassOffer, "BattlePassOffer") || + !battlePassOffer.TryGetValue(out FStructFallback rewardItem, "RewardItem") || + !rewardItem.TryGetValue(out FSoftObjectPath itemDefinition, "ItemDefinition") || + !Utils.TryLoadObject(itemDefinition.AssetPathName.Text, out UObject uObject)) continue; + + p.RewardEntryList[j] = new Reward(uObject); + } + + _bookXpSchedule[i++] = p; + } + break; } } - for (var i = 0; i < paidLevels.Length; i++) - { - if (!paidLevels[i].TryGetValue(out rewards, "Rewards")) continue; - - foreach (var reward in rewards) - { - if (!reward.TryGetValue(out FSoftObjectPath itemDefinition, "ItemDefinition") || - !Utils.TryLoadObject(itemDefinition.AssetPathName.Text, out UObject uObject)) continue; - - _bookXpSchedule[i].Add(new Reward(uObject)); - break; - } - } - - Height += 100 * _bookXpSchedule.Count / 10; + Height += 100 * _bookXpSchedule.Sum(x => x.RewardEntryList.Length) / _bookXpSchedule.Length; } public override SKImage Draw() @@ -151,23 +150,15 @@ namespace FModel.Creator.Bases.FN { var x = 20; var y = _headerHeight + 50; - foreach (var (index, reward) in _bookXpSchedule) + foreach (var page in _bookXpSchedule) { - if (index == 0 || reward.Count == 0 || !reward[0].HasReward()) - continue; - - c.DrawText(index.ToString(), new SKPoint(x + _DEFAULT_AREA_SIZE / 2, y - 5), _bookPaint); - reward[0].DrawSeason(c, x, y, _DEFAULT_AREA_SIZE); - - if (index != 1 && index % 10 == 0) - { - y += _DEFAULT_AREA_SIZE + 20; - x = 20; - } - else + foreach (var reward in page.RewardEntryList) { + reward.DrawSeason(c, x, y, _DEFAULT_AREA_SIZE); x += _DEFAULT_AREA_SIZE + 20; } + y += _DEFAULT_AREA_SIZE + 20; + x = 20; } } } diff --git a/FModel/Creator/Bases/FN/Reward.cs b/FModel/Creator/Bases/FN/Reward.cs index cef0b920..0b2b287d 100644 --- a/FModel/Creator/Bases/FN/Reward.cs +++ b/FModel/Creator/Bases/FN/Reward.cs @@ -120,13 +120,13 @@ namespace FModel.Creator.Bases.FN _theReward = new BaseIcon(null, EIconStyle.Default); _theReward.Border[0] = SKColor.Parse("FFDB67"); _theReward.Background[0] = SKColor.Parse("8F4A20"); - _theReward.Preview = Utils.GetBitmap("FortniteGame/Content/UI/Foundation/Textures/Icons/Items/T-FNBR-BattlePoints.T-FNBR-BattlePoints"); + _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-XPMedium.T-FNBR-XPMedium"); + _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); diff --git a/FModel/Creator/Bases/SOD2/BaseDecayIcon.cs b/FModel/Creator/Bases/SOD2/BaseDecayIcon.cs new file mode 100644 index 00000000..3dbe6ced --- /dev/null +++ b/FModel/Creator/Bases/SOD2/BaseDecayIcon.cs @@ -0,0 +1,98 @@ +using CUE4Parse.UE4.Assets.Exports; +using CUE4Parse.UE4.Assets.Objects; +using CUE4Parse.UE4.Objects.Core.i18N; +using FModel.Creator.Bases.FN; +using SkiaSharp; + +namespace FModel.Creator.Bases.SOD2 +{ + public class BaseDecayIcon : BaseIcon + { + private int _maxStackCount; + private readonly SKBitmap _backgroundOverlay; + + public BaseDecayIcon(UObject uObject, EIconStyle style) : base(uObject, style) + { + Margin = 0; + Background = new[] {SKColor.Parse("000000"), SKColor.Parse("B24E18")}; + _backgroundOverlay = Utils.GetBitmap("StateOfDecay2/Content/Art/UI/settings/icon_sod_eagle_09_square.icon_sod_eagle_09_square").Resize(512); + } + + public override void ParseForInfo() + { + if (Object.TryGetValue(out FStructFallback stackingInfo, "StackingInfo") && + stackingInfo.TryGetValue(out int maxStackCount, "MaxStackCount")) + _maxStackCount = maxStackCount; + + if (Object.Class.SuperStruct != null && Utils.TryGetPackageIndexExport(Object.Class.SuperStruct, out UObject t)) + { + // TODO + } + + if (Object.TryGetValue(out FStructFallback displayInfo, "DisplayInfo")) + { + if (displayInfo.TryGetValue(out FText displayName, "DisplayName")) + DisplayName = displayName.Text; + if (displayInfo.TryGetValue(out FText displayDescription, "DisplayDescription")) + Description = displayDescription.Text; + + if (displayInfo.TryGetValue(out string iconFills, "IconFills")) + Preview = Utils.GetBitmap(iconFills); + } + } + + // TODO + // + // ExtraLargeBackpackBase - for extra large + // SmallBackpackBase - for small + // ExtraSmallBackpackBase - for extra small + // MediumBackpackBase - for medium + // LargeBackpackBase - for large + + public override SKImage Draw() + { + using var ret = new SKBitmap(Width, Height, SKColorType.Rgba8888, SKAlphaType.Premul); + using var c = new SKCanvas(ret); + + DrawBackground(c); + DrawPreview(c); + + return SKImage.FromBitmap(ret); + } + + private new void DrawPreview(SKCanvas c) + { + c.DrawBitmap(Preview ?? DefaultPreview, new SKRect(Margin, Margin, Width - Margin, Height - Margin), new SKPaint + { + // BlendMode = SKBlendMode.SrcATop -- Need Asval's assistance. + }); + } + + private new void DrawBackground(SKCanvas c) + { + c.DrawRect(new SKRect(Margin, Margin, Width - Margin, Height - Margin), + new SKPaint + { + IsAntialias = true, FilterQuality = SKFilterQuality.High, + Shader = SKShader.CreateRadialGradient(new SKPoint(Width / 2, Height / 2), Width / 5 * 2, + new[] {Background[0], Background[1]}, + SKShaderTileMode.Clamp) + }); + + for (var i = 0; i < _backgroundOverlay.Width; i++) + for (var j = 0; j < _backgroundOverlay.Height; j++) + if (_backgroundOverlay.GetPixel(i, j) == SKColors.Black) + { + _backgroundOverlay.SetPixel(i, j, SKColors.Transparent); + } + + c.DrawBitmap(_backgroundOverlay, new SKRect(Margin, Margin, Width - Margin, Height - Margin), new SKPaint + { + IsAntialias = true, + ColorFilter = SKColorFilter.CreateBlendMode(SKColors.Black.WithAlpha(150), SKBlendMode.DstIn), + ImageFilter = SKImageFilter.CreateDropShadow(2, 2, 4, 4, new SKColor(0, 0, 0)) + }); + c.DrawColor(SKColors.Black.WithAlpha(125), SKBlendMode.DstIn); + } + } +} \ No newline at end of file diff --git a/FModel/Creator/CreatorPackage.cs b/FModel/Creator/CreatorPackage.cs index 72d852ea..6ac1382b 100644 --- a/FModel/Creator/CreatorPackage.cs +++ b/FModel/Creator/CreatorPackage.cs @@ -5,6 +5,7 @@ using FModel.Creator.Bases; using FModel.Creator.Bases.BB; using FModel.Creator.Bases.FN; using FModel.Creator.Bases.SB; +using FModel.Creator.Bases.SOD2; namespace FModel.Creator { @@ -125,7 +126,8 @@ namespace FModel.Creator case "MaterialInstanceConstant" when _object.Owner != null && (_object.Owner.Name.EndsWith($"/MI_OfferImages/{_object.Name}") || - _object.Owner.Name.EndsWith($"/RenderSwitch_Materials/{_object.Name}")): + _object.Owner.Name.EndsWith($"/RenderSwitch_Materials/{_object.Name}") || + _object.Owner.Name.EndsWith($"/MI_BPTile/{_object.Name}")): creator = new BaseMaterialInstance(_object, _style); return true; case "FortMtxOfferData": @@ -225,10 +227,38 @@ namespace FModel.Creator case "GLeagueDivision": creator = new BaseDivision(_object, EIconStyle.Default); return true; - // TODO: Draw this properly - // case "GGameModeInfo": - // creator = new BaseGameModeInfo(_object, EIconStyle.Default); - // return true; + // State of Decay 2 + case "CureItem": + case "AmmoItem": + case "Pro_Brake_C": + case "BackpackItem": + case "MagicAmmoItem": + case "ConsumableItem": + case "MeleeWeaponItem": + case "CloseCombatItem": + case "FacilityModItem": + case "RangedWeaponItem": + case "MiscellaneousItem": + case "RepairVehicleItem": + case "ResourceItemBase_C": + case "FuelResourceBase_C": + case "MedsResourceBase_C": + case "PartsResourceBase_C": + case "RangedWeaponModItem": + case "VehicleDeliveryItem": + case "ConsumableBase_BP_C": + case "SmallBackpackBase_C": + case "LargeBackpackBase_C": + case "MediumBackpackBase_C": + case "ConsumableMedsBase_BP_C": + case "MaterialsResourceBase_C": + case "ExtraLargeBackpackBase_C": + case "ExtraSmallBackpackBase_C": + case "ConsumableStimsBase_BP_C": + case "Consumable_TimedStatBuff_Base_BP_C": + case "Consumable_SuspendFatigue_Base_BP_C": + creator = new BaseDecayIcon(_object, EIconStyle.Default); + return true; default: creator = null; return false; diff --git a/FModel/Creator/Utils.cs b/FModel/Creator/Utils.cs index 3be4a495..f451a22d 100644 --- a/FModel/Creator/Utils.cs +++ b/FModel/Creator/Utils.cs @@ -6,7 +6,6 @@ using System.Text.RegularExpressions; using CUE4Parse.UE4.Assets.Exports; using CUE4Parse.UE4.Assets.Exports.Material; using CUE4Parse.UE4.Assets.Exports.Texture; -using CUE4Parse.UE4.Assets.Objects; using CUE4Parse.UE4.Objects.UObject; using CUE4Parse.Utils; using CUE4Parse_Conversion.Textures; @@ -38,12 +37,6 @@ namespace FModel.Creator public static bool TryGetDisplayAsset(UObject uObject, out SKBitmap preview) { - if (uObject.TryGetValue(out FSoftObjectPath displayAsset, "DisplayAssetPath")) - { - preview = GetDisplayAsset(displayAsset); - return preview != null; - } - if (uObject.TryGetValue(out FSoftObjectPath sidePanelIcon, "SidePanelIcon")) { preview = GetBitmap(sidePanelIcon); @@ -61,35 +54,22 @@ namespace FModel.Creator return preview != null; } - public static SKBitmap GetDisplayAsset(FSoftObjectPath path) - { - if (!TryLoadObject(path.AssetPathName.Text, out UObject obj)) return null; - - if (obj.TryGetValue(out FStructFallback type, "DetailsImage") && - type.TryGetValue(out FPackageIndex resource, "ResourceObject") && resource.ResolvedObject?.Outer != null && - !resource.ResolvedObject.Outer.Name.Text.Contains("FortniteGame/Content/Athena/Prototype/Textures/")) - { - return GetBitmap(resource); - } - - return null; - } public static SKBitmap GetBitmap(FPackageIndex packageIndex) { while (true) { - if (!TryGetPackageIndexExport(packageIndex, out UExport export)) return null; + if (!TryGetPackageIndexExport(packageIndex, out UObject export)) return null; switch (export) { case UTexture2D texture: return GetBitmap(texture); case UMaterialInstanceConstant material: return GetBitmap(material); - case UObject uObject: + default: { - if (uObject.TryGetValue(out FSoftObjectPath previewImage, "LargePreviewImage", "SmallPreviewImage")) return GetBitmap(previewImage); - if (uObject.TryGetValue(out string largePreview, "LargePreviewImage")) return GetBitmap(largePreview); - if (uObject.TryGetValue(out FPackageIndex smallPreview, "SmallPreviewImage")) + if (export.TryGetValue(out FSoftObjectPath previewImage, "LargePreviewImage", "SmallPreviewImage")) return GetBitmap(previewImage); + if (export.TryGetValue(out string largePreview, "LargePreviewImage")) return GetBitmap(largePreview); + if (export.TryGetValue(out FPackageIndex smallPreview, "SmallPreviewImage")) { packageIndex = smallPreview; continue; @@ -97,8 +77,6 @@ namespace FModel.Creator return null; } - default: - return null; } } } @@ -139,7 +117,7 @@ namespace FModel.Creator return bmp; } - public static bool TryGetPackageIndexExport(FPackageIndex packageIndex, out T export) where T : UExport + public static bool TryGetPackageIndexExport(FPackageIndex packageIndex, out T export) where T : UObject { if (packageIndex.ResolvedObject == null) { @@ -172,19 +150,19 @@ namespace FModel.Creator } // fullpath must be either without any extension or with the export objectname - public static bool TryLoadObject(string fullPath, out T export) where T : UExport + public static bool TryLoadObject(string fullPath, out T export) where T : UObject { return _applicationView.CUE4Parse.Provider.TryLoadObject(fullPath, out export); } - public static IEnumerable LoadExports(string fullPath) + public static IEnumerable LoadExports(string fullPath) { return _applicationView.CUE4Parse.Provider.LoadObjectExports(fullPath); } - public static string GetLocalizedResource(string namespacee, string key, string defaultValue) + public static string GetLocalizedResource(string @namespace, string key, string defaultValue) { - return _applicationView.CUE4Parse.Provider.GetLocalizedString(namespacee, key, defaultValue); + return _applicationView.CUE4Parse.Provider.GetLocalizedString(@namespace, key, defaultValue); } public static string GetFullPath(string partialPath) diff --git a/FModel/Enums.cs b/FModel/Enums.cs index 802d5839..58dca396 100644 --- a/FModel/Enums.cs +++ b/FModel/Enums.cs @@ -88,7 +88,9 @@ namespace FModel [Description("Star Wars: Jedi Fallen Order")] SwGame, [Description("Core")] - Platform + Platform, + [Description("Days Gone")] + BendGame } public enum ELoadingMode diff --git a/FModel/FModel.csproj b/FModel/FModel.csproj index d040aea5..01cc0e98 100644 --- a/FModel/FModel.csproj +++ b/FModel/FModel.csproj @@ -6,13 +6,14 @@ true FModel.ico 4.0.0 - 4.0.0.1 - 4.0.0.1 + 4.0.1.0 + 4.0.1.0 false true win-x64 true true + FModel.App @@ -71,14 +72,13 @@ + - - @@ -162,14 +162,13 @@ + - - diff --git a/FModel/Framework/ViewModelCommand.cs b/FModel/Framework/ViewModelCommand.cs index 1730984e..4d064f2e 100644 --- a/FModel/Framework/ViewModelCommand.cs +++ b/FModel/Framework/ViewModelCommand.cs @@ -27,7 +27,7 @@ namespace FModel.Framework protected ViewModelCommand(TContextViewModel contextViewModel) { - ContextViewModel = contextViewModel ?? throw new ArgumentNullException(nameof(contextViewModel)); + ContextViewModel = contextViewModel /*?? throw new ArgumentNullException(nameof(contextViewModel))*/; } public sealed override void Execute(object parameter) diff --git a/FModel/MainWindow.xaml b/FModel/MainWindow.xaml index 0369c96e..9a0ef621 100644 --- a/FModel/MainWindow.xaml +++ b/FModel/MainWindow.xaml @@ -14,11 +14,8 @@ @@ -132,12 +129,12 @@ - + @@ -438,9 +435,10 @@ - - + + + diff --git a/FModel/MainWindow.xaml.cs b/FModel/MainWindow.xaml.cs index 1c9b1554..1219d593 100644 --- a/FModel/MainWindow.xaml.cs +++ b/FModel/MainWindow.xaml.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.ComponentModel; using System.Linq; using System.Windows; @@ -57,8 +57,10 @@ namespace FModel private async void OnLoaded(object sender, RoutedEventArgs e) { +#if !DEBUG ApplicationService.ApiEndpointView.FModelApi.CheckForUpdates(UserSettings.Default.UpdateMode); - +#endif + switch (UserSettings.Default.AesReload) { case EAesReload.Always: @@ -236,7 +238,7 @@ namespace FModel private void OnMouseDoubleClick(object sender, MouseButtonEventArgs e) { - if (sender is not ListBox listBox) return; + if (!_applicationView.IsReady || sender is not ListBox listBox) return; UserSettings.Default.LoadingMode = ELoadingMode.Multiple; _applicationView.LoadingModes.LoadCommand.Execute(listBox.SelectedItems); } diff --git a/FModel/Resources/city_pin.png b/FModel/Resources/city_pin.png deleted file mode 100644 index 1595e70b..00000000 Binary files a/FModel/Resources/city_pin.png and /dev/null differ diff --git a/FModel/Resources/pin.png b/FModel/Resources/pin.png deleted file mode 100644 index 502e2b56..00000000 Binary files a/FModel/Resources/pin.png and /dev/null differ diff --git a/FModel/Resources/stateofdecay2.png b/FModel/Resources/stateofdecay2.png new file mode 100644 index 00000000..2b505d96 Binary files /dev/null and b/FModel/Resources/stateofdecay2.png differ diff --git a/FModel/Services/DiscordService.cs b/FModel/Services/DiscordService.cs index 0b1380b5..d28a8273 100644 --- a/FModel/Services/DiscordService.cs +++ b/FModel/Services/DiscordService.cs @@ -22,7 +22,7 @@ namespace FModel.Services private readonly Assets _staticAssets = new() { - LargeImageKey = "official_logo", SmallImageKey = "verified", SmallImageText = $"v{Constants.APP_VERSION[..5]}" + LargeImageKey = "official_logo", SmallImageKey = "verified", SmallImageText = $"v{Constants.APP_VERSION}" }; private readonly Button[] _buttons = @@ -38,7 +38,7 @@ namespace FModel.Services Assets = _staticAssets, Timestamps = _timestamps, Buttons = _buttons, - State = $"{game.GetDescription()} - Idling" + Details = $"{game.GetDescription()} - Idling" }; _client.OnReady += (_, args) => Log.Information("{Username}#{Discriminator} ({UserId}) is now ready", args.User.Username, args.User.Discriminator, args.User.ID); @@ -60,7 +60,7 @@ namespace FModel.Services _client.Invoke(); } - public void UpdateButDontSavePresence(string details, string state) + public void UpdateButDontSavePresence(string details = null, string state = null) { if (!_client.IsInitialized) return; _client.SetPresence(new RichPresence @@ -68,8 +68,8 @@ namespace FModel.Services Assets = _staticAssets, Timestamps = _timestamps, Buttons = _buttons, - Details = details, - State = state + Details = details ?? _currentPresence.Details, + State = state ?? _currentPresence.State }); _client.Invoke(); } diff --git a/FModel/Settings/UserSettings.cs b/FModel/Settings/UserSettings.cs index 06b36fe9..759796e8 100644 --- a/FModel/Settings/UserSettings.cs +++ b/FModel/Settings/UserSettings.cs @@ -1,8 +1,10 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Windows.Input; using CUE4Parse.UE4.Versions; +using CUE4Parse_Conversion.Meshes; +using CUE4Parse_Conversion.Textures; using FModel.Framework; using FModel.ViewModels; using FModel.ViewModels.ApiEndpoints.Models; @@ -132,7 +134,7 @@ namespace FModel.Settings set => SetProperty(ref _loadingMode, value); } - private EUpdateMode _updateMode = EUpdateMode.Stable; + private EUpdateMode _updateMode = EUpdateMode.Beta; public EUpdateMode UpdateMode { get => _updateMode; @@ -205,12 +207,13 @@ namespace FModel.Settings {FGame.Dungeons, EGame.GAME_UE4_LATEST}, {FGame.WorldExplorers, EGame.GAME_UE4_LATEST}, {FGame.g3, EGame.GAME_UE4_22}, - {FGame.StateOfDecay2, EGame.GAME_UE4_LATEST}, + {FGame.StateOfDecay2, EGame.GAME_SOD2}, {FGame.Prospect, EGame.GAME_UE4_LATEST}, {FGame.Indiana, EGame.GAME_UE4_LATEST}, {FGame.RogueCompany, EGame.GAME_UE4_LATEST}, {FGame.SwGame, EGame.GAME_UE4_LATEST}, - {FGame.Platform, EGame.GAME_UE4_24} + {FGame.Platform, EGame.GAME_UE4_25}, + {FGame.BendGame, EGame.GAME_UE4_11} }; public IDictionary OverridedGame { @@ -233,7 +236,8 @@ namespace FModel.Settings {FGame.Indiana, UE4Version.VER_UE4_DETERMINE_BY_GAME}, {FGame.RogueCompany, UE4Version.VER_UE4_DETERMINE_BY_GAME}, {FGame.SwGame, UE4Version.VER_UE4_DETERMINE_BY_GAME}, - {FGame.Platform, UE4Version.VER_UE4_DETERMINE_BY_GAME} + {FGame.Platform, UE4Version.VER_UE4_DETERMINE_BY_GAME}, + {FGame.BendGame, UE4Version.VER_UE4_DETERMINE_BY_GAME} }; public IDictionary OverridedUEVersion { @@ -292,7 +296,8 @@ namespace FModel.Settings {FGame.Indiana, new List()}, {FGame.RogueCompany, new List()}, {FGame.SwGame, new List()}, - {FGame.Platform, new List()} + {FGame.Platform, new List()}, + {FGame.BendGame, new List()} }; public IDictionary> CustomDirectories { @@ -300,7 +305,7 @@ namespace FModel.Settings set => SetProperty(ref _customDirectories, value); } - private DateTime _lastAesReload = DateTime.Today; + private DateTime _lastAesReload = DateTime.Today.AddDays(-1); public DateTime LastAesReload { get => _lastAesReload; @@ -425,5 +430,26 @@ namespace FModel.Settings get => _nextAudio; set => SetProperty(ref _nextAudio, value); } + + private EMeshFormat _meshExportFormat = EMeshFormat.ActorX; + public EMeshFormat MeshExportFormat + { + get => _meshExportFormat; + set => SetProperty(ref _meshExportFormat, value); + } + + private ELodFormat _lodExportFormat = ELodFormat.FirstLod; + public ELodFormat LodExportFormat + { + get => _lodExportFormat; + set => SetProperty(ref _lodExportFormat, value); + } + + private ETextureFormat _textureExportFormat = ETextureFormat.Png; + public ETextureFormat TextureExportFormat + { + get => _textureExportFormat; + set => SetProperty(ref _textureExportFormat, value); + } } } \ No newline at end of file diff --git a/FModel/ViewModels/AesManagerViewModel.cs b/FModel/ViewModels/AesManagerViewModel.cs index aeb1c9e5..c81ca5eb 100644 --- a/FModel/ViewModels/AesManagerViewModel.cs +++ b/FModel/ViewModels/AesManagerViewModel.cs @@ -64,28 +64,35 @@ namespace FModel.ViewModels _keysFromSettings.MainKey = key; } + else if (!_keysFromSettings.HasDynamicKeys) + { + HasChange = true; + _keysFromSettings.DynamicKeys = new List + { + new() + { + Key = key, + FileName = collection[e.CollectionIndex].Name, + Guid = collection[e.CollectionIndex].Guid.ToString() + } + }; + } + else if (_keysFromSettings.DynamicKeys.FirstOrDefault(x => x.Guid == collection[e.CollectionIndex].Guid.ToString()) is { } d) + { + if (!HasChange) + HasChange = FixKey(d.Key) != key; + + d.Key = key; + } else { - if (!_keysFromSettings.HasDynamicKeys) + HasChange = true; + _keysFromSettings.DynamicKeys.Add(new DynamicKey { - HasChange = true; - _keysFromSettings.DynamicKeys = new[] - { - new DynamicKey - { - Key = key, - FileName = collection[e.CollectionIndex].Name, - Guid = collection[e.CollectionIndex].Guid.ToString() - } - }; - } - else if (_keysFromSettings.DynamicKeys.FirstOrDefault(x => x.Guid == collection[e.CollectionIndex].Guid.ToString()) is { } d) - { - if (!HasChange) - HasChange = FixKey(d.Key) != key; - - d.Key = key; - } + Key = key, + FileName = collection[e.CollectionIndex].Name, + Guid = collection[e.CollectionIndex].Guid.ToString() + }); } } diff --git a/FModel/ViewModels/ApiEndpoints/FModelApi.cs b/FModel/ViewModels/ApiEndpoints/FModelApi.cs index acff5a6e..73041505 100644 --- a/FModel/ViewModels/ApiEndpoints/FModelApi.cs +++ b/FModel/ViewModels/ApiEndpoints/FModelApi.cs @@ -5,6 +5,7 @@ using System.Threading; using System.Threading.Tasks; using System.Windows; using AutoUpdaterDotNET; +using FModel.Settings; using FModel.ViewModels.ApiEndpoints.Models; using Newtonsoft.Json; using RestSharp; @@ -107,13 +108,16 @@ namespace FModel.ViewModels.ApiEndpoints private void CheckForUpdateEvent(UpdateInfoEventArgs args) { - if (args != null) + if (args is {CurrentVersion: { }}) { - if (!args.IsUpdateAvailable) return; + var currentVersion = new Version(args.CurrentVersion); + if (currentVersion == args.InstalledVersion) return; + + var downgrade = currentVersion < args.InstalledVersion; var messageBox = new MessageBoxModel { - Text = $"FModel {args.CurrentVersion} is available. You are using version {args.InstalledVersion}. Do you want to update the application now?", - Caption = "Update Available", + 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?", + Caption = $"{(downgrade ? "Downgrade" : "Update")} Available", Icon = MessageBoxImage.Question, Buttons = MessageBoxButtons.YesNo(), IsSoundEnabled = false diff --git a/FModel/ViewModels/ApiEndpoints/Models/AesResponse.cs b/FModel/ViewModels/ApiEndpoints/Models/AesResponse.cs index 5d1c9071..64b8e410 100644 --- a/FModel/ViewModels/ApiEndpoints/Models/AesResponse.cs +++ b/FModel/ViewModels/ApiEndpoints/Models/AesResponse.cs @@ -1,4 +1,5 @@ -using System.Diagnostics; +using System.Collections.Generic; +using System.Diagnostics; using J = Newtonsoft.Json.JsonPropertyAttribute; namespace FModel.ViewModels.ApiEndpoints.Models @@ -8,9 +9,9 @@ namespace FModel.ViewModels.ApiEndpoints.Models { [J("version")] public string Version { get; private set; } [J("mainKey")] public string MainKey { get; set; } - [J("dynamicKeys")] public DynamicKey[] DynamicKeys { get; set; } + [J("dynamicKeys")] public List DynamicKeys { get; set; } - public bool HasDynamicKeys => DynamicKeys is {Length: > 0}; + public bool HasDynamicKeys => DynamicKeys is {Count: > 0}; } [DebuggerDisplay("{" + nameof(Key) + "}")] diff --git a/FModel/ViewModels/ApplicationViewModel.cs b/FModel/ViewModels/ApplicationViewModel.cs index 38a2a71e..bfc7aca2 100644 --- a/FModel/ViewModels/ApplicationViewModel.cs +++ b/FModel/ViewModels/ApplicationViewModel.cs @@ -44,7 +44,6 @@ namespace FModel.ViewModels set { SetProperty(ref _status, value); - RaisePropertyChanged(nameof(TitleExtra)); IsReady = Status != EStatusKind.Loading && Status != EStatusKind.Stopping; } } @@ -105,6 +104,11 @@ namespace FModel.ViewModels UserSettings.Default.GameDirectory = gameLauncherViewModel.SelectedDetectedGame.GameDirectory; if (!bAlreadyLaunched || gameDirectory.Equals(gameLauncherViewModel.SelectedDetectedGame.GameDirectory)) return; + RestartWithWarning(); + } + + public void RestartWithWarning() + { MessageBox.Show("It looks like you just changed something.\nFModel will restart to apply your changes.", "Uh oh, a restart is needed", MessageBoxButton.OK, MessageBoxImage.Warning); Restart(); } diff --git a/FModel/ViewModels/AudioPlayerViewModel.cs b/FModel/ViewModels/AudioPlayerViewModel.cs index f4bb64ff..7d0c7ce6 100644 --- a/FModel/ViewModels/AudioPlayerViewModel.cs +++ b/FModel/ViewModels/AudioPlayerViewModel.cs @@ -15,6 +15,7 @@ using System.Windows.Data; using CSCore.CoreAudioAPI; using FModel.Extensions; using FModel.Framework; +using FModel.Services; using FModel.Settings; using FModel.ViewModels.Commands; using FModel.Views.Resources.Controls; @@ -154,6 +155,7 @@ namespace FModel.ViewModels public class AudioPlayerViewModel : ViewModel, ISource, IDisposable { + private DiscordHandler _discordHandler => DiscordService.DiscordHandler; private static IWaveSource _waveSource; private static ISoundOut _soundOut; private Timer _sourceTimer; @@ -385,6 +387,7 @@ namespace FModel.ViewModels public void Play() { if (_soundOut == null || IsPlaying) return; + _discordHandler.UpdateButDontSavePresence(null, $"Audio Player: {PlayedFile.FileName} ({PlayedFile.Duration:g})"); _soundOut.Play(); } diff --git a/FModel/ViewModels/CUE4ParseViewModel.cs b/FModel/ViewModels/CUE4ParseViewModel.cs index 2e168471..eead9d3c 100644 --- a/FModel/ViewModels/CUE4ParseViewModel.cs +++ b/FModel/ViewModels/CUE4ParseViewModel.cs @@ -6,6 +6,7 @@ using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using System.Windows; +using System.Windows.Media; using System.Windows.Media.Imaging; using AdonisUI.Controls; using CUE4Parse.Encryption.Aes; @@ -15,13 +16,19 @@ using CUE4Parse.MappingsProvider; using CUE4Parse.UE4.AssetRegistry; using CUE4Parse.UE4.Assets.Exports; 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.Wwise; using CUE4Parse.UE4.Localization; +using CUE4Parse.UE4.Objects.UObject; using CUE4Parse.UE4.Oodle.Objects; +using CUE4Parse.UE4.Shaders; using CUE4Parse.UE4.Wwise; +using CUE4Parse_Conversion; using CUE4Parse_Conversion.Materials; +using CUE4Parse_Conversion.Meshes; using CUE4Parse_Conversion.Sounds; using CUE4Parse_Conversion.Textures; using EpicManifestParser.Objects; @@ -81,9 +88,18 @@ namespace FModel.ViewModels } default: { - Game = gameDirectory.SubstringBeforeLast("\\Content\\Paks").SubstringAfterLast("\\").ToEnum(FGame.Unknown); - Provider = new DefaultFileProvider(gameDirectory, SearchOption.TopDirectoryOnly, true, - UserSettings.Default.OverridedGame[Game], UserSettings.Default.OverridedUEVersion[Game]); + Game = gameDirectory.SubstringBeforeLast("\\Content").SubstringAfterLast("\\").ToEnum(FGame.Unknown); + + if (Game == FGame.StateOfDecay2) + Provider = new DefaultFileProvider(new DirectoryInfo(gameDirectory), new List + { + new(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + "\\StateOfDecay2\\Saved\\Paks"), + new(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + "\\StateOfDecay2\\Saved\\DisabledPaks") + }, + SearchOption.AllDirectories, true, UserSettings.Default.OverridedGame[Game], UserSettings.Default.OverridedUEVersion[Game]); + else + Provider = new DefaultFileProvider(gameDirectory, SearchOption.AllDirectories, true, UserSettings.Default.OverridedGame[Game], UserSettings.Default.OverridedUEVersion[Game]); + break; } } @@ -202,7 +218,7 @@ namespace FModel.ViewModels foreach (var file in GameDirectory.DirectoryFiles) { cancellationToken.ThrowIfCancellationRequested(); - if (!(Provider.MountedVfs.FirstOrDefault(x => x.Name == file.Name) is { } vfs)) + if (Provider.MountedVfs.FirstOrDefault(x => x.Name == file.Name) is not { } vfs) continue; file.IsEnabled = true; @@ -232,7 +248,7 @@ namespace FModel.ViewModels await _threadWorkerView.Begin(cancellationToken => { var aes = _apiEndpointView.BenbotApi.GetAesKeys(cancellationToken); - if (aes == null) return; + if (aes?.MainKey == null && aes?.DynamicKeys == null && aes?.Version == null) return; UserSettings.Default.AesKeys[Game] = aes; }); @@ -381,6 +397,7 @@ namespace FModel.ViewModels { case "ini": case "txt": + case "log": case "po": case "bat": case "xml": @@ -466,14 +483,15 @@ namespace FModel.ViewModels TabControl.SelectedTab.Image = null; if (Provider.TryCreateReader(fullPath, out var archive)) { - var header = new FDictionaryHeader(archive); + var header = new FOodleDictionaryArchive(archive).Header; TabControl.SelectedTab.SetDocumentText(JsonConvert.SerializeObject(header, Formatting.Indented), bulkSave); } - + break; } case "png": case "jpg": + case "bmp": { if (Provider.TrySaveAsset(fullPath, out var data)) { @@ -488,8 +506,18 @@ namespace FModel.ViewModels FLogger.AppendText($"Export '{fullPath.SubstringAfterLast('/')}' and change its extension if you want it to be an installable font file", Constants.WHITE, true); break; case "ushaderbytecode": + case "ushadercode": + { TabControl.SelectedTab.Image = null; + + if (Provider.TryCreateReader(fullPath, out var archive)) + { + var ar = new FShaderCodeArchive(archive); + TabControl.SelectedTab.SetDocumentText(JsonConvert.SerializeObject(ar, Formatting.Indented), bulkSave); + } + break; + } default: { var exports = Provider.LoadObjectExports(fullPath); @@ -522,13 +550,19 @@ namespace FModel.ViewModels ExportData(fullPath); } - private bool CheckExport(UExport export) // return if this loads an image + private bool CheckExport(UObject export) // return if this loads an image { switch (export) { case UTexture2D texture: { - SetImage(texture.Decode()); + var bNearest = false; + if (texture.TryGetValue(out FName trigger, "LODGroup", "Filter") && !trigger.IsNone) + bNearest = trigger.Text.EndsWith("TEXTUREGROUP_Pixels2D", StringComparison.OrdinalIgnoreCase) || + trigger.Text.EndsWith("TF_Nearest", StringComparison.OrdinalIgnoreCase); + + TabControl.SelectedTab.ImageRender = bNearest ? BitmapScalingMode.NearestNeighbor : BitmapScalingMode.Linear; + SetImage(texture.Decode(bNearest)); return true; } case UAkMediaAssetData: @@ -542,30 +576,30 @@ namespace FModel.ViewModels SaveAndPlaySound(Path.Combine(TabControl.SelectedTab.Directory, TabControl.SelectedTab.Header.SubstringBeforeLast('.')).Replace('\\', '/'), audioFormat, data); return false; } - case UMaterialInterface material when UserSettings.Default.IsAutoSaveMaterials: + case UMaterialInterface when UserSettings.Default.IsAutoSaveMaterials: + case UStaticMesh when UserSettings.Default.IsAutoSaveMeshes: + case USkeletalMesh when UserSettings.Default.IsAutoSaveMeshes: { - var mat = new MaterialExporter(material); - if (mat.TryWriteTo( - Path.Combine(UserSettings.Default.OutputDirectory, "Saves"), - Path.Combine(UserSettings.Default.OutputDirectory, "Textures"), - out var savedFileName)) + var toSave = new Exporter(export, UserSettings.Default.TextureExportFormat, UserSettings.Default.LodExportFormat); + var toSaveDirectory = new DirectoryInfo(Path.Combine(UserSettings.Default.OutputDirectory, "Saves")); + if (toSave.TryWriteToDir(toSaveDirectory, out var savedFileName)) { - Log.Information("{FileName} successfully saved", savedFileName); + Log.Information("Successfully saved {FileName}", savedFileName); FLogger.AppendInformation(); - FLogger.AppendText($"Successfully saved '{savedFileName}'", Constants.WHITE, true); + FLogger.AppendText($"Successfully saved {savedFileName}", Constants.WHITE, true); } else { Log.Error("{FileName} could not be saved", savedFileName); FLogger.AppendError(); - FLogger.AppendText($"Could not saved '{savedFileName}'", Constants.WHITE, true); + FLogger.AppendText($"Could not save '{savedFileName}'", Constants.WHITE, true); } return false; } - case UObject uObject: + default: { - using var package = new CreatorPackage(uObject, UserSettings.Default.CosmeticStyle); + using var package = new CreatorPackage(export, UserSettings.Default.CosmeticStyle); if (!package.TryConstructCreator(out var creator)) return false; creator.ParseForInfo(); @@ -573,8 +607,6 @@ namespace FModel.ViewModels return true; } } - - return false; } private void SetImage(SKImage img) @@ -635,7 +667,7 @@ namespace FModel.ViewModels Directory.CreateDirectory(path.SubstringBeforeLast('/')); File.WriteAllBytes(path, kvp.Value); } - + Log.Information("{FileName} successfully exported", fileName); FLogger.AppendInformation(); FLogger.AppendText($"Successfully exported '{fileName}'", Constants.WHITE, true); diff --git a/FModel/ViewModels/Commands/GoToCommand.cs b/FModel/ViewModels/Commands/GoToCommand.cs index 13ec2c66..1b9845c3 100644 --- a/FModel/ViewModels/Commands/GoToCommand.cs +++ b/FModel/ViewModels/Commands/GoToCommand.cs @@ -32,7 +32,8 @@ namespace FModel.ViewModels.Commands { foreach (var folder in root) { - if (!folder.Header.Equals(folders[i])) continue; + if (!folder.Header.Equals(folders[i], i == 0 ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal)) + continue; folder.IsExpanded = true; // folder found = expand diff --git a/FModel/ViewModels/Commands/ImageCommand.cs b/FModel/ViewModels/Commands/ImageCommand.cs index 18b8c38d..59d7a5e7 100644 --- a/FModel/ViewModels/Commands/ImageCommand.cs +++ b/FModel/ViewModels/Commands/ImageCommand.cs @@ -1,4 +1,5 @@ using System.Windows; +using System.Windows.Media; using AdonisUI.Controls; using FModel.Framework; using FModel.Views.Resources.Controls; @@ -21,14 +22,16 @@ namespace FModel.ViewModels.Commands { Helper.OpenWindow(contextViewModel.Header + " (Image)", () => { - new ImagePopout + var popout = new ImagePopout { Title = contextViewModel.Header + " (Image)", Width = contextViewModel.Image.Width, Height = contextViewModel.Image.Height, WindowState = contextViewModel.Image.Height > 1000 ? WindowState.Maximized : WindowState.Normal, ImageCtrl = {Source = contextViewModel.Image} - }.Show(); + }; + RenderOptions.SetBitmapScalingMode(popout.ImageCtrl, contextViewModel.ImageRender); + popout.Show(); }); break; } diff --git a/FModel/ViewModels/Commands/LoadCommand.cs b/FModel/ViewModels/Commands/LoadCommand.cs index 461da185..86ca6251 100644 --- a/FModel/ViewModels/Commands/LoadCommand.cs +++ b/FModel/ViewModels/Commands/LoadCommand.cs @@ -38,6 +38,13 @@ namespace FModel.ViewModels.Commands public override async void Execute(LoadingModesViewModel contextViewModel, object parameter) { + if (_applicationView.CUE4Parse.GameDirectory.HasNoFile) return; + if (_applicationView.CUE4Parse.Game == FGame.FortniteGame && + _applicationView.CUE4Parse.Provider.MappingsContainer == null) + { + FLogger.AppendError(); + FLogger.AppendText("Mappings could not get pulled, extracting assets might not work properly. If so, press F12 or please restart.", Constants.WHITE, true); + } #if DEBUG var loadingTime = Stopwatch.StartNew(); #endif diff --git a/FModel/ViewModels/GameDirectoryViewModel.cs b/FModel/ViewModels/GameDirectoryViewModel.cs index a606edd1..220d8ab0 100644 --- a/FModel/ViewModels/GameDirectoryViewModel.cs +++ b/FModel/ViewModels/GameDirectoryViewModel.cs @@ -80,6 +80,7 @@ namespace FModel.ViewModels public class GameDirectoryViewModel : ViewModel { + public bool HasNoFile => DirectoryFiles.Count < 1; public readonly ObservableCollection DirectoryFiles; public ICollectionView DirectoryFilesView { get; } diff --git a/FModel/ViewModels/GameSelectorViewModel.cs b/FModel/ViewModels/GameSelectorViewModel.cs index 5385276b..be2e0fb2 100644 --- a/FModel/ViewModels/GameSelectorViewModel.cs +++ b/FModel/ViewModels/GameSelectorViewModel.cs @@ -55,11 +55,12 @@ namespace FModel.ViewModels yield return GetUnrealEngineGame("Rosemallow", "\\Indiana\\Content\\Paks"); yield return GetUnrealEngineGame("Catnip", "\\OakGame\\Content\\Paks"); yield return GetUnrealEngineGame("AzaleaAlpha", "\\Prospect\\Content\\Paks"); - yield return GetUnrealEngineGame("WorldExplorersLive", "\\WorldExplorers\\Content\\Paks"); + yield return GetUnrealEngineGame("WorldExplorersLive", "\\WorldExplorers\\Content"); yield return GetUnrealEngineGame("Newt", "\\g3\\Content\\Paks"); yield return GetUnrealEngineGame("shoebill", "\\SwGame\\Content\\Paks"); yield return GetUnrealEngineGame("Snoek", "\\StateOfDecay2\\Content\\Paks"); yield return GetUnrealEngineGame("a99769d95d8f400baad1f67ab5dfe508", "\\Core\\Platform\\Content\\Paks"); + yield return GetUnrealEngineGame("Nebula", "\\BendGame\\Content"); yield return GetRiotGame("VALORANT", "ShooterGame\\Content\\Paks"); yield return new DetectedGame {GameName = "Valorant [LIVE]", GameDirectory = Constants._VAL_LIVE_TRIGGER}; yield return GetMojangGame("MinecraftDungeons", "\\dungeons\\dungeons\\Dungeons\\Content\\Paks"); diff --git a/FModel/ViewModels/MapViewerViewModel.cs b/FModel/ViewModels/MapViewerViewModel.cs index dc57dde4..e85b0064 100644 --- a/FModel/ViewModels/MapViewerViewModel.cs +++ b/FModel/ViewModels/MapViewerViewModel.cs @@ -1,8 +1,11 @@ -using System; +using System; +using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; -using System.Windows; using System.Windows.Media.Imaging; 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; @@ -18,118 +21,278 @@ using SkiaSharp.HarfBuzz; namespace FModel.ViewModels { + public class MapLayer + { + public SKBitmap Layer; + public bool IsEnabled; + } + + public enum EWaypointType + { + Parkour, + TimeTrials + } + public class MapViewerViewModel : ViewModel { private ThreadWorkerViewModel _threadWorkerView => ApplicationService.ThreadWorkerView; + private DiscordHandler _discordHandler => DiscordService.DiscordHandler; - private bool _showCities; - public bool ShowCities + #region BINDINGS + + private bool _brPois; + public bool BrPois { - get => _showCities; - set => SetProperty(ref _showCities, value, nameof(ShowCities)); + get => _brPois; + set => SetProperty(ref _brPois, value, "ApolloGameplay_MapPois"); } - private bool _showLandmarks; - public bool ShowLandmarks + private bool _brLandmarks; + public bool BrLandmarks { - get => _showLandmarks; - set => SetProperty(ref _showLandmarks, value, nameof(ShowLandmarks)); + get => _brLandmarks; + set => SetProperty(ref _brLandmarks, value, "ApolloGameplay_MapLandmarks"); + } + + private bool _brPatrolsPath; + public bool BrPatrolsPath + { + get => _brPatrolsPath; + set => SetProperty(ref _brPatrolsPath, value, "ApolloGameplay_PatrolsPath"); + } + + private bool _brUpgradeBenches; + public bool BrUpgradeBenches + { + get => _brUpgradeBenches; + set => SetProperty(ref _brUpgradeBenches, value, "ApolloGameplay_UpgradeBenches"); + } + + private bool _brPhonebooths; + public bool BrPhonebooths + { + get => _brPhonebooths; + set => SetProperty(ref _brPhonebooths, value, "ApolloGameplay_Phonebooths"); } - private bool _showPatrolPaths; - public bool ShowPatrolPaths + private bool _brTagsLocation; + public bool BrTagsLocation { - get => _showPatrolPaths; - set => SetProperty(ref _showPatrolPaths, value, nameof(ShowPatrolPaths)); + get => _brTagsLocation; + set => SetProperty(ref _brTagsLocation, value, "ApolloGameplay_TagsLocation"); + } + + private bool _brAlienArtifacts; + public bool BrAlienArtifacts + { + get => _brAlienArtifacts; + set => SetProperty(ref _brAlienArtifacts, value, "ApolloGameplay_AlienArtifacts"); + } + + private bool _prLandmarks; + public bool PrLandmarks + { + get => _prLandmarks; + set => SetProperty(ref _prLandmarks, value, "PapayaGameplay_MapLandmarks"); + } + + private bool _prCannonball; + public bool PrCannonball + { + get => _prCannonball; + set => SetProperty(ref _prCannonball, value, "PapayaGameplay_CannonballGame"); + } + + private bool _prSkydive; + public bool PrSkydive + { + get => _prSkydive; + set => SetProperty(ref _prSkydive, value, "PapayaGameplay_SkydiveGame"); + } + + private bool _prShootingTargets; + public bool PrShootingTargets + { + get => _prShootingTargets; + set => SetProperty(ref _prShootingTargets, value, "PapayaGameplay_ShootingTargets"); + } + + private bool _prParkour; + public bool PrParkour + { + get => _prParkour; + set => SetProperty(ref _prParkour, value, "PapayaGameplay_ParkourGame"); + } + + private bool _prTimeTrials; + public bool PrTimeTrials + { + get => _prTimeTrials; + set => SetProperty(ref _prTimeTrials, value, "PapayaGameplay_TimeTrials"); + } + + private bool _prVendingMachines; + public bool PrVendingMachines + { + get => _prVendingMachines; + set => SetProperty(ref _prVendingMachines, value, "PapayaGameplay_VendingMachines"); + } + + private bool _prMusicBlocks; + public bool PrMusicBlocks + { + get => _prMusicBlocks; + set => SetProperty(ref _prMusicBlocks, value, "PapayaGameplay_MusicBlocks"); } + #endregion + + #region BITMAP IMAGES + + private BitmapImage _brMiniMapImage; + private BitmapImage _prMiniMapImage; private BitmapImage _mapImage; public BitmapImage MapImage { get => _mapImage; set => SetProperty(ref _mapImage, value); } - - private BitmapImage _citiesImage; - public BitmapImage CitiesImage - { - get => _citiesImage; - set => SetProperty(ref _citiesImage, value); - } - private BitmapImage _landmarksImage; - public BitmapImage LandmarksImage + private BitmapImage _brLayerImage; + private BitmapImage _prLayerImage; + private BitmapImage _layerImage; + public BitmapImage LayerImage { - get => _landmarksImage; - set => SetProperty(ref _landmarksImage, value); + get => _layerImage; + set => SetProperty(ref _layerImage, value); } - private BitmapImage _patrolPathImage; - public BitmapImage PatrolPathImage + private const int _widthHeight = 2048; + private const int _brRadius = 135000; + private const int _prRadius = 51000; + private int _mapIndex; + public int MapIndex // 0 is BR, 1 is PR { - get => _patrolPathImage; - set => SetProperty(ref _patrolPathImage, value); + get => _mapIndex; + set + { + SetProperty(ref _mapIndex, value); + TriggerChange(); + } } + #endregion + + private const string _FIRST_BITMAP = "MapCheck"; + private readonly Dictionary[] _bitmaps; // first bitmap is the displayed map, others are overlays of the map private readonly CUE4ParseViewModel _cue4Parse; public MapViewerViewModel(CUE4ParseViewModel cue4Parse) { + _bitmaps = new[] + { + new Dictionary(), + new Dictionary() + }; _cue4Parse = cue4Parse; } public async void Initialize() { Utils.Typefaces ??= new Typefaces(_cue4Parse); - if (MapImage == null && _mapBitmap == null) - { - await LoadMiniMap(); - } + _textPaint.Typeface = _fillPaint.Typeface = Utils.Typefaces.Bottom ?? Utils.Typefaces.DisplayName; + await LoadBrMiniMap(); + await LoadPrMiniMap(); + TriggerChange(); } - public BitmapImage GetImageToSave() + public BitmapImage GetImageToSave() => GetImageSource(GetLayerBitmap(true)); + private SKBitmap GetLayerBitmap(bool withMap) { - var ret = new SKBitmap(_mapBitmap.Width, _mapBitmap.Height, SKColorType.Rgba8888, SKAlphaType.Premul); + var ret = new SKBitmap(_widthHeight, _widthHeight, SKColorType.Rgba8888, SKAlphaType.Premul); using var c = new SKCanvas(ret); - c.DrawBitmap(_mapBitmap, 0, 0); - if (ShowCities) - c.DrawBitmap(_citiesBitmap, 0, 0); - if (ShowLandmarks) - c.DrawBitmap(_landmarksBitmap, 0, 0); - if (ShowPatrolPaths) - c.DrawBitmap(_patrolPathBitmap, 0, 0); + foreach (var (key, value) in _bitmaps[MapIndex]) + { + if (!value.IsEnabled || !withMap && key == _FIRST_BITMAP) continue; + c.DrawBitmap(value.Layer, new SKRect(0, 0, _widthHeight, _widthHeight)); + } - return GetImageSource(ret); - } - - protected override bool SetProperty(ref T storage, T value, string propertyName = null) - { - var ret = base.SetProperty(ref storage, value, propertyName); - CheckForStuffToDraw(propertyName); return ret; } - private async void CheckForStuffToDraw(string propertyName = null) + protected override bool SetProperty(ref T storage, T value, string propertyName = null) // don't delete, else nothing will update for some reason { - switch (propertyName) + var ret = base.SetProperty(ref storage, value, propertyName); + if (bool.TryParse(value.ToString(), out var b)) GenericToggle(propertyName, b); + return ret; + } + + private async void GenericToggle(string key, bool enabled) + { + if (_bitmaps[MapIndex].TryGetValue(key, out var layer) && layer.Layer != null) { - case nameof(ShowCities) when _citiesBitmap == null && _landmarksBitmap == null && _mapBitmap != null: - { - await LoadCities(); - break; - } - case nameof(ShowLandmarks) when _landmarksBitmap == null && _citiesBitmap == null && _mapBitmap != null: - { - await LoadCities(); - break; - } - case nameof(ShowPatrolPaths) when _patrolPathBitmap == null && _mapBitmap != null: - { - await LoadPatrolPaths(); - break; - } + layer.IsEnabled = enabled; } + else if (enabled) // load layer + { + switch (key) + { + case "ApolloGameplay_MapPois": + case "ApolloGameplay_MapLandmarks": + case "PapayaGameplay_MapLandmarks": + await LoadQuestIndicatorData(); + break; + case "ApolloGameplay_PatrolsPath": + await LoadPatrolsPath(); + break; + case "ApolloGameplay_UpgradeBenches": + await LoadUpgradeBenches(); + break; + case "ApolloGameplay_Phonebooths": + await LoadPhonebooths(); + break; + case "ApolloGameplay_AlienArtifacts": + await LoadAlienArtifacts(); + break; + case "ApolloGameplay_TagsLocation": + await LoadTagsLocation(); + break; + case "PapayaGameplay_CannonballGame": + await LoadCannonballGame(); + break; + case "PapayaGameplay_SkydiveGame": + await LoadSkydiveGame(); + break; + case "PapayaGameplay_ShootingTargets": + await LoadShootingTargets(); + break; + case "PapayaGameplay_ParkourGame": + await LoadWaypoint(EWaypointType.Parkour); + break; + case "PapayaGameplay_TimeTrials": + await LoadWaypoint(EWaypointType.TimeTrials); + break; + case "PapayaGameplay_VendingMachines": + await LoadVendingMachines(); + break; + case "PapayaGameplay_MusicBlocks": + await LoadMusicBlocks(); + break; + } + _bitmaps[MapIndex][key].IsEnabled = true; + } + + switch (MapIndex) + { + case 0: + _brLayerImage = GetImageSource(GetLayerBitmap(false)); + break; + case 1: + _prLayerImage = GetImageSource(GetLayerBitmap(false)); + break; + } + TriggerChange(); } private BitmapImage GetImageSource(SKBitmap bitmap) @@ -145,37 +308,58 @@ namespace FModel.ViewModels return image; } - private const int WorldRadius = 135000; - private SKBitmap _mapBitmap; - private SKBitmap _citiesBitmap; - private SKBitmap _landmarksBitmap; - private SKBitmap _patrolPathBitmap; - private readonly SKBitmap _pinBitmap = - SKBitmap.Decode(Application.GetResourceStream(new Uri("pack://application:,,,/Resources/pin.png"))?.Stream); - private readonly SKBitmap _cityPinBitmap = - SKBitmap.Decode(Application.GetResourceStream(new Uri("pack://application:,,,/Resources/city_pin.png"))?.Stream); - private readonly SKPaint _imagePaint = new() + private void TriggerChange() + { + var layerCount = _bitmaps[_mapIndex].Count(x => x.Value.IsEnabled); + var layerString = $"{layerCount} Layer{(layerCount > 1 ? "s" : "")}"; + switch (_mapIndex) + { + case 0: + _discordHandler.UpdateButDontSavePresence(null, $"Map Viewer: Battle Royale ({layerString})"); + _mapImage = _brMiniMapImage; + _layerImage = _brLayerImage; + break; + case 1: + _discordHandler.UpdateButDontSavePresence(null, $"Map Viewer: Party Royale ({layerString})"); + _mapImage = _prMiniMapImage; + _layerImage = _prLayerImage; + break; + } + + RaisePropertyChanged(nameof(MapImage)); + RaisePropertyChanged(nameof(LayerImage)); + } + + private readonly SKPaint _textPaint = new() { IsAntialias = true, FilterQuality = SKFilterQuality.High, - Color = SKColors.White, TextAlign = SKTextAlign.Center, TextSize = 25, - ImageFilter = SKImageFilter.CreateDropShadow(4, 4, 8, 8, SKColors.Black) + Color = SKColors.White, TextAlign = SKTextAlign.Center, TextSize = 26 + }; + private readonly SKPaint _fillPaint = new() + { + IsAntialias = true, FilterQuality = SKFilterQuality.High, + IsStroke = true, Color = SKColors.Black, TextSize = 26, + TextAlign = SKTextAlign.Center }; private readonly SKPaint _pathPaint = new() { IsAntialias = true, FilterQuality = SKFilterQuality.High, IsStroke = true, - Style = SKPaintStyle.Stroke, StrokeWidth = 10, Color = SKColors.Red, + Style = SKPaintStyle.Stroke, StrokeWidth = 5, Color = SKColors.Red, ImageFilter = SKImageFilter.CreateDropShadow(4, 4, 8, 8, SKColors.Black) }; - private FVector2D GetMapPosition(FVector vector) + private FVector2D GetMapPosition(FVector vector, int mapRadius) { - var nx = (vector.Y + WorldRadius) / (WorldRadius * 2) * _mapBitmap.Width; - var ny = (1 - (vector.X + WorldRadius) / (WorldRadius * 2)) * _mapBitmap.Height; + var nx = (vector.Y + mapRadius) / (mapRadius * 2) * _widthHeight; + var ny = (1 - (vector.X + mapRadius) / (mapRadius * 2)) * _widthHeight; return new FVector2D(nx, ny); } - - private async Task LoadMiniMap() + + private async Task LoadBrMiniMap() { + if (_bitmaps[0].TryGetValue(_FIRST_BITMAP, out var brMap) && brMap.Layer != null) + return; // if map already loaded + await _threadWorkerView.Begin(_ => { if (!Utils.TryLoadObject("FortniteGame/Content/UI/IngameMap/UIMapManagerBR.Default__UIMapManagerBR_C", out UObject mapManager) || @@ -183,21 +367,39 @@ namespace FModel.ViewModels !mapMaterial.TryGetValue(out FStructFallback cachedExpressionData, "CachedExpressionData") || !cachedExpressionData.TryGetValue(out FStructFallback parameters, "Parameters") || !parameters.TryGetValue(out UTexture2D[] textureValues, "TextureValues")) return; - - _imagePaint.Typeface = Utils.Typefaces.Bottom ?? Utils.Typefaces.DisplayName; - _mapBitmap = Utils.GetBitmap(textureValues[0]); - MapImage = GetImageSource(_mapBitmap); + + _bitmaps[0][_FIRST_BITMAP] = new MapLayer{Layer = Utils.GetBitmap(textureValues[0]), IsEnabled = true}; + _brMiniMapImage = GetImageSource(_bitmaps[0][_FIRST_BITMAP].Layer); }); } - private async Task LoadCities() + private async Task LoadPrMiniMap() + { + if (_bitmaps[1].TryGetValue(_FIRST_BITMAP, out var prMap) && prMap.Layer != null) + return; // if map already loaded + + await _threadWorkerView.Begin(_ => + { + if (!Utils.TryLoadObject("FortniteGame/Content/UI/IngameMap/UIMapManagerPapaya.Default__UIMapManagerPapaya_C", out UObject mapManager) || + !mapManager.TryGetValue(out UMaterial mapMaterial, "MapMaterial") || + mapMaterial.ReferencedTextures.Count < 1) return; + + _bitmaps[1][_FIRST_BITMAP] = new MapLayer{Layer = Utils.GetBitmap(mapMaterial.ReferencedTextures[0] as UTexture2D), IsEnabled = true}; + _prMiniMapImage = GetImageSource(_bitmaps[1][_FIRST_BITMAP].Layer); + }); + } + + private async Task LoadQuestIndicatorData() { await _threadWorkerView.Begin(_ => { - _citiesBitmap = new SKBitmap(_mapBitmap.Width, _mapBitmap.Height, SKColorType.Rgba8888, SKAlphaType.Premul); - _landmarksBitmap = new SKBitmap(_mapBitmap.Width, _mapBitmap.Height, SKColorType.Rgba8888, SKAlphaType.Premul); - using var cities = new SKCanvas(_citiesBitmap); - using var landmarks = new SKCanvas(_landmarksBitmap); + var poisBitmap = new SKBitmap(_widthHeight, _widthHeight, SKColorType.Rgba8888, SKAlphaType.Premul); + var brLandmarksBitmap = new SKBitmap(_widthHeight, _widthHeight, SKColorType.Rgba8888, SKAlphaType.Premul); + var prLandmarksBitmap = new SKBitmap(_widthHeight, _widthHeight, SKColorType.Rgba8888, SKAlphaType.Premul); + using var pois = new SKCanvas(poisBitmap); + using var brLandmarks = new SKCanvas(brLandmarksBitmap); + using var prLandmarks = new SKCanvas(prLandmarksBitmap); + if (Utils.TryLoadObject("FortniteGame/Content/Quests/QuestIndicatorData", out UObject indicatorData) && indicatorData.TryGetValue(out FStructFallback[] challengeMapPoiData, "ChallengeMapPoiData")) { @@ -205,86 +407,435 @@ namespace FModel.ViewModels { if (!poiData.TryGetValue(out FSoftObjectPath discoveryQuest, "DiscoveryQuest") || !poiData.TryGetValue(out FText text, "Text") || string.IsNullOrEmpty(text.Text) || - !poiData.TryGetValue(out FVector worldLocation, "WorldLocation")) continue; + !poiData.TryGetValue(out FVector worldLocation, "WorldLocation") || + !poiData.TryGetValue(out FName discoverBackend, "DiscoverObjectiveBackendName")) continue; - var shaper = new CustomSKShaper(_imagePaint.Typeface); - var shapedText = shaper.Shape(text.Text, _imagePaint); - var vector = GetMapPosition(worldLocation); + var shaper = new CustomSKShaper(_textPaint.Typeface); + var shapedText = shaper.Shape(text.Text, _textPaint); - if (discoveryQuest.AssetPathName.Text.Contains("landmarks", StringComparison.OrdinalIgnoreCase)) + if (discoverBackend.Text.Contains("papaya", StringComparison.OrdinalIgnoreCase)) { - landmarks.DrawPoint(vector.X, vector.Y, _pathPaint); - landmarks.DrawShapedText(shaper, text.Text, vector.X - shapedText.Points[^1].X / 2, vector.Y - 12.5F, _imagePaint); + _fillPaint.StrokeWidth = 5; + var vector = GetMapPosition(worldLocation, _prRadius); + prLandmarks.DrawPoint(vector.X, vector.Y, _pathPaint); + prLandmarks.DrawShapedText(shaper, text.Text, vector.X - shapedText.Points[^1].X / 2, vector.Y - 12.5F, _fillPaint); + prLandmarks.DrawShapedText(shaper, text.Text, vector.X - shapedText.Points[^1].X / 2, vector.Y - 12.5F, _textPaint); + } + else if (discoveryQuest.AssetPathName.Text.Contains("landmarks", StringComparison.OrdinalIgnoreCase)) + { + _fillPaint.StrokeWidth = 5; + var vector = GetMapPosition(worldLocation, _brRadius); + brLandmarks.DrawPoint(vector.X, vector.Y, _pathPaint); + brLandmarks.DrawShapedText(shaper, text.Text, vector.X - shapedText.Points[^1].X / 2, vector.Y - 12.5F, _fillPaint); + brLandmarks.DrawShapedText(shaper, text.Text, vector.X - shapedText.Points[^1].X / 2, vector.Y - 12.5F, _textPaint); } else { - cities.DrawPoint(vector.X, vector.Y, _pathPaint); - cities.DrawBitmap(_cityPinBitmap, vector.X - 50, vector.Y - 90, _imagePaint); - cities.DrawShapedText(shaper, text.Text, vector.X - shapedText.Points[^1].X / 2, vector.Y - 12.5F, _imagePaint); + _fillPaint.StrokeWidth = 10; + var vector = GetMapPosition(worldLocation, _brRadius); + pois.DrawShapedText(shaper, text.Text.ToUpperInvariant(), vector.X - shapedText.Points[^1].X / 2, vector.Y, _fillPaint); + pois.DrawShapedText(shaper, text.Text.ToUpperInvariant(), vector.X - shapedText.Points[^1].X / 2, vector.Y, _textPaint); } } } - CitiesImage = GetImageSource(_citiesBitmap); - LandmarksImage = GetImageSource(_landmarksBitmap); + _bitmaps[0]["ApolloGameplay_MapPois"] = new MapLayer {Layer = poisBitmap, IsEnabled = false}; + _bitmaps[0]["ApolloGameplay_MapLandmarks"] = new MapLayer {Layer = brLandmarksBitmap, IsEnabled = false}; + _bitmaps[1]["PapayaGameplay_MapLandmarks"] = new MapLayer {Layer = prLandmarksBitmap, IsEnabled = false}; }); } - - private async Task LoadPatrolPaths() + + private async Task LoadPatrolsPath() { await _threadWorkerView.Begin(_ => { - _patrolPathBitmap = new SKBitmap(_mapBitmap.Width, _mapBitmap.Height, SKColorType.Rgba8888, SKAlphaType.Premul); - using var c = new SKCanvas(_patrolPathBitmap); + _fillPaint.StrokeWidth = 5; + var patrolsPathBitmap = new SKBitmap(_widthHeight, _widthHeight, SKColorType.Rgba8888, SKAlphaType.Premul); + using var c = new SKCanvas(patrolsPathBitmap); - if (!Utils.TryLoadObject("FortniteGame/Plugins/GameFeatures/NPCLibrary/Content/GameFeatureData.GameFeatureData", out UObject gameFeatureData) || - !gameFeatureData.TryGetValue(out FPackageIndex levelOverlayConfig, "LevelOverlayConfig") || - !Utils.TryGetPackageIndexExport(levelOverlayConfig, out UObject npcLibrary) || - !npcLibrary.TryGetValue(out FStructFallback[] overlayList, "OverlayList")) - return; - - foreach (var overlay in overlayList) + var exports = Utils.LoadExports("/NPCLibrary/LevelOverlays/Apollo_Terrain_NPCLibraryS17_Overlay"); + foreach (var export in exports) { - if (!overlay.TryGetValue(out FSoftObjectPath overlayWorld, "OverlayWorld")) - continue; + if (export is not { } uObject) continue; + if (!uObject.ExportType.Equals("FortAthenaPatrolPath", StringComparison.OrdinalIgnoreCase) || + !uObject.TryGetValue(out FGameplayTagContainer gameplayTags, "GameplayTags") || + !uObject.TryGetValue(out FPackageIndex[] patrolPoints, "PatrolPoints")) continue; - var exports = Utils.LoadExports(overlayWorld.AssetPathName.Text.SubstringBeforeLast(".")); - foreach (var export in exports) + var displayName = gameplayTags.GameplayTags[0].Text.SubstringAfterLast("."); + if (displayName.Equals("Generic", StringComparison.OrdinalIgnoreCase)) continue; + + if (!Utils.TryGetPackageIndexExport(patrolPoints[0], out uObject) || + !uObject.TryGetValue(out FPackageIndex rootComponent, "RootComponent") || + !Utils.TryGetPackageIndexExport(rootComponent, out uObject) || + !uObject.TryGetValue(out FVector relativeLocation, "RelativeLocation")) continue; + + var path = new SKPath(); + var vector = GetMapPosition(relativeLocation, _brRadius); + path.MoveTo(vector.X, vector.Y); + + for (var i = 1; i < patrolPoints.Length; i++) { - if (!(export is UObject uObject)) continue; - if (!uObject.ExportType.Equals("FortAthenaPatrolPath", StringComparison.OrdinalIgnoreCase) || - !uObject.TryGetValue(out FGameplayTagContainer gameplayTags, "GameplayTags") || - !uObject.TryGetValue(out FPackageIndex[] patrolPoints, "PatrolPoints")) continue; - - if (!Utils.TryGetPackageIndexExport(patrolPoints[0], out uObject) || - !uObject.TryGetValue(out FPackageIndex rootComponent, "RootComponent") || + if (!Utils.TryGetPackageIndexExport(patrolPoints[i], out uObject) || + !uObject.TryGetValue(out rootComponent, "RootComponent") || !Utils.TryGetPackageIndexExport(rootComponent, out uObject) || - !uObject.TryGetValue(out FVector relativeLocation, "RelativeLocation")) continue; + !uObject.TryGetValue(out relativeLocation, "RelativeLocation")) continue; - var path = new SKPath(); - var vector = GetMapPosition(relativeLocation); - path.MoveTo(vector.X, vector.Y); - - for (var i = 1; i < patrolPoints.Length; i++) - { - if (!Utils.TryGetPackageIndexExport(patrolPoints[i], out uObject) || - !uObject.TryGetValue(out rootComponent, "RootComponent") || - !Utils.TryGetPackageIndexExport(rootComponent, out uObject) || - !uObject.TryGetValue(out relativeLocation, "RelativeLocation")) continue; - - vector = GetMapPosition(relativeLocation); - path.LineTo(vector.X, vector.Y); - } - - path.Close(); - c.DrawPath(path, _pathPaint); - c.DrawBitmap(_pinBitmap, vector.X - 50, vector.Y - 90, _imagePaint); - c.DrawText(gameplayTags.GameplayTags[0].Text.SubstringAfterLast("."), vector.X, vector.Y - 12.5F, _imagePaint); + vector = GetMapPosition(relativeLocation, _brRadius); + path.LineTo(vector.X, vector.Y); } + + c.DrawPath(path, _pathPaint); + c.DrawText(displayName, vector.X, vector.Y - 12.5F, _fillPaint); + c.DrawText(displayName, vector.X, vector.Y - 12.5F, _textPaint); } - PatrolPathImage = GetImageSource(_patrolPathBitmap); + _bitmaps[0]["ApolloGameplay_PatrolsPath"] = new MapLayer {Layer = patrolsPathBitmap, IsEnabled = false}; + }); + } + + private async Task LoadCannonballGame() + { + await _threadWorkerView.Begin(_ => + { + _fillPaint.StrokeWidth = 5; + var cannonballBitmap = new SKBitmap(_widthHeight, _widthHeight, SKColorType.Rgba8888, SKAlphaType.Premul); + using var c = new SKCanvas(cannonballBitmap); + + var exports = Utils.LoadExports("/PapayaGameplay/LevelOverlays/PapayaGameplay_CannonballGame"); + foreach (var export in exports) + { + if (export is not { } uObject) continue; + if (!uObject.ExportType.Equals("BP_CannonballGame_Target_C", StringComparison.OrdinalIgnoreCase) && + !uObject.ExportType.Equals("CannonballGame_VehicleSpawner_C", StringComparison.OrdinalIgnoreCase)) continue; + + if (!uObject.TryGetValue(out FPackageIndex rootComponent, "RootComponent") || + !Utils.TryGetPackageIndexExport(rootComponent, out uObject) || + !uObject.TryGetValue(out FVector relativeLocation, "RelativeLocation")) continue; + + var displayName = Utils.GetLocalizedResource("", "D998BEF44F051E0885C6C58565934BEA", "Cannonball"); + var vector = GetMapPosition(relativeLocation, _prRadius); + + c.DrawPoint(vector.X, vector.Y, _pathPaint); + c.DrawText(displayName, vector.X, vector.Y - 12.5F, _fillPaint); + c.DrawText(displayName, vector.X, vector.Y - 12.5F, _textPaint); + } + + _bitmaps[1]["PapayaGameplay_CannonballGame"] = new MapLayer {Layer = cannonballBitmap, IsEnabled = false}; + }); + } + + private async Task LoadSkydiveGame() + { + await _threadWorkerView.Begin(_ => + { + _fillPaint.StrokeWidth = 5; + var skydiveBitmap = new SKBitmap(_widthHeight, _widthHeight, SKColorType.Rgba8888, SKAlphaType.Premul); + using var c = new SKCanvas(skydiveBitmap); + + var exports = Utils.LoadExports("/PapayaGameplay/LevelOverlays/PapayaGameplay_SkydiveGame"); + foreach (var export in exports) + { + if (export is not { } uObject) continue; + if (!uObject.ExportType.Equals("BP_Waypoint_Papaya_Skydive_Start_C", StringComparison.OrdinalIgnoreCase)) continue; + + if (!uObject.TryGetValue(out FPackageIndex rootComponent, "RootComponent") || + !uObject.TryGetValue(out FText minigameActivityName, "MinigameActivityName") || + !Utils.TryGetPackageIndexExport(rootComponent, out uObject) || + !uObject.TryGetValue(out FVector relativeLocation, "RelativeLocation")) continue; + + var vector = GetMapPosition(relativeLocation, _prRadius); + + c.DrawPoint(vector.X, vector.Y, _pathPaint); + c.DrawText(minigameActivityName.Text, vector.X, vector.Y - 12.5F, _fillPaint); + c.DrawText(minigameActivityName.Text, vector.X, vector.Y - 12.5F, _textPaint); + } + + _bitmaps[1]["PapayaGameplay_SkydiveGame"] = new MapLayer {Layer = skydiveBitmap, IsEnabled = false}; + }); + } + + private async Task LoadShootingTargets() + { + await _threadWorkerView.Begin(_ => + { + _fillPaint.StrokeWidth = 5; + var shootingTargetsBitmap = new SKBitmap(_widthHeight, _widthHeight, SKColorType.Rgba8888, SKAlphaType.Premul); + using var c = new SKCanvas(shootingTargetsBitmap); + + var bDone = false; + var exports = Utils.LoadExports("/PapayaGameplay/LevelOverlays/PapayaGameplay_ShootingTargets"); + foreach (var export in exports) + { + if (export is not { } uObject) continue; + if (!uObject.ExportType.Equals("PapayaShootingTarget_C", StringComparison.OrdinalIgnoreCase)) continue; + + if (!uObject.TryGetValue(out FPackageIndex rootComponent, "RootComponent") || + !Utils.TryGetPackageIndexExport(rootComponent, out uObject) || + !uObject.TryGetValue(out FVector relativeLocation, "RelativeLocation")) continue; + + var vector = GetMapPosition(relativeLocation, _prRadius); + c.DrawPoint(vector.X, vector.Y, _pathPaint); + if (bDone) continue; + + bDone = true; + c.DrawText("Shooting Target", vector.X, vector.Y - 12.5F, _fillPaint); + c.DrawText("Shooting Target", vector.X, vector.Y - 12.5F, _textPaint); + } + + _bitmaps[1]["PapayaGameplay_ShootingTargets"] = new MapLayer {Layer = shootingTargetsBitmap, IsEnabled = false}; + }); + } + + private async Task LoadWaypoint(EWaypointType type) + { + await _threadWorkerView.Begin(_ => + { + _fillPaint.StrokeWidth = 5; + var waypointBitmap = new SKBitmap(_widthHeight, _widthHeight, SKColorType.Rgba8888, SKAlphaType.Premul); + using var c = new SKCanvas(waypointBitmap); + + string file; + string name; + switch (type) + { + case EWaypointType.Parkour: + file = "PapayaGameplay_ParkourGame"; + name = "Parkour"; + break; + case EWaypointType.TimeTrials: + file = "PapayaGameplay_TimeTrials"; + name = "Time Trials"; + break; + default: + throw new ArgumentOutOfRangeException(nameof(type), type, null); + } + + var path = new SKPath(); + var exports = Utils.LoadExports($"/PapayaGameplay/LevelOverlays/{file}"); + foreach (var export in exports) + { + if (export is not { } uObject) continue; + if (!uObject.ExportType.Equals("BP_Waypoint_Parent_Papaya_C", StringComparison.OrdinalIgnoreCase)) continue; + + if (!uObject.TryGetValue(out FPackageIndex rootComponent, "RootComponent") || + !Utils.TryGetPackageIndexExport(rootComponent, out UObject root) || + !root.TryGetValue(out FVector relativeLocation, "RelativeLocation")) continue; + + var vector = GetMapPosition(relativeLocation, _prRadius); + if (path.IsEmpty || uObject.TryGetValue(out bool startsTrial, "StartsTrial") && startsTrial) + { + path.MoveTo(vector.X, vector.Y); + c.DrawText(name, vector.X, vector.Y - 12.5F, _fillPaint); + c.DrawText(name, vector.X, vector.Y - 12.5F, _textPaint); + } + else if (uObject.TryGetValue(out bool endsTrial, "EndsTrial") && endsTrial) + { + path.LineTo(vector.X, vector.Y); + c.DrawPath(path, _pathPaint); + path = new SKPath(); + } + else path.LineTo(vector.X, vector.Y); + } + + _bitmaps[1][file] = new MapLayer {Layer = waypointBitmap, IsEnabled = false}; + }); + } + + private async Task LoadVendingMachines() + { + await _threadWorkerView.Begin(_ => + { + _fillPaint.StrokeWidth = 5; + var set = new HashSet(); + var timeTrialsBitmap = new SKBitmap(_widthHeight, _widthHeight, SKColorType.Rgba8888, SKAlphaType.Premul); + using var c = new SKCanvas(timeTrialsBitmap); + + var exports = Utils.LoadExports("/PapayaGameplay/LevelOverlays/PapayaGameplay_VendingMachines"); + foreach (var export in exports) + { + if (export is not { } uObject) continue; + if (!uObject.ExportType.Equals("B_Papaya_VendingMachine_Boat_C", StringComparison.OrdinalIgnoreCase) && + !uObject.ExportType.Equals("B_Papaya_VendingMachine_BoogieBomb_C", StringComparison.OrdinalIgnoreCase) && + !uObject.ExportType.Equals("B_Papaya_VendingMachine_Burger_C", StringComparison.OrdinalIgnoreCase) && + !uObject.ExportType.Equals("B_Papaya_VendingMachine_CrashPad_C", StringComparison.OrdinalIgnoreCase) && + !uObject.ExportType.Equals("B_Papaya_VendingMachine_FishingPole_C", StringComparison.OrdinalIgnoreCase) && + !uObject.ExportType.Equals("B_Papaya_VendingMachine_Grappler_C", StringComparison.OrdinalIgnoreCase) && + !uObject.ExportType.Equals("B_Papaya_VendingMachine_Jetpack_C", StringComparison.OrdinalIgnoreCase) && + !uObject.ExportType.Equals("B_Papaya_VendingMachine_PaintGrenade_Blue_C", StringComparison.OrdinalIgnoreCase) && + !uObject.ExportType.Equals("B_Papaya_VendingMachine_PaintGrenade_Red_C", StringComparison.OrdinalIgnoreCase) && + !uObject.ExportType.Equals("B_Papaya_VendingMachine_PaintLauncher_Blue_C", StringComparison.OrdinalIgnoreCase) && + !uObject.ExportType.Equals("B_Papaya_VendingMachine_PaintLauncher_Red_C", StringComparison.OrdinalIgnoreCase) && + !uObject.ExportType.Equals("B_Papaya_VendingMachine_PlungerBow_C", StringComparison.OrdinalIgnoreCase) && + !uObject.ExportType.Equals("B_Papaya_VendingMachine_Quad_C", StringComparison.OrdinalIgnoreCase) && + !uObject.ExportType.Equals("B_Papaya_VendingMachine_Tomato_C", StringComparison.OrdinalIgnoreCase)) continue; + + if (!uObject.TryGetValue(out FPackageIndex rootComponent, "RootComponent") || + !Utils.TryGetPackageIndexExport(rootComponent, out UObject root) || + !root.TryGetValue(out FVector relativeLocation, "RelativeLocation")) continue; + + var name = uObject.ExportType.SubstringAfter("B_Papaya_VendingMachine_").SubstringBeforeLast("_C"); + var vector = GetMapPosition(relativeLocation, _prRadius); + c.DrawPoint(vector.X, vector.Y, _pathPaint); + if (!set.Add(name)) continue; + + c.DrawText(name, vector.X, vector.Y - 12.5F, _fillPaint); + c.DrawText(name, vector.X, vector.Y - 12.5F, _textPaint); + } + + _bitmaps[1]["PapayaGameplay_VendingMachines"] = new MapLayer {Layer = timeTrialsBitmap, IsEnabled = false}; + }); + } + + private async Task LoadMusicBlocks() + { + await _threadWorkerView.Begin(_ => + { + _fillPaint.StrokeWidth = 5; + var shootingTargetsBitmap = new SKBitmap(_widthHeight, _widthHeight, SKColorType.Rgba8888, SKAlphaType.Premul); + using var c = new SKCanvas(shootingTargetsBitmap); + + var bDone = false; + var exports = Utils.LoadExports("/PapayaGameplay/LevelOverlays/PapayaGameplay_MusicBlocks"); + foreach (var export in exports) + { + if (export is not { } uObject) continue; + if (!uObject.ExportType.Equals("MusicBlock_Piano3_Papaya_C", StringComparison.OrdinalIgnoreCase)) continue; + + if (!uObject.TryGetValue(out FPackageIndex rootComponent, "RootComponent") || + !Utils.TryGetPackageIndexExport(rootComponent, out uObject) || + !uObject.TryGetValue(out FVector relativeLocation, "RelativeLocation")) continue; + + var vector = GetMapPosition(relativeLocation, _prRadius); + c.DrawPoint(vector.X, vector.Y, _pathPaint); + if (bDone) continue; + + bDone = true; + c.DrawText("Music Blocks", vector.X, vector.Y - 12.5F, _fillPaint); + c.DrawText("Music Blocks", vector.X, vector.Y - 12.5F, _textPaint); + } + + _bitmaps[1]["PapayaGameplay_MusicBlocks"] = new MapLayer {Layer = shootingTargetsBitmap, IsEnabled = false}; + }); + } + + private async Task LoadUpgradeBenches() + { + await _threadWorkerView.Begin(_ => + { + _fillPaint.StrokeWidth = 5; + var upgradeBenchesBitmap = new SKBitmap(_widthHeight, _widthHeight, SKColorType.Rgba8888, SKAlphaType.Premul); + using var c = new SKCanvas(upgradeBenchesBitmap); + + var exports = Utils.LoadExports("/NPCLibrary/LevelOverlays/Apollo_Terrain_NPCLibrary_Stations_UpgradeBenches"); + foreach (var export in exports) + { + if (export is not { } uObject) continue; + if (!uObject.ExportType.Equals("B_Athena_Spawner_UpgradeStation_C", StringComparison.OrdinalIgnoreCase)) continue; + var displayName = uObject.Name["B_Athena_Spawner_".Length..]; + + if (!uObject.TryGetValue(out FPackageIndex rootComponent, "RootComponent") || + !Utils.TryGetPackageIndexExport(rootComponent, out uObject) || + !uObject.TryGetValue(out FVector relativeLocation, "RelativeLocation")) continue; + + var vector = GetMapPosition(relativeLocation, _brRadius); + c.DrawPoint(vector.X, vector.Y, _pathPaint); + c.DrawText(displayName, vector.X, vector.Y - 12.5F, _fillPaint); + c.DrawText(displayName, vector.X, vector.Y - 12.5F, _textPaint); + } + + _bitmaps[0]["ApolloGameplay_UpgradeBenches"] = new MapLayer {Layer = upgradeBenchesBitmap, IsEnabled = false}; + }); + } + + private async Task LoadPhonebooths() + { + await _threadWorkerView.Begin(_ => + { + _fillPaint.StrokeWidth = 5; + var phoneboothsBitmap = new SKBitmap(_widthHeight, _widthHeight, SKColorType.Rgba8888, SKAlphaType.Premul); + using var c = new SKCanvas(phoneboothsBitmap); + + var exports = Utils.LoadExports("/NPCLibrary/LevelOverlays/Apollo_Terrain_NPCLibrary_Stations_Phonebooths"); + foreach (var export in exports) + { + if (export is not { } uObject) continue; + if (!uObject.ExportType.Equals("B_Athena_Spawner_Payphone_C", StringComparison.OrdinalIgnoreCase)) continue; + var displayName = uObject.Name["B_Athena_Spawner_".Length..]; + + if (!uObject.TryGetValue(out FPackageIndex rootComponent, "RootComponent") || + !Utils.TryGetPackageIndexExport(rootComponent, out uObject) || + !uObject.TryGetValue(out FVector relativeLocation, "RelativeLocation")) continue; + + var vector = GetMapPosition(relativeLocation, _brRadius); + c.DrawPoint(vector.X, vector.Y, _pathPaint); + c.DrawText(displayName, vector.X, vector.Y - 12.5F, _fillPaint); + c.DrawText(displayName, vector.X, vector.Y - 12.5F, _textPaint); + } + + _bitmaps[0]["ApolloGameplay_Phonebooths"] = new MapLayer {Layer = phoneboothsBitmap, IsEnabled = false}; + }); + } + + private async Task LoadAlienArtifacts() + { + await _threadWorkerView.Begin(_ => + { + _fillPaint.StrokeWidth = 5; + var alienArtifactsBitmap = new SKBitmap(_widthHeight, _widthHeight, SKColorType.Rgba8888, SKAlphaType.Premul); + using var c = new SKCanvas(alienArtifactsBitmap); + + var exports = Utils.LoadExports("/BattlepassS17/Maps/Apollo_ItemCollect_S17_Overlay"); + foreach (var export in exports) + { + if (export is not { } uObject) continue; + if (!uObject.ExportType.Equals("BP_S17_AlienArtifact_Variant1_C", StringComparison.OrdinalIgnoreCase)) continue; + var displayName = uObject.Name; + + if (!uObject.TryGetValue(out FPackageIndex rootComponent, "RootComponent") || + !Utils.TryGetPackageIndexExport(rootComponent, out uObject) || + !uObject.TryGetValue(out FVector relativeLocation, "RelativeLocation")) continue; + + var vector = GetMapPosition(relativeLocation, _brRadius); + c.DrawPoint(vector.X, vector.Y, _pathPaint); + c.DrawText(displayName, vector.X, vector.Y - 12.5F, _fillPaint); + c.DrawText(displayName, vector.X, vector.Y - 12.5F, _textPaint); + } + + _bitmaps[0]["ApolloGameplay_AlienArtifacts"] = new MapLayer {Layer = alienArtifactsBitmap, IsEnabled = false}; + }); + } + + private async Task LoadTagsLocation() + { + await _threadWorkerView.Begin(_ => + { + _fillPaint.StrokeWidth = 5; + var tagsLocationBitmap = new SKBitmap(_widthHeight, _widthHeight, SKColorType.Rgba8888, SKAlphaType.Premul); + using var c = new SKCanvas(tagsLocationBitmap); + + if (!Utils.TryLoadObject("FortniteGame/Content/Quests/QuestTagToLocationDataRows.QuestTagToLocationDataRows", out UDataTable locationData)) + return; + + foreach (var (key, uObject) in locationData.RowMap) + { + if (key.Text.StartsWith("Athena.Location.POI", StringComparison.OrdinalIgnoreCase) || + key.Text.StartsWith("Athena.Location.Unnamed", StringComparison.OrdinalIgnoreCase) || + key.Text.Contains(".Tandem.", StringComparison.OrdinalIgnoreCase) || + !uObject.TryGetValue(out FVector worldLocation, "WorldLocation")) continue; + + var parts = key.Text.Split('.'); + var displayName = parts[^2]; + if (!int.TryParse(parts[^1], out var _)) + displayName += " " + parts[^1]; + + var vector = GetMapPosition(worldLocation, _brRadius); + c.DrawPoint(vector.X, vector.Y, _pathPaint); + c.DrawText(displayName, vector.X, vector.Y - 12.5F, _fillPaint); + c.DrawText(displayName, vector.X, vector.Y - 12.5F, _textPaint); + } + + _bitmaps[0]["ApolloGameplay_TagsLocation"] = new MapLayer {Layer = tagsLocationBitmap, IsEnabled = false}; }); } } -} \ No newline at end of file +} diff --git a/FModel/ViewModels/SettingsViewModel.cs b/FModel/ViewModels/SettingsViewModel.cs index 1bec79be..ba4de94e 100644 --- a/FModel/ViewModels/SettingsViewModel.cs +++ b/FModel/ViewModels/SettingsViewModel.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using CUE4Parse.UE4.Versions; +using CUE4Parse_Conversion.Meshes; +using CUE4Parse_Conversion.Textures; using FModel.Framework; using FModel.Services; using FModel.Settings; @@ -82,6 +84,27 @@ namespace FModel.ViewModels get => _selectedCosmeticDisplayAsset; set => SetProperty(ref _selectedCosmeticDisplayAsset, value); } + + private EMeshFormat _selectedMeshExportFormat; + public EMeshFormat SelectedMeshExportFormat + { + get => _selectedMeshExportFormat; + set => SetProperty(ref _selectedMeshExportFormat, value); + } + + private ELodFormat _selectedLodExportFormat; + public ELodFormat SelectedLodExportFormat + { + get => _selectedLodExportFormat; + set => SetProperty(ref _selectedLodExportFormat, value); + } + + private ETextureFormat _selectedTextureExportFormat; + public ETextureFormat SelectedTextureExportFormat + { + get => _selectedTextureExportFormat; + set => SetProperty(ref _selectedTextureExportFormat, value); + } public ReadOnlyObservableCollection UpdateModes { get; private set; } public ReadOnlyObservableCollection UeGames { get; private set; } @@ -93,6 +116,9 @@ namespace FModel.ViewModels public ReadOnlyObservableCollection CompressedAudios { get; private set; } public ReadOnlyObservableCollection CosmeticStyles { get; private set; } public ReadOnlyObservableCollection CosmeticDisplayAssets { get; private set; } + public ReadOnlyObservableCollection MeshExportFormats { get; private set; } + public ReadOnlyObservableCollection LodExportFormats { get; private set; } + public ReadOnlyObservableCollection TextureExportFormats { get; private set; } private readonly FGame _game; private string _outputSnapshot; @@ -105,6 +131,9 @@ namespace FModel.ViewModels private ECompressedAudio _compressedAudioSnapshot; private EIconStyle _cosmeticStyleSnapshot; private EEnabledDisabled _cosmeticDisplayAssetSnapshot; + private EMeshFormat _meshExportFormatSnapshot; + private ELodFormat _lodExportFormatSnapshot; + private ETextureFormat _textureExportFormatSnapshot; public SettingsViewModel(FGame game) { @@ -123,6 +152,9 @@ namespace FModel.ViewModels _compressedAudioSnapshot = UserSettings.Default.CompressedAudioMode; _cosmeticStyleSnapshot = UserSettings.Default.CosmeticStyle; _cosmeticDisplayAssetSnapshot = UserSettings.Default.CosmeticDisplayAsset; + _meshExportFormatSnapshot = UserSettings.Default.MeshExportFormat; + _lodExportFormatSnapshot = UserSettings.Default.LodExportFormat; + _textureExportFormatSnapshot = UserSettings.Default.TextureExportFormat; SelectedUpdateMode = _updateModeSnapshot; SelectedUeGame = _ueGameSnapshot; @@ -132,6 +164,9 @@ namespace FModel.ViewModels SelectedCompressedAudio = _compressedAudioSnapshot; SelectedCosmeticStyle = _cosmeticStyleSnapshot; SelectedCosmeticDisplayAsset = _cosmeticDisplayAssetSnapshot; + SelectedMeshExportFormat = _meshExportFormatSnapshot; + SelectedLodExportFormat = _lodExportFormatSnapshot; + SelectedTextureExportFormat = _textureExportFormatSnapshot; SelectedAesReload = UserSettings.Default.AesReload; SelectedDiscordRpc = UserSettings.Default.DiscordRpc; @@ -145,6 +180,9 @@ namespace FModel.ViewModels CompressedAudios = new ReadOnlyObservableCollection(new ObservableCollection(EnumerateCompressedAudios())); CosmeticStyles = new ReadOnlyObservableCollection(new ObservableCollection(EnumerateCosmeticStyles())); CosmeticDisplayAssets = new ReadOnlyObservableCollection(new ObservableCollection(EnumerateEnabledDisabled())); + MeshExportFormats = new ReadOnlyObservableCollection(new ObservableCollection(EnumerateMeshExportFormat())); + LodExportFormats = new ReadOnlyObservableCollection(new ObservableCollection(EnumerateLodExportFormat())); + TextureExportFormats = new ReadOnlyObservableCollection(new ObservableCollection(EnumerateTextureExportFormat())); } public SettingsOut Save() @@ -167,6 +205,9 @@ namespace FModel.ViewModels UserSettings.Default.CompressedAudioMode = SelectedCompressedAudio; UserSettings.Default.CosmeticStyle = SelectedCosmeticStyle; UserSettings.Default.CosmeticDisplayAsset = SelectedCosmeticDisplayAsset; + UserSettings.Default.MeshExportFormat = SelectedMeshExportFormat; + UserSettings.Default.LodExportFormat = SelectedLodExportFormat; + UserSettings.Default.TextureExportFormat = SelectedTextureExportFormat; UserSettings.Default.AesReload = SelectedAesReload; UserSettings.Default.DiscordRpc = SelectedDiscordRpc; @@ -185,5 +226,8 @@ namespace FModel.ViewModels private IEnumerable EnumerateCompressedAudios() => Enum.GetValues(SelectedCompressedAudio.GetType()).Cast(); private IEnumerable EnumerateCosmeticStyles() => Enum.GetValues(SelectedCosmeticStyle.GetType()).Cast(); private IEnumerable EnumerateEnabledDisabled() => Enum.GetValues(SelectedCosmeticDisplayAsset.GetType()).Cast(); + private IEnumerable EnumerateMeshExportFormat() => Enum.GetValues(SelectedMeshExportFormat.GetType()).Cast(); + private IEnumerable EnumerateLodExportFormat() => Enum.GetValues(SelectedLodExportFormat.GetType()).Cast(); + private IEnumerable EnumerateTextureExportFormat() => Enum.GetValues(SelectedTextureExportFormat.GetType()).Cast(); } } \ No newline at end of file diff --git a/FModel/ViewModels/TabControlViewModel.cs b/FModel/ViewModels/TabControlViewModel.cs index 0b07f22c..458bec22 100644 --- a/FModel/ViewModels/TabControlViewModel.cs +++ b/FModel/ViewModels/TabControlViewModel.cs @@ -5,6 +5,7 @@ using System.Collections.ObjectModel; using System.IO; using System.Linq; using System.Windows; +using System.Windows.Media; using System.Windows.Media.Imaging; using FModel.Extensions; using FModel.Settings; @@ -125,6 +126,13 @@ namespace FModel.ViewModels } } + private BitmapScalingMode _imageRender = BitmapScalingMode.Linear; + public BitmapScalingMode ImageRender + { + get => _imageRender; + set => SetProperty(ref _imageRender, value); + } + public bool HasImage => Image != null; public bool ShouldScroll => !string.IsNullOrEmpty(ScrollTrigger); diff --git a/FModel/ViewModels/ThreadWorkerViewModel.cs b/FModel/ViewModels/ThreadWorkerViewModel.cs index f1f7f5f0..dc7f2e21 100644 --- a/FModel/ViewModels/ThreadWorkerViewModel.cs +++ b/FModel/ViewModels/ThreadWorkerViewModel.cs @@ -96,6 +96,7 @@ namespace FModel.ViewModels CurrentCancellationTokenSource = null; // kill token Log.Error("{Exception}", e); + FLogger.AppendError(); FLogger.AppendText(e.Message, Constants.WHITE, true); return; diff --git a/FModel/Views/About.xaml b/FModel/Views/About.xaml index 794eaef4..d0187ebf 100644 --- a/FModel/Views/About.xaml +++ b/FModel/Views/About.xaml @@ -1,6 +1,7 @@  @@ -18,7 +19,7 @@ - + diff --git a/FModel/Views/AudioPlayer.xaml.cs b/FModel/Views/AudioPlayer.xaml.cs index c2cad795..53da1f43 100644 --- a/FModel/Views/AudioPlayer.xaml.cs +++ b/FModel/Views/AudioPlayer.xaml.cs @@ -30,6 +30,7 @@ namespace FModel.Views { _applicationView.AudioPlayer.Stop(); _applicationView.AudioPlayer.Dispose(); + DiscordService.DiscordHandler.UpdateToSavedPresence(); } private void OnDeviceSwap(object sender, SelectionChangedEventArgs e) diff --git a/FModel/Views/BackupManager.xaml b/FModel/Views/BackupManager.xaml index e6d9dee7..8c3af476 100644 --- a/FModel/Views/BackupManager.xaml +++ b/FModel/Views/BackupManager.xaml @@ -6,7 +6,7 @@ xmlns:adonisControls="clr-namespace:AdonisUI.Controls;assembly=AdonisUI" xmlns:adonisExtensions="clr-namespace:AdonisUI.Extensions;assembly=AdonisUI" WindowStartupLocation="CenterScreen" ResizeMode="NoResize" IconVisibility="Collapsed" SizeToContent="Height" Loaded="OnLoaded" - Width="{Binding Source={x:Static SystemParameters.MaximizedPrimaryScreenWidth}, Converter={converters:RatioConverter}, ConverterParameter='0.35'}"> + Width="{Binding Source={x:Static SystemParameters.MaximizedPrimaryScreenWidth}, Converter={converters:RatioConverter}, ConverterParameter='0.225'}"> - - - - - - + + + + + + + + + + + + + + + + + + + + diff --git a/FModel/Views/MapViewer.xaml b/FModel/Views/MapViewer.xaml index f21ef599..b68d3f7f 100644 --- a/FModel/Views/MapViewer.xaml +++ b/FModel/Views/MapViewer.xaml @@ -1,13 +1,14 @@  + MinWidth="{Binding Source={x:Static SystemParameters.MaximizedPrimaryScreenWidth}, Converter={converters:RatioConverter}, ConverterParameter='0.50'}"> + + \ No newline at end of file diff --git a/FModel/Views/SearchView.xaml.cs b/FModel/Views/SearchView.xaml.cs index 1684ebe8..f9262d8c 100644 --- a/FModel/Views/SearchView.xaml.cs +++ b/FModel/Views/SearchView.xaml.cs @@ -38,6 +38,8 @@ namespace FModel.Views MainWindow.YesWeCats.AssetsListName.ItemsSource = null; var folder = _applicationView.CustomDirectories.GoToCommand.JumpTo(assetItem.FullPath.SubstringBeforeLast('/')); if (folder == null) return; + + MainWindow.YesWeCats.Activate(); do { await Task.Delay(100); } while (MainWindow.YesWeCats.AssetsListName.Items.Count < folder.AssetsList.Assets.Count); MainWindow.YesWeCats.LeftTabControl.SelectedIndex = 2; // assets tab diff --git a/FModel/Views/SettingsView.xaml b/FModel/Views/SettingsView.xaml index ad88f3a7..c0dbd8c5 100644 --- a/FModel/Views/SettingsView.xaml +++ b/FModel/Views/SettingsView.xaml @@ -35,6 +35,7 @@ + @@ -45,11 +46,11 @@ - +