Merge pull request #349 from 4sval/dev

This commit is contained in:
Valentin 2022-12-29 10:51:06 +01:00 committed by GitHub
commit 8e2363d114
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
137 changed files with 8277 additions and 2456 deletions

1
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1 @@
custom: "https://fmodel.app/donate"

View File

@ -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

3
.gitmodules vendored
View File

@ -1,3 +1,6 @@
[submodule "CUE4Parse"]
path = CUE4Parse
url = https://github.com/FabianFG/CUE4Parse
[submodule "EpicManifestParser"]
path = EpicManifestParser
url = https://github.com/FModel/EpicManifestParser

@ -1 +1 @@
Subproject commit d1251ca4502cc374e65e8bdd6c920fffce20a6f6
Subproject commit 5655c30f180a50bd07dc0282daace6b6edc9ea72

1
EpicManifestParser Submodule

@ -0,0 +1 @@
Subproject commit 97174265894eddcb0d94ef570d36ddc559a8573a

View File

@ -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;
}
}
}

View File

@ -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";
}
}

View File

@ -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
}
}
}
}

View File

@ -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<string, SKBitmap> 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 <SeasonText>{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;
}
}
}
}

View File

@ -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);
}
}
}

View File

@ -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<string>) _fighterType;
private readonly List<SKBitmap> _recommendedPerks;
private readonly List<SKBitmap> _availableTaunts;
private readonly List<SKBitmap> _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<string>();
_recommendedPerks = new List<SKBitmap>();
_availableTaunts = new List<SKBitmap>();
_skins = new List<SKBitmap>();
}
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
}

View File

@ -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
}

View File

@ -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<BasePandaIcon> _perks;
public BasePerkGroup(UObject uObject, EIconStyle style) : base(uObject, style)
{
_perks = new List<BasePandaIcon>();
}
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;
}
}

View File

@ -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
}

View File

@ -226,4 +226,4 @@ public abstract class UCreator
break;
}
}
}
}

View File

@ -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;
}
}
}

View File

@ -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);
}
}
}

View File

@ -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>(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;
}
}
}

View File

@ -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
}
}
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
}

View File

@ -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:

View File

@ -5,9 +5,9 @@
<TargetFramework>net6.0-windows</TargetFramework>
<UseWPF>true</UseWPF>
<ApplicationIcon>FModel.ico</ApplicationIcon>
<Version>4.3.0</Version>
<AssemblyVersion>4.3.0.0</AssemblyVersion>
<FileVersion>4.3.0.0</FileVersion>
<Version>4.4.1.1</Version>
<AssemblyVersion>4.4.1.1</AssemblyVersion>
<FileVersion>4.4.1.1</FileVersion>
<IsPackable>false</IsPackable>
<IsPublishable>true</IsPublishable>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
@ -93,6 +93,20 @@
<None Remove="Resources\edit.png" />
<None Remove="Resources\go_to_directory.png" />
<None Remove="Resources\npcleftside.png" />
<None Remove="Resources\default.frag" />
<None Remove="Resources\default.vert" />
<None Remove="Resources\grid.frag" />
<None Remove="Resources\grid.vert" />
<None Remove="Resources\skybox.frag" />
<None Remove="Resources\skybox.vert" />
<None Remove="Resources\framebuffer.frag" />
<None Remove="Resources\framebuffer.vert" />
<None Remove="Resources\outline.frag" />
<None Remove="Resources\outline.vert" />
<None Remove="Resources\picking.frag" />
<None Remove="Resources\picking.vert" />
<None Remove="Resources\light.frag" />
<None Remove="Resources\light.vert" />
</ItemGroup>
<ItemGroup>
@ -102,6 +116,20 @@
<EmbeddedResource Include="Resources\Xml.xshd" />
<EmbeddedResource Include="Resources\Cpp.xshd" />
<EmbeddedResource Include="Resources\Changelog.xshd" />
<EmbeddedResource Include="Resources\default.frag" />
<EmbeddedResource Include="Resources\default.vert" />
<EmbeddedResource Include="Resources\grid.frag" />
<EmbeddedResource Include="Resources\grid.vert" />
<EmbeddedResource Include="Resources\skybox.frag" />
<EmbeddedResource Include="Resources\skybox.vert" />
<EmbeddedResource Include="Resources\framebuffer.frag" />
<EmbeddedResource Include="Resources\framebuffer.vert" />
<EmbeddedResource Include="Resources\outline.frag" />
<EmbeddedResource Include="Resources\outline.vert" />
<EmbeddedResource Include="Resources\picking.frag" />
<EmbeddedResource Include="Resources\picking.vert" />
<EmbeddedResource Include="Resources\light.frag" />
<EmbeddedResource Include="Resources\light.vert" />
</ItemGroup>
<ItemGroup>
@ -111,24 +139,25 @@
<PackageReference Include="AvalonEdit" Version="6.1.3.50" />
<PackageReference Include="CSCore" Version="1.2.1.2" />
<PackageReference Include="DiscordRichPresence" Version="1.0.175" />
<PackageReference Include="EpicManifestParser" Version="1.2.70-temp" />
<PackageReference Include="HelixToolkit.SharpDX.Core.Wpf" Version="2.21.0" />
<PackageReference Include="ImGui.NET" Version="1.88.0" />
<PackageReference Include="K4os.Compression.LZ4.Streams" Version="1.2.16" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="NVorbis" Version="0.10.4" />
<PackageReference Include="Oodle.NET" Version="1.0.1" />
<PackageReference Include="Ookii.Dialogs.Wpf" Version="5.0.1" />
<PackageReference Include="OpenTK" Version="4.7.5" />
<PackageReference Include="RestSharp" Version="108.0.1" />
<PackageReference Include="Serilog" Version="2.11.0" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageReference Include="SixLabors.ImageSharp" Version="2.1.3" />
<PackageReference Include="SkiaSharp.HarfBuzz" Version="2.88.0" />
<PackageReference Include="SkiaSharp.Svg" Version="1.60.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\CUE4Parse\CUE4Parse-Conversion\CUE4Parse-Conversion.csproj" />
<ProjectReference Include="..\CUE4Parse\CUE4Parse-Fortnite\CUE4Parse-Fortnite.csproj" />
<ProjectReference Include="..\CUE4Parse\CUE4Parse\CUE4Parse.csproj" />
<ProjectReference Include="..\EpicManifestParser\src\EpicManifestParser\EpicManifestParser.csproj" />
</ItemGroup>
<ItemGroup>
@ -189,8 +218,15 @@
<Resource Include="Resources\delete.png" />
<Resource Include="Resources\edit.png" />
<Resource Include="Resources\go_to_directory.png" />
<Resource Include="Resources\approaching_storm_cubemap.dds" />
<Resource Include="Resources\npcleftside.png" />
<Resource Include="Resources\nx.png" />
<Resource Include="Resources\ny.png" />
<Resource Include="Resources\nz.png" />
<Resource Include="Resources\px.png" />
<Resource Include="Resources\py.png" />
<Resource Include="Resources\pz.png" />
<Resource Include="Resources\pointlight.png" />
<Resource Include="Resources\spotlight.png" />
</ItemGroup>
<ItemGroup>

View File

@ -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

View File

@ -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;
}
}
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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<ImDrawVert>();
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");
}
/// <summary>
/// Recreates the device texture used to render text.
/// </summary>
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();
}
/// <summary>
/// Renders the ImGui draw list data.
/// </summary>
public void Render()
{
if (_frameBegun)
{
_frameBegun = false;
ImGui.Render();
RenderImDrawData(ImGui.GetDrawData());
}
CheckGLError("End of frame");
}
/// <summary>
/// Updates ImGui input and IO configuration state.
/// </summary>
public void Update(GameWindow wnd, float deltaSeconds)
{
if (_frameBegun)
{
ImGui.Render();
}
SetPerFrameImGuiData(deltaSeconds);
UpdateImGuiInput(wnd);
_frameBegun = true;
ImGui.NewFrame();
}
/// <summary>
/// Sets per-frame data based on the associated window.
/// This is called by Update(float).
/// </summary>
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<char> PressedChars = new List<char>();
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<int> 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<ImDrawVert>();
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<ImDrawVert>(), 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);
}
/// <summary>
/// Frees all graphics resources used by the renderer.
/// </summary>
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}");
}
}
}

View File

@ -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<T>(string windowName, Action action) where T : Window
{
if (!IsWindowOpen<T>(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;
}
}
public static float DegreesToRadians(float degrees)
{
return MathF.PI / 180f * degrees;
}
public static float RadiansToDegrees(float radians)
{
return radians* 180f / MathF.PI;
}
}

View File

@ -46,7 +46,7 @@
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="AES" Command="{Binding MenuCommand}" CommandParameter="Directory_AES" IsEnabled="{Binding IsReady}">
<MenuItem Header="AES" Command="{Binding MenuCommand}" CommandParameter="Directory_AES" IsEnabled="{Binding Status.IsReady}">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
@ -55,7 +55,7 @@
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Backup" Command="{Binding MenuCommand}" CommandParameter="Directory_Backup" IsEnabled="{Binding IsReady}">
<MenuItem Header="Backup" Command="{Binding MenuCommand}" CommandParameter="Directory_Backup" IsEnabled="{Binding Status.IsReady}">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
@ -64,7 +64,7 @@
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Archives Info" Command="{Binding MenuCommand}" CommandParameter="Directory_ArchivesInfo" IsEnabled="{Binding IsReady}">
<MenuItem Header="Archives Info" Command="{Binding MenuCommand}" CommandParameter="Directory_ArchivesInfo" IsEnabled="{Binding Status.IsReady}">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
@ -75,7 +75,7 @@
</MenuItem>
</MenuItem>
<MenuItem Header="Packages">
<MenuItem Header="Search" IsEnabled="{Binding IsReady}" InputGestureText="Ctrl+Shift+F" Click="OnSearchViewClick">
<MenuItem Header="Search" IsEnabled="{Binding Status.IsReady}" InputGestureText="Ctrl+Shift+F" Click="OnSearchViewClick">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
@ -84,7 +84,7 @@
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Directories" ItemsSource="{Binding CustomDirectories.Directories}" IsEnabled="{Binding IsReady}">
<MenuItem Header="Directories" ItemsSource="{Binding CustomDirectories.Directories}" IsEnabled="{Binding Status.IsReady}">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
@ -94,71 +94,19 @@
</MenuItem.Icon>
</MenuItem>
<Separator />
<MenuItem Header="Export Raw Data" Command="{Binding RightClickMenuCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="Assets_Export_Data" />
<Binding Path="SelectedItems" ElementName="AssetsListName" />
</MultiBinding>
</MenuItem.CommandParameter>
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource ExportIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Save Property" Command="{Binding RightClickMenuCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="Assets_Save_Properties" />
<Binding Path="SelectedItems" ElementName="AssetsListName" />
</MultiBinding>
</MenuItem.CommandParameter>
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource SaveIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Save Texture" Command="{Binding RightClickMenuCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="Assets_Save_Texture" />
<Binding Path="SelectedItems" ElementName="AssetsListName" />
</MultiBinding>
</MenuItem.CommandParameter>
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource TextureIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Auto">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.AccentForegroundBrush}}" Data="{StaticResource StatusBarIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
<MenuItem Header="Save Properties" IsCheckable="True" StaysOpenOnClick="True"
InputGestureText="{Binding AutoSaveProps, Source={x:Static local:Settings.UserSettings.Default}}"
IsChecked="{Binding IsAutoSaveProps, Source={x:Static local:Settings.UserSettings.Default}}" />
<MenuItem Header="Save Textures" IsCheckable="True" StaysOpenOnClick="True"
InputGestureText="{Binding AutoSaveTextures, Source={x:Static local:Settings.UserSettings.Default}}"
IsChecked="{Binding IsAutoSaveTextures, Source={x:Static local:Settings.UserSettings.Default}}" />
<MenuItem Header="Open Sounds" IsCheckable="True" StaysOpenOnClick="True"
InputGestureText="{Binding AutoOpenSounds, Source={x:Static local:Settings.UserSettings.Default}}"
IsChecked="{Binding IsAutoOpenSounds, Source={x:Static local:Settings.UserSettings.Default}}" />
</MenuItem>
<MenuItem Header="Auto Open Sounds" IsCheckable="True" StaysOpenOnClick="True"
IsChecked="{Binding IsAutoOpenSounds, Source={x:Static local:Settings.UserSettings.Default}}" />
</MenuItem>
<MenuItem Header="Views">
<MenuItem Header="3D Viewer" Command="{Binding MenuCommand}" CommandParameter="Views_3dViewer">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.AccentForegroundBrush}}" Data="{StaticResource MeshIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Audio Player" Command="{Binding MenuCommand}" CommandParameter="Views_AudioPlayer">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
@ -168,7 +116,7 @@
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Map Viewer" Command="{Binding MenuCommand}" CommandParameter="Views_MapViewer" IsEnabled="{Binding IsReady}">
<MenuItem Header="Map Viewer" Command="{Binding MenuCommand}" CommandParameter="Views_MapViewer" IsEnabled="{Binding Status.IsReady}">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
@ -272,7 +220,7 @@
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Text="Loading Mode" VerticalAlignment="Center" />
<ComboBox Grid.Row="0" Grid.Column="2" ItemsSource="{Binding LoadingModes.Modes}" IsEnabled="{Binding IsReady}"
<ComboBox Grid.Row="0" Grid.Column="2" ItemsSource="{Binding LoadingModes.Modes}" IsEnabled="{Binding Status.IsReady}"
SelectedItem="{Binding LoadingMode, Source={x:Static local:Settings.UserSettings.Default}, Mode=TwoWay}">
<ComboBox.ItemTemplate>
<DataTemplate>
@ -281,7 +229,7 @@
</ComboBox.ItemTemplate>
</ComboBox>
<Button Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="3" Content="Load"
Command="{Binding LoadingModes.LoadCommand}" IsEnabled="{Binding IsReady}"
Command="{Binding LoadingModes.LoadCommand}" IsEnabled="{Binding Status.IsReady}"
CommandParameter="{Binding SelectedItems, ElementName=DirectoryFilesListBox}" />
</Grid>
<Grid DockPanel.Dock="Top">
@ -391,7 +339,7 @@
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Export Folder's Packages Raw Data" Click="OnFolderExportClick">
<MenuItem Header="Export Folder's Packages Raw Data (.uasset)" Click="OnFolderExportClick">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
@ -400,7 +348,7 @@
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Save Folder's Packages Properties" Click="OnFolderSaveClick">
<MenuItem Header="Save Folder's Packages Properties (.json)" Click="OnFolderSaveClick">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
@ -409,6 +357,33 @@
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Save Folder's Packages Textures (.png)" Click="OnFolderTextureClick">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource TextureIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Save Folder's Packages Models (.psk)" Click="OnFolderModelClick">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource ModelIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Save Folder's Packages Animations (.psa)" Click="OnFolderAnimationClick">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource AnimationIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<Separator />
<MenuItem Header="Save Directory" Click="OnSaveDirectoryClick">
<MenuItem.Icon>
@ -450,7 +425,7 @@
<TextBlock Grid.Row="0" Grid.Column="1" Text="Packages Count" VerticalAlignment="Center" HorizontalAlignment="Right" />
<TextBlock Grid.Row="1" Grid.Column="0" Text="{Binding SelectedItem.FoldersView.Count, ElementName=AssetsFolderName, FallbackValue=0}" VerticalAlignment="Center" HorizontalAlignment="Left" />
<TextBlock Grid.Row="1" Grid.Column="1" Text="Folders Count" VerticalAlignment="Center" HorizontalAlignment="Right" />
<TextBlock Grid.Row="2" Grid.Column="0" Text="{Binding SelectedItem.Package, ElementName=AssetsFolderName, FallbackValue='None'}" VerticalAlignment="Center" HorizontalAlignment="Left" />
<TextBlock Grid.Row="2" Grid.Column="0" Text="{Binding SelectedItem.Archive, ElementName=AssetsFolderName, FallbackValue='None'}" VerticalAlignment="Center" HorizontalAlignment="Left" />
<TextBlock Grid.Row="2" Grid.Column="1" Text="Included In Archive" VerticalAlignment="Center" HorizontalAlignment="Right" />
<TextBlock Grid.Row="3" Grid.Column="0" Text="{Binding SelectedItem.MountPoint, ElementName=AssetsFolderName, FallbackValue='/', Converter={x:Static converters:TrimRightToLeftConverter.Instance}, ConverterParameter=275}" VerticalAlignment="Center" HorizontalAlignment="Left" />
<TextBlock Grid.Row="3" Grid.Column="1" Text="Archive Mount Point" VerticalAlignment="Center" HorizontalAlignment="Right" />
@ -519,7 +494,7 @@
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Export Raw Data" Command="{Binding DataContext.RightClickMenuCommand}">
<MenuItem Header="Export Raw Data (.uasset)" Command="{Binding DataContext.RightClickMenuCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="Assets_Export_Data" />
@ -534,7 +509,7 @@
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Save Properties" Command="{Binding DataContext.RightClickMenuCommand}">
<MenuItem Header="Save Properties (.json)" Command="{Binding DataContext.RightClickMenuCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="Assets_Save_Properties" />
@ -549,10 +524,10 @@
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Save Texture" Command="{Binding DataContext.RightClickMenuCommand}">
<MenuItem Header="Save Texture (.png)" Command="{Binding DataContext.RightClickMenuCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="Assets_Save_Texture" />
<Binding Source="Assets_Save_Textures" />
<Binding Path="SelectedItems" />
</MultiBinding>
</MenuItem.CommandParameter>
@ -564,6 +539,36 @@
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Save Model (.psk)" Command="{Binding DataContext.RightClickMenuCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="Assets_Save_Models" />
<Binding Path="SelectedItems" />
</MultiBinding>
</MenuItem.CommandParameter>
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource ModelIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Save Animation (.psa)" Command="{Binding DataContext.RightClickMenuCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="Assets_Save_Animations" />
<Binding Path="SelectedItems" />
</MultiBinding>
</MenuItem.CommandParameter>
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource AnimationIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<Separator />
<MenuItem Header="Copy">
<MenuItem.Icon>
@ -640,7 +645,7 @@
<TextBlock Grid.Row="2" Grid.Column="1" Text="Compression Method" VerticalAlignment="Center" HorizontalAlignment="Right" />
<TextBlock Grid.Row="3" Grid.Column="0" Text="{Binding SelectedItem.IsEncrypted, ElementName=AssetsListName, FallbackValue='False'}" VerticalAlignment="Center" HorizontalAlignment="Left" />
<TextBlock Grid.Row="3" Grid.Column="1" Text="Is Encrypted" VerticalAlignment="Center" HorizontalAlignment="Right" />
<TextBlock Grid.Row="4" Grid.Column="0" Text="{Binding SelectedItem.Package, ElementName=AssetsListName, FallbackValue='None'}" VerticalAlignment="Center" HorizontalAlignment="Left" />
<TextBlock Grid.Row="4" Grid.Column="0" Text="{Binding SelectedItem.Archive, ElementName=AssetsListName, FallbackValue='None'}" VerticalAlignment="Center" HorizontalAlignment="Left" />
<TextBlock Grid.Row="4" Grid.Column="1" Text="Included In Archive" VerticalAlignment="Center" HorizontalAlignment="Right" />
</Grid>
</StackPanel>
@ -703,27 +708,27 @@
<Style TargetType="{x:Type StatusBar}" BasedOn="{StaticResource {x:Type StatusBar}}">
<Style.Triggers>
<!--don't mind me, MultiDataTrigger just sucks-->
<DataTrigger Binding="{Binding Status}" Value="{x:Static local:EStatusKind.Ready}">
<DataTrigger Binding="{Binding Status.Kind}" Value="{x:Static local:EStatusKind.Ready}">
<Setter Property="Background" Value="{DynamicResource {x:Static adonisUi:Brushes.AccentBrush}}" />
<Setter Property="Foreground" Value="White" />
</DataTrigger>
<DataTrigger Binding="{Binding Status}" Value="{x:Static local:EStatusKind.Completed}">
<DataTrigger Binding="{Binding Status.Kind}" Value="{x:Static local:EStatusKind.Completed}">
<Setter Property="Background" Value="{DynamicResource {x:Static adonisUi:Brushes.AccentBrush}}" />
<Setter Property="Foreground" Value="White" />
</DataTrigger>
<DataTrigger Binding="{Binding Status}" Value="{x:Static local:EStatusKind.Loading}">
<DataTrigger Binding="{Binding Status.Kind}" Value="{x:Static local:EStatusKind.Loading}">
<Setter Property="Background" Value="{DynamicResource {x:Static adonisUi:Brushes.AlertBrush}}" />
<Setter Property="Foreground" Value="White" />
</DataTrigger>
<DataTrigger Binding="{Binding Status}" Value="{x:Static local:EStatusKind.Stopping}">
<DataTrigger Binding="{Binding Status.Kind}" Value="{x:Static local:EStatusKind.Stopping}">
<Setter Property="Background" Value="{DynamicResource {x:Static adonisUi:Brushes.AlertBrush}}" />
<Setter Property="Foreground" Value="White" />
</DataTrigger>
<DataTrigger Binding="{Binding Status}" Value="{x:Static local:EStatusKind.Stopped}">
<DataTrigger Binding="{Binding Status.Kind}" Value="{x:Static local:EStatusKind.Stopped}">
<Setter Property="Background" Value="{DynamicResource {x:Static adonisUi:Brushes.ErrorBrush}}" />
<Setter Property="Foreground" Value="White" />
</DataTrigger>
<DataTrigger Binding="{Binding Status}" Value="{x:Static local:EStatusKind.Failed}">
<DataTrigger Binding="{Binding Status.Kind}" Value="{x:Static local:EStatusKind.Failed}">
<Setter Property="Background" Value="{DynamicResource {x:Static adonisUi:Brushes.ErrorBrush}}" />
<Setter Property="Foreground" Value="White" />
</DataTrigger>
@ -766,17 +771,17 @@
<TextBlock>
<TextBlock.Style>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="Text" Value="{Binding Status}" />
<Setter Property="Text" Value="{Binding Status.Label}" />
<Style.Triggers>
<DataTrigger Binding="{Binding Status}" Value="{x:Static local:EStatusKind.Loading}">
<Setter Property="Text" Value="{Binding Status, StringFormat='{}{0} …'}" />
<DataTrigger Binding="{Binding Status.Kind}" Value="{x:Static local:EStatusKind.Loading}">
<Setter Property="Text" Value="{Binding Status.Label, StringFormat='{}{0} …'}" />
</DataTrigger>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding CanBeCanceled, Source={x:Static local:Services.ApplicationService.ThreadWorkerView}}" Value="True" />
<Condition Binding="{Binding Status}" Value="{x:Static local:EStatusKind.Loading}" />
<Condition Binding="{Binding Status.Kind}" Value="{x:Static local:EStatusKind.Loading}" />
</MultiDataTrigger.Conditions>
<Setter Property="Text" Value="{Binding Status, StringFormat='{}{0} … ESC to Cancel'}" />
<Setter Property="Text" Value="{Binding Status.Label, StringFormat='{}{0} … ESC to Cancel'}" />
</MultiDataTrigger>
</Style.Triggers>
</Style>
@ -794,32 +799,6 @@
</Viewbox>
</StatusBarItem>
<StatusBarItem Width="30" HorizontalContentAlignment="Stretch" ToolTip="Auto Save Properties Enabled">
<StatusBarItem.Style>
<Style TargetType="StatusBarItem">
<Style.Triggers>
<DataTrigger Binding="{Binding IsAutoSaveProps, Source={x:Static local:Settings.UserSettings.Default}}" Value="False">
<Setter Property="Visibility" Value="Hidden" />
</DataTrigger>
</Style.Triggers>
</Style>
</StatusBarItem.Style>
<TextBlock HorizontalAlignment="Center" FontWeight="SemiBold" Text="PRP" />
</StatusBarItem>
<StatusBarItem Width="30" HorizontalContentAlignment="Stretch" ToolTip="Auto Save Textures Enabled">
<StatusBarItem.Style>
<Style TargetType="StatusBarItem">
<Style.Triggers>
<DataTrigger Binding="{Binding IsAutoSaveTextures, Source={x:Static local:Settings.UserSettings.Default}}" Value="False">
<Setter Property="Visibility" Value="Hidden" />
</DataTrigger>
</Style.Triggers>
</Style>
</StatusBarItem.Style>
<TextBlock HorizontalAlignment="Center" FontWeight="SemiBold" Text="TEX" />
</StatusBarItem>
<StatusBarItem Width="30" HorizontalContentAlignment="Stretch" ToolTip="Auto Open Sounds Enabled">
<StatusBarItem.Style>
<Style TargetType="StatusBarItem">

View File

@ -27,9 +27,6 @@ public partial class MainWindow
public MainWindow()
{
CommandBindings.Add(new CommandBinding(new RoutedCommand("AutoSaveProps", typeof(MainWindow), new InputGestureCollection { new KeyGesture(UserSettings.Default.AutoSaveProps.Key, UserSettings.Default.AutoSaveProps.Modifiers) }), OnAutoTriggerExecuted));
CommandBindings.Add(new CommandBinding(new RoutedCommand("AutoSaveTextures", typeof(MainWindow), new InputGestureCollection { new KeyGesture(UserSettings.Default.AutoSaveTextures.Key, UserSettings.Default.AutoSaveTextures.Modifiers) }), OnAutoTriggerExecuted));
CommandBindings.Add(new CommandBinding(new RoutedCommand("AutoOpenSounds", typeof(MainWindow), new InputGestureCollection { new KeyGesture(UserSettings.Default.AutoOpenSounds.Key, UserSettings.Default.AutoOpenSounds.Modifiers) }), OnAutoTriggerExecuted));
CommandBindings.Add(new CommandBinding(new RoutedCommand("ReloadMappings", typeof(MainWindow), new InputGestureCollection { new KeyGesture(Key.F12) }), OnMappingsReload));
CommandBindings.Add(new CommandBinding(ApplicationCommands.Find, (_, _) => OnOpenAvalonFinder()));
@ -66,13 +63,28 @@ public partial class MainWindow
await _applicationView.CUE4Parse.Initialize();
await _applicationView.AesManager.InitAes();
await _applicationView.AesManager.UpdateProvider(true);
#if !DEBUG
await _applicationView.CUE4Parse.InitInformation();
await _applicationView.CUE4Parse.InitBenMappings();
#endif
await _applicationView.CUE4Parse.InitMappings();
await _applicationView.InitImGuiSettings();
await _applicationView.InitVgmStream();
await _applicationView.InitOodle();
if (UserSettings.Default.DiscordRpc == EDiscordRpc.Always)
_discordHandler.Initialize(_applicationView.CUE4Parse.Game);
#if DEBUG
// await _threadWorkerView.Begin(cancellationToken =>
// _applicationView.CUE4Parse.Extract(cancellationToken,
// "FortniteGame/Content/Characters/Player/Female/Medium/Bodies/F_MED_RoseDust/Meshes/F_MED_RoseDust.uasset"));
// await _threadWorkerView.Begin(cancellationToken =>
// _applicationView.CUE4Parse.Extract(cancellationToken,
// "fortnitegame/Content/Accessories/FORT_Backpacks/Backpack_M_MED_Despair/Meshes/M_MED_Despair_Pack.uasset"));
// await _threadWorkerView.Begin(cancellationToken =>
// _applicationView.CUE4Parse.Extract(cancellationToken,
// "FortniteGame/Content/Animation/Game/MainPlayer/Emotes/Acrobatic_Superhero/Emote_AcrobaticSuperhero_CMM.uasset"));
#endif
}
private void OnGridSplitterDoubleClick(object sender, MouseButtonEventArgs e)
@ -87,10 +99,10 @@ public partial class MainWindow
if (_threadWorkerView.CanBeCanceled && e.Key == Key.Escape)
{
_applicationView.Status = EStatusKind.Stopping;
_applicationView.Status.SetStatus(EStatusKind.Stopping);
_threadWorkerView.Cancel();
}
else if (_applicationView.IsReady && e.Key == Key.F && Keyboard.Modifiers.HasFlag(ModifierKeys.Control) && Keyboard.Modifiers.HasFlag(ModifierKeys.Shift))
else if (_applicationView.Status.IsReady && e.Key == Key.F && Keyboard.Modifiers.HasFlag(ModifierKeys.Control) && Keyboard.Modifiers.HasFlag(ModifierKeys.Shift))
OnSearchViewClick(null, null);
else if (e.Key == Key.Left && _applicationView.CUE4Parse.TabControl.SelectedTab.HasImage)
_applicationView.CUE4Parse.TabControl.SelectedTab.GoPreviousImage();
@ -125,23 +137,7 @@ public partial class MainWindow
private async void OnMappingsReload(object sender, ExecutedRoutedEventArgs e)
{
await _applicationView.CUE4Parse.InitBenMappings();
}
private void OnAutoTriggerExecuted(object sender, ExecutedRoutedEventArgs e)
{
switch ((e.Command as RoutedCommand)?.Name)
{
case "AutoSaveProps":
UserSettings.Default.IsAutoSaveProps = !UserSettings.Default.IsAutoSaveProps;
break;
case "AutoSaveTextures":
UserSettings.Default.IsAutoSaveTextures = !UserSettings.Default.IsAutoSaveTextures;
break;
case "AutoOpenSounds":
UserSettings.Default.IsAutoOpenSounds = !UserSettings.Default.IsAutoOpenSounds;
break;
}
await _applicationView.CUE4Parse.InitMappings();
}
private void OnOpenAvalonFinder()
@ -190,6 +186,30 @@ public partial class MainWindow
}
}
private async void OnFolderTextureClick(object sender, RoutedEventArgs e)
{
if (AssetsFolderName.SelectedItem is TreeItem folder)
{
await _threadWorkerView.Begin(cancellationToken => { _applicationView.CUE4Parse.TextureFolder(cancellationToken, folder); });
}
}
private async void OnFolderModelClick(object sender, RoutedEventArgs e)
{
if (AssetsFolderName.SelectedItem is TreeItem folder)
{
await _threadWorkerView.Begin(cancellationToken => { _applicationView.CUE4Parse.ModelFolder(cancellationToken, folder); });
}
}
private async void OnFolderAnimationClick(object sender, RoutedEventArgs e)
{
if (AssetsFolderName.SelectedItem is TreeItem folder)
{
await _threadWorkerView.Begin(cancellationToken => { _applicationView.CUE4Parse.AnimationFolder(cancellationToken, folder); });
}
}
private void OnSaveDirectoryClick(object sender, RoutedEventArgs e)
{
if (AssetsFolderName.SelectedItem is not TreeItem folder) return;
@ -222,14 +242,14 @@ public partial class MainWindow
private void OnMouseDoubleClick(object sender, MouseButtonEventArgs e)
{
if (!_applicationView.IsReady || sender is not ListBox listBox) return;
if (!_applicationView.Status.IsReady || sender is not ListBox listBox) return;
UserSettings.Default.LoadingMode = ELoadingMode.Multiple;
_applicationView.LoadingModes.LoadCommand.Execute(listBox.SelectedItems);
}
private async void OnPreviewKeyDown(object sender, KeyEventArgs e)
{
if (!_applicationView.IsReady || sender is not ListBox listBox) return;
if (!_applicationView.Status.IsReady || sender is not ListBox listBox) return;
switch (e.Key)
{

View File

@ -0,0 +1,290 @@
#version 460 core
#define PI 3.1415926535897932384626433832795
#define MAX_UV_COUNT 8
#define MAX_LIGHT_COUNT 100
in vec3 fPos;
in vec3 fNormal;
in vec3 fTangent;
in vec2 fTexCoords;
in float fTexLayer;
in vec4 fColor;
struct Texture
{
sampler2D Sampler;
vec4 Color;
};
struct Boost
{
vec3 Color;
float Exponent;
};
struct AoParams
{
sampler2D Sampler;
float AmbientOcclusion;
Boost ColorBoost;
bool HasColorBoost;
};
struct Parameters
{
Texture Diffuse[MAX_UV_COUNT];
Texture Normals[MAX_UV_COUNT];
Texture SpecularMasks[MAX_UV_COUNT];
Texture Emissive[MAX_UV_COUNT];
AoParams Ao;
bool HasAo;
vec4 EmissiveRegion;
float Specular;
float Roughness;
float EmissiveMult;
float UVScale;
};
struct BaseLight
{
vec4 Color;
vec3 Position;
float Intensity;
};
//struct PointLight
//{
// BaseLight Light;
//
// float Linear;
// float Quadratic;
//};
//
//struct SpotLight
//{
// BaseLight Light;
//
// float InnerConeAngle;
// float OuterConeAngle;
// float Attenuation;
//};
struct Light {
BaseLight Base;
float InnerConeAngle;
float OuterConeAngle;
float Attenuation;
float Linear;
float Quadratic;
int Type; // 0 Point, 1 Spot
};
uniform Parameters uParameters;
uniform Light uLights[MAX_LIGHT_COUNT];
uniform int uNumLights;
uniform int uUvCount;
uniform bool uHasVertexColors;
uniform bool bVertexColors[6];
uniform vec3 uViewPos;
out vec4 FragColor;
int LayerToIndex()
{
return clamp(int(fTexLayer), 0, uUvCount - 1);
}
vec2 ScaledTexCoords()
{
return fTexCoords * uParameters.UVScale;
}
vec4 SamplerToVector(sampler2D s, vec2 coords)
{
return texture(s, coords);
}
vec4 SamplerToVector(sampler2D s)
{
return SamplerToVector(s, ScaledTexCoords());
}
vec3 ComputeNormals(int layer)
{
vec3 normal = SamplerToVector(uParameters.Normals[layer].Sampler).rgb * 2.0 - 1.0;
vec3 t = normalize(fTangent);
vec3 n = normalize(fNormal);
vec3 b = -normalize(cross(n, t));
mat3 tbn = mat3(t, b, n);
return normalize(tbn * normal);
}
vec3 schlickFresnel(vec3 fLambert, float metallic, float hDotv)
{
vec3 f0 = vec3(0.04);
f0 = mix(f0, fLambert, metallic);
return f0 + (1.0 - f0) * pow(clamp(1.0 - hDotv, 0.0, 1.0), 5);
}
float ggxDistribution(float roughness, float nDoth)
{
float alpha2 = roughness * roughness * roughness * roughness;
float d = nDoth * nDoth * (alpha2- 1.0) + 1.0;
return alpha2 / (PI * d * d);
}
float geomSmith(float roughness, float dp)
{
float k = (roughness + 1.0) * (roughness + 1.0) / 8.0;
float denom = dp * (1.0 - k) + k;
return dp / denom;
}
vec3 CalcLight(int layer, vec3 normals, vec3 position, vec3 color, float attenuation, bool global)
{
vec3 fLambert = SamplerToVector(uParameters.Diffuse[layer].Sampler).rgb * uParameters.Diffuse[layer].Color.rgb;
vec3 specular_masks = SamplerToVector(uParameters.SpecularMasks[layer].Sampler).rgb;
float roughness = max(0.0f, specular_masks.b * uParameters.Roughness);
vec3 l = normalize(uViewPos - fPos);
vec3 n = normals;
vec3 v = normalize(position - fPos);
vec3 h = normalize(v + l);
float nDotH = max(dot(n, h), 0.0);
float hDotv = max(dot(h, v), 0.0);
float nDotL = max(dot(n, l), 0.0);
float nDotV = max(dot(n, v), 0.0);
vec3 f = schlickFresnel(fLambert, specular_masks.g, hDotv);
vec3 kS = f;
vec3 kD = 1.0 - kS;
kD *= 1.0 - specular_masks.g;
vec3 specBrdfNom = ggxDistribution(roughness, nDotH) * geomSmith(roughness, nDotL) * geomSmith(roughness, nDotV) * f;
float specBrdfDenom = 4.0 * nDotV * nDotL + 0.0001;
vec3 specBrdf = uParameters.Specular * specular_masks.r * specBrdfNom / specBrdfDenom;
vec3 diffuseBrdf = fLambert;
if (!global) diffuseBrdf = kD * fLambert / PI;
return (diffuseBrdf + specBrdf) * color * nDotL * attenuation;
}
vec3 CalcBaseLight(int layer, vec3 normals, BaseLight base, float attenuation, bool global)
{
return CalcLight(layer, normals, base.Position, base.Color.rgb * base.Intensity, attenuation, global);
}
vec3 CalcPointLight(int layer, vec3 normals, Light light)
{
float distanceToLight = length(light.Base.Position - fPos);
float attenuation = 1.0 / (1.0 + light.Linear * distanceToLight + light.Quadratic * pow(distanceToLight, 2));
return CalcBaseLight(layer, normals, light.Base, attenuation, true);
}
vec3 CalcSpotLight(int layer, vec3 normals, Light light)
{
vec3 v = normalize(light.Base.Position - fPos);
float inner = cos(radians(light.InnerConeAngle));
float outer = cos(radians(light.OuterConeAngle));
float distanceToLight = length(light.Base.Position - fPos);
float theta = dot(v, normalize(-vec3(0, -1, 0)));
float epsilon = inner - outer;
float attenuation = 1.0 / (1.0 + light.Attenuation * pow(distanceToLight, 2));
light.Base.Intensity *= smoothstep(0.0, 1.0, (theta - outer) / epsilon);
if(theta > outer)
{
return CalcBaseLight(layer, normals, light.Base, attenuation, true);
}
else
{
return vec3(0.0);
}
}
void main()
{
if (bVertexColors[2] && uHasVertexColors)
{
FragColor = fColor;
}
else if (bVertexColors[3])
{
FragColor = vec4(fNormal, 1);
}
else if (bVertexColors[4])
{
FragColor = vec4(fTangent, 1);
}
else if (bVertexColors[5])
{
FragColor = vec4(fTexCoords, 0, 1);
}
else
{
int layer = LayerToIndex();
vec3 normals = ComputeNormals(layer);
vec4 diffuse = SamplerToVector(uParameters.Diffuse[layer].Sampler);
vec3 result = uParameters.Diffuse[layer].Color.rgb * diffuse.rgb;
if (uParameters.HasAo)
{
vec3 m = SamplerToVector(uParameters.Ao.Sampler).rgb;
if (uParameters.Ao.HasColorBoost)
{
vec3 color = uParameters.Ao.ColorBoost.Color * uParameters.Ao.ColorBoost.Exponent;
result = mix(result, result * color, m.b);
}
result = mix(result * m.r * uParameters.Ao.AmbientOcclusion, result, m.g);
}
vec2 coords = ScaledTexCoords();
if (coords.x > uParameters.EmissiveRegion.x &&
coords.y > uParameters.EmissiveRegion.y &&
coords.x < uParameters.EmissiveRegion.z &&
coords.y < uParameters.EmissiveRegion.w)
{
coords.x -= uParameters.EmissiveRegion.x;
coords.y -= uParameters.EmissiveRegion.y;
coords.x *= 1.0 / (uParameters.EmissiveRegion.z - uParameters.EmissiveRegion.x);
coords.y *= 1.0 / (uParameters.EmissiveRegion.w - uParameters.EmissiveRegion.y);
vec4 emissive = SamplerToVector(uParameters.Emissive[layer].Sampler, coords);
result += uParameters.Emissive[layer].Color.rgb * emissive.rgb * uParameters.EmissiveMult;
}
if (!bVertexColors[1])
{
result += CalcLight(layer, normals, uViewPos, vec3(0.75), 1.0, false);
vec3 lights = vec3(uNumLights > 0 ? 0 : 1);
for (int i = 0; i < uNumLights; i++)
{
if (uLights[i].Type == 0)
{
lights += CalcPointLight(layer, normals, uLights[i]);
}
else if (uLights[i].Type == 1)
{
lights += CalcSpotLight(layer, normals, uLights[i]);
}
}
result *= lights; // use * to darken the scene, + to lighten it
}
result = result / (result + vec3(1.0));
FragColor = vec4(pow(result, vec3(1.0 / 2.2)), 1.0);
}
}

View File

@ -0,0 +1,52 @@
#version 460 core
layout (location = 1) in vec3 vPos;
layout (location = 2) in vec3 vNormal;
layout (location = 3) in vec3 vTangent;
layout (location = 4) in vec2 vTexCoords;
layout (location = 5) in float vTexLayer;
layout (location = 6) in vec4 vColor;
layout (location = 7) in vec4 vBoneIds;
layout (location = 8) in vec4 vBoneWeights;
layout (location = 9) in mat4 vInstanceMatrix;
layout (location = 13) in vec3 vMorphTarget;
//const int MAX_BONES = 0;
//const int MAX_BONE_INFLUENCE = 0;
uniform mat4 uView;
uniform mat4 uProjection;
uniform float uMorphTime;
//uniform mat4 uFinalBonesMatrix[MAX_BONES];
out vec3 fPos;
out vec3 fNormal;
out vec3 fTangent;
out vec2 fTexCoords;
out float fTexLayer;
out vec4 fColor;
void main()
{
vec4 pos = vec4(mix(vPos, vMorphTarget, uMorphTime), 1.0);
// for(int i = 0 ; i < MAX_BONE_INFLUENCE; i++)
// {
// if(vBoneIds[i] == -1) continue;
// if(vBoneIds[i] >= MAX_BONES)
// {
// break;
// }
//
// vec4 localPos = uFinalBonesMatrix[int(vBoneIds[i])] * pos;
// pos += localPos * vBoneWeights[i];
// }
gl_Position = uProjection * uView * vInstanceMatrix * pos;
fPos = vec3(vInstanceMatrix * pos);
fNormal = mat3(transpose(inverse(vInstanceMatrix))) * vNormal;
fTangent = mat3(transpose(inverse(vInstanceMatrix))) * vTangent;
fTexCoords = vTexCoords;
fTexLayer = vTexLayer;
fColor = vColor;
}

View File

@ -0,0 +1,12 @@
#version 460 core
in vec2 fTexCoords;
uniform sampler2D screenTexture;
out vec4 FragColor;
void main()
{
FragColor = texture(screenTexture, fTexCoords);
}

View File

@ -0,0 +1,12 @@
#version 460 core
layout (location = 0) in vec2 vPos;
layout (location = 1) in vec2 vTexCoords;
out vec2 fTexCoords;
void main()
{
gl_Position = vec4(vPos.x, vPos.y, 0.0, 1.0);
fTexCoords = vTexCoords;
}

View File

@ -0,0 +1,63 @@
#version 460 core
// --------------------- IN ---------------------
in OUT_IN_VARIABLES {
vec3 nearPoint;
vec3 farPoint;
mat4 proj;
mat4 view;
float near;
float far;
} inVar;
// --------------------- OUT --------------------
out vec4 FragColor;
// ------------------- UNIFORM ------------------
uniform vec3 uCamDir;
vec4 grid(vec3 fragPos, float scale) {
vec2 coord = fragPos.xz * scale;
vec2 derivative = fwidth(coord);
vec2 grid = abs(fract(coord - 0.5) - 0.5) / derivative;
float line = min(grid.x, grid.y);
float minimumz = min(derivative.y, 1) * 0.1;
float minimumx = min(derivative.x, 1) * 0.1;
vec4 color = vec4(0.102, 0.102, 0.129, 1.0 - min(line, 1.0));
if(abs(fragPos.x) < minimumx)
color.z = 1.0;
if(abs(fragPos.z) < minimumz)
color.x = 1.0;
return color;
}
float computeDepth(vec3 pos) {
vec4 clip_space_pos = inVar.proj * inVar.view * vec4(pos.xyz, 1.0);
float clip_space_depth = clip_space_pos.z / clip_space_pos.w;
float far = gl_DepthRange.far;
float near = gl_DepthRange.near;
float depth = (((far-near) * clip_space_depth) + near + far) / 2.0;
return depth;
}
float computeLinearDepth(vec3 pos) {
vec4 clip_space_pos = inVar.proj * inVar.view * vec4(pos.xyz, 1.0);
float clip_space_depth = (clip_space_pos.z / clip_space_pos.w) * 2.0 - 1.0;
float linearDepth = (2.0 * inVar.near * inVar.far) / (inVar.far + inVar.near - clip_space_depth * (inVar.far - inVar.near));
return linearDepth / inVar.far;
}
void main() {
float t = -inVar.nearPoint.y / (inVar.farPoint.y - inVar.nearPoint.y);
vec3 fragPos3D = inVar.nearPoint + t * (inVar.farPoint - inVar.nearPoint);
gl_FragDepth = computeDepth(fragPos3D);
float linearDepth = computeLinearDepth(fragPos3D);
float fading = max(0, (0.5 - linearDepth));
FragColor = (grid(fragPos3D, 10) + grid(fragPos3D, 1)) * float(t > 0);
FragColor.a *= fading;
}

View File

@ -0,0 +1,36 @@
#version 460 core
layout (location = 0) in vec3 vPos;
// --------------------- OUT ---------------------
out OUT_IN_VARIABLES {
vec3 nearPoint;
vec3 farPoint;
mat4 proj;
mat4 view;
float near;
float far;
} outVar;
uniform mat4 proj;
uniform mat4 view;
uniform float uNear;
uniform float uFar;
vec3 UnprojectPoint(vec2 xy, float z) {
mat4 viewInv = inverse(view);
mat4 projInv = inverse(proj);
vec4 unprojectedPoint = viewInv * projInv * vec4(xy, z, 1.0);
return unprojectedPoint.xyz / unprojectedPoint.w;
}
void main()
{
outVar.near = uNear;
outVar.far = uFar;
outVar.proj = proj;
outVar.view = view;
outVar.nearPoint = UnprojectPoint(vPos.xy, -1.0).xyz;
outVar.farPoint = UnprojectPoint(vPos.xy, 1.0).xyz;
gl_Position = vec4(vPos, 1.0f);
}

View File

@ -0,0 +1,16 @@
#version 460 core
uniform sampler2D uIcon;
uniform vec4 uColor;
in vec2 fTexCoords;
out vec4 FragColor;
void main()
{
vec4 color = uColor * texture(uIcon, fTexCoords);
if (color.a < 0.1) discard;
FragColor = uColor;
}

View File

@ -0,0 +1,15 @@
#version 460 core
layout (location = 0) in vec3 vPos;
layout (location = 9) in mat4 vInstanceMatrix;
uniform mat4 uView;
uniform mat4 uProjection;
out vec2 fTexCoords;
void main()
{
gl_Position = uProjection * uView * vInstanceMatrix * vec4(inverse(mat3(uView)) * vPos, 1.0);
fTexCoords = -vPos.xy;
}

BIN
FModel/Resources/nx.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
FModel/Resources/ny.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
FModel/Resources/nz.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@ -0,0 +1,8 @@
#version 460 core
out vec4 FragColor;
void main()
{
FragColor = vec4(0.929, 0.588, 0.196, 1.0);
}

View File

@ -0,0 +1,21 @@
#version 460 core
layout (location = 1) in vec3 vPos;
layout (location = 2) in vec3 vNormal;
layout (location = 9) in mat4 vInstanceMatrix;
layout (location = 13) in vec3 vMorphTarget;
uniform mat4 uView;
uniform vec3 uViewPos;
uniform mat4 uProjection;
uniform float uMorphTime;
void main()
{
vec3 pos = vec3(vInstanceMatrix * vec4(mix(vPos, vMorphTarget, uMorphTime), 1.0));
vec3 nor = mat3(transpose(inverse(vInstanceMatrix))) * vNormal;
float scaleFactor = distance(pos, uViewPos) * 0.0025;
vec3 scaleVertex = pos + nor * scaleFactor;
gl_Position = uProjection * uView * vec4(scaleVertex, 1.0);
}

View File

@ -0,0 +1,13 @@
#version 460 core
uniform uint uA;
uniform uint uB;
uniform uint uC;
uniform uint uD;
out uvec4 FragColor;
void main()
{
FragColor = uvec4(uA, uB, uC, uD);
}

View File

@ -0,0 +1,15 @@
#version 460 core
layout (location = 1) in vec3 vPos;
layout (location = 9) in mat4 vInstanceMatrix;
layout (location = 13) in vec3 vMorphTarget;
uniform mat4 uView;
uniform mat4 uProjection;
uniform float uMorphTime;
void main()
{
vec3 pos = mix(vPos, vMorphTarget, uMorphTime);
gl_Position = uProjection * uView * vInstanceMatrix * vec4(pos, 1.0);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

BIN
FModel/Resources/px.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

BIN
FModel/Resources/py.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

BIN
FModel/Resources/pz.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -0,0 +1,12 @@
#version 460 core
in vec3 fPos;
uniform samplerCube cubemap;
out vec4 FragColor;
void main()
{
FragColor = texture(cubemap, fPos);
}

View File

@ -0,0 +1,16 @@
#version 460 core
layout (location = 0) in vec3 vPos;
uniform mat4 uView;
uniform mat4 uProjection;
out vec3 fPos;
void main()
{
fPos = vPos;
vec4 pos = uProjection * uView * vec4(vPos, 1.0);
gl_Position = pos.xyww;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

View File

@ -8,6 +8,7 @@ using CUE4Parse.UE4.Objects.Core.Serialization;
using CUE4Parse.UE4.Versions;
using CUE4Parse_Conversion.Meshes;
using CUE4Parse_Conversion.Textures;
using CUE4Parse.UE4.Assets.Exports.Material;
using FModel.Framework;
using FModel.ViewModels;
using FModel.ViewModels.ApiEndpoints.Models;
@ -39,6 +40,16 @@ namespace FModel.Settings
if (File.Exists(FilePath)) File.Delete(FilePath);
}
public static bool IsEndpointValid(FGame game, EEndpointType type, out FEndpoint endpoint)
{
endpoint = null;
if (!Default.CustomEndpoints.TryGetValue(game, out var endpoints))
return false;
endpoint = endpoints[(int) type];
return endpoint.Overwrite || endpoint.IsValid;
}
private bool _showChangelog = true;
public bool ShowChangelog
{
@ -95,20 +106,6 @@ namespace FModel.Settings
set => SetProperty(ref _gameDirectory, value);
}
private bool _overwriteMapping;
public bool OverwriteMapping
{
get => _overwriteMapping;
set => SetProperty(ref _overwriteMapping, value);
}
private string _mappingFilePath;
public string MappingFilePath
{
get => _mappingFilePath;
set => SetProperty(ref _mappingFilePath, value);
}
private int _lastOpenedSettingTab;
public int LastOpenedSettingTab
{
@ -116,20 +113,6 @@ namespace FModel.Settings
set => SetProperty(ref _lastOpenedSettingTab, value);
}
private bool _isAutoSaveProps;
public bool IsAutoSaveProps
{
get => _isAutoSaveProps;
set => SetProperty(ref _isAutoSaveProps, value);
}
private bool _isAutoSaveTextures;
public bool IsAutoSaveTextures
{
get => _isAutoSaveTextures;
set => SetProperty(ref _isAutoSaveTextures, value);
}
private bool _isAutoOpenSounds = true;
public bool IsAutoOpenSounds
{
@ -144,7 +127,7 @@ namespace FModel.Settings
set => SetProperty(ref _isLoggerExpanded, value);
}
private GridLength _avalonImageSize = GridLength.Auto;
private GridLength _avalonImageSize = new (200);
public GridLength AvalonImageSize
{
get => _avalonImageSize;
@ -259,13 +242,6 @@ namespace FModel.Settings
set => SetProperty(ref _overridedPlatform, value);
}
private bool _saveMorphTargets = true;
public bool SaveMorphTargets
{
get => _saveMorphTargets;
set => SetProperty(ref _saveMorphTargets, value);
}
private IDictionary<FGame, string> _presets = new Dictionary<FGame, string>
{
{FGame.Unknown, Constants._NO_PRESET_TRIGGER},
@ -286,7 +262,10 @@ namespace FModel.Settings
{FGame.TslGame, Constants._NO_PRESET_TRIGGER},
{FGame.PortalWars, Constants._NO_PRESET_TRIGGER},
{FGame.Gameface, Constants._NO_PRESET_TRIGGER},
{FGame.Athena, Constants._NO_PRESET_TRIGGER}
{FGame.Athena, Constants._NO_PRESET_TRIGGER},
{FGame.PandaGame, Constants._NO_PRESET_TRIGGER},
{FGame.Hotta, Constants._NO_PRESET_TRIGGER},
{FGame.eFootball, Constants._NO_PRESET_TRIGGER}
};
public IDictionary<FGame, string> Presets
{
@ -297,24 +276,27 @@ namespace FModel.Settings
private IDictionary<FGame, EGame> _overridedGame = new Dictionary<FGame, EGame>
{
{FGame.Unknown, EGame.GAME_UE4_LATEST},
{FGame.FortniteGame, EGame.GAME_UE5_LATEST},
{FGame.FortniteGame, EGame.GAME_UE5_2},
{FGame.ShooterGame, EGame.GAME_Valorant},
{FGame.DeadByDaylight, EGame.GAME_UE4_LATEST},
{FGame.DeadByDaylight, EGame.GAME_UE4_27},
{FGame.OakGame, EGame.GAME_Borderlands3},
{FGame.Dungeons, EGame.GAME_UE4_LATEST},
{FGame.WorldExplorers, EGame.GAME_UE4_LATEST},
{FGame.Dungeons, EGame.GAME_UE4_22},
{FGame.WorldExplorers, EGame.GAME_UE4_24},
{FGame.g3, EGame.GAME_UE4_22},
{FGame.StateOfDecay2, EGame.GAME_StateOfDecay2},
{FGame.Prospect, EGame.GAME_UE4_LATEST},
{FGame.Indiana, EGame.GAME_UE4_LATEST},
{FGame.Prospect, EGame.GAME_Splitgate},
{FGame.Indiana, EGame.GAME_UE4_21},
{FGame.RogueCompany, EGame.GAME_RogueCompany},
{FGame.SwGame, EGame.GAME_UE4_LATEST},
{FGame.Platform, EGame.GAME_UE4_25},
{FGame.Platform, EGame.GAME_UE4_26},
{FGame.BendGame, EGame.GAME_UE4_11},
{FGame.TslGame, EGame.GAME_PlayerUnknownsBattlegrounds},
{FGame.PortalWars, EGame.GAME_UE4_LATEST},
{FGame.PortalWars, EGame.GAME_UE4_27},
{FGame.Gameface, EGame.GAME_GTATheTrilogyDefinitiveEdition},
{FGame.Athena, EGame.GAME_SeaOfThieves}
{FGame.Athena, EGame.GAME_SeaOfThieves},
{FGame.PandaGame, EGame.GAME_UE4_26},
{FGame.Hotta, EGame.GAME_TowerOfFantasy},
{FGame.eFootball, EGame.GAME_UE4_26}
};
public IDictionary<FGame, EGame> OverridedGame
{
@ -342,7 +324,10 @@ namespace FModel.Settings
{FGame.TslGame, null},
{FGame.PortalWars, null},
{FGame.Gameface, null},
{FGame.Athena, null}
{FGame.Athena, null},
{FGame.PandaGame, null},
{FGame.Hotta, null},
{FGame.eFootball, null}
};
public IDictionary<FGame, List<FCustomVersion>> OverridedCustomVersions
{
@ -370,7 +355,10 @@ namespace FModel.Settings
{FGame.TslGame, null},
{FGame.PortalWars, null},
{FGame.Gameface, null},
{FGame.Athena, null}
{FGame.Athena, null},
{FGame.PandaGame, null},
{FGame.Hotta, null},
{FGame.eFootball, null}
};
public IDictionary<FGame, Dictionary<string, bool>> OverridedOptions
{
@ -378,6 +366,43 @@ namespace FModel.Settings
set => SetProperty(ref _overridedOptions, value);
}
private IDictionary<FGame, FEndpoint[]> _customEndpoints = new Dictionary<FGame, FEndpoint[]>
{
{FGame.Unknown, new FEndpoint[]{new (), new ()}},
{
FGame.FortniteGame, new []
{
new FEndpoint("https://fortnitecentral.gmatrixgames.ga/api/v1/aes", "$.['mainKey','dynamicKeys']"),
new FEndpoint("https://fortnitecentral.gmatrixgames.ga/api/v1/mappings", "$.[?(@.meta.compressionMethod=='Oodle')].['url','fileName']") // && @.meta.platform=='Windows'
}
},
{FGame.ShooterGame, new FEndpoint[]{new (), new ()}},
{FGame.DeadByDaylight, new FEndpoint[]{new (), new ()}},
{FGame.OakGame, new FEndpoint[]{new (), new ()}},
{FGame.Dungeons, new FEndpoint[]{new (), new ()}},
{FGame.WorldExplorers, new FEndpoint[]{new (), new ()}},
{FGame.g3, new FEndpoint[]{new (), new ()}},
{FGame.StateOfDecay2, new FEndpoint[]{new (), new ()}},
{FGame.Prospect, new FEndpoint[]{new (), new ()}},
{FGame.Indiana, new FEndpoint[]{new (), new ()}},
{FGame.RogueCompany, new FEndpoint[]{new (), new ()}},
{FGame.SwGame, new FEndpoint[]{new (), new ()}},
{FGame.Platform, new FEndpoint[]{new (), new ()}},
{FGame.BendGame, new FEndpoint[]{new (), new ()}},
{FGame.TslGame, new FEndpoint[]{new (), new ()}},
{FGame.PortalWars, new FEndpoint[]{new (), new ()}},
{FGame.Gameface, new FEndpoint[]{new (), new ()}},
{FGame.Athena, new FEndpoint[]{new (), new ()}},
{FGame.PandaGame, new FEndpoint[]{new (), new ()}},
{FGame.Hotta, new FEndpoint[]{new (), new ()}},
{FGame.eFootball, new FEndpoint[]{new (), new ()}}
};
public IDictionary<FGame, FEndpoint[]> CustomEndpoints
{
get => _customEndpoints;
set => SetProperty(ref _customEndpoints, value);
}
private IDictionary<FGame, IList<CustomDirectory>> _customDirectories = new Dictionary<FGame, IList<CustomDirectory>>
{
{FGame.Unknown, new List<CustomDirectory>()},
@ -445,7 +470,10 @@ namespace FModel.Settings
{FGame.TslGame, new List<CustomDirectory>()},
{FGame.PortalWars, new List<CustomDirectory>()},
{FGame.Gameface, new List<CustomDirectory>()},
{FGame.Athena, new List<CustomDirectory>()}
{FGame.Athena, new List<CustomDirectory>()},
{FGame.PandaGame, new List<CustomDirectory>()},
{FGame.Hotta, new List<CustomDirectory>()},
{FGame.eFootball, new List<CustomDirectory>()}
};
public IDictionary<FGame, IList<CustomDirectory>> CustomDirectories
{
@ -509,27 +537,6 @@ namespace FModel.Settings
set => SetProperty(ref _assetRemoveTab, value);
}
private Hotkey _autoSaveProps = new(Key.F1);
public Hotkey AutoSaveProps
{
get => _autoSaveProps;
set => SetProperty(ref _autoSaveProps, value);
}
private Hotkey _autoSaveTextures = new(Key.F2);
public Hotkey AutoSaveTextures
{
get => _autoSaveTextures;
set => SetProperty(ref _autoSaveTextures, value);
}
private Hotkey _autoOpenSounds = new(Key.F3);
public Hotkey AutoOpenSounds
{
get => _autoOpenSounds;
set => SetProperty(ref _autoOpenSounds, value);
}
private Hotkey _addAudio = new(Key.N, ModifierKeys.Control);
public Hotkey AddAudio
{
@ -565,97 +572,11 @@ namespace FModel.Settings
set => SetProperty(ref _meshExportFormat, value);
}
private ELodFormat _lodExportFormat = ELodFormat.FirstLod;
public ELodFormat LodExportFormat
private EMaterialFormat _materialExportFormat = EMaterialFormat.FirstLayer;
public EMaterialFormat MaterialExportFormat
{
get => _lodExportFormat;
set => SetProperty(ref _lodExportFormat, value);
}
private bool _previewStaticMeshes = true;
public bool PreviewStaticMeshes
{
get => _previewStaticMeshes;
set
{
SetProperty(ref _previewStaticMeshes, value);
if (_previewStaticMeshes && SaveStaticMeshes)
SaveStaticMeshes = false;
}
}
private bool _previewSkeletalMeshes = true;
public bool PreviewSkeletalMeshes
{
get => _previewSkeletalMeshes;
set
{
SetProperty(ref _previewSkeletalMeshes, value);
if (_previewSkeletalMeshes && SaveSkeletalMeshes)
SaveSkeletalMeshes = false;
}
}
private bool _previewMaterials = true;
public bool PreviewMaterials
{
get => _previewMaterials;
set
{
SetProperty(ref _previewMaterials, value);
if (_previewMaterials && SaveMaterials)
SaveMaterials = false;
}
}
private bool _saveStaticMeshes;
public bool SaveStaticMeshes
{
get => _saveStaticMeshes;
set
{
SetProperty(ref _saveStaticMeshes, value);
if (_saveStaticMeshes && PreviewStaticMeshes)
PreviewStaticMeshes = false;
}
}
private bool _saveSkeletalMeshes;
public bool SaveSkeletalMeshes
{
get => _saveSkeletalMeshes;
set
{
SetProperty(ref _saveSkeletalMeshes, value);
if (_saveSkeletalMeshes && PreviewSkeletalMeshes)
PreviewSkeletalMeshes = false;
}
}
private bool _saveMaterials;
public bool SaveMaterials
{
get => _saveMaterials;
set
{
SetProperty(ref _saveMaterials, value);
if (_saveMaterials && PreviewMaterials)
PreviewMaterials = false;
}
}
private bool _saveAnimations;
public bool SaveAnimations
{
get => _saveAnimations;
set => SetProperty(ref _saveAnimations, value);
}
private bool _saveSkeletonAsMesh;
public bool SaveSkeletonAsMesh
{
get => _saveSkeletonAsMesh;
set => SetProperty(ref _saveSkeletonAsMesh, value);
get => _materialExportFormat;
set => SetProperty(ref _materialExportFormat, value);
}
private ETextureFormat _textureExportFormat = ETextureFormat.Png;
@ -664,5 +585,75 @@ namespace FModel.Settings
get => _textureExportFormat;
set => SetProperty(ref _textureExportFormat, value);
}
private ESocketFormat _socketExportFormat = ESocketFormat.Bone;
public ESocketFormat SocketExportFormat
{
get => _socketExportFormat;
set => SetProperty(ref _socketExportFormat, value);
}
private ELodFormat _lodExportFormat = ELodFormat.FirstLod;
public ELodFormat LodExportFormat
{
get => _lodExportFormat;
set => SetProperty(ref _lodExportFormat, value);
}
private bool _showSkybox = true;
public bool ShowSkybox
{
get => _showSkybox;
set => SetProperty(ref _showSkybox, value);
}
private bool _showGrid = true;
public bool ShowGrid
{
get => _showGrid;
set => SetProperty(ref _showGrid, value);
}
private bool _previewStaticMeshes = true;
public bool PreviewStaticMeshes
{
get => _previewStaticMeshes;
set => SetProperty(ref _previewStaticMeshes, value);
}
private bool _previewSkeletalMeshes = true;
public bool PreviewSkeletalMeshes
{
get => _previewSkeletalMeshes;
set => SetProperty(ref _previewSkeletalMeshes, value);
}
private bool _previewMaterials = true;
public bool PreviewMaterials
{
get => _previewMaterials;
set => SetProperty(ref _previewMaterials, value);
}
private bool _previewWorlds = true;
public bool PreviewWorlds
{
get => _previewWorlds;
set => SetProperty(ref _previewWorlds, value);
}
private bool _saveMorphTargets = true;
public bool SaveMorphTargets
{
get => _saveMorphTargets;
set => SetProperty(ref _saveMorphTargets, value);
}
private bool _saveSkeletonAsMesh;
public bool SaveSkeletonAsMesh
{
get => _saveSkeletonAsMesh;
set => SetProperty(ref _saveSkeletonAsMesh, value);
}
}
}

View File

@ -51,7 +51,7 @@ public class AesManagerViewModel : ViewModel
DynamicKeys = null
};
_mainKey.Key = FixKey(_keysFromSettings.MainKey);
_mainKey.Key = Helper.FixKey(_keysFromSettings.MainKey);
AesKeys = new FullyObservableCollection<FileItem>(EnumerateAesKeys());
AesKeys.ItemPropertyChanged += AesKeysOnItemPropertyChanged;
AesKeysView = new ListCollectionView(AesKeys) { SortDescriptions = { new SortDescription("Name", ListSortDirection.Ascending) } };
@ -63,11 +63,11 @@ public class AesManagerViewModel : ViewModel
if (e.PropertyName != "Key" || sender is not FullyObservableCollection<FileItem> collection)
return;
var key = FixKey(collection[e.CollectionIndex].Key);
var key = Helper.FixKey(collection[e.CollectionIndex].Key);
if (e.CollectionIndex == 0)
{
if (!HasChange)
HasChange = FixKey(_keysFromSettings.MainKey) != key;
HasChange = Helper.FixKey(_keysFromSettings.MainKey) != key;
_keysFromSettings.MainKey = key;
}
@ -79,7 +79,7 @@ public class AesManagerViewModel : ViewModel
new()
{
Key = key,
FileName = collection[e.CollectionIndex].Name,
Name = collection[e.CollectionIndex].Name,
Guid = collection[e.CollectionIndex].Guid.ToString()
}
};
@ -87,7 +87,7 @@ public class AesManagerViewModel : ViewModel
else if (_keysFromSettings.DynamicKeys.FirstOrDefault(x => x.Guid == collection[e.CollectionIndex].Guid.ToString()) is { } d)
{
if (!HasChange)
HasChange = FixKey(d.Key) != key;
HasChange = Helper.FixKey(d.Key) != key;
d.Key = key;
}
@ -97,7 +97,7 @@ public class AesManagerViewModel : ViewModel
_keysFromSettings.DynamicKeys.Add(new DynamicKey
{
Key = key,
FileName = collection[e.CollectionIndex].Name,
Name = collection[e.CollectionIndex].Name,
Guid = collection[e.CollectionIndex].Guid.ToString()
});
}
@ -117,17 +117,6 @@ public class AesManagerViewModel : ViewModel
Log.Information("{@Json}", UserSettings.Default);
}
private string FixKey(string key)
{
if (string.IsNullOrEmpty(key))
return string.Empty;
if (key.StartsWith("0x"))
key = key[2..];
return "0x" + key.ToUpper().Trim();
}
private IEnumerable<FileItem> EnumerateAesKeys()
{
yield return _mainKey;
@ -145,7 +134,7 @@ public class AesManagerViewModel : ViewModel
k = dynamicKey.Key;
}
file.Key = FixKey(k);
file.Key = Helper.FixKey(k);
yield return file;
}
}

View File

@ -1,4 +1,7 @@
using FModel.Framework;
using System;
using System.IO;
using System.Threading.Tasks;
using FModel.Framework;
using FModel.ViewModels.ApiEndpoints;
using RestSharp;
@ -17,16 +20,30 @@ public class ApiEndpointViewModel
public FortniteApiEndpoint FortniteApi { get; }
public ValorantApiEndpoint ValorantApi { get; }
public BenbotApiEndpoint BenbotApi { get; }
public FortniteCentralApiEndpoint CentralApi { get; }
public EpicApiEndpoint EpicApi { get; }
public FModelApi FModelApi { get; }
public FModelApiEndpoint FModelApi { get; }
public DynamicApiEndpoint DynamicApi { get; }
public ApiEndpointViewModel()
{
FortniteApi = new FortniteApiEndpoint(_client);
ValorantApi = new ValorantApiEndpoint(_client);
BenbotApi = new BenbotApiEndpoint(_client);
CentralApi = new FortniteCentralApiEndpoint(_client);
EpicApi = new EpicApiEndpoint(_client);
FModelApi = new FModelApi(_client);
FModelApi = new FModelApiEndpoint(_client);
DynamicApi = new DynamicApiEndpoint(_client);
}
public async Task DownloadFileAsync(string fileLink, string installationPath)
{
var request = new FRestRequest(fileLink);
var data = _client.DownloadData(request) ?? Array.Empty<byte>();
await File.WriteAllBytesAsync(installationPath, data);
}
public void DownloadFile(string fileLink, string installationPath)
{
DownloadFileAsync(fileLink, installationPath).GetAwaiter().GetResult();
}
}

View File

@ -1,77 +0,0 @@
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using FModel.ViewModels.ApiEndpoints.Models;
using RestSharp;
using Serilog;
namespace FModel.ViewModels.ApiEndpoints;
public class BenbotApiEndpoint : AbstractApiProvider
{
public BenbotApiEndpoint(RestClient client) : base(client)
{
}
public async Task<AesResponse> GetAesKeysAsync(CancellationToken token)
{
var request = new RestRequest("https://benbot.app/api/v2/aes")
{
OnBeforeDeserialization = resp => { resp.ContentType = "application/json; charset=utf-8"; }
};
var response = await _client.ExecuteAsync<AesResponse>(request, token).ConfigureAwait(false);
Log.Information("[{Method}] [{Status}({StatusCode})] '{Resource}'", request.Method, response.StatusDescription, (int) response.StatusCode, response.ResponseUri?.OriginalString);
return response.Data;
}
public AesResponse GetAesKeys(CancellationToken token)
{
return GetAesKeysAsync(token).GetAwaiter().GetResult();
}
public async Task<MappingsResponse[]> GetMappingsAsync(CancellationToken token)
{
var request = new RestRequest("https://benbot.app/api/v1/mappings")
{
OnBeforeDeserialization = resp => { resp.ContentType = "application/json; charset=utf-8"; }
};
var response = await _client.ExecuteAsync<MappingsResponse[]>(request, token).ConfigureAwait(false);
Log.Information("[{Method}] [{Status}({StatusCode})] '{Resource}'", request.Method, response.StatusDescription, (int) response.StatusCode, response.ResponseUri?.OriginalString);
return response.Data;
}
public MappingsResponse[] GetMappings(CancellationToken token)
{
return GetMappingsAsync(token).GetAwaiter().GetResult();
}
public async Task<Dictionary<string, Dictionary<string, string>>> GetHotfixesAsync(CancellationToken token, string language = "en-US")
{
var request = new RestRequest("https://benbot.app/api/v1/hotfixes")
{
OnBeforeDeserialization = resp => { resp.ContentType = "application/json; charset=utf-8"; }
};
request.AddParameter("lang", language);
var response = await _client.ExecuteAsync<Dictionary<string, Dictionary<string, string>>>(request, token).ConfigureAwait(false);
Log.Information("[{Method}] [{Status}({StatusCode})] '{Resource}'", request.Method, response.StatusDescription, (int) response.StatusCode, response.ResponseUri?.OriginalString);
return response.Data;
}
public Dictionary<string, Dictionary<string, string>> GetHotfixes(CancellationToken token, string language = "en-US")
{
return GetHotfixesAsync(token, language).GetAwaiter().GetResult();
}
public async Task DownloadFileAsync(string fileLink, string installationPath)
{
var request = new RestRequest(fileLink);
var data = _client.DownloadData(request);
await File.WriteAllBytesAsync(installationPath, data);
}
public void DownloadFile(string fileLink, string installationPath)
{
DownloadFileAsync(fileLink, installationPath).GetAwaiter().GetResult();
}
}

View File

@ -0,0 +1,75 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using FModel.Extensions;
using FModel.Framework;
using FModel.ViewModels.ApiEndpoints.Models;
using Newtonsoft.Json.Linq;
using RestSharp;
using Serilog;
namespace FModel.ViewModels.ApiEndpoints;
public class DynamicApiEndpoint : AbstractApiProvider
{
public DynamicApiEndpoint(RestClient client) : base(client)
{
}
public async Task<AesResponse> GetAesKeysAsync(CancellationToken token, string url, string path)
{
var body = await GetRequestBody(token, url).ConfigureAwait(false);
var tokens = body.SelectTokens(path);
var ret = new AesResponse { MainKey = Helper.FixKey(tokens.ElementAtOrDefault(0)?.ToString()) };
if (tokens.ElementAtOrDefault(1) is JArray dynamicKeys)
{
foreach (var dynamicKey in dynamicKeys)
{
if (dynamicKey["guid"] is not { } guid || dynamicKey["key"] is not { } key)
continue;
ret.DynamicKeys.Add(new DynamicKey
{
Name = dynamicKey["name"]?.ToString(),
Guid = guid.ToString(), Key = Helper.FixKey(key.ToString())
});
}
}
return ret;
}
public AesResponse GetAesKeys(CancellationToken token, string url, string path)
{
return GetAesKeysAsync(token, url, path).GetAwaiter().GetResult();
}
public async Task<MappingsResponse[]> GetMappingsAsync(CancellationToken token, string url, string path)
{
var body = await GetRequestBody(token, url).ConfigureAwait(false);
var tokens = body.SelectTokens(path);
var ret = new MappingsResponse[] {new()};
ret[0].Url = tokens.ElementAtOrDefault(0)?.ToString();
if (tokens.ElementAtOrDefault(1) is not { } fileName)
fileName = ret[0].Url?.SubstringAfterLast("/");
ret[0].FileName = fileName.ToString();
return ret;
}
public MappingsResponse[] GetMappings(CancellationToken token, string url, string path)
{
return GetMappingsAsync(token, url, path).GetAwaiter().GetResult();
}
public async Task<JToken> GetRequestBody(CancellationToken token, string url)
{
var request = new FRestRequest(url)
{
OnBeforeDeserialization = resp => { resp.ContentType = "application/json; charset=utf-8"; }
};
var response = await _client.ExecuteAsync(request, token).ConfigureAwait(false);
Log.Information("[{Method}] [{Status}({StatusCode})] '{Resource}'", request.Method, response.StatusDescription, (int) response.StatusCode, response.ResponseUri?.OriginalString);
return response.IsSuccessful && !string.IsNullOrEmpty(response.Content) ? JToken.Parse(response.Content) : JToken.Parse("{}");
}
}

View File

@ -1,10 +1,15 @@
using System;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using EpicManifestParser.Objects;
using FModel.Framework;
using FModel.Settings;
using FModel.ViewModels.ApiEndpoints.Models;
using RestSharp;
using Serilog;
namespace FModel.ViewModels.ApiEndpoints;
@ -21,16 +26,16 @@ public class EpicApiEndpoint : AbstractApiProvider
public async Task<ManifestInfo> GetManifestAsync(CancellationToken token)
{
if (IsExpired())
if (await IsExpired().ConfigureAwait(false))
{
var auth = await GetAuthAsync(token);
var auth = await GetAuthAsync(token).ConfigureAwait(false);
if (auth != null)
{
UserSettings.Default.LastAuthResponse = auth;
}
}
var request = new RestRequest(_LAUNCHER_ASSETS);
var request = new FRestRequest(_LAUNCHER_ASSETS);
request.AddHeader("Authorization", $"bearer {UserSettings.Default.LastAuthResponse.AccessToken}");
var response = await _client.ExecuteAsync(request, token).ConfigureAwait(false);
Log.Information("[{Method}] [{Status}({StatusCode})] '{Resource}'", request.Method, response.StatusDescription, (int) response.StatusCode, response.ResponseUri?.OriginalString);
@ -44,7 +49,7 @@ public class EpicApiEndpoint : AbstractApiProvider
private async Task<AuthResponse> GetAuthAsync(CancellationToken token)
{
var request = new RestRequest(_OAUTH_URL, Method.Post);
var request = new FRestRequest(_OAUTH_URL, Method.Post);
request.AddHeader("Authorization", _BASIC_TOKEN);
request.AddParameter("grant_type", "client_credentials");
var response = await _client.ExecuteAsync<AuthResponse>(request, token).ConfigureAwait(false);
@ -52,9 +57,12 @@ public class EpicApiEndpoint : AbstractApiProvider
return response.Data;
}
private bool IsExpired()
private async Task<bool> IsExpired()
{
if (string.IsNullOrEmpty(UserSettings.Default.LastAuthResponse.AccessToken)) return true;
return DateTime.Now.Subtract(TimeSpan.FromHours(1)) >= UserSettings.Default.LastAuthResponse.ExpiresAt;
var request = new FRestRequest("https://account-public-service-prod.ol.epicgames.com/account/api/oauth/verify");
request.AddHeader("Authorization", $"bearer {UserSettings.Default.LastAuthResponse.AccessToken}");
var response = await _client.ExecuteGetAsync(request).ConfigureAwait(false);
return !response.IsSuccessful;
}
}

View File

@ -6,6 +6,7 @@ using System.Threading.Tasks;
using System.Windows;
using AutoUpdaterDotNET;
using FModel.Extensions;
using FModel.Framework;
using FModel.Services;
using FModel.Settings;
using FModel.ViewModels.ApiEndpoints.Models;
@ -19,7 +20,7 @@ using MessageBoxResult = AdonisUI.Controls.MessageBoxResult;
namespace FModel.ViewModels.ApiEndpoints;
public class FModelApi : AbstractApiProvider
public class FModelApiEndpoint : AbstractApiProvider
{
private News _news;
private Info _infos;
@ -28,13 +29,13 @@ public class FModelApi : AbstractApiProvider
private readonly IDictionary<string, CommunityDesign> _communityDesigns = new Dictionary<string, CommunityDesign>();
private ApplicationViewModel _applicationView => ApplicationService.ApplicationView;
public FModelApi(RestClient client) : base(client)
public FModelApiEndpoint(RestClient client) : base(client)
{
}
public async Task<News> GetNewsAsync(CancellationToken token, string game)
{
var request = new RestRequest($"https://api.fmodel.app/v1/news/{Constants.APP_VERSION}");
var request = new FRestRequest($"https://api.fmodel.app/v1/news/{Constants.APP_VERSION}");
request.AddParameter("game", game);
var response = await _client.ExecuteAsync<News>(request, token).ConfigureAwait(false);
Log.Information("[{Method}] [{Status}({StatusCode})] '{Resource}'", request.Method, response.StatusDescription, (int) response.StatusCode, response.ResponseUri?.OriginalString);
@ -48,7 +49,7 @@ public class FModelApi : AbstractApiProvider
public async Task<Info> GetInfosAsync(CancellationToken token, EUpdateMode updateMode)
{
var request = new RestRequest($"https://api.fmodel.app/v1/infos/{updateMode}");
var request = new FRestRequest($"https://api.fmodel.app/v1/infos/{updateMode}");
var response = await _client.ExecuteAsync<Info>(request, token).ConfigureAwait(false);
Log.Information("[{Method}] [{Status}({StatusCode})] '{Resource}'", request.Method, response.StatusDescription, (int) response.StatusCode, response.ResponseUri?.OriginalString);
return response.Data;
@ -61,7 +62,7 @@ public class FModelApi : AbstractApiProvider
public async Task<Backup[]> GetBackupsAsync(CancellationToken token, string gameName)
{
var request = new RestRequest($"https://api.fmodel.app/v1/backups/{gameName}");
var request = new FRestRequest($"https://api.fmodel.app/v1/backups/{gameName}");
var response = await _client.ExecuteAsync<Backup[]>(request, token).ConfigureAwait(false);
Log.Information("[{Method}] [{Status}({StatusCode})] '{Resource}'", request.Method, response.StatusDescription, (int) response.StatusCode, response.ResponseUri?.OriginalString);
return response.Data;
@ -74,7 +75,7 @@ public class FModelApi : AbstractApiProvider
public async Task<Game> GetGamesAsync(CancellationToken token, string gameName)
{
var request = new RestRequest($"https://api.fmodel.app/v1/games/{gameName}");
var request = new FRestRequest($"https://api.fmodel.app/v1/games/{gameName}");
var response = await _client.ExecuteAsync<Game>(request, token).ConfigureAwait(false);
Log.Information("[{Method}] [{Status}({StatusCode})] '{Resource}'", request.Method, response.StatusDescription, (int) response.StatusCode, response.ResponseUri?.OriginalString);
return response.Data;
@ -87,7 +88,7 @@ public class FModelApi : AbstractApiProvider
public async Task<CommunityDesign> GetDesignAsync(string designName)
{
var request = new RestRequest($"https://api.fmodel.app/v1/designs/{designName}");
var request = new FRestRequest($"https://api.fmodel.app/v1/designs/{designName}");
var response = await _client.ExecuteAsync<Community>(request).ConfigureAwait(false);
Log.Information("[{Method}] [{Status}({StatusCode})] '{Resource}'", request.Method, response.StatusDescription, (int) response.StatusCode, response.ResponseUri?.OriginalString);
return response.Data != null ? new CommunityDesign(response.Data) : null;
@ -173,7 +174,7 @@ public class FModelApi : AbstractApiProvider
private void ShowChangelog(UpdateInfoEventArgs args)
{
var request = new RestRequest(args.ChangelogURL);
var request = new FRestRequest(args.ChangelogURL);
var response = _client.Execute(request);
if (string.IsNullOrEmpty(response.Content)) return;

View File

@ -2,6 +2,7 @@
using FModel.ViewModels.ApiEndpoints.Models;
using RestSharp;
using System.Threading.Tasks;
using FModel.Framework;
namespace FModel.ViewModels.ApiEndpoints;
@ -13,7 +14,7 @@ public class FortniteApiEndpoint : AbstractApiProvider
public async Task<PlaylistResponse> GetPlaylistAsync(string playlistId)
{
var request = new RestRequest($"https://fortnite-api.com/v1/playlists/{playlistId}");
var request = new FRestRequest($"https://fortnite-api.com/v1/playlists/{playlistId}");
var response = await _client.ExecuteAsync<PlaylistResponse>(request).ConfigureAwait(false);
return response.Data;
}
@ -25,7 +26,7 @@ public class FortniteApiEndpoint : AbstractApiProvider
public bool TryGetBytes(Uri link, out byte[] data)
{
var request = new RestRequest(link);
var request = new FRestRequest(link);
data = _client.DownloadData(request);
return data != null;
}

View File

@ -0,0 +1,32 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using FModel.Framework;
using RestSharp;
using Serilog;
namespace FModel.ViewModels.ApiEndpoints;
public class FortniteCentralApiEndpoint : AbstractApiProvider
{
public FortniteCentralApiEndpoint(RestClient client) : base(client)
{
}
public async Task<Dictionary<string, Dictionary<string, string>>> GetHotfixesAsync(CancellationToken token, string language = "en")
{
var request = new FRestRequest("https://fortnitecentral.gmatrixgames.ga/api/v1/hotfixes")
{
OnBeforeDeserialization = resp => { resp.ContentType = "application/json; charset=utf-8"; }
};
request.AddParameter("lang", language);
var response = await _client.ExecuteAsync<Dictionary<string, Dictionary<string, string>>>(request, token).ConfigureAwait(false);
Log.Information("[{Method}] [{Status}({StatusCode})] '{Resource}'", request.Method, response.StatusDescription, (int) response.StatusCode, response.ResponseUri?.OriginalString);
return response.Data;
}
public Dictionary<string, Dictionary<string, string>> GetHotfixes(CancellationToken token, string language = "en")
{
return GetHotfixesAsync(token, language).GetAwaiter().GetResult();
}
}

View File

@ -1,23 +1,33 @@
using System.Collections.Generic;
using System.Diagnostics;
using J = Newtonsoft.Json.JsonPropertyAttribute;
using I = Newtonsoft.Json.JsonIgnoreAttribute;
namespace FModel.ViewModels.ApiEndpoints.Models;
[DebuggerDisplay("{" + nameof(Version) + "}")]
public class AesResponse
{
[J("version")] public string Version { get; private set; }
[I][J("version")] public string Version { get; private set; }
[J("mainKey")] public string MainKey { get; set; }
[J("dynamicKeys")] public List<DynamicKey> DynamicKeys { get; set; }
public bool HasDynamicKeys => DynamicKeys is { Count: > 0 };
public AesResponse()
{
MainKey = string.Empty;
DynamicKeys = new List<DynamicKey>();
}
[I] public bool HasDynamicKeys => DynamicKeys is { Count: > 0 };
[I] public bool IsValid => MainKey.Length == 66 || HasDynamicKeys;
}
[DebuggerDisplay("{" + nameof(Key) + "}")]
public class DynamicKey
{
[J("fileName")] public string FileName { get; set; }
[J("name")] public string Name { get; set; }
[J("guid")] public string Guid { get; set; }
[J("key")] public string Key { get; set; }
}
[I] public bool IsValid => Guid.Length == 32 && Key.Length == 66;
}

View File

@ -1,22 +1,32 @@
using System.Diagnostics;
using J = Newtonsoft.Json.JsonPropertyAttribute;
using I = Newtonsoft.Json.JsonIgnoreAttribute;
namespace FModel.ViewModels.ApiEndpoints.Models;
[DebuggerDisplay("{" + nameof(FileName) + "}")]
public class MappingsResponse
{
[J] public string Url { get; private set; }
[J] public string FileName { get; private set; }
[J] public string Hash { get; private set; }
[J] public long Length { get; private set; }
[J] public string Uploaded { get; private set; }
[J] public Meta Meta { get; private set; }
[J] public string Url { get; set; }
[J] public string FileName { get; set; }
[I][J] public string Hash { get; private set; }
[I][J] public long Length { get; private set; }
[I][J] public string Uploaded { get; private set; }
[I][J] public Meta Meta { get; set; }
public MappingsResponse()
{
Url = string.Empty;
FileName = string.Empty;
}
[I] public bool IsValid => !string.IsNullOrEmpty(Url) &&
!string.IsNullOrEmpty(FileName);
}
[DebuggerDisplay("{" + nameof(CompressionMethod) + "}")]
public class Meta
{
[J] public string Version { get; private set; }
[J] public string CompressionMethod { get; private set; }
}
[I][J] public string Version { get; private set; }
[J] public string CompressionMethod { get; set; }
}

View File

@ -12,10 +12,9 @@ public class PlaylistResponse
[J] public Playlist Data { get; private set; }
[J] public string Error { get; private set; }
public bool IsSuccess => Status == 200;
public bool HasError => Error != null;
private object DebuggerDisplay => IsSuccess ? Data : $"Error: {Status} | {Error}";
[I] public bool IsSuccess => Status == 200;
[I] public bool HasError => Error != null;
[I] private object DebuggerDisplay => IsSuccess ? Data : $"Error: {Status} | {Error}";
}
[DebuggerDisplay("{" + nameof(Id) + "}")]
@ -32,4 +31,4 @@ public class PlaylistImages
[I] public bool HasShowcase => Showcase != null;
[I] public bool HasMissionIcon => MissionIcon != null;
}
}

View File

@ -13,6 +13,7 @@ using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using FModel.Framework;
namespace FModel.ViewModels.ApiEndpoints;
@ -26,7 +27,7 @@ public class ValorantApiEndpoint : AbstractApiProvider
public async Task<VManifest> GetManifestAsync(CancellationToken token)
{
var request = new RestRequest(_URL);
var request = new FRestRequest(_URL);
var response = await _client.ExecuteAsync(request, token).ConfigureAwait(false);
return new VManifest(response.RawBytes);
}

View File

@ -22,7 +22,6 @@ namespace FModel.ViewModels;
public class ApplicationViewModel : ViewModel
{
private EBuildKind _build;
public EBuildKind Build
{
get => _build;
@ -33,24 +32,11 @@ public class ApplicationViewModel : ViewModel
}
}
private bool _isReady;
public bool IsReady
{
get => _isReady;
private set => SetProperty(ref _isReady, value);
}
private EStatusKind _status;
public EStatusKind Status
private FStatus _status;
public FStatus Status
{
get => _status;
set
{
SetProperty(ref _status, value);
IsReady = Status != EStatusKind.Loading && Status != EStatusKind.Stopping;
}
set => SetProperty(ref _status, value);
}
public RightClickMenuCommand RightClickMenuCommand => _rightClickMenuCommand ??= new RightClickMenuCommand(this);
@ -72,12 +58,11 @@ public class ApplicationViewModel : ViewModel
public AesManagerViewModel AesManager { get; }
public AudioPlayerViewModel AudioPlayer { get; }
public MapViewerViewModel MapViewer { get; }
public ModelViewerViewModel ModelViewer { get; }
private OodleCompressor _oodle;
public ApplicationViewModel()
{
Status = EStatusKind.Loading;
Status = new FStatus();
#if DEBUG
Build = EBuildKind.Debug;
#elif RELEASE
@ -94,8 +79,7 @@ public class ApplicationViewModel : ViewModel
AesManager = new AesManagerViewModel(CUE4Parse);
MapViewer = new MapViewerViewModel(CUE4Parse);
AudioPlayer = new AudioPlayerViewModel();
ModelViewer = new ModelViewerViewModel(CUE4Parse.Game);
Status = EStatusKind.Ready;
Status.SetStatus(EStatusKind.Ready);
}
public void AvoidEmptyGameDirectoryAndSetEGame(bool bAlreadyLaunched)
@ -160,7 +144,7 @@ public class ApplicationViewModel : ViewModel
var vgmZipFilePath = Path.Combine(UserSettings.Default.OutputDirectory, ".data", "vgmstream-win.zip");
if (File.Exists(vgmZipFilePath)) return;
await ApplicationService.ApiEndpointView.BenbotApi.DownloadFileAsync("https://github.com/vgmstream/vgmstream/releases/latest/download/vgmstream-win.zip", vgmZipFilePath);
await ApplicationService.ApiEndpointView.DownloadFileAsync("https://github.com/vgmstream/vgmstream/releases/latest/download/vgmstream-win.zip", vgmZipFilePath);
if (new FileInfo(vgmZipFilePath).Length > 0)
{
var zip = ZipFile.Read(vgmZipFilePath);
@ -201,4 +185,17 @@ public class ApplicationViewModel : ViewModel
(OodleLZ_FuzzSafe) a, (OodleLZ_CheckCRC) b, (OodleLZ_Verbosity) c, d, e, f, g, h, i, (OodleLZ_Decode_ThreadPhase) threadModule);
}
}
}
public async Task InitImGuiSettings()
{
var imgui = Path.Combine(/*UserSettings.Default.OutputDirectory, ".data", */"imgui.ini");
if (File.Exists(imgui)) return;
await ApplicationService.ApiEndpointView.DownloadFileAsync("https://cdn.fmodel.app/d/configurations/imgui.ini", imgui);
if (new FileInfo(imgui).Length == 0)
{
FLogger.AppendError();
FLogger.AppendText("Could not download ImGui settings", Constants.WHITE, true);
}
}
}

View File

@ -1,11 +1,9 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Data;
using CUE4Parse.UE4.Versions;
using CUE4Parse.UE4.Vfs;
using FModel.Framework;
using FModel.Services;
@ -35,11 +33,11 @@ public class TreeItem : ViewModel
set => SetProperty(ref _isSelected, value);
}
private string _package;
public string Package
private string _archive;
public string Archive
{
get => _package;
private set => SetProperty(ref _package, value);
get => _archive;
private set => SetProperty(ref _archive, value);
}
private string _mountPoint;
@ -61,10 +59,10 @@ public class TreeItem : ViewModel
public RangeObservableCollection<TreeItem> Folders { get; }
public ICollectionView FoldersView { get; }
public TreeItem(string header, string package, string mountPoint, int version, string pathHere)
public TreeItem(string header, string archive, string mountPoint, int version, string pathHere)
{
Header = header;
Package = package;
Archive = archive;
MountPoint = mountPoint;
Version = version;
PathAtThisPoint = pathHere;
@ -129,7 +127,7 @@ public class AssetsFolderViewModel
if (lastNode == null)
{
var nodePath = builder.ToString();
lastNode = new TreeItem(folder, item.Package, entry.Vfs.MountPoint, entry.Vfs.Ver.Value, nodePath[..^1]);
lastNode = new TreeItem(folder, item.Archive, entry.Vfs.MountPoint, entry.Vfs.Ver.Value, nodePath[..^1]);
lastNode.Folders.SetSuppressionState(true);
lastNode.AssetsList.Assets.SetSuppressionState(true);
parentNode.Add(lastNode);
@ -167,4 +165,4 @@ public class AssetsFolderViewModel
}
});
}
}
}

View File

@ -35,11 +35,11 @@ public class AssetItem : ViewModel
private set => SetProperty(ref _size, value);
}
private string _package;
public string Package
private string _archive;
public string Archive
{
get => _package;
private set => SetProperty(ref _package, value);
get => _archive;
private set => SetProperty(ref _archive, value);
}
private CompressionMethod _compression;
@ -49,13 +49,13 @@ public class AssetItem : ViewModel
private set => SetProperty(ref _compression, value);
}
public AssetItem(string fullPath, bool isEncrypted, long offset, long size, string package, CompressionMethod compression)
public AssetItem(string fullPath, bool isEncrypted, long offset, long size, string archive, CompressionMethod compression)
{
FullPath = fullPath;
IsEncrypted = isEncrypted;
Offset = offset;
Size = size;
Package = package;
Archive = archive;
Compression = compression;
}
@ -75,4 +75,4 @@ public class AssetsListViewModel
SortDescriptions = { new SortDescription("FullPath", ListSortDirection.Ascending) }
};
}
}
}

View File

@ -163,6 +163,7 @@ public class AudioPlayerViewModel : ViewModel, ISource, IDisposable
private TimeSpan _length => _waveSource?.GetLength() ?? TimeSpan.Zero;
private TimeSpan _position => _waveSource?.GetPosition() ?? TimeSpan.Zero;
private PlaybackState _playbackState => _soundOut?.PlaybackState ?? PlaybackState.Stopped;
private bool _hideToggle = false;
public SpectrumProvider Spectrum { get; private set; }
public float[] FftData { get; private set; }
@ -409,6 +410,13 @@ public class AudioPlayerViewModel : ViewModel, ISource, IDisposable
_soundOut.Stop();
}
public void HideToggle()
{
if (!IsPlaying) return;
_hideToggle = !_hideToggle;
RaiseSourcePropertyChangedEvent(ESourceProperty.HideToggle, _hideToggle);
}
public void SkipTo(double percentage)
{
if (_soundOut == null || _waveSource == null) return;
@ -580,4 +588,4 @@ public class AudioPlayerViewModel : ViewModel, ISource, IDisposable
File.Delete(SelectedAudioFile.FilePath);
return vgmProcess?.ExitCode == 0 && File.Exists(wavFilePath);
}
}
}

View File

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.IO;
@ -93,7 +93,7 @@ public class BackupManagerViewModel : ViewModel
await _threadWorkerView.Begin(_ =>
{
var fullPath = Path.Combine(Path.Combine(UserSettings.Default.OutputDirectory, "Backups"), SelectedBackup.FileName);
_apiEndpointView.BenbotApi.DownloadFile(SelectedBackup.DownloadUrl, fullPath);
_apiEndpointView.DownloadFile(SelectedBackup.DownloadUrl, fullPath);
SaveCheck(fullPath, SelectedBackup.FileName, "downloaded", "download");
});
}

View File

@ -23,6 +23,7 @@ using CUE4Parse.UE4.Assets.Exports.Texture;
using CUE4Parse.UE4.Assets.Exports.Wwise;
using CUE4Parse.UE4.IO;
using CUE4Parse.UE4.Localization;
using CUE4Parse.UE4.Objects.Engine;
using CUE4Parse.UE4.Oodle.Objects;
using CUE4Parse.UE4.Readers;
using CUE4Parse.UE4.Shaders;
@ -38,7 +39,11 @@ using FModel.Services;
using FModel.Settings;
using FModel.Views;
using FModel.Views.Resources.Controls;
using FModel.Views.Snooper;
using Newtonsoft.Json;
using Ookii.Dialogs.Wpf;
using OpenTK.Windowing.Common;
using OpenTK.Windowing.Desktop;
using Serilog;
using SkiaSharp;
@ -54,7 +59,6 @@ public class CUE4ParseViewModel : ViewModel
RegexOptions.Compiled | RegexOptions.Singleline | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
private FGame _game;
public FGame Game
{
get => _game;
@ -68,6 +72,39 @@ public class CUE4ParseViewModel : ViewModel
set => SetProperty(ref _modelIsOverwritingMaterial, value);
}
private bool _modelIsWaitingAnimation;
public bool ModelIsWaitingAnimation
{
get => _modelIsWaitingAnimation;
set => SetProperty(ref _modelIsWaitingAnimation, value);
}
private Snooper _snooper;
public Snooper SnooperViewer
{
get
{
return Application.Current.Dispatcher.Invoke(delegate
{
return _snooper ??= new Snooper(GameWindowSettings.Default,
new NativeWindowSettings
{
Size = new OpenTK.Mathematics.Vector2i(
Convert.ToInt32(SystemParameters.MaximizedPrimaryScreenWidth * .75),
Convert.ToInt32(SystemParameters.MaximizedPrimaryScreenHeight * .85)),
NumberOfSamples = Constants.SAMPLES_COUNT,
WindowBorder = WindowBorder.Resizable,
Flags = ContextFlags.ForwardCompatible,
Profile = ContextProfile.Core,
APIVersion = new Version(4, 6),
StartVisible = false,
StartFocused = false,
Title = "3D Viewer"
});
});
}
}
public AbstractVfsFileProvider Provider { get; }
public GameDirectoryViewModel GameDirectory { get; }
public AssetsFolderViewModel AssetsFolder { get; }
@ -103,7 +140,9 @@ public class CUE4ParseViewModel : ViewModel
}
default:
{
Game = gameDirectory.SubstringBeforeLast("\\Content").SubstringAfterLast("\\").ToEnum(FGame.Unknown);
var parent = gameDirectory.SubstringBeforeLast("\\Content").SubstringAfterLast("\\");
if (gameDirectory.Contains("eFootball")) parent = gameDirectory.SubstringBeforeLast("\\pak").SubstringAfterLast("\\");
Game = Helper.IAmThePanda(parent) ? FGame.PandaGame : parent.ToEnum(FGame.Unknown);
var versions = new VersionContainer(UserSettings.Default.OverridedGame[Game], UserSettings.Default.OverridedPlatform,
customVersions: UserSettings.Default.OverridedCustomVersions[Game],
optionOverrides: UserSettings.Default.OverridedOptions[Game]);
@ -127,6 +166,13 @@ public class CUE4ParseViewModel : ViewModel
},
SearchOption.AllDirectories, true, versions);
break;
case FGame.eFootball:
Provider = new DefaultFileProvider(new DirectoryInfo(gameDirectory), new List<DirectoryInfo>
{
new(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData) + "\\KONAMI\\eFootball\\ST\\Download")
},
SearchOption.AllDirectories, true, versions);
break;
case FGame.Unknown when UserSettings.Default.ManualGames.TryGetValue(gameDirectory, out var settings):
{
versions = new VersionContainer(settings.OverridedGame, UserSettings.Default.OverridedPlatform,
@ -275,7 +321,7 @@ public class CUE4ParseViewModel : ViewModel
file.FileCount = vfs.FileCount;
}
Game = Provider.GameName.ToEnum(Game);
Game = Helper.IAmThePanda(Provider.GameName) ? FGame.PandaGame : Provider.GameName.ToEnum(Game);
});
}
@ -292,16 +338,18 @@ public class CUE4ParseViewModel : ViewModel
public async Task RefreshAes()
{
if (Game == FGame.FortniteGame) // game directory dependent, we don't have the provider game name yet since we don't have aes keys
{
await _threadWorkerView.Begin(cancellationToken =>
{
var aes = _apiEndpointView.BenbotApi.GetAesKeys(cancellationToken);
if (aes?.MainKey == null && aes?.DynamicKeys == null && aes?.Version == null) return;
// game directory dependent, we don't have the provider game name yet since we don't have aes keys
// except when this comes from the AES Manager
if (!UserSettings.IsEndpointValid(Game, EEndpointType.Aes, out var endpoint))
return;
UserSettings.Default.AesKeys[Game] = aes;
});
}
await _threadWorkerView.Begin(cancellationToken =>
{
var aes = _apiEndpointView.DynamicApi.GetAesKeys(cancellationToken, endpoint.Url, endpoint.Path);
if (aes is not { IsValid: true }) return;
UserSettings.Default.AesKeys[Game] = aes;
});
}
public async Task InitInformation()
@ -318,32 +366,36 @@ public class CUE4ParseViewModel : ViewModel
});
}
public async Task InitBenMappings()
public async Task InitMappings()
{
if (Game != FGame.FortniteGame) return;
if (!UserSettings.IsEndpointValid(Game, EEndpointType.Mapping, out var endpoint))
{
Provider.MappingsContainer = null;
return;
}
await _threadWorkerView.Begin(cancellationToken =>
{
if (UserSettings.Default.OverwriteMapping && File.Exists(UserSettings.Default.MappingFilePath))
if (endpoint.Overwrite && File.Exists(endpoint.FilePath))
{
Provider.MappingsContainer = new FileUsmapTypeMappingsProvider(UserSettings.Default.MappingFilePath);
Provider.MappingsContainer = new FileUsmapTypeMappingsProvider(endpoint.FilePath);
FLogger.AppendInformation();
FLogger.AppendText($"Mappings pulled from '{UserSettings.Default.MappingFilePath.SubstringAfterLast("\\")}'", Constants.WHITE, true);
FLogger.AppendText($"Mappings pulled from '{endpoint.FilePath.SubstringAfterLast("\\")}'", Constants.WHITE, true);
}
else
else if (endpoint.IsValid)
{
var mappingsFolder = Path.Combine(UserSettings.Default.OutputDirectory, ".data");
var mappings = _apiEndpointView.BenbotApi.GetMappings(cancellationToken);
var mappings = _apiEndpointView.DynamicApi.GetMappings(cancellationToken, endpoint.Url, endpoint.Path);
if (mappings is { Length: > 0 })
{
foreach (var mapping in mappings)
{
if (mapping.Meta.CompressionMethod != "Oodle") continue;
if (!mapping.IsValid) continue;
var mappingPath = Path.Combine(mappingsFolder, mapping.FileName);
if (!File.Exists(mappingPath))
{
_apiEndpointView.BenbotApi.DownloadFile(mapping.Url, mappingPath);
_apiEndpointView.DownloadFile(mapping.Url, mappingPath);
}
Provider.MappingsContainer = new FileUsmapTypeMappingsProvider(mappingPath);
@ -352,10 +404,11 @@ public class CUE4ParseViewModel : ViewModel
break;
}
}
else
if (Provider.MappingsContainer == null)
{
var latestUsmaps = new DirectoryInfo(mappingsFolder).GetFiles("*_oo.usmap");
if (Provider.MappingsContainer != null || latestUsmaps.Length <= 0) return;
if (latestUsmaps.Length <= 0) return;
var latestUsmapInfo = latestUsmaps.OrderBy(f => f.LastWriteTime).Last();
Provider.MappingsContainer = new FileUsmapTypeMappingsProvider(latestUsmapInfo.FullName);
@ -370,6 +423,7 @@ public class CUE4ParseViewModel : ViewModel
{
await LoadGameLocalizedResources();
await LoadHotfixedLocalizedResources();
await _threadWorkerView.Begin(_ => Utils.Typefaces = new Typefaces(this));
if (LocalizedResourcesCount > 0)
{
FLogger.AppendInformation();
@ -388,7 +442,6 @@ public class CUE4ParseViewModel : ViewModel
await _threadWorkerView.Begin(cancellationToken =>
{
LocalizedResourcesCount = Provider.LoadLocalization(UserSettings.Default.AssetLanguage, cancellationToken);
Utils.Typefaces = new Typefaces(this);
});
}
@ -401,7 +454,7 @@ public class CUE4ParseViewModel : ViewModel
if (Game != FGame.FortniteGame || HotfixedResourcesDone) return;
await _threadWorkerView.Begin(cancellationToken =>
{
var hotfixes = ApplicationService.ApiEndpointView.BenbotApi.GetHotfixes(cancellationToken, Provider.GetLanguageCode(UserSettings.Default.AssetLanguage));
var hotfixes = ApplicationService.ApiEndpointView.CentralApi.GetHotfixes(cancellationToken, Provider.GetLanguageCode(UserSettings.Default.AssetLanguage));
if (hotfixes == null) return;
HotfixedResourcesDone = true;
@ -440,67 +493,54 @@ public class CUE4ParseViewModel : ViewModel
});
}
public void ExtractFolder(CancellationToken cancellationToken, TreeItem folder)
{
foreach (var asset in folder.AssetsList.Assets)
{
Thread.Sleep(10);
cancellationToken.ThrowIfCancellationRequested();
try
{
Extract(asset.FullPath, TabControl.HasNoTabs);
}
catch
{
// ignore
}
}
foreach (var f in folder.Folders) ExtractFolder(cancellationToken, f);
}
public void ExportFolder(CancellationToken cancellationToken, TreeItem folder)
{
foreach (var asset in folder.AssetsList.Assets)
{
Thread.Sleep(10);
cancellationToken.ThrowIfCancellationRequested();
ExportData(asset.FullPath);
}
foreach (var f in folder.Folders) ExportFolder(cancellationToken, f);
}
public void SaveFolder(CancellationToken cancellationToken, TreeItem folder)
{
foreach (var asset in folder.AssetsList.Assets)
{
Thread.Sleep(10);
cancellationToken.ThrowIfCancellationRequested();
try
{
Extract(asset.FullPath, TabControl.HasNoTabs, true);
}
catch
{
// ignore
}
}
foreach (var f in folder.Folders) SaveFolder(cancellationToken, f);
}
public void ExtractSelected(CancellationToken cancellationToken, IEnumerable<AssetItem> assetItems)
{
foreach (var asset in assetItems)
{
Thread.Sleep(10);
cancellationToken.ThrowIfCancellationRequested();
Extract(asset.FullPath, TabControl.HasNoTabs);
Extract(cancellationToken, asset.FullPath, TabControl.HasNoTabs);
}
}
public void Extract(string fullPath, bool addNewTab = false, bool bulkSave = false)
private void BulkFolder(CancellationToken cancellationToken, TreeItem folder, Action<AssetItem> action)
{
foreach (var asset in folder.AssetsList.Assets)
{
Thread.Sleep(10);
cancellationToken.ThrowIfCancellationRequested();
try
{
action(asset);
}
catch
{
// ignore
}
}
foreach (var f in folder.Folders) BulkFolder(cancellationToken, f, action);
}
public void ExportFolder(CancellationToken cancellationToken, TreeItem folder)
=> BulkFolder(cancellationToken, folder, asset => ExportData(asset.FullPath));
public void ExtractFolder(CancellationToken cancellationToken, TreeItem folder)
=> BulkFolder(cancellationToken, folder, asset => Extract(cancellationToken, asset.FullPath, TabControl.HasNoTabs));
public void SaveFolder(CancellationToken cancellationToken, TreeItem folder)
=> BulkFolder(cancellationToken, folder, asset => Extract(cancellationToken, asset.FullPath, TabControl.HasNoTabs, EBulkType.Properties | EBulkType.Auto));
public void TextureFolder(CancellationToken cancellationToken, TreeItem folder)
=> BulkFolder(cancellationToken, folder, asset => Extract(cancellationToken, asset.FullPath, TabControl.HasNoTabs, EBulkType.Textures | EBulkType.Auto));
public void ModelFolder(CancellationToken cancellationToken, TreeItem folder)
=> BulkFolder(cancellationToken, folder, asset => Extract(cancellationToken, asset.FullPath, TabControl.HasNoTabs, EBulkType.Meshes | EBulkType.Auto));
public void AnimationFolder(CancellationToken cancellationToken, TreeItem folder)
=> BulkFolder(cancellationToken, folder, asset => Extract(cancellationToken, asset.FullPath, TabControl.HasNoTabs, EBulkType.Animations | EBulkType.Auto));
public void Extract(CancellationToken cancellationToken, string fullPath, bool addNewTab = false, EBulkType bulk = EBulkType.None)
{
Log.Information("User DOUBLE-CLICKED to extract '{FullPath}'", fullPath);
@ -518,6 +558,8 @@ public class CUE4ParseViewModel : ViewModel
TabControl.SelectedTab.Directory = directory;
}
var autoProperties = bulk == (EBulkType.Properties | EBulkType.Auto);
var autoTextures = bulk == (EBulkType.Textures | EBulkType.Auto);
TabControl.SelectedTab.ClearImages();
TabControl.SelectedTab.ResetDocumentText();
TabControl.SelectedTab.ScrollTrigger = null;
@ -527,13 +569,13 @@ public class CUE4ParseViewModel : ViewModel
case "uasset":
case "umap":
{
var exports = Provider.LoadObjectExports(fullPath);
TabControl.SelectedTab.SetDocumentText(JsonConvert.SerializeObject(exports, Formatting.Indented), bulkSave);
if (bulkSave) break;
var exports = Provider.LoadObjectExports(fullPath); // cancellationToken
TabControl.SelectedTab.SetDocumentText(JsonConvert.SerializeObject(exports, Formatting.Indented), autoProperties);
if (HasFlag(bulk, EBulkType.Properties)) break; // do not search for viewable exports if we are dealing with jsons
foreach (var e in exports)
{
if (CheckExport(e))
if (CheckExport(cancellationToken, e, bulk))
break;
}
@ -558,6 +600,8 @@ public class CUE4ParseViewModel : ViewModel
case "xml":
case "css":
case "csv":
case "pem":
case "tps":
case "js":
case "po":
case "h":
@ -567,7 +611,7 @@ public class CUE4ParseViewModel : ViewModel
using var stream = new MemoryStream(data) { Position = 0 };
using var reader = new StreamReader(stream);
TabControl.SelectedTab.SetDocumentText(reader.ReadToEnd(), bulkSave);
TabControl.SelectedTab.SetDocumentText(reader.ReadToEnd(), autoProperties);
}
break;
@ -577,7 +621,7 @@ public class CUE4ParseViewModel : ViewModel
if (Provider.TryCreateReader(fullPath, out var archive))
{
var metadata = new FTextLocalizationMetaDataResource(archive);
TabControl.SelectedTab.SetDocumentText(JsonConvert.SerializeObject(metadata, Formatting.Indented), bulkSave);
TabControl.SelectedTab.SetDocumentText(JsonConvert.SerializeObject(metadata, Formatting.Indented), autoProperties);
}
break;
@ -587,7 +631,7 @@ public class CUE4ParseViewModel : ViewModel
if (Provider.TryCreateReader(fullPath, out var archive))
{
var locres = new FTextLocalizationResource(archive);
TabControl.SelectedTab.SetDocumentText(JsonConvert.SerializeObject(locres, Formatting.Indented), bulkSave);
TabControl.SelectedTab.SetDocumentText(JsonConvert.SerializeObject(locres, Formatting.Indented), autoProperties);
}
break;
@ -597,7 +641,7 @@ public class CUE4ParseViewModel : ViewModel
if (Provider.TryCreateReader(fullPath, out var archive))
{
var registry = new FAssetRegistryState(archive);
TabControl.SelectedTab.SetDocumentText(JsonConvert.SerializeObject(registry, Formatting.Indented), bulkSave);
TabControl.SelectedTab.SetDocumentText(JsonConvert.SerializeObject(registry, Formatting.Indented), autoProperties);
}
break;
@ -608,7 +652,7 @@ public class CUE4ParseViewModel : ViewModel
if (Provider.TryCreateReader(fullPath, out var archive))
{
var wwise = new WwiseReader(archive);
TabControl.SelectedTab.SetDocumentText(JsonConvert.SerializeObject(wwise, Formatting.Indented), bulkSave);
TabControl.SelectedTab.SetDocumentText(JsonConvert.SerializeObject(wwise, Formatting.Indented), autoProperties);
foreach (var (name, data) in wwise.WwiseEncodedMedias)
{
SaveAndPlaySound(fullPath.SubstringBeforeWithLast("/") + name, "WEM", data);
@ -629,7 +673,7 @@ public class CUE4ParseViewModel : ViewModel
if (Provider.TryCreateReader(fullPath, out var archive))
{
var header = new FOodleDictionaryArchive(archive).Header;
TabControl.SelectedTab.SetDocumentText(JsonConvert.SerializeObject(header, Formatting.Indented), bulkSave);
TabControl.SelectedTab.SetDocumentText(JsonConvert.SerializeObject(header, Formatting.Indented), autoProperties);
}
break;
@ -641,7 +685,7 @@ public class CUE4ParseViewModel : ViewModel
if (Provider.TrySaveAsset(fullPath, out var data))
{
using var stream = new MemoryStream(data) { Position = 0 };
TabControl.SelectedTab.AddImage(fileName.SubstringBeforeLast("."), false, SKBitmap.Decode(stream));
TabControl.SelectedTab.AddImage(fileName.SubstringBeforeLast("."), false, SKBitmap.Decode(stream), autoTextures);
}
break;
@ -661,7 +705,7 @@ public class CUE4ParseViewModel : ViewModel
canvas.DrawPicture(svg.Picture, paint);
}
TabControl.SelectedTab.AddImage(fileName.SubstringBeforeLast("."), false, bitmap);
TabControl.SelectedTab.AddImage(fileName.SubstringBeforeLast("."), false, bitmap, autoTextures);
}
break;
@ -678,7 +722,7 @@ public class CUE4ParseViewModel : ViewModel
if (Provider.TryCreateReader(fullPath, out var archive))
{
var ar = new FShaderCodeArchive(archive);
TabControl.SelectedTab.SetDocumentText(JsonConvert.SerializeObject(ar, Formatting.Indented), bulkSave);
TabControl.SelectedTab.SetDocumentText(JsonConvert.SerializeObject(ar, Formatting.Indented), autoProperties);
}
break;
@ -692,28 +736,30 @@ public class CUE4ParseViewModel : ViewModel
}
}
public void ExtractAndScroll(string fullPath, string objectName)
public void ExtractAndScroll(CancellationToken cancellationToken, string fullPath, string objectName)
{
Log.Information("User CTRL-CLICKED to extract '{FullPath}'", fullPath);
TabControl.AddTab(fullPath.SubstringAfterLast('/'), fullPath.SubstringBeforeLast('/'));
TabControl.SelectedTab.ScrollTrigger = objectName;
var exports = Provider.LoadObjectExports(fullPath);
var exports = Provider.LoadObjectExports(fullPath); // cancellationToken
TabControl.SelectedTab.Highlighter = AvalonExtensions.HighlighterSelector(""); // json
TabControl.SelectedTab.SetDocumentText(JsonConvert.SerializeObject(exports, Formatting.Indented), false);
foreach (var e in exports)
{
if (CheckExport(e))
if (CheckExport(cancellationToken, e))
break;
}
}
private bool CheckExport(UObject export) // return true once you wanna stop searching for exports
private bool CheckExport(CancellationToken cancellationToken, UObject export, EBulkType bulk = EBulkType.None) // return true once you wanna stop searching for exports
{
var isNone = bulk == EBulkType.None;
var loadTextures = isNone || HasFlag(bulk, EBulkType.Textures);
switch (export)
{
case USolarisDigest solarisDigest:
case USolarisDigest solarisDigest when isNone:
{
if (!TabControl.CanAddTabs) return false;
@ -722,13 +768,13 @@ public class CUE4ParseViewModel : ViewModel
TabControl.SelectedTab.SetDocumentText(solarisDigest.ReadableCode, false);
return true;
}
case UTexture2D texture:
case UTexture2D texture when loadTextures:
{
TabControl.SelectedTab.AddImage(texture);
TabControl.SelectedTab.AddImage(texture, HasFlag(bulk, EBulkType.Auto));
return false;
}
case UAkMediaAssetData:
case USoundWave:
case UAkMediaAssetData when isNone:
case USoundWave when isNone:
{
var shouldDecompress = UserSettings.Default.CompressedAudioMode == ECompressedAudio.PlayDecompressed;
export.Decode(shouldDecompress, out var audioFormat, out var data);
@ -738,45 +784,50 @@ public class CUE4ParseViewModel : ViewModel
SaveAndPlaySound(Path.Combine(TabControl.SelectedTab.Directory, TabControl.SelectedTab.Header.SubstringBeforeLast('.')).Replace('\\', '/'), audioFormat, data);
return false;
}
case UStaticMesh when UserSettings.Default.PreviewStaticMeshes:
case USkeletalMesh when UserSettings.Default.PreviewSkeletalMeshes:
case UMaterialInstance when UserSettings.Default.PreviewMaterials && !ModelIsOverwritingMaterial &&
case UWorld when isNone && UserSettings.Default.PreviewWorlds:
case UStaticMesh when isNone && UserSettings.Default.PreviewStaticMeshes:
case USkeletalMesh when isNone && UserSettings.Default.PreviewSkeletalMeshes:
case UMaterialInstance when isNone && UserSettings.Default.PreviewMaterials && !ModelIsOverwritingMaterial &&
!(Game == FGame.FortniteGame && export.Owner != null && (export.Owner.Name.EndsWith($"/MI_OfferImages/{export.Name}", StringComparison.OrdinalIgnoreCase) ||
export.Owner.Name.EndsWith($"/RenderSwitch_Materials/{export.Name}", StringComparison.OrdinalIgnoreCase) ||
export.Owner.Name.EndsWith($"/MI_BPTile/{export.Name}", StringComparison.OrdinalIgnoreCase))):
export.Owner.Name.EndsWith($"/RenderSwitch_Materials/{export.Name}", StringComparison.OrdinalIgnoreCase) ||
export.Owner.Name.EndsWith($"/MI_BPTile/{export.Name}", StringComparison.OrdinalIgnoreCase))):
{
Application.Current.Dispatcher.Invoke(delegate
{
var modelViewer = Helper.GetWindow<ModelViewer>("Model Viewer", () => new ModelViewer().Show());
modelViewer.Load(export);
});
if (SnooperViewer.TryLoadExport(cancellationToken, export))
SnooperViewer.Run();
return true;
}
case UMaterialInstance m when ModelIsOverwritingMaterial:
case UMaterialInstance m when isNone && ModelIsOverwritingMaterial:
{
Application.Current.Dispatcher.Invoke(delegate
{
var modelViewer = Helper.GetWindow<ModelViewer>("Model Viewer", () => new ModelViewer().Show());
modelViewer.Overwrite(m);
});
SnooperViewer.Renderer.Swap(m);
SnooperViewer.Run();
return true;
}
case UStaticMesh when UserSettings.Default.SaveStaticMeshes:
case USkeletalMesh when UserSettings.Default.SaveSkeletalMeshes:
case UMaterialInstance when UserSettings.Default.SaveMaterials:
case USkeleton when UserSettings.Default.SaveSkeletonAsMesh:
case UAnimSequence when UserSettings.Default.SaveAnimations:
case UAnimSequence a when isNone && ModelIsWaitingAnimation:
{
SaveExport(export);
SnooperViewer.Renderer.Animate(a);
SnooperViewer.Run();
return true;
}
case UStaticMesh when HasFlag(bulk, EBulkType.Meshes):
case USkeletalMesh when HasFlag(bulk, EBulkType.Meshes):
case USkeleton when UserSettings.Default.SaveSkeletonAsMesh && HasFlag(bulk, EBulkType.Meshes):
// case UMaterialInstance when HasFlag(bulk, EBulkType.Materials): // read the fucking json
case UAnimSequence when HasFlag(bulk, EBulkType.Animations):
{
SaveExport(export, HasFlag(bulk, EBulkType.Auto));
return true;
}
default:
{
if (!loadTextures)
return false;
using var package = new CreatorPackage(export, UserSettings.Default.CosmeticStyle);
if (!package.TryConstructCreator(out var creator)) return false;
if (!package.TryConstructCreator(out var creator))
return false;
creator.ParseForInfo();
TabControl.SelectedTab.AddImage(export.Name, false, creator.Draw());
TabControl.SelectedTab.AddImage(export.Name, false, creator.Draw(), HasFlag(bulk, EBulkType.Auto));
return true;
}
}
@ -809,23 +860,36 @@ public class CUE4ParseViewModel : ViewModel
});
}
private void SaveExport(UObject export)
private void SaveExport(UObject export, bool auto)
{
var exportOptions = new ExporterOptions
{
TextureFormat = UserSettings.Default.TextureExportFormat,
LodFormat = UserSettings.Default.LodExportFormat,
MeshFormat = UserSettings.Default.MeshExportFormat,
MaterialFormat = UserSettings.Default.MaterialExportFormat,
TextureFormat = UserSettings.Default.TextureExportFormat,
SocketFormat = UserSettings.Default.SocketExportFormat,
Platform = UserSettings.Default.OverridedPlatform,
ExportMorphTargets = UserSettings.Default.SaveMorphTargets
};
var toSave = new Exporter(export, exportOptions);
var toSaveDirectory = new DirectoryInfo(UserSettings.Default.ModelDirectory);
if (toSave.TryWriteToDir(toSaveDirectory, out var savedFileName))
string dir;
if (!auto)
{
Log.Information("Successfully saved {FileName}", savedFileName);
var folderBrowser = new VistaFolderBrowserDialog();
if (folderBrowser.ShowDialog() == true)
dir = folderBrowser.SelectedPath;
else return;
}
else dir = UserSettings.Default.ModelDirectory;
var toSaveDirectory = new DirectoryInfo(dir);
if (toSave.TryWriteToDir(toSaveDirectory, out var label, out var savedFilePath))
{
Log.Information("Successfully saved {FilePath}", savedFilePath);
FLogger.AppendInformation();
FLogger.AppendText($"Successfully saved {savedFileName}", Constants.WHITE, true);
FLogger.AppendText($"Successfully saved {label}", Constants.WHITE, true);
}
else
{
@ -858,4 +922,9 @@ public class CUE4ParseViewModel : ViewModel
FLogger.AppendText($"Could not export '{fileName}'", Constants.WHITE, true);
}
}
private static bool HasFlag(EBulkType a, EBulkType b)
{
return (a & b) == b;
}
}

View File

@ -105,8 +105,7 @@ public class LoadCommand : ViewModelCommand<LoadingModesViewModel>
private void FilterDirectoryFilesToDisplay(CancellationToken cancellationToken, IEnumerable<FileItem> directoryFiles)
{
HashSet<string> filter;
if (directoryFiles == null)
filter = null;
if (directoryFiles == null) filter = null;
else
{
filter = new HashSet<string>();
@ -132,12 +131,19 @@ public class LoadCommand : ViewModelCommand<LoadingModesViewModel>
if (hasFilter)
{
if (filter.Contains(entry.Vfs.Name))
{
entries.Add(entry);
_applicationView.Status.UpdateStatusLabel(entry.Vfs.Name);
}
}
else
{
entries.Add(entry);
_applicationView.Status.UpdateStatusLabel(entry.Vfs.Name);
}
}
_applicationView.Status.UpdateStatusLabel("Folders & Packages");
_applicationView.CUE4Parse.AssetsFolder.BulkPopulate(entries);
}
@ -171,7 +177,8 @@ public class LoadCommand : ViewModelCommand<LoadingModesViewModel>
using var archive = new FStreamArchive(fileStream.Name, memoryStream);
var entries = new List<VfsEntry>();
switch (UserSettings.Default.LoadingMode)
var mode = UserSettings.Default.LoadingMode;
switch (mode)
{
case ELoadingMode.AllButNew:
{
@ -192,6 +199,7 @@ public class LoadCommand : ViewModelCommand<LoadingModesViewModel>
entry.Path.EndsWith(".ubulk") || entry.Path.EndsWith(".uptnl")) continue;
entries.Add(entry);
_applicationView.Status.UpdateStatusLabel(entry.Vfs.Name);
}
break;
@ -215,12 +223,14 @@ public class LoadCommand : ViewModelCommand<LoadingModesViewModel>
continue;
entries.Add(entry);
_applicationView.Status.UpdateStatusLabel(entry.Vfs.Name);
}
break;
}
}
_applicationView.Status.UpdateStatusLabel($"{mode.ToString()[6..]} Folders & Packages");
_applicationView.CUE4Parse.AssetsFolder.BulkPopulate(entries);
}
}

View File

@ -35,6 +35,9 @@ public class MenuCommand : ViewModelCommand<ApplicationViewModel>
contextViewModel.CUE4Parse.TabControl.SelectedTab.Highlighter = AvalonExtensions.HighlighterSelector("json");
contextViewModel.CUE4Parse.TabControl.SelectedTab.SetDocumentText(JsonConvert.SerializeObject(contextViewModel.CUE4Parse.GameDirectory.DirectoryFiles, Formatting.Indented), false);
break;
case "Views_3dViewer":
contextViewModel.CUE4Parse.SnooperViewer.Run();
break;
case "Views_AudioPlayer":
Helper.OpenWindow<AdonisWindow>("Audio Player", () => new AudioPlayer().Show());
break;
@ -109,4 +112,4 @@ public class MenuCommand : ViewModelCommand<ApplicationViewModel>
cancellationToken.ThrowIfCancellationRequested();
foreach (var f in parent.Folders) LoopFolders(cancellationToken, f, isExpanded);
}
}
}

View File

@ -29,9 +29,8 @@ public class RightClickMenuCommand : ViewModelCommand<ApplicationViewModel>
foreach (var asset in assetItems)
{
cancellationToken.ThrowIfCancellationRequested();
contextViewModel.CUE4Parse.Extract(asset.FullPath, true);
contextViewModel.CUE4Parse.Extract(cancellationToken, asset.FullPath, true);
}
break;
case "Assets_Export_Data":
foreach (var asset in assetItems)
@ -39,27 +38,38 @@ public class RightClickMenuCommand : ViewModelCommand<ApplicationViewModel>
cancellationToken.ThrowIfCancellationRequested();
contextViewModel.CUE4Parse.ExportData(asset.FullPath);
}
break;
case "Assets_Save_Properties":
foreach (var asset in assetItems)
{
cancellationToken.ThrowIfCancellationRequested();
contextViewModel.CUE4Parse.Extract(asset.FullPath);
contextViewModel.CUE4Parse.Extract(cancellationToken, asset.FullPath, false, EBulkType.Properties);
contextViewModel.CUE4Parse.TabControl.SelectedTab.SaveProperty(false);
}
break;
case "Assets_Save_Texture":
case "Assets_Save_Textures":
foreach (var asset in assetItems)
{
cancellationToken.ThrowIfCancellationRequested();
contextViewModel.CUE4Parse.Extract(asset.FullPath);
contextViewModel.CUE4Parse.TabControl.SelectedTab.SaveImage(false);
contextViewModel.CUE4Parse.Extract(cancellationToken, asset.FullPath, false, EBulkType.Textures);
contextViewModel.CUE4Parse.TabControl.SelectedTab.SaveImages(false);
}
break;
case "Assets_Save_Models":
foreach (var asset in assetItems)
{
cancellationToken.ThrowIfCancellationRequested();
contextViewModel.CUE4Parse.Extract(cancellationToken, asset.FullPath, false, EBulkType.Meshes);
}
break;
case "Assets_Save_Animations":
foreach (var asset in assetItems)
{
cancellationToken.ThrowIfCancellationRequested();
contextViewModel.CUE4Parse.Extract(cancellationToken, asset.FullPath, false, EBulkType.Animations);
}
break;
}
});
}
}
}

View File

@ -9,12 +9,13 @@ namespace FModel.ViewModels.Commands;
public class TabCommand : ViewModelCommand<TabItem>
{
private ApplicationViewModel _applicationView => ApplicationService.ApplicationView;
private ThreadWorkerViewModel _threadWorkerView => ApplicationService.ThreadWorkerView;
public TabCommand(TabItem contextViewModel) : base(contextViewModel)
{
}
public override void Execute(TabItem contextViewModel, object parameter)
public override async void Execute(TabItem contextViewModel, object parameter)
{
switch (parameter)
{
@ -30,6 +31,35 @@ public class TabCommand : ViewModelCommand<TabItem>
case "Close_Other_Tabs":
_applicationView.CUE4Parse.TabControl.RemoveOtherTabs(contextViewModel);
break;
case "Asset_Export_Data":
await _threadWorkerView.Begin(_ => _applicationView.CUE4Parse.ExportData(contextViewModel.FullPath));
break;
case "Asset_Save_Properties":
await _threadWorkerView.Begin(cancellationToken =>
{
_applicationView.CUE4Parse.Extract(cancellationToken, contextViewModel.FullPath, false, EBulkType.Properties);
_applicationView.CUE4Parse.TabControl.SelectedTab.SaveProperty(false);
});
break;
case "Asset_Save_Textures":
await _threadWorkerView.Begin(cancellationToken =>
{
_applicationView.CUE4Parse.Extract(cancellationToken, contextViewModel.FullPath, false, EBulkType.Textures);
_applicationView.CUE4Parse.TabControl.SelectedTab.SaveImages(false);
});
break;
case "Asset_Save_Models":
await _threadWorkerView.Begin(cancellationToken =>
{
_applicationView.CUE4Parse.Extract(cancellationToken, contextViewModel.FullPath, false, EBulkType.Meshes);
});
break;
case "Asset_Save_Animations":
await _threadWorkerView.Begin(cancellationToken =>
{
_applicationView.CUE4Parse.Extract(cancellationToken, contextViewModel.FullPath, false, EBulkType.Animations);
});
break;
case "Open_Properties":
if (contextViewModel.Header == "New Tab" || contextViewModel.Document == null) return;
Helper.OpenWindow<AdonisWindow>(contextViewModel.Header + " (Properties)", () =>
@ -40,9 +70,9 @@ public class TabCommand : ViewModelCommand<TabItem>
}.Show();
});
break;
case "Copy_Asset_Name":
Clipboard.SetText(contextViewModel.Header);
case "Copy_Asset_Path":
Clipboard.SetText(contextViewModel.FullPath);
break;
}
}
}
}

View File

@ -97,18 +97,20 @@ public class GameSelectorViewModel : ViewModel
private IEnumerable<DetectedGame> EnumerateDetectedGames()
{
yield return GetUnrealEngineGame("Fortnite", "\\FortniteGame\\Content\\Paks");
yield return GetUnrealEngineGame("Fortnite");
yield return new DetectedGame { GameName = "Fortnite [LIVE]", GameDirectory = Constants._FN_LIVE_TRIGGER };
yield return GetUnrealEngineGame("Pewee", "\\RogueCompany\\Content\\Paks");
yield return GetUnrealEngineGame("Rosemallow", "\\Indiana\\Content\\Paks");
yield return GetUnrealEngineGame("Catnip", "\\OakGame\\Content\\Paks");
yield return GetUnrealEngineGame("AzaleaAlpha", "\\Prospect\\Content\\Paks");
yield return GetUnrealEngineGame("WorldExplorersLive", "\\WorldExplorers\\Content");
yield return GetUnrealEngineGame("Newt", "\\g3\\Content\\Paks");
yield return GetUnrealEngineGame("shoebill", "\\SwGame\\Content\\Paks");
yield return GetUnrealEngineGame("Snoek", "\\StateOfDecay2\\Content\\Paks");
yield return GetUnrealEngineGame("a99769d95d8f400baad1f67ab5dfe508", "\\Core\\Platform\\Content\\Paks");
yield return GetUnrealEngineGame("Nebula", "\\BendGame\\Content");
yield return GetUnrealEngineGame("Pewee");
yield return GetUnrealEngineGame("Rosemallow");
yield return GetUnrealEngineGame("Catnip");
yield return GetUnrealEngineGame("AzaleaAlpha");
yield return GetUnrealEngineGame("WorldExplorersLive");
yield return GetUnrealEngineGame("Newt");
yield return GetUnrealEngineGame("shoebill");
yield return GetUnrealEngineGame("Snoek");
yield return GetUnrealEngineGame("a99769d95d8f400baad1f67ab5dfe508");
yield return GetUnrealEngineGame("Nebula");
yield return GetUnrealEngineGame("711c5e95dc094ca58e5f16bd48e751d6");
yield return GetUnrealEngineGame("9361c8c6d2f34b42b5f2f61093eedf48");
yield return GetRiotGame("VALORANT", "ShooterGame\\Content\\Paks");
yield return new DetectedGame { GameName = "Valorant [LIVE]", GameDirectory = Constants._VAL_LIVE_TRIGGER };
yield return GetMojangGame("MinecraftDungeons", "\\dungeons\\dungeons\\Dungeons\\Content\\Paks");
@ -116,13 +118,15 @@ public class GameSelectorViewModel : ViewModel
yield return GetSteamGame(578080, "\\TslGame\\Content\\Paks"); // PUBG
yield return GetSteamGame(677620, "\\PortalWars\\Content\\Paks"); // Splitgate
yield return GetSteamGame(1172620, "\\Athena\\Content\\Paks"); // Sea of Thieves
yield return GetSteamGame(1665460, "\\pak"); // eFootball 2023
yield return GetRockstarGamesGame("GTA III - Definitive Edition", "\\Gameface\\Content\\Paks");
yield return GetRockstarGamesGame("GTA San Andreas - Definitive Edition", "\\Gameface\\Content\\Paks");
yield return GetRockstarGamesGame("GTA Vice City - Definitive Edition", "\\Gameface\\Content\\Paks");
yield return GetLevelInfiniteGame("tof_launcher", "\\Hotta\\Content\\Paks");
}
private LauncherInstalled _launcherInstalled;
private DetectedGame GetUnrealEngineGame(string gameName, string pakDirectory)
private DetectedGame GetUnrealEngineGame(string gameName)
{
_launcherInstalled ??= GetDriveLauncherInstalls<LauncherInstalled>("ProgramData\\Epic\\UnrealEngineLauncher\\LauncherInstalled.dat");
if (_launcherInstalled?.InstallationList != null)
@ -130,7 +134,10 @@ public class GameSelectorViewModel : ViewModel
foreach (var installationList in _launcherInstalled.InstallationList)
{
if (installationList.AppName.Equals(gameName, StringComparison.OrdinalIgnoreCase))
return new DetectedGame { GameName = installationList.AppName, GameDirectory = $"{installationList.InstallLocation}{pakDirectory}" };
{
var pak = Directory.GetDirectories(installationList.InstallLocation, "Paks*", SearchOption.AllDirectories);
if (pak.Length > 0) return new DetectedGame { GameName = installationList.AppName, GameDirectory = pak[0] };
}
}
}
@ -195,6 +202,28 @@ public class GameSelectorViewModel : ViewModel
return null;
}
private DetectedGame GetLevelInfiniteGame(string key, string pakDirectory)
{
var installLocation = string.Empty;
var displayName = string.Empty;
try
{
installLocation = App.GetRegistryValue($@"Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{key}", "GameInstallPath", RegistryHive.CurrentUser);
displayName = App.GetRegistryValue($@"Software\Microsoft\Windows\CurrentVersion\Uninstall\{key}", "DisplayName", RegistryHive.CurrentUser);
}
catch
{
// ignored
}
if (!string.IsNullOrEmpty(installLocation))
return new DetectedGame { GameName = displayName, GameDirectory = $"{installLocation}{pakDirectory}" };
Log.Warning("Could not find {GameName} in the registry", key);
return null;
}
private T GetDriveLauncherInstalls<T>(string jsonFile)
{
foreach (var drive in DriveInfo.GetDrives())
@ -310,22 +339,14 @@ public class GameSelectorViewModel : ViewModel
dic[key] = val;
}
if (dic.Keys.Count <= 0) return null;
AppInfo appInfo = new();
var appId = dic["appid"];
var name = dic["name"];
var installDir = dic["installDir"];
if (!dic.TryGetValue("appid", out var appId) ||
!dic.TryGetValue("name", out var name) ||
!dic.TryGetValue("installDir", out var installDir)) return null;
var path = Path.GetDirectoryName(appMetaFile);
var libGameRoot = Path.Combine(path, "common", installDir);
if (!Directory.Exists(libGameRoot)) return null;
appInfo.Id = appId;
appInfo.Name = name;
appInfo.GameRoot = libGameRoot;
return appInfo;
return !Directory.Exists(libGameRoot) ? null : new AppInfo { Id = appId, Name = name, GameRoot = libGameRoot };
}
private static List<string> GetSteamLibs()
@ -365,4 +386,4 @@ public class GameSelectorViewModel : ViewModel
}
}
}
}
}

View File

@ -175,7 +175,7 @@ public class MapViewerViewModel : ViewModel
}
private const int _widthHeight = 2048;
private const int _brRadius = 135000;
private const int _brRadius = 141000;
private const int _prRadius = 51000;
private int _mapIndex;
public int MapIndex // 0 is BR, 1 is PR
@ -368,8 +368,9 @@ public class MapViewerViewModel : ViewModel
private FVector2D GetMapPosition(FVector vector, int mapRadius)
{
var nx = (vector.Y + mapRadius) / (mapRadius * 2) * _widthHeight;
var ny = (1 - (vector.X + mapRadius) / (mapRadius * 2)) * _widthHeight;
const int wh = 2048 + 128 + 32;
var nx = (vector.Y + mapRadius) / (mapRadius * 2) * wh;
var ny = (1 - (vector.X + mapRadius) / (mapRadius * 2)) * wh;
return new FVector2D(nx, ny);
}
@ -471,7 +472,7 @@ public class MapViewerViewModel : ViewModel
var patrolsPathBitmap = new SKBitmap(_widthHeight, _widthHeight, SKColorType.Rgba8888, SKAlphaType.Premul);
using var c = new SKCanvas(patrolsPathBitmap);
var exports = Utils.LoadExports("/NPCLibrary/LevelOverlays/Artemis_Overlay_S21_NPCLibrary");
var exports = Utils.LoadExports("/NPCLibrary/LevelOverlays/Artemis_Overlay_S22_NPCLibrary");
foreach (var export in exports)
{
if (!export.ExportType.Equals("FortAthenaPatrolPath", StringComparison.OrdinalIgnoreCase) ||
@ -822,7 +823,7 @@ public class MapViewerViewModel : ViewModel
var bountyBoardsBitmap = new SKBitmap(_widthHeight, _widthHeight, SKColorType.Rgba8888, SKAlphaType.Premul);
using var c = new SKCanvas(bountyBoardsBitmap);
var exports = Utils.LoadExports("/Bounties/Maps/BB_Overlay_S19_ServiceStations");
var exports = Utils.LoadExports("/Bounties/Maps/BB_Overlay_ServiceStations");
foreach (var export in exports)
{
if (!export.ExportType.Equals("B_Bounties_Spawner_BountyBoard_C", StringComparison.OrdinalIgnoreCase)) continue;

View File

@ -1,783 +0,0 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Numerics;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Data;
using System.Windows.Media.Media3D;
using CUE4Parse_Conversion;
using CUE4Parse.UE4.Assets;
using CUE4Parse.UE4.Assets.Exports;
using CUE4Parse.UE4.Assets.Exports.Material;
using CUE4Parse.UE4.Assets.Exports.SkeletalMesh;
using CUE4Parse.UE4.Assets.Exports.StaticMesh;
using CUE4Parse.UE4.Assets.Exports.Texture;
using CUE4Parse.UE4.Objects.Core.Math;
using CUE4Parse.Utils;
using CUE4Parse_Conversion.Materials;
using CUE4Parse_Conversion.Meshes;
using CUE4Parse_Conversion.Meshes.glTF;
using CUE4Parse_Conversion.Meshes.PSK;
using CUE4Parse_Conversion.Textures;
using FModel.Framework;
using FModel.Services;
using FModel.Settings;
using FModel.Views.Resources.Controls;
using HelixToolkit.SharpDX.Core;
using HelixToolkit.Wpf.SharpDX;
using Ookii.Dialogs.Wpf;
using Serilog;
using SharpDX;
using SharpGLTF.Geometry;
using SharpGLTF.Geometry.VertexTypes;
using SharpGLTF.Scenes;
using SharpGLTF.Schema2;
using SharpGLTF.Transforms;
using SkiaSharp;
using Camera = HelixToolkit.Wpf.SharpDX.Camera;
using Exporter = CUE4Parse_Conversion.Exporter;
using Geometry3D = HelixToolkit.SharpDX.Core.Geometry3D;
using PerspectiveCamera = HelixToolkit.Wpf.SharpDX.PerspectiveCamera;
using Vector2 = SharpDX.Vector2;
using Vector3 = SharpDX.Vector3;
using VERTEX = SharpGLTF.Geometry.VertexTypes.VertexPositionNormalTangent;
namespace FModel.ViewModels;
public class ModelViewerViewModel : ViewModel
{
private ThreadWorkerViewModel _threadWorkerView => ApplicationService.ThreadWorkerView;
private EffectsManager _effectManager;
public EffectsManager EffectManager
{
get => _effectManager;
set => SetProperty(ref _effectManager, value);
}
private Camera _cam;
public Camera Cam
{
get => _cam;
set => SetProperty(ref _cam, value);
}
private ModelAndCam _selectedModel; // selected mesh
public ModelAndCam SelectedModel
{
get => _selectedModel;
set => SetProperty(ref _selectedModel, value);
}
private readonly ObservableCollection<ModelAndCam> _loadedModels; // mesh list
public ICollectionView LoadedModelsView { get; }
private bool _appendMode;
public bool AppendMode
{
get => _appendMode;
set => SetProperty(ref _appendMode, value);
}
public bool CanAppend => SelectedModel != null;
public TextureModel HDRi { get; private set; }
private readonly FGame _game;
private readonly int[] _facesIndex = { 1, 0, 2 };
public ModelViewerViewModel(FGame game)
{
_game = game;
_loadedModels = new ObservableCollection<ModelAndCam>();
EffectManager = new DefaultEffectsManager();
LoadedModelsView = new ListCollectionView(_loadedModels);
Cam = new PerspectiveCamera { NearPlaneDistance = 0.1, FarPlaneDistance = double.PositiveInfinity, FieldOfView = 90 };
LoadHDRi();
}
private void LoadHDRi()
{
var cubeMap = Application.GetResourceStream(new Uri("/FModel;component/Resources/approaching_storm_cubemap.dds", UriKind.Relative));
HDRi = TextureModel.Create(cubeMap?.Stream);
}
public void LoadExport(UObject export)
{
#if DEBUG
LoadHDRi();
#endif
ModelAndCam p;
if (AppendMode && CanAppend)
{
p = SelectedModel;
_loadedModels.Add(new ModelAndCam(export) { IsVisible = false });
}
else
{
p = new ModelAndCam(export);
_loadedModels.Add(p);
}
switch (export)
{
case UStaticMesh st:
LoadStaticMesh(st, p);
break;
case USkeletalMesh sk:
LoadSkeletalMesh(sk, p);
break;
case UMaterialInstance mi:
LoadMaterialInstance(mi, p);
break;
default:
throw new ArgumentOutOfRangeException(nameof(export));
}
if (AppendMode && CanAppend) return;
SelectedModel = p;
Cam.UpDirection = new Vector3D(0, 1, 0);
Cam.Position = p.Position;
Cam.LookDirection = p.LookDirection;
}
#region PUBLIC METHODS
public void RenderingToggle()
{
if (SelectedModel == null) return;
SelectedModel.RenderingToggle = !SelectedModel.RenderingToggle;
}
public void WirefreameToggle()
{
if (SelectedModel == null) return;
SelectedModel.WireframeToggle = !SelectedModel.WireframeToggle;
}
public void MaterialColorToggle()
{
if (SelectedModel == null) return;
SelectedModel.ShowMaterialColor = !SelectedModel.ShowMaterialColor;
}
public void DiffuseOnlyToggle()
{
if (SelectedModel == null) return;
SelectedModel.DiffuseOnlyToggle = !SelectedModel.DiffuseOnlyToggle;
}
public void FocusOnSelectedMesh()
{
Cam.AnimateTo(SelectedModel.Position, SelectedModel.LookDirection, new Vector3D(0, 1, 0), 500);
}
public async Task SaveLoadedModels()
{
if (_loadedModels.Count < 1) return;
var folderBrowser = new VistaFolderBrowserDialog { ShowNewFolderButton = true };
if (folderBrowser.ShowDialog() == false) return;
await _threadWorkerView.Begin(_ =>
{
var exportOptions = new ExporterOptions
{
TextureFormat = UserSettings.Default.TextureExportFormat,
LodFormat = UserSettings.Default.LodExportFormat,
MeshFormat = UserSettings.Default.MeshExportFormat,
Platform = UserSettings.Default.OverridedPlatform,
ExportMorphTargets = UserSettings.Default.SaveMorphTargets
};
foreach (var model in _loadedModels)
{
var toSave = new Exporter(model.Export, exportOptions);
if (toSave.TryWriteToDir(new DirectoryInfo(folderBrowser.SelectedPath), out var savedFileName))
{
Log.Information("Successfully saved {FileName}", savedFileName);
FLogger.AppendInformation();
FLogger.AppendText($"Successfully saved {savedFileName}", Constants.WHITE, true);
}
else
{
Log.Error("{FileName} could not be saved", savedFileName);
FLogger.AppendError();
FLogger.AppendText($"Could not save '{savedFileName}'", Constants.WHITE, true);
}
}
});
}
public void SaveAsScene()
{
if (_loadedModels.Count < 1) return;
var fileBrowser = new VistaSaveFileDialog
{
Title = "Save Loaded Models As...",
DefaultExt = ".glb",
Filter = "glTF Binary File (*.glb)|*.glb|glTF ASCII File (*.gltf)|*.gltf|All Files(*.*)|*.*",
AddExtension = true,
OverwritePrompt = true,
CheckPathExists = true
};
if (fileBrowser.ShowDialog() == false || string.IsNullOrEmpty(fileBrowser.FileName)) return;
var sceneBuilder = new SceneBuilder();
var materialExports = new List<MaterialExporter>();
foreach (var model in _loadedModels)
{
switch (model.Export)
{
case UStaticMesh sm:
{
var mesh = new MeshBuilder<VERTEX, VertexColorXTextureX, VertexEmpty>(sm.Name);
if (sm.TryConvert(out var convertedMesh) && convertedMesh.LODs.Count > 0)
{
var lod = convertedMesh.LODs.First();
for (var i = 0; i < lod.Sections.Value.Length; i++)
{
Gltf.ExportStaticMeshSections(i, lod, lod.Sections.Value[i], materialExports, mesh);
}
sceneBuilder.AddRigidMesh(mesh, AffineTransform.Identity);
}
break;
}
case USkeletalMesh sk:
{
var mesh = new MeshBuilder<VERTEX, VertexColorXTextureX, VertexJoints4>(sk.Name);
if (sk.TryConvert(out var convertedMesh) && convertedMesh.LODs.Count > 0)
{
var lod = convertedMesh.LODs.First();
for (var i = 0; i < lod.Sections.Value.Length; i++)
{
Gltf.ExportSkelMeshSections(i, lod, lod.Sections.Value[i], materialExports, mesh);
}
var armatureNodeBuilder = new NodeBuilder(sk.Name + ".ao");
var armature = Gltf.CreateGltfSkeleton(convertedMesh.RefSkeleton, armatureNodeBuilder);
sceneBuilder.AddSkinnedMesh(mesh, Matrix4x4.Identity, armature);
}
break;
}
}
}
var scene = sceneBuilder.ToGltf2();
var fileName = fileBrowser.FileName;
if (fileName.EndsWith(".glb", StringComparison.OrdinalIgnoreCase))
scene.SaveGLB(fileName);
else if (fileName.EndsWith(".gltf", StringComparison.OrdinalIgnoreCase))
scene.SaveGLTF(fileName);
else if (fileName.EndsWith(".obj", StringComparison.OrdinalIgnoreCase))
scene.SaveAsWavefront(fileName);
else
throw new ArgumentOutOfRangeException(nameof(fileName), $@"Unknown file format {fileName.SubstringAfterWithLast('.')}");
if (!CheckIfSaved(fileName)) return;
foreach (var materialExport in materialExports)
{
materialExport.TryWriteToDir(new DirectoryInfo(StringUtils.SubstringBeforeWithLast(fileName, '\\')), out _);
}
}
public void CopySelectedMaterialName()
{
if (SelectedModel is not { } m || m.SelectedGeometry is null)
return;
Clipboard.SetText(m.SelectedGeometry.DisplayName.TrimEnd());
}
#endregion
public bool TryOverwriteMaterial(UMaterialInstance materialInstance)
{
if (SelectedModel?.SelectedGeometry == null || _loadedModels.Count < 1) return false;
var (m, _, _) = LoadMaterial(materialInstance);
var obj = new ResolvedLoadedObject(materialInstance);
switch (_loadedModels[SelectedModel.SelectedGeometry.ExportIndex].Export)
{
case UStaticMesh { Materials: { } } st:
st.Materials[SelectedModel.SelectedGeometry.MaterialIndex] = obj;
break;
case USkeletalMesh sk:
sk.Materials[SelectedModel.SelectedGeometry.MaterialIndex].Material = obj;
break;
case UMaterialInstance:
SelectedModel.SwapExport(materialInstance);
break;
}
SelectedModel.SelectedGeometry.Material = m;
return m != null;
}
private void LoadMaterialInstance(UMaterialInstance materialInstance, ModelAndCam cam)
{
var builder = new MeshBuilder();
builder.AddBox(Vector3.Zero, 10, 10, 10);
cam.TriangleCount = 12; // no need to count
SetupCameraAndAxis(new FBox(new FVector(-8), new FVector(8)), cam);
var (m, isRendering, isTransparent) = LoadMaterial(materialInstance);
cam.Group3d.Add(new CustomMeshGeometryModel3D
{
Transform = new RotateTransform3D(new AxisAngleRotation3D(new Vector3D(0, 1, 0), -45)),
DisplayName = materialInstance.Name, Geometry = builder.ToMeshGeometry3D(), MaterialIndex = 0,
Material = m, IsTransparent = isTransparent, IsRendering = isRendering, ExportIndex = _loadedModels.Count - 1
});
}
private void LoadStaticMesh(UStaticMesh mesh, ModelAndCam cam)
{
if (!mesh.TryConvert(out var convertedMesh) || convertedMesh.LODs.Count <= 0)
{
return;
}
SetupCameraAndAxis(convertedMesh.BoundingBox, cam);
foreach (var lod in convertedMesh.LODs.Where(lod => !lod.SkipLod))
{
PushLod(lod.Sections.Value, lod.Verts, lod.Indices.Value, cam);
break;
}
}
private void LoadSkeletalMesh(USkeletalMesh mesh, ModelAndCam cam)
{
if (!mesh.TryConvert(out var convertedMesh) || convertedMesh.LODs.Count <= 0)
{
return;
}
SetupCameraAndAxis(convertedMesh.BoundingBox, cam);
foreach (var lod in convertedMesh.LODs.Where(lod => !lod.SkipLod))
{
PushLod(lod.Sections.Value, lod.Verts, lod.Indices.Value, cam);
break;
}
}
private void PushLod(CMeshSection[] sections, CMeshVertex[] verts, FRawStaticIndexBuffer indices, ModelAndCam cam)
{
for (var i = 0; i < sections.Length; i++) // each section is a mesh part with its own material
{
var section = sections[i];
var builder = new MeshBuilder();
cam.TriangleCount += section.NumFaces; // NumFaces * 3 (triangle) = next section FirstIndex
for (var j = 0; j < section.NumFaces; j++) // draw a triangle for each face
{
foreach (var t in _facesIndex) // triangle face 1 then 0 then 2
{
var id = section.FirstIndex + j * 3 + t;
var vert = verts[indices[id]];
var p = new Vector3(vert.Position.X, vert.Position.Z, vert.Position.Y); // up direction is Y
var n = new Vector3(vert.Normal.X, vert.Normal.Z, vert.Normal.Y);
n.Normalize();
builder.AddNode(p, n, new Vector2(vert.UV.U, vert.UV.V));
builder.TriangleIndices.Add(j * 3 + t); // one mesh part is "j * 3 + t" use "id" if you're building the full mesh
}
}
if (section.Material == null || !section.Material.TryLoad(out var o) || o is not UMaterialInterface material)
{
cam.Group3d.Add(new CustomMeshGeometryModel3D
{
DisplayName = section.Material?.Name.ToString() ?? $"material_{section.MaterialIndex}", MaterialIndex = section.MaterialIndex,
Geometry = builder.ToMeshGeometry3D(), Material = new PBRMaterial(), IsTransparent = false,
IsRendering = true, ExportIndex = _loadedModels.Count - 1
});
}
else
{
var (m, isRendering, isTransparent) = LoadMaterial(material);
cam.Group3d.Add(new CustomMeshGeometryModel3D
{
DisplayName = material.Name, MaterialIndex = section.MaterialIndex,
Geometry = builder.ToMeshGeometry3D(), Material = m, IsTransparent = isTransparent,
IsRendering = isRendering, ExportIndex = _loadedModels.Count - 1
});
}
}
}
private (PBRMaterial material, bool isRendering, bool isTransparent) LoadMaterial(UMaterialInterface unrealMaterial)
{
var m = new PBRMaterial { RenderShadowMap = true, EnableAutoTangent = true, RenderEnvironmentMap = true };
var parameters = new CMaterialParams();
unrealMaterial.GetParams(parameters);
var isRendering = !parameters.IsNull;
if (isRendering)
{
if (!parameters.HasTopDiffuseTexture && parameters.DiffuseColor is { A: > 0 } diffuseColor)
{
m.AlbedoColor = new Color4(diffuseColor.R, diffuseColor.G, diffuseColor.B, diffuseColor.A);
}
else if (parameters.Diffuse is UTexture2D diffuse)
{
m.AlbedoMap = new TextureModel(diffuse.Decode(UserSettings.Default.OverridedPlatform)?.Encode(SKEncodedImageFormat.Png, 100).AsStream());
}
if (parameters.Normal is UTexture2D normal)
{
m.NormalMap = new TextureModel(normal.Decode(UserSettings.Default.OverridedPlatform)?.Encode(SKEncodedImageFormat.Png, 100).AsStream());
}
if (parameters.Specular is UTexture2D specular)
{
var mip = specular.GetFirstMip();
byte[] data;
SKColorType colorType;
switch (UserSettings.Default.OverridedPlatform)
{
case ETexturePlatform.Playstation:
PlaystationDecoder.DecodeTexturePlaystation(mip, specular.Format, specular.isNormalMap,
out data, out colorType);
break;
case ETexturePlatform.NintendoSwitch:
NintendoSwitchDecoder.DecodeTextureNSW(mip, specular.Format, specular.isNormalMap,
out data, out colorType);
break;
default:
TextureDecoder.DecodeTexture(mip, specular.Format, specular.isNormalMap,
out data, out colorType);
break;
}
switch (_game)
{
case FGame.FortniteGame:
{
// Fortnite's Specular Texture Channels
// R Specular
// G Metallic
// B Roughness
unsafe
{
var offset = 0;
fixed (byte* d = data)
{
for (var i = 0; i < mip.SizeX * mip.SizeY; i++)
{
d[offset] = 0;
(d[offset + 1], d[offset + 2]) = (d[offset + 2], d[offset + 1]); // swap G and B
offset += 4;
}
}
}
parameters.RoughnessValue = 1;
parameters.MetallicValue = 1;
break;
}
case FGame.ShooterGame:
{
var packedPBRType = specular.Name[(specular.Name.LastIndexOf('_') + 1)..];
switch (packedPBRType)
{
case "MRAE": // R: Metallic, G: AO (0-127) & Emissive (128-255), B: Roughness (Character PBR)
unsafe
{
var offset = 0;
fixed (byte* d = data)
{
for (var i = 0; i < mip.SizeX * mip.SizeY; i++)
{
(d[offset], d[offset + 2]) = (d[offset + 2], d[offset]); // swap R and B
(d[offset], d[offset + 1]) = (d[offset + 1], d[offset]); // swap R and G
offset += 4;
}
}
}
break;
case "MRAS": // R: Metallic, B: Roughness, B: AO, A: Specular (Legacy PBR)
case "MRA": // R: Metallic, B: Roughness, B: AO (Environment PBR)
case "MRS": // R: Metallic, G: Roughness, B: Specular (Weapon PBR)
unsafe
{
var offset = 0;
fixed (byte* d = data)
{
for (var i = 0; i < mip.SizeX * mip.SizeY; i++)
{
(d[offset], d[offset + 2]) = (d[offset + 2], d[offset]); // swap R and B
offset += 4;
}
}
}
break;
}
parameters.RoughnessValue = 1;
parameters.MetallicValue = 1;
break;
}
case FGame.Gameface:
{
// GTA's Specular Texture Channels
// R Metallic
// G Roughness
// B Specular
unsafe
{
var offset = 0;
fixed (byte* d = data)
{
for (var i = 0; i < mip.SizeX * mip.SizeY; i++)
{
(d[offset], d[offset + 2]) = (d[offset + 2], d[offset]); // swap R and B
offset += 4;
}
}
}
break;
}
}
using var bitmap = new SKBitmap(new SKImageInfo(mip.SizeX, mip.SizeY, colorType, SKAlphaType.Unpremul));
unsafe
{
fixed (byte* p = data)
{
bitmap.SetPixels(new IntPtr(p));
}
}
// R -> AO G -> Roughness B -> Metallic
m.RoughnessMetallicMap = new TextureModel(bitmap.Encode(SKEncodedImageFormat.Png, 100).AsStream());
m.RoughnessFactor = parameters.RoughnessValue;
m.MetallicFactor = parameters.MetallicValue;
m.RenderAmbientOcclusionMap = parameters.SpecularValue > 0;
}
if (parameters.HasTopEmissiveTexture && parameters.Emissive is UTexture2D emissive && parameters.EmissiveColor is { A: > 0 } emissiveColor)
{
m.EmissiveColor = new Color4(emissiveColor.R, emissiveColor.G, emissiveColor.B, emissiveColor.A);
m.EmissiveMap = new TextureModel(emissive.Decode(UserSettings.Default.OverridedPlatform)?.Encode(SKEncodedImageFormat.Png, 100).AsStream());
}
}
else
{
m.AlbedoColor = new Color4(1, 0, 0, 1);
}
return (m, isRendering, parameters.IsTransparent);
}
private void SetupCameraAndAxis(FBox box, ModelAndCam cam)
{
if (AppendMode && CanAppend) return;
var center = box.GetCenter();
var lineBuilder = new LineBuilder();
lineBuilder.AddLine(new Vector3(box.Min.X, center.Z, center.Y), new Vector3(box.Max.X, center.Z, center.Y));
cam.XAxis = lineBuilder.ToLineGeometry3D();
lineBuilder = new LineBuilder();
lineBuilder.AddLine(new Vector3(center.X, box.Min.Z, center.Y), new Vector3(center.X, box.Max.Z, center.Y));
cam.YAxis = lineBuilder.ToLineGeometry3D();
lineBuilder = new LineBuilder();
lineBuilder.AddLine(new Vector3(center.X, center.Z, box.Min.Y), new Vector3(center.X, center.Z, box.Max.Y));
cam.ZAxis = lineBuilder.ToLineGeometry3D();
cam.Position = new Point3D(box.Max.X + center.X * 2, center.Z, box.Min.Y + center.Y * 2);
cam.LookDirection = new Vector3D(-cam.Position.X + center.X, 0, -cam.Position.Z + center.Y);
}
private bool CheckIfSaved(string path)
{
if (File.Exists(path))
{
Log.Information("Successfully saved {FileName}", path);
FLogger.AppendInformation();
FLogger.AppendText($"Successfully saved {path}", Constants.WHITE, true);
return true;
}
Log.Error("{FileName} could not be saved", path);
FLogger.AppendError();
FLogger.AppendText($"Could not save '{path}'", Constants.WHITE, true);
return false;
}
public void Clear()
{
foreach (var g in _loadedModels.ToList())
{
g.Dispose();
_loadedModels.Remove(g);
}
}
}
public class ModelAndCam : ViewModel
{
public UObject Export { get; private set; }
public Point3D Position { get; set; }
public Vector3D LookDirection { get; set; }
public Geometry3D XAxis { get; set; }
public Geometry3D YAxis { get; set; }
public Geometry3D ZAxis { get; set; }
public int TriangleCount { get; set; }
private bool _isVisible = true;
public bool IsVisible
{
get => _isVisible;
set => SetProperty(ref _isVisible, value);
}
private bool _renderingToggle;
public bool RenderingToggle
{
get => _renderingToggle;
set
{
SetProperty(ref _renderingToggle, value);
foreach (var g in Group3d)
{
if (g is not CustomMeshGeometryModel3D geometryModel)
continue;
geometryModel.IsRendering = !geometryModel.IsRendering;
}
}
}
private bool _wireframeToggle;
public bool WireframeToggle
{
get => _wireframeToggle;
set
{
SetProperty(ref _wireframeToggle, value);
foreach (var g in Group3d)
{
if (g is not CustomMeshGeometryModel3D geometryModel)
continue;
geometryModel.RenderWireframe = !geometryModel.RenderWireframe;
}
}
}
private bool _showMaterialColor;
public bool ShowMaterialColor
{
get => _showMaterialColor;
set
{
SetProperty(ref _showMaterialColor, value);
for (var i = 0; i < Group3d.Count; i++)
{
if (Group3d[i] is not CustomMeshGeometryModel3D { Material: PBRMaterial material } m)
continue;
var index = B(i);
material.RenderAlbedoMap = !_showMaterialColor;
if (_showMaterialColor)
{
m.Tag = material.AlbedoColor;
material.AlbedoColor = new Color4(_table[C(index)] / 255, _table[C(index >> 1)] / 255, _table[C(index >> 2)] / 255, 1);
}
else material.AlbedoColor = (Color4) m.Tag;
}
}
}
private bool _diffuseOnlyToggle;
public bool DiffuseOnlyToggle
{
get => _diffuseOnlyToggle;
set
{
SetProperty(ref _diffuseOnlyToggle, value);
foreach (var g in Group3d)
{
if (g is not CustomMeshGeometryModel3D { Material: PBRMaterial material })
continue;
material.RenderAmbientOcclusionMap = !material.RenderAmbientOcclusionMap;
material.RenderDisplacementMap = !material.RenderDisplacementMap;
// material.RenderEmissiveMap = !material.RenderEmissiveMap;
// material.RenderEnvironmentMap = !material.RenderEnvironmentMap;
material.RenderIrradianceMap = !material.RenderIrradianceMap;
material.RenderRoughnessMetallicMap = !material.RenderRoughnessMetallicMap;
material.RenderShadowMap = !material.RenderShadowMap;
material.RenderNormalMap = !material.RenderNormalMap;
}
}
}
private CustomMeshGeometryModel3D _selectedGeometry; // selected material
public CustomMeshGeometryModel3D SelectedGeometry
{
get => _selectedGeometry;
set => SetProperty(ref _selectedGeometry, value);
}
private ObservableElement3DCollection _group3d; // material list
public ObservableElement3DCollection Group3d
{
get => _group3d;
set => SetProperty(ref _group3d, value);
}
private readonly float[] _table = { 255 * 0.9f, 25 * 3.0f, 255 * 0.6f, 255 * 0.0f };
private readonly int[] _table2 = { 0, 1, 2, 4, 7, 3, 5, 6 };
public ModelAndCam(UObject export)
{
Export = export;
TriangleCount = 0;
Group3d = new ObservableElement3DCollection();
}
private int B(int x) => (x & 0xFFF8) | _table2[x & 7] ^ 7;
private int C(int x) => (x & 1) | ((x >> 2) & 2);
public void SwapExport(UObject e)
{
Export = e;
}
public void Dispose()
{
TriangleCount = 0;
SelectedGeometry = null;
foreach (var g in Group3d.ToList())
{
g.Dispose();
Group3d.Remove(g);
}
}
}
public class CustomMeshGeometryModel3D : MeshGeometryModel3D
{
public string DisplayName { get; set; }
public int MaterialIndex { get; set; }
public int ExportIndex { get; set; }
}

View File

@ -8,6 +8,7 @@ using CUE4Parse.UE4.Objects.Core.Serialization;
using CUE4Parse.UE4.Versions;
using CUE4Parse_Conversion.Meshes;
using CUE4Parse_Conversion.Textures;
using CUE4Parse.UE4.Assets.Exports.Material;
using FModel.Extensions;
using FModel.Framework;
using FModel.Services;
@ -22,6 +23,13 @@ public class SettingsViewModel : ViewModel
private ApiEndpointViewModel _apiEndpointView => ApplicationService.ApiEndpointView;
private readonly DiscordHandler _discordHandler = DiscordService.DiscordHandler;
private bool _useCustomOutputFolders;
public bool UseCustomOutputFolders
{
get => _useCustomOutputFolders;
set => SetProperty(ref _useCustomOutputFolders, value);
}
private EUpdateMode _selectedUpdateMode;
public EUpdateMode SelectedUpdateMode
{
@ -68,6 +76,20 @@ public class SettingsViewModel : ViewModel
set => SetProperty(ref _selectedOptions, value);
}
private FEndpoint _aesEndpoint;
public FEndpoint AesEndpoint
{
get => _aesEndpoint;
set => SetProperty(ref _aesEndpoint, value);
}
private FEndpoint _mappingEndpoint;
public FEndpoint MappingEndpoint
{
get => _mappingEndpoint;
set => SetProperty(ref _mappingEndpoint, value);
}
private ELanguage _selectedAssetLanguage;
public ELanguage SelectedAssetLanguage
{
@ -110,6 +132,13 @@ public class SettingsViewModel : ViewModel
set => SetProperty(ref _selectedMeshExportFormat, value);
}
private ESocketFormat _selectedSocketExportFormat;
public ESocketFormat SelectedSocketExportFormat
{
get => _selectedSocketExportFormat;
set => SetProperty(ref _selectedSocketExportFormat, value);
}
private ELodFormat _selectedLodExportFormat;
public ELodFormat SelectedLodExportFormat
{
@ -117,6 +146,13 @@ public class SettingsViewModel : ViewModel
set => SetProperty(ref _selectedLodExportFormat, value);
}
private EMaterialFormat _selectedMaterialExportFormat;
public EMaterialFormat SelectedMaterialExportFormat
{
get => _selectedMaterialExportFormat;
set => SetProperty(ref _selectedMaterialExportFormat, value);
}
private ETextureFormat _selectedTextureExportFormat;
public ETextureFormat SelectedTextureExportFormat
{
@ -133,7 +169,9 @@ public class SettingsViewModel : ViewModel
public ReadOnlyObservableCollection<ECompressedAudio> CompressedAudios { get; private set; }
public ReadOnlyObservableCollection<EIconStyle> CosmeticStyles { get; private set; }
public ReadOnlyObservableCollection<EMeshFormat> MeshExportFormats { get; private set; }
public ReadOnlyObservableCollection<ESocketFormat> SocketExportFormats { get; private set; }
public ReadOnlyObservableCollection<ELodFormat> LodExportFormats { get; private set; }
public ReadOnlyObservableCollection<EMaterialFormat> MaterialExportFormats { get; private set; }
public ReadOnlyObservableCollection<ETextureFormat> TextureExportFormats { get; private set; }
public ReadOnlyObservableCollection<ETexturePlatform> Platforms { get; private set; }
@ -158,9 +196,13 @@ public class SettingsViewModel : ViewModel
private ECompressedAudio _compressedAudioSnapshot;
private EIconStyle _cosmeticStyleSnapshot;
private EMeshFormat _meshExportFormatSnapshot;
private ESocketFormat _socketExportFormatSnapshot;
private ELodFormat _lodExportFormatSnapshot;
private EMaterialFormat _materialExportFormatSnapshot;
private ETextureFormat _textureExportFormatSnapshot;
private bool _mappingsUpdate = false;
public SettingsViewModel(FGame game)
{
_game = game;
@ -191,11 +233,24 @@ public class SettingsViewModel : ViewModel
_optionsSnapshot = UserSettings.Default.OverridedOptions[_game];
}
if (UserSettings.Default.CustomEndpoints.TryGetValue(_game, out var endpoints))
{
AesEndpoint = endpoints[0];
MappingEndpoint = endpoints[1];
MappingEndpoint.PropertyChanged += (_, args) =>
{
if (!_mappingsUpdate)
_mappingsUpdate = args.PropertyName is "Overwrite" or "FilePath";
};
}
_assetLanguageSnapshot = UserSettings.Default.AssetLanguage;
_compressedAudioSnapshot = UserSettings.Default.CompressedAudioMode;
_cosmeticStyleSnapshot = UserSettings.Default.CosmeticStyle;
_meshExportFormatSnapshot = UserSettings.Default.MeshExportFormat;
_socketExportFormatSnapshot = UserSettings.Default.SocketExportFormat;
_lodExportFormatSnapshot = UserSettings.Default.LodExportFormat;
_materialExportFormatSnapshot = UserSettings.Default.MaterialExportFormat;
_textureExportFormatSnapshot = UserSettings.Default.TextureExportFormat;
SelectedUpdateMode = _updateModeSnapshot;
@ -208,7 +263,9 @@ public class SettingsViewModel : ViewModel
SelectedCompressedAudio = _compressedAudioSnapshot;
SelectedCosmeticStyle = _cosmeticStyleSnapshot;
SelectedMeshExportFormat = _meshExportFormatSnapshot;
SelectedSocketExportFormat = _socketExportFormatSnapshot;
SelectedLodExportFormat = _lodExportFormatSnapshot;
SelectedMaterialExportFormat = _materialExportFormatSnapshot;
SelectedTextureExportFormat = _textureExportFormatSnapshot;
SelectedAesReload = UserSettings.Default.AesReload;
SelectedDiscordRpc = UserSettings.Default.DiscordRpc;
@ -222,7 +279,9 @@ public class SettingsViewModel : ViewModel
CompressedAudios = new ReadOnlyObservableCollection<ECompressedAudio>(new ObservableCollection<ECompressedAudio>(EnumerateCompressedAudios()));
CosmeticStyles = new ReadOnlyObservableCollection<EIconStyle>(new ObservableCollection<EIconStyle>(EnumerateCosmeticStyles()));
MeshExportFormats = new ReadOnlyObservableCollection<EMeshFormat>(new ObservableCollection<EMeshFormat>(EnumerateMeshExportFormat()));
SocketExportFormats = new ReadOnlyObservableCollection<ESocketFormat>(new ObservableCollection<ESocketFormat>(EnumerateSocketExportFormat()));
LodExportFormats = new ReadOnlyObservableCollection<ELodFormat>(new ObservableCollection<ELodFormat>(EnumerateLodExportFormat()));
MaterialExportFormats = new ReadOnlyObservableCollection<EMaterialFormat>(new ObservableCollection<EMaterialFormat>(EnumerateMaterialExportFormat()));
TextureExportFormats = new ReadOnlyObservableCollection<ETextureFormat>(new ObservableCollection<ETextureFormat>(EnumerateTextureExportFormat()));
Platforms = new ReadOnlyObservableCollection<ETexturePlatform>(new ObservableCollection<ETexturePlatform>(EnumerateUePlatforms()));
}
@ -267,9 +326,17 @@ public class SettingsViewModel : ViewModel
SelectedOptions = _optionsSnapshot;
}
public SettingsOut Save()
public bool Save(out List<SettingsOut> whatShouldIDo)
{
var ret = SettingsOut.Nothing;
var restart = false;
whatShouldIDo = new List<SettingsOut>();
if (_assetLanguageSnapshot != SelectedAssetLanguage)
whatShouldIDo.Add(SettingsOut.ReloadLocres);
if (_mappingsUpdate)
whatShouldIDo.Add(SettingsOut.ReloadMappings);
if (_updateModeSnapshot != SelectedUpdateMode)
whatShouldIDo.Add(SettingsOut.CheckForUpdates);
if (_ueGameSnapshot != SelectedUeGame || _customVersionsSnapshot != SelectedCustomVersions ||
_uePlatformSnapshot != SelectedUePlatform || _optionsSnapshot != SelectedOptions || // combobox
@ -280,13 +347,7 @@ public class SettingsViewModel : ViewModel
_audioSnapshot != UserSettings.Default.AudioDirectory || // textbox
_modelSnapshot != UserSettings.Default.ModelDirectory || // textbox
_gameSnapshot != UserSettings.Default.GameDirectory) // textbox
ret = SettingsOut.Restart;
if (_assetLanguageSnapshot != SelectedAssetLanguage)
ret = SettingsOut.ReloadLocres;
if (_updateModeSnapshot != SelectedUpdateMode)
ret = SettingsOut.CheckForUpdates;
restart = true;
UserSettings.Default.UpdateMode = SelectedUpdateMode;
UserSettings.Default.Presets[_game] = SelectedPreset;
@ -308,7 +369,9 @@ public class SettingsViewModel : ViewModel
UserSettings.Default.CompressedAudioMode = SelectedCompressedAudio;
UserSettings.Default.CosmeticStyle = SelectedCosmeticStyle;
UserSettings.Default.MeshExportFormat = SelectedMeshExportFormat;
UserSettings.Default.SocketExportFormat = SelectedSocketExportFormat;
UserSettings.Default.LodExportFormat = SelectedLodExportFormat;
UserSettings.Default.MaterialExportFormat = SelectedMaterialExportFormat;
UserSettings.Default.TextureExportFormat = SelectedTextureExportFormat;
UserSettings.Default.AesReload = SelectedAesReload;
UserSettings.Default.DiscordRpc = SelectedDiscordRpc;
@ -316,7 +379,7 @@ public class SettingsViewModel : ViewModel
if (SelectedDiscordRpc == EDiscordRpc.Never)
_discordHandler.Shutdown();
return ret;
return restart;
}
private IEnumerable<EUpdateMode> EnumerateUpdateModes() => Enum.GetValues<EUpdateMode>();
@ -331,7 +394,9 @@ public class SettingsViewModel : ViewModel
private IEnumerable<ECompressedAudio> EnumerateCompressedAudios() => Enum.GetValues<ECompressedAudio>();
private IEnumerable<EIconStyle> EnumerateCosmeticStyles() => Enum.GetValues<EIconStyle>();
private IEnumerable<EMeshFormat> EnumerateMeshExportFormat() => Enum.GetValues<EMeshFormat>();
private IEnumerable<ESocketFormat> EnumerateSocketExportFormat() => Enum.GetValues<ESocketFormat>();
private IEnumerable<ELodFormat> EnumerateLodExportFormat() => Enum.GetValues<ELodFormat>();
private IEnumerable<EMaterialFormat> EnumerateMaterialExportFormat() => Enum.GetValues<EMaterialFormat>();
private IEnumerable<ETextureFormat> EnumerateTextureExportFormat() => Enum.GetValues<ETextureFormat>();
private IEnumerable<ETexturePlatform> EnumerateUePlatforms() => Enum.GetValues<ETexturePlatform>();
}

View File

@ -17,6 +17,7 @@ using System.Windows;
using System.Windows.Media.Imaging;
using CUE4Parse.UE4.Assets.Exports.Texture;
using CUE4Parse_Conversion.Textures;
using Ookii.Dialogs.Wpf;
namespace FModel.ViewModels;
@ -63,6 +64,12 @@ public class TabImage : ViewModel
private void SetImage(SKBitmap bitmap)
{
if (bitmap is null)
{
ImageBuffer = null;
Image = null;
return;
}
_bmp = bitmap;
using var data = _bmp.Encode(NoAlpha ? SKEncodedImageFormat.Jpeg : SKEncodedImageFormat.Png, 100);
using var stream = new MemoryStream(ImageBuffer = data.ToArray(), false);
@ -95,6 +102,8 @@ public class TabItem : ViewModel
set => SetProperty(ref _directory, value);
}
public string FullPath => this.Directory + "/" + this.Header;
private bool _hasSearchOpen;
public bool HasSearchOpen
{
@ -221,19 +230,20 @@ public class TabItem : ViewModel
});
}
public void AddImage(UTexture2D texture) => AddImage(texture.Name, texture.bRenderNearestNeighbor, texture.Decode(UserSettings.Default.OverridedPlatform));
public void AddImage(UTexture2D texture, bool bulkTexture)
=> AddImage(texture.Name, texture.bRenderNearestNeighbor, texture.Decode(UserSettings.Default.OverridedPlatform), bulkTexture);
public void AddImage(string name, bool rnn, SKBitmap[] img)
public void AddImage(string name, bool rnn, SKBitmap[] img, bool bulkTexture)
{
foreach (var i in img) AddImage(name, rnn, i);
foreach (var i in img) AddImage(name, rnn, i, bulkTexture);
}
public void AddImage(string name, bool rnn, SKBitmap img)
public void AddImage(string name, bool rnn, SKBitmap img, bool bulkTexture)
{
Application.Current.Dispatcher.Invoke(() =>
{
var t = new TabImage(name, rnn, img);
if (UserSettings.Default.IsAutoSaveTextures)
if (bulkTexture)
SaveImage(t, true);
_images.Add(t);
@ -253,7 +263,7 @@ public class TabItem : ViewModel
Document ??= new TextDocument();
Document.Text = text;
if (UserSettings.Default.IsAutoSaveProps || bulkSave)
if (bulkSave)
SaveProperty(true);
});
}
@ -267,16 +277,45 @@ public class TabItem : ViewModel
});
}
public void SaveImage(bool autoSave) => SaveImage(SelectedImage, autoSave);
public void SaveImages(bool bulkTexture)
{
switch (_images.Count)
{
case 1:
SaveImage(bulkTexture);
break;
case > 1:
var directory = Path.Combine(UserSettings.Default.TextureDirectory,
UserSettings.Default.KeepDirectoryStructure ? Directory : "").Replace('\\', '/');
private void SaveImage(TabImage image, bool autoSave)
if (!bulkTexture)
{
var folderBrowser = new VistaFolderBrowserDialog();
if (folderBrowser.ShowDialog() == true)
directory = folderBrowser.SelectedPath;
else return;
}
else System.IO.Directory.CreateDirectory(directory);
foreach (var image in _images)
{
if (image == null) return;
var fileName = $"{image.ExportName}.png";
SaveImage(image, Path.Combine(directory, fileName), fileName);
}
break;
}
}
public void SaveImage(bool bulkTexture) => SaveImage(SelectedImage, bulkTexture);
private void SaveImage(TabImage image, bool bulkTexture)
{
if (image == null) return;
var fileName = $"{image.ExportName}.png";
var directory = Path.Combine(UserSettings.Default.TextureDirectory,
var path = Path.Combine(UserSettings.Default.TextureDirectory,
UserSettings.Default.KeepDirectoryStructure ? Directory : "", fileName!).Replace('\\', '/');
if (!autoSave)
if (!bulkTexture)
{
var saveFileDialog = new SaveFileDialog
{
@ -287,19 +326,23 @@ public class TabItem : ViewModel
};
var result = saveFileDialog.ShowDialog();
if (!result.HasValue || !result.Value) return;
directory = saveFileDialog.FileName;
}
else
{
System.IO.Directory.CreateDirectory(directory.SubstringBeforeLast('/'));
path = saveFileDialog.FileName;
}
else System.IO.Directory.CreateDirectory(path.SubstringBeforeLast('/'));
using (var fs = new FileStream(directory, FileMode.Create, FileAccess.Write, FileShare.Read))
{
fs.Write(image.ImageBuffer, 0, image.ImageBuffer.Length);
}
SaveImage(image, path, fileName);
}
SaveCheck(directory, fileName);
private void SaveImage(TabImage image, string path, string fileName)
{
SaveImage(image, path);
SaveCheck(path, fileName);
}
private void SaveImage(TabImage image, string path)
{
using var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read);
fs.Write(image.ImageBuffer, 0, image.ImageBuffer.Length);
}
public void SaveProperty(bool autoSave)
@ -451,4 +494,4 @@ public class TabControlViewModel : ViewModel
{
yield return new TabItem("New Tab", string.Empty);
}
}
}

View File

@ -1,7 +1,6 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using FModel.Extensions;
using FModel.Framework;
using FModel.Services;
using FModel.Views.Resources.Controls;
@ -41,6 +40,10 @@ public class ThreadWorkerViewModel : ViewModel
private ApplicationViewModel _applicationView => ApplicationService.ApplicationView;
private readonly AsyncQueue<Action<CancellationToken>> _jobs;
private const string _at = " at ";
private const char _dot = '.';
private const char _colon = ':';
private const string _gray = "#999";
public ThreadWorkerViewModel()
{
@ -49,7 +52,7 @@ public class ThreadWorkerViewModel : ViewModel
public async Task Begin(Action<CancellationToken> action)
{
if (!_applicationView.IsReady)
if (!_applicationView.Status.IsReady)
{
SignalOperationInProgress();
return;
@ -75,7 +78,7 @@ public class ThreadWorkerViewModel : ViewModel
{
if (_jobs.Count > 0)
{
_applicationView.Status = EStatusKind.Loading;
_applicationView.Status.SetStatus(EStatusKind.Loading);
await foreach (var job in _jobs)
{
try
@ -85,7 +88,7 @@ public class ThreadWorkerViewModel : ViewModel
}
catch (OperationCanceledException)
{
_applicationView.Status = EStatusKind.Stopped;
_applicationView.Status.SetStatus(EStatusKind.Stopped);
CurrentCancellationTokenSource = null; // kill token
OperationCancelled = true;
OperationCancelled = false;
@ -93,19 +96,44 @@ public class ThreadWorkerViewModel : ViewModel
}
catch (Exception e)
{
_applicationView.Status = EStatusKind.Failed;
_applicationView.Status.SetStatus(EStatusKind.Failed);
CurrentCancellationTokenSource = null; // kill token
Log.Error("{Exception}", e);
FLogger.AppendError();
FLogger.AppendText(e.Message, Constants.WHITE, true);
FLogger.AppendText(" " + e.StackTrace.SubstringBefore('\n').Trim(), Constants.WHITE, true);
if ((e.InnerException ?? e) is { TargetSite.DeclaringType: not null } exception)
{
if (exception.TargetSite.ToString() == "CUE4Parse.FileProvider.GameFile get_Item(System.String)")
{
FLogger.AppendText(e.Message, Constants.WHITE, true);
}
else
{
var t = exception.GetType();
FLogger.AppendText(t.Namespace + _dot, Constants.GRAY);
FLogger.AppendText(t.Name, Constants.WHITE);
FLogger.AppendText(_colon + " ", Constants.GRAY);
FLogger.AppendText(exception.Message, Constants.RED, true);
FLogger.AppendText(_at, _gray);
FLogger.AppendText(exception.TargetSite.DeclaringType.FullName + _dot, Constants.GRAY);
FLogger.AppendText(exception.TargetSite.Name, Constants.YELLOW);
var p = exception.TargetSite.GetParameters();
var parameters = new string[p.Length];
for (int i = 0; i < parameters.Length; i++)
{
parameters[i] = p[i].ParameterType.Name + " " + p[i].Name;
}
FLogger.AppendText("(" + string.Join(", ", parameters) + ")", Constants.GRAY, true);
}
}
return;
}
}
_applicationView.Status = EStatusKind.Completed;
_applicationView.Status.SetStatus(EStatusKind.Completed);
CurrentCancellationTokenSource = null; // kill token
}
}
@ -115,4 +143,4 @@ public class ThreadWorkerViewModel : ViewModel
StatusChangeAttempted = true;
StatusChangeAttempted = false;
}
}
}

View File

@ -29,7 +29,7 @@
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" Orientation="Vertical" Margin="10 5 10 10">
<TextBlock Text="What to do?" HorizontalAlignment="Center" FontSize="20" FontWeight="SemiBold" />
<TextBlock Text="Instruction" HorizontalAlignment="Center" FontSize="20" FontWeight="SemiBold" />
<TextBlock TextAlignment="Center" TextWrapping="Wrap" HorizontalAlignment="Center" MaxWidth="576"
Text="In order to decipher archives' information, an AES key, in most cases, is needed. Here you can set the key for your static and dynamic archives. If you don't know what key to use for your set game, simply Google it. Keys must start with &quot;0x&quot; and contains 64 more characters." />
</StackPanel>
@ -81,16 +81,13 @@
HorizontalAlignment="Right" VerticalAlignment="Bottom" Content="OK" Click="OnClick" />
<Button Grid.Column="2" MinWidth="78" Margin="0 0 12 0" IsDefault="False" IsCancel="False"
HorizontalAlignment="Right" VerticalAlignment="Bottom" Content="Refresh" Click="OnRefreshAes">
<Button.Style>
<Style TargetType="Button" BasedOn="{StaticResource {x:Type Button}}">
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers>
<DataTrigger Binding="{Binding CUE4Parse.Game}" Value="{x:Static local:FGame.FortniteGame}">
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
<Button.Visibility>
<!-- if aes custom endpoint is enabled, make this visible -->
<MultiBinding Converter="{x:Static converters:EndpointToTypeConverter.Instance}">
<Binding RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type local:Views.AesManager}}" Path="DataContext" />
<Binding Source="{x:Static local:EEndpointType.Aes}" />
</MultiBinding>
</Button.Visibility>
</Button>
</Grid>
</Border>

View File

@ -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'}">
<adonisControls:AdonisWindow.Style>
@ -27,7 +28,7 @@
<ColumnDefinition Width="350" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<GroupBox Grid.Column="0" Padding="{adonisUi:Space 0}" Background="Transparent">
<DockPanel Margin="10">
<Grid DockPanel.Dock="Top">
@ -43,7 +44,7 @@
<ColumnDefinition Width="10" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Text="Devices" VerticalAlignment="Center" Margin="0 0 0 10" />
<ComboBox Grid.Row="0" Grid.Column="2" ItemsSource="{Binding AudioPlayer.AudioDevicesView, IsAsync=True}"
SelectedItem="{Binding AudioPlayer.SelectedAudioDevice, Mode=TwoWay}" SelectionChanged="OnDeviceSwap">
@ -53,18 +54,18 @@
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<TextBlock Grid.Row="1" Grid.Column="0" Text="Volume" VerticalAlignment="Center" Margin="0 0 0 10" />
<Slider Grid.Row="1" Grid.Column="2" TickPlacement="None" Minimum="0" Maximum="100" TickFrequency="1"
AutoToolTipPlacement="BottomRight" IsMoveToPointEnabled="True" ValueChanged="OnVolumeChange"
Value="{Binding AudioPlayerVolume, Source={x:Static local:Settings.UserSettings.Default}, Mode=TwoWay}"/>
<TextBlock Grid.Row="2" Grid.Column="0" Text="Encoding" VerticalAlignment="Center" Margin="0 0 0 10" />
<TextBlock Grid.Row="2" Grid.Column="2" Text="{Binding AudioPlayer.PlayedFile.Encoding, Converter={x:Static converters:EnumToStringConverter.Instance}}" VerticalAlignment="Center" HorizontalAlignment="Right" />
<TextBlock Grid.Row="3" Grid.Column="0" Text="Length" VerticalAlignment="Center" Margin="0 0 0 10" />
<TextBlock Grid.Row="3" Grid.Column="2" Text="{Binding AudioPlayer.PlayedFile.Duration}" VerticalAlignment="Center" HorizontalAlignment="Right" />
<TextBlock Grid.Row="4" Grid.Column="0" Text="Bytes Per Second" VerticalAlignment="Center" />
<TextBlock Grid.Row="4" Grid.Column="2" Text="{Binding AudioPlayer.PlayedFile.BytesPerSecond}" VerticalAlignment="Center" HorizontalAlignment="Right" />
</Grid>
@ -74,7 +75,7 @@
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid Grid.Column="0" ZIndex="1" HorizontalAlignment="Left" Margin="5 2 0 0">
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
@ -130,14 +131,14 @@
</ListBox>
</DockPanel>
</GroupBox>
<Grid Grid.Column="1">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<audioControls:Timeclock Grid.Row="0" />
<StackPanel Grid.Row="1" Margin="0 0 25 0"
HorizontalAlignment="Right" VerticalAlignment="Top" Orientation="Horizontal">

View File

@ -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)); };
}
}
private void OnActivatedDeactivated(object sender, EventArgs e)
{
_applicationView.AudioPlayer.HideToggle();
}
}

View File

@ -28,7 +28,7 @@
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" Orientation="Vertical" Margin="10 5 10 10">
<TextBlock Text="What to do?" HorizontalAlignment="Center" FontSize="20" FontWeight="SemiBold" />
<TextBlock Text="Instruction" HorizontalAlignment="Center" FontSize="20" FontWeight="SemiBold" />
<TextBlock TextAlignment="Center" TextWrapping="Wrap" HorizontalAlignment="Center"
Text="We like to make things as simple as possible. Choose between the detected games or manually add your own. FModel will use this information to automatically find archives to load and decrypt if needed. Make sure to not skip this step!" />
</StackPanel>

View File

@ -24,41 +24,41 @@
<DataTemplate x:Key="BrTemplate">
<StackPanel VerticalAlignment="Center" Margin="25 0">
<CheckBox Content="Points Of Interest" Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" IsChecked="{Binding MapViewer.BrPois}"
DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding IsReady}" />
DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding Status.IsReady}" />
<CheckBox Content="Landmarks" Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" IsChecked="{Binding MapViewer.BrLandmarks}"
DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding IsReady}" />
DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding Status.IsReady}" />
<!-- <CheckBox Content="Tags Location" Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" IsChecked="{Binding MapViewer.BrTagsLocation}" -->
<!-- DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding IsReady}" /> -->
<CheckBox Content="Patrols Path" Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" IsChecked="{Binding MapViewer.BrPatrolsPath}"
DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding IsReady}" />
<CheckBox Content="Upgrade Benches" Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" IsChecked="{Binding MapViewer.BrUpgradeBenches}"
DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding IsReady}" />
<!-- DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding Status.IsReady}" /> -->
<!-- <CheckBox Content="Patrols Path" Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" IsChecked="{Binding MapViewer.BrPatrolsPath}" -->
<!-- DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding Status.IsReady}" /> -->
<!-- <CheckBox Content="Upgrade Benches" Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" IsChecked="{Binding MapViewer.BrUpgradeBenches}" -->
<!-- DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding Status.IsReady}" /> -->
<!-- <CheckBox Content="Phonebooths" Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" IsChecked="{Binding MapViewer.BrPhonebooths}" -->
<!-- DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding IsReady}" /> -->
<CheckBox Content="Weapon-o-matic/Mending Machines" Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" IsChecked="{Binding MapViewer.BrVendingMachines}"
DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding IsReady}" />
<CheckBox Content="Bounty Boards" Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" IsChecked="{Binding MapViewer.BrBountyBoards}"
DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding IsReady}" />
<!-- DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding Status.IsReady}" /> -->
<!-- <CheckBox Content="Weapon-o-matic/Mending Machines" Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" IsChecked="{Binding MapViewer.BrVendingMachines}" -->
<!-- DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding Status.IsReady}" /> -->
<!-- <CheckBox Content="Bounty Boards" Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" IsChecked="{Binding MapViewer.BrBountyBoards}" -->
<!-- DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding Status.IsReady}" /> -->
</StackPanel>
</DataTemplate>
<DataTemplate x:Key="PrTemplate">
<StackPanel VerticalAlignment="Center" Margin="25 0">
<CheckBox Content="Landmarks" Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" IsChecked="{Binding MapViewer.PrLandmarks}"
DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding IsReady}" />
DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding Status.IsReady}" />
<CheckBox Content="Cannonball" Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" IsChecked="{Binding MapViewer.PrCannonball}"
DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding IsReady}" />
DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding Status.IsReady}" />
<CheckBox Content="Skydive" Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" IsChecked="{Binding MapViewer.PrSkydive}"
DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding IsReady}" />
DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding Status.IsReady}" />
<CheckBox Content="Shooting Targets" Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" IsChecked="{Binding MapViewer.PrShootingTargets}"
DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding IsReady}" />
DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding Status.IsReady}" />
<CheckBox Content="Parkour" Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" IsChecked="{Binding MapViewer.PrParkour}"
DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding IsReady}" />
DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding Status.IsReady}" />
<CheckBox Content="Time Trials" Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" IsChecked="{Binding MapViewer.PrTimeTrials}"
DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding IsReady}" />
DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding Status.IsReady}" />
<CheckBox Content="Vending Machines" Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" IsChecked="{Binding MapViewer.PrVendingMachines}"
DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding IsReady}" />
<CheckBox Content="Music Blocks" Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" IsChecked="{Binding MapViewer.PrMusicBlocks}"
DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding IsReady}" />
DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding Status.IsReady}" />
<!-- <CheckBox Content="Music Blocks" Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" IsChecked="{Binding MapViewer.PrMusicBlocks}" -->
<!-- DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding Status.IsReady}" /> -->
</StackPanel>
</DataTemplate>
</ResourceDictionary>
@ -108,7 +108,7 @@
<ContentControl ContentTemplateSelector="{StaticResource TagTemplateSelector}" Content="{Binding SelectedItem.Tag, ElementName=MapTree}" />
</Grid>
<Button Grid.Row="2" Content="Save Image" Margin="5" IsEnabled="{Binding IsReady}" VerticalAlignment="Bottom" Click="OnClick" />
<Button Grid.Row="2" Content="Save Image" Margin="5" IsEnabled="{Binding Status.IsReady}" VerticalAlignment="Bottom" Click="OnClick" />
</Grid>
<Grid Grid.Column="1" HorizontalAlignment="Stretch">
@ -118,9 +118,9 @@
<TextBlock Text="Minimap is loading, please wait..." HorizontalAlignment="Center" VerticalAlignment="Center"/>
<Image UseLayoutRounding="True" Source="{Binding MapViewer.MapImage}" HorizontalAlignment="Right" VerticalAlignment="Center"
Visibility="{Binding IsReady, Converter={StaticResource BoolToVisibilityConverter}}" />
Visibility="{Binding Status.IsReady, Converter={StaticResource BoolToVisibilityConverter}}" />
<Image UseLayoutRounding="True" Source="{Binding MapViewer.LayerImage}" HorizontalAlignment="Right" VerticalAlignment="Center"
Visibility="{Binding IsReady, Converter={StaticResource BoolToVisibilityConverter}}" />
Visibility="{Binding Status.IsReady, Converter={StaticResource BoolToVisibilityConverter}}" />
</Grid>
</Grid>
</adonisControls:AdonisWindow>

View File

@ -1,135 +0,0 @@
<adonisControls:AdonisWindow x:Class="FModel.Views.ModelViewer"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converters="clr-namespace:FModel.Views.Resources.Converters"
xmlns:adonisUi="clr-namespace:AdonisUI;assembly=AdonisUI"
xmlns:adonisControls="clr-namespace:AdonisUI.Controls;assembly=AdonisUI"
xmlns:helix="http://helix-toolkit.org/wpf/SharpDX"
WindowStartupLocation="CenterScreen" ResizeMode="CanResize" IconVisibility="Collapsed"
PreviewKeyDown="OnWindowKeyDown" Closing="OnClosing"
Height="{Binding Source={x:Static SystemParameters.MaximizedPrimaryScreenHeight}, Converter={converters:RatioConverter}, ConverterParameter='0.60'}"
Width="{Binding Source={x:Static SystemParameters.MaximizedPrimaryScreenWidth}, Converter={converters:RatioConverter}, ConverterParameter='0.60'}">
<adonisControls:AdonisWindow.Style>
<Style TargetType="adonisControls:AdonisWindow" BasedOn="{StaticResource {x:Type adonisControls:AdonisWindow}}" >
<Setter Property="Title" Value="Model Viewer" />
</Style>
</adonisControls:AdonisWindow.Style>
<adonisControls:AdonisWindow.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Resources/Resources.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</adonisControls:AdonisWindow.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="350" MinWidth="250" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="4*" />
</Grid.ColumnDefinitions>
<GroupBox Grid.Column="0" Padding="{adonisUi:Space 0}" Background="Transparent">
<DockPanel Margin="10">
<Grid DockPanel.Dock="Top">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="10" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Text="Models" VerticalAlignment="Center" Margin="0 0 0 10" />
<ComboBox Grid.Row="0" Grid.Column="2" Style="{StaticResource ModelsComboBox}" IsEnabled="{Binding IsReady}" />
<TextBlock Grid.Row="1" Grid.Column="0" Text="Triangles" VerticalAlignment="Center" Margin="0 0 0 10" />
<TextBlock Grid.Row="1" Grid.Column="2" Text="{Binding ModelViewer.SelectedModel.TriangleCount, FallbackValue=0, StringFormat={}{0:### ### ### ###}}" VerticalAlignment="Center" HorizontalAlignment="Right" />
<TextBlock Grid.Row="2" Grid.Column="0" Text="Materials" VerticalAlignment="Center" Margin="0 0 0 10" />
<TextBlock Grid.Row="2" Grid.Column="2" Text="{Binding ModelViewer.SelectedModel.Group3d.Count, FallbackValue=0, StringFormat={}{0:### ###}}" VerticalAlignment="Center" HorizontalAlignment="Right" />
<Grid Grid.Row="3" Grid.Column="0" Grid.ColumnSpan="3">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="10" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="10" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Button Grid.Row="0" Grid.Column="0" Content="Focus" Click="OnFocusClick" IsEnabled="{Binding IsReady}" />
<ToggleButton Grid.Row="0" Grid.Column="2" IsChecked="{Binding ModelViewer.AppendMode}" IsEnabled="{Binding IsReady}"
Style="{DynamicResource {x:Static adonisUi:Styles.ToolbarToggleButton}}">
<TextBlock Text="{Binding IsChecked, Converter={x:Static converters:BoolToToggleConverter.Instance},
StringFormat={}Append {0}, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ToggleButton}}}" />
</ToggleButton>
<Button Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="3" Click="Save" IsEnabled="{Binding IsReady}"
Style="{DynamicResource {x:Static adonisUi:Styles.AccentButton}}">
<TextBlock Text="{Binding ModelViewer.LoadedModelsView.Count, StringFormat={}Save All Loaded Models ({0})}" />
</Button>
</Grid>
</Grid>
<Separator DockPanel.Dock="Top" Tag="MATERIALS" Style="{StaticResource CustomSeparator}" />
<ListBox x:Name="MaterialsListName" DockPanel.Dock="Top" Style="{StaticResource MaterialsListBox}" IsEnabled="{Binding IsReady}">
<ListBox.ContextMenu>
<ContextMenu DataContext="{Binding PlacementTarget, RelativeSource={RelativeSource Self}}">
<MenuItem Header="Copy Name" Click="OnCopyClick">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource CopyIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Overwrite Material" Click="OnOverwriteMaterialClick">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource OverwriteIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
</ContextMenu>
</ListBox.ContextMenu>
</ListBox>
</DockPanel>
</GroupBox>
<GridSplitter Grid.Column="1" ResizeDirection="Columns" Width="4" VerticalAlignment="Stretch" ResizeBehavior="PreviousAndNext"
Background="{DynamicResource {x:Static adonisUi:Brushes.Layer3BackgroundBrush}}" />
<TextBlock Grid.Column="2" Text="Model is loading, please wait..." HorizontalAlignment="Center" VerticalAlignment="Center"/>
<helix:Viewport3DX Grid.Column="2" EffectsManager="{Binding ModelViewer.EffectManager}" Camera="{Binding ModelViewer.Cam}"
ShowViewCube="False" IsChangeFieldOfViewEnabled="False" IsMoveEnabled="False" UseDefaultGestures="False"
ShowCameraTarget="False" FXAALevel="Ultra" MSAA="Maximum" BackgroundColor="#2A2B34"
EnableSSAO="True" SSAOIntensity="1" EnableSwapChainRendering="True">
<helix:Viewport3DX.InputBindings>
<MouseBinding Command="helix:ViewportCommands.Rotate" Gesture="LeftClick" />
<MouseBinding Command="helix:ViewportCommands.Zoom" Gesture="RightClick" />
<MouseBinding Command="helix:ViewportCommands.Pan" Gesture="MiddleClick" />
<KeyBinding Command="helix:ViewportCommands.ZoomExtents" Modifiers="Shift" Key="C"/>
</helix:Viewport3DX.InputBindings>
<helix:EnvironmentMap3D Texture="{Binding ModelViewer.HDRi}" />
<helix:DirectionalLight3D Color="White" Direction="{Binding Camera.LookDirection,
RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type helix:Viewport3DX}}}" />
<helix:LineGeometryModel3D Geometry="{Binding ModelViewer.SelectedModel.XAxis}" Color="#FC3854" />
<helix:LineGeometryModel3D Geometry="{Binding ModelViewer.SelectedModel.YAxis}" Color="#85CB22" />
<helix:LineGeometryModel3D Geometry="{Binding ModelViewer.SelectedModel.ZAxis}" Color="#388EED" />
<helix:GroupModel3D x:Name="MyAntiCrashGroup" Mouse3DDown="OnMouse3DDown"
ItemsSource="{Binding ModelViewer.SelectedModel.Group3d, IsAsync=True}"/>
</helix:Viewport3DX>
</Grid>
</adonisControls:AdonisWindow>

View File

@ -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();
}
}

View File

@ -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;
}
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}
}

View File

@ -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);
}
}
}

View File

@ -322,7 +322,15 @@ public sealed class Timeclock : UserControl
}
else
{
Dispatcher.BeginInvoke((Action) delegate { _timeText.Text = TimeSpan.Zero.ToString(TimeFormat); });
ZeroTime();
}
}
}
private void ZeroTime()
{
Dispatcher.BeginInvoke((Action) delegate
{
_timeText.Text = TimeSpan.Zero.ToString(TimeFormat);
});
}
}

View File

@ -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);
}
}
}

View File

@ -21,7 +21,7 @@
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<avalonEdit:TextEditor x:Name="MyAvalonEditor" Grid.Row="0" Background="{DynamicResource {x:Static adonisUi:Brushes.Layer3BackgroundBrush}}"
FontFamily="Consolas" FontSize="8pt" IsReadOnly="True" ShowLineNumbers="True" Foreground="#DAE5F2" />
@ -33,15 +33,18 @@
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock x:Name="HeBrokeIt" Grid.Column="0" Text="IF YOU DON'T KNOW WHAT THIS DOES, DON'T TOUCH IT, EVER!"
<TextBlock x:Name="HeBrokeIt" Grid.Column="0" Text="IF YOU DON'T KNOW WHAT THIS DOES, DON'T TOUCH IT!"
HorizontalAlignment="Right" VerticalAlignment="Center" FontSize="11" Margin="0 0 10 0" FontWeight="DemiBold"
Foreground="{DynamicResource {x:Static adonisUi:Brushes.Layer1InteractionForegroundBrush}}" />
<Button Grid.Column="1" MinWidth="78" Margin="0 0 12 0" IsDefault="True" IsCancel="False"
HorizontalAlignment="Right" VerticalAlignment="Bottom" Content="OK" Click="OnClick" />
<Button Grid.Column="2" MinWidth="78" Margin="0 0 12 0" IsDefault="False" IsCancel="True"
<Button Grid.Column="2" MinWidth="78" Margin="0 0 12 0" IsDefault="False" IsCancel="False"
HorizontalAlignment="Right" VerticalAlignment="Bottom" Content="Reset" Click="OnReset" />
<Button Grid.Column="3" MinWidth="78" Margin="0 0 12 0" IsDefault="False" IsCancel="True"
HorizontalAlignment="Right" VerticalAlignment="Bottom" Content="Cancel" />
</Grid>
</Border>

View File

@ -64,12 +64,14 @@ public partial class DictionaryEditor
{
case "Versioning Configuration (Custom Versions)":
CustomVersions = JsonConvert.DeserializeObject<List<FCustomVersion>>(MyAvalonEditor.Document.Text);
DialogResult = !CustomVersions.SequenceEqual(_defaultCustomVersions);
// DialogResult = !CustomVersions.SequenceEqual(_defaultCustomVersions);
DialogResult = true;
Close();
break;
case "Versioning Configuration (Options)":
Options = JsonConvert.DeserializeObject<Dictionary<string, bool>>(MyAvalonEditor.Document.Text);
DialogResult = !Options.SequenceEqual(_defaultOptions);
// DialogResult = !Options.SequenceEqual(_defaultOptions);
DialogResult = true;
Close();
break;
default:
@ -78,8 +80,27 @@ public partial class DictionaryEditor
}
catch
{
HeBrokeIt.Text = "GG YOU BROKE THE FORMAT, FIX THE JSON OR CANCEL THE CHANGES!";
HeBrokeIt.Text = "GG YOU BROKE THE FORMAT, FIX THE JSON OR RESET THE CHANGES!";
HeBrokeIt.Foreground = new SolidColorBrush((Color) ColorConverter.ConvertFromString(Constants.RED));
}
}
}
private void OnReset(object sender, RoutedEventArgs e)
{
if (!_enableElements)
return;
MyAvalonEditor.Document = Title switch
{
"Versioning Configuration (Custom Versions)" => new TextDocument
{
Text = JsonConvert.SerializeObject(_defaultCustomVersions, Formatting.Indented)
},
"Versioning Configuration (Options)" => new TextDocument
{
Text = JsonConvert.SerializeObject(_defaultOptions, Formatting.Indented)
},
_ => throw new NotImplementedException()
};
}
}

View File

@ -0,0 +1,116 @@
<adonisControls:AdonisWindow x:Class="FModel.Views.Resources.Controls.EndpointEditor"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converters="clr-namespace:FModel.Views.Resources.Converters"
xmlns:avalonEdit="http://icsharpcode.net/sharpdevelop/avalonedit"
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" ResizeMode="NoResize"
Width="{Binding Source={x:Static SystemParameters.MaximizedPrimaryScreenWidth}, Converter={converters:RatioConverter}, ConverterParameter='0.50'}"
Height="{Binding Source={x:Static SystemParameters.MaximizedPrimaryScreenWidth}, Converter={converters:RatioConverter}, ConverterParameter='0.35'}">
<adonisControls:AdonisWindow.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="../Resources.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</adonisControls:AdonisWindow.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="5" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid Grid.Column="0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="5"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid Grid.Row="0" Margin="{adonisUi:Space 1, 0.5}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="10" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="5" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="Endpoint" VerticalAlignment="Center" Margin="0 0 0 5" />
<TextBox Grid.Column="2" Margin="0 0 0 5" Text="{Binding Url, Mode=TwoWay}" TextChanged="OnTextChanged" />
<Button Grid.Column="4" Content="Send" HorizontalAlignment="Right" Margin="0 0 0 5"
Style="{DynamicResource {x:Static adonisUi:Styles.AccentButton}}" Click="OnSend"/>
</Grid>
<avalonEdit:TextEditor x:Name="EndpointResponse" Grid.Row="2" Background="{DynamicResource {x:Static adonisUi:Brushes.Layer3BackgroundBrush}}"
FontFamily="Consolas" FontSize="8pt" IsReadOnly="True" ShowLineNumbers="True" Foreground="#DAE5F2" />
</Grid>
<Grid Grid.Column="2">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="5"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" Orientation="Vertical" Margin="10 5 10 10">
<TextBlock Text="Instruction" HorizontalAlignment="Center" FontSize="20" FontWeight="SemiBold" />
<TextBlock x:Name="InstructionBox" TextAlignment="Justify" TextWrapping="Wrap" HorizontalAlignment="Center" />
</StackPanel>
<Grid Grid.Row="1" Margin="{adonisUi:Space 1, 0.5}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="10" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="5" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="Expression" VerticalAlignment="Center" Margin="0 0 0 5" />
<TextBox Grid.Column="2" Margin="0 0 0 5" Text="{Binding Path, Mode=TwoWay}" TextChanged="OnTextChanged" />
<Button Grid.Column="4" Content="Test" HorizontalAlignment="Right" Margin="0 0 0 5"
Style="{DynamicResource {x:Static adonisUi:Styles.AccentButton}}" Click="OnTest"/>
</Grid>
<avalonEdit:TextEditor x:Name="TargetResponse" Grid.Row="3" Background="{DynamicResource {x:Static adonisUi:Brushes.Layer3BackgroundBrush}}"
FontFamily="Consolas" FontSize="8pt" IsReadOnly="True" ShowLineNumbers="True" Foreground="#DAE5F2" />
</Grid>
</Grid>
<Border Grid.Row="1"
Background="{DynamicResource {x:Static adonisUi:Brushes.Layer1BackgroundBrush}}"
adonisExtensions:LayerExtension.IncreaseLayer="True">
<Grid Margin="30, 12, 6, 12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="{Binding Label}"
HorizontalAlignment="Right" VerticalAlignment="Center" FontSize="11" Margin="0 0 10 0" FontWeight="DemiBold"
Foreground="{DynamicResource {x:Static adonisUi:Brushes.Layer1InteractionForegroundBrush}}" />
<Button Grid.Column="1" MinWidth="78" Margin="0 0 12 0" IsDefault="True" IsCancel="False"
HorizontalAlignment="Right" VerticalAlignment="Bottom" Content="OK" Click="OnClick" />
<Button Grid.Column="2" MinWidth="78" Margin="0 0 12 0" IsDefault="False" IsCancel="False"
HorizontalAlignment="Right" VerticalAlignment="Bottom" Content="Expression Syntax" Click="OnSyntax" />
<Button Grid.Column="3" MinWidth="78" Margin="0 0 12 0" IsDefault="False" IsCancel="False"
HorizontalAlignment="Right" VerticalAlignment="Bottom" Content="Online Evaluator" Click="OnEvaluator" />
</Grid>
</Border>
</Grid>
</adonisControls:AdonisWindow>

View File

@ -0,0 +1,99 @@
using System.Diagnostics;
using System.Windows;
using System.Windows.Controls;
using FModel.Extensions;
using FModel.Framework;
using FModel.Services;
using ICSharpCode.AvalonEdit.Document;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using RestSharp;
namespace FModel.Views.Resources.Controls;
public partial class EndpointEditor
{
private readonly EEndpointType _type;
private bool _isTested;
public EndpointEditor(FEndpoint endpoint, string title, EEndpointType type)
{
DataContext = endpoint;
_type = type;
_isTested = endpoint.IsValid;
InitializeComponent();
Title = title;
TargetResponse.SyntaxHighlighting =
EndpointResponse.SyntaxHighlighting = AvalonExtensions.HighlighterSelector("json");
InstructionBox.Text = type switch
{
EEndpointType.Aes =>
@"In order to make this work, you first need to understand JSON and its query language. If you don't, please close this window. If your game never changes its AES keys or is not even encrypted, please close this window. If you do understand what you are doing, you have to know that the AES expression supports up to 2 elements.
The first element is mandatory and will be assigned to the main AES key. It has to be looking like a key, else your configuration will not be valid (the key validity against your files will not be checked). Said key must be hexadecimal and can start without ""0x"".
If your game uses several AES keys, you can specify a second element that will be your list of dynamic keys. The format needed is a list of objects with, at least, the next 2 variables:
{
""guid"": ""the archive guid"",
""key"": ""the archive aes key""
}",
EEndpointType.Mapping =>
@"In order to make this work, you first need to understand JSON and its query language. If you don't, please close this window. If your game does not use unversioned package properties, please close this window. If you do understand what you are doing, you have to know that the mapping expression supports up to 2 elements.
The first element is mandatory and will be assigned to the mapping download URL, which can be all kinds of mapping but not Brotli compressed.
The second element is optional and will be assigned to the mapping file name. If unspecified, said file name will be grabbed from the URL.",
_ => ""
};
}
private void OnClick(object sender, RoutedEventArgs e)
{
DialogResult = _isTested && DataContext is FEndpoint { IsValid: true };
Close();
}
private async void OnSend(object sender, RoutedEventArgs e)
{
if (DataContext is not FEndpoint endpoint) return;
var body = await ApplicationService.ApiEndpointView.DynamicApi.GetRequestBody(default, endpoint.Url).ConfigureAwait(false);
Application.Current.Dispatcher.Invoke(delegate
{
EndpointResponse.Document ??= new TextDocument();
EndpointResponse.Document.Text = body.ToString(Formatting.Indented);
});
}
private void OnTest(object sender, RoutedEventArgs e)
{
if (DataContext is not FEndpoint endpoint) return;
endpoint.TryValidate(ApplicationService.ApiEndpointView.DynamicApi, _type, out var response);
_isTested = true;
TargetResponse.Document ??= new TextDocument();
TargetResponse.Document.Text = JsonConvert.SerializeObject(response, Formatting.Indented);
}
private void OnTextChanged(object sender, TextChangedEventArgs e)
{
if (sender is not TextBox { IsLoaded: true } ||
DataContext is not FEndpoint endpoint) return;
endpoint.IsValid = false;
}
private void OnSyntax(object sender, RoutedEventArgs e)
{
Process.Start(new ProcessStartInfo { FileName = "https://support.smartbear.com/alertsite/docs/monitors/api/endpoint/jsonpath.html", UseShellExecute = true });
}
private void OnEvaluator(object sender, RoutedEventArgs e)
{
Process.Start(new ProcessStartInfo { FileName = "https://jsonpath.herokuapp.com/", UseShellExecute = true });
}
}

View File

@ -29,6 +29,8 @@ public partial class PropertiesPopout
MyAvalonEditor.FontSize = contextViewModel.FontSize;
MyAvalonEditor.SyntaxHighlighting = contextViewModel.Highlighter;
MyAvalonEditor.ScrollToVerticalOffset(contextViewModel.ScrollPosition);
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());
_manager = new JsonFoldingStrategies(MyAvalonEditor);
@ -67,7 +69,7 @@ public partial class PropertiesPopout
_ => fontSize
};
}
private void OnPreviewKeyDown(object sender, KeyEventArgs e)
{
switch (e.Key)
@ -96,4 +98,4 @@ public partial class PropertiesPopout
c.Green * c.Green * .587 +
c.Blue * c.Blue * .114);
}
}
}

View File

@ -1,29 +0,0 @@
using System;
using System.Globalization;
using System.Windows.Data;
using SharpDX.Direct3D11;
namespace FModel.Views.Resources.Converters;
public class BoolToFillModeConverter : IValueConverter
{
public static readonly BoolToFillModeConverter Instance = new();
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return value switch
{
FillMode.Solid => true,
_ => false
};
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return value switch
{
true => FillMode.Solid,
_ => FillMode.Wireframe
};
}
}

Some files were not shown because too many files have changed in this diff Show More