diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..1c2f8efd --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +custom: "https://fmodel.app/donate" \ No newline at end of file diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4c543700..8e6f8404 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -30,7 +30,7 @@ jobs: run: dotnet restore FModel - name: .NET Publish - run: dotnet publish FModel -c Release --no-self-contained -r win-x64 -f net7.0-windows -o "./FModel/bin/Publish/" -p:PublishReadyToRun=false -p:PublishSingleFile=true -p:DebugType=None -p:GenerateDocumentationFile=false -p:DebugSymbols=false -p:AssemblyVersion=${{ github.event.inputs.appVersion }} -p:FileVersion=${{ github.event.inputs.appVersion }} + run: dotnet publish FModel -c Release --no-self-contained -r win-x64 -f net6.0-windows -o "./FModel/bin/Publish/" -p:PublishReadyToRun=false -p:PublishSingleFile=true -p:DebugType=None -p:GenerateDocumentationFile=false -p:DebugSymbols=false -p:AssemblyVersion=${{ github.event.inputs.appVersion }} -p:FileVersion=${{ github.event.inputs.appVersion }} - name: ZIP File uses: papeloto/action-zip@v1 @@ -45,4 +45,4 @@ jobs: automatic_release_tag: ${{ github.event.inputs.appVersion }} repo_token: ${{ secrets.GITHUB_TOKEN }} prerelease: false - files: FModel.zip + files: FModel.zip \ No newline at end of file diff --git a/.gitmodules b/.gitmodules index 2221fc41..959570f6 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "CUE4Parse"] path = CUE4Parse url = https://github.com/FabianFG/CUE4Parse +[submodule "EpicManifestParser"] + path = EpicManifestParser + url = https://github.com/FModel/EpicManifestParser \ No newline at end of file diff --git a/CUE4Parse b/CUE4Parse index d1251ca4..5655c30f 160000 --- a/CUE4Parse +++ b/CUE4Parse @@ -1 +1 @@ -Subproject commit d1251ca4502cc374e65e8bdd6c920fffce20a6f6 +Subproject commit 5655c30f180a50bd07dc0282daace6b6edc9ea72 diff --git a/EpicManifestParser b/EpicManifestParser new file mode 160000 index 00000000..97174265 --- /dev/null +++ b/EpicManifestParser @@ -0,0 +1 @@ +Subproject commit 97174265894eddcb0d94ef570d36ddc559a8573a diff --git a/FModel/App.xaml.cs b/FModel/App.xaml.cs index e991c0ad..f889e0f4 100644 --- a/FModel/App.xaml.cs +++ b/FModel/App.xaml.cs @@ -2,9 +2,9 @@ using Microsoft.Win32; using Serilog; using System; +using System.Globalization; using System.IO; using System.Runtime.InteropServices; -using System.Threading; using System.Windows; using System.Windows.Threading; using FModel.Framework; @@ -89,14 +89,18 @@ public partial class App Directory.CreateDirectory(Path.Combine(UserSettings.Default.OutputDirectory, "Logs")); Directory.CreateDirectory(Path.Combine(UserSettings.Default.OutputDirectory, ".data")); +#if DEBUG + Log.Logger = new LoggerConfiguration().WriteTo.Console(theme: AnsiConsoleTheme.Literate).CreateLogger(); +#else Log.Logger = new LoggerConfiguration().WriteTo.Console(theme: AnsiConsoleTheme.Literate).WriteTo.File( Path.Combine(UserSettings.Default.OutputDirectory, "Logs", $"FModel-Log-{DateTime.Now:yyyy-MM-dd}.txt"), outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss} [FModel] [{Level:u3}] {Message:lj}{NewLine}{Exception}").CreateLogger(); +#endif Log.Information("Version {Version}", Constants.APP_VERSION); Log.Information("{OS}", GetOperatingSystemProductName()); Log.Information("{RuntimeVer}", RuntimeInformation.FrameworkDescription); - Log.Information("Culture {SysLang}", Thread.CurrentThread.CurrentUICulture); + Log.Information("Culture {SysLang}", CultureInfo.CurrentCulture); } private void AppExit(object sender, ExitEventArgs e) @@ -162,4 +166,4 @@ public partial class App return rk.GetValue(name, null) as string; return string.Empty; } -} \ No newline at end of file +} diff --git a/FModel/Constants.cs b/FModel/Constants.cs index 6cb078ac..e2667514 100644 --- a/FModel/Constants.cs +++ b/FModel/Constants.cs @@ -9,7 +9,11 @@ public static class Constants public const string ZERO_64_CHAR = "0000000000000000000000000000000000000000000000000000000000000000"; public static readonly FGuid ZERO_GUID = new(0U); + public const float SCALE_DOWN_RATIO = 0.01F; + public const int SAMPLES_COUNT = 4; + public const string WHITE = "#DAE5F2"; + public const string GRAY = "#BBBBBB"; public const string RED = "#E06C75"; public const string GREEN = "#98C379"; public const string YELLOW = "#E5C07B"; @@ -23,4 +27,4 @@ public static class Constants public const string _VAL_LIVE_TRIGGER = "valorant-live.manifest"; public const string _NO_PRESET_TRIGGER = "Hand Made"; -} \ No newline at end of file +} diff --git a/FModel/Creator/Bases/FN/BaseCommunity.cs b/FModel/Creator/Bases/FN/BaseCommunity.cs index 8daf66a4..761725c9 100644 --- a/FModel/Creator/Bases/FN/BaseCommunity.cs +++ b/FModel/Creator/Bases/FN/BaseCommunity.cs @@ -1,8 +1,8 @@ +using CUE4Parse.GameTypes.FN.Enums; using CUE4Parse.UE4.Assets.Exports; using CUE4Parse.UE4.Objects.GameplayTags; using CUE4Parse.UE4.Objects.UObject; using CUE4Parse.UE4.Versions; -using CUE4Parse_Fortnite.Enums; using FModel.Extensions; using FModel.Framework; using FModel.Services; @@ -101,20 +101,8 @@ public class BaseCommunity : BaseIcon { if (!bShort) return base.GetCosmeticSeason(seasonNumber); var s = seasonNumber["Cosmetics.Filter.Season.".Length..]; - var number = int.Parse(s); - - switch (number) - { - case 10: - s = "X"; - break; - case > 18: - number += 2; - s = number.ToString(); - break; - } - - return $"C{number / 10 + 1} S{s[^1..]}"; + (int chapterIdx, int seasonIdx) = GetInternalSID(int.Parse(s)); + return $"C{chapterIdx} S{seasonIdx}"; } private new void DrawBackground(SKCanvas c) @@ -272,4 +260,4 @@ public class BaseCommunity : BaseIcon // draw } } -} \ No newline at end of file +} diff --git a/FModel/Creator/Bases/FN/BaseIcon.cs b/FModel/Creator/Bases/FN/BaseIcon.cs index dd00cb57..b47109e8 100644 --- a/FModel/Creator/Bases/FN/BaseIcon.cs +++ b/FModel/Creator/Bases/FN/BaseIcon.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Windows; +using CUE4Parse.GameTypes.FN.Enums; using CUE4Parse.UE4.Assets.Exports; using CUE4Parse.UE4.Assets.Exports.Engine; using CUE4Parse.UE4.Assets.Exports.Material; @@ -12,7 +13,6 @@ using CUE4Parse.UE4.Objects.Core.Math; using CUE4Parse.UE4.Objects.GameplayTags; using CUE4Parse.UE4.Objects.UObject; using CUE4Parse_Conversion.Textures; -using CUE4Parse_Fortnite.Enums; using FModel.Settings; using SkiaSharp; @@ -25,9 +25,7 @@ public class BaseIcon : UCreator protected string CosmeticSource { get; set; } protected Dictionary UserFacingFlags { get; set; } - public BaseIcon(UObject uObject, EIconStyle style) : base(uObject, style) - { - } + public BaseIcon(UObject uObject, EIconStyle style) : base(uObject, style) { } public void ParseForReward(bool isUsingDisplayAsset) { @@ -195,29 +193,45 @@ public class BaseIcon : UCreator return string.Format(format, name); } + protected (int, int) GetInternalSID(int number) + { + static int GetSeasonsInChapter(int chapter) => chapter switch + { + 1 => 10, + 2 => 8, + 3 => 4, + _ => 10 + }; + + var chapterIdx = 0; + var seasonIdx = 0; + while (number > 0) + { + var seasonsInChapter = GetSeasonsInChapter(++chapterIdx); + if (number > seasonsInChapter) + number -= seasonsInChapter; + else + { + seasonIdx = number; + number = 0; + } + } + return (chapterIdx, seasonIdx); + } + protected string GetCosmeticSeason(string seasonNumber) { var s = seasonNumber["Cosmetics.Filter.Season.".Length..]; - var number = int.Parse(s); - - switch (number) - { - case 10: - s = "X"; - break; - case > 18: - number += 2; - s = number.ToString(); - break; - } + var initial = int.Parse(s); + (int chapterIdx, int seasonIdx) = GetInternalSID(initial); var season = Utils.GetLocalizedResource("AthenaSeasonItemDefinitionInternal", "SeasonTextFormat", "Season {0}"); var introduced = Utils.GetLocalizedResource("Fort.Cosmetics", "CosmeticItemDescription_Season", "\nIntroduced in {0}."); - if (number <= 10) return Utils.RemoveHtmlTags(string.Format(introduced, string.Format(season, s))); + if (initial <= 10) return Utils.RemoveHtmlTags(string.Format(introduced, string.Format(season, s))); var chapter = Utils.GetLocalizedResource("AthenaSeasonItemDefinitionInternal", "ChapterTextFormat", "Chapter {0}"); var chapterFormat = Utils.GetLocalizedResource("AthenaSeasonItemDefinitionInternal", "ChapterSeasonTextFormat", "{0}, {1}"); - var d = string.Format(chapterFormat, string.Format(chapter, number / 10 + 1), string.Format(season, s[^1..])); + var d = string.Format(chapterFormat, string.Format(chapter, chapterIdx), string.Format(season, seasonIdx)); return Utils.RemoveHtmlTags(string.Format(introduced, d)); } @@ -281,4 +295,4 @@ public class BaseIcon : UCreator x += size; } } -} \ No newline at end of file +} diff --git a/FModel/Creator/Bases/FN/BaseIconStats.cs b/FModel/Creator/Bases/FN/BaseIconStats.cs index a23e02fc..3a25871f 100644 --- a/FModel/Creator/Bases/FN/BaseIconStats.cs +++ b/FModel/Creator/Bases/FN/BaseIconStats.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using CUE4Parse.GameTypes.FN.Enums; using CUE4Parse.UE4.Assets.Exports; using CUE4Parse.UE4.Assets.Exports.Engine; using CUE4Parse.UE4.Assets.Objects; @@ -7,7 +8,6 @@ using CUE4Parse.UE4.Objects.Core.i18N; using CUE4Parse.UE4.Objects.Engine.Curves; using CUE4Parse.UE4.Objects.GameplayTags; using CUE4Parse.UE4.Objects.UObject; -using CUE4Parse_Fortnite.Enums; using FModel.Extensions; using FModel.Framework; using SkiaSharp; @@ -288,4 +288,4 @@ public class IconStat var sliderWidth = (sliderRight - height * 2) * (floatValue / _maxValue); c.DrawRect(new SKRect(height * 2, y, Math.Min(height * 2 + sliderWidth, sliderRight), y + 5), _statPaint); } -} \ No newline at end of file +} diff --git a/FModel/Creator/Bases/MV/BaseFighter.cs b/FModel/Creator/Bases/MV/BaseFighter.cs new file mode 100644 index 00000000..4d8128a3 --- /dev/null +++ b/FModel/Creator/Bases/MV/BaseFighter.cs @@ -0,0 +1,287 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using System.ComponentModel; +using CUE4Parse.UE4.Assets.Exports; +using CUE4Parse.UE4.Assets.Exports.Engine; +using CUE4Parse.UE4.Assets.Exports.Material; +using CUE4Parse.UE4.Assets.Objects; +using CUE4Parse.UE4.Objects.Core.i18N; +using CUE4Parse.UE4.Objects.Core.Math; +using CUE4Parse.UE4.Objects.UObject; +using FModel.Extensions; +using SkiaSharp; + +namespace FModel.Creator.Bases.MV; + +public class BaseFighter : UCreator +{ + private float _xOffset = 1f; + private float _yOffset = 1f; + private float _zoom = 1f; + + private readonly SKBitmap _pattern; + private readonly SKBitmap _perk; + private readonly SKBitmap _emote; + private readonly SKBitmap _skin; + + private (SKBitmap, List) _fighterType; + private readonly List _recommendedPerks; + private readonly List _availableTaunts; + private readonly List _skins; + + public BaseFighter(UObject uObject, EIconStyle style) : base(uObject, style) + { + Width = 1024; + DisplayNamePaint.TextSize = 100; + DisplayNamePaint.TextAlign = SKTextAlign.Left; + DisplayNamePaint.Typeface = Utils.Typefaces.TandemDisplayName; + DescriptionPaint.TextSize = 25; + DescriptionPaint.Typeface = Utils.Typefaces.TandemGenDescription; + DefaultPreview = Utils.GetBitmap("/Game/Panda_Main/UI/PreMatch/Images/DiamondPortraits/0010_Random.0010_Random"); + + _pattern = Utils.GetBitmap("/Game/Panda_Main/UI/Assets/UI_Textures/halftone_jagged.halftone_jagged"); + _perk = Utils.GetBitmap("/Game/Panda_Main/UI/Assets/Icons/ui_icons_perks.ui_icons_perks"); + _emote = Utils.GetBitmap("/Game/Panda_Main/UI/Assets/Icons/ui_icons_emote.ui_icons_emote"); + _skin = Utils.GetBitmap("/Game/Panda_Main/UI/Assets/Icons/ui_icons_skins.ui_icons_skins"); + _fighterType.Item2 = new List(); + _recommendedPerks = new List(); + _availableTaunts = new List(); + _skins = new List(); + } + + public override void ParseForInfo() + { + if (Object.TryGetValue(out FLinearColor backgroundColor, "BackgroundColor")) + Background = new[] { SKColor.Parse(backgroundColor.Hex) }; + + if (Object.TryGetValue(out FSoftObjectPath portraitMaterial, "CollectionsPortraitMaterial") && + portraitMaterial.TryLoad(out UMaterialInstanceConstant portrait)) + { + _xOffset = Math.Abs(portrait.ScalarParameterValues.FirstOrDefault(x => x.ParameterInfo.Name.Text == "XOffset")?.ParameterValue ?? 1f); + _yOffset = Math.Abs(portrait.ScalarParameterValues.FirstOrDefault(x => x.ParameterInfo.Name.Text == "YOffset")?.ParameterValue / 10 ?? 1f); + _zoom = Math.Clamp(portrait.ScalarParameterValues.FirstOrDefault(x => x.ParameterInfo.Name.Text == "Zoom")?.ParameterValue ?? 1f, 0, 1); + Preview = Utils.GetBitmap(portrait); + } + else if (Object.TryGetValue(out FSoftObjectPath portraitTexture, "NewCharacterSelectPortraitTexture", "HUDPortraitTexture")) + Preview = Utils.GetBitmap(portraitTexture); + + if (Object.TryGetValue(out FText displayName, "DisplayName")) + DisplayName = displayName.Text; + + GetFighterClassInfo(Object.GetOrDefault("Class", EFighterClass.Support)); + _fighterType.Item2.Add(Utils.GetLocalizedResource(Object.GetOrDefault("Type", EFighterType.Horizontal))); + if (Object.TryGetValue(out FText property, "Property")) + _fighterType.Item2.Add(property.Text); + + if (Object.TryGetValue(out UScriptSet recommendedPerks, "RecommendedPerkDatas")) // PORCO DIO WB USE ARRAYS!!!!!! + { + foreach (var recommendedPerk in recommendedPerks.Properties) + { + if (recommendedPerk.GenericValue is not FPackageIndex packageIndex || + !Utils.TryGetPackageIndexExport(packageIndex, out UObject export) || + !export.TryGetValue(out FSoftObjectPath rewardThumbnail, "RewardThumbnail")) + continue; + + _recommendedPerks.Add(Utils.GetBitmap(rewardThumbnail)); + } + } + + if (Object.TryGetValue(out FSoftObjectPath[] availableTaunts, "AvailableTauntData")) + { + foreach (var taunt in availableTaunts) + { + if (!Utils.TryLoadObject(taunt.AssetPathName.Text, out UObject export) || + !export.TryGetValue(out FSoftObjectPath rewardThumbnail, "RewardThumbnail")) + continue; + + _availableTaunts.Add(Utils.GetBitmap(rewardThumbnail)); + } + } + + if (Object.TryGetValue(out FSoftObjectPath[] skins, "Skins")) + { + foreach (var skin in skins) + { + if (!Utils.TryLoadObject(skin.AssetPathName.Text, out UObject export) || + !export.TryGetValue(out FSoftObjectPath rewardThumbnail, "RewardThumbnail")) + continue; + + _skins.Add(Utils.GetBitmap(rewardThumbnail)); + } + } + } + + public override SKBitmap[] Draw() + { + var ret = new SKBitmap(Width, Height, SKColorType.Rgba8888, SKAlphaType.Premul); + using var c = new SKCanvas(ret); + + DrawBackground(c); + DrawPreview(c); + DrawDisplayName(c); + DrawFighterInfo(c); + DrawRecommendedPerks(c); + DrawAvailableTaunts(c); + DrawSkins(c); + + return new[] { ret }; + } + + private void GetFighterClassInfo(EFighterClass clas) + { + if (!Utils.TryLoadObject("/Game/Panda_Main/UI/In-Game/Data/UICharacterClassInfo_Datatable.UICharacterClassInfo_Datatable", out UDataTable dataTable)) + return; + + var row = dataTable.RowMap.ElementAt((int) clas).Value; + if (!row.TryGetValue(out FText displayName, "DisplayName_5_9DB5DDFF490E1F4AD72329866F96B81D") || + !row.TryGetValue(out FPackageIndex icon, "Icon_8_711534AD4F240D4B001AA6A471EA1895")) + return; + + _fighterType.Item1 = Utils.GetBitmap(icon); + _fighterType.Item2.Add(displayName.Text); + } + + private new void DrawBackground(SKCanvas c) + { + c.DrawRect(new SKRect(0, 0, Width, Height), + new SKPaint + { + IsAntialias = true, FilterQuality = SKFilterQuality.High, Color = Background[0] + }); + + if (!string.IsNullOrWhiteSpace(DisplayName)) + { + c.DrawText(DisplayName, -50, 125, new() + { + IsAntialias = true, FilterQuality = SKFilterQuality.High, + Typeface = Utils.Typefaces.DisplayName, TextSize = 200, + TextScaleX = .95f, TextSkewX = -0.25f, Color = SKColors.Black.WithAlpha(25) + }); + } + + c.DrawBitmap(_pattern, new SKRect(0, Height / 2, Width, Height), new SKPaint + { + IsAntialias = true, FilterQuality = SKFilterQuality.High, BlendMode = SKBlendMode.SoftLight + }); + + var path = new SKPath { FillType = SKPathFillType.EvenOdd }; + path.MoveTo(0, Height); + path.LineTo(0, Height - 20); + path.LineTo(Width, Height - 60); + path.LineTo(Width, Height); + path.Close(); + c.DrawPath(path, new SKPaint + { + IsAntialias = true, + FilterQuality = SKFilterQuality.High, + Color = SKColor.Parse("#141629") + }); + } + + private new void DrawPreview(SKCanvas c) + { + var img = (Preview ?? DefaultPreview).ResizeWithRatio(_zoom); + var x_offset = img.Width * _xOffset; + var y_offset = img.Height * -_yOffset; + c.DrawBitmap(img, new SKRect(Width + x_offset - img.Width, y_offset, Width + x_offset, img.Height + y_offset), ImagePaint); + } + + private new void DrawDisplayName(SKCanvas c) + { + if (string.IsNullOrWhiteSpace(DisplayName)) return; + c.DrawText(DisplayName.ToUpper(), 50, 100, DisplayNamePaint); + } + + private void DrawFighterInfo(SKCanvas c) + { + if (_fighterType.Item1 != null) + c.DrawBitmap(_fighterType.Item1, new SKRect(50, 112.5f, 98, 160.5f), ImagePaint); + + c.DrawText(string.Join(" | ", _fighterType.Item2), 98, 145, DescriptionPaint); + } + + private void DrawRecommendedPerks(SKCanvas c) + { + const int x = 50; + const int y = 200; + const int size = 64; + + ImagePaint.ImageFilter = null; + ImagePaint.BlendMode = SKBlendMode.SoftLight; + c.DrawBitmap(_perk, new SKRect(x, y, x + size / 2, y + size / 2), ImagePaint); + if (_recommendedPerks.Count < 1) return; + + ImagePaint.BlendMode = SKBlendMode.SrcOver; + ImagePaint.ImageFilter = SKImageFilter.CreateDropShadow(0, 0, 2.5f, 2.5f, SKColors.Black); + c.DrawBitmap(_recommendedPerks[1], new SKRect(161, y, 225, y + size), ImagePaint); + c.DrawBitmap(_recommendedPerks[2], new SKRect(193, y + size / 2, 257, y + size * 1.5f), ImagePaint); + c.DrawBitmap(_recommendedPerks[3], new SKRect(161, y + size, 225, y + size * 2), ImagePaint); + + ImagePaint.ImageFilter = SKImageFilter.CreateDropShadow(0, 0, 5, 5, SKColors.Black.WithAlpha(150)); + c.DrawBitmap(_recommendedPerks[0], new SKRect(x, y, x + size * 2, y + size * 2), ImagePaint); + } + + private void DrawAvailableTaunts(SKCanvas c) + { + var x = 300; + const int y = 232; + const int size = 64; + + ImagePaint.ImageFilter = null; + ImagePaint.BlendMode = SKBlendMode.SoftLight; + c.DrawBitmap(_emote, new SKRect(x, y - size / 2, x + size / 2, y), ImagePaint); + if (_availableTaunts.Count < 1) return; + + ImagePaint.BlendMode = SKBlendMode.SrcOver; + ImagePaint.ImageFilter = SKImageFilter.CreateDropShadow(0, 0, 1.5f, 1.5f, SKColors.Black); + + foreach (var taunt in _availableTaunts) + { + c.DrawBitmap(taunt, new SKRect(x, y, x + size, y + size), ImagePaint); + x += size; + } + } + + private void DrawSkins(SKCanvas c) + { + var x = 50; + const int y = 333; + const int size = 128; + + ImagePaint.ImageFilter = null; + ImagePaint.BlendMode = SKBlendMode.SoftLight; + c.DrawBitmap(_skin, new SKRect(x, y, x + size / 4, y + size / 4), ImagePaint); + if (_skins.Count < 1) return; + + ImagePaint.BlendMode = SKBlendMode.SrcOver; + ImagePaint.ImageFilter = SKImageFilter.CreateDropShadow(0, 0, 1.5f, 1.5f, SKColors.Black); + foreach (var skin in _skins) + { + c.DrawBitmap(skin, new SKRect(x, y, x + size, y + size), ImagePaint); + x += size; + } + } +} + +public enum EFighterClass : byte +{ + Mage = 4, + Tank = 3, + Fighter = 2, + Bruiser = 2, + Assassin = 1, + Support = 0 // Default +} + +public enum EFighterType : byte +{ + [Description("B980C82D40FF37FD359C74A339CE1B3A")] + Hybrid = 2, + + [Description("2C55443D47164019BE73A5ABDC670F36")] + Vertical = 1, + + [Description("97A60DD54AA23D4B93D5B891F729BF5C")] + Horizontal = 0 // Default +} diff --git a/FModel/Creator/Bases/MV/BasePandaIcon.cs b/FModel/Creator/Bases/MV/BasePandaIcon.cs new file mode 100644 index 00000000..cab2f3a1 --- /dev/null +++ b/FModel/Creator/Bases/MV/BasePandaIcon.cs @@ -0,0 +1,344 @@ +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using CUE4Parse.UE4.Assets.Exports; +using CUE4Parse.UE4.Assets.Exports.Engine; +using CUE4Parse.UE4.Objects.Core.i18N; +using CUE4Parse.UE4.Objects.Core.Math; +using CUE4Parse.UE4.Objects.UObject; +using SkiaSharp; + +namespace FModel.Creator.Bases.MV; + +public class BasePandaIcon : UCreator +{ + private float _y_offset; + private ERewardRarity _rarity; + private string _type; + + protected readonly List<(SKBitmap, string)> Pictos; + + public BasePandaIcon(UObject uObject, EIconStyle style) : base(uObject, style) + { + Width = 1024; + Margin = 30; + DisplayNamePaint.TextSize = 50; + DisplayNamePaint.TextAlign = SKTextAlign.Left; + DisplayNamePaint.Color = SKColor.Parse("#191C33"); + DescriptionPaint.TextSize = 25; + DefaultPreview = Utils.GetBitmap("/Game/Panda_Main/UI/PreMatch/Images/DiamondPortraits/0010_Random.0010_Random"); + + _y_offset = Height / 2 + DescriptionPaint.TextSize; + Pictos = new List<(SKBitmap, string)>(); + } + + public override void ParseForInfo() + { + var category = Object.GetOrDefault("Category", EPerkCategory.Offense); + _rarity = Object.GetOrDefault("Rarity", ERewardRarity.None); + + _type = Object.ExportType; + var t = _type switch // ERewardType like + { + "StatTrackingBundleData" => EItemType.Badge, + "AnnouncerPackData" => EItemType.Announcer, + "CharacterGiftData" => EItemType.ExperiencePoints, + "ProfileIconData" => EItemType.ProfileIcon, + "RingOutVfxData" => EItemType.Ringout, + "BannerData" => EItemType.Banner, + "EmoteData" => EItemType.Sticker, + "QuestData" => EItemType.Mission, + "TauntData" => EItemType.Emote, + "SkinData" => EItemType.Variant, + "PerkData" when category == EPerkCategory.CharacterSpecific => EItemType.SignaturePerk, + "PerkData" => EItemType.Perk, + _ => EItemType.Unknown + }; + + if (t == EItemType.SignaturePerk) _rarity = ERewardRarity.Legendary; + Background = GetRarityBackground(_rarity); + + if (Object.TryGetValue(out FSoftObjectPath rewardThumbnail, "RewardThumbnail", "DisplayTextureRef", "Texture")) + Preview = Utils.GetBitmap(rewardThumbnail); + else if (Object.TryGetValue(out FPackageIndex icon, "Icon")) + Preview = Utils.GetBitmap(icon); + + if (Object.TryGetValue(out FText displayName, "DisplayName", "QuestName")) + DisplayName = displayName.Text; + if (Object.TryGetValue(out FText description, "Description", "QuestDescription")) + Description = Utils.RemoveHtmlTags(description.Text); + + var unlockLocation = Object.GetOrDefault("UnlockLocation", EUnlockLocation.None); + if (t == EItemType.Unknown && unlockLocation == EUnlockLocation.CharacterMastery) t = EItemType.MasteryLevel; + + Pictos.Add((Utils.GetBitmap("/Game/Panda_Main/UI/Assets/Icons/ui_icons_unlocked.ui_icons_unlocked"), Utils.GetLocalizedResource(unlockLocation))); + if (Object.TryGetValue(out string slug, "Slug")) + { + t = _type switch + { + "HydraSyncedDataAsset" when slug == "gold" => EItemType.Gold, + "HydraSyncedDataAsset" when slug == "gleamium" => EItemType.Gleamium, + "HydraSyncedDataAsset" when slug == "match_toasts" => EItemType.Toast, + _ => t + }; + Pictos.Add((Utils.GetBitmap("/Game/Panda_Main/UI/Assets/Icons/ui_icons_link.ui_icons_link"), slug)); + } + + if (Object.TryGetValue(out int xpValue, "XPValue")) + DisplayName += $" (+{xpValue})"; + + if (Utils.TryLoadObject("/Game/Panda_Main/UI/Prototype/Foundation/Types/DT_EconomyGlossary.DT_EconomyGlossary", out UDataTable dataTable)) + { + if (t != EItemType.Unknown && + dataTable.RowMap.ElementAt((int) t).Value.TryGetValue(out FText name, "Name_14_7F75AD6047CBDEA7B252B1BD76EF84B9")) + _type = name.Text; + } + } + + public override SKBitmap[] Draw() + { + var ret = new SKBitmap(Width, Height, SKColorType.Rgba8888, SKAlphaType.Premul); + using var c = new SKCanvas(ret); + + DrawBackground(c); + DrawPreview(c); + DrawDisplayName(c); + DrawDescription(c); + DrawPictos(c); + + return new[] { ret }; + } + + private SKColor[] GetRarityBackground(ERewardRarity rarity) + { + return rarity switch // the colors here are the base color and brighter color that the game uses for rarities from the "Rarity to Color" blueprint function + { + ERewardRarity.Common => new[] + { + SKColor.Parse(new FLinearColor(0.068478f, 0.651406f, 0.016807f, 1.000000f).Hex), + SKColor.Parse(new FLinearColor(0.081422f, 1.000000f, 0.000000f, 1.000000f).Hex) + }, + ERewardRarity.Rare => new[] + { + SKColor.Parse(new FLinearColor(0.035911f, 0.394246f, 0.900000f, 1.000000f).Hex), + SKColor.Parse(new FLinearColor(0.033333f, 0.434207f, 1.000000f, 1.000000f).Hex) + }, + ERewardRarity.Epic => new[] + { + SKColor.Parse(new FLinearColor(0.530391f, 0.060502f, 0.900000f, 1.000000f).Hex), + SKColor.Parse(new FLinearColor(0.579907f, 0.045833f, 1.000000f, 1.000000f).Hex) + }, + ERewardRarity.Legendary => new[] + { + SKColor.Parse(new FLinearColor(1.000000f, 0.223228f, 0.002428f, 1.000000f).Hex), + SKColor.Parse(new FLinearColor(1.000000f, 0.479320f, 0.030713f, 1.000000f).Hex) + }, + _ => new[] + { + SKColor.Parse(new FLinearColor(0.194618f, 0.651406f, 0.630757f, 1.000000f).Hex), + SKColor.Parse(new FLinearColor(0.273627f, 0.955208f, 0.914839f, 1.000000f).Hex) + } + }; + } + + private new void DrawBackground(SKCanvas c) + { + c.DrawRect(new SKRect(0, 0, Width, Height), + new SKPaint + { + IsAntialias = true, FilterQuality = SKFilterQuality.High, Color = SKColor.Parse("#F3FCF0") + }); + + var has_tr = _rarity != ERewardRarity.None; + var tr = Utils.GetLocalizedResource(_rarity); + var tr_paint = new SKPaint + { + IsAntialias = true, + FilterQuality = SKFilterQuality.High, + TextAlign = SKTextAlign.Right, + TextSize = 35, + Color = SKColors.White, + Typeface = Utils.Typefaces.DisplayName + }; + + var path = new SKPath { FillType = SKPathFillType.EvenOdd }; + path.MoveTo(0, Height); + path.LineTo(14, Height); + path.LineTo(20, 20); + if (has_tr) + { + const int margin = 15; + var width = tr_paint.MeasureText(tr); + path.LineTo(Width - width - margin * 2, 15); + path.LineTo(Width - width - margin * 2.5f, 60); + path.LineTo(Width, 55); + } + else + { + path.LineTo(Width, 14); + } + path.LineTo(Width, 0); + path.LineTo(0, 0); + path.LineTo(0, Height); + path.Close(); + c.DrawPath(path, new SKPaint + { + IsAntialias = true, + FilterQuality = SKFilterQuality.High, + Shader = SKShader.CreateLinearGradient( + new SKPoint(Width / 2, Height), new SKPoint(Width, Height / 4), Background, SKShaderTileMode.Clamp) + }); + + if (has_tr) + { + var x = Width - 20f; + foreach (var a in tr.Select(character => character.ToString()).Reverse()) + { + c.DrawText(a, x, 40, tr_paint); + x -= tr_paint.MeasureText(a) - 2; + } + } + } + + private new void DrawPreview(SKCanvas c) + { + const int size = 384; + var y = Height - size - Margin * 2; + c.DrawBitmap(Preview ?? DefaultPreview, new SKRect(Margin, y, size + Margin, y + size), ImagePaint); + } + + private new void DrawDisplayName(SKCanvas c) + { + if (string.IsNullOrWhiteSpace(DisplayName)) + return; + + var x = 450f; + while (DisplayNamePaint.MeasureText(DisplayName) > Width - x / 1.25) + { + DisplayNamePaint.TextSize -= 1; + } + + var y = Height / 2 - DisplayNamePaint.TextSize / 4; + foreach (var a in DisplayName.Select(character => character.ToString())) + { + c.DrawText(a, x, y, DisplayNamePaint); + x += DisplayNamePaint.MeasureText(a) - 4; + } + } + + private new void DrawDescription(SKCanvas c) + { + const int x = 450; + DescriptionPaint.Color = Background[0]; + c.DrawText(_type.ToUpper(), x, 170, DescriptionPaint); + + if (string.IsNullOrWhiteSpace(Description)) return; + + DescriptionPaint.Color = SKColor.Parse("#191C33"); + Utils.DrawMultilineText(c, Description, Width - x, Margin, SKTextAlign.Left, + new SKRect(x, _y_offset, Width - Margin, Height - Margin), DescriptionPaint, out _y_offset); + } + + private void DrawPictos(SKCanvas c) + { + if (Pictos.Count < 1) return; + + const float x = 450f; + const int size = 24; + var color = SKColor.Parse("#495B6E"); + var paint = new SKPaint + { + IsAntialias = true, + FilterQuality = SKFilterQuality.High, + TextSize = 27, + Color = color, + Typeface = Utils.Typefaces.Default + }; + + ImagePaint.ColorFilter = SKColorFilter.CreateBlendMode(color, SKBlendMode.SrcIn); + + foreach (var picto in Pictos) + { + c.DrawBitmap(picto.Item1, new SKRect(x, _y_offset + 10, x + size, _y_offset + 10 + size), ImagePaint); + c.DrawText(picto.Item2, x + size + 10, _y_offset + size + 6, paint); + _y_offset += size + 5; + } + } +} + +public enum ERewardRarity : byte +{ + [Description("0D4B15CE4FB6F2BC5E5F5FAA9E8B376C")] + None = 0, // Default + + [Description("0FCDEF47485E2C3D0D477988C481D8E3")] + Common = 1, + + [Description("18241CA7441AE16AAFB6EFAB499FF981")] + Rare = 2, + + [Description("D999D9CB4754D1078BF9A1B34A231005")] + Epic = 3, + + [Description("705AE967407D6EF8870E988A08C6900E")] + Legendary = 4 +} + +public enum EUnlockLocation : byte +{ + [Description("0D4B15CE4FB6F2BC5E5F5FAA9E8B376C")] + None = 0, // Default + + [Description("0AFBCE5F41D930D6E9B5138C8EBCFE87")] + Shop = 1, + + [Description("062F178B4EE74502C9AD9D878F3D7CEA")] + AccountLevel = 2, + + [Description("1AE7A5DF477B2B5F4B3CCC8DCD732884")] + CharacterMastery = 3, + + [Description("0B37731C49DC9AE1EAC566950C1A329D")] + Battlepass = 4, + + [Description("16F160084187479E5D471786190AE5B7")] + CharacterAffinity = 5, + + [Description("E5C1E35C406C585E83B5D18A817FA0B4")] + GuildBoss = 6, + + [Description("4A89F5DD432113750EF52D8B58977DCE")] + Tutorial = 7 +} + +public enum EPerkCategory : byte +{ + Offense = 0, // Default + Defense = 1, + Utility = 2, + CharacterSpecific = 3 +} + +public enum EItemType +{ + Unknown = -1, + Announcer, + Badge, + Banner, + BattlePassPoints, + Emote, + ExperiencePoints, + Gleamium, + Gold, + MasteryLevel, + Mission, + Perk, + PlayerLevel, + ProfileIcon, + Rested, + Ringout, + SignaturePerk, + Sticker, + Toast, + Variant +} diff --git a/FModel/Creator/Bases/MV/BasePerkGroup.cs b/FModel/Creator/Bases/MV/BasePerkGroup.cs new file mode 100644 index 00000000..e580c4cb --- /dev/null +++ b/FModel/Creator/Bases/MV/BasePerkGroup.cs @@ -0,0 +1,45 @@ +using System.Collections.Generic; +using CUE4Parse.UE4.Assets.Exports; +using CUE4Parse.UE4.Assets.Objects; +using CUE4Parse.UE4.Objects.UObject; +using SkiaSharp; + +namespace FModel.Creator.Bases.MV; + +public class BasePerkGroup : UCreator +{ + private readonly List _perks; + + public BasePerkGroup(UObject uObject, EIconStyle style) : base(uObject, style) + { + _perks = new List(); + } + + public override void ParseForInfo() + { + if (Object.TryGetValue(out UScriptSet perks, "Perks")) // PORCO DIO WB USE ARRAYS!!!!!! + { + foreach (var perk in perks.Properties) + { + if (perk.GenericValue is not FPackageIndex packageIndex || + !Utils.TryGetPackageIndexExport(packageIndex, out UObject export)) + continue; + + var icon = new BasePandaIcon(export, Style); + icon.ParseForInfo(); + _perks.Add(icon); + } + } + } + + public override SKBitmap[] Draw() + { + var ret = new SKBitmap[_perks.Count]; + for (var i = 0; i < ret.Length; i++) + { + ret[i] = _perks[i].Draw()[0]; + } + + return ret; + } +} diff --git a/FModel/Creator/Bases/MV/BaseQuest.cs b/FModel/Creator/Bases/MV/BaseQuest.cs new file mode 100644 index 00000000..71d24624 --- /dev/null +++ b/FModel/Creator/Bases/MV/BaseQuest.cs @@ -0,0 +1,54 @@ +using System.ComponentModel; +using CUE4Parse.UE4.Assets.Exports; +using CUE4Parse.UE4.Assets.Objects; +using CUE4Parse.UE4.Objects.UObject; +using FModel.Extensions; + +namespace FModel.Creator.Bases.MV; + +public class BaseQuest : BasePandaIcon +{ + public BaseQuest(UObject uObject, EIconStyle style) : base(uObject, style) + { + } + + public override void ParseForInfo() + { + if (Object.TryGetValue(out FStructFallback[] questCompletionRewards, "QuestCompletionRewards") && + questCompletionRewards.Length > 0 && questCompletionRewards[0] is { } actualReward) + { + var rewardType = actualReward.GetOrDefault("RewardType", EQuestRewardType.Inventory); + var count = actualReward.GetOrDefault("Count", 0); + Pictos.Add((Utils.GetBitmap("/Game/Panda_Main/UI/Assets/Icons/ui_icons_plus.ui_icons_plus"), count.ToString())); + + base.ParseForInfo(); + + if (actualReward.TryGetValue(out FPackageIndex assetReward, "AssetReward") && + Utils.TryGetPackageIndexExport(assetReward, out UObject export)) + { + var item = new BasePandaIcon(export, Style); + item.ParseForInfo(); + Preview = item.Preview; + } + else if (rewardType != EQuestRewardType.Inventory) + { + Preview = Utils.GetBitmap(rewardType.GetDescription()); + } + } + else + { + base.ParseForInfo(); + } + } +} + +public enum EQuestRewardType : byte +{ + Inventory = 0, // Default + + [Description("/Game/Panda_Main/UI/Assets/Icons/UI_CharacterTicket.UI_CharacterTicket")] + AccountXP = 1, + + [Description("/Game/Panda_Main/UI/Assets/Icons/UI_BattlepassToken.UI_BattlepassToken")] + BattlepassXP = 2 +} diff --git a/FModel/Creator/Bases/UCreator.cs b/FModel/Creator/Bases/UCreator.cs index d0577b37..7295c450 100644 --- a/FModel/Creator/Bases/UCreator.cs +++ b/FModel/Creator/Bases/UCreator.cs @@ -226,4 +226,4 @@ public abstract class UCreator break; } } -} \ No newline at end of file +} diff --git a/FModel/Creator/CreatorPackage.cs b/FModel/Creator/CreatorPackage.cs index 22a842e7..ac794ab2 100644 --- a/FModel/Creator/CreatorPackage.cs +++ b/FModel/Creator/CreatorPackage.cs @@ -4,6 +4,7 @@ using CUE4Parse.UE4.Assets.Exports; using FModel.Creator.Bases; using FModel.Creator.Bases.BB; using FModel.Creator.Bases.FN; +using FModel.Creator.Bases.MV; using FModel.Creator.Bases.SB; namespace FModel.Creator; @@ -89,6 +90,7 @@ public class CreatorPackage : IDisposable case "FortPlaysetPropItemDefinition": case "FortHomebaseNodeItemDefinition": case "FortNeverPersistItemDefinition": + case "FortPlayerAugmentItemDefinition": case "RadioContentSourceItemDefinition": case "FortPlaysetGrenadeItemDefinition": case "FortPersonalVehicleItemDefinition": @@ -148,7 +150,7 @@ public class CreatorPackage : IDisposable case "FortQuestItemDefinition_Athena": case "AthenaDailyQuestDefinition": case "FortUrgentQuestItemDefinition": - creator = new BaseQuest(_object, _style); + creator = new Bases.FN.BaseQuest(_object, _style); return true; case "FortCompendiumItemDefinition": case "FortChallengeBundleItemDefinition": @@ -173,6 +175,29 @@ public class CreatorPackage : IDisposable case "PlaylistUserOptionCollisionProfileEnum": creator = new BaseUserControl(_object, _style); return true; + // PandaGame + case "CharacterData": + creator = new BaseFighter(_object, _style); + return true; + case "PerkGroup": + creator = new BasePerkGroup(_object, _style); + return true; + case "StatTrackingBundleData": + case "HydraSyncedDataAsset": + case "AnnouncerPackData": + case "CharacterGiftData": + case "ProfileIconData": + case "RingOutVfxData": + case "BannerData": + case "EmoteData": + case "TauntData": + case "SkinData": + case "PerkData": + creator = new BasePandaIcon(_object, _style); + return true; + case "QuestData": + creator = new Bases.MV.BaseQuest(_object, _style); + return true; // Battle Breakers case "WExpGenericAccountItemDefinition": case "WExpGearAccountItemDefinition": @@ -248,4 +273,4 @@ public class CreatorPackage : IDisposable { _object = null; } -} \ No newline at end of file +} diff --git a/FModel/Creator/Typefaces.cs b/FModel/Creator/Typefaces.cs index 2b84b600..4e11a74a 100644 --- a/FModel/Creator/Typefaces.cs +++ b/FModel/Creator/Typefaces.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.IO; using System.Windows; using CUE4Parse.UE4.Versions; @@ -41,6 +41,14 @@ public class Typefaces private const string _BURBANK_SMALL_BLACK = "burbanksmall-black"; private const string _BURBANK_SMALL_BOLD = "burbanksmall-bold"; + // PandaGame + private const string _PANDAGAME_BASE_PATH = "/Game/Panda_Main/UI/Fonts/"; + private const string _NORMS_STD_CONDENSED_EXTRABOLD_ITALIC = "Norms/TT_Norms_Std_Condensed_ExtraBold_Italic"; + private const string _NORMS_PRO_EXTRABOLD_ITALIC = "Norms/TT_Norms_Pro_ExtraBold_Italic"; + private const string _NORMS_STD_CONDENSED_MEDIUM = "Norms/TT_Norms_Std_Condensed_Medium"; + private const string _XIANGHEHEI_SC_PRO_BLACK = "XiangHeHei_SC/MXiangHeHeiSCPro-Black"; + private const string _XIANGHEHEI_SC_PRO_HEAVY = "XiangHeHei_SC/MXiangHeHeiSCPro-Heavy"; + // Spellbreak private const string _SPELLBREAK_BASE_PATH = "/Game/UI/Fonts/"; private const string _MONTSERRAT_SEMIBOLD = "Montserrat-Semibold"; @@ -72,207 +80,167 @@ public class Typefaces public Typefaces(CUE4ParseViewModel viewModel) { - byte[] data; _viewModel = viewModel; var language = UserSettings.Default.AssetLanguage; + Default = SKTypeface.FromStream(Application.GetResourceStream(_BURBANK_BIG_CONDENSED_BOLD)?.Stream); switch (viewModel.Game) { case FGame.FortniteGame: { - var namePath = _FORTNITE_BASE_PATH + - language switch - { - ELanguage.Korean => _ASIA_ERINM, - ELanguage.Russian => _BURBANK_BIG_CONDENSED_BLACK, - ELanguage.Japanese => _NIS_JYAU, - ELanguage.Arabic => _NOTO_SANS_ARABIC_BLACK, - ELanguage.TraditionalChinese => _NOTO_SANS_TC_BLACK, - ELanguage.Chinese => _NOTO_SANS_SC_BLACK, - _ => string.Empty - }; - if (viewModel.Provider.TrySaveAsset(namePath + _EXT, out data)) - { - var m = new MemoryStream(data) { Position = 0 }; - DisplayName = SKTypeface.FromStream(m); - } - else DisplayName = Default; + DisplayName = OnTheFly(_FORTNITE_BASE_PATH + + language switch + { + ELanguage.Korean => _ASIA_ERINM, + ELanguage.Russian => _BURBANK_BIG_CONDENSED_BLACK, + ELanguage.Japanese => _NIS_JYAU, + ELanguage.Arabic => _NOTO_SANS_ARABIC_BLACK, + ELanguage.TraditionalChinese => _NOTO_SANS_TC_BLACK, + ELanguage.Chinese => _NOTO_SANS_SC_BLACK, + _ => string.Empty + } + _EXT); - var descriptionPath = _FORTNITE_BASE_PATH + - language switch - { - ELanguage.Korean => _NOTO_SANS_KR_REGULAR, - ELanguage.Japanese => _NOTO_SANS_JP_BOLD, - ELanguage.Arabic => _NOTO_SANS_ARABIC_REGULAR, - ELanguage.TraditionalChinese => _NOTO_SANS_TC_REGULAR, - ELanguage.Chinese => _NOTO_SANS_SC_REGULAR, - _ => _NOTO_SANS_REGULAR - }; - if (viewModel.Provider.TrySaveAsset(descriptionPath + _EXT, out data)) - { - var m = new MemoryStream(data) { Position = 0 }; - Description = SKTypeface.FromStream(m); - } - else Description = Default; + Description = OnTheFly(_FORTNITE_BASE_PATH + + language switch + { + ELanguage.Korean => _NOTO_SANS_KR_REGULAR, + ELanguage.Japanese => _NOTO_SANS_JP_BOLD, + ELanguage.Arabic => _NOTO_SANS_ARABIC_REGULAR, + ELanguage.TraditionalChinese => _NOTO_SANS_TC_REGULAR, + ELanguage.Chinese => _NOTO_SANS_SC_REGULAR, + _ => _NOTO_SANS_REGULAR + } + _EXT); - var bottomPath = _FORTNITE_BASE_PATH + - language switch - { - ELanguage.Korean => string.Empty, - ELanguage.Japanese => string.Empty, - ELanguage.Arabic => string.Empty, - ELanguage.TraditionalChinese => string.Empty, - ELanguage.Chinese => string.Empty, - _ => _BURBANK_SMALL_BOLD - }; - if (viewModel.Provider.TrySaveAsset(bottomPath + _EXT, out data)) - { - var m = new MemoryStream(data) { Position = 0 }; - Bottom = SKTypeface.FromStream(m); - } - // else keep it null + Bottom = OnTheFly(_FORTNITE_BASE_PATH + + language switch + { + ELanguage.Korean => string.Empty, + ELanguage.Japanese => string.Empty, + ELanguage.Arabic => string.Empty, + ELanguage.TraditionalChinese => string.Empty, + ELanguage.Chinese => string.Empty, + _ => _BURBANK_SMALL_BOLD + } + _EXT, true); - if (viewModel.Provider.TrySaveAsset(_FORTNITE_BASE_PATH + _BURBANK_BIG_CONDENSED_BLACK + _EXT, out data)) - { - var m = new MemoryStream(data) { Position = 0 }; - BundleNumber = SKTypeface.FromStream(m); - } - else BundleNumber = Default; + BundleNumber = OnTheFly(_FORTNITE_BASE_PATH + _BURBANK_BIG_CONDENSED_BLACK + _EXT); - var bundleNamePath = _FORTNITE_BASE_PATH + - language switch - { - ELanguage.Korean => _ASIA_ERINM, - ELanguage.Russian => _BURBANK_BIG_CONDENSED_BLACK, - ELanguage.Japanese => _NIS_JYAU, - ELanguage.Arabic => _NOTO_SANS_ARABIC_BLACK, - ELanguage.TraditionalChinese => _NOTO_SANS_TC_BLACK, - ELanguage.Chinese => _NOTO_SANS_SC_BLACK, - _ => string.Empty - }; - if (viewModel.Provider.TrySaveAsset(bundleNamePath + _EXT, out data)) - { - var m = new MemoryStream(data) { Position = 0 }; - Bundle = SKTypeface.FromStream(m); - } - else Bundle = BundleNumber; + Bundle = OnTheFly(_FORTNITE_BASE_PATH + + language switch + { + ELanguage.Korean => _ASIA_ERINM, + ELanguage.Russian => _BURBANK_BIG_CONDENSED_BLACK, + ELanguage.Japanese => _NIS_JYAU, + ELanguage.Arabic => _NOTO_SANS_ARABIC_BLACK, + ELanguage.TraditionalChinese => _NOTO_SANS_TC_BLACK, + ELanguage.Chinese => _NOTO_SANS_SC_BLACK, + _ => string.Empty + } + _EXT, true) ?? BundleNumber; - var tandemDisplayNamePath = _FORTNITE_BASE_PATH + - language switch - { - ELanguage.Korean => _ASIA_ERINM, - ELanguage.Russian => _BURBANK_BIG_CONDENSED_BLACK, - ELanguage.Japanese => _NIS_JYAU, - ELanguage.Arabic => _NOTO_SANS_ARABIC_BLACK, - ELanguage.TraditionalChinese => _NOTO_SANS_TC_BLACK, - ELanguage.Chinese => _NOTO_SANS_SC_BLACK, - _ => _BURBANK_BIG_REGULAR_BLACK - }; - if (viewModel.Provider.TrySaveAsset(tandemDisplayNamePath + _EXT, out data)) - { - var m = new MemoryStream(data) { Position = 0 }; - TandemDisplayName = SKTypeface.FromStream(m); - } - else TandemDisplayName = Default; + TandemDisplayName = OnTheFly(_FORTNITE_BASE_PATH + + language switch + { + ELanguage.Korean => _ASIA_ERINM, + ELanguage.Russian => _BURBANK_BIG_CONDENSED_BLACK, + ELanguage.Japanese => _NIS_JYAU, + ELanguage.Arabic => _NOTO_SANS_ARABIC_BLACK, + ELanguage.TraditionalChinese => _NOTO_SANS_TC_BLACK, + ELanguage.Chinese => _NOTO_SANS_SC_BLACK, + _ => _BURBANK_BIG_REGULAR_BLACK + } + _EXT); - var tandemGeneralDescPath = _FORTNITE_BASE_PATH + - language switch - { - ELanguage.Korean => _ASIA_ERINM, - ELanguage.Japanese => _NIS_JYAU, - ELanguage.Arabic => _NOTO_SANS_ARABIC_BLACK, - ELanguage.TraditionalChinese => _NOTO_SANS_TC_BLACK, - ELanguage.Chinese => _NOTO_SANS_SC_BLACK, - _ => _BURBANK_SMALL_BLACK - }; - if (viewModel.Provider.TrySaveAsset(tandemGeneralDescPath + _EXT, out data)) - { - var m = new MemoryStream(data) { Position = 0 }; - TandemGenDescription = SKTypeface.FromStream(m); - } - else TandemGenDescription = Default; - - var tandemAdditionalDescPath = _FORTNITE_BASE_PATH + - language switch - { - ELanguage.Korean => _ASIA_ERINM, - ELanguage.Japanese => _NIS_JYAU, - ELanguage.Arabic => _NOTO_SANS_ARABIC_BLACK, - ELanguage.TraditionalChinese => _NOTO_SANS_TC_BLACK, - ELanguage.Chinese => _NOTO_SANS_SC_BLACK, - _ => _BURBANK_SMALL_BOLD - }; - if (viewModel.Provider.TrySaveAsset(tandemAdditionalDescPath + _EXT, out data)) - { - var m = new MemoryStream(data) { Position = 0 }; - TandemAddDescription = SKTypeface.FromStream(m); - } - else TandemAddDescription = Default; + TandemGenDescription = OnTheFly(_FORTNITE_BASE_PATH + + language switch + { + ELanguage.Korean => _ASIA_ERINM, + ELanguage.Japanese => _NIS_JYAU, + ELanguage.Arabic => _NOTO_SANS_ARABIC_BLACK, + ELanguage.TraditionalChinese => _NOTO_SANS_TC_BLACK, + ELanguage.Chinese => _NOTO_SANS_SC_BLACK, + _ => _BURBANK_SMALL_BLACK + } + _EXT); + TandemAddDescription = OnTheFly(_FORTNITE_BASE_PATH + + language switch + { + ELanguage.Korean => _ASIA_ERINM, + ELanguage.Japanese => _NIS_JYAU, + ELanguage.Arabic => _NOTO_SANS_ARABIC_BLACK, + ELanguage.TraditionalChinese => _NOTO_SANS_TC_BLACK, + ELanguage.Chinese => _NOTO_SANS_SC_BLACK, + _ => _BURBANK_SMALL_BOLD + } + _EXT); break; } case FGame.WorldExplorers: { - var namePath = _BATTLE_BREAKERS_BASE_PATH + - language switch - { - ELanguage.Korean => _NOTO_SANS_KR_REGULAR, - ELanguage.Russian => _LATO_BLACK, - ELanguage.Japanese => _NOTO_SANS_JP_REGULAR, - ELanguage.Chinese => _NOTO_SANS_SC_REGULAR, - _ => _HEMIHEAD426 - }; - if (viewModel.Provider.TrySaveAsset(namePath + _EXT, out data)) - { - var m = new MemoryStream(data) { Position = 0 }; - DisplayName = SKTypeface.FromStream(m); - } - else DisplayName = Default; - - var descriptionPath = _BATTLE_BREAKERS_BASE_PATH + - language switch - { - ELanguage.Korean => _NOTO_SANS_KR_REGULAR, - ELanguage.Russian => _LATO_BLACK, - ELanguage.Japanese => _NOTO_SANS_JP_REGULAR, - ELanguage.Chinese => _NOTO_SANS_SC_REGULAR, - _ => _HEMIHEAD426 - }; - if (viewModel.Provider.TrySaveAsset(descriptionPath + _EXT, out data)) - { - var m = new MemoryStream(data) { Position = 0 }; - Description = SKTypeface.FromStream(m); - } - else Description = Default; + DisplayName = OnTheFly(_BATTLE_BREAKERS_BASE_PATH + + language switch + { + ELanguage.Korean => _NOTO_SANS_KR_REGULAR, + ELanguage.Russian => _LATO_BLACK, + ELanguage.Japanese => _NOTO_SANS_JP_REGULAR, + ELanguage.Chinese => _NOTO_SANS_SC_REGULAR, + _ => _HEMIHEAD426 + } + _EXT); + Description = OnTheFly(_BATTLE_BREAKERS_BASE_PATH + + language switch + { + ELanguage.Korean => _NOTO_SANS_KR_REGULAR, + ELanguage.Russian => _LATO_BLACK, + ELanguage.Japanese => _NOTO_SANS_JP_REGULAR, + ELanguage.Chinese => _NOTO_SANS_SC_REGULAR, + _ => _HEMIHEAD426 + } + _EXT); break; } case FGame.g3: { - if (viewModel.Provider.TrySaveAsset(_SPELLBREAK_BASE_PATH + _QUADRAT_BOLD + _EXT, out data)) + DisplayName = OnTheFly(_SPELLBREAK_BASE_PATH + _QUADRAT_BOLD + _EXT); + Description = OnTheFly(_SPELLBREAK_BASE_PATH + _MONTSERRAT_SEMIBOLD + _EXT); + break; + } + case FGame.PandaGame: + { + DisplayName = OnTheFly(_PANDAGAME_BASE_PATH + language switch { - var m = new MemoryStream(data) { Position = 0 }; - DisplayName = SKTypeface.FromStream(m); - } - else DisplayName = Default; + ELanguage.Chinese => _XIANGHEHEI_SC_PRO_HEAVY, + _ => _NORMS_PRO_EXTRABOLD_ITALIC + } + _EXT); - if (viewModel.Provider.TrySaveAsset(_SPELLBREAK_BASE_PATH + _MONTSERRAT_SEMIBOLD + _EXT, out data)) + Description = OnTheFly(_PANDAGAME_BASE_PATH + language switch { - var m = new MemoryStream(data) { Position = 0 }; - Description = SKTypeface.FromStream(m); - } - else Description = Default; + ELanguage.Chinese => _XIANGHEHEI_SC_PRO_BLACK, + _ => _NORMS_STD_CONDENSED_MEDIUM + } + _EXT); + TandemDisplayName = OnTheFly(_PANDAGAME_BASE_PATH + language switch + { + ELanguage.Chinese => _XIANGHEHEI_SC_PRO_BLACK, + _ => _NORMS_STD_CONDENSED_EXTRABOLD_ITALIC + } + _EXT); + + TandemGenDescription = OnTheFly(_PANDAGAME_BASE_PATH + language switch + { + ELanguage.Chinese => _XIANGHEHEI_SC_PRO_HEAVY, + _ => _NORMS_STD_CONDENSED_MEDIUM + } + _EXT); + break; + } + default: + { + DisplayName = Default; + Description = Default; break; } } } - public SKTypeface OnTheFly(string path) + public SKTypeface OnTheFly(string path, bool fallback = false) { - if (!_viewModel.Provider.TrySaveAsset(path, out var data)) return Default; + if (!_viewModel.Provider.TrySaveAsset(path, out var data)) return fallback ? null : Default; var m = new MemoryStream(data) { Position = 0 }; return SKTypeface.FromStream(m); } -} \ No newline at end of file +} diff --git a/FModel/Creator/Utils.cs b/FModel/Creator/Utils.cs index 96757c73..40970544 100644 --- a/FModel/Creator/Utils.cs +++ b/FModel/Creator/Utils.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; using System.Text; @@ -9,9 +10,9 @@ using CUE4Parse.UE4.Assets.Exports.Material; using CUE4Parse.UE4.Assets.Exports.Texture; using CUE4Parse.UE4.Objects.UObject; using CUE4Parse.UE4.Versions; -using CUE4Parse.Utils; using CUE4Parse_Conversion.Textures; using FModel.Framework; +using FModel.Extensions; using FModel.Services; using FModel.Settings; using FModel.ViewModels; @@ -93,6 +94,7 @@ public static class Utils switch (textureParameter.ParameterInfo.Name.Text) { case "MainTex": + case "Texture": case "TextureA": case "TextureB": case "OfferImage": @@ -117,12 +119,14 @@ public static class Utils { var ratioX = width / me.Width; var ratioY = height / me.Height; - var ratio = ratioX < ratioY ? ratioX : ratioY; + return ResizeWithRatio(me, ratioX < ratioY ? ratioX : ratioY); + } + public static SKBitmap ResizeWithRatio(this SKBitmap me, double ratio) + { return me.Resize(Convert.ToInt32(me.Width * ratio), Convert.ToInt32(me.Height * ratio)); } public static SKBitmap Resize(this SKBitmap me, int size) => me.Resize(size, size); - public static SKBitmap Resize(this SKBitmap me, int width, int height) { var bmp = new SKBitmap(new SKImageInfo(width, height), SKBitmapAllocFlags.ZeroPixels); @@ -180,6 +184,11 @@ public static class Utils { return _applicationView.CUE4Parse.Provider.GetLocalizedString(@namespace, key, defaultValue); } + public static string GetLocalizedResource(T @enum) where T : Enum + { + var resource = _applicationView.CUE4Parse.Provider.GetLocalizedString("", @enum.GetDescription(), @enum.ToString()); + return CultureInfo.CurrentCulture.TextInfo.ToTitleCase(resource.ToLower()); + } public static string GetFullPath(string partialPath) { @@ -195,6 +204,9 @@ public static class Utils return string.Empty; } + public static string FixPath(string weirdPath) => + _applicationView.CUE4Parse.Provider.FixPath(weirdPath, StringComparison.Ordinal); + public static void DrawCenteredMultilineText(SKCanvas c, string text, int maxCount, int size, int margin, SKTextAlign side, SKRect area, SKPaint paint) { var lineHeight = paint.TextSize * 1.2f; @@ -391,4 +403,4 @@ public static class Utils return ret; } -} \ No newline at end of file +} diff --git a/FModel/Enums.cs b/FModel/Enums.cs index 04fff3f1..1fa255bd 100644 --- a/FModel/Enums.cs +++ b/FModel/Enums.cs @@ -1,4 +1,5 @@ -using System.ComponentModel; +using System; +using System.ComponentModel; namespace FModel; @@ -18,10 +19,9 @@ public enum EErrorKind public enum SettingsOut { - Restart, ReloadLocres, - CheckForUpdates, - Nothing + ReloadMappings, + CheckForUpdates } public enum EStatusKind @@ -91,7 +91,13 @@ public enum FGame [Description("GTA: The Trilogy - Definitive Edition")] Gameface, [Description("Sea of Thieves")] - Athena + Athena, + [Description("Your Beloved ™ Panda")] + PandaGame, + [Description("Tower of Fantasy")] + Hotta, + [Description("eFootball 2023")] + eFootball } public enum ELoadingMode @@ -138,4 +144,22 @@ public enum EIconStyle Cataba, // [Description("Community")] // CommunityMade -} \ No newline at end of file +} + +public enum EEndpointType +{ + Aes, + Mapping +} + +[Flags] +public enum EBulkType +{ + None = 0, + Auto = 1 << 0, + Properties = 1 << 1, + Textures = 1 << 2, + Meshes = 1 << 3, + Skeletons = 1 << 4, + Animations = 1 << 5 +} diff --git a/FModel/Extensions/AvalonExtensions.cs b/FModel/Extensions/AvalonExtensions.cs index 8acccd5c..9c058b51 100644 --- a/FModel/Extensions/AvalonExtensions.cs +++ b/FModel/Extensions/AvalonExtensions.cs @@ -33,6 +33,7 @@ public static class AvalonExtensions case "csv": return _iniHighlighter; case "xml": + case "tps": return _xmlHighlighter; case "h": case "cpp": @@ -43,6 +44,7 @@ public static class AvalonExtensions return _ulangHighlighter; case "bat": case "txt": + case "pem": case "po": return null; default: diff --git a/FModel/FModel.csproj b/FModel/FModel.csproj index d44cd668..cb392a09 100644 --- a/FModel/FModel.csproj +++ b/FModel/FModel.csproj @@ -5,9 +5,9 @@ net6.0-windows true FModel.ico - 4.3.0 - 4.3.0.0 - 4.3.0.0 + 4.4.1.1 + 4.4.1.1 + 4.4.1.1 false true win-x64 @@ -93,6 +93,20 @@ + + + + + + + + + + + + + + @@ -102,6 +116,20 @@ + + + + + + + + + + + + + + @@ -111,24 +139,25 @@ - - + + + - + @@ -189,8 +218,15 @@ - + + + + + + + + diff --git a/FModel/FModel.sln b/FModel/FModel.sln index 7de93e7b..52492681 100644 --- a/FModel/FModel.sln +++ b/FModel/FModel.sln @@ -7,10 +7,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FModel", "FModel.csproj", " EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CUE4Parse", "..\CUE4Parse\CUE4Parse\CUE4Parse.csproj", "{C4620341-BBB7-4384-AC7D-5082D3E0386E}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CUE4Parse-Fortnite", "..\CUE4Parse\CUE4Parse-Fortnite\CUE4Parse-Fortnite.csproj", "{7765FB4C-B54D-427B-ABB6-1073687E56BD}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CUE4Parse-Conversion", "..\CUE4Parse\CUE4Parse-Conversion\CUE4Parse-Conversion.csproj", "{D0E1E8F7-F56D-469A-8E24-C2439B9FFD83}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EpicManifestParser", "..\EpicManifestParser\src\EpicManifestParser\EpicManifestParser.csproj", "{D4958A8B-815B-421D-A988-2A4E8E2B582D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -25,14 +25,14 @@ Global {C4620341-BBB7-4384-AC7D-5082D3E0386E}.Debug|Any CPU.Build.0 = Debug|Any CPU {C4620341-BBB7-4384-AC7D-5082D3E0386E}.Release|Any CPU.ActiveCfg = Release|Any CPU {C4620341-BBB7-4384-AC7D-5082D3E0386E}.Release|Any CPU.Build.0 = Release|Any CPU - {7765FB4C-B54D-427B-ABB6-1073687E56BD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7765FB4C-B54D-427B-ABB6-1073687E56BD}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7765FB4C-B54D-427B-ABB6-1073687E56BD}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7765FB4C-B54D-427B-ABB6-1073687E56BD}.Release|Any CPU.Build.0 = Release|Any CPU {D0E1E8F7-F56D-469A-8E24-C2439B9FFD83}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D0E1E8F7-F56D-469A-8E24-C2439B9FFD83}.Debug|Any CPU.Build.0 = Debug|Any CPU {D0E1E8F7-F56D-469A-8E24-C2439B9FFD83}.Release|Any CPU.ActiveCfg = Release|Any CPU {D0E1E8F7-F56D-469A-8E24-C2439B9FFD83}.Release|Any CPU.Build.0 = Release|Any CPU + {D4958A8B-815B-421D-A988-2A4E8E2B582D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D4958A8B-815B-421D-A988-2A4E8E2B582D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D4958A8B-815B-421D-A988-2A4E8E2B582D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D4958A8B-815B-421D-A988-2A4E8E2B582D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/FModel/Framework/FEndpoint.cs b/FModel/Framework/FEndpoint.cs new file mode 100644 index 00000000..cd80f109 --- /dev/null +++ b/FModel/Framework/FEndpoint.cs @@ -0,0 +1,92 @@ +using System.Linq; +using FModel.ViewModels.ApiEndpoints; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace FModel.Framework; + +public class FEndpoint : ViewModel +{ + private string _url; + public string Url + { + get => _url; + set => SetProperty(ref _url, value); + } + + private string _path; + public string Path + { + get => _path; + set => SetProperty(ref _path, value); + } + + private bool _overwrite; + public bool Overwrite + { + get => _overwrite; + set => SetProperty(ref _overwrite, value); + } + + private string _filePath; + public string FilePath + { + get => _filePath; + set => SetProperty(ref _filePath, value); + } + + private bool _isValid; + public bool IsValid + { + get => _isValid; + set + { + SetProperty(ref _isValid, value); + RaisePropertyChanged(nameof(Label)); + } + } + + [JsonIgnore] + public string Label => IsValid ? + "Your endpoint configuration is valid! Please, avoid any unnecessary modifications!" : + "Your endpoint configuration DOES NOT seem to be valid yet! Please, test it out!"; + + public FEndpoint() {} + public FEndpoint(string url, string path) + { + Url = url; + Path = path; + IsValid = !string.IsNullOrEmpty(url) && !string.IsNullOrEmpty(path); // be careful with this + } + + public void TryValidate(DynamicApiEndpoint endpoint, EEndpointType type, out JToken response) + { + response = null; + if (string.IsNullOrEmpty(Url) || string.IsNullOrEmpty(Path)) + { + IsValid = false; + } + else switch (type) + { + case EEndpointType.Aes: + { + var r = endpoint.GetAesKeys(default, Url, Path); + response = JToken.FromObject(r); + IsValid = r.IsValid; + break; + } + case EEndpointType.Mapping: + { + var r = endpoint.GetMappings(default, Url, Path); + response = JToken.FromObject(r); + IsValid = r.Any(x => x.IsValid); + break; + } + default: + { + IsValid = false; + break; + } + } + } +} diff --git a/FModel/Framework/FRestRequest.cs b/FModel/Framework/FRestRequest.cs new file mode 100644 index 00000000..549a00e3 --- /dev/null +++ b/FModel/Framework/FRestRequest.cs @@ -0,0 +1,19 @@ +using System; +using RestSharp; + +namespace FModel.Framework; + +public class FRestRequest : RestRequest +{ + private const int _timeout = 3 * 1000; + + public FRestRequest(string url, Method method = Method.Get) : base(url, method) + { + Timeout = _timeout; + } + + public FRestRequest(Uri uri, Method method = Method.Get) : base(uri, method) + { + Timeout = _timeout; + } +} diff --git a/FModel/Framework/FStatus.cs b/FModel/Framework/FStatus.cs new file mode 100644 index 00000000..28421deb --- /dev/null +++ b/FModel/Framework/FStatus.cs @@ -0,0 +1,45 @@ +namespace FModel.Framework; + +public class FStatus : ViewModel +{ + private bool _isReady; + public bool IsReady + { + get => _isReady; + private set => SetProperty(ref _isReady, value); + } + + private EStatusKind _kind; + public EStatusKind Kind + { + get => _kind; + private set + { + SetProperty(ref _kind, value); + IsReady = Kind != EStatusKind.Loading && Kind != EStatusKind.Stopping; + } + } + + private string _label; + public string Label + { + get => _label; + private set => SetProperty(ref _label, value); + } + + public FStatus() + { + SetStatus(EStatusKind.Loading); + } + + public void SetStatus(EStatusKind kind, string label = "") + { + Kind = kind; + UpdateStatusLabel(label); + } + + public void UpdateStatusLabel(string label) + { + Label = Kind == EStatusKind.Loading ? $"{Kind} {label}".Trim() : Kind.ToString(); + } +} diff --git a/FModel/Framework/ImGuiController.cs b/FModel/Framework/ImGuiController.cs new file mode 100644 index 00000000..982b7e7f --- /dev/null +++ b/FModel/Framework/ImGuiController.cs @@ -0,0 +1,637 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Numerics; +using System.Runtime.CompilerServices; +using ImGuiNET; +using OpenTK.Graphics.OpenGL4; +using OpenTK.Windowing.Desktop; +using OpenTK.Windowing.GraphicsLibraryFramework; +using ErrorCode = OpenTK.Graphics.OpenGL4.ErrorCode; + +namespace FModel.Framework; + +public class ImGuiController : IDisposable +{ + private bool _frameBegun; + + private int _vertexArray; + private int _vertexBuffer; + private int _vertexBufferSize; + private int _indexBuffer; + private int _indexBufferSize; + + //private Texture _fontTexture; + + private int _fontTexture; + + private int _shader; + private int _shaderFontTextureLocation; + private int _shaderProjectionMatrixLocation; + + private int _windowWidth; + private int _windowHeight; + // private string _iniPath; + + private ImFontPtr _normal; + private ImFontPtr _bold; + private ImFontPtr _semiBold; + + private readonly Vector2 _scaleFactor = Vector2.One; + + private static bool KHRDebugAvailable = false; + + public ImGuiController(int width, int height) + { + _windowWidth = width; + _windowHeight = height; + // _iniPath = Path.Combine(UserSettings.Default.OutputDirectory, ".data", "imgui.ini"); + + int major = GL.GetInteger(GetPName.MajorVersion); + int minor = GL.GetInteger(GetPName.MinorVersion); + + KHRDebugAvailable = (major == 4 && minor >= 3) || IsExtensionSupported("KHR_debug"); + + IntPtr context = ImGui.CreateContext(); + ImGui.SetCurrentContext(context); + // ImGui.LoadIniSettingsFromDisk(_iniPath); + + var io = ImGui.GetIO(); + _normal = io.Fonts.AddFontFromFileTTF("C:\\Windows\\Fonts\\segoeui.ttf", 16); + _bold = io.Fonts.AddFontFromFileTTF("C:\\Windows\\Fonts\\segoeuib.ttf", 16); + _semiBold = io.Fonts.AddFontFromFileTTF("C:\\Windows\\Fonts\\seguisb.ttf", 16); + + io.BackendFlags |= ImGuiBackendFlags.RendererHasVtxOffset; + io.ConfigFlags |= ImGuiConfigFlags.NavEnableKeyboard; + io.ConfigFlags |= ImGuiConfigFlags.DockingEnable; + io.Fonts.Flags |= ImFontAtlasFlags.NoBakedLines; + + CreateDeviceResources(); + SetKeyMappings(); + + SetPerFrameImGuiData(1f / 60f); + + ImGui.NewFrame(); + _frameBegun = true; + } + + public void Bold() => PushFont(_bold); + public void SemiBold() => PushFont(_semiBold); + + public void PopFont() + { + ImGui.PopFont(); + PushFont(_normal); + } + + private void PushFont(ImFontPtr ptr) => ImGui.PushFont(ptr); + + public void WindowResized(int width, int height) + { + _windowWidth = width; + _windowHeight = height; + } + + public void DestroyDeviceObjects() + { + Dispose(); + } + + public void CreateDeviceResources() + { + _vertexBufferSize = 10000; + _indexBufferSize = 2000; + + int prevVAO = GL.GetInteger(GetPName.VertexArrayBinding); + int prevArrayBuffer = GL.GetInteger(GetPName.ArrayBufferBinding); + + _vertexArray = GL.GenVertexArray(); + GL.BindVertexArray(_vertexArray); + LabelObject(ObjectLabelIdentifier.VertexArray, _vertexArray, "ImGui"); + + _vertexBuffer = GL.GenBuffer(); + GL.BindBuffer(BufferTarget.ArrayBuffer, _vertexBuffer); + LabelObject(ObjectLabelIdentifier.Buffer, _vertexBuffer, "VBO: ImGui"); + GL.BufferData(BufferTarget.ArrayBuffer, _vertexBufferSize, IntPtr.Zero, BufferUsageHint.DynamicDraw); + + _indexBuffer = GL.GenBuffer(); + GL.BindBuffer(BufferTarget.ElementArrayBuffer, _indexBuffer); + LabelObject(ObjectLabelIdentifier.Buffer, _indexBuffer, "EBO: ImGui"); + GL.BufferData(BufferTarget.ElementArrayBuffer, _indexBufferSize, IntPtr.Zero, BufferUsageHint.DynamicDraw); + + RecreateFontDeviceTexture(); + + string VertexSource = @"#version 460 core +uniform mat4 projection_matrix; +layout(location = 0) in vec2 in_position; +layout(location = 1) in vec2 in_texCoord; +layout(location = 2) in vec4 in_color; +out vec4 color; +out vec2 texCoord; +void main() +{ +gl_Position = projection_matrix * vec4(in_position, 0, 1); +color = in_color; +texCoord = in_texCoord; +}"; + string FragmentSource = @"#version 460 core +uniform sampler2D in_fontTexture; +in vec4 color; +in vec2 texCoord; +out vec4 outputColor; +void main() +{ +outputColor = color * texture(in_fontTexture, texCoord); +}"; + + _shader = CreateProgram("ImGui", VertexSource, FragmentSource); + _shaderProjectionMatrixLocation = GL.GetUniformLocation(_shader, "projection_matrix"); + _shaderFontTextureLocation = GL.GetUniformLocation(_shader, "in_fontTexture"); + + int stride = Unsafe.SizeOf(); + GL.VertexAttribPointer(0, 2, VertexAttribPointerType.Float, false, stride, 0); + GL.VertexAttribPointer(1, 2, VertexAttribPointerType.Float, false, stride, 8); + GL.VertexAttribPointer(2, 4, VertexAttribPointerType.UnsignedByte, true, stride, 16); + + GL.EnableVertexAttribArray(0); + GL.EnableVertexAttribArray(1); + GL.EnableVertexAttribArray(2); + + GL.BindVertexArray(prevVAO); + GL.BindBuffer(BufferTarget.ArrayBuffer, prevArrayBuffer); + + CheckGLError("End of ImGui setup"); + } + + /// + /// Recreates the device texture used to render text. + /// + public void RecreateFontDeviceTexture() + { + ImGuiIOPtr io = ImGui.GetIO(); + io.Fonts.GetTexDataAsRGBA32(out IntPtr pixels, out int width, out int height, out int bytesPerPixel); + + int mips = (int)Math.Floor(Math.Log(Math.Max(width, height), 2)); + + int prevActiveTexture = GL.GetInteger(GetPName.ActiveTexture); + GL.ActiveTexture(TextureUnit.Texture0); + int prevTexture2D = GL.GetInteger(GetPName.TextureBinding2D); + + _fontTexture = GL.GenTexture(); + GL.BindTexture(TextureTarget.Texture2D, _fontTexture); + GL.TexStorage2D(TextureTarget2d.Texture2D, mips, SizedInternalFormat.Rgba8, width, height); + LabelObject(ObjectLabelIdentifier.Texture, _fontTexture, "ImGui Text Atlas"); + + GL.TexSubImage2D(TextureTarget.Texture2D, 0, 0, 0, width, height, PixelFormat.Bgra, PixelType.UnsignedByte, pixels); + + GL.GenerateMipmap(GenerateMipmapTarget.Texture2D); + + GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, (int)TextureWrapMode.Repeat); + GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, (int)TextureWrapMode.Repeat); + + GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMaxLevel, mips - 1); + + GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear); + GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Linear); + + // Restore state + GL.BindTexture(TextureTarget.Texture2D, prevTexture2D); + GL.ActiveTexture((TextureUnit)prevActiveTexture); + + io.Fonts.SetTexID((IntPtr)_fontTexture); + + io.Fonts.ClearTexData(); + } + + /// + /// Renders the ImGui draw list data. + /// + public void Render() + { + if (_frameBegun) + { + _frameBegun = false; + ImGui.Render(); + RenderImDrawData(ImGui.GetDrawData()); + } + CheckGLError("End of frame"); + } + + /// + /// Updates ImGui input and IO configuration state. + /// + public void Update(GameWindow wnd, float deltaSeconds) + { + if (_frameBegun) + { + ImGui.Render(); + } + + SetPerFrameImGuiData(deltaSeconds); + UpdateImGuiInput(wnd); + + _frameBegun = true; + ImGui.NewFrame(); + } + + /// + /// Sets per-frame data based on the associated window. + /// This is called by Update(float). + /// + private void SetPerFrameImGuiData(float deltaSeconds) + { + ImGuiIOPtr io = ImGui.GetIO(); + // if (io.WantSaveIniSettings) ImGui.SaveIniSettingsToDisk(_iniPath); + io.DisplaySize = new Vector2( + _windowWidth / _scaleFactor.X, + _windowHeight / _scaleFactor.Y); + io.DisplayFramebufferScale = _scaleFactor; + io.DeltaTime = deltaSeconds; // DeltaTime is in seconds. + } + + readonly List PressedChars = new List(); + + private void UpdateImGuiInput(GameWindow wnd) + { + ImGuiIOPtr io = ImGui.GetIO(); + var mState = wnd.MouseState; + var kState = wnd.KeyboardState; + + io.AddMousePosEvent(mState.X, mState.Y); + io.AddMouseButtonEvent(0, mState[MouseButton.Left]); + io.AddMouseButtonEvent(1, mState[MouseButton.Right]); + io.AddMouseButtonEvent(2, mState[MouseButton.Middle]); + io.AddMouseButtonEvent(3, mState[MouseButton.Button1]); + io.AddMouseButtonEvent(4, mState[MouseButton.Button2]); + io.AddMouseWheelEvent(mState.ScrollDelta.X, mState.ScrollDelta.Y); + + foreach (Keys key in Enum.GetValues(typeof(Keys))) + { + if (key == Keys.Unknown || io.KeyMap[(int) key] == -1) continue; + io.AddKeyEvent((ImGuiKey) io.KeyMap[(int) key], kState.IsKeyDown(key)); + } + + foreach (var c in PressedChars) + { + io.AddInputCharacter(c); + } + PressedChars.Clear(); + + io.KeyShift = kState.IsKeyDown(Keys.LeftShift) || kState.IsKeyDown(Keys.RightShift); + io.KeyCtrl = kState.IsKeyDown(Keys.LeftControl) || kState.IsKeyDown(Keys.RightControl); + io.KeyAlt = kState.IsKeyDown(Keys.LeftAlt) || kState.IsKeyDown(Keys.RightAlt); + io.KeySuper = kState.IsKeyDown(Keys.LeftSuper) || kState.IsKeyDown(Keys.RightSuper); + } + + public void PressChar(char keyChar) + { + PressedChars.Add(keyChar); + } + + private static void SetKeyMappings() + { + ImGuiIOPtr io = ImGui.GetIO(); + io.KeyMap[(int)ImGuiKey.LeftShift] = (int)Keys.LeftShift; + io.KeyMap[(int)ImGuiKey.RightShift] = (int)Keys.RightShift; + io.KeyMap[(int)ImGuiKey.LeftCtrl] = (int)Keys.LeftControl; + io.KeyMap[(int)ImGuiKey.RightCtrl] = (int)Keys.RightControl; + io.KeyMap[(int)ImGuiKey.LeftAlt] = (int)Keys.LeftAlt; + io.KeyMap[(int)ImGuiKey.RightAlt] = (int)Keys.RightAlt; + io.KeyMap[(int)ImGuiKey.LeftSuper] = (int)Keys.LeftSuper; + io.KeyMap[(int)ImGuiKey.RightSuper] = (int)Keys.RightSuper; + io.KeyMap[(int)ImGuiKey.Menu] = (int)Keys.Menu; + io.KeyMap[(int)ImGuiKey.UpArrow] = (int)Keys.Up; + io.KeyMap[(int)ImGuiKey.DownArrow] = (int)Keys.Down; + io.KeyMap[(int)ImGuiKey.LeftArrow] = (int)Keys.Left; + io.KeyMap[(int)ImGuiKey.RightArrow] = (int)Keys.Right; + io.KeyMap[(int)ImGuiKey.Enter] = (int)Keys.Enter; + io.KeyMap[(int)ImGuiKey.Escape] = (int)Keys.Escape; + io.KeyMap[(int)ImGuiKey.Space] = (int)Keys.Space; + io.KeyMap[(int)ImGuiKey.Tab] = (int)Keys.Tab; + io.KeyMap[(int)ImGuiKey.Backspace] = (int)Keys.Backspace; + io.KeyMap[(int)ImGuiKey.Insert] = (int)Keys.Insert; + io.KeyMap[(int)ImGuiKey.Delete] = (int)Keys.Delete; + io.KeyMap[(int)ImGuiKey.PageUp] = (int)Keys.PageUp; + io.KeyMap[(int)ImGuiKey.PageDown] = (int)Keys.PageDown; + io.KeyMap[(int)ImGuiKey.Home] = (int)Keys.Home; + io.KeyMap[(int)ImGuiKey.End] = (int)Keys.End; + io.KeyMap[(int)ImGuiKey.CapsLock] = (int)Keys.CapsLock; + io.KeyMap[(int)ImGuiKey.ScrollLock] = (int)Keys.ScrollLock; + io.KeyMap[(int)ImGuiKey.PrintScreen] = (int)Keys.PrintScreen; + io.KeyMap[(int)ImGuiKey.Pause] = (int)Keys.Pause; + io.KeyMap[(int)ImGuiKey.NumLock] = (int)Keys.NumLock; + io.KeyMap[(int)ImGuiKey.KeypadDivide] = (int)Keys.KeyPadDivide; + io.KeyMap[(int)ImGuiKey.KeypadMultiply] = (int)Keys.KeyPadMultiply; + io.KeyMap[(int)ImGuiKey.KeypadSubtract] = (int)Keys.KeyPadSubtract; + io.KeyMap[(int)ImGuiKey.KeypadAdd] = (int)Keys.KeyPadAdd; + io.KeyMap[(int)ImGuiKey.KeypadDecimal] = (int)Keys.KeyPadDecimal; + io.KeyMap[(int)ImGuiKey.KeypadEnter] = (int)Keys.KeyPadEnter; + io.KeyMap[(int)ImGuiKey.GraveAccent] = (int)Keys.GraveAccent; + io.KeyMap[(int)ImGuiKey.Minus] = (int)Keys.Minus; + io.KeyMap[(int)ImGuiKey.Equal] = (int)Keys.Equal; + io.KeyMap[(int)ImGuiKey.LeftBracket] = (int)Keys.LeftBracket; + io.KeyMap[(int)ImGuiKey.RightBracket] = (int)Keys.RightBracket; + io.KeyMap[(int)ImGuiKey.Semicolon] = (int)Keys.Semicolon; + io.KeyMap[(int)ImGuiKey.Apostrophe] = (int)Keys.Apostrophe; + io.KeyMap[(int)ImGuiKey.Comma] = (int)Keys.Comma; + io.KeyMap[(int)ImGuiKey.Period] = (int)Keys.Period; + io.KeyMap[(int)ImGuiKey.Slash] = (int)Keys.Slash; + io.KeyMap[(int)ImGuiKey.Backslash] = (int)Keys.Backslash; + io.KeyMap[(int)ImGuiKey.F1] = (int)Keys.F1; + io.KeyMap[(int)ImGuiKey.F2] = (int)Keys.F2; + io.KeyMap[(int)ImGuiKey.F3] = (int)Keys.F3; + io.KeyMap[(int)ImGuiKey.F4] = (int)Keys.F4; + io.KeyMap[(int)ImGuiKey.F5] = (int)Keys.F5; + io.KeyMap[(int)ImGuiKey.F6] = (int)Keys.F6; + io.KeyMap[(int)ImGuiKey.F7] = (int)Keys.F7; + io.KeyMap[(int)ImGuiKey.F8] = (int)Keys.F8; + io.KeyMap[(int)ImGuiKey.F9] = (int)Keys.F9; + io.KeyMap[(int)ImGuiKey.F10] = (int)Keys.F10; + io.KeyMap[(int)ImGuiKey.F11] = (int)Keys.F11; + io.KeyMap[(int)ImGuiKey.F12] = (int)Keys.F12; + io.KeyMap[(int)ImGuiKey.Keypad0] = (int)Keys.KeyPad0; + io.KeyMap[(int)ImGuiKey.Keypad1] = (int)Keys.KeyPad1; + io.KeyMap[(int)ImGuiKey.Keypad2] = (int)Keys.KeyPad2; + io.KeyMap[(int)ImGuiKey.Keypad3] = (int)Keys.KeyPad3; + io.KeyMap[(int)ImGuiKey.Keypad4] = (int)Keys.KeyPad4; + io.KeyMap[(int)ImGuiKey.Keypad5] = (int)Keys.KeyPad5; + io.KeyMap[(int)ImGuiKey.Keypad6] = (int)Keys.KeyPad6; + io.KeyMap[(int)ImGuiKey.Keypad7] = (int)Keys.KeyPad7; + io.KeyMap[(int)ImGuiKey.Keypad8] = (int)Keys.KeyPad8; + io.KeyMap[(int)ImGuiKey.Keypad9] = (int)Keys.KeyPad9; + io.KeyMap[(int)ImGuiKey._0] = (int)Keys.D0; + io.KeyMap[(int)ImGuiKey._1] = (int)Keys.D1; + io.KeyMap[(int)ImGuiKey._2] = (int)Keys.D2; + io.KeyMap[(int)ImGuiKey._3] = (int)Keys.D3; + io.KeyMap[(int)ImGuiKey._4] = (int)Keys.D4; + io.KeyMap[(int)ImGuiKey._5] = (int)Keys.D5; + io.KeyMap[(int)ImGuiKey._6] = (int)Keys.D6; + io.KeyMap[(int)ImGuiKey._7] = (int)Keys.D7; + io.KeyMap[(int)ImGuiKey._8] = (int)Keys.D8; + io.KeyMap[(int)ImGuiKey._9] = (int)Keys.D9; + io.KeyMap[(int)ImGuiKey.A] = (int)Keys.A; + io.KeyMap[(int)ImGuiKey.B] = (int)Keys.B; + io.KeyMap[(int)ImGuiKey.C] = (int)Keys.C; + io.KeyMap[(int)ImGuiKey.D] = (int)Keys.D; + io.KeyMap[(int)ImGuiKey.E] = (int)Keys.E; + io.KeyMap[(int)ImGuiKey.F] = (int)Keys.F; + io.KeyMap[(int)ImGuiKey.G] = (int)Keys.G; + io.KeyMap[(int)ImGuiKey.H] = (int)Keys.H; + io.KeyMap[(int)ImGuiKey.I] = (int)Keys.I; + io.KeyMap[(int)ImGuiKey.J] = (int)Keys.J; + io.KeyMap[(int)ImGuiKey.K] = (int)Keys.K; + io.KeyMap[(int)ImGuiKey.L] = (int)Keys.L; + io.KeyMap[(int)ImGuiKey.M] = (int)Keys.M; + io.KeyMap[(int)ImGuiKey.N] = (int)Keys.N; + io.KeyMap[(int)ImGuiKey.O] = (int)Keys.O; + io.KeyMap[(int)ImGuiKey.P] = (int)Keys.P; + io.KeyMap[(int)ImGuiKey.Q] = (int)Keys.Q; + io.KeyMap[(int)ImGuiKey.R] = (int)Keys.R; + io.KeyMap[(int)ImGuiKey.S] = (int)Keys.S; + io.KeyMap[(int)ImGuiKey.T] = (int)Keys.T; + io.KeyMap[(int)ImGuiKey.U] = (int)Keys.U; + io.KeyMap[(int)ImGuiKey.V] = (int)Keys.V; + io.KeyMap[(int)ImGuiKey.W] = (int)Keys.W; + io.KeyMap[(int)ImGuiKey.X] = (int)Keys.X; + io.KeyMap[(int)ImGuiKey.Y] = (int)Keys.Y; + io.KeyMap[(int)ImGuiKey.Z] = (int)Keys.Z; + } + + private void RenderImDrawData(ImDrawDataPtr draw_data) + { + if (draw_data.CmdListsCount == 0) + { + return; + } + + // Get intial state. + int prevVAO = GL.GetInteger(GetPName.VertexArrayBinding); + int prevArrayBuffer = GL.GetInteger(GetPName.ArrayBufferBinding); + int prevProgram = GL.GetInteger(GetPName.CurrentProgram); + bool prevBlendEnabled = GL.GetBoolean(GetPName.Blend); + bool prevScissorTestEnabled = GL.GetBoolean(GetPName.ScissorTest); + int prevBlendEquationRgb = GL.GetInteger(GetPName.BlendEquationRgb); + int prevBlendEquationAlpha = GL.GetInteger(GetPName.BlendEquationAlpha); + int prevBlendFuncSrcRgb = GL.GetInteger(GetPName.BlendSrcRgb); + int prevBlendFuncSrcAlpha = GL.GetInteger(GetPName.BlendSrcAlpha); + int prevBlendFuncDstRgb = GL.GetInteger(GetPName.BlendDstRgb); + int prevBlendFuncDstAlpha = GL.GetInteger(GetPName.BlendDstAlpha); + bool prevCullFaceEnabled = GL.GetBoolean(GetPName.CullFace); + bool prevDepthTestEnabled = GL.GetBoolean(GetPName.DepthTest); + int prevActiveTexture = GL.GetInteger(GetPName.ActiveTexture); + GL.ActiveTexture(TextureUnit.Texture0); + int prevTexture2D = GL.GetInteger(GetPName.TextureBinding2D); + Span prevScissorBox = stackalloc int[4]; + unsafe + { + fixed (int* iptr = &prevScissorBox[0]) + { + GL.GetInteger(GetPName.ScissorBox, iptr); + } + } + + // Bind the element buffer (thru the VAO) so that we can resize it. + GL.BindVertexArray(_vertexArray); + // Bind the vertex buffer so that we can resize it. + GL.BindBuffer(BufferTarget.ArrayBuffer, _vertexBuffer); + for (int i = 0; i < draw_data.CmdListsCount; i++) + { + ImDrawListPtr cmd_list = draw_data.CmdListsRange[i]; + + int vertexSize = cmd_list.VtxBuffer.Size * Unsafe.SizeOf(); + if (vertexSize > _vertexBufferSize) + { + int newSize = (int)Math.Max(_vertexBufferSize * 1.5f, vertexSize); + + GL.BufferData(BufferTarget.ArrayBuffer, newSize, IntPtr.Zero, BufferUsageHint.DynamicDraw); + _vertexBufferSize = newSize; + } + + int indexSize = cmd_list.IdxBuffer.Size * sizeof(ushort); + if (indexSize > _indexBufferSize) + { + int newSize = (int)Math.Max(_indexBufferSize * 1.5f, indexSize); + GL.BufferData(BufferTarget.ElementArrayBuffer, newSize, IntPtr.Zero, BufferUsageHint.DynamicDraw); + _indexBufferSize = newSize; + } + } + + // Setup orthographic projection matrix into our constant buffer + ImGuiIOPtr io = ImGui.GetIO(); + var mvp = OpenTK.Mathematics.Matrix4.CreateOrthographicOffCenter( + 0.0f, + io.DisplaySize.X, + io.DisplaySize.Y, + 0.0f, + -1.0f, + 1.0f); + + GL.UseProgram(_shader); + GL.UniformMatrix4(_shaderProjectionMatrixLocation, false, ref mvp); + GL.Uniform1(_shaderFontTextureLocation, 0); + CheckGLError("Projection"); + + GL.BindVertexArray(_vertexArray); + CheckGLError("VAO"); + + draw_data.ScaleClipRects(io.DisplayFramebufferScale); + + GL.Enable(EnableCap.Blend); + GL.Enable(EnableCap.ScissorTest); + GL.BlendEquation(BlendEquationMode.FuncAdd); + GL.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha); + GL.Disable(EnableCap.CullFace); + GL.Disable(EnableCap.DepthTest); + + // Render command lists + for (int n = 0; n < draw_data.CmdListsCount; n++) + { + ImDrawListPtr cmd_list = draw_data.CmdListsRange[n]; + + GL.BufferSubData(BufferTarget.ArrayBuffer, IntPtr.Zero, cmd_list.VtxBuffer.Size * Unsafe.SizeOf(), cmd_list.VtxBuffer.Data); + CheckGLError($"Data Vert {n}"); + + GL.BufferSubData(BufferTarget.ElementArrayBuffer, IntPtr.Zero, cmd_list.IdxBuffer.Size * sizeof(ushort), cmd_list.IdxBuffer.Data); + CheckGLError($"Data Idx {n}"); + + for (int cmd_i = 0; cmd_i < cmd_list.CmdBuffer.Size; cmd_i++) + { + ImDrawCmdPtr pcmd = cmd_list.CmdBuffer[cmd_i]; + if (pcmd.UserCallback != IntPtr.Zero) + { + throw new NotImplementedException(); + } + else + { + GL.ActiveTexture(TextureUnit.Texture0); + GL.BindTexture(TextureTarget.Texture2D, (int)pcmd.TextureId); + CheckGLError("Texture"); + + // We do _windowHeight - (int)clip.W instead of (int)clip.Y because gl has flipped Y when it comes to these coordinates + var clip = pcmd.ClipRect; + GL.Scissor((int)clip.X, _windowHeight - (int)clip.W, (int)(clip.Z - clip.X), (int)(clip.W - clip.Y)); + CheckGLError("Scissor"); + + if ((io.BackendFlags & ImGuiBackendFlags.RendererHasVtxOffset) != 0) + { + GL.DrawElementsBaseVertex(PrimitiveType.Triangles, (int)pcmd.ElemCount, DrawElementsType.UnsignedShort, (IntPtr)(pcmd.IdxOffset * sizeof(ushort)), unchecked((int)pcmd.VtxOffset)); + } + else + { + GL.DrawElements(BeginMode.Triangles, (int)pcmd.ElemCount, DrawElementsType.UnsignedShort, (int)pcmd.IdxOffset * sizeof(ushort)); + } + CheckGLError("Draw"); + } + } + } + + GL.Disable(EnableCap.Blend); + GL.Disable(EnableCap.ScissorTest); + + // Reset state + GL.BindTexture(TextureTarget.Texture2D, prevTexture2D); + GL.ActiveTexture((TextureUnit)prevActiveTexture); + GL.UseProgram(prevProgram); + GL.BindVertexArray(prevVAO); + GL.Scissor(prevScissorBox[0], prevScissorBox[1], prevScissorBox[2], prevScissorBox[3]); + GL.BindBuffer(BufferTarget.ArrayBuffer, prevArrayBuffer); + GL.BlendEquationSeparate((BlendEquationMode)prevBlendEquationRgb, (BlendEquationMode)prevBlendEquationAlpha); + GL.BlendFuncSeparate( + (BlendingFactorSrc)prevBlendFuncSrcRgb, + (BlendingFactorDest)prevBlendFuncDstRgb, + (BlendingFactorSrc)prevBlendFuncSrcAlpha, + (BlendingFactorDest)prevBlendFuncDstAlpha); + if (prevBlendEnabled) GL.Enable(EnableCap.Blend); else GL.Disable(EnableCap.Blend); + if (prevDepthTestEnabled) GL.Enable(EnableCap.DepthTest); else GL.Disable(EnableCap.DepthTest); + if (prevCullFaceEnabled) GL.Enable(EnableCap.CullFace); else GL.Disable(EnableCap.CullFace); + if (prevScissorTestEnabled) GL.Enable(EnableCap.ScissorTest); else GL.Disable(EnableCap.ScissorTest); + } + + /// + /// Frees all graphics resources used by the renderer. + /// + public void Dispose() + { + GL.DeleteVertexArray(_vertexArray); + GL.DeleteBuffer(_vertexBuffer); + GL.DeleteBuffer(_indexBuffer); + + GL.DeleteTexture(_fontTexture); + GL.DeleteProgram(_shader); + } + + public static void LabelObject(ObjectLabelIdentifier objLabelIdent, int glObject, string name) + { + if (KHRDebugAvailable) + GL.ObjectLabel(objLabelIdent, glObject, name.Length, name); + } + + static bool IsExtensionSupported(string name) + { + int n = GL.GetInteger(GetPName.NumExtensions); + for (int i = 0; i < n; i++) + { + string extension = GL.GetString(StringNameIndexed.Extensions, i); + if (extension == name) return true; + } + + return false; + } + + public static int CreateProgram(string name, string vertexSource, string fragmentSoruce) + { + int program = GL.CreateProgram(); + LabelObject(ObjectLabelIdentifier.Program, program, $"Program: {name}"); + + int vertex = CompileShader(name, ShaderType.VertexShader, vertexSource); + int fragment = CompileShader(name, ShaderType.FragmentShader, fragmentSoruce); + + GL.AttachShader(program, vertex); + GL.AttachShader(program, fragment); + + GL.LinkProgram(program); + + GL.GetProgram(program, GetProgramParameterName.LinkStatus, out int success); + if (success == 0) + { + string info = GL.GetProgramInfoLog(program); + Debug.WriteLine($"GL.LinkProgram had info log [{name}]:\n{info}"); + } + + GL.DetachShader(program, vertex); + GL.DetachShader(program, fragment); + + GL.DeleteShader(vertex); + GL.DeleteShader(fragment); + + return program; + } + + private static int CompileShader(string name, ShaderType type, string source) + { + int shader = GL.CreateShader(type); + LabelObject(ObjectLabelIdentifier.Shader, shader, $"Shader: {name}"); + + GL.ShaderSource(shader, source); + GL.CompileShader(shader); + + GL.GetShader(shader, ShaderParameter.CompileStatus, out int success); + if (success == 0) + { + string info = GL.GetShaderInfoLog(shader); + Debug.WriteLine($"GL.CompileShader for shader '{name}' [{type}] had info log:\n{info}"); + } + + return shader; + } + + public static void CheckGLError(string title) + { + ErrorCode error; + int i = 1; + while ((error = GL.GetError()) != ErrorCode.NoError) + { + Debug.Print($"{title} ({i++}): {error}"); + } + } +} diff --git a/FModel/Helper.cs b/FModel/Helper.cs index 8e1586c8..a44a3d09 100644 --- a/FModel/Helper.cs +++ b/FModel/Helper.cs @@ -16,6 +16,22 @@ public static class Helper internal readonly ulong UlongValue; } + public static bool IAmThePanda(string key) + => key.Length == 11 && + key.StartsWith("mu", StringComparison.OrdinalIgnoreCase) && + key.EndsWith("sus", StringComparison.OrdinalIgnoreCase); + + public static string FixKey(string key) + { + if (string.IsNullOrEmpty(key)) + return string.Empty; + + if (key.StartsWith("0x")) + key = key[2..]; + + return "0x" + key.ToUpper().Trim(); + } + public static void OpenWindow(string windowName, Action action) where T : Window { if (!IsWindowOpen(windowName)) @@ -84,4 +100,14 @@ public static class Helper var d = (Math.Abs(d1) + Math.Abs(d2) + 10) * 1.0e-15; return -d < n && d > n; } -} \ No newline at end of file + + public static float DegreesToRadians(float degrees) + { + return MathF.PI / 180f * degrees; + } + + public static float RadiansToDegrees(float radians) + { + return radians* 180f / MathF.PI; + } +} diff --git a/FModel/MainWindow.xaml b/FModel/MainWindow.xaml index 065dbf01..9f32042d 100644 --- a/FModel/MainWindow.xaml +++ b/FModel/MainWindow.xaml @@ -46,7 +46,7 @@ - + @@ -55,7 +55,7 @@ - + @@ -64,7 +64,7 @@ - + @@ -75,7 +75,7 @@ - + @@ -84,7 +84,7 @@ - + @@ -94,71 +94,19 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + @@ -168,7 +116,7 @@ - + @@ -272,7 +220,7 @@ - @@ -281,7 +229,7 @@ diff --git a/FModel/Views/AudioPlayer.xaml b/FModel/Views/AudioPlayer.xaml index dd4a7221..b11ca288 100644 --- a/FModel/Views/AudioPlayer.xaml +++ b/FModel/Views/AudioPlayer.xaml @@ -7,7 +7,8 @@ xmlns:adonisUi="clr-namespace:AdonisUI;assembly=AdonisUI" xmlns:adonisControls="clr-namespace:AdonisUI.Controls;assembly=AdonisUI" xmlns:adonisExtensions="clr-namespace:AdonisUI.Extensions;assembly=AdonisUI" - WindowStartupLocation="CenterScreen" IconVisibility="Collapsed" Closing="OnClosing" PreviewKeyDown="OnPreviewKeyDown" + WindowStartupLocation="CenterScreen" IconVisibility="Collapsed" + Closing="OnClosing" PreviewKeyDown="OnPreviewKeyDown" Activated="OnActivatedDeactivated" Deactivated="OnActivatedDeactivated" Height="{Binding Source={x:Static SystemParameters.MaximizedPrimaryScreenHeight}, Converter={converters:RatioConverter}, ConverterParameter='0.50'}" Width="{Binding Source={x:Static SystemParameters.MaximizedPrimaryScreenWidth}, Converter={converters:RatioConverter}, ConverterParameter='0.55'}"> @@ -27,7 +28,7 @@ - + @@ -43,7 +44,7 @@ - + @@ -53,18 +54,18 @@ - + - + - + - + @@ -74,7 +75,7 @@ - + @@ -130,14 +131,14 @@ - + - + diff --git a/FModel/Views/AudioPlayer.xaml.cs b/FModel/Views/AudioPlayer.xaml.cs index 56487a1f..6367e35c 100644 --- a/FModel/Views/AudioPlayer.xaml.cs +++ b/FModel/Views/AudioPlayer.xaml.cs @@ -90,4 +90,9 @@ public partial class AudioPlayer var filters = textBox.Text.Trim().Split(' '); _applicationView.AudioPlayer.AudioFilesView.Filter = o => { return o is AudioFile audio && filters.All(x => audio.FileName.Contains(x, StringComparison.OrdinalIgnoreCase)); }; } -} \ No newline at end of file + + private void OnActivatedDeactivated(object sender, EventArgs e) + { + _applicationView.AudioPlayer.HideToggle(); + } +} diff --git a/FModel/Views/DirectorySelector.xaml b/FModel/Views/DirectorySelector.xaml index 147c6641..c92476b3 100644 --- a/FModel/Views/DirectorySelector.xaml +++ b/FModel/Views/DirectorySelector.xaml @@ -28,7 +28,7 @@ - + diff --git a/FModel/Views/MapViewer.xaml b/FModel/Views/MapViewer.xaml index 34b57e04..e4d4b0a3 100644 --- a/FModel/Views/MapViewer.xaml +++ b/FModel/Views/MapViewer.xaml @@ -24,41 +24,41 @@ + DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding Status.IsReady}" /> + DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding Status.IsReady}" /> - - - + + + + + - - - + + + + + + DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding Status.IsReady}" /> + DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding Status.IsReady}" /> + DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding Status.IsReady}" /> + DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding Status.IsReady}" /> + DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding Status.IsReady}" /> + DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding Status.IsReady}" /> - + DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding Status.IsReady}" /> + + @@ -108,7 +108,7 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/FModel/Views/ModelViewer.xaml.cs b/FModel/Views/ModelViewer.xaml.cs deleted file mode 100644 index 07d6a2bd..00000000 --- a/FModel/Views/ModelViewer.xaml.cs +++ /dev/null @@ -1,115 +0,0 @@ -using System.ComponentModel; -using System.Windows; -using System.Windows.Input; -using AdonisUI.Controls; -using CUE4Parse.UE4.Assets.Exports; -using CUE4Parse.UE4.Assets.Exports.Material; -using FModel.Services; -using FModel.ViewModels; -using HelixToolkit.Wpf.SharpDX; -using MessageBox = AdonisUI.Controls.MessageBox; -using MessageBoxImage = AdonisUI.Controls.MessageBoxImage; - -namespace FModel.Views; - -public partial class ModelViewer -{ - private bool _messageShown; - private ApplicationViewModel _applicationView => ApplicationService.ApplicationView; - - public ModelViewer() - { - DataContext = _applicationView; - InitializeComponent(); - } - - public void Load(UObject export) => _applicationView.ModelViewer.LoadExport(export); - public void Overwrite(UMaterialInstance materialInstance) - { - if (_applicationView.ModelViewer.TryOverwriteMaterial(materialInstance)) - { - _applicationView.CUE4Parse.ModelIsOverwritingMaterial = false; - } - else - { - MessageBox.Show(new MessageBoxModel - { - Text = "An attempt to load a material failed.", - Caption = "Error", - Icon = MessageBoxImage.Error, - Buttons = MessageBoxButtons.OkCancel(), - IsSoundEnabled = false - }); - } - } - - private void OnClosing(object sender, CancelEventArgs e) - { - _applicationView.ModelViewer.Clear(); - _applicationView.ModelViewer.AppendMode = false; - _applicationView.CUE4Parse.ModelIsOverwritingMaterial = false; - MyAntiCrashGroup.ItemsSource = null; // <3 - } - - private async void OnWindowKeyDown(object sender, KeyEventArgs e) - { - switch (e.Key) - { - case Key.W: - _applicationView.ModelViewer.WirefreameToggle(); - break; - case Key.H: - _applicationView.ModelViewer.RenderingToggle(); - break; - case Key.D: - _applicationView.ModelViewer.DiffuseOnlyToggle(); - break; - case Key.M: - _applicationView.ModelViewer.MaterialColorToggle(); - break; - case Key.Decimal: - _applicationView.ModelViewer.FocusOnSelectedMesh(); - break; - case Key.S when Keyboard.Modifiers.HasFlag(ModifierKeys.Control) && Keyboard.Modifiers.HasFlag(ModifierKeys.Shift): - _applicationView.ModelViewer.SaveAsScene(); - break; - case Key.S when Keyboard.Modifiers.HasFlag(ModifierKeys.Control): - await _applicationView.ModelViewer.SaveLoadedModels(); - break; - } - } - - private void OnMouse3DDown(object sender, MouseDown3DEventArgs e) - { - if (!Keyboard.Modifiers.HasFlag(ModifierKeys.Shift) || e.HitTestResult.ModelHit is not CustomMeshGeometryModel3D m) return; - _applicationView.ModelViewer.SelectedModel.SelectedGeometry = m; - MaterialsListName.ScrollIntoView(m); - } - - private void OnFocusClick(object sender, RoutedEventArgs e) - => _applicationView.ModelViewer.FocusOnSelectedMesh(); - - private void OnCopyClick(object sender, RoutedEventArgs e) - => _applicationView.ModelViewer.CopySelectedMaterialName(); - - private async void Save(object sender, RoutedEventArgs e) - => await _applicationView.ModelViewer.SaveLoadedModels(); - - private void OnOverwriteMaterialClick(object sender, RoutedEventArgs e) - { - _applicationView.CUE4Parse.ModelIsOverwritingMaterial = true; - if (!_messageShown) - { - MessageBox.Show(new MessageBoxModel - { - Text = "Simply extract a material once FModel will be brought to the foreground. This message will be shown once per Model Viewer's lifetime, close it to begin.", - Caption = "How To Overwrite Material?", - Icon = MessageBoxImage.Information, - IsSoundEnabled = false - }); - _messageShown = true; - } - - MainWindow.YesWeCats.Activate(); - } -} \ No newline at end of file diff --git a/FModel/Views/Resources/Controls/Aed/GamePathVisualLineText.cs b/FModel/Views/Resources/Controls/Aed/GamePathVisualLineText.cs index ccd348d3..dd323ca6 100644 --- a/FModel/Views/Resources/Controls/Aed/GamePathVisualLineText.cs +++ b/FModel/Views/Resources/Controls/Aed/GamePathVisualLineText.cs @@ -29,8 +29,13 @@ public class GamePathVisualLineText : VisualLineText if (context == null) throw new ArgumentNullException(nameof(context)); - TextRunProperties.SetForegroundBrush(Brushes.Plum); - return base.CreateTextRun(startVisualColumn, context); + var relativeOffset = startVisualColumn - VisualColumn; + var text = context.GetText(context.VisualLine.FirstDocumentLine.Offset + RelativeTextOffset + relativeOffset, DocumentLength - relativeOffset); + + if (text.Count != 2) // ": " + TextRunProperties.SetForegroundBrush(Brushes.Plum); + + return new TextCharacters(text.Text, text.Offset, text.Count, TextRunProperties); } private bool GamePathIsClickable() => !string.IsNullOrEmpty(_gamePath) && Keyboard.Modifiers == ModifierKeys.None; @@ -70,10 +75,10 @@ public class GamePathVisualLineText : VisualLineText } else { - await _threadWorkerView.Begin(_ => - _applicationView.CUE4Parse.ExtractAndScroll(fullPath, obj)); + await _threadWorkerView.Begin(cancellationToken => + _applicationView.CUE4Parse.ExtractAndScroll(cancellationToken, fullPath, obj)); } }; return a; } -} \ No newline at end of file +} diff --git a/FModel/Views/Resources/Controls/Aed/HexColorVisualLineText.cs b/FModel/Views/Resources/Controls/Aed/HexColorVisualLineText.cs index dbef316c..b275add4 100644 --- a/FModel/Views/Resources/Controls/Aed/HexColorVisualLineText.cs +++ b/FModel/Views/Resources/Controls/Aed/HexColorVisualLineText.cs @@ -19,12 +19,17 @@ public class HexColorVisualLineText : VisualLineText if (context == null) throw new ArgumentNullException(nameof(context)); - TextRunProperties.SetForegroundBrush(Brushes.PeachPuff); - return base.CreateTextRun(startVisualColumn, context); + var relativeOffset = startVisualColumn - VisualColumn; + var text = context.GetText(context.VisualLine.FirstDocumentLine.Offset + RelativeTextOffset + relativeOffset, DocumentLength - relativeOffset); + + if (text.Count != 2) // ": " + TextRunProperties.SetForegroundBrush(Brushes.PeachPuff); + + return new TextCharacters(text.Text, text.Offset, text.Count, TextRunProperties); } protected override VisualLineText CreateInstance(int length) { return new HexColorVisualLineText(_hexColor, ParentVisualLine, length); } -} \ No newline at end of file +} diff --git a/FModel/Views/Resources/Controls/Aup/SourcePropertyChangedEventArgs.cs b/FModel/Views/Resources/Controls/Aup/SourcePropertyChangedEventArgs.cs index 9fc1b620..c5e23c4b 100644 --- a/FModel/Views/Resources/Controls/Aup/SourcePropertyChangedEventArgs.cs +++ b/FModel/Views/Resources/Controls/Aup/SourcePropertyChangedEventArgs.cs @@ -6,6 +6,7 @@ public enum ESourceProperty { FftData, PlaybackState, + HideToggle, Length, Position, WaveformData, @@ -22,4 +23,4 @@ public class SourcePropertyChangedEventArgs : EventArgs Property = property; Value = value; } -} \ No newline at end of file +} diff --git a/FModel/Views/Resources/Controls/Aup/SpectrumAnalyzer.cs b/FModel/Views/Resources/Controls/Aup/SpectrumAnalyzer.cs index 5d7da0d0..1fd7a869 100644 --- a/FModel/Views/Resources/Controls/Aup/SpectrumAnalyzer.cs +++ b/FModel/Views/Resources/Controls/Aup/SpectrumAnalyzer.cs @@ -133,10 +133,12 @@ public sealed class SpectrumAnalyzer : UserControl case ESourceProperty.FftData: UpdateSpectrum(SpectrumResolution, _source.FftData); break; + case ESourceProperty.HideToggle when (bool) e.Value == false: case ESourceProperty.PlaybackState when (PlaybackState) e.Value == PlaybackState.Playing: case ESourceProperty.RecordingState when (RecordingState) e.Value == RecordingState.Recording: CreateBars(); break; + case ESourceProperty.HideToggle when (bool) e.Value: case ESourceProperty.RecordingState when (RecordingState) e.Value == RecordingState.Stopped: SilenceBars(); break; @@ -332,15 +334,15 @@ public sealed class SpectrumAnalyzer : UserControl { if (_spectrumGrid == null) return; + var borderBrush = FrequencyBarBorderBrush.Clone(); + var barBrush = FrequencyBarBrush.Clone(); + borderBrush.Freeze(); + barBrush.Freeze(); + _spectrumGrid.Children.Clear(); _bars = new Border[FrequencyBarCount]; for (var i = 0; i < _bars.Length; i++) { - var borderBrush = FrequencyBarBorderBrush.Clone(); - var barBrush = FrequencyBarBrush.Clone(); - borderBrush.Freeze(); - barBrush.Freeze(); - _bars[i] = new Border { CornerRadius = FrequencyBarCornerRadius, @@ -440,4 +442,4 @@ public sealed class SpectrumAnalyzer : UserControl } }, DispatcherPriority.ApplicationIdle); } -} \ No newline at end of file +} diff --git a/FModel/Views/Resources/Controls/Aup/Timeclock.cs b/FModel/Views/Resources/Controls/Aup/Timeclock.cs index f6582880..66d2483d 100644 --- a/FModel/Views/Resources/Controls/Aup/Timeclock.cs +++ b/FModel/Views/Resources/Controls/Aup/Timeclock.cs @@ -322,7 +322,15 @@ public sealed class Timeclock : UserControl } else { - Dispatcher.BeginInvoke((Action) delegate { _timeText.Text = TimeSpan.Zero.ToString(TimeFormat); }); + ZeroTime(); } } -} \ No newline at end of file + + private void ZeroTime() + { + Dispatcher.BeginInvoke((Action) delegate + { + _timeText.Text = TimeSpan.Zero.ToString(TimeFormat); + }); + } +} diff --git a/FModel/Views/Resources/Controls/AvalonEditor.xaml.cs b/FModel/Views/Resources/Controls/AvalonEditor.xaml.cs index 4833d57e..04e5ed5e 100644 --- a/FModel/Views/Resources/Controls/AvalonEditor.xaml.cs +++ b/FModel/Views/Resources/Controls/AvalonEditor.xaml.cs @@ -40,6 +40,8 @@ public partial class AvalonEditor YesWeEditor = MyAvalonEditor; YesWeSearch = WpfSuckMyDick; + MyAvalonEditor.TextArea.TextView.LinkTextBackgroundBrush = null; + MyAvalonEditor.TextArea.TextView.LinkTextForegroundBrush = Brushes.Cornsilk; MyAvalonEditor.TextArea.TextView.ElementGenerators.Add(new GamePathElementGenerator()); MyAvalonEditor.TextArea.TextView.ElementGenerators.Add(new HexColorElementGenerator()); @@ -250,4 +252,4 @@ public partial class AvalonEditor { SaveCaretLoc(MyAvalonEditor.CaretOffset); } -} \ No newline at end of file +} diff --git a/FModel/Views/Resources/Controls/DictionaryEditor.xaml b/FModel/Views/Resources/Controls/DictionaryEditor.xaml index 8e12a075..6aabf7c6 100644 --- a/FModel/Views/Resources/Controls/DictionaryEditor.xaml +++ b/FModel/Views/Resources/Controls/DictionaryEditor.xaml @@ -21,7 +21,7 @@ - + @@ -33,15 +33,18 @@ + - - + + + +