Compare commits

..

2 Commits

Author SHA1 Message Date
Valentin
d132bc46e2
Merge pull request #510 from 4sval/dev
Dev
2024-10-23 09:14:58 +02:00
Valentin
bcbda59523
Merge pull request #472 from 4sval/dev
Dev
2024-05-26 19:26:31 +02:00
148 changed files with 2794 additions and 7471 deletions

View File

@ -14,7 +14,7 @@ jobs:
steps:
- name: GIT Checkout
uses: actions/checkout@v6
uses: actions/checkout@v2
with:
submodules: 'true'
@ -22,7 +22,7 @@ jobs:
run: git submodule update --init --recursive
- name: .NET 8 Setup
uses: actions/setup-dotnet@v5
uses: actions/setup-dotnet@v2
with:
dotnet-version: '8.0.x'
@ -33,7 +33,7 @@ jobs:
run: dotnet publish FModel -c Release --no-self-contained -r win-x64 -f net8.0-windows -o "./FModel/bin/Publish/" -p:PublishReadyToRun=false -p:PublishSingleFile=true -p:DebugType=None -p:GenerateDocumentationFile=false -p:DebugSymbols=false -p:AssemblyVersion=${{ github.event.inputs.appVersion }} -p:FileVersion=${{ github.event.inputs.appVersion }}
- name: ZIP File
uses: papeloto/action-zip@v1.2
uses: papeloto/action-zip@v1
with:
files: ./FModel/bin/Publish/FModel.exe
dest: FModel.zip # will end up in working directory not the Publish folder

View File

@ -10,17 +10,17 @@ jobs:
steps:
- name: GIT Checkout
uses: actions/checkout@v6
uses: actions/checkout@v4
with:
submodules: 'recursive'
- name: .NET 8 Setup
uses: actions/setup-dotnet@v5
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
- name: .NET Restore
run: dotnet restore "./FModel/FModel.slnx"
run: dotnet restore FModel
- name: .NET Publish
run: dotnet publish "./FModel/FModel.csproj" -c Release --no-restore --no-self-contained -r win-x64 -f net8.0-windows -o "./FModel/bin/Publish/" -p:PublishReadyToRun=false -p:PublishSingleFile=true -p:DebugType=None -p:GenerateDocumentationFile=false -p:DebugSymbols=false
@ -33,7 +33,7 @@ jobs:
path: ./FModel/bin/Publish/FModel.exe
- name: Edit QA Artifact
uses: ncipollo/release-action@v1.20.0
uses: ncipollo/release-action@v1.14.0
with:
token: ${{ secrets.GITHUB_TOKEN }}
name: 'FModel QA Testing'
@ -51,13 +51,13 @@ jobs:
- name: FModel Auth
id: fmodel_auth
uses: fjogeleit/http-request-action@v1.16.6
uses: fjogeleit/http-request-action@v1.15.5
with:
url: "https://api.fmodel.app/v1/oauth/token"
data: '{"username": "${{ secrets.API_USERNAME }}", "password": "${{ secrets.API_PASSWORD }}"}'
- name: FModel Deploy Build
uses: fjogeleit/http-request-action@v1.16.6
uses: fjogeleit/http-request-action@v1.15.5
with:
url: "https://api.fmodel.app/v1/infos/${{ secrets.QA_ID }}"
method: "PATCH"

@ -1 +1 @@
Subproject commit d2f6ce6e618576dbbe7f6dd9ed3171f14513182a
Subproject commit 455b72e5e38bfe9476b5823bc642fc8ef488347f

View File

@ -1,4 +1,4 @@
<Application x:Class="FModel.App"
<Application x:Class="FModel.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:adonisUi="clr-namespace:AdonisUI;assembly=AdonisUI"
@ -9,14 +9,6 @@
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/AdonisUI;component/ColorSchemes/Dark.xaml"/>
<ResourceDictionary Source="pack://application:,,,/AdonisUI.ClassicTheme;component/Resources.xaml"/>
<ResourceDictionary Source="Views/Resources/Colors.xaml" />
<ResourceDictionary Source="Views/Resources/Icons.xaml" />
<ResourceDictionary Source="Views/Resources/Controls/ContextMenus/FileContextMenu.xaml" />
<ResourceDictionary Source="Views/Resources/Controls/ContextMenus/FolderContextMenu.xaml" />
<ResourceDictionary Source="Views/Resources/Resources.xaml" />
<ResourceDictionary Source="Views/Resources/Controls/TiledExplorer/Resources.xaml" />
</ResourceDictionary.MergedDictionaries>
<Color x:Key="{x:Static adonisUi:Colors.AccentColor}">#206BD4</Color>

View File

@ -7,7 +7,6 @@ using System.IO;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Threading;
using CUE4Parse;
using FModel.Framework;
using FModel.Services;
using FModel.Settings;
@ -52,20 +51,13 @@ public partial class App
if (!Directory.Exists(UserSettings.Default.OutputDirectory))
{
var currentDir = Directory.GetCurrentDirectory();
try
{
var outputDir = Directory.CreateDirectory(Path.Combine(currentDir, "Output"));
using (File.Create(Path.Combine(outputDir.FullName, Path.GetRandomFileName()), 1, FileOptions.DeleteOnClose))
{
var dirInfo = new DirectoryInfo(currentDir);
if (dirInfo.Attributes.HasFlag(FileAttributes.Archive))
throw new Exception("FModel cannot be run from an archive file. Please extract it and try again.");
if (dirInfo.Attributes.HasFlag(FileAttributes.ReadOnly))
throw new Exception("FModel cannot be run from a read-only directory. Please move it to a writable location.");
}
UserSettings.Default.OutputDirectory = outputDir.FullName;
}
catch (UnauthorizedAccessException exception)
{
throw new Exception("FModel cannot create the output directory where it is currently located. Please move FModel.exe to a different location.", exception);
}
UserSettings.Default.OutputDirectory = Path.Combine(currentDir, "Output");
}
if (!Directory.Exists(UserSettings.Default.RawDataDirectory))
@ -104,20 +96,15 @@ public partial class App
Directory.CreateDirectory(Path.Combine(UserSettings.Default.OutputDirectory, "Logs"));
Directory.CreateDirectory(Path.Combine(UserSettings.Default.OutputDirectory, ".data"));
const string template = "{Timestamp:yyyy-MM-dd HH:mm:ss} [{Level:u3}] {Enriched}: {Message:lj}{NewLine}{Exception}";
Log.Logger = new LoggerConfiguration()
#if DEBUG
.Enrich.With<SourceEnricher>()
.MinimumLevel.Verbose()
.WriteTo.Console(outputTemplate: template, theme: AnsiConsoleTheme.Literate)
.WriteTo.File(outputTemplate: template,
path: Path.Combine(UserSettings.Default.OutputDirectory, "Logs", $"FModel-Debug-Log-{DateTime.Now:yyyy-MM-dd}.log"))
Log.Logger = new LoggerConfiguration().WriteTo.Console(theme: AnsiConsoleTheme.Literate).WriteTo.File(
Path.Combine(UserSettings.Default.OutputDirectory, "Logs", $"FModel-Debug-Log-{DateTime.Now:yyyy-MM-dd}.txt"),
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss} [FModel] [{Level:u3}] {Message:lj}{NewLine}{Exception}").CreateLogger();
#else
.Enrich.With<CallerEnricher>()
.WriteTo.File(outputTemplate: template,
path: Path.Combine(UserSettings.Default.OutputDirectory, "Logs", $"FModel-Log-{DateTime.Now:yyyy-MM-dd}.log"))
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
.CreateLogger();
Log.Information("Version {Version} ({CommitId})", Constants.APP_VERSION, Constants.APP_COMMIT_ID);
Log.Information("{OS}", GetOperatingSystemProductName());
@ -139,15 +126,15 @@ public partial class App
var messageBox = new MessageBoxModel
{
Text = $"An unhandled {e.Exception.GetBaseException().GetType()} occurred: {e.Exception.Message}",
Text = $"An unhandled exception occurred: {e.Exception.Message}",
Caption = "Fatal Error",
Icon = MessageBoxImage.Error,
Buttons =
[
Buttons = new[]
{
MessageBoxButtons.Custom("Reset Settings", EErrorKind.ResetSettings),
MessageBoxButtons.Custom("Restart", EErrorKind.Restart),
MessageBoxButtons.Custom("OK", EErrorKind.Ignore)
],
},
IsSoundEnabled = false
};

View File

@ -1,9 +1,10 @@
using System;
using System;
using System.Diagnostics;
using System.IO;
using System.Numerics;
using System.Reflection;
using CUE4Parse.UE4.Objects.Core.Misc;
using CUE4Parse.Utils;
using FModel.Extensions;
namespace FModel;
@ -11,9 +12,8 @@ public static class Constants
{
public static readonly string APP_PATH = Path.GetFullPath(Environment.GetCommandLineArgs()[0]);
public static readonly string APP_VERSION = FileVersionInfo.GetVersionInfo(APP_PATH).FileVersion;
public static readonly string APP_COMMIT_ID = FileVersionInfo.GetVersionInfo(APP_PATH).ProductVersion?.SubstringAfter('+');
public static readonly string APP_COMMIT_ID = FileVersionInfo.GetVersionInfo(APP_PATH).ProductVersion.SubstringAfter('+');
public static readonly string APP_SHORT_COMMIT_ID = APP_COMMIT_ID[..7];
public static readonly DateTime APP_BUILD_DATE = File.GetLastWriteTime(APP_PATH);
public const string ZERO_64_CHAR = "0000000000000000000000000000000000000000000000000000000000000000";
public static readonly FGuid ZERO_GUID = new(0U);

View File

@ -3,7 +3,7 @@ using CUE4Parse.UE4.Assets.Exports;
using CUE4Parse.UE4.Assets.Objects;
using CUE4Parse.UE4.Objects.Core.i18N;
using CUE4Parse.UE4.Objects.UObject;
using CUE4Parse.Utils;
using FModel.Extensions;
using FModel.Framework;
using SkiaSharp;
using SkiaSharp.HarfBuzz;
@ -13,11 +13,12 @@ namespace FModel.Creator.Bases.FN;
public class BaseBundle : UCreator
{
private IList<BaseQuest> _quests;
private const int _headerHeight = 100;
public BaseBundle(UObject uObject, EIconStyle style) : base(uObject, style)
{
Width = 1024;
Height = 0;
Height = _headerHeight;
Margin = 0;
}
@ -55,31 +56,84 @@ public class BaseBundle : UCreator
if (!completionReward.TryGetValue(out int completionCount, "CompletionCount") ||
!completionReward.TryGetValue(out FStructFallback[] rewards, "Rewards")) continue;
var quest = new BaseQuest(completionCount, Style);
foreach (var reward in rewards)
{
if (!reward.TryGetValue(out FSoftObjectPath itemDefinition, "ItemDefinition")) continue;
quest.AddCompletionReward(itemDefinition);
if (!reward.TryGetValue(out int quantity, "Quantity") ||
!reward.TryGetValue(out string templateId, "TemplateId") ||
!reward.TryGetValue(out FSoftObjectPath itemDefinition, "ItemDefinition")) continue;
if (!itemDefinition.AssetPathName.IsNone &&
!itemDefinition.AssetPathName.Text.Contains("/Items/Tokens/") &&
!itemDefinition.AssetPathName.Text.Contains("/Items/Quests"))
{
_quests.Add(new BaseQuest(completionCount, itemDefinition, Style));
}
else if (!string.IsNullOrWhiteSpace(templateId))
{
_quests.Add(new BaseQuest(completionCount, quantity, templateId, Style));
}
}
_quests.Add(quest);
}
}
Height += 200 * _quests.Count;
Height += 256 * _quests.Count;
}
public override SKBitmap[] Draw()
{
var ret = new SKBitmap(Width, Height, SKColorType.Rgba8888, SKAlphaType.Unpremul);
var ret = new SKBitmap(Width, Height, SKColorType.Rgba8888, SKAlphaType.Opaque);
using var c = new SKCanvas(ret);
var y = 0;
DrawHeader(c);
DrawDisplayName(c);
DrawQuests(c);
return new[] { ret };
}
private readonly SKPaint _headerPaint = new()
{
IsAntialias = true, FilterQuality = SKFilterQuality.High,
Typeface = Utils.Typefaces.Bundle, TextSize = 50,
TextAlign = SKTextAlign.Center, Color = SKColor.Parse("#262630")
};
private void DrawHeader(SKCanvas c)
{
c.DrawRect(new SKRect(0, 0, Width, _headerHeight), _headerPaint);
var background = _quests.Count > 0 ? _quests[0].Background : Background;
_headerPaint.Shader = SKShader.CreateRadialGradient(new SKPoint(Width / 2, _headerHeight / 2), Width / 5 * 4,
new[] { background[0].WithAlpha(50), background[1].WithAlpha(50) }, SKShaderTileMode.Clamp);
c.DrawRect(new SKRect(0, 0, Width, _headerHeight), _headerPaint);
_headerPaint.Shader = SKShader.CreateLinearGradient(new SKPoint(Width / 2, _headerHeight), new SKPoint(Width / 2, 75),
new[] { SKColors.Black.WithAlpha(25), background[1].WithAlpha(0) }, SKShaderTileMode.Clamp);
c.DrawRect(new SKRect(0, 75, Width, _headerHeight), _headerPaint);
}
private new void DrawDisplayName(SKCanvas c)
{
if (string.IsNullOrEmpty(DisplayName)) return;
_headerPaint.Shader = null;
_headerPaint.Color = SKColors.White;
while (_headerPaint.MeasureText(DisplayName) > Width)
{
_headerPaint.TextSize -= 1;
}
var shaper = new CustomSKShaper(_headerPaint.Typeface);
c.DrawShapedText(shaper, DisplayName, Width / 2f, _headerHeight / 2f + _headerPaint.TextSize / 2 - 10, _headerPaint);
}
private void DrawQuests(SKCanvas c)
{
var y = _headerHeight;
foreach (var quest in _quests)
{
quest.DrawQuest(c, y);
y += quest.Height;
}
return [ret];
}
}

View File

@ -58,9 +58,6 @@ public class BaseCommunity : BaseIcon
if (Object.TryGetValue(out FGameplayTagContainer gameplayTags, "GameplayTags"))
CheckGameplayTags(gameplayTags);
else if (Object.TryGetValue(out FInstancedStruct[] dataList, "DataList"))
CheckGameplayTags(dataList);
if (Object.TryGetValue(out FPackageIndex cosmeticItem, "cosmetic_item"))
CosmeticSource = cosmeticItem.Name.ToUpper();
@ -94,7 +91,7 @@ public class BaseCommunity : BaseIcon
return new[] { ret };
}
protected override void CheckGameplayTags(FGameplayTagContainer gameplayTags)
private void CheckGameplayTags(FGameplayTagContainer gameplayTags)
{
if (_design == null) return;
if (_design.DrawSource)
@ -123,9 +120,8 @@ public class BaseCommunity : BaseIcon
{
if (!bShort) return base.GetCosmeticSeason(seasonNumber);
var s = seasonNumber["Cosmetics.Filter.Season.".Length..];
(string chapterIdx, string seasonIdx, bool onlySeason) = GetInternalSID(s);
var prefix = int.TryParse(seasonIdx, out _) ? "S" : "";
return onlySeason ? $"{prefix}{seasonIdx}" : $"C{chapterIdx} {prefix}{seasonIdx}";
(int chapterIdx, int seasonIdx) = GetInternalSID(int.Parse(s));
return $"C{chapterIdx} S{seasonIdx}";
}
private new void DrawBackground(SKCanvas c)

View File

@ -62,7 +62,7 @@ public class BaseIcon : UCreator
// text
if (Object.TryGetValue(out FText displayName, "DisplayName", "ItemName", "BundleName", "DefaultHeaderText", "UIDisplayName", "EntryName", "EventCalloutTitle"))
DisplayName = displayName.Text;
if (Object.TryGetValue(out FText description, "Description", "ItemDescription", "SetDescription", "BundleDescription", "GeneralDescription", "DefaultBodyText", "UIDescription", "UIDisplayDescription", "EntryDescription", "EventCalloutDescription"))
if (Object.TryGetValue(out FText description, "Description", "ItemDescription", "BundleDescription", "GeneralDescription", "DefaultBodyText", "UIDescription", "UIDisplayDescription", "EntryDescription", "EventCalloutDescription"))
Description = description.Text;
else if (Object.TryGetValue(out FText[] descriptions, "Description"))
Description = string.Join('\n', descriptions.Select(x => x.Text));
@ -88,8 +88,6 @@ public class BaseIcon : UCreator
{
ParseForReward(UserSettings.Default.CosmeticDisplayAsset);
if (Object.TryGetValue(out FInstancedStruct[] dataList, "DataList"))
CheckGameplayTags(dataList);
if (Object.TryGetValue(out FGameplayTagContainer gameplayTags, "GameplayTags"))
CheckGameplayTags(gameplayTags);
if (Object.TryGetValue(out FPackageIndex cosmeticItem, "cosmetic_item"))
@ -159,7 +157,7 @@ public class BaseIcon : UCreator
{
if (uObject is UTexture2D texture2D)
{
SeriesBackground = texture2D.Decode().ToSkBitmap();
SeriesBackground = texture2D.Decode();
return;
}
@ -217,31 +215,46 @@ public class BaseIcon : UCreator
if (uObject.TryGetValue(out FText displayName, "DisplayName"))
name = displayName.Text;
var format = Utils.GetLocalizedResource("Fort.Cosmetics", "CosmeticItemDescription_SetMembership", "\nPart of the <SetName>{0}</> set.");
return Utils.RemoveHtmlTags(string.Format(format, name));
var format = Utils.GetLocalizedResource("Fort.Cosmetics", "CosmeticItemDescription_SetMembership_NotRich", "\nPart of the {0} set.");
return string.Format(format, name);
}
protected (string, string, bool) GetInternalSID(string number)
protected (int, int) GetInternalSID(int number)
{
if (!Utils.TryLoadObject("FortniteGame/Plugins/GameFeatures/BattlePassBase/Content/DataTables/Athena_SeasonTitles.Athena_SeasonTitles", out UDataTable seasonTitles) ||
!seasonTitles.TryGetDataTableRow(number, StringComparison.InvariantCulture, out var row) ||
!row.TryGetValue(out FText chapterText, "DisplayChapterText") ||
!row.TryGetValue(out FText seasonText, "DisplaySeasonText") ||
!row.TryGetValue(out FName displayType, "DisplayType"))
return (string.Empty, string.Empty, true);
static int GetSeasonsInChapter(int chapter) => chapter switch
{
1 => 10,
2 => 8,
3 => 4,
4 => 5,
_ => 10
};
var onlySeason = displayType.Text.EndsWith("::OnlySeason") || (chapterText.Text == seasonText.Text && !int.TryParse(seasonText.Text, out _));
return (chapterText.Text, seasonText.Text, onlySeason);
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..];
(string chapterIdx, string seasonIdx, bool onlySeason) = GetInternalSID(s);
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 (onlySeason) return Utils.RemoveHtmlTags(string.Format(introduced, string.Format(season, seasonIdx)));
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}");
@ -249,15 +262,7 @@ public class BaseIcon : UCreator
return Utils.RemoveHtmlTags(string.Format(introduced, d));
}
protected void CheckGameplayTags(FInstancedStruct[] dataList)
{
if (dataList.FirstOrDefault(d => d.NonConstStruct?.TryGetValue(out FGameplayTagContainer _, "Tags") ?? false) is { NonConstStruct: not null } tags)
{
CheckGameplayTags(tags.NonConstStruct.Get<FGameplayTagContainer>("Tags"));
}
}
protected virtual void CheckGameplayTags(FGameplayTagContainer gameplayTags)
private void CheckGameplayTags(FGameplayTagContainer gameplayTags)
{
if (gameplayTags.TryGetGameplayTag("Cosmetics.Source.", out var source))
CosmeticSource = source.Text["Cosmetics.Source.".Length..];

View File

@ -84,80 +84,44 @@ public class BaseIconStats : BaseIcon
weaponStatHandle.TryGetValue(out UDataTable dataTable, "DataTable") &&
dataTable.TryGetDataTableRow(weaponRowName.Text, StringComparison.OrdinalIgnoreCase, out var weaponRowValue))
{
weaponRowValue.TryGetValue(out float dmgPb, "DmgPB"); //Damage at point blank
weaponRowValue.TryGetValue(out float mdpc, "MaxDamagePerCartridge"); //Max damage a weapon can do in a single hit, usually used for shotguns
weaponRowValue.TryGetValue(out float dmgCritical, "DamageZone_Critical"); //Headshot multiplier
weaponRowValue.TryGetValue(out int clipSize, "ClipSize"); //Item magazine size
weaponRowValue.TryGetValue(out float firingRate, "FiringRate"); //Item firing rate, value is shots per second
weaponRowValue.TryGetValue(out float swingTime, "SwingTime"); //Item swing rate, value is swing per second
weaponRowValue.TryGetValue(out float armTime, "ArmTime"); //Time it takes for traps to be able to be set off
weaponRowValue.TryGetValue(out float reloadTime, "ReloadTime"); //Time it takes for a weapon to reload
weaponRowValue.TryGetValue(out int bpc, "BulletsPerCartridge"); //Amount of pellets shot by a weapon at once, usually for shotguns
weaponRowValue.TryGetValue(out float heatMax, "OverheatingMaxValue"); //Maximum heat overheating weapons can hold before they need to cool off
weaponRowValue.TryGetValue(out float heatPerShot, "OverheatHeatingValue"); //Heat generated per shot on overheat weapons
weaponRowValue.TryGetValue(out float overheatCooldown, "OverheatedCooldownDelay"); //Cooldown after a weapon reaches its maximum heat capacity
weaponRowValue.TryGetValue(out int cartridgePerFire, "CartridgePerFire"); //Amount of bullets shot after pressing the fire button once
weaponRowValue.TryGetValue(out float burstFiringRate, "BurstFiringRate"); //Item firing rate during a burst, value is shots per second
if (weaponRowValue.TryGetValue(out int bpc, "BulletsPerCartridge"))
{
var multiplier = bpc != 0f ? bpc : 1;
if (dmgPb != 0f)
if (weaponRowValue.TryGetValue(out float dmgPb, "DmgPB") && dmgPb != 0f)
{
_statistics.Add(new IconStat(Utils.GetLocalizedResource("", "35D04D1B45737BEA25B69686D9E085B9", "Damage"), dmgPb * multiplier, 160));
_statistics.Add(new IconStat(Utils.GetLocalizedResource("", "35D04D1B45737BEA25B69686D9E085B9", "Damage"), dmgPb * multiplier, 200));
}
if (mdpc > 0f && dmgPb * dmgCritical * multiplier > mdpc)
if (weaponRowValue.TryGetValue(out float mdpc, "MaxDamagePerCartridge") && mdpc >= 0f)
{
_statistics.Add(new IconStat(Utils.GetLocalizedResource("", "0DEF2455463B008C4499FEA03D149EDF", "Headshot Damage"), mdpc, 160));
_statistics.Add(new IconStat(Utils.GetLocalizedResource("", "0DEF2455463B008C4499FEA03D149EDF", "Headshot Damage"), mdpc, 200));
}
else if (dmgCritical != 0f && dmgCritical != 1f && dmgPb != 0f)
else if (weaponRowValue.TryGetValue(out float dmgCritical, "DamageZone_Critical"))
{
_statistics.Add(new IconStat(Utils.GetLocalizedResource("", "0DEF2455463B008C4499FEA03D149EDF", "Headshot Damage"), dmgPb * dmgCritical * multiplier, 160));
_statistics.Add(new IconStat(Utils.GetLocalizedResource("", "0DEF2455463B008C4499FEA03D149EDF", "Headshot Damage"), dmgPb * dmgCritical * multiplier, 200));
}
}
if (clipSize > 999f || clipSize == 0f)
if (weaponRowValue.TryGetValue(out int clipSize, "ClipSize") && clipSize != 0)
{
_statistics.Add(new IconStat(Utils.GetLocalizedResource("", "068239DD4327B36124498C9C5F61C038", "Magazine Size"), Utils.GetLocalizedResource("", "0FAE8E5445029F2AA209ADB0FE49B23C", "Infinite"), -1));
_statistics.Add(new IconStat(Utils.GetLocalizedResource("", "068239DD4327B36124498C9C5F61C038", "Magazine Size"), clipSize, 50));
}
else if (clipSize != 0f)
if (weaponRowValue.TryGetValue(out float firingRate, "FiringRate") && firingRate != 0f)
{
_statistics.Add(new IconStat(Utils.GetLocalizedResource("", "068239DD4327B36124498C9C5F61C038", "Magazine Size"), clipSize, 40));
_statistics.Add(new IconStat(Utils.GetLocalizedResource("", "27B80BA44805ABD5A2D2BAB2902B250C", "Fire Rate"), firingRate, 15));
}
var burstEquation = cartridgePerFire != 0f && burstFiringRate != 0f ? (cartridgePerFire / (((cartridgePerFire - 1f) / burstFiringRate) + (1f / firingRate))) : 0f;
if (burstEquation != 0f)
if (weaponRowValue.TryGetValue(out float armTime, "ArmTime") && armTime != 0f)
{
_statistics.Add(new IconStat(Utils.GetLocalizedResource("", "27B80BA44805ABD5A2D2BAB2902B250C", "Fire Rate"), burstEquation, 11));
}
else if (firingRate != 0f)
{
_statistics.Add(new IconStat(Utils.GetLocalizedResource("", "27B80BA44805ABD5A2D2BAB2902B250C", "Fire Rate"), firingRate, 11));
}
else if (swingTime != 0f)
{
_statistics.Add(new IconStat(Utils.GetLocalizedResource("", "27B80BA44805ABD5A2D2BAB2902B250C", "Fire Rate"), swingTime, 11));
_statistics.Add(new IconStat(Utils.GetLocalizedResource("", "3BFEB8BD41A677CC5F45B9A90D6EAD6F", "Arming Delay"), armTime, 125));
}
if (armTime != 0f)
if (weaponRowValue.TryGetValue(out float reloadTime, "ReloadTime") && reloadTime != 0f)
{
_statistics.Add(new IconStat(Utils.GetLocalizedResource("", "3BFEB8BD41A677CC5F45B9A90D6EAD6F", "Arming Delay"), armTime, 5));
_statistics.Add(new IconStat(Utils.GetLocalizedResource("", "6EA26D1A4252034FBD869A90F9A6E49A", "Reload Time"), reloadTime, 15));
}
if (reloadTime != 0f && clipSize < 999f && clipSize != 0f)
{
_statistics.Add(new IconStat(Utils.GetLocalizedResource("", "6EA26D1A4252034FBD869A90F9A6E49A", "Reload Time"), reloadTime, 10));
}
if (overheatCooldown != 0f && clipSize > 999f || overheatCooldown != 0f && clipSize == 0f)
{
_statistics.Add(new IconStat("Overheat Cooldown", overheatCooldown, 5));
}
if (heatMax != 0f && heatPerShot != 0f && clipSize > 999f || heatMax != 0f && heatPerShot != 0f && clipSize == 0f)
{
_statistics.Add(new IconStat("Shots to Overheat", Math.Ceiling(heatMax / heatPerShot), 80));
}
if ((Object.ExportType.Equals("FortContextTrapItemDefinition", StringComparison.OrdinalIgnoreCase) ||
Object.ExportType.Equals("FortTrapItemDefinition", StringComparison.OrdinalIgnoreCase)) &&
weaponRowValue.TryGetValue(out UDataTable durabilityTable, "Durability") &&
@ -251,7 +215,6 @@ public class BaseIconStats : BaseIcon
_informationPaint.TextSize = 50;
_informationPaint.Color = SKColors.White;
_informationPaint.Typeface = Utils.Typefaces.Bundle;
_informationPaint.FakeBoldText = true;
while (_informationPaint.MeasureText(DisplayName) > Width - _headerHeight * 2)
{
_informationPaint.TextSize -= 1;
@ -321,10 +284,6 @@ public class IconStat
_statPaint.Color = SKColors.White;
c.DrawText(_value.ToString(), new SKPoint(width - 50, y + 10), _statPaint);
if (_maxValue == -1) //fill bar if max value is set to -1, for things that don't return a number here but should still be represented as the maximum value
{
c.DrawRect(new SKRect(height * 2, y, Math.Min(width - height, sliderRight), y + 5), _statPaint);
}
if (_maxValue < 1 || !float.TryParse(_value.ToString(), out var floatValue)) return;
if (floatValue < 0)
floatValue = 0;

View File

@ -28,7 +28,7 @@ public class BaseJuno : BaseIcon
{
foreach (var data in additionalData)
{
if (data.NonConstStruct?.TryGetValue(out FSoftObjectPath largePreview, "LargePreviewImage", "SmallPreviewImage") == true)
if (data.NonConstStruct?.TryGetValue(out FSoftObjectPath largePreview, "LargePreviewImage", "SmallPreviewImage") ?? false)
{
_character.Preview = Utils.GetBitmap(largePreview);
break;

View File

@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using CUE4Parse.UE4.Assets.Exports;
using CUE4Parse.UE4.Assets.Exports.Engine;
@ -8,7 +7,7 @@ using CUE4Parse.UE4.Assets.Exports.Texture;
using CUE4Parse.UE4.Assets.Objects;
using CUE4Parse.UE4.Objects.Core.i18N;
using CUE4Parse.UE4.Objects.UObject;
using CUE4Parse.Utils;
using FModel.Extensions;
using FModel.Framework;
using SkiaSharp;
using SkiaSharp.HarfBuzz;
@ -18,8 +17,9 @@ namespace FModel.Creator.Bases.FN;
public class BaseQuest : BaseIcon
{
private int _count;
private readonly List<Reward> _rewards;
private Reward _reward;
private readonly bool _screenLayer;
private readonly string[] _unauthorizedReward = { "Token", "ChallengeBundle", "GiftBox" };
public string NextQuestName { get; private set; }
@ -27,15 +27,15 @@ public class BaseQuest : BaseIcon
{
Margin = 0;
Width = 1024;
Height = 200;
_rewards = [];
Height = 256;
DefaultPreview = Utils.GetBitmap("FortniteGame/Content/Athena/HUD/Quests/Art/T_NPC_Default.T_NPC_Default");
if (uObject != null)
{
_screenLayer = uObject.ExportType.Equals("FortFeatItemDefinition", StringComparison.OrdinalIgnoreCase);
}
}
public BaseQuest(int completionCount, EIconStyle style) : this(null, style) // completion
private BaseQuest(int completionCount, EIconStyle style) : this(null, style) // completion
{
var description = completionCount < 0 ?
Utils.GetLocalizedResource("AthenaChallengeDetailsEntry", "CompletionRewardFormat_All", "Complete <text color=\"FFF\" case=\"upper\" fontface=\"black\">all {0} challenges</> to earn the reward item") :
@ -44,14 +44,14 @@ public class BaseQuest : BaseIcon
DisplayName = ReformatString(description, completionCount.ToString(), completionCount < 0);
}
public void AddCompletionReward(FSoftObjectPath itemDefinition)
public BaseQuest(int completionCount, FSoftObjectPath itemDefinition, EIconStyle style) : this(completionCount, style) // completion
{
_rewards.Add(itemDefinition.TryLoad(out UObject uObject) ? new Reward(uObject) : new Reward());
_reward = Utils.TryLoadObject(itemDefinition.AssetPathName.Text, out UObject uObject) ? new Reward(uObject) : new Reward();
}
public void AddCompletionReward(int quantity, string reward)
public BaseQuest(int completionCount, int quantity, string reward, EIconStyle style) : this(completionCount, style) // completion
{
_rewards.Add(new Reward(quantity, reward));
_reward = new Reward(quantity, reward);
}
public override void ParseForInfo()
@ -69,7 +69,12 @@ public class BaseQuest : BaseIcon
}
else
{
Description = string.Empty;
if (!string.IsNullOrEmpty(ShortDescription))
Description = ShortDescription;
if (string.IsNullOrEmpty(DisplayName) && !string.IsNullOrEmpty(Description))
DisplayName = Description;
if (DisplayName == Description)
Description = string.Empty;
if ((Object.TryGetValue(out FSoftObjectPath icon, "QuestGiverWidgetIcon", "NotificationIconOverride") &&
Utils.TryLoadObject(icon.AssetPathName.Text, out UObject iconObject)) ||
@ -93,8 +98,8 @@ public class BaseQuest : BaseIcon
if (Object.TryGetValue(out FStructFallback[] objectives, "Objectives") && objectives.Length > 0)
{
// actual description doesn't exist
if (string.IsNullOrEmpty(DisplayName) && objectives[0].TryGetValue(out FText description, "Description"))
DisplayName = description.Text;
if (string.IsNullOrEmpty(Description) && objectives[0].TryGetValue(out FText description, "Description"))
Description = description.Text;
// ObjectiveCompletionCount doesn't exist
if (_count == 0)
@ -106,65 +111,77 @@ public class BaseQuest : BaseIcon
}
}
if (Object.TryGetValue(out FPackageIndex[] questDefinitionComponents, "QuestDefinitionComponents"))
if (Object.TryGetValue(out FStructFallback[] rewards, "Rewards"))
{
foreach (var questDefinitionComponent in questDefinitionComponents)
foreach (var reward in rewards)
{
if (!questDefinitionComponent.Name.StartsWith("FortQuestDefinitionComponent_Rewards") ||
!questDefinitionComponent.TryLoad(out var rewardComponent) ||
!rewardComponent.TryGetValue(out FInstancedStruct[] questRewardsArray, "QuestRewardsArray")) continue;
if (!reward.TryGetValue(out FStructFallback itemPrimaryAssetId, "ItemPrimaryAssetId") ||
!reward.TryGetValue(out int quantity, "Quantity")) continue;
foreach (var questReward in questRewardsArray)
if (!itemPrimaryAssetId.TryGetValue(out FStructFallback primaryAssetType, "PrimaryAssetType") ||
!itemPrimaryAssetId.TryGetValue(out FName primaryAssetName, "PrimaryAssetName") ||
!primaryAssetType.TryGetValue(out FName name, "Name")) continue;
if (name.Text.Equals("Quest", StringComparison.OrdinalIgnoreCase))
{
if (questReward.NonConstStruct.TryGetValue(out FStructFallback[] resourceDataTableRewards, "ResourceDataTableRewards") &&
resourceDataTableRewards.Length > 0 && resourceDataTableRewards[0].TryGetValue(out FStructFallback tableRowEntry, "TableRowEntry") &&
tableRowEntry.TryGetValue(out UDataTable rewardsTable, "DataTable") &&
tableRowEntry.TryGetValue(out FName rowName, "RowName") &&
rewardsTable.TryGetDataTableRow(rowName.Text, StringComparison.InvariantCulture, out var row) &&
row.TryGetValue(out FSoftObjectPath resourceDefinition, "ResourceDefinition") &&
row.TryGetValue(out int quantity, "Quantity"))
{
_rewards.Add(new Reward(quantity, resourceDefinition));
}
else if (questReward.NonConstStruct.TryGetValue(out FInstancedStruct[] rewards, "CosmeticRewards", "CurrencyRewards", "VariantTokenRewards", "ResourceRewards"))
{
foreach (var reward in rewards)
{
if (reward.NonConstStruct.TryGetValue(out FSoftObjectPath cosmeticDefinition, "CosmeticDefinition", "CurrencyDefinition", "VariantTokenDefinition", "ResourceDefinition"))
{
if (reward.NonConstStruct.TryGetValue(out int count, "CurrencyCount", "ResourceCount"))
{
_rewards.Add(new Reward(count, cosmeticDefinition));
}
else if (cosmeticDefinition.TryLoad(out var cosmetic))
{
_rewards.Add(new Reward(cosmetic));
}
}
}
}
NextQuestName = primaryAssetName.Text;
}
else if (!_unauthorizedReward.Contains(name.Text))
{
_reward = new Reward(quantity, $"{name}:{primaryAssetName}");
}
}
}
if (_reward == null)
{
FName rowName = null;
if (Object.TryGetValue(out UDataTable rewardsTable, "RewardsTable"))
rowName = new FName("Default");
else if (Object.TryGetValue(out FStructFallback[] rewardTableRows, "IndividualRewardTableRows") &&
rewardTableRows.Length > 0 && rewardTableRows[0].TryGetValue(out rowName, "RowName") &&
rewardTableRows[0].TryGetValue(out rewardsTable, "DataTable")) {}
if (rewardsTable != null && rowName != null && rewardsTable.TryGetDataTableRow(rowName.Text, StringComparison.InvariantCulture, out var row))
{
if (row.TryGetValue(out FName templateId, "TemplateId") &&
row.TryGetValue(out int quantity, "Quantity"))
{
_reward = new Reward(quantity, templateId);
}
}
}
if (_reward == null && Object.TryGetValue(out FStructFallback[] hiddenRewards, "HiddenRewards"))
{
foreach (var hiddenReward in hiddenRewards)
{
if (!hiddenReward.TryGetValue(out FName templateId, "TemplateId") ||
!hiddenReward.TryGetValue(out int quantity, "Quantity")) continue;
_reward = new Reward(quantity, templateId);
break;
}
}
_reward ??= new Reward();
}
public void DrawQuest(SKCanvas c, int y)
{
var x = Preview is null ? 0 : Height + 10;
DrawBackground(c, x, y);
DrawBackground(c, y);
DrawPreview(c, y);
DrawTexts(c, x + 50, y, 50);
DrawTexts(c, y);
}
public override SKBitmap[] Draw()
{
var ret = new SKBitmap(Width, Height, SKColorType.Rgba8888, SKAlphaType.Unpremul);
var ret = new SKBitmap(Width, Height, SKColorType.Rgba8888, SKAlphaType.Opaque);
using var c = new SKCanvas(ret);
DrawQuest(c, 0);
return [ret];
return new[] { ret };
}
private string ReformatString(string s, string completionCount, bool isAll)
@ -185,77 +202,77 @@ public class BaseQuest : BaseIcon
private readonly SKPaint _informationPaint = new()
{
IsAntialias = true, FilterQuality = SKFilterQuality.High,
Color = SKColor.Parse("#183E94"),
Color = SKColor.Parse("#262630")
};
private void DrawBackground(SKCanvas c, int x, int y)
private void DrawBackground(SKCanvas c, int y)
{
_informationPaint.Shader = SKShader.CreateRadialGradient(new SKPoint(Width * 0.75f, y + Height * 0.5f), Width * 0.75f,
[SKColor.Parse("#1565D0"), SKColor.Parse("#1B1150")], SKShaderTileMode.Clamp);
c.DrawRoundRect(new SKRect(x, y, Width, y + Height), 25, 25, _informationPaint);
c.DrawRect(new SKRect(Margin, y, Width, y + Height), _informationPaint);
_informationPaint.Shader = SKShader.CreateRadialGradient(new SKPoint(Width / 2, y + Height / 2), Width / 5 * 4,
new[] { Background[0].WithAlpha(50), Background[1].WithAlpha(50) }, SKShaderTileMode.Clamp);
c.DrawRect(new SKRect(Height / 2, y, Width, y + Height), _informationPaint);
_informationPaint.Shader = SKShader.CreateLinearGradient(new SKPoint(Width / 2, y + Height), new SKPoint(Width / 2, 75),
new[] { SKColors.Black.WithAlpha(25), Background[1].WithAlpha(0) }, SKShaderTileMode.Clamp);
c.DrawRect(new SKRect(0, y + 75, Width, y + Height), _informationPaint);
_informationPaint.Shader = SKShader.CreateRadialGradient(new SKPoint(Width / 2, y + Height / 2), Width / 5 * 4, Background, SKShaderTileMode.Clamp);
c.DrawRect(new SKRect(Margin, y, Height / 2, y + Height), _informationPaint);
_informationPaint.Shader = SKShader.CreateLinearGradient(new SKPoint(Height / 2, y + Height / 2), new SKPoint(Height / 2 + 100, y + Height / 2),
new[] { SKColors.Black.WithAlpha(25), Background[1].WithAlpha(0) }, SKShaderTileMode.Clamp);
c.DrawRect(new SKRect(Height / 2, y, Height / 2 + 100, y + Height), _informationPaint);
}
private void DrawPreview(SKCanvas c, int y)
{
if (Preview is null) return;
ImagePaint.BlendMode = _screenLayer ? SKBlendMode.Screen : Preview == null ? SKBlendMode.ColorBurn : SKBlendMode.SrcOver;
var rect = new SKRect(0, y, Height, y + Height);
c.Save();
using (var roundRectPath = new SKPath())
{
roundRectPath.AddRoundRect(rect, 15, 15);
c.ClipPath(roundRectPath, antialias: true);
}
_informationPaint.Shader = SKShader.CreateLinearGradient(
new SKPoint(rect.Left, rect.Bottom),
new SKPoint(rect.Left, rect.Top),
[
_informationPaint.Color,
_informationPaint.Color.WithAlpha(0)
],
[0, 1],
SKShaderTileMode.Clamp
);
c.DrawRect(rect, _informationPaint);
c.DrawBitmap(Preview, rect, ImagePaint);
c.Restore();
c.DrawBitmap(Preview ?? DefaultPreview, new SKRect(Margin, y, Height - Margin, y + Height), ImagePaint);
}
private void DrawTexts(SKCanvas c, int x, int y, int padding)
private void DrawTexts(SKCanvas c, int y)
{
_informationPaint.Shader = null;
_informationPaint.Color = SKColors.White;
float maxX = Width - padding;
float steps = Height * 0.5f;
foreach (var reward in _rewards)
{
reward.DrawQuest(c, new SKRect(maxX - steps, y + padding, maxX, y + padding + steps));
maxX -= steps;
}
maxX -= steps * 0.5f;
if (!string.IsNullOrWhiteSpace(DisplayName))
{
_informationPaint.TextSize = 25;
_informationPaint.TextSize = 40;
_informationPaint.Color = SKColors.White;
_informationPaint.Typeface = Utils.Typefaces.Bundle;
Utils.DrawMultilineText(c, DisplayName, Width - padding, 0, SKTextAlign.Left,
new SKRect(x, y + padding, maxX, Height - padding * 1.5f), _informationPaint, out _);
while (_informationPaint.MeasureText(DisplayName) > Width - Height - 10)
{
_informationPaint.TextSize -= 1;
}
var shaper = new CustomSKShaper(_informationPaint.Typeface);
c.DrawShapedText(shaper, DisplayName, Height, y + 50, _informationPaint);
}
maxX -= steps * 0.5f;
if (maxX > Width * 0.7f) maxX = Width * 0.7f;
var outY = y + 75f;
if (!string.IsNullOrWhiteSpace(Description))
{
_informationPaint.TextSize = 16;
_informationPaint.Color = SKColors.White.WithAlpha(175);
_informationPaint.Typeface = Utils.Typefaces.Description;
Utils.DrawMultilineText(c, Description, Width - Height, y, SKTextAlign.Left,
new SKRect(Height, outY, Width - 10, y + Height), _informationPaint, out outY);
}
_informationPaint.Color = Border[0].WithAlpha(100);
c.DrawRect(new SKRect(Height, outY, Width - 150, outY + 5), _informationPaint);
if (_count > 0)
{
_informationPaint.TextSize = 20;
_informationPaint.TextSize = 25;
_informationPaint.Color = SKColors.White;
_informationPaint.Typeface = Utils.Typefaces.BundleNumber;
c.DrawText($"0/{_count}", new SKPoint(maxX, y + Height - padding), _informationPaint);
c.DrawText("0 / ", new SKPoint(Width - 130, outY + 10), _informationPaint);
_informationPaint.Color = Border[0];
c.DrawText(_count.ToString(), new SKPoint(Width - 95, outY + 10), _informationPaint);
}
_informationPaint.Color = SKColor.Parse("#121A45").WithAlpha(200);
c.DrawRoundRect(new SKRect(x, y + Height - padding - 12.5f, maxX - 12.5f, y + Height - padding), 5, 5, _informationPaint);
_reward.DrawQuest(c, new SKRect(Height, outY + 25, Width - 20, y + Height - 25));
}
}

View File

@ -19,18 +19,6 @@ public class Reward
_rewardQuantity = "x0";
}
public Reward(int quantity, FSoftObjectPath softObjectPath) : this()
{
_rewardQuantity = $"{quantity / 1000f}k";
if (softObjectPath.TryLoad(out UObject d))
{
_theReward = new BaseIcon(d, EIconStyle.Default);
_theReward.ParseForReward(false);
_theReward.Border[0] = SKColors.White;
}
}
public Reward(int quantity, FName primaryAssetName) : this(quantity, primaryAssetName.Text)
{
}
@ -45,7 +33,7 @@ public class Reward
if (parts[0].Equals("HomebaseBannerIcon", StringComparison.CurrentCultureIgnoreCase))
{
if (!Utils.TryLoadObject($"FortniteGame/Plugins/GameFeatures/BRCosmetics/Content/Items/BannerIcons/{parts[1]}.{parts[1]}", out UObject p))
if (!Utils.TryLoadObject($"FortniteGame/Content/Items/BannerIcons/{parts[1]}.{parts[1]}", out UObject p))
return;
_theReward = new BaseIcon(p, EIconStyle.Default);
@ -63,6 +51,7 @@ public class Reward
_theReward = new BaseIcon(uObject, EIconStyle.Default);
_theReward.ParseForReward(false);
_theReward.Border[0] = SKColors.White;
_rewardQuantity = _theReward.DisplayName;
}
private readonly SKPaint _rewardPaint = new()
@ -72,30 +61,28 @@ public class Reward
public void DrawQuest(SKCanvas c, SKRect rect)
{
_rewardPaint.TextSize = 25;
_rewardPaint.TextSize = 50;
if (HasReward())
{
var origin = new SKPoint(rect.Left, rect.Top);
if (!string.IsNullOrEmpty(_rewardQuantity))
c.DrawBitmap((_theReward.Preview ?? _theReward.DefaultPreview).Resize((int) rect.Height), new SKPoint(rect.Left, rect.Top), _rewardPaint);
_rewardPaint.Color = _theReward.Border[0];
_rewardPaint.Typeface = _rewardQuantity.StartsWith("x") ? Utils.Typefaces.BundleNumber : Utils.Typefaces.Bundle;
_rewardPaint.ImageFilter = SKImageFilter.CreateDropShadow(0, 0, 5, 5, _theReward.Background[0].WithAlpha(150));
while (_rewardPaint.MeasureText(_rewardQuantity) > rect.Width)
{
origin.Y -= _rewardPaint.TextSize / 2;
_rewardPaint.TextSize -= 1;
}
c.DrawBitmap((_theReward.Preview ?? _theReward.DefaultPreview).Resize((int) rect.Height), origin, _rewardPaint);
if (string.IsNullOrEmpty(_rewardQuantity)) return;
_rewardPaint.TextAlign = SKTextAlign.Center;
_rewardPaint.FakeBoldText = true;
_rewardPaint.Color = SKColors.White;
var shaper = new CustomSKShaper(Utils.Typefaces.BundleNumber);
c.DrawShapedText(shaper, _rewardQuantity, rect.Left + rect.Width * 0.5f, rect.Bottom, _rewardPaint);
var shaper = new CustomSKShaper(_rewardPaint.Typeface);
c.DrawShapedText(shaper, _rewardQuantity, rect.Left + rect.Height + 25, rect.MidY + 20, _rewardPaint);
}
else
{
_rewardPaint.Color = SKColors.White;
_rewardPaint.Typeface = Utils.Typefaces.BundleNumber;
c.DrawText("No Reward", new SKPoint(rect.Left, rect.MidY + 20), _rewardPaint);
}
// else
// {
// _rewardPaint.Color = SKColors.White;
// _rewardPaint.Typeface = Utils.Typefaces.BundleNumber;
// c.DrawText("No Reward", new SKPoint(rect.Left, rect.MidY + 20), _rewardPaint);
// }
}
public void DrawSeasonWin(SKCanvas c, int size)

View File

@ -1,24 +1,21 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using CUE4Parse.UE4.Assets.Exports;
using FModel.Creator.Bases;
using FModel.Creator.Bases.BB;
using FModel.Creator.Bases.FN;
using FModel.Creator.Bases.MV;
using FModel.Creator.Bases.SB;
namespace FModel.Creator;
public class CreatorPackage : IDisposable
{
private string _pkgName;
private string _exportType;
private Lazy<UObject> _object;
private UObject _object;
private EIconStyle _style;
public CreatorPackage(string packageName, string exportType, Lazy<UObject> uObject, EIconStyle style)
public CreatorPackage(UObject uObject, EIconStyle style)
{
_pkgName = packageName;
_exportType = exportType;
_object = uObject;
_style = style;
}
@ -31,12 +28,12 @@ public class CreatorPackage : IDisposable
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryConstructCreator([MaybeNullWhen(false)] out UCreator creator)
public bool TryConstructCreator(out UCreator creator)
{
// TODO: convert to a type based system
switch (_exportType)
switch (_object.ExportType)
{
// Fortnite
case "FortCreativeWeaponMeleeItemDefinition":
case "AthenaConsumableEmoteItemDefinition":
case "AthenaSkyDiveContrailItemDefinition":
case "AthenaLoadingScreenItemDefinition":
@ -47,13 +44,9 @@ public class CreatorPackage : IDisposable
case "AthenaCharacterItemDefinition":
case "AthenaMapMarkerItemDefinition":
case "AthenaBackpackItemDefinition":
case "CosmeticShoesItemDefinition":
case "CosmeticCompanionItemDefinition":
case "CosmeticCompanionReactFXItemDefinition":
case "AthenaPickaxeItemDefinition":
case "AthenaGadgetItemDefinition":
case "AthenaGliderItemDefinition":
case "AthenaHatItemDefinition":
case "AthenaSprayItemDefinition":
case "AthenaDanceItemDefinition":
case "AthenaEmojiItemDefinition":
@ -95,7 +88,6 @@ public class CreatorPackage : IDisposable
case "FortBackpackItemDefinition":
case "FortEventQuestMapDataAsset":
case "FortBuildingItemDefinition":
case "FortItemCacheItemDefinition":
case "FortWeaponModItemDefinition":
case "FortCodeTokenItemDefinition":
case "FortSchematicItemDefinition":
@ -108,7 +100,7 @@ public class CreatorPackage : IDisposable
case "FortConsumableItemDefinition":
case "StWFortAccoladeItemDefinition":
case "FortAccountBuffItemDefinition":
case "FortFOBCoreDecoItemDefinition":
case "FortWeaponMeleeItemDefinition":
case "FortPlayerPerksItemDefinition":
case "FortPlaysetPropItemDefinition":
case "FortPrerollDataItemDefinition":
@ -118,7 +110,6 @@ public class CreatorPackage : IDisposable
case "FortPlayerAugmentItemDefinition":
case "FortSmartBuildingItemDefinition":
case "FortGiftBoxUnlockItemDefinition":
case "FortCreativeGadgetItemDefinition":
case "FortWeaponModItemDefinitionOptic":
case "RadioContentSourceItemDefinition":
case "FortPlaysetGrenadeItemDefinition":
@ -143,7 +134,9 @@ public class CreatorPackage : IDisposable
case "FortCampaignHeroLoadoutItemDefinition":
case "FortConditionalResourceItemDefinition":
case "FortChallengeBundleScheduleDefinition":
case "FortWeaponMeleeDualWieldItemDefinition":
case "FortDailyRewardScheduleTokenDefinition":
case "FortCreativeWeaponRangedItemDefinition":
case "FortVehicleCosmeticsItemDefinition_Body":
case "FortVehicleCosmeticsItemDefinition_Skin":
case "FortVehicleCosmeticsItemDefinition_Wheel":
@ -155,46 +148,43 @@ public class CreatorPackage : IDisposable
case "FortVehicleCosmeticsItemDefinition_EngineAudio":
creator = _style switch
{
EIconStyle.Cataba => new BaseCommunity(_object.Value, _style, "Cataba"),
_ => new BaseIcon(_object.Value, _style)
EIconStyle.Cataba => new BaseCommunity(_object, _style, "Cataba"),
_ => new BaseIcon(_object, _style)
};
return true;
case "JunoAthenaCharacterItemOverrideDefinition":
case "JunoAthenaDanceItemOverrideDefinition":
creator = new BaseJuno(_object.Value, _style);
creator = new BaseJuno(_object, _style);
return true;
case "FortTandemCharacterData":
creator = new BaseTandem(_object.Value, _style);
creator = new BaseTandem(_object, _style);
return true;
case "FortTrapItemDefinition":
case "FortSpyTechItemDefinition":
case "FortAccoladeItemDefinition":
case "FortContextTrapItemDefinition":
case "FortWeaponMeleeItemDefinition":
case "FortWeaponRangedItemDefinition":
case "FortCreativeWeaponMeleeItemDefinition":
case "FortWeaponMeleeDualWieldItemDefinition":
case "FortCreativeWeaponRangedItemDefinition":
case "Daybreak_LevelExitVehicle_PartItemDefinition_C":
creator = new BaseIconStats(_object.Value, _style);
creator = new BaseIconStats(_object, _style);
return true;
case "FortItemSeriesDefinition":
creator = new BaseSeries(_object.Value, _style);
creator = new BaseSeries(_object, _style);
return true;
case "MaterialInstanceConstant"
when _pkgName.Contains("/MI_OfferImages/", StringComparison.OrdinalIgnoreCase) ||
_pkgName.Contains("/RenderSwitch_Materials/", StringComparison.OrdinalIgnoreCase) ||
_pkgName.Contains("/MI_BPTile/", StringComparison.OrdinalIgnoreCase):
creator = new BaseMaterialInstance(_object.Value, _style);
when _object.Owner != null &&
(_object.Owner.Name.Contains("/MI_OfferImages/", StringComparison.OrdinalIgnoreCase) ||
_object.Owner.Name.EndsWith($"/RenderSwitch_Materials/{_object.Name}", StringComparison.OrdinalIgnoreCase) ||
_object.Owner.Name.EndsWith($"/MI_BPTile/{_object.Name}", StringComparison.OrdinalIgnoreCase)):
creator = new BaseMaterialInstance(_object, _style);
return true;
case "AthenaItemShopOfferDisplayData":
creator = new BaseOfferDisplayData(_object.Value, _style);
creator = new BaseOfferDisplayData(_object, _style);
return true;
case "FortMtxOfferData":
creator = new BaseMtxOffer(_object.Value, _style);
creator = new BaseMtxOffer(_object, _style);
return true;
case "FortPlaylistAthena":
creator = new BasePlaylist(_object.Value, _style);
creator = new BasePlaylist(_object, _style);
return true;
case "FortFeatItemDefinition":
case "FortQuestItemDefinition":
@ -202,18 +192,17 @@ public class CreatorPackage : IDisposable
case "FortQuestItemDefinition_Campaign":
case "AthenaDailyQuestDefinition":
case "FortUrgentQuestItemDefinition":
creator = new Bases.FN.BaseQuest(_object.Value, _style);
creator = new Bases.FN.BaseQuest(_object, _style);
return true;
case "FortCompendiumItemDefinition":
case "FortCompendiumBundleDefinition":
case "FortChallengeBundleItemDefinition":
creator = new BaseBundle(_object.Value, _style);
creator = new BaseBundle(_object, _style);
return true;
// case "AthenaSeasonItemDefinition":
// creator = new BaseSeason(_object, _style);
// return true;
case "FortItemAccessTokenType":
creator = new BaseItemAccessToken(_object.Value, _style);
creator = new BaseItemAccessToken(_object, _style);
return true;
case "FortCreativeOption":
case "PlaylistUserOptionEnum":
@ -227,14 +216,14 @@ public class CreatorPackage : IDisposable
case "PlaylistUserTintedIconIntEnum":
case "PlaylistUserOptionPrimaryAsset":
case "PlaylistUserOptionCollisionProfileEnum":
creator = new BaseUserControl(_object.Value, _style);
creator = new BaseUserControl(_object, _style);
return true;
// PandaGame
case "CharacterData":
creator = new BaseFighter(_object.Value, _style);
creator = new BaseFighter(_object, _style);
return true;
case "PerkGroup":
creator = new BasePerkGroup(_object.Value, _style);
creator = new BasePerkGroup(_object, _style);
return true;
case "StatTrackingBundleData":
case "HydraSyncedDataAsset":
@ -247,10 +236,10 @@ public class CreatorPackage : IDisposable
case "TauntData":
case "SkinData":
case "PerkData":
creator = new BasePandaIcon(_object.Value, _style);
creator = new BasePandaIcon(_object, _style);
return true;
case "QuestData":
creator = new Bases.MV.BaseQuest(_object.Value, _style);
creator = new Bases.MV.BaseQuest(_object, _style);
return true;
default:
creator = null;
@ -258,7 +247,7 @@ public class CreatorPackage : IDisposable
}
}
public override string ToString() => $"{_exportType} | {_style}";
public override string ToString() => $"{_object.ExportType} | {_style}";
public void Dispose()
{

View File

@ -14,7 +14,7 @@ public class Typefaces
private const string _EXT = ".ufont";
// FortniteGame
private const string _FORTNITE_BASE_PATH = "FortniteGame/Plugins/FortUILibrary/Content/Fonts/";
private const string _FORTNITE_BASE_PATH = "/Game/UI/Foundation/Fonts/";
private const string _ASIA_ERINM = "AsiaERINM"; // korean fortnite
private const string _BURBANK_BIG_CONDENSED_BLACK = "BurbankBigCondensed-Black"; // russian
private const string _BURBANK_BIG_REGULAR_BLACK = "BurbankBigRegular-Black";
@ -67,7 +67,7 @@ public class Typefaces
Default = SKTypeface.FromStream(Application.GetResourceStream(_BURBANK_BIG_CONDENSED_BOLD)?.Stream);
switch (viewModel.Provider.ProjectName.ToUpperInvariant())
switch (viewModel.Provider.InternalGameName.ToUpperInvariant())
{
case "FORTNITEGAME":
{
@ -105,7 +105,7 @@ public class Typefaces
_ => _BURBANK_SMALL_BOLD
} + _EXT, true);
BundleNumber = OnTheFly(_FORTNITE_BASE_PATH + _BURBANK_SMALL_BOLD + _EXT);
BundleNumber = OnTheFly(_FORTNITE_BASE_PATH + _BURBANK_BIG_CONDENSED_BLACK + _EXT);
Bundle = OnTheFly(_FORTNITE_BASE_PATH +
language switch
@ -116,7 +116,7 @@ public class Typefaces
ELanguage.Arabic => _NOTO_SANS_ARABIC_BLACK,
ELanguage.TraditionalChinese => _NOTO_SANS_TC_BLACK,
ELanguage.Chinese => _NOTO_SANS_SC_BLACK,
_ => _BURBANK_SMALL_BOLD
_ => string.Empty
} + _EXT, true) ?? BundleNumber;
TandemDisplayName = OnTheFly(_FORTNITE_BASE_PATH +
@ -193,7 +193,7 @@ public class Typefaces
public SKTypeface OnTheFly(string path, bool fallback = false)
{
if (!_viewModel.Provider.TrySaveAsset(path, out var data)) return fallback ? null : Default;
var m = new MemoryStream(data) { Position = _viewModel.Provider.Versions.Game >= EGame.GAME_UE5_6 ? 4 : 0 };
var m = new MemoryStream(data) { Position = 0 };
return SKTypeface.FromStream(m);
}
}

View File

@ -12,7 +12,6 @@ using CUE4Parse.UE4.Objects.UObject;
using CUE4Parse.UE4.Versions;
using CUE4Parse_Conversion.Textures;
using CUE4Parse.UE4.Assets.Objects;
using CUE4Parse.Utils;
using FModel.Framework;
using FModel.Extensions;
using FModel.Services;
@ -128,9 +127,9 @@ public static class Utils
}
public static SKBitmap GetB64Bitmap(string b64) => SKBitmap.Decode(new MemoryStream(Convert.FromBase64String(b64)) { Position = 0 });
public static SKBitmap GetBitmap(FSoftObjectPath softObjectPath) => GetBitmap(softObjectPath.Load<UTexture2D>());
public static SKBitmap GetBitmap(FSoftObjectPath softObjectPath) => GetBitmap(softObjectPath.AssetPathName.Text);
public static SKBitmap GetBitmap(string fullPath) => TryLoadObject(fullPath, out UTexture2D texture) ? GetBitmap(texture) : null;
public static SKBitmap GetBitmap(UTexture2D texture) => texture.Decode(UserSettings.Default.CurrentDir.TexturePlatform).ToSkBitmap();
public static SKBitmap GetBitmap(UTexture2D texture) => texture.Decode(UserSettings.Default.CurrentDir.TexturePlatform);
public static SKBitmap GetBitmap(byte[] data) => SKBitmap.Decode(data);
public static SKBitmap ResizeWithRatio(this SKBitmap me, double width, double height)
@ -161,7 +160,12 @@ public static class Utils
// fullpath must be either without any extension or with the export objectname
public static bool TryLoadObject<T>(string fullPath, out T export) where T : UObject
{
return _applicationView.CUE4Parse.Provider.TryLoadPackageObject(fullPath, out export);
return _applicationView.CUE4Parse.Provider.TryLoadObject(fullPath, out export);
}
public static IEnumerable<UObject> LoadExports(string packagePath)
{
return _applicationView.CUE4Parse.Provider.LoadAllObjects(packagePath);
}
public static float GetMaxFontSize(double sectorSize, SKTypeface typeface, string text, float degreeOfCertainty = 1f, float maxFont = 100f)
@ -195,11 +199,11 @@ public static class Utils
public static string GetLocalizedResource(string @namespace, string key, string defaultValue)
{
return _applicationView.CUE4Parse.Provider.Internationalization.SafeGet(@namespace, key, defaultValue);
return _applicationView.CUE4Parse.Provider.GetLocalizedString(@namespace, key, defaultValue);
}
public static string GetLocalizedResource<T>(T @enum) where T : Enum
{
var resource = _applicationView.CUE4Parse.Provider.Internationalization.SafeGet("", @enum.GetDescription(), @enum.ToString());
var resource = _applicationView.CUE4Parse.Provider.GetLocalizedString("", @enum.GetDescription(), @enum.ToString());
return CultureInfo.CurrentCulture.TextInfo.ToTitleCase(resource.ToLower());
}
@ -413,4 +417,4 @@ public static class Utils
return ret;
}
}
}

View File

@ -1,6 +1,5 @@
using System;
using System.ComponentModel;
using FModel.Extensions;
namespace FModel;
@ -61,11 +60,19 @@ public enum ELoadingMode
[Description("All (New)")]
AllButNew,
[Description("All (Modified)")]
AllButModified,
[Description("All (Except Patched Assets)")]
AllButPatched,
AllButModified
}
// public enum EUpdateMode
// {
// [Description("Stable")]
// Stable,
// [Description("Beta")]
// Beta,
// [Description("QA Testing")]
// Qa
// }
public enum ECompressedAudio
{
[Description("Play the decompressed data")]
@ -105,46 +112,5 @@ public enum EBulkType
Textures = 1 << 2,
Meshes = 1 << 3,
Skeletons = 1 << 4,
Animations = 1 << 5,
Audio = 1 << 6
}
public enum EAssetCategory : uint
{
All = AssetCategoryExtensions.CategoryBase + (0 << 16),
Blueprints = AssetCategoryExtensions.CategoryBase + (1 << 16),
BlueprintGeneratedClass = Blueprints + 1,
WidgetBlueprintGeneratedClass = Blueprints + 2,
AnimBlueprintGeneratedClass = Blueprints + 3,
RigVMBlueprintGeneratedClass = Blueprints + 4,
UserDefinedEnum = Blueprints + 5,
UserDefinedStruct = Blueprints + 6,
//Metadata
Blueprint = Blueprints + 8,
CookedMetaData = Blueprints + 9,
Mesh = AssetCategoryExtensions.CategoryBase + (2 << 16),
StaticMesh = Mesh + 1,
SkeletalMesh = Mesh + 2,
Skeleton = Mesh + 3,
Texture = AssetCategoryExtensions.CategoryBase + (3 << 16),
Materials = AssetCategoryExtensions.CategoryBase + (4 << 16),
Material = Materials + 1,
MaterialEditorData = Materials + 2,
MaterialFunction = Materials + 3,
MaterialParameterCollection = Materials + 4,
Animation = AssetCategoryExtensions.CategoryBase + (5 << 16),
Level = AssetCategoryExtensions.CategoryBase + (6 << 16),
World = Level + 1,
BuildData = Level + 2,
LevelSequence = Level + 3,
Foliage = Level + 4,
Data = AssetCategoryExtensions.CategoryBase + (7 << 16),
ItemDefinitionBase = Data + 1,
CurveBase = Data + 2,
PhysicsAsset = Data + 3,
Media = AssetCategoryExtensions.CategoryBase + (8 << 16),
Audio = Media + 1,
Video = Media + 2,
Font = Media + 3,
Particle = AssetCategoryExtensions.CategoryBase + (9 << 16),
Animations = 1 << 5
}

View File

@ -1,30 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace FModel.Extensions;
public static class AssetCategoryExtensions
{
public const uint CategoryBase = 0x00010000;
public static EAssetCategory GetBaseCategory(this EAssetCategory category)
{
return (EAssetCategory) ((uint) category & 0xFFFF0000);
}
public static bool IsOfCategory(this EAssetCategory item, EAssetCategory category)
{
return item.GetBaseCategory() == category.GetBaseCategory();
}
public static bool IsBaseCategory(this EAssetCategory category)
{
return category == category.GetBaseCategory();
}
public static IEnumerable<EAssetCategory> GetBaseCategories()
{
return Enum.GetValues<EAssetCategory>().Where(c => c.IsBaseCategory());
}
}

View File

@ -1,70 +0,0 @@
using System;
using CUE4Parse.FileProvider;
using CUE4Parse.FileProvider.Objects;
using CUE4Parse.UE4.Assets;
using CUE4Parse.UE4.Objects.UObject;
using FModel.Settings;
namespace FModel.Extensions;
public static class CUE4ParseExtensions
{
public class LoadPackageResult
{
// more than 1 export per page currently break the inner package navigation feature
// if you have 1k exports per page, at page 2, you click on export index 932
// it will find the export index 932 in the current page, which would realistically be 1932
// fix would be to use InclusiveStart and ExclusiveEnd to determine the page the export index is in
// giving the document access to this would fix the issue and we could re-use Package instead of reloading it but it's quite a bit of work atm
private const int PaginationThreshold = 5000;
private const int MaxExportPerPage = 1;
public IPackage Package;
public int RequestedIndex;
public bool IsPaginated => Package.ExportMapLength >= PaginationThreshold;
/// <summary>
/// index of the first export on the current page
/// this index is the starting point for additional data preview
///
/// it can be >0 even if <see cref="IsPaginated"/> is false if we want to focus data preview on a specific export
/// in this case, we will display all exports but only the focused one will be checked for data preview
/// </summary>
public int InclusiveStart => Math.Max(0, RequestedIndex - RequestedIndex % MaxExportPerPage);
/// <summary>
/// last exclusive export index of the current page
/// </summary>
public int ExclusiveEnd => IsPaginated
? Math.Min(InclusiveStart + MaxExportPerPage, Package.ExportMapLength)
: Package.ExportMapLength;
public int PageSize => ExclusiveEnd - InclusiveStart;
public string TabTitleExtra => IsPaginated ? $"Export{(PageSize > 1 ? "s" : "")} {InclusiveStart}{(PageSize > 1 ? $"-{ExclusiveEnd - 1}" : "")} of {Package.ExportMapLength - 1}" : null;
/// <summary>
/// display all exports unless paginated
/// </summary>
/// <param name="save">if we save the data we will display all exports even if <see cref="IsPaginated"/> is true</param>
/// <returns></returns>
public object GetDisplayData(bool save = false) => !save && IsPaginated
? Package.GetExports(InclusiveStart, PageSize)
: Package.GetExports();
}
public static LoadPackageResult GetLoadPackageResult(this IFileProvider provider, GameFile file, string objectName = null)
{
var result = new LoadPackageResult { Package = provider.LoadPackage(file) };
if (result.IsPaginated || (result.Package.HasFlags(EPackageFlags.PKG_ContainsMap) && UserSettings.Default.PreviewWorlds)) // focus on UWorld if it's a map we want to preview
{
result.RequestedIndex = result.Package.GetExportIndex(file.NameWithoutExtension);
if (objectName != null)
{
result.RequestedIndex = int.TryParse(objectName, out var index) ? index : result.Package.GetExportIndex(objectName);
}
}
return result;
}
}

View File

@ -1,7 +1,6 @@
using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using CUE4Parse.UE4.Versions;
namespace FModel.Extensions;
@ -19,8 +18,7 @@ public static class EnumExtensions
var suffix = $"{value:D}";
var current = Convert.ToInt32(suffix);
var mask = value.GetType() == typeof(EGame) ? ~0xFFFF : ~0xF;
var target = current & mask;
var target = current & ~0xF;
if (current != target)
{
var values = Enum.GetValues(value.GetType());

View File

@ -6,14 +6,14 @@ using ICSharpCode.AvalonEdit.Document;
namespace FModel.Extensions;
public static partial class StringExtensions
public static class StringExtensions
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string GetReadableSize(double size)
{
if (size == 0) return "0 B";
string[] sizes = ["B", "KB", "MB", "GB", "TB"];
string[] sizes = { "B", "KB", "MB", "GB", "TB" };
var order = 0;
while (size >= 1024 && order < sizes.Length - 1)
{
@ -25,29 +25,93 @@ public static partial class StringExtensions
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int GetNameLineNumber(this string s, string lineToFind)
public static string SubstringBefore(this string s, char delimiter)
{
if (KismetRegex().IsMatch(lineToFind))
return s.GetKismetLineNumber(lineToFind);
if (int.TryParse(lineToFind, out var index))
return s.GetLineNumber(index);
return s.GetNameLineNumberText($" \"Name\": \"{lineToFind}\",");
var index = s.IndexOf(delimiter);
return index == -1 ? s : s[..index];
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int GetNameLineNumberText(this string s, string lineToFind)
public static string SubstringBefore(this string s, string delimiter, StringComparison comparisonType = StringComparison.Ordinal)
{
var index = s.IndexOf(delimiter, comparisonType);
return index == -1 ? s : s[..index];
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string SubstringAfter(this string s, char delimiter)
{
var index = s.IndexOf(delimiter);
return index == -1 ? s : s.Substring(index + 1, s.Length - index - 1);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string SubstringAfter(this string s, string delimiter, StringComparison comparisonType = StringComparison.Ordinal)
{
var index = s.IndexOf(delimiter, comparisonType);
return index == -1 ? s : s.Substring(index + delimiter.Length, s.Length - index - delimiter.Length);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string SubstringBeforeLast(this string s, char delimiter)
{
var index = s.LastIndexOf(delimiter);
return index == -1 ? s : s[..index];
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string SubstringBeforeLast(this string s, string delimiter, StringComparison comparisonType = StringComparison.Ordinal)
{
var index = s.LastIndexOf(delimiter, comparisonType);
return index == -1 ? s : s[..index];
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string SubstringBeforeWithLast(this string s, char delimiter)
{
var index = s.LastIndexOf(delimiter);
return index == -1 ? s : s[..(index + 1)];
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string SubstringBeforeWithLast(this string s, string delimiter, StringComparison comparisonType = StringComparison.Ordinal)
{
var index = s.LastIndexOf(delimiter, comparisonType);
return index == -1 ? s : s[..(index + 1)];
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string SubstringAfterLast(this string s, char delimiter)
{
var index = s.LastIndexOf(delimiter);
return index == -1 ? s : s.Substring(index + 1, s.Length - index - 1);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string SubstringAfterLast(this string s, string delimiter, StringComparison comparisonType = StringComparison.Ordinal)
{
var index = s.LastIndexOf(delimiter, comparisonType);
return index == -1 ? s : s.Substring(index + delimiter.Length, s.Length - index - delimiter.Length);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int GetNameLineNumber(this string s, string lineToFind)
{
if (int.TryParse(lineToFind, out var index))
return s.GetLineNumber(index);
lineToFind = $" \"Name\": \"{lineToFind}\",";
using var reader = new StringReader(s);
var lineNum = 0;
while (reader.ReadLine() is { } line)
string line;
while ((line = reader.ReadLine()) != null)
{
lineNum++;
if (line.Equals(lineToFind, StringComparison.OrdinalIgnoreCase))
return lineNum;
}
return -1;
return 1;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@ -70,17 +134,18 @@ public static partial class StringExtensions
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int GetKismetLineNumber(this string s, string input)
public static int GetKismetLineNumber(this string s, string input)
{
var match = KismetRegex().Match(input);
var match = Regex.Match(input, @"^(.+)\[(\d+)\]$");
var name = match.Groups[1].Value;
int index = int.Parse(match.Groups[2].Value);
var lineToFind = $" \"Name\": \"{name}\",";
var offset = $"\"StatementIndex\": {index}";
using var reader = new StringReader(s);
var lineNum = 0;
string line;
while (reader.ReadLine() is { } line)
while ((line = reader.ReadLine()) != null)
{
lineNum++;
if (line.Equals(lineToFind, StringComparison.OrdinalIgnoreCase))
@ -96,7 +161,7 @@ public static partial class StringExtensions
}
}
return -1;
return 1;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@ -104,7 +169,8 @@ public static partial class StringExtensions
{
using var reader = new StringReader(s);
var lineNum = 0;
while (reader.ReadLine() is { } line)
string line;
while ((line = reader.ReadLine()) != null)
{
lineNum++;
if (line.Equals(" {"))
@ -114,9 +180,6 @@ public static partial class StringExtensions
return lineNum + 1;
}
return -1;
return 1;
}
[GeneratedRegex(@"^(.+)\[(\d+)\]$", RegexOptions.Compiled | RegexOptions.Singleline | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
private static partial Regex KismetRegex();
}

View File

@ -11,25 +11,23 @@
<IsPackable>false</IsPackable>
<IsPublishable>true</IsPublishable>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<PlatformTarget>x64</PlatformTarget>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<PublishSingleFile Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">true</PublishSingleFile>
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
<StartupObject>FModel.App</StartupObject>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<PlatformTarget>x64</PlatformTarget>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<NoWarn>1701;1702;NU1701</NoWarn>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<PublishSingleFile>true</PublishSingleFile>
<PlatformTarget>x64</PlatformTarget>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<NoWarn>NU1701</NoWarn>
</PropertyGroup>
<PropertyGroup Condition="'$(GITHUB_ACTIONS)' == 'true'">
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
</PropertyGroup>
<ItemGroup>
<None Remove="Resources\android.png" />
<None Remove="Resources\apple.png" />
@ -104,7 +102,6 @@
<None Remove="Resources\npcleftside.png" />
<None Remove="Resources\default.frag" />
<None Remove="Resources\default.vert" />
<None Remove="Resources\spline.vert" />
<None Remove="Resources\grid.frag" />
<None Remove="Resources\grid.vert" />
<None Remove="Resources\skybox.frag" />
@ -125,7 +122,6 @@
<ItemGroup>
<EmbeddedResource Include="Resources\Json.xshd" />
<EmbeddedResource Include="Resources\Ini.xshd" />
<EmbeddedResource Include="Resources\spline.vert" />
<EmbeddedResource Include="Resources\Verse.xshd" />
<EmbeddedResource Include="Resources\Xml.xshd" />
<EmbeddedResource Include="Resources\Cpp.xshd" />
@ -153,24 +149,22 @@
<PackageReference Include="AdonisUI" Version="1.17.1" />
<PackageReference Include="AdonisUI.ClassicTheme" Version="1.17.1" />
<PackageReference Include="Autoupdater.NET.Official" Version="1.9.2" />
<PackageReference Include="AvalonEdit" Version="6.3.1.120" />
<PackageReference Include="AvalonEdit" Version="6.3.0.90" />
<PackageReference Include="CSCore" Version="1.2.1.2" />
<PackageReference Include="DiscordRichPresence" Version="1.6.1.70" />
<PackageReference Include="EpicManifestParser" Version="2.4.1" />
<PackageReference Include="EpicManifestParser.ZlibngDotNetDecompressor" Version="1.0.1" />
<PackageReference Include="DiscordRichPresence" Version="1.2.1.24" />
<PackageReference Include="EpicManifestParser" Version="2.3.3" />
<PackageReference Include="ImGui.NET" Version="1.91.0.1" />
<PackageReference Include="K4os.Compression.LZ4.Streams" Version="1.3.8" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="NVorbis" Version="0.10.5" />
<PackageReference Include="Ookii.Dialogs.Wpf" Version="5.0.1" />
<PackageReference Include="OpenTK" Version="4.9.4" />
<PackageReference Include="RestSharp" Version="113.0.0" />
<PackageReference Include="Serilog" Version="4.3.0" />
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
<PackageReference Include="SkiaSharp.HarfBuzz" Version="2.88.9" />
<PackageReference Include="Svg.Skia" Version="3.2.1" />
<PackageReference Include="Twizzle.ImGui-Bundle.NET" Version="1.91.5.2" />
<PackageReference Include="VirtualizingWrapPanel" Version="2.3.2" />
<PackageReference Include="OpenTK" Version="4.8.2" />
<PackageReference Include="RestSharp" Version="112.1.0" />
<PackageReference Include="Serilog" Version="4.0.2" />
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.5" />
<PackageReference Include="SkiaSharp.HarfBuzz" Version="2.88.8" />
<PackageReference Include="SkiaSharp.Svg" Version="1.60.0" />
</ItemGroup>
<ItemGroup>

View File

@ -1,5 +0,0 @@
<Solution>
<Project Path="../CUE4Parse/CUE4Parse-Conversion/CUE4Parse-Conversion.csproj" />
<Project Path="../CUE4Parse/CUE4Parse/CUE4Parse.csproj" />
<Project Path="FModel.csproj" />
</Solution>

View File

@ -1,23 +0,0 @@
using System;
using CUE4Parse.Compression;
using CUE4Parse.FileProvider.Objects;
using CUE4Parse.UE4.Assets.Objects;
using CUE4Parse.UE4.Readers;
namespace FModel.Framework;
public class FakeGameFile(string path) : GameFile(path, 0)
{
public override bool IsEncrypted => false;
public override CompressionMethod CompressionMethod => CompressionMethod.None;
public override byte[] Read(FByteBulkDataHeader? header = null)
{
throw new NotImplementedException();
}
public override FArchive CreateReader(FByteBulkDataHeader? header = null)
{
throw new NotImplementedException();
}
}

View File

@ -3,12 +3,12 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Forms;
using FModel.Settings;
using ImGuiNET;
using ImGuizmoNET;
using OpenTK.Graphics.OpenGL4;
using OpenTK.Windowing.Desktop;
using OpenTK.Windowing.GraphicsLibraryFramework;
@ -54,11 +54,11 @@ public class ImGuiController : IDisposable
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);
ImGuizmo.SetImGuiContext(context);
var io = ImGui.GetIO();
unsafe
@ -69,24 +69,29 @@ public class ImGuiController : IDisposable
FontNormal = io.Fonts.AddFontFromFileTTF("C:\\Windows\\Fonts\\segoeui.ttf", 16 * DpiScale);
FontBold = io.Fonts.AddFontFromFileTTF("C:\\Windows\\Fonts\\segoeuib.ttf", 16 * DpiScale);
FontSemiBold = io.Fonts.AddFontFromFileTTF("C:\\Windows\\Fonts\\seguisb.ttf", 16 * DpiScale);
io.Fonts.AddFontDefault();
io.Fonts.Build(); // Build font atlas
io.BackendFlags |= ImGuiBackendFlags.RendererHasVtxOffset;
io.ConfigFlags |= ImGuiConfigFlags.NavEnableKeyboard;
io.ConfigFlags |= ImGuiConfigFlags.DockingEnable;
io.Fonts.Flags |= ImFontAtlasFlags.NoBakedLines;
io.ConfigDockingWithShift = true;
io.ConfigWindowsMoveFromTitleBarOnly = true;
io.BackendRendererUserData = 0;
CreateDeviceResources();
SetPerFrameImGuiData(1f / 60f);
ImGui.NewFrame();
_frameBegun = true;
}
public void Normal() => PushFont(FontNormal);
public void Bold() => PushFont(FontBold);
public void SemiBold() => PushFont(FontSemiBold);
public void PopFont()
{
ImGui.PopFont();
PushFont(FontNormal);
}
private void PushFont(ImFontPtr ptr) => ImGui.PushFont(ptr);
public void WindowResized(int width, int height)
@ -124,42 +129,34 @@ public class ImGuiController : IDisposable
RecreateFontDeviceTexture();
string VertexSource = @"#version 330 core
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;
gl_Position = projection_matrix * vec4(in_position, 0, 1);
color = in_color;
texCoord = in_texCoord;
}";
string FragmentSource = @"#version 330 core
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);
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 = Marshal.SizeOf<ImDrawVert>();
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);
@ -225,6 +222,7 @@ void main()
ImGui.Render();
RenderImDrawData(ImGui.GetDrawData());
}
CheckGLError("End of frame");
}
/// <summary>
@ -242,7 +240,6 @@ void main()
_frameBegun = true;
ImGui.NewFrame();
ImGuizmo.BeginFrame();
}
/// <summary>
@ -252,6 +249,7 @@ void main()
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);
@ -264,7 +262,6 @@ void main()
private void UpdateImGuiInput(GameWindow wnd)
{
ImGuiIOPtr io = ImGui.GetIO();
var mState = wnd.MouseState;
var kState = wnd.KeyboardState;
@ -276,7 +273,7 @@ void main()
io.AddMouseButtonEvent(4, mState[MouseButton.Button2]);
io.AddMouseWheelEvent(mState.ScrollDelta.X, mState.ScrollDelta.Y);
foreach (Keys key in Enum.GetValues<Keys>())
foreach (Keys key in Enum.GetValues(typeof(Keys)))
{
if (key == Keys.Unknown) continue;
io.AddKeyEvent(TranslateKey(key), kState.IsKeyDown(key));
@ -294,7 +291,7 @@ void main()
io.KeySuper = kState.IsKeyDown(Keys.LeftSuper) || kState.IsKeyDown(Keys.RightSuper);
}
internal void PressChar(char keyChar)
public void PressChar(char keyChar)
{
PressedChars.Add(keyChar);
}
@ -340,7 +337,7 @@ void main()
{
ImDrawListPtr cmd_list = draw_data.CmdLists[i];
int vertexSize = cmd_list.VtxBuffer.Size * Marshal.SizeOf<ImDrawVert>();
int vertexSize = cmd_list.VtxBuffer.Size * Unsafe.SizeOf<ImDrawVert>();
if (vertexSize > _vertexBufferSize)
{
int newSize = (int)Math.Max(_vertexBufferSize * 1.5f, vertexSize);
@ -390,7 +387,7 @@ void main()
{
ImDrawListPtr cmd_list = draw_data.CmdLists[n];
GL.BufferSubData(BufferTarget.ArrayBuffer, IntPtr.Zero, cmd_list.VtxBuffer.Size * Marshal.SizeOf<ImDrawVert>(), cmd_list.VtxBuffer.Data);
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);
@ -544,16 +541,16 @@ void main()
public static ImGuiKey TranslateKey(Keys key)
{
if (key >= Keys.D0 && key <= Keys.D9)
if (key is >= Keys.D0 and <= Keys.D9)
return key - Keys.D0 + ImGuiKey._0;
if (key >= Keys.A && key <= Keys.Z)
if (key is >= Keys.A and <= Keys.Z)
return key - Keys.A + ImGuiKey.A;
if (key >= Keys.KeyPad0 && key <= Keys.KeyPad9)
if (key is >= Keys.KeyPad0 and <= Keys.KeyPad9)
return key - Keys.KeyPad0 + ImGuiKey.Keypad0;
if (key >= Keys.F1 && key <= Keys.F24)
if (key is >= Keys.F1 and <= Keys.F24)
return key - Keys.F1 + ImGuiKey.F24;
return key switch
@ -596,7 +593,7 @@ void main()
Keys.KeyPadAdd => ImGuiKey.KeypadAdd,
Keys.KeyPadEnter => ImGuiKey.KeypadEnter,
Keys.KeyPadEqual => ImGuiKey.KeypadEqual,
Keys.LeftShift => ImGuiKey.ModShift,
Keys.LeftShift => ImGuiKey.LeftShift,
Keys.LeftControl => ImGuiKey.LeftCtrl,
Keys.LeftAlt => ImGuiKey.LeftAlt,
Keys.LeftSuper => ImGuiKey.LeftSuper,

View File

@ -1,63 +0,0 @@
using System;
using System.Diagnostics;
using System.Reflection;
using Serilog.Core;
using Serilog.Events;
namespace FModel.Framework;
public abstract class SerilogEnricher : ILogEventEnricher
{
public abstract void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory);
protected bool TryGetCaller(out MethodBase method)
{
method = null;
var serilogAssembly = typeof(Serilog.Log).Assembly;
var stack = new StackTrace(3);
foreach (var frame in stack.GetFrames())
{
var m = frame.GetMethod();
if (m?.DeclaringType is null) continue;
if (m.DeclaringType.Assembly != serilogAssembly)
{
method = m;
break;
}
}
return method != null;
}
}
public class SourceEnricher : SerilogEnricher
{
public override void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
{
var source = "N/A";
if (logEvent.Properties.TryGetValue("SourceContext", out var sourceContext))
{
source = sourceContext.ToString()[1..^1];
}
else if (TryGetCaller(out var method))
{
source = method.DeclaringType?.Namespace;
}
logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("Enriched", source.Split('.')[0]));
}
}
public class CallerEnricher : SerilogEnricher
{
public override void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
{
if (TryGetCaller(out var method))
{
logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("Enriched", $"{method.DeclaringType?.FullName}.{method.Name}"));
}
}
}

View File

@ -38,7 +38,7 @@ public static class Helper
else
{
var w = GetOpenedWindow<T>(windowName);
if (windowName == "Search For Packages") w.WindowState = WindowState.Normal;
if (windowName == "Search View") w.WindowState = WindowState.Normal;
w.Focus();
}
}

View File

@ -3,7 +3,6 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:FModel"
xmlns:controls="clr-namespace:FModel.Views.Resources.Controls"
xmlns:inputs="clr-namespace:FModel.Views.Resources.Controls.Inputs"
xmlns:converters="clr-namespace:FModel.Views.Resources.Converters"
xmlns:settings="clr-namespace:FModel.Settings"
xmlns:services="clr-namespace:FModel.Services"
@ -11,18 +10,8 @@
xmlns:adonisControls="clr-namespace:AdonisUI.Controls;assembly=AdonisUI"
xmlns:adonisExtensions="clr-namespace:AdonisUI.Extensions;assembly=AdonisUI"
WindowStartupLocation="CenterScreen" Closing="OnClosing" Loaded="OnLoaded" PreviewKeyDown="OnWindowKeyDown"
Height="{Binding Source={x:Static SystemParameters.MaximizedPrimaryScreenHeight}, Converter={converters:RatioConverter}, ConverterParameter='0.95'}"
Width="{Binding Source={x:Static SystemParameters.MaximizedPrimaryScreenWidth}, Converter={converters:RatioConverter}, ConverterParameter='0.90'}">
<Window.TaskbarItemInfo>
<TaskbarItemInfo ProgressValue="1.0">
<TaskbarItemInfo.ProgressState>
<MultiBinding Converter="{converters:StatusToTaskbarStateConverter}">
<Binding Path="Status.Kind" />
<Binding Path="IsActive" RelativeSource="{RelativeSource AncestorType=Window}" />
</MultiBinding>
</TaskbarItemInfo.ProgressState>
</TaskbarItemInfo>
</Window.TaskbarItemInfo>
Height="{Binding Source={x:Static SystemParameters.MaximizedPrimaryScreenHeight}, Converter={converters:RatioConverter}, ConverterParameter='0.85'}"
Width="{Binding Source={x:Static SystemParameters.MaximizedPrimaryScreenWidth}, Converter={converters:RatioConverter}, ConverterParameter='0.75'}">
<adonisControls:AdonisWindow.Style>
<Style TargetType="adonisControls:AdonisWindow" BasedOn="{StaticResource {x:Type adonisControls:AdonisWindow}}" >
<Setter Property="Title" Value="{Binding DataContext.InitialWindowTitle, RelativeSource={RelativeSource Self}}" />
@ -41,179 +30,161 @@
</Style.Triggers>
</Style>
</adonisControls:AdonisWindow.Style>
<adonisControls:AdonisWindow.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Views/Resources/Resources.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</adonisControls:AdonisWindow.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="8" />
<RowDefinition Height="{adonisUi:Space 1}" />
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Menu Grid.Column="0">
<MenuItem Header="Directory">
<MenuItem Header="Selector" Command="{Binding MenuCommand}" CommandParameter="Directory_Selector">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource DirectoryIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<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">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource KeyIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<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">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource BackupIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<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">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource InfoIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<Menu Grid.Row="0">
<MenuItem Header="Directory">
<MenuItem Header="Selector" Command="{Binding MenuCommand}" CommandParameter="Directory_Selector">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource DirectoryIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Packages">
<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">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource SearchIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="References" IsEnabled="{Binding Status.IsReady}" InputGestureText="Ctrl+Shift+R" Click="OnRefViewClick">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource SearchIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Favorite Directories" ItemsSource="{Binding CustomDirectories.Directories}" IsEnabled="{Binding Status.IsReady}">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource DirectoriesIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<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">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource KeyIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</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">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.AccentForegroundBrush}}" Data="{StaticResource AudioIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Image Merger" Command="{Binding MenuCommand}" CommandParameter="Views_ImageMerger">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.AccentForegroundBrush}}" Data="{StaticResource ImageMergerIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<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">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource BackupIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Settings" Command="{Binding MenuCommand}" CommandParameter="Settings" />
<MenuItem Header="Help" >
<MenuItem Header="Donate" Command="{Binding MenuCommand}" CommandParameter="Help_Donate">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.AccentForegroundBrush}}" Data="{StaticResource GiftIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Releases" Command="{Binding MenuCommand}" CommandParameter="Help_Releases">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.AccentForegroundBrush}}" Data="{StaticResource GitHubIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Bugs Report" Command="{Binding MenuCommand}" CommandParameter="Help_BugsReport">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.AccentForegroundBrush}}" Data="{StaticResource BugIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Discord Server" Command="{Binding MenuCommand}" CommandParameter="Help_Discord">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.AccentForegroundBrush}}" Data="{StaticResource DiscordIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="About FModel" Command="{Binding MenuCommand}" CommandParameter="Help_About">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.AccentForegroundBrush}}" Data="{StaticResource InfoIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<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">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource InfoIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
</Menu>
<Grid Grid.Column="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="Preview New Explorer System" VerticalAlignment="Center" />
<CheckBox Grid.Column="1" Margin="5 2 5 0" Unchecked="FeaturePreviewOnUnchecked" KeyboardNavigation.TabNavigation="None" KeyboardNavigation.ControlTabNavigation="None"
IsChecked="{Binding FeaturePreviewNewAssetExplorer, Source={x:Static local:Settings.UserSettings.Default}, Mode=TwoWay}"
Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}"/>
</Grid>
</Grid>
</MenuItem>
<MenuItem Header="Packages">
<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">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource SearchIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Favorite Directories" ItemsSource="{Binding CustomDirectories.Directories}" IsEnabled="{Binding Status.IsReady}">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource DirectoriesIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<Separator />
<MenuItem Header="Auto Open Sounds" IsCheckable="True" StaysOpenOnClick="True"
IsChecked="{Binding IsAutoOpenSounds, Source={x:Static 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">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.AccentForegroundBrush}}" Data="{StaticResource AudioIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Image Merger" Command="{Binding MenuCommand}" CommandParameter="Views_ImageMerger">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.AccentForegroundBrush}}" Data="{StaticResource ImageMergerIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
</MenuItem>
<MenuItem Header="Settings" Command="{Binding MenuCommand}" CommandParameter="Settings" />
<MenuItem Header="Help" >
<MenuItem Header="Donate" Command="{Binding MenuCommand}" CommandParameter="Help_Donate">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.AccentForegroundBrush}}" Data="{StaticResource GiftIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Releases" Command="{Binding MenuCommand}" CommandParameter="Help_Releases">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.AccentForegroundBrush}}" Data="{StaticResource GitHubIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Bugs Report" Command="{Binding MenuCommand}" CommandParameter="Help_BugsReport">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.AccentForegroundBrush}}" Data="{StaticResource BugIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Discord Server" Command="{Binding MenuCommand}" CommandParameter="Help_Discord">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.AccentForegroundBrush}}" Data="{StaticResource DiscordIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="About FModel" Command="{Binding MenuCommand}" CommandParameter="Help_About">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.AccentForegroundBrush}}" Data="{StaticResource InfoIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
</MenuItem>
</Menu>
<Grid x:Name="RootGrid" Grid.Row="2">
<Grid.ColumnDefinitions>
@ -223,8 +194,8 @@
</Grid.ColumnDefinitions>
<GroupBox Grid.Column="0" adonisExtensions:LayerExtension.Layer="2"
Padding="0" Background="Transparent">
<TabControl x:Name="LeftTabControl" SelectionChanged="OnTabItemChange" SelectedIndex="{Binding SelectedLeftTabIndex, Mode=TwoWay}">
Padding="{adonisUi:Space 0}" Background="Transparent">
<TabControl x:Name="LeftTabControl" SelectionChanged="OnTabItemChange">
<TabItem Style="{StaticResource TabItemFillSpace}" Header="Archives">
<DockPanel>
<Grid DockPanel.Dock="Top">
@ -289,8 +260,7 @@
</Grid>
</DockPanel>
</TabItem>
<TabItem Style="{StaticResource TabItemFillSpace}"
Header="Folders">
<TabItem Style="{StaticResource TabItemFillSpace}" Header="Folders">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
@ -348,11 +318,85 @@
</StackPanel>
</Grid>
<Separator Grid.Row="1" Style="{StaticResource CustomSeparator}" Margin="0" />
<TreeView Grid.Row="2"
x:Name="AssetsFolderName"
Style="{StaticResource AssetsFolderTreeView}"
PreviewKeyDown="OnFoldersPreviewKeyDown"
PreviewMouseDoubleClick="OnAssetsTreeMouseDoubleClick">
<TreeView Grid.Row="2" x:Name="AssetsFolderName" Style="{StaticResource AssetsFolderTreeView}" PreviewMouseDoubleClick="OnAssetsTreeMouseDoubleClick">
<TreeView.ContextMenu>
<ContextMenu>
<!-- <MenuItem Header="Extract Folder's Packages" Click="OnFolderExtractClick"> -->
<!-- <MenuItem.Icon> -->
<!-- <Viewbox Width="16" Height="16"> -->
<!-- <Canvas Width="24" Height="24"> -->
<!-- <Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource ExtractIcon}" /> -->
<!-- </Canvas> -->
<!-- </Viewbox> -->
<!-- </MenuItem.Icon> -->
<!-- </MenuItem> -->
<!-- <Separator /> -->
<MenuItem Header="Export Folder's Packages Raw Data (.uasset)" Click="OnFolderExportClick">
<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 Folder's Packages Properties (.json)" Click="OnFolderSaveClick">
<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 Folder's Packages Textures" 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" 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" 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="Favorite Directory" Click="OnFavoriteDirectoryClick">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource DirectoriesAddIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Copy Directory Path" Click="OnCopyDirectoryPathClick">
<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>
</ContextMenu>
</TreeView.ContextMenu>
</TreeView>
<Separator Grid.Row="3" Style="{StaticResource CustomSeparator}" Tag="INFORMATION" />
<StackPanel Grid.Row="4" Orientation="Vertical" Margin="0 0 0 5">
@ -387,11 +431,32 @@
Header="{Binding SelectedItem.AssetsList.Assets.Count, FallbackValue=0, ElementName=AssetsFolderName}"
HeaderStringFormat="{}{0} Packages">
<DockPanel>
<Grid DockPanel.Dock="Top" ZIndex="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<inputs:SearchTextBox DockPanel.Dock="Top" x:Name="AssetsSearchTextBox"
Text="{Binding SelectedItem.SearchText, ElementName=AssetsFolderName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
ClearButtonClick="OnClearFilterClick" />
<Grid Grid.Column="0" ZIndex="1" HorizontalAlignment="Left" Margin="5 2 0 0">
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource SearchIcon}" />
</Canvas>
</Viewbox>
</Grid>
<TextBox Grid.Column="0" Grid.ColumnSpan="2" x:Name="AssetsSearchName" AcceptsTab="False" AcceptsReturn="False"
Padding="25 0 0 0" HorizontalAlignment="Stretch" TextChanged="OnFilterTextChanged"
adonisExtensions:WatermarkExtension.Watermark="Search by name..." />
<StackPanel Grid.Column="1" Orientation="Horizontal">
<Button ToolTip="Clear Search Filter" Padding="5" Style="{DynamicResource {x:Static adonisUi:Styles.ToolbarButton}}" Click="OnDeleteSearchClick">
<Viewbox Width="16" Height="16" HorizontalAlignment="Center">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource BackspaceIcon}"/>
</Canvas>
</Viewbox>
</Button>
</StackPanel>
</Grid>
<Grid DockPanel.Dock="Top">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
@ -400,15 +465,156 @@
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<controls:Breadcrumb Grid.Row="0" HorizontalAlignment="Left" Margin="0 5 0 5"
MaxWidth="{Binding ActualWidth, ElementName=AssetsSearchTextBox}"
<controls:Breadcrumb Grid.Row="0" MaxWidth="{Binding ActualWidth, ElementName=AssetsSearchName}" HorizontalAlignment="Left" Margin="0 5 0 5"
DataContext="{Binding SelectedItem.PathAtThisPoint, ElementName=AssetsFolderName, FallbackValue='No/Directory/Detected/In/Folder'}"/>
<ListBox Grid.Row="1" x:Name="AssetsListName"
Style="{StaticResource AssetsListBox}"
PreviewMouseDoubleClick="OnAssetsListMouseDoubleClick"
PreviewKeyDown="OnPreviewKeyDown" />
<ListBox Grid.Row="1" x:Name="AssetsListName" Style="{StaticResource AssetsListBox}" PreviewMouseDoubleClick="OnAssetsListMouseDoubleClick" PreviewKeyDown="OnPreviewKeyDown">
<ListBox.ContextMenu>
<ContextMenu DataContext="{Binding PlacementTarget, RelativeSource={RelativeSource Self}}">
<MenuItem Header="Extract in New Tab" Command="{Binding DataContext.RightClickMenuCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="Assets_Extract_New_Tab" />
<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 ExtractIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<Separator />
<MenuItem Header="Export Raw Data (.uasset)" Command="{Binding DataContext.RightClickMenuCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="Assets_Export_Data" />
<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 ExportIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Save Properties (.json)" Command="{Binding DataContext.RightClickMenuCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="Assets_Save_Properties" />
<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 SaveIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Save Texture" Command="{Binding DataContext.RightClickMenuCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="Assets_Save_Textures" />
<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 TextureIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Save Model" 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" 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>
<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 Header="Package Path" Command="{Binding DataContext.CopyCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="File_Path" />
<Binding Path="SelectedItems" />
</MultiBinding>
</MenuItem.CommandParameter>
</MenuItem>
<MenuItem Header="Package Name" Command="{Binding DataContext.CopyCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="File_Name" />
<Binding Path="SelectedItems" />
</MultiBinding>
</MenuItem.CommandParameter>
</MenuItem>
<MenuItem Header="Directory Path" Command="{Binding DataContext.CopyCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="Directory_Path" />
<Binding Path="SelectedItems" />
</MultiBinding>
</MenuItem.CommandParameter>
</MenuItem>
<MenuItem Header="Package Path w/o Extension" Command="{Binding DataContext.CopyCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="File_Path_No_Extension" />
<Binding Path="SelectedItems" />
</MultiBinding>
</MenuItem.CommandParameter>
</MenuItem>
<MenuItem Header="Package Name w/o Extension" Command="{Binding DataContext.CopyCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="File_Name_No_Extension" />
<Binding Path="SelectedItems" />
</MultiBinding>
</MenuItem.CommandParameter>
</MenuItem>
</MenuItem>
</ContextMenu>
</ListBox.ContextMenu>
</ListBox>
<Separator Grid.Row="2" Style="{StaticResource CustomSeparator}" Tag="INFORMATION" />
<StackPanel Grid.Row="3" Orientation="Vertical" Margin="0 0 0 5">
<Grid HorizontalAlignment="Stretch">
@ -424,15 +630,15 @@
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Text="{Binding SelectedItem.Asset.Offset, ElementName=AssetsListName, FallbackValue=0, StringFormat='{}0x{0:X}'}" VerticalAlignment="Center" HorizontalAlignment="Left" />
<TextBlock Grid.Row="0" Grid.Column="0" Text="{Binding SelectedItem.Offset, ElementName=AssetsListName, FallbackValue=0, StringFormat='{}0x{0:X}'}" VerticalAlignment="Center" HorizontalAlignment="Left" />
<TextBlock Grid.Row="0" Grid.Column="1" Text="Offset" VerticalAlignment="Center" HorizontalAlignment="Right" />
<TextBlock Grid.Row="1" Grid.Column="0" Text="{Binding SelectedItem.Asset.Size, ElementName=AssetsListName, FallbackValue=0, Converter={x:Static converters:SizeToStringConverter.Instance}}" VerticalAlignment="Center" HorizontalAlignment="Left" />
<TextBlock Grid.Row="1" Grid.Column="0" Text="{Binding SelectedItem.Size, ElementName=AssetsListName, FallbackValue=0, Converter={x:Static converters:SizeToStringConverter.Instance}}" VerticalAlignment="Center" HorizontalAlignment="Left" />
<TextBlock Grid.Row="1" Grid.Column="1" Text="Size" VerticalAlignment="Center" HorizontalAlignment="Right" />
<TextBlock Grid.Row="2" Grid.Column="0" Text="{Binding SelectedItem.Asset.CompressionMethod, ElementName=AssetsListName, FallbackValue='Unknown'}" VerticalAlignment="Center" HorizontalAlignment="Left" />
<TextBlock Grid.Row="2" Grid.Column="0" Text="{Binding SelectedItem.Compression, ElementName=AssetsListName, FallbackValue='Unknown'}" VerticalAlignment="Center" HorizontalAlignment="Left" />
<TextBlock Grid.Row="2" Grid.Column="1" Text="Compression Method" VerticalAlignment="Center" HorizontalAlignment="Right" />
<TextBlock Grid.Row="3" Grid.Column="0" Text="{Binding SelectedItem.Asset.IsEncrypted, ElementName=AssetsListName, FallbackValue='False'}" VerticalAlignment="Center" HorizontalAlignment="Left" />
<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.Asset.Vfs.Name, 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>
@ -447,154 +653,14 @@
Background="{DynamicResource {x:Static adonisUi:Brushes.Layer0BackgroundBrush}}" />
<GroupBox Grid.Column="2" adonisExtensions:LayerExtension.Layer="2"
Padding="0" Background="Transparent">
Padding="{adonisUi:Space 0}" Background="Transparent">
<Grid Margin="0 0 3 0">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<Border BorderThickness="1" Padding="10"
BorderBrush="{DynamicResource {x:Static adonisUi:Brushes.Layer3BorderBrush}}"
Background="{DynamicResource {x:Static adonisUi:Brushes.Layer3BackgroundBrush}}"
Visibility="{Binding IsAssetsExplorerVisible, Converter={StaticResource BoolToVisibilityConverter}}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid Grid.Row="0" adonisExtensions:LayerExtension.Layer="3">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="5" />
<ColumnDefinition Width="Auto" MinWidth="150" />
</Grid.ColumnDefinitions>
<inputs:SearchTextBox x:Name="AssetsExplorerSearch" Grid.Column="0"
Text="{Binding SelectedItem.SearchText, ElementName=AssetsFolderName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
ClearButtonClick="OnClearFilterClick" />
<ComboBox x:Name="CategoriesSelector" Grid.Column="2"
ItemsSource="{Binding Categories}"
SelectedItem="{Binding SelectedItem.SelectedCategory, ElementName=AssetsFolderName, Mode=TwoWay}" />
</Grid>
<controls:Breadcrumb Grid.Row="1" Margin="0 5 0 0"
HorizontalAlignment="Left"
DataContext="{Binding SelectedItem.PathAtThisPoint, ElementName=AssetsFolderName}" />
<ListBox x:Name="AssetsExplorer" Grid.Row="2"
ItemsSource="{Binding SelectedItem.CombinedEntries, ElementName=AssetsFolderName, IsAsync=True}"
Style="{StaticResource TiledExplorer}"
Background="{DynamicResource {x:Static adonisUi:Brushes.Layer3BackgroundBrush}}"
PreviewKeyDown="OnPreviewKeyDown" />
</Grid>
</Border>
<TabControl x:Name="TabControlName"
Style="{StaticResource GameFilesTabControl}"
Visibility="{Binding IsAssetsExplorerVisible, Converter={x:Static converters:InvertBoolToVisibilityConverter.Instance}, ConverterParameter=Hidden}" />
</Grid>
<Border Grid.Row="0"
VerticalAlignment="Bottom"
HorizontalAlignment="Right"
Margin="12,12,20,12"
Opacity="0.5"
Visibility="{Binding FeaturePreviewNewAssetExplorer, Source={x:Static settings:UserSettings.Default}, Converter={StaticResource BoolToVisibilityConverter}}"
Background="{DynamicResource {x:Static adonisUi:Brushes.Layer2BackgroundBrush}}"
CornerRadius="8"
Padding="4"
Effect="{DynamicResource ShadowEffect}">
<Border.Triggers>
<EventTrigger RoutedEvent="MouseEnter">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity"
To="1"
Duration="0:0:0.2" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
<EventTrigger RoutedEvent="MouseLeave">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity"
To="0.5"
Duration="0:0:0.2" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Border.Triggers>
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Center">
<ToggleButton Width="32"
Height="32"
Cursor="Hand"
Margin="0, 0, 2, 0"
Checked="OnPreviewTexturesToggled"
Focusable="False"
IsChecked="{Binding PreviewTexturesAssetExplorer, Source={x:Static settings:UserSettings.Default}, Mode=TwoWay}">
<ToggleButton.Style>
<Style TargetType="ToggleButton" BasedOn="{StaticResource ModernToggleButtonStyle}">
<Setter Property="ToolTip" Value="Preview Textures (OFF)" />
<Style.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter Property="Background" Value="MediumPurple" />
<Setter Property="ToolTip" Value="Preview Textures (ON)" />
</Trigger>
</Style.Triggers>
</Style>
</ToggleButton.Style>
<Viewbox Width="16"
Height="16">
<Canvas Width="24"
Height="24">
<Path Data="{StaticResource TextureIconAlt}"
Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" />
</Canvas>
</Viewbox>
</ToggleButton>
<ToggleButton Width="32"
Height="32"
Cursor="Hand"
Margin="0, 0, 2, 0"
Checked="OnPreviewTexturesToggled"
ToolTip="Assets Explorer"
Focusable="False"
IsChecked="{Binding IsAssetsExplorerVisible, Mode=TwoWay}"
Style="{StaticResource ModernToggleButtonStyle}">
<Viewbox Width="16"
Height="16">
<Canvas Width="24"
Height="24">
<Path Data="{StaticResource FolderIconAlt}"
Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" />
</Canvas>
</Viewbox>
</ToggleButton>
<ToggleButton Width="32"
Height="32"
Cursor="Hand"
ToolTip="File Properties"
Focusable="False"
IsChecked="{Binding IsAssetsExplorerVisible, Converter={x:Static converters:InvertBooleanConverter.Instance}, Mode=TwoWay}"
Style="{StaticResource ModernToggleButtonStyle}">
<Viewbox Width="16"
Height="16">
<Canvas Width="24"
Height="24">
<Path Data="{StaticResource AssetIcon}"
Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" />
</Canvas>
</Viewbox>
</ToggleButton>
</StackPanel>
</Border>
<TabControl Grid.Row="0" x:Name="TabControlName" Style="{StaticResource GameFilesTabControl}" />
<Expander Grid.Row="1" Margin="0 5 0 5" ExpandDirection="Down"
IsExpanded="{Binding IsLoggerExpanded, Source={x:Static settings:UserSettings.Default}}">
<Grid>
@ -607,7 +673,7 @@
<controls:CustomRichTextBox Grid.Column="0" Grid.ColumnSpan="3" x:Name="LogRtbName" Style="{StaticResource CustomRichTextBox}" />
<StackPanel Grid.Column="2" Orientation="Vertical" VerticalAlignment="Bottom" Margin="-5 -5 5 5">
<Button Style="{DynamicResource {x:Static adonisUi:Styles.ToolbarButton}}" ToolTip="Open Output Folder" Padding="0,4,0,4"
Command="{Binding MenuCommand}" CommandParameter="ToolBox_Open_Output_Directory" Focusable="False">
Command="{Binding MenuCommand}" CommandParameter="ToolBox_Open_Output_Directory">
<Viewbox Width="16" Height="16" HorizontalAlignment="Center">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource FolderIcon}" />
@ -615,7 +681,7 @@
</Viewbox>
</Button>
<Button Style="{DynamicResource {x:Static adonisUi:Styles.ToolbarButton}}" ToolTip="Clear Logs" Padding="0,4,0,4"
Command="{Binding MenuCommand}" CommandParameter="ToolBox_Clear_Logs" Focusable="False">
Command="{Binding MenuCommand}" CommandParameter="ToolBox_Clear_Logs">
<Viewbox Width="16" Height="16" HorizontalAlignment="Center">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource TrashIcon}"/>
@ -726,6 +792,19 @@
</Viewbox>
</StatusBarItem>
<StatusBarItem Width="30" HorizontalContentAlignment="Stretch" ToolTip="Auto Open Sounds Enabled">
<StatusBarItem.Style>
<Style TargetType="StatusBarItem">
<Style.Triggers>
<DataTrigger Binding="{Binding IsAutoOpenSounds, Source={x:Static settings:UserSettings.Default}}" Value="False">
<Setter Property="Visibility" Value="Collapsed" />
</DataTrigger>
</Style.Triggers>
</Style>
</StatusBarItem.Style>
<TextBlock HorizontalAlignment="Center" FontWeight="SemiBold" Text="SND" />
</StatusBarItem>
<StatusBarItem Margin="10 0 0 0">
<TextBlock Text="{Binding LastUpdateCheck, Source={x:Static local:Settings.UserSettings.Default}, Converter={x:Static converters:RelativeDateTimeConverter.Instance}, StringFormat=Last Refresh: {0}}" />
</StatusBarItem>

View File

@ -4,8 +4,9 @@ using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using AdonisUI.Controls;
using FModel.Extensions;
using FModel.Services;
using FModel.Settings;
using FModel.ViewModels;
@ -29,54 +30,14 @@ public partial class MainWindow
{
CommandBindings.Add(new CommandBinding(new RoutedCommand("ReloadMappings", typeof(MainWindow), new InputGestureCollection { new KeyGesture(Key.F12) }), OnMappingsReload));
CommandBindings.Add(new CommandBinding(ApplicationCommands.Find, (_, _) => OnOpenAvalonFinder()));
CommandBindings.Add(new CommandBinding(NavigationCommands.BrowseBack, (_, _) =>
{
if (UserSettings.Default.FeaturePreviewNewAssetExplorer && !_applicationView.IsAssetsExplorerVisible)
{
// back browsing the json view will reopen the assets explorer
_applicationView.IsAssetsExplorerVisible = true;
return;
}
if (LeftTabControl.SelectedIndex == 2)
{
LeftTabControl.SelectedIndex = 1;
}
else if (LeftTabControl.SelectedIndex == 1 && AssetsFolderName.SelectedItem is TreeItem { Parent: TreeItem parent })
{
AssetsFolderName.Focus();
parent.IsSelected = true;
}
}));
DataContext = _applicationView;
InitializeComponent();
AssetsExplorer.ItemContainerGenerator.StatusChanged += ItemContainerGenerator_StatusChanged;
AssetsListName.ItemContainerGenerator.StatusChanged += ItemContainerGenerator_StatusChanged;
AssetsExplorer.SelectionChanged += (_, e) => SyncSelection(AssetsListName, e);
AssetsListName.SelectionChanged += (_, e) => SyncSelection(AssetsExplorer, e);
FLogger.Logger = LogRtbName;
YesWeCats = this;
}
// Hack to sync selection between packages tab and explorer
private void SyncSelection(ListBox target, SelectionChangedEventArgs e)
{
foreach (var added in e.AddedItems.OfType<GameFileViewModel>())
{
if (!target.SelectedItems.Contains(added))
target.SelectedItems.Add(added);
}
foreach (var removed in e.RemovedItems.OfType<GameFileViewModel>())
{
if (target.SelectedItems.Contains(removed))
target.SelectedItems.Remove(removed);
}
}
private void OnClosing(object sender, CancelEventArgs e)
{
_discordHandler.Dispose();
@ -100,23 +61,18 @@ public partial class MainWindow
break;
}
await Task.WhenAll(
ApplicationViewModel.InitOodle(),
ApplicationViewModel.InitZlib()
).ConfigureAwait(false);
await ApplicationViewModel.InitOodle();
await ApplicationViewModel.InitZlib();
await _applicationView.CUE4Parse.Initialize();
await _applicationView.AesManager.InitAes();
await _applicationView.UpdateProvider(true);
#if !DEBUG
await _applicationView.CUE4Parse.InitInformation();
#endif
await Task.WhenAll(
_applicationView.CUE4Parse.VerifyConsoleVariables(),
_applicationView.CUE4Parse.VerifyOnDemandArchives(),
_applicationView.CUE4Parse.InitMappings(),
ApplicationViewModel.InitDetex(),
ApplicationViewModel.InitVgmStream(),
ApplicationViewModel.InitImGuiSettings(newOrUpdated),
Task.Run(() =>
@ -129,7 +85,10 @@ public partial class MainWindow
#if DEBUG
// await _threadWorkerView.Begin(cancellationToken =>
// _applicationView.CUE4Parse.Extract(cancellationToken,
// _applicationView.CUE4Parse.Provider["Marvel/Content/Marvel/Wwise/Assets/Events/Music/music_new/event/Entry.uasset"]));
// "FortniteGame/Content/Athena/Apollo/Maps/UI/Apollo_Terrain_Minimap.uasset"));
// await _threadWorkerView.Begin(cancellationToken =>
// _applicationView.CUE4Parse.Extract(cancellationToken,
// "FortniteGame/Content/Environments/Helios/Props/GlacierHotel/GlacierHotel_Globe_A/Meshes/SM_GlacierHotel_Globe_A.uasset"));
#endif
}
@ -150,33 +109,10 @@ public partial class MainWindow
}
else if (_applicationView.Status.IsReady && e.Key == Key.F && Keyboard.Modifiers.HasFlag(ModifierKeys.Control) && Keyboard.Modifiers.HasFlag(ModifierKeys.Shift))
OnSearchViewClick(null, null);
else if (_applicationView.Status.IsReady && e.Key == Key.R && Keyboard.Modifiers.HasFlag(ModifierKeys.Control) && Keyboard.Modifiers.HasFlag(ModifierKeys.Shift))
OnRefViewClick(null, null);
else if (e.Key == Key.F3)
OnOpenAvalonFinder();
else if (e.Key == Key.Left && !_applicationView.IsAssetsExplorerVisible && _applicationView.CUE4Parse.TabControl.SelectedTab is { HasImage: true })
else if (e.Key == Key.Left && _applicationView.CUE4Parse.TabControl.SelectedTab.HasImage)
_applicationView.CUE4Parse.TabControl.SelectedTab.GoPreviousImage();
else if (e.Key == Key.Right && !_applicationView.IsAssetsExplorerVisible && _applicationView.CUE4Parse.TabControl.SelectedTab is { HasImage: true })
else if (e.Key == Key.Right && _applicationView.CUE4Parse.TabControl.SelectedTab.HasImage)
_applicationView.CUE4Parse.TabControl.SelectedTab.GoNextImage();
else if (_applicationView.Status.IsReady && _applicationView.IsAssetsExplorerVisible && Keyboard.Modifiers.HasFlag(ModifierKeys.Alt))
{
CategoriesSelector.SelectedIndex = e.SystemKey switch
{
Key.D0 or Key.NumPad0 => 0,
Key.D1 or Key.NumPad1 => 1,
Key.D2 or Key.NumPad2 => 2,
Key.D3 or Key.NumPad3 => 3,
Key.D4 or Key.NumPad4 => 4,
Key.D5 or Key.NumPad5 => 5,
Key.D6 or Key.NumPad6 => 6,
Key.D7 or Key.NumPad7 => 7,
Key.D8 or Key.NumPad8 => 8,
Key.D9 or Key.NumPad9 => 9,
_ => CategoriesSelector.SelectedIndex
};
}
else if (_applicationView.Status.IsReady && UserSettings.Default.FeaturePreviewNewAssetExplorer && UserSettings.Default.SwitchAssetExplorer.IsTriggered(e.Key))
_applicationView.IsAssetsExplorerVisible = !_applicationView.IsAssetsExplorerVisible;
else if (UserSettings.Default.AssetAddTab.IsTriggered(e.Key))
_applicationView.CUE4Parse.TabControl.AddTab();
else if (UserSettings.Default.AssetRemoveTab.IsTriggered(e.Key))
@ -185,22 +121,15 @@ public partial class MainWindow
_applicationView.CUE4Parse.TabControl.GoLeftTab();
else if (UserSettings.Default.AssetRightTab.IsTriggered(e.Key))
_applicationView.CUE4Parse.TabControl.GoRightTab();
else if (UserSettings.Default.DirLeftTab.IsTriggered(e.Key) && _applicationView.SelectedLeftTabIndex > 0)
_applicationView.SelectedLeftTabIndex--;
else if (UserSettings.Default.DirRightTab.IsTriggered(e.Key) && _applicationView.SelectedLeftTabIndex < LeftTabControl.Items.Count - 1)
_applicationView.SelectedLeftTabIndex++;
else if (UserSettings.Default.DirLeftTab.IsTriggered(e.Key) && LeftTabControl.SelectedIndex > 0)
LeftTabControl.SelectedIndex--;
else if (UserSettings.Default.DirRightTab.IsTriggered(e.Key) && LeftTabControl.SelectedIndex < LeftTabControl.Items.Count - 1)
LeftTabControl.SelectedIndex++;
}
private void OnSearchViewClick(object sender, RoutedEventArgs e)
{
var searchView = Helper.GetWindow<SearchView>("Search For Packages", () => new SearchView().Show());
searchView.FocusTab(ESearchViewTab.SearchView);
}
private void OnRefViewClick(object sender, RoutedEventArgs e)
{
var searchView = Helper.GetWindow<SearchView>("Search For Packages", () => new SearchView().Show());
searchView.FocusTab(ESearchViewTab.RefView);
Helper.OpenWindow<AdonisWindow>("Search View", () => new SearchView().Show());
}
private void OnTabItemChange(object sender, SelectionChangedEventArgs e)
@ -208,18 +137,7 @@ public partial class MainWindow
if (e.OriginalSource is not TabControl tabControl)
return;
switch (tabControl.SelectedIndex)
{
case 0:
DirectoryFilesListBox.Focus();
break;
case 1:
AssetsFolderName.Focus();
break;
case 2:
AssetsListName.Focus();
break;
}
(tabControl.SelectedItem as System.Windows.Controls.TabItem)?.Focus();
}
private async void OnMappingsReload(object sender, ExecutedRoutedEventArgs e)
@ -229,71 +147,119 @@ public partial class MainWindow
private void OnOpenAvalonFinder()
{
if (_applicationView.IsAssetsExplorerVisible)
{
AssetsExplorerSearch.TextBox.Focus();
AssetsExplorerSearch.TextBox.SelectAll();
}
else if (_applicationView.CUE4Parse.TabControl.SelectedTab is { } tab)
{
tab.HasSearchOpen = true;
AvalonEditor.YesWeSearch.Focus();
AvalonEditor.YesWeSearch.SelectAll();
}
_applicationView.CUE4Parse.TabControl.SelectedTab.HasSearchOpen = true;
AvalonEditor.YesWeSearch.Focus();
AvalonEditor.YesWeSearch.SelectAll();
}
private void OnAssetsTreeMouseDoubleClick(object sender, MouseButtonEventArgs e)
{
if (sender is not TreeView { SelectedItem: TreeItem treeItem } || treeItem.Folders.Count > 0) return;
_applicationView.SelectedLeftTabIndex++;
}
private void OnPreviewTexturesToggled(object sender, RoutedEventArgs e) => ItemContainerGenerator_StatusChanged(AssetsExplorer.ItemContainerGenerator, EventArgs.Empty);
private void ItemContainerGenerator_StatusChanged(object sender, EventArgs e)
{
if (sender is not ItemContainerGenerator { Status: GeneratorStatus.ContainersGenerated } generator)
return;
var foundVisibleItem = false;
var itemCount = generator.Items.Count;
for (var i = 0; i < itemCount; i++)
{
var container = generator.ContainerFromIndex(i);
if (container == null)
{
if (foundVisibleItem) break; // we're past the visible range already
continue; // keep scrolling to find visible items
}
if (container is FrameworkElement { IsVisible: true } && generator.Items[i] is GameFileViewModel file)
{
foundVisibleItem = true;
file.OnIsVisible();
}
}
LeftTabControl.SelectedIndex++;
}
private async void OnAssetsListMouseDoubleClick(object sender, MouseButtonEventArgs e)
{
if (sender is not ListBox listBox) return;
var selectedItems = listBox.SelectedItems.OfType<GameFileViewModel>().Select(gvm => gvm.Asset).ToArray();
if (selectedItems.Length == 0) return;
var selectedItems = listBox.SelectedItems.Cast<AssetItem>().ToList();
await _threadWorkerView.Begin(cancellationToken => { _applicationView.CUE4Parse.ExtractSelected(cancellationToken, selectedItems); });
}
private void OnClearFilterClick(object sender, RoutedEventArgs e)
private async void OnFolderExtractClick(object sender, RoutedEventArgs e)
{
if (AssetsFolderName.SelectedItem is TreeItem folder)
{
folder.SearchText = string.Empty;
folder.SelectedCategory = EAssetCategory.All;
await _threadWorkerView.Begin(cancellationToken => { _applicationView.CUE4Parse.ExtractFolder(cancellationToken, folder); });
}
}
private async void OnFolderExportClick(object sender, RoutedEventArgs e)
{
if (AssetsFolderName.SelectedItem is TreeItem folder)
{
await _threadWorkerView.Begin(cancellationToken => { _applicationView.CUE4Parse.ExportFolder(cancellationToken, folder); });
FLogger.Append(ELog.Information, () =>
{
FLogger.Text("Successfully exported ", Constants.WHITE);
FLogger.Link(folder.PathAtThisPoint, UserSettings.Default.RawDataDirectory, true);
});
}
}
private async void OnFolderSaveClick(object sender, RoutedEventArgs e)
{
if (AssetsFolderName.SelectedItem is TreeItem folder)
{
await _threadWorkerView.Begin(cancellationToken => { _applicationView.CUE4Parse.SaveFolder(cancellationToken, folder); });
FLogger.Append(ELog.Information, () =>
{
FLogger.Text("Successfully saved ", Constants.WHITE);
FLogger.Link(folder.PathAtThisPoint, UserSettings.Default.PropertiesDirectory, true);
});
}
}
private async void OnFolderTextureClick(object sender, RoutedEventArgs e)
{
if (AssetsFolderName.SelectedItem is TreeItem folder)
{
await _threadWorkerView.Begin(cancellationToken => { _applicationView.CUE4Parse.TextureFolder(cancellationToken, folder); });
FLogger.Append(ELog.Information, () =>
{
FLogger.Text("Successfully saved ", Constants.WHITE);
FLogger.Link(folder.PathAtThisPoint, UserSettings.Default.TextureDirectory, true);
});
}
}
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 OnFavoriteDirectoryClick(object sender, RoutedEventArgs e)
{
if (AssetsFolderName.SelectedItem is not TreeItem folder) return;
_applicationView.CustomDirectories.Add(new CustomDirectory(folder.Header, folder.PathAtThisPoint));
FLogger.Append(ELog.Information, () =>
FLogger.Text($"Successfully saved '{folder.PathAtThisPoint}' as a new favorite directory", Constants.WHITE, true));
}
private void OnCopyDirectoryPathClick(object sender, RoutedEventArgs e)
{
if (AssetsFolderName.SelectedItem is not TreeItem folder) return;
Clipboard.SetText(folder.PathAtThisPoint);
}
private void OnDeleteSearchClick(object sender, RoutedEventArgs e)
{
AssetsSearchName.Text = string.Empty;
AssetsListName.ScrollIntoView(AssetsListName.SelectedItem);
}
private void OnFilterTextChanged(object sender, TextChangedEventArgs e)
{
if (sender is not TextBox textBox || AssetsFolderName.SelectedItem is not TreeItem folder)
return;
var filters = textBox.Text.Trim().Split(' ');
folder.AssetsList.AssetsView.Filter = o => { return o is AssetItem assetItem && filters.All(x => assetItem.FullPath.SubstringAfterLast('/').Contains(x, StringComparison.OrdinalIgnoreCase)); };
}
private void OnMouseDoubleClick(object sender, MouseButtonEventArgs e)
{
if (!_applicationView.Status.IsReady || sender is not ListBox listBox) return;
@ -303,67 +269,14 @@ public partial class MainWindow
private async void OnPreviewKeyDown(object sender, KeyEventArgs e)
{
if (!_applicationView.Status.IsReady || sender is not ListBox listBox)
return;
if (e.Key != Key.Enter)
return;
if (listBox.SelectedItem == null)
return;
if (!_applicationView.Status.IsReady || sender is not ListBox listBox) return;
switch (listBox.SelectedItem)
switch (e.Key)
{
case GameFileViewModel file:
_applicationView.IsAssetsExplorerVisible = false;
ApplicationService.ApplicationView.SelectedLeftTabIndex = 2;
await _threadWorkerView.Begin(cancellationToken => _applicationView.CUE4Parse.ExtractSelected(cancellationToken, [file.Asset]));
break;
case TreeItem folder:
ApplicationService.ApplicationView.SelectedLeftTabIndex = 1;
var parent = folder.Parent;
while (parent != null)
{
parent.IsExpanded = true;
parent = parent.Parent;
}
var childFolder = folder;
while (childFolder.Folders.Count == 1 && childFolder.AssetsList.Assets.Count == 0)
{
childFolder.IsExpanded = true;
childFolder = childFolder.Folders[0];
}
childFolder.IsExpanded = true;
childFolder.IsSelected = true;
case Key.Enter:
var selectedItems = listBox.SelectedItems.Cast<AssetItem>().ToList();
await _threadWorkerView.Begin(cancellationToken => { _applicationView.CUE4Parse.ExtractSelected(cancellationToken, selectedItems); });
break;
}
}
private void FeaturePreviewOnUnchecked(object sender, RoutedEventArgs e)
{
_applicationView.IsAssetsExplorerVisible = false;
}
private async void OnFoldersPreviewKeyDown(object sender, KeyEventArgs e)
{
if (e.Key != Key.Enter || sender is not TreeView treeView || treeView.SelectedItem is not TreeItem folder)
return;
if ((folder.IsExpanded || folder.Folders.Count == 0) && folder.AssetsList.Assets.Count > 0)
{
_applicationView.SelectedLeftTabIndex++;
return;
}
var childFolder = folder;
while (childFolder.Folders.Count == 1 && childFolder.AssetsList.Assets.Count == 0)
{
childFolder.IsExpanded = true;
childFolder = childFolder.Folders[0];
}
childFolder.IsExpanded = true;
childFolder.IsSelected = true;
}
}

View File

@ -1,150 +1,195 @@
<SyntaxDefinition name="C++" extensions=".cpp;.h;.hpp;.c" xmlns="http://icsharpcode.net/sharpdevelop/syntaxdefinition/2008">
<Color name="Comment" foreground="#84c36b" />
<Color name="Keyword" foreground="#82AAFF" fontWeight="bold" />
<Color name="Type" foreground="#4cc9b0" />
<Color name="String" foreground="#ECC48D" />
<Color name="Preprocessor" foreground="#82AAFF" fontWeight="bold" />
<Color name="Number" foreground="#F78C6C" />
<Color name="Function" foreground="#C3E88D" />
<Color name="AccessModifier" foreground="#f20f5c" fontWeight="bold" />
<Color name="UEMacro" foreground="#82AAFF" fontWeight="bold" />
<Color name="LabelColor" foreground="#808080" />
<Color name="JumpKeywords" foreground="#dda0dd" />
<Color name="CompoundKeywords" foreground="#FF569CD6" fontWeight="bold" />
<Color name="Pointer" foreground="#ff8888" fontWeight="bold"/>
<Color name="StaticClass" foreground="#c2a8ff" />
<Color name="Brace" foreground="#89DDFF" />
<Color name="This" foreground="#FF569CD6" fontWeight="bold" />
<Color name="BooleanConstants" foreground="#569cd6" fontWeight="bold" />
<RuleSet ignoreCase="false">
<Span color="String" begin="&quot;" end="&quot;" />
<!-- UE Macros -->
<Keywords color="UEMacro">
<Word>UCLASS</Word>
<Word>USTRUCT</Word>
<Word>UPROPERTY</Word>
<Word>UFUNCTION</Word>
<Word>GENERATED_BODY</Word>
<Word>GENERATED_USTRUCT_BODY</Word>
<Word>GENERATED_UCLASS_BODY</Word>
</Keywords>
<Keywords color="JumpKeywords">
<Word>goto</Word>
<Word>return</Word>
<Word>throw</Word>
</Keywords>
<!-- C++ Keywords -->
<Keywords color="Keyword">
<Word>void</Word>
<Word>int</Word>
<Word>Int8</Word>
<Word>Int16</Word>
<Word>Int32</Word>
<Word>Int64</Word>
<Word>uint</Word>
<Word>UInt16</Word>
<Word>UInt32</Word>
<Word>UInt64</Word>
<Word>float</Word>
<Word>double</Word>
<Word>bool</Word>
<Word>return</Word>
<Word>if</Word>
<Word>else</Word>
<Word>for</Word>
<Word>while</Word>
<Word>do</Word>
<Word>switch</Word>
<Word>case</Word>
<Word>break</Word>
<Word>continue</Word>
<Word>namespace</Word>
<Word>using</Word>
<Word>typedef</Word>
<Word>sizeof</Word>
<Word>new</Word>
<Word>delete</Word>
<Word>class</Word>
<Word>struct</Word>
<Word>enum</Word>
<Word>template</Word>
<Word>typename</Word>
<Word>const</Word>
<Word>static</Word>
<Word>mutable</Word>
<Word>volatile</Word>
<Word>override</Word>
<Word>virtual</Word>
<Word>explicit</Word>
<Word>friend</Word>
<Word>inline</Word>
<Word>constexpr</Word>
<Word>default</Word>
</Keywords>
<Keywords color="Pointer">
<Word>nullptr</Word>
</Keywords>
<Keywords color="BooleanConstants">
<Word>true</Word>
<Word>True</Word>
<Word>false</Word>
<Word>False</Word>
<Word>NULL</Word>
</Keywords>
<Keywords color="AccessModifier">
<Word>public</Word>
<Word>protected</Word>
<Word>private</Word>
</Keywords>
<Keywords color="This">
<Word>this</Word>
</Keywords>
<!-- Reference symbols -->
<Rule color="Pointer">(?&lt;=[A-Za-z0-9_&gt;&amp;\]])&amp;(?=\s*[A-Za-z_&lt;])</Rule>
<Rule color="LabelColor">\bLabel_\d+:</Rule>
<!-- Numbers (hex too) -->
<Rule color="Number">\b(0x[0-9a-fA-F]+|[0-9]+(\.[0-9]+)?)\b</Rule>
<Rule color="StaticClass">\bU[A-Z][A-Za-z0-9_]*\b(?=::)</Rule>
<Rule color="Function">[A-Za-z_][A-Za-z0-9_]*\s*(?=\()</Rule>
<Rule color="Brace">[\[\]\{\}]</Rule>
<Rule color="Comment">(\/\/.*|\/\*[\s\S]*?\*\/)</Rule>
<!-- Template Functions -->
<Rule color="Function">\b[A-Za-z_][A-Za-z0-9_]*\b(?=&lt;)</Rule>
<!-- Types -->
<Rule color="Type">\b[A-Z][A-Za-z0-9_]*(?:&lt;[^&gt;]+&gt;)?[*&amp;]?(?=\s+[*&amp;]?[A-Za-z_][A-Za-z0-9_]*\s*(=|;|\)|,))</Rule>
<!-- Types inside <> -->
<Rule color="Type">(?&lt;=&lt;)\s*[A-Z][A-Za-z0-9_]*(?:&lt;[^&gt;]+&gt;)?[*&amp;]?\s*(?=[&gt;,])</Rule>
<!-- Match class name after the 'class' keyword -->
<Rule color="Type">\b(?&lt;=class\s)[A-Za-z_][A-Za-z0-9_]*</Rule>
<!-- Match name after 'public' keyword -->
<Rule color="Type">\b(?&lt;=public\s)[A-Za-z_][A-Za-z0-9_]*</Rule>
<!-- Types in function parameters -->
<Rule color="Type">\b(?:T|F|U|E)[A-Z][A-Za-z0-9_]*(?:&lt;[^&gt;]+&gt;)?[*&amp;]?(?=\s+[*&amp;]?[A-Za-z_][A-Za-z0-9_]*\s*(?:=|,|\)))</Rule>
<Rule color="Type">\b(?&lt;=[,(]\s*const\s)(?:T|F|U)[A-Z][A-Za-z0-9_]*[*&amp;]?(?=\s)</Rule>
<!-- First parameter type in function -->
<Rule color="Type">\b(?&lt;=\()\s*[TUF][A-Z][A-Za-z0-9_]*(?=\s*&lt;)</Rule>
<Rule color="Type">\b(?&lt;=\()\s*[TUF][A-Z][A-Za-z0-9_]*[*&amp;]?(?=\s)</Rule>
<Rule color="Type">\b(?&lt;=\(\s*const\s)[TUF][A-Z][A-Za-z0-9_]*[*&amp;]?(?=\s)</Rule>
</RuleSet>
</SyntaxDefinition>
<?xml version="1.0"?>
<!-- syntaxdefinition for C/C++ 2001 by Andrea Paatz and Mike Krueger -->
<!-- converted to AvalonEdit format by Siegfried Pammer in 2010 -->
<SyntaxDefinition name="C++" extensions=".c;.h;.cc;.cpp;.hpp" xmlns="http://icsharpcode.net/sharpdevelop/syntaxdefinition/2008">
<Color name="Comment" foreground="Green" />
<Color name="Character" foreground="Fuchsia" />
<Color name="String" foreground="Fuchsia" />
<Color name="Preprocessor" foreground="Green" />
<Color name="Punctuation" foreground="DarkGreen" />
<Color name="MethodName" foreground="MidnightBlue" fontWeight="bold" />
<Color name="Digits" foreground="#F78C6C" />
<Color name="CompoundKeywords" foreground="Black" fontWeight="bold" />
<Color name="This" foreground="Black" fontWeight="bold" />
<Color name="Operators" foreground="#FF008B8B" fontWeight="bold" />
<Color name="Namespace" foreground="#FF008000" fontWeight="bold" />
<Color name="Friend" foreground="#FFA52A2A" />
<Color name="Modifiers" foreground="#FF0000FF" fontWeight="bold" />
<Color name="TypeKeywords" foreground="#FFFF0000" />
<Color name="BooleanConstants" foreground="#FF000000" fontWeight="bold" />
<Color name="Keywords" foreground="#FF0000FF" fontWeight="bold" />
<Color name="LoopKeywords" foreground="#FF0000FF" fontWeight="bold" />
<Color name="JumpKeywords" foreground="#FF000080" />
<Color name="ExceptionHandling" foreground="#FF008080" fontWeight="bold" />
<Color name="ControlFlow" foreground="#FF0000FF" fontWeight="bold" />
<RuleSet ignoreCase="false">
<Rule color="Punctuation">
[?,.;()\[\]{}+\-/%*&lt;&gt;^=~!&amp;]+
</Rule>
<Keywords color="CompoundKeywords">
<Word>__abstract</Word>
<Word>__box</Word>
<Word>__delegate</Word>
<Word>__gc</Word>
<Word>__identifier</Word>
<Word>__nogc</Word>
<Word>__pin</Word>
<Word>__property</Word>
<Word>__sealed</Word>
<Word>__try_cast</Word>
<Word>__typeof</Word>
<Word>__value</Word>
<Word>__event</Word>
<Word>__hook</Word>
<Word>__raise</Word>
<Word>__unhook</Word>
<Word>__interface</Word>
<Word>ref class</Word>
<Word>ref struct</Word>
<Word>value class</Word>
<Word>value struct</Word>
<Word>interface class</Word>
<Word>interface struct</Word>
<Word>enum class</Word>
<Word>enum struct</Word>
<Word>delegate</Word>
<Word>event</Word>
<Word>property</Word>
<Word>abstract</Word>
<Word>override</Word>
<Word>sealed</Word>
<Word>generic</Word>
<Word>where</Word>
<Word>finally</Word>
<Word>for each</Word>
<Word>gcnew</Word>
<Word>in</Word>
<Word>initonly</Word>
<Word>literal</Word>
<Word>nullptr</Word>
</Keywords>
<Keywords color="This">
<Word>this</Word>
</Keywords>
<Keywords color="Operators">
<Word>and</Word>
<Word>and_eq</Word>
<Word>bitand</Word>
<Word>bitor</Word>
<Word>new</Word>
<Word>not</Word>
<Word>not_eq</Word>
<Word>or</Word>
<Word>or_eq</Word>
<Word>xor</Word>
<Word>xor_eq</Word>
</Keywords>
<Keywords color="Namespace">
<Word>using</Word>
<Word>namespace</Word>
</Keywords>
<Keywords color="Friend">
<Word>friend</Word>
</Keywords>
<Keywords color="Modifiers">
<Word>private</Word>
<Word>protected</Word>
<Word>public</Word>
<Word>const</Word>
<Word>volatile</Word>
<Word>static</Word>
</Keywords>
<Keywords color="TypeKeywords">
<Word>bool</Word>
<Word>char</Word>
<Word>unsigned</Word>
<Word>union</Word>
<Word>virtual</Word>
<Word>double</Word>
<Word>float</Word>
<Word>short</Word>
<Word>signed</Word>
<Word>void</Word>
<Word>class</Word>
<Word>enum</Word>
<Word>struct</Word>
</Keywords>
<Keywords color="BooleanConstants">
<Word>false</Word>
<Word>true</Word>
</Keywords>
<Keywords color="LoopKeywords">
<Word>do</Word>
<Word>for</Word>
<Word>while</Word>
</Keywords>
<Keywords color="JumpKeywords">
<Word>break</Word>
<Word>continue</Word>
<Word>goto</Word>
<Word>return</Word>
</Keywords>
<Keywords color="ExceptionHandling">
<Word>catch</Word>
<Word>throw</Word>
<Word>try</Word>
</Keywords>
<Keywords color="ControlFlow">
<Word>case</Word>
<Word>else</Word>
<Word>if</Word>
<Word>switch</Word>
<Word>default</Word>
</Keywords>
<Keywords color="Keywords">
<Word>asm</Word>
<Word>auto</Word>
<Word>compl</Word>
<Word>mutable</Word>
<Word>const_cast</Word>
<Word>delete</Word>
<Word>dynamic_cast</Word>
<Word>explicit</Word>
<Word>export</Word>
<Word>extern</Word>
<Word>inline</Word>
<Word>int</Word>
<Word>long</Word>
<Word>operator</Word>
<Word>register</Word>
<Word>reinterpret_cast</Word>
<Word>sizeof</Word>
<Word>static_cast</Word>
<Word>template</Word>
<Word>typedef</Word>
<Word>typeid</Word>
<Word>typename</Word>
</Keywords>
<Span color="Preprocessor">
<Begin>\#</Begin>
</Span>
<Span color="Comment">
<Begin>//</Begin>
</Span>
<Span color="Comment" multiline="true">
<Begin>/\*</Begin>
<End>\*/</End>
</Span>
<Span color="String">
<Begin>"</Begin>
<End>"</End>
<RuleSet>
<Span begin="\\" end="." />
</RuleSet>
</Span>
<Span color="Character">
<Begin>'</Begin>
<End>'</End>
<RuleSet>
<Span begin="\\" end="." />
</RuleSet>
</Span>
<Rule color="MethodName">[\d\w_]+(?=(\s*\())</Rule>
<Rule color="Digits">\b0[xX][0-9a-fA-F]+|(\b\d+(\.[0-9]+)?|\.[0-9]+)([eE][+-]?[0-9]+)?</Rule>
</RuleSet>
</SyntaxDefinition>

View File

@ -12,7 +12,7 @@ out vec3 fColor;
void main()
{
gl_PointSize = 7.5;
gl_PointSize = 7.5f;
gl_Position = uProjection * uView * uInstanceMatrix * vec4(vPos, 1.0);
fPos = vec3(uInstanceMatrix * vec4(vPos, 1.0));
fColor = vColor;

View File

@ -13,7 +13,7 @@ out vec3 fColor;
void main()
{
gl_PointSize = 7.5;
gl_PointSize = 7.5f;
gl_Position = uProjection * uView * uInstanceMatrix * uCollisionMatrix * vec4(vPos.xzy * uScaleDown, 1.0);
fPos = vec3(uInstanceMatrix * uCollisionMatrix * vec4(vPos.xzy * uScaleDown, 1.0));
fColor = vec3(1.0);

View File

@ -81,18 +81,6 @@ void main()
finalNormal = normalize(finalNormal);
finalTangent = normalize(finalTangent);
}
else if (uIsSpline)
{
GpuSplineMeshParams params = uSplineParameters[gl_InstanceID];
float distanceAlong = GetAxisValueRef(params.ForwardAxis, bindPos.xzy);
vec3 computed = ComputeRatioAlongSpline(params, distanceAlong);
mat4 sliceTransform = CalcSliceTransformAtSplineOffset(params, computed);
SetAxisValueRef(params.ForwardAxis, bindPos.xzy, 0.0);
finalPos = (sliceTransform * bindPos.xzyw).xzyw;
finalNormal = bindNormal;
finalTangent = bindTangent;
}
else
{
finalPos = bindPos;

View File

@ -32,5 +32,5 @@ void main()
outVar.view = view;
outVar.nearPoint = UnprojectPoint(vPos.xy, -1.0).xyz;
outVar.farPoint = UnprojectPoint(vPos.xy, 1.0).xyz;
gl_Position = vec4(vPos, 1.0);
gl_Position = vec4(vPos, 1.0f);
}

View File

@ -62,17 +62,6 @@ void main()
}
}
}
else if (uIsSpline)
{
GpuSplineMeshParams params = uSplineParameters[gl_InstanceID];
float distanceAlong = GetAxisValueRef(params.ForwardAxis, bindPos.xzy);
vec3 computed = ComputeRatioAlongSpline(params, distanceAlong);
mat4 sliceTransform = CalcSliceTransformAtSplineOffset(params, computed);
SetAxisValueRef(params.ForwardAxis, bindPos.xzy, 0.0);
finalPos = (sliceTransform * bindPos.xzyw).xzyw;
finalNormal = bindNormal;
}
else
{
finalPos = bindPos;

View File

@ -47,16 +47,6 @@ void main()
}
}
}
else if (uIsSpline)
{
GpuSplineMeshParams params = uSplineParameters[gl_InstanceID];
float distanceAlong = GetAxisValueRef(params.ForwardAxis, bindPos.xzy);
vec3 computed = ComputeRatioAlongSpline(params, distanceAlong);
mat4 sliceTransform = CalcSliceTransformAtSplineOffset(params, computed);
SetAxisValueRef(params.ForwardAxis, bindPos.xzy, 0.0);
finalPos = (sliceTransform * bindPos.xzyw).xzyw;
}
else finalPos = bindPos;
gl_Position = uProjection * uView * vInstanceMatrix * finalPos;

View File

@ -1,216 +0,0 @@
#version 460 core
// yeeted from minshu https://github.com/FabianFG/CUE4Parse/commit/61cef25b8eef4160651ee41e2b1ceefc5135803f
struct GpuSplineMeshParams {
int ForwardAxis;
float SplineBoundaryMin;
float SplineBoundaryMax;
bool bSmoothInterpRollScale;
vec3 MeshOrigin;
vec3 MeshBoxExtent;
vec3 StartPos;
float StartRoll;
vec3 StartTangent;
vec2 StartScale;
vec2 StartOffset;
vec3 EndPos;
float EndRoll;
vec3 EndTangent;
vec2 EndScale;
vec2 EndOffset;
vec3 SplineUpDir;
};
layout(std430, binding = 3) buffer SplineParameters
{
GpuSplineMeshParams uSplineParameters[];
};
uniform bool uIsSpline;
vec3 getSafeNormal(vec3 vector) {
float squareSum = dot(vector, vector);
if (squareSum == 1.0) {
return vector;
}
if (squareSum < 1e-8) {
return vec3(0.0); // Return a zero vector
}
// Calculate the scale factor to normalize the vector
float scale = inversesqrt(squareSum);
return vector * scale;
}
float GetAxisValueRef(int forwardAxis, vec3 pos)
{
switch (forwardAxis)
{
case 0: return pos.x;
case 1: return pos.y;
case 2: return pos.z;
default: return 0;
}
}
void SetAxisValueRef(int forwardAxis, inout vec3 pos, float v)
{
switch (forwardAxis)
{
case 0: pos.x = v; break;
case 1: pos.y = v; break;
case 2: pos.z = v; break;
}
}
vec3 SplineEvalTangent(GpuSplineMeshParams params, float a)
{
vec3 c = (6 * params.StartPos) + (3 * params.StartTangent) + (3 * params.EndTangent) - (6 * params.EndPos);
vec3 d = (-6 * params.StartPos) - (4 * params.StartTangent) - (2 * params.EndTangent) + (6 * params.EndPos);
vec3 e = params.StartTangent;
float a2 = a * a;
return (c * a2) + (d * a) + e;
}
vec3 SplineEvalDir(GpuSplineMeshParams params, float a)
{
return getSafeNormal(SplineEvalTangent(params, a));
}
vec3 SplineEvalPos(GpuSplineMeshParams params, float a)
{
float a2 = a * a;
float a3 = a2 * a;
return (((2 * a3) - (3 * a2) + 1) * params.StartPos) + ((a3 - (2 * a2) + a) * params.StartTangent) + ((a3 - a2) * params.EndTangent) + (((-2 * a3) + (3 * a2)) * params.EndPos);
}
vec3 ComputeRatioAlongSpline(GpuSplineMeshParams params, float distanceAlong)
{
float alpha = 0.0;
float minT = 0.0;
float maxT = 1.0;
const float SmallNumber = 1e-8;
bool bHasCustomBoundary = abs(params.SplineBoundaryMin - params.SplineBoundaryMax) > SmallNumber;
if (bHasCustomBoundary)
{
float splineLength = params.SplineBoundaryMax - params.SplineBoundaryMin;
if (splineLength > 0)
{
alpha = (distanceAlong - params.SplineBoundaryMin) / splineLength;
}
float boundMin = GetAxisValueRef(params.ForwardAxis, params.MeshOrigin - params.MeshBoxExtent);
float boundMax = GetAxisValueRef(params.ForwardAxis, params.MeshOrigin + params.MeshBoxExtent);
float boundMinT = (boundMin - params.SplineBoundaryMin) / (params.SplineBoundaryMax - params.SplineBoundaryMin);
float boundMaxT = (boundMax - params.SplineBoundaryMin) / (params.SplineBoundaryMax - params.SplineBoundaryMin);
const float MaxSplineExtrapolation = 4.0;
minT = max(-MaxSplineExtrapolation, boundMinT);
maxT = min(boundMaxT, MaxSplineExtrapolation);
}
else
{
float meshMinZ = GetAxisValueRef(params.ForwardAxis, params.MeshOrigin) - GetAxisValueRef(params.ForwardAxis, params.MeshBoxExtent);
float meshRangeZ = 2 * GetAxisValueRef(params.ForwardAxis, params.MeshBoxExtent);
if (meshRangeZ > SmallNumber) {
alpha = (distanceAlong - meshMinZ) / meshRangeZ;
}
}
return vec3(alpha, minT, maxT);
}
mat4 CalcSliceTransformAtSplineOffset(GpuSplineMeshParams params, vec3 computed)
{
float alpha = computed.x;
float minT = computed.y;
float maxT = computed.z;
float hermiteAlpha = params.bSmoothInterpRollScale ? smoothstep(0.0, 1.0, alpha) : alpha;
vec3 splinePos;
vec3 splineDir;
if (alpha < minT)
{
vec3 startTangent = SplineEvalTangent(params, minT);
splinePos = SplineEvalPos(params, minT) + (startTangent * (alpha - minT));
splineDir = getSafeNormal(startTangent);
}
else if (alpha > maxT)
{
vec3 endTangent = SplineEvalTangent(params, maxT);
splinePos = SplineEvalPos(params, maxT) + (endTangent * (alpha - maxT));
splineDir = getSafeNormal(endTangent);
}
else
{
splinePos = SplineEvalPos(params, alpha);
splineDir = SplineEvalDir(params, alpha);
}
// base
vec3 baseXVec = getSafeNormal(cross(params.SplineUpDir, splineDir));
vec3 baseYVec = getSafeNormal(cross(splineDir, baseXVec));
// Offset the spline by the desired amount
vec2 sliceOffset = mix(params.StartOffset, params.EndOffset, hermiteAlpha);
splinePos += sliceOffset.x * baseXVec;
splinePos += sliceOffset.y * baseYVec;
// Apply Roll
float useRoll = mix(params.StartRoll, params.EndRoll, hermiteAlpha);
float cosAng = cos(useRoll);
float sinAng = sin(useRoll);
vec3 xVec = (cosAng * baseXVec) - (sinAng * baseYVec);
vec3 yVec = (cosAng * baseYVec) + (sinAng * baseXVec);
// Find Scale
vec2 useScale = mix(params.StartScale, params.EndScale, hermiteAlpha);
// Build overall transform
mat4 sliceTransform = mat4(0);
vec3 scale;
switch (params.ForwardAxis) {
case 0:
sliceTransform[0] = vec4(splineDir, 0.0);
sliceTransform[1] = vec4(xVec, 0.0);
sliceTransform[2] = vec4(yVec, 0.0);
sliceTransform[3] = vec4(splinePos, 1.0);
scale = vec3(1.0, useScale.x, useScale.y);
break;
case 1:
sliceTransform[0] = vec4(yVec, 0.0);
sliceTransform[1] = vec4(splineDir, 0.0);
sliceTransform[2] = vec4(xVec, 0.0);
sliceTransform[3] = vec4(splinePos, 1.0);
scale = vec3(useScale.y, 1.0, useScale.x);
break;
case 2:
sliceTransform[0] = vec4(xVec, 0.0);
sliceTransform[1] = vec4(yVec, 0.0);
sliceTransform[2] = vec4(splineDir, 0.0);
sliceTransform[3] = vec4(splinePos, 1.0);
scale = vec3(useScale.x, useScale.y, 1.0);
break;
}
mat4 scaleMatrix = mat4(
vec4(scale.x, 0.0, 0.0, 0.0),
vec4(0.0, scale.y, 0.0, 0.0),
vec4(0.0, 0.0, scale.z, 0.0),
vec4(0.0, 0.0, 0.0, 1.0)
);
return sliceTransform * scaleMatrix;
}

View File

@ -22,7 +22,7 @@ namespace FModel.Services
private readonly Assets _staticAssets = new()
{
LargeImageKey = "official_logo", SmallImageKey = "verified", SmallImageText = $"v{Constants.APP_VERSION} ({Constants.APP_SHORT_COMMIT_ID})"
LargeImageKey = "official_logo", SmallImageKey = "verified", SmallImageText = $"v{Constants.APP_VERSION}"
};
private readonly Button[] _buttons =
@ -48,7 +48,7 @@ namespace FModel.Services
public void UpdatePresence(CUE4ParseViewModel viewModel) =>
UpdatePresence(
$"{viewModel.Provider.GameDisplayName ?? viewModel.Provider.ProjectName} - {viewModel.Provider.MountedVfs.Count}/{viewModel.Provider.MountedVfs.Count + viewModel.Provider.UnloadedVfs.Count} Packages",
$"{viewModel.Provider.GameDisplayName ?? viewModel.Provider.InternalGameName} - {viewModel.Provider.MountedVfs.Count}/{viewModel.Provider.MountedVfs.Count + viewModel.Provider.UnloadedVfs.Count} Packages",
$"Mode: {UserSettings.Default.LoadingMode.GetDescription()} - {viewModel.SearchVm.ResultsCount:### ### ###} Loaded Assets".Trim());
public void UpdatePresence(string details, string state)

View File

@ -30,21 +30,6 @@ public class CustomDirectory : ViewModel
new("Shop Backgrounds", "ShooterGame/Content/UI/OutOfGame/MainMenu/Store/Shared/Textures/"),
new("Weapon Renders", "ShooterGame/Content/UI/Screens/OutOfGame/MainMenu/Collection/Assets/Large/")
};
case "Dead by Daylight":
return new List<CustomDirectory>
{
new("Characters V1", "DeadByDaylight/Plugins/DBDCharacters/"),
new("Characters V2", "DeadByDaylight/Plugins/Runtime/Bhvr/DBDCharacters/"),
new("Characters (Deprecated)", "DeadbyDaylight/Content/Characters/"),
new("Meshes", "DeadByDaylight/Content/Meshes/"),
new("Textures", "DeadByDaylight/Content/Textures/"),
new("Icons", "DeadByDaylight/Content/UI/UMGAssets/Icons/"),
new("Blueprints", "DeadByDaylight/Content/Blueprints/"),
new("Audio Events", "DeadByDaylight/Content/Audio/Events/"),
new("Audio", "DeadByDaylight/Content/WwiseAudio/Cooked/"),
new("Data Tables", "DeadByDaylight/Content/Data/"),
new("Localization", "DeadByDaylight/Content/Localization/")
};
default:
return new List<CustomDirectory>();
}

View File

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using CUE4Parse.UE4.Assets.Exports.Texture;
using CUE4Parse.UE4.Versions;
@ -24,8 +24,7 @@ public class DirectorySettings : ViewModel, ICloneable
Endpoints = old?.Endpoints ?? EndpointSettings.Default(gameName),
Directories = old?.Directories ?? CustomDirectory.Default(gameName),
AesKeys = old?.AesKeys ?? new AesResponse { MainKey = aes, DynamicKeys = null },
LastAesReload = old?.LastAesReload ?? DateTime.Today.AddDays(-1),
CriwareDecryptionKey = old?.CriwareDecryptionKey ?? 0
LastAesReload = old?.LastAesReload ?? DateTime.Today.AddDays(-1)
};
}
@ -99,13 +98,6 @@ public class DirectorySettings : ViewModel, ICloneable
set => SetProperty(ref _lastAesReload, value);
}
private ulong _criwareDecryptionKey;
public ulong CriwareDecryptionKey
{
get => _criwareDecryptionKey;
set => SetProperty(ref _criwareDecryptionKey, value);
}
private bool Equals(DirectorySettings other)
{
return GameDirectory == other.GameDirectory && UeVersion == other.UeVersion;

View File

@ -16,8 +16,8 @@ public class EndpointSettings : ViewModel
case "Fortnite [LIVE]":
return new EndpointSettings[]
{
new("https://uedb.dev/svc/api/v1/fortnite/aes", "$.['mainKey','dynamicKeys']"),
new("https://uedb.dev/svc/api/v1/fortnite/mappings", "$.mappings.ZStandard")
new("https://fortnitecentral.genxgames.gg/api/v1/aes", "$.['mainKey','dynamicKeys']"),
new("https://fortnitecentral.genxgames.gg/api/v1/mappings", "$.[0].['url','fileName']") // just get the first available, not just oodle! (Unfortunately not default except when resetting settings)
};
default:
return new EndpointSettings[] { new(), new() };

View File

@ -3,14 +3,13 @@ using System.Collections.Generic;
using System.IO;
using System.Windows;
using System.Windows.Input;
using CUE4Parse.UE4.Assets.Exports.Material;
using CUE4Parse.UE4.Assets.Exports.Nanite;
using CUE4Parse.UE4.Versions;
using CUE4Parse_Conversion;
using CUE4Parse_Conversion.Animations;
using CUE4Parse.UE4.Versions;
using CUE4Parse_Conversion.Meshes;
using CUE4Parse_Conversion.Textures;
using CUE4Parse_Conversion.UEFormat.Enums;
using CUE4Parse.UE4.Assets.Exports.Material;
using FModel.Framework;
using FModel.ViewModels;
using FModel.ViewModels.ApiEndpoints.Models;
@ -61,7 +60,6 @@ namespace FModel.Settings
{
LodFormat = Default.LodExportFormat,
MeshFormat = Default.MeshExportFormat,
NaniteMeshFormat = Default.NaniteMeshExportFormat,
AnimFormat = Default.MeshExportFormat switch
{
EMeshFormat.UEFormat => EAnimFormat.UEFormat,
@ -73,8 +71,7 @@ namespace FModel.Settings
CompressionFormat = Default.CompressionFormat,
Platform = Default.CurrentDir.TexturePlatform,
ExportMorphTargets = Default.SaveMorphTargets,
ExportMaterials = Default.SaveEmbeddedMaterials,
ExportHdrTexturesAsHdr = Default.SaveHdrTexturesAsHdr
ExportMaterials = Default.SaveEmbeddedMaterials
};
private bool _showChangelog = true;
@ -140,6 +137,13 @@ namespace FModel.Settings
set => SetProperty(ref _lastOpenedSettingTab, value);
}
private bool _isAutoOpenSounds = true;
public bool IsAutoOpenSounds
{
get => _isAutoOpenSounds;
set => SetProperty(ref _isAutoOpenSounds, value);
}
private bool _isLoggerExpanded = true;
public bool IsLoggerExpanded
{
@ -196,13 +200,6 @@ namespace FModel.Settings
set => SetProperty(ref _keepDirectoryStructure, value);
}
private bool _showDecompileOption = false;
public bool ShowDecompileOption
{
get => _showDecompileOption;
set => SetProperty(ref _showDecompileOption, value);
}
private ECompressedAudio _compressedAudioMode = ECompressedAudio.PlayDecompressed;
public ECompressedAudio CompressedAudioMode
{
@ -259,13 +256,6 @@ namespace FModel.Settings
set => SetProperty(ref _readScriptData, value);
}
private bool _readShaderMaps;
public bool ReadShaderMaps
{
get => _readShaderMaps;
set => SetProperty(ref _readShaderMaps, value);
}
private IDictionary<string, DirectorySettings> _perDirectory = new Dictionary<string, DirectorySettings>();
public IDictionary<string, DirectorySettings> PerDirectory
{
@ -307,13 +297,6 @@ namespace FModel.Settings
set => SetProperty(ref _dirRightTab, value);
}
private Hotkey _switchAssetExplorer = new(Key.Z);
public Hotkey SwitchAssetExplorer
{
get => _switchAssetExplorer;
set => SetProperty(ref _switchAssetExplorer, value);
}
private Hotkey _assetLeftTab = new(Key.Q);
public Hotkey AssetLeftTab
{
@ -370,20 +353,13 @@ namespace FModel.Settings
set => SetProperty(ref _nextAudio, value);
}
private EMeshFormat _meshExportFormat = EMeshFormat.UEFormat;
private EMeshFormat _meshExportFormat = EMeshFormat.ActorX;
public EMeshFormat MeshExportFormat
{
get => _meshExportFormat;
set => SetProperty(ref _meshExportFormat, value);
}
private ENaniteMeshFormat _naniteMeshExportFormat = ENaniteMeshFormat.OnlyNaniteLOD;
public ENaniteMeshFormat NaniteMeshExportFormat
{
get => _naniteMeshExportFormat;
set => SetProperty(ref _naniteMeshExportFormat, value);
}
private EMaterialFormat _materialExportFormat = EMaterialFormat.FirstLayer;
public EMaterialFormat MaterialExportFormat
{
@ -447,13 +423,6 @@ namespace FModel.Settings
set => SetProperty(ref _cameraMode, value);
}
private int _wwiseMaxBnkPrefetch;
public int WwiseMaxBnkPrefetch
{
get => _wwiseMaxBnkPrefetch;
set => SetProperty(ref _wwiseMaxBnkPrefetch, value);
}
private int _previewMaxTextureSize = 1024;
public int PreviewMaxTextureSize
{
@ -475,13 +444,6 @@ namespace FModel.Settings
set => SetProperty(ref _previewSkeletalMeshes, value);
}
private bool _previewAnimations = true;
public bool PreviewAnimations
{
get => _previewAnimations;
set => SetProperty(ref _previewAnimations, value);
}
private bool _previewMaterials = true;
public bool PreviewMaterials
{
@ -516,26 +478,5 @@ namespace FModel.Settings
get => _saveSkeletonAsMesh;
set => SetProperty(ref _saveSkeletonAsMesh, value);
}
private bool _saveHdrTexturesAsHdr = true;
public bool SaveHdrTexturesAsHdr
{
get => _saveHdrTexturesAsHdr;
set => SetProperty(ref _saveHdrTexturesAsHdr, value);
}
private bool _featurePreviewNewAssetExplorer = true;
public bool FeaturePreviewNewAssetExplorer
{
get => _featurePreviewNewAssetExplorer;
set => SetProperty(ref _featurePreviewNewAssetExplorer, value);
}
private bool _previewTexturesAssetExplorer = true;
public bool PreviewTexturesAssetExplorer
{
get => _previewTexturesAssetExplorer;
set => SetProperty(ref _previewTexturesAssetExplorer, value);
}
}
}

View File

@ -1,19 +1,13 @@
using RestSharp;
using RestSharp.Interceptors;
namespace FModel.ViewModels.ApiEndpoints;
public abstract class AbstractApiProvider
{
protected readonly RestClient _client;
protected readonly Interceptor _interceptor;
protected AbstractApiProvider(RestClient client)
{
_client = client;
_interceptor = new CompatibilityInterceptor
{
OnBeforeDeserialization = resp => { resp.ContentType = "application/json; charset=utf-8"; }
};
}
}

View File

@ -1,7 +1,7 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using CUE4Parse.Utils;
using FModel.Extensions;
using FModel.Framework;
using FModel.ViewModels.ApiEndpoints.Models;
using Newtonsoft.Json.Linq;
@ -66,7 +66,7 @@ public class DynamicApiEndpoint : AbstractApiProvider
{
var request = new FRestRequest(url)
{
Interceptors = [_interceptor]
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);

View File

@ -16,7 +16,7 @@ namespace FModel.ViewModels.ApiEndpoints;
public class EpicApiEndpoint : AbstractApiProvider
{
private const string _OAUTH_URL = "https://account-public-service-prod03.ol.epicgames.com/account/api/oauth/token";
private const string _BASIC_TOKEN = "basic ZWM2ODRiOGM2ODdmNDc5ZmFkZWEzY2IyYWQ4M2Y1YzY6ZTFmMzFjMjExZjI4NDEzMTg2MjYyZDM3YTEzZmM4NGQ=";
private const string _BASIC_TOKEN = "basic MzQ0NmNkNzI2OTRjNGE0NDg1ZDgxYjc3YWRiYjIxNDE6OTIwOWQ0YTVlMjVhNDU3ZmI5YjA3NDg5ZDMxM2I0MWE=";
private const string _APP_URL = "https://launcher-public-service-prod06.ol.epicgames.com/launcher/api/public/assets/v2/platform/Windows/namespace/fn/catalogItem/4fe75bbc5a674f4f9b356b5c90567da5/app/Fortnite/label/Live";
public EpicApiEndpoint(RestClient client) : base(client) { }

View File

@ -5,7 +5,6 @@ using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using AutoUpdaterDotNET;
using CUE4Parse.Utils;
using FModel.Extensions;
using FModel.Framework;
using FModel.Services;
@ -106,11 +105,7 @@ public class FModelApiEndpoint : AbstractApiProvider
public void CheckForUpdates(bool launch = false)
{
if (DateTime.Now < UserSettings.Default.NextUpdateCheck)
{
Log.Warning("Updates have been silenced until {DateTime}", UserSettings.Default.NextUpdateCheck);
return;
}
if (DateTime.Now < UserSettings.Default.NextUpdateCheck) return;
if (launch)
{
@ -144,8 +139,7 @@ public class FModelApiEndpoint : AbstractApiProvider
{
UserSettings.Default.LastUpdateCheck = DateTime.Now;
var targetHash = ((CustomMandatory) args.Mandatory).CommitHash;
if (targetHash == Constants.APP_COMMIT_ID)
if (((CustomMandatory)args.Mandatory).CommitHash == Constants.APP_COMMIT_ID)
{
if (UserSettings.Default.ShowChangelog)
ShowChangelog(args);
@ -157,7 +151,6 @@ public class FModelApiEndpoint : AbstractApiProvider
UserSettings.Default.ShowChangelog = currentVersion != args.InstalledVersion;
const string message = "A new update is available!";
Log.Warning("{message} Version {CurrentVersion} ({Hash})", message, currentVersion, targetHash);
Helper.OpenWindow<AdonisWindow>(message, () => new UpdateView { Title = message, ResizeMode = ResizeMode.NoResize }.ShowDialog());
}
else

View File

@ -11,19 +11,19 @@ public class FortniteCentralApiEndpoint : AbstractApiProvider
{
public FortniteCentralApiEndpoint(RestClient client) : base(client) { }
public async Task<IDictionary<string, IDictionary<string, string>>> GetHotfixesAsync(CancellationToken token, string language = "en")
public async Task<Dictionary<string, Dictionary<string, string>>> GetHotfixesAsync(CancellationToken token, string language = "en")
{
var request = new FRestRequest("https://fortnitecentral.genxgames.gg/api/v1/hotfixes")
{
Interceptors = [_interceptor]
OnBeforeDeserialization = resp => { resp.ContentType = "application/json; charset=utf-8"; }
};
request.AddParameter("lang", language);
var response = await _client.ExecuteAsync<IDictionary<string, IDictionary<string, string>>>(request, token).ConfigureAwait(false);
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 IDictionary<string, IDictionary<string, string>> GetHotfixes(CancellationToken token, string language = "en")
public Dictionary<string, Dictionary<string, string>> GetHotfixes(CancellationToken token, string language = "en")
{
return GetHotfixesAsync(token, language).GetAwaiter().GetResult();
}

View File

@ -1,13 +1,15 @@
using System.Threading.Tasks;
using System.Threading.Tasks;
using FModel.Framework;
using FModel.ViewModels.ApiEndpoints.Models;
using RestSharp;
namespace FModel.ViewModels.ApiEndpoints;
public class GitHubApiEndpoint(RestClient client) : AbstractApiProvider(client)
public class GitHubApiEndpoint : AbstractApiProvider
{
public async Task<GitHubCommit[]> GetCommitHistoryAsync(string branch = "dev", int page = 1, int limit = 30)
public GitHubApiEndpoint(RestClient client) : base(client) { }
public async Task<GitHubCommit[]> GetCommitHistoryAsync(string branch = "dev", int page = 1, int limit = 20)
{
var request = new FRestRequest(Constants.GH_COMMITS_HISTORY);
request.AddParameter("sha", branch);
@ -23,11 +25,4 @@ public class GitHubApiEndpoint(RestClient client) : AbstractApiProvider(client)
var response = await _client.ExecuteAsync<GitHubRelease>(request).ConfigureAwait(false);
return response.Data;
}
public async Task<Author> GetUserAsync(string username)
{
var request = new FRestRequest($"https://api.github.com/users/{username}");
var response = await _client.ExecuteAsync<Author>(request).ConfigureAwait(false);
return response.Data;
}
}

View File

@ -1,5 +1,4 @@
using System;
using System.Linq;
using System.Windows;
using AdonisUI.Controls;
using AutoUpdaterDotNET;
@ -38,7 +37,8 @@ public class GitHubAsset : ViewModel
public class GitHubCommit : ViewModel
{
private string _sha;
[J("sha")] public string Sha
[J("sha")]
public string Sha
{
get => _sha;
set
@ -52,35 +52,6 @@ public class GitHubCommit : ViewModel
[J("commit")] public Commit Commit { get; set; }
[J("author")] public Author Author { get; set; }
private Author[] _coAuthors = [];
public Author[] CoAuthors
{
get => _coAuthors;
set
{
SetProperty(ref _coAuthors, value);
RaisePropertyChanged(nameof(Authors));
RaisePropertyChanged(nameof(AuthorNames));
}
}
public Author[] Authors => Author != null ? new[] { Author }.Concat(CoAuthors).ToArray() : CoAuthors;
public string AuthorNames
{
get
{
var authors = Authors;
return authors.Length switch
{
0 => string.Empty,
1 => authors[0].Login,
2 => $"{authors[0].Login} and {authors[1].Login}",
_ => string.Join(", ", authors.Take(authors.Length - 1).Select(a => a.Login)) + $", and {authors[^1].Login}"
};
}
}
private GitHubAsset _asset;
public GitHubAsset Asset
{

View File

@ -22,7 +22,7 @@ namespace FModel.ViewModels.ApiEndpoints;
public class ValorantApiEndpoint : AbstractApiProvider
{
private const string _URL = "https://valorant-api.com/v1/fmodel/manifest";
private const string _URL = "https://fmodel.fortnite-api.com/valorant/v2/manifest";
public ValorantApiEndpoint(RestClient client) : base(client) { }
@ -30,8 +30,6 @@ public class ValorantApiEndpoint : AbstractApiProvider
{
var request = new FRestRequest(_URL);
var response = await _client.ExecuteAsync(request, token).ConfigureAwait(false);
if (!response.IsSuccessful)
return null;
return new VManifest(response.RawBytes);
}
@ -118,6 +116,8 @@ public class VManifest
return chunkBytes;
}
public VPakStream GetPakStream(int index) => new VPakStream(this, index);
}
public readonly struct VHeader
@ -168,7 +168,6 @@ public readonly struct VPak
}
public string GetFullName() => $"ValorantLive/ShooterGame/Content/Paks/{Name}";
public VPakStream GetStream(VManifest manifest) => new(manifest, this);
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
@ -177,21 +176,22 @@ public readonly struct VChunk
public readonly ulong Id;
public readonly uint Size;
public string GetUrl() => $"https://valorant-api.com/v1/fmodel/chunks/{Id}";
public string GetUrl() => $"https://fmodel.fortnite-api.com/valorant/v2/chunks/{Id}";
}
public class VPakStream : RandomAccessStream, ICloneable
public class VPakStream : Stream, IRandomAccessStream, ICloneable
{
private readonly VManifest _manifest;
private readonly VPak _pak;
private readonly int _pakIndex;
private readonly VChunk[] _chunks;
public VPakStream(VManifest manifest, in VPak pak, long position = 0L)
public VPakStream(VManifest manifest, int pakIndex, long position = 0L)
{
_manifest = manifest;
_pak = pak;
_pakIndex = pakIndex;
_position = position;
var pak = manifest.Paks[pakIndex];
_chunks = new VChunk[pak.ChunkIndices.Length];
for (var i = 0; i < _chunks.Length; i++)
{
@ -201,12 +201,12 @@ public class VPakStream : RandomAccessStream, ICloneable
Length = pak.Size;
}
public object Clone() => new VPakStream(_manifest, _pak, _position);
public object Clone() => new VPakStream(_manifest, _pakIndex, _position);
public override int Read(byte[] buffer, int offset, int count) =>
ReadAsync(buffer, offset, count, CancellationToken.None).GetAwaiter().GetResult();
public override int ReadAt(long position, byte[] buffer, int offset, int count) =>
public int ReadAt(long position, byte[] buffer, int offset, int count) =>
ReadAtAsync(position, buffer, offset, count, CancellationToken.None).GetAwaiter().GetResult();
public override async Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
@ -216,7 +216,7 @@ public class VPakStream : RandomAccessStream, ICloneable
return bytesRead;
}
public override async Task<int> ReadAtAsync(long position, byte[] buffer, int offset, int count, CancellationToken cancellationToken = default)
public async Task<int> ReadAtAsync(long position, byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
var (i, startPos) = GetChunkIndex(position);
if (i == -1) return 0;
@ -248,6 +248,11 @@ public class VPakStream : RandomAccessStream, ICloneable
return bytesRead;
}
public Task<int> ReadAtAsync(long position, Memory<byte> memory, CancellationToken cancellationToken)
{
throw new NotSupportedException();
}
private async Task PrefetchAsync(int i, uint startPos, long count, CancellationToken cancellationToken, int concurrentDownloads = 4)
{
var tasks = new List<Task>();
@ -257,7 +262,7 @@ public class VPakStream : RandomAccessStream, ICloneable
await s.WaitAsync(cancellationToken).ConfigureAwait(false);
var chunk = _chunks[i++];
tasks.Add(PrefetchChunkAsync(_manifest, chunk, s, cancellationToken));
tasks.Add(PrefetchChunkAsync(chunk));
if (i == _chunks.Length) break;
count -= chunk.Size - startPos;
@ -266,12 +271,11 @@ public class VPakStream : RandomAccessStream, ICloneable
await Task.WhenAll(tasks).ConfigureAwait(false);
s.Dispose();
return;
static async Task PrefetchChunkAsync(VManifest manifest, VChunk chunk, SemaphoreSlim semaphore, CancellationToken cancellationToken)
async Task PrefetchChunkAsync(VChunk chunk)
{
await manifest.PrefetchChunk(chunk, cancellationToken).ConfigureAwait(false);
semaphore.Release();
await _manifest.PrefetchChunk(chunk, cancellationToken).ConfigureAwait(false);
s.Release(); // This is intended
}
}

View File

@ -6,12 +6,10 @@ using System.IO.Compression;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using CUE4Parse_Conversion.Textures.BC;
using CUE4Parse.Compression;
using CUE4Parse.Encryption.Aes;
using CUE4Parse.UE4.Objects.Core.Misc;
using CUE4Parse.UE4.VirtualFileSystem;
using FModel.Extensions;
using FModel.Framework;
using FModel.Services;
using FModel.Settings;
@ -44,32 +42,6 @@ public class ApplicationViewModel : ViewModel
private init => SetProperty(ref _status, value);
}
public IEnumerable<EAssetCategory> Categories { get; } = AssetCategoryExtensions.GetBaseCategories();
private bool _isAssetsExplorerVisible;
public bool IsAssetsExplorerVisible
{
get => _isAssetsExplorerVisible;
set
{
if (value && !UserSettings.Default.FeaturePreviewNewAssetExplorer)
return;
SetProperty(ref _isAssetsExplorerVisible, value);
}
}
private int _selectedLeftTabIndex;
public int SelectedLeftTabIndex
{
get => _selectedLeftTabIndex;
set
{
if (value is < 0 or > 2) return;
SetProperty(ref _selectedLeftTabIndex, value);
}
}
public RightClickMenuCommand RightClickMenuCommand => _rightClickMenuCommand ??= new RightClickMenuCommand(this);
private RightClickMenuCommand _rightClickMenuCommand;
public MenuCommand MenuCommand => _menuCommand ??= new MenuCommand(this);
@ -77,7 +49,7 @@ public class ApplicationViewModel : ViewModel
public CopyCommand CopyCommand => _copyCommand ??= new CopyCommand(this);
private CopyCommand _copyCommand;
public string InitialWindowTitle => $"FModel ({Constants.APP_SHORT_COMMIT_ID} - {Constants.APP_BUILD_DATE:MMM d, yyyy})";
public string InitialWindowTitle => $"FModel ({Constants.APP_SHORT_COMMIT_ID})";
public string GameDisplayName => CUE4Parse.Provider.GameDisplayName ?? "Unknown";
public string TitleExtra => $"({UserSettings.Default.CurrentDir.UeVersion}){(Build != EBuildKind.Release ? $" ({Build})" : "")}";
@ -223,37 +195,32 @@ public class ApplicationViewModel : ViewModel
public static async Task InitVgmStream()
{
var vgmZipFilePath = Path.Combine(UserSettings.Default.OutputDirectory, ".data", "vgmstream-win.zip");
var vgmFileInfo = new FileInfo(vgmZipFilePath);
if (File.Exists(vgmZipFilePath)) return;
if (!vgmFileInfo.Exists || vgmFileInfo.LastWriteTimeUtc < DateTime.UtcNow.AddMonths(-4))
await ApplicationService.ApiEndpointView.DownloadFileAsync("https://github.com/vgmstream/vgmstream/releases/latest/download/vgmstream-win.zip", vgmZipFilePath);
if (new FileInfo(vgmZipFilePath).Length > 0)
{
await ApplicationService.ApiEndpointView.DownloadFileAsync("https://github.com/vgmstream/vgmstream/releases/latest/download/vgmstream-win.zip", vgmZipFilePath);
vgmFileInfo.Refresh();
var zipDir = Path.GetDirectoryName(vgmZipFilePath)!;
await using var zipFs = File.OpenRead(vgmZipFilePath);
using var zip = new ZipArchive(zipFs, ZipArchiveMode.Read);
if (vgmFileInfo.Length > 0)
foreach (var entry in zip.Entries)
{
var zipDir = Path.GetDirectoryName(vgmZipFilePath)!;
await using var zipFs = File.OpenRead(vgmZipFilePath);
using var zip = new ZipArchive(zipFs, ZipArchiveMode.Read);
foreach (var entry in zip.Entries)
{
var entryPath = Path.Combine(zipDir, entry.FullName);
await using var entryFs = File.Create(entryPath);
await using var entryStream = entry.Open();
await entryStream.CopyToAsync(entryFs);
}
}
else
{
FLogger.Append(ELog.Error, () => FLogger.Text("Could not download VgmStream", Constants.WHITE, true));
var entryPath = Path.Combine(zipDir, entry.FullName);
await using var entryFs = File.Create(entryPath);
await using var entryStream = entry.Open();
await entryStream.CopyToAsync(entryFs);
}
}
else
{
FLogger.Append(ELog.Error, () => FLogger.Text("Could not download VgmStream", Constants.WHITE, true));
}
}
public static async Task InitImGuiSettings(bool forceDownload)
{
const string imgui = "imgui.ini";
var imgui = "imgui.ini";
var imguiPath = Path.Combine(UserSettings.Default.OutputDirectory, ".data", imgui);
if (File.Exists(imgui)) File.Move(imgui, imguiPath, true);
@ -266,64 +233,29 @@ public class ApplicationViewModel : ViewModel
}
}
public static async Task InitOodle()
public static async ValueTask InitOodle()
{
if (File.Exists(OodleHelper.OODLE_DLL_NAME_OLD))
var oodlePath = Path.Combine(UserSettings.Default.OutputDirectory, ".data", OodleHelper.OODLE_DLL_NAME);
if (File.Exists(OodleHelper.OODLE_DLL_NAME))
{
try
{
File.Delete(OodleHelper.OODLE_DLL_NAME_OLD);
}
catch { /* ignored */}
File.Move(OodleHelper.OODLE_DLL_NAME, oodlePath, true);
}
var oodlePath = Path.Combine(UserSettings.Default.OutputDirectory, ".data", OodleHelper.OODLE_DLL_NAME_OLD);
if (!File.Exists(oodlePath))
else if (!File.Exists(oodlePath))
{
oodlePath = Path.Combine(UserSettings.Default.OutputDirectory, ".data", OodleHelper.OODLE_DLL_NAME);
}
if (!File.Exists(oodlePath))
{
if (!await OodleHelper.DownloadOodleDllAsync(oodlePath))
{
FLogger.Append(ELog.Error, () => FLogger.Text("Failed to download Oodle", Constants.WHITE, true));
return;
}
await OodleHelper.DownloadOodleDllAsync(oodlePath);
}
OodleHelper.Initialize(oodlePath);
}
public static async Task InitZlib()
public static async ValueTask InitZlib()
{
var zlibPath = Path.Combine(UserSettings.Default.OutputDirectory, ".data", ZlibHelper.DLL_NAME);
var zlibFileInfo = new FileInfo(zlibPath);
if (!zlibFileInfo.Exists || zlibFileInfo.LastWriteTimeUtc < DateTime.UtcNow.AddMonths(-4))
if (!File.Exists(zlibPath))
{
if (!await ZlibHelper.DownloadDllAsync(zlibPath))
{
FLogger.Append(ELog.Error, () => FLogger.Text("Failed to download Zlib-ng", Constants.WHITE, true));
if (!zlibFileInfo.Exists) return;
}
await ZlibHelper.DownloadDllAsync(zlibPath);
}
ZlibHelper.Initialize(zlibPath);
}
public static async Task InitDetex()
{
var detexPath = Path.Combine(UserSettings.Default.OutputDirectory, ".data", DetexHelper.DLL_NAME);
if (File.Exists(DetexHelper.DLL_NAME))
{
File.Move(DetexHelper.DLL_NAME, detexPath, true);
}
else if (!File.Exists(detexPath))
{
await DetexHelper.LoadDllAsync(detexPath);
}
DetexHelper.Initialize(detexPath);
}
}

View File

@ -1,15 +1,11 @@
using System;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Data;
using CUE4Parse.FileProvider.Objects;
using CUE4Parse.UE4.Versions;
using CUE4Parse.UE4.VirtualFileSystem;
using FModel.Extensions;
using FModel.Framework;
using FModel.Services;
@ -17,11 +13,11 @@ namespace FModel.ViewModels;
public class TreeItem : ViewModel
{
private readonly string _header;
private string _header;
public string Header
{
get => _header;
private init => SetProperty(ref _header, value);
private set => SetProperty(ref _header, value);
}
private bool _isExpanded;
@ -59,139 +55,21 @@ public class TreeItem : ViewModel
private set => SetProperty(ref _version, value);
}
private string _searchText = string.Empty;
public string SearchText
{
get => _searchText;
set
{
if (SetProperty(ref _searchText, value))
{
RefreshFilters();
}
}
}
private EAssetCategory _selectedCategory = EAssetCategory.All;
public EAssetCategory SelectedCategory
{
get => _selectedCategory;
set
{
if (SetProperty(ref _selectedCategory, value))
_ = OnSelectedCategoryChanged();
}
}
public string PathAtThisPoint { get; }
public AssetsListViewModel AssetsList { get; } = new();
public RangeObservableCollection<TreeItem> Folders { get; } = [];
public AssetsListViewModel AssetsList { get; }
public RangeObservableCollection<TreeItem> Folders { get; }
public ICollectionView FoldersView { get; }
private ICollectionView _foldersView;
public ICollectionView FoldersView
{
get
{
_foldersView ??= new ListCollectionView(Folders)
{
SortDescriptions = { new SortDescription(nameof(Header), ListSortDirection.Ascending) }
};
return _foldersView;
}
}
private ICollectionView? _filteredFoldersView;
public ICollectionView? FilteredFoldersView
{
get
{
_filteredFoldersView ??= new ListCollectionView(Folders)
{
SortDescriptions = { new SortDescription(nameof(Header), ListSortDirection.Ascending) },
Filter = e => ItemFilter(e, SearchText.Trim().Split(' ', StringSplitOptions.RemoveEmptyEntries))
};
return _filteredFoldersView;
}
}
private CompositeCollection _combinedEntries;
public CompositeCollection CombinedEntries
{
get
{
if (_combinedEntries == null)
{
void CreateCombinedEntries()
{
_combinedEntries = new CompositeCollection
{
new CollectionContainer { Collection = FilteredFoldersView },
new CollectionContainer { Collection = AssetsList.AssetsView }
};
}
if (!Application.Current.Dispatcher.CheckAccess())
{
Application.Current.Dispatcher.Invoke(CreateCombinedEntries);
}
else
{
CreateCombinedEntries();
}
}
return _combinedEntries;
}
}
public TreeItem Parent { get; init; }
public TreeItem(string header, GameFile entry, string pathHere)
public TreeItem(string header, string archive, string mountPoint, FPackageFileVersion version, string pathHere)
{
Header = header;
if (entry is VfsEntry vfsEntry)
{
Archive = vfsEntry.Vfs.Name;
MountPoint = vfsEntry.Vfs.MountPoint;
Version = vfsEntry.Vfs.Ver;
}
Archive = archive;
MountPoint = mountPoint;
Version = version;
PathAtThisPoint = pathHere;
AssetsList.AssetsView.Filter = o => ItemFilter(o, SearchText.Trim().Split(' ', StringSplitOptions.RemoveEmptyEntries));
}
private void RefreshFilters()
{
AssetsList.AssetsView.Refresh();
FilteredFoldersView?.Refresh();
}
private bool ItemFilter(object item, IEnumerable<string> filters)
{
var f = filters.ToArray();
switch (item)
{
case GameFileViewModel entry:
{
bool matchesSearch = f.Length == 0 || f.All(x => entry.Asset.Name.Contains(x, StringComparison.OrdinalIgnoreCase));
bool matchesCategory = SelectedCategory == EAssetCategory.All || entry.AssetCategory.IsOfCategory(SelectedCategory);
return matchesSearch && matchesCategory;
}
case TreeItem folder:
{
bool matchesSearch = f.Length == 0 || f.All(x => folder.Header.Contains(x, StringComparison.OrdinalIgnoreCase));
bool matchesCategory = SelectedCategory == EAssetCategory.All;
return matchesSearch && matchesCategory;
}
}
return false;
}
private async Task OnSelectedCategoryChanged()
{
await Task.WhenAll(AssetsList.Assets.Select(asset => asset.ResolveAsync(EResolveCompute.Category)));
RefreshFilters();
AssetsList = new AssetsListViewModel();
Folders = new RangeObservableCollection<TreeItem>();
FoldersView = new ListCollectionView(Folders) { SortDescriptions = { new SortDescription("Header", ListSortDirection.Ascending) } };
}
public override string ToString() => $"{Header} | {Folders.Count} Folders | {AssetsList.Assets.Count} Files";
@ -204,11 +82,11 @@ public class AssetsFolderViewModel
public AssetsFolderViewModel()
{
Folders = [];
Folders = new RangeObservableCollection<TreeItem>();
FoldersView = new ListCollectionView(Folders) { SortDescriptions = { new SortDescription("Header", ListSortDirection.Ascending) } };
}
public void BulkPopulate(IReadOnlyCollection<GameFile> entries)
public void BulkPopulate(IReadOnlyCollection<VfsEntry> entries)
{
if (entries == null || entries.Count == 0)
return;
@ -217,59 +95,54 @@ public class AssetsFolderViewModel
{
var treeItems = new RangeObservableCollection<TreeItem>();
treeItems.SetSuppressionState(true);
var items = new List<AssetItem>(entries.Count);
foreach (var entry in entries)
{
TreeItem lastNode = null;
TreeItem parentItem = null;
var folders = entry.Path.Split('/', StringSplitOptions.RemoveEmptyEntries);
var builder = new StringBuilder(64);
var parentNode = treeItems;
var item = new AssetItem(entry.Path, entry.IsEncrypted, entry.Offset, entry.Size, entry.Vfs.Name, entry.CompressionMethod);
items.Add(item);
for (var i = 0; i < folders.Length - 1; i++)
{
var folder = folders[i];
builder.Append(folder).Append('/');
lastNode = FindByHeaderOrNull(parentNode, folder);
TreeItem lastNode = null;
var folders = item.FullPath.Split('/', StringSplitOptions.RemoveEmptyEntries);
var builder = new StringBuilder(64);
var parentNode = treeItems;
static TreeItem FindByHeaderOrNull(IReadOnlyList<TreeItem> list, string header)
for (var i = 0; i < folders.Length - 1; i++)
{
for (var i = 0; i < list.Count; i++)
var folder = folders[i];
builder.Append(folder).Append('/');
lastNode = FindByHeaderOrNull(parentNode, folder);
static TreeItem FindByHeaderOrNull(IReadOnlyList<TreeItem> list, string header)
{
if (list[i].Header == header)
return list[i];
for (var i = 0; i < list.Count; i++)
{
if (list[i].Header == header)
return list[i];
}
return null;
}
return null;
}
if (lastNode == null)
{
var nodePath = builder.ToString();
lastNode = new TreeItem(folder, entry, nodePath[..^1])
if (lastNode == null)
{
Parent = parentItem
};
lastNode.Folders.SetSuppressionState(true);
lastNode.AssetsList.Assets.SetSuppressionState(true);
parentNode.Add(lastNode);
var nodePath = builder.ToString();
lastNode = new TreeItem(folder, item.Archive, entry.Vfs.MountPoint, entry.Vfs.Ver, nodePath[..^1]);
lastNode.Folders.SetSuppressionState(true);
lastNode.AssetsList.Assets.SetSuppressionState(true);
parentNode.Add(lastNode);
}
parentNode = lastNode.Folders;
}
parentItem = lastNode;
parentNode = lastNode.Folders;
lastNode?.AssetsList.Assets.Add(item);
}
lastNode?.AssetsList.Add(entry);
}
if (treeItems.Count > 0)
{
var projectName = ApplicationService.ApplicationView.CUE4Parse.Provider.ProjectName;
(treeItems.FirstOrDefault(x => x.Header.Equals(projectName, StringComparison.OrdinalIgnoreCase)) ?? treeItems[0]).IsSelected = true;
}
Folders.AddRange(treeItems);
ApplicationService.ApplicationView.CUE4Parse.SearchVm.ChangeCollection(entries);
ApplicationService.ApplicationView.CUE4Parse.SearchVm.SearchResults.AddRange(items);
foreach (var folder in Folders)
InvokeOnCollectionChanged(folder);
@ -281,6 +154,7 @@ public class AssetsFolderViewModel
if (item.Folders.Count != 0)
{
item.Folders.SetSuppressionState(false);
item.Folders.InvokeOnCollectionChanged();
foreach (var folderItem in item.Folders)

View File

@ -1,26 +1,78 @@
using System.ComponentModel;
using System.ComponentModel;
using System.Windows.Data;
using CUE4Parse.FileProvider.Objects;
using CUE4Parse.Compression;
using FModel.Framework;
namespace FModel.ViewModels;
public class AssetsListViewModel
public class AssetItem : ViewModel
{
public RangeObservableCollection<GameFileViewModel> Assets { get; } = [];
private ICollectionView _assetsView;
public ICollectionView AssetsView
private string _fullPath;
public string FullPath
{
get
{
_assetsView ??= new ListCollectionView(Assets)
{
SortDescriptions = { new SortDescription("Asset.Path", ListSortDirection.Ascending) }
};
return _assetsView;
}
get => _fullPath;
private set => SetProperty(ref _fullPath, value);
}
public void Add(GameFile gameFile) => Assets.Add(new GameFileViewModel(gameFile));
private bool _isEncrypted;
public bool IsEncrypted
{
get => _isEncrypted;
private set => SetProperty(ref _isEncrypted, value);
}
private long _offset;
public long Offset
{
get => _offset;
private set => SetProperty(ref _offset, value);
}
private long _size;
public long Size
{
get => _size;
private set => SetProperty(ref _size, value);
}
private string _archive;
public string Archive
{
get => _archive;
private set => SetProperty(ref _archive, value);
}
private CompressionMethod _compression;
public CompressionMethod Compression
{
get => _compression;
private set => SetProperty(ref _compression, value);
}
public AssetItem(string fullPath, bool isEncrypted, long offset, long size, string archive, CompressionMethod compression)
{
FullPath = fullPath;
IsEncrypted = isEncrypted;
Offset = offset;
Size = size;
Archive = archive;
Compression = compression;
}
public override string ToString() => FullPath;
}
public class AssetsListViewModel
{
public RangeObservableCollection<AssetItem> Assets { get; }
public ICollectionView AssetsView { get; }
public AssetsListViewModel()
{
Assets = new RangeObservableCollection<AssetItem>();
AssetsView = new ListCollectionView(Assets)
{
SortDescriptions = { new SortDescription("FullPath", ListSortDirection.Ascending) }
};
}
}

View File

@ -1,3 +1,7 @@
using CSCore;
using CSCore.DSP;
using CSCore.SoundOut;
using CSCore.Streams;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
@ -8,15 +12,7 @@ using System.Linq;
using System.Threading;
using System.Windows;
using System.Windows.Data;
using CSCore;
using CSCore.CoreAudioAPI;
using CSCore.DSP;
using CSCore.SoundOut;
using CSCore.Streams;
using CUE4Parse.UE4.CriWare.Decoders;
using CUE4Parse.UE4.CriWare.Decoders.ADX;
using CUE4Parse.UE4.CriWare.Decoders.HCA;
using CUE4Parse.Utils;
using FModel.Extensions;
using FModel.Framework;
using FModel.Services;
@ -297,14 +293,6 @@ public class AudioPlayerViewModel : ViewModel, ISource, IDisposable
{
Save(a, true);
}
FLogger.Append(ELog.Information, () =>
{
FLogger.Text("Successfully saved audio from ", Constants.WHITE);
FLogger.Link(_audioFiles.First().FileName, _audioFiles.First().FilePath, true);
});
if (_audioFiles.Count > 1)
FLogger.Append(ELog.Information, () => FLogger.Text($"Successfully saved {_audioFiles.Count} audio files", Constants.WHITE, true));
});
}
@ -340,22 +328,16 @@ public class AudioPlayerViewModel : ViewModel, ISource, IDisposable
if (File.Exists(path))
{
Log.Information("{FileName} successfully saved", fileToSave.FileName);
if (!auto)
FLogger.Append(ELog.Information, () =>
{
FLogger.Append(ELog.Information, () =>
{
FLogger.Text("Successfully saved ", Constants.WHITE);
FLogger.Link(fileToSave.FileName, path, true);
});
}
FLogger.Text("Successfully saved ", Constants.WHITE);
FLogger.Link(fileToSave.FileName, path, true);
});
}
else
{
Log.Error("{FileName} could not be saved", fileToSave.FileName);
if (!auto)
{
FLogger.Append(ELog.Error, () => FLogger.Text($"Could not save '{fileToSave.FileName}'", Constants.WHITE, true));
}
FLogger.Append(ELog.Error, () => FLogger.Text($"Could not save '{fileToSave.FileName}'", Constants.WHITE, true));
}
}
@ -566,9 +548,7 @@ public class AudioPlayerViewModel : ViewModel, ISource, IDisposable
switch (SelectedAudioFile.Extension)
{
case "binka":
case "adpcm":
case "xvag":
case "opus":
case "wem":
case "at9":
@ -583,12 +563,9 @@ public class AudioPlayerViewModel : ViewModel, ISource, IDisposable
return false;
}
case "adx":
case "hca":
return TryConvertCriware();
case "rada":
case "binka":
{
if (TryDecode(SelectedAudioFile.Extension, out var rawFilePath))
if (TryDecode(out var rawFilePath))
{
var newAudio = new AudioFile(SelectedAudioFile.Id, new FileInfo(rawFilePath));
Replace(newAudio);
@ -602,48 +579,6 @@ public class AudioPlayerViewModel : ViewModel, ISource, IDisposable
return true;
}
private bool TryConvertCriware()
{
try
{
byte[] wavData = SelectedAudioFile.Extension switch
{
"hca" => HcaWaveStream.ConvertHcaToWav(
SelectedAudioFile.Data,
UserSettings.Default.CurrentDir.CriwareDecryptionKey),
"adx" => AdxDecoder.ConvertAdxToWav(
SelectedAudioFile.Data,
UserSettings.Default.CurrentDir.CriwareDecryptionKey),
_ => throw new NotSupportedException()
};
string wavFilePath = Path.Combine(
UserSettings.Default.AudioDirectory,
SelectedAudioFile.FilePath.TrimStart('/'));
wavFilePath = Path.ChangeExtension(wavFilePath, ".wav");
Directory.CreateDirectory(Path.GetDirectoryName(wavFilePath)!);
File.WriteAllBytes(wavFilePath, wavData);
var newAudio = new AudioFile(SelectedAudioFile.Id, new FileInfo(wavFilePath));
Replace(newAudio);
return true;
}
catch (CriwareDecryptionException ex)
{
FLogger.Append(ELog.Error, () => FLogger.Text($"Encrypted {SelectedAudioFile.Extension.ToUpper()}: {ex.Message}", Constants.WHITE, true));
Log.Error($"Encrypted {SelectedAudioFile.Extension.ToUpper()}: {ex.Message}");
return false;
}
catch (Exception ex)
{
FLogger.Append(ELog.Error, () => FLogger.Text($"Failed to convert {SelectedAudioFile.Extension.ToUpper()}: {ex.Message}", Constants.WHITE, true));
Log.Error($"Failed to convert {SelectedAudioFile.Extension.ToUpper()}: {ex.Message}");
return false;
}
}
private bool TryConvert(out string wavFilePath) => TryConvert(SelectedAudioFile.FilePath, SelectedAudioFile.Data, out wavFilePath);
private bool TryConvert(string inputFilePath, byte[] inputFileData, out string wavFilePath)
{
@ -672,11 +607,11 @@ public class AudioPlayerViewModel : ViewModel, ISource, IDisposable
return vgmProcess?.ExitCode == 0 && File.Exists(wavFilePath);
}
private bool TryDecode(string extension, out string rawFilePath)
private bool TryDecode(out string rawFilePath)
{
rawFilePath = string.Empty;
var decoderPath = Path.Combine(UserSettings.Default.OutputDirectory, ".data", $"{extension}dec.exe");
if (!File.Exists(decoderPath))
var binkadecPath = Path.Combine(UserSettings.Default.OutputDirectory, ".data", "binkadec.exe");
if (!File.Exists(binkadecPath))
{
return false;
}
@ -685,16 +620,16 @@ public class AudioPlayerViewModel : ViewModel, ISource, IDisposable
File.WriteAllBytes(SelectedAudioFile.FilePath, SelectedAudioFile.Data);
rawFilePath = Path.ChangeExtension(SelectedAudioFile.FilePath, ".wav");
var decoderProcess = Process.Start(new ProcessStartInfo
var binkadecProcess = Process.Start(new ProcessStartInfo
{
FileName = decoderPath,
FileName = binkadecPath,
Arguments = $"-i \"{SelectedAudioFile.FilePath}\" -o \"{rawFilePath}\"",
UseShellExecute = false,
CreateNoWindow = true
});
decoderProcess?.WaitForExit(5000);
binkadecProcess?.WaitForExit(5000);
File.Delete(SelectedAudioFile.FilePath);
return decoderProcess?.ExitCode == 0 && File.Exists(rawFilePath);
return binkadecProcess?.ExitCode == 0 && File.Exists(rawFilePath);
}
}

View File

@ -7,6 +7,7 @@ using System.Threading.Tasks;
using System.Windows;
using System.Windows.Data;
using CUE4Parse.FileProvider.Objects;
using CUE4Parse.UE4.VirtualFileSystem;
using FModel.Framework;
using FModel.Services;
using FModel.Settings;
@ -66,7 +67,7 @@ public class BackupManagerViewModel : ViewModel
var backupFolder = Path.Combine(UserSettings.Default.OutputDirectory, "Backups");
var fileName = $"{_gameName}_{DateTime.Now:MM'_'dd'_'yyyy}.fbkp";
var fullPath = Path.Combine(backupFolder, fileName);
var func = new Func<GameFile, bool>(x => !x.IsUePackagePayload);
var func = new Func<GameFile, bool>(x => !x.Path.EndsWith(".uexp") && !x.Path.EndsWith(".ubulk") && !x.Path.EndsWith(".uptnl"));
using var fileStream = new FileStream(fullPath, FileMode.Create);
using var compressedStream = LZ4Stream.Encode(fileStream, LZ4Level.L00_FAST);
@ -80,7 +81,7 @@ public class BackupManagerViewModel : ViewModel
if (!func(asset)) continue;
writer.Write(asset.Size);
writer.Write(asset.IsEncrypted);
writer.Write(asset.Path);
writer.Write($"/{asset.Path.ToLower()}");
}
SaveCheck(fullPath, fileName, "created", "create");
@ -121,7 +122,6 @@ public enum EBackupVersion : byte
{
BeforeVersionWasAdded = 0,
Initial,
PerfectPath, // no more leading slash and ToLower
LatestPlusOne,
Latest = LatestPlusOne - 1

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,8 @@
using System.Collections;
using System.Collections.Generic;
using System.Collections;
using System.Linq;
using System.Text;
using System.Windows;
using CUE4Parse.FileProvider.Objects;
using FModel.Extensions;
using FModel.Framework;
namespace FModel.ViewModels.Commands;
@ -19,37 +18,29 @@ public class CopyCommand : ViewModelCommand<ApplicationViewModel>
if (parameter is not object[] parameters || parameters[0] is not string trigger)
return;
var entries = (parameters[1] as IEnumerable)?.OfType<object>()
.SelectMany(item => item switch
{
GameFile gf => new[] { gf },
GameFileViewModel gvm => new[] { gvm.Asset },
_ => []
}) ?? [];
if (!entries.Any())
return;
var assetItems = ((IList) parameters[1]).Cast<AssetItem>().ToArray();
if (!assetItems.Any()) return;
var sb = new StringBuilder();
switch (trigger)
{
case "File_Path":
foreach (var entry in entries) sb.AppendLine(entry.Path);
foreach (var asset in assetItems) sb.AppendLine(asset.FullPath);
break;
case "File_Name":
foreach (var entry in entries) sb.AppendLine(entry.Name);
foreach (var asset in assetItems) sb.AppendLine(asset.FullPath.SubstringAfterLast('/'));
break;
case "Directory_Path":
foreach (var entry in entries) sb.AppendLine(entry.Directory);
foreach (var asset in assetItems) sb.AppendLine(asset.FullPath.SubstringBeforeLast('/'));
break;
case "File_Path_No_Extension":
foreach (var entry in entries) sb.AppendLine(entry.PathWithoutExtension);
foreach (var asset in assetItems) sb.AppendLine(asset.FullPath.SubstringBeforeLast('.'));
break;
case "File_Name_No_Extension":
foreach (var entry in entries) sb.AppendLine(entry.NameWithoutExtension);
foreach (var asset in assetItems) sb.AppendLine(asset.FullPath.SubstringAfterLast('/').SubstringBeforeLast('.'));
break;
}
Clipboard.SetText(sb.ToString().TrimEnd());
}
}
}

View File

@ -21,7 +21,7 @@ public class GoToCommand : ViewModelCommand<CustomDirectoriesViewModel>
public TreeItem JumpTo(string directory)
{
_applicationView.SelectedLeftTabIndex = 1; // folders tab
MainWindow.YesWeCats.LeftTabControl.SelectedIndex = 1; // folders tab
var root = _applicationView.CUE4Parse.AssetsFolder.Folders;
if (root is not { Count: > 0 }) return null;
@ -54,4 +54,4 @@ public class GoToCommand : ViewModelCommand<CustomDirectoriesViewModel>
return null;
}
}
}

View File

@ -14,34 +14,34 @@ public class ImageCommand : ViewModelCommand<TabItem>
{
}
public override void Execute(TabItem tabViewModel, object parameter)
public override void Execute(TabItem contextViewModel, object parameter)
{
if (parameter == null || !tabViewModel.HasImage) return;
if (parameter == null || !contextViewModel.HasImage) return;
switch (parameter)
{
case "Open":
{
Helper.OpenWindow<AdonisWindow>(tabViewModel.SelectedImage.ExportName + " (Image)", () =>
Helper.OpenWindow<AdonisWindow>(contextViewModel.SelectedImage.ExportName + " (Image)", () =>
{
var popout = new ImagePopout
{
Title = tabViewModel.SelectedImage.ExportName + " (Image)",
Width = tabViewModel.SelectedImage.Image.Width,
Height = tabViewModel.SelectedImage.Image.Height,
WindowState = tabViewModel.SelectedImage.Image.Height > 1000 ? WindowState.Maximized : WindowState.Normal,
ImageCtrl = { Source = tabViewModel.SelectedImage.Image }
Title = contextViewModel.SelectedImage.ExportName + " (Image)",
Width = contextViewModel.SelectedImage.Image.Width,
Height = contextViewModel.SelectedImage.Image.Height,
WindowState = contextViewModel.SelectedImage.Image.Height > 1000 ? WindowState.Maximized : WindowState.Normal,
ImageCtrl = { Source = contextViewModel.SelectedImage.Image }
};
RenderOptions.SetBitmapScalingMode(popout.ImageCtrl, BoolToRenderModeConverter.Instance.Convert(tabViewModel.SelectedImage.RenderNearestNeighbor));
RenderOptions.SetBitmapScalingMode(popout.ImageCtrl, BoolToRenderModeConverter.Instance.Convert(contextViewModel.SelectedImage.RenderNearestNeighbor));
popout.Show();
});
break;
}
case "Copy":
ClipboardExtensions.SetImage(tabViewModel.SelectedImage.ImageBuffer, $"{tabViewModel.SelectedImage.ExportName}.png");
ClipboardExtensions.SetImage(contextViewModel.SelectedImage.ImageBuffer, $"{contextViewModel.SelectedImage.ExportName}.png");
break;
case "Save":
tabViewModel.SaveImage();
contextViewModel.SaveImage();
break;
}
}

View File

@ -8,10 +8,9 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using AdonisUI.Controls;
using CUE4Parse.FileProvider.Objects;
using CUE4Parse.UE4.Readers;
using CUE4Parse.UE4.VirtualFileSystem;
using CUE4Parse.Utils;
using FModel.Creator;
using FModel.Extensions;
using FModel.Framework;
using FModel.Services;
@ -38,26 +37,21 @@ public class LoadCommand : ViewModelCommand<LoadingModesViewModel>
public override async void Execute(LoadingModesViewModel contextViewModel, object parameter)
{
if (_applicationView.CUE4Parse.GameDirectory.HasNoFile) return;
if (_applicationView.CUE4Parse.Provider.Keys.Count == 0 && _applicationView.CUE4Parse.Provider.RequiredKeys.Count > 0)
{
FLogger.Append(ELog.Error, () =>
FLogger.Text("An encrypted archive has been found. In order to decrypt it, please specify a working AES encryption key", Constants.WHITE, true));
return;
}
if (_applicationView.CUE4Parse.Provider.Files.Count == 0)
{
FLogger.Append(ELog.Error, () => FLogger.Text("No files were found in the archives or the specified directory", Constants.WHITE, true));
return;
}
#if DEBUG
var loadingTime = Stopwatch.StartNew();
#endif
_applicationView.CUE4Parse.AssetsFolder.Folders.Clear();
_applicationView.CUE4Parse.SearchVm.SearchResults.Clear();
_applicationView.SelectedLeftTabIndex = 1; // folders tab
_applicationView.IsAssetsExplorerVisible = true;
Helper.CloseWindow<AdonisWindow>("Search For Packages"); // close search window if opened
MainWindow.YesWeCats.LeftTabControl.SelectedIndex = 1; // folders tab
Helper.CloseWindow<AdonisWindow>("Search View"); // close search window if opened
await Task.WhenAll(
_applicationView.CUE4Parse.LoadLocalizedResources(), // load locres if not already loaded,
@ -65,17 +59,12 @@ public class LoadCommand : ViewModelCommand<LoadingModesViewModel>
_threadWorkerView.Begin(cancellationToken =>
{
// filter what to show
_applicationView.Status.UpdateStatusLabel("Packages", "Filtering");
switch (UserSettings.Default.LoadingMode)
{
case ELoadingMode.Multiple:
{
var l = (IList) parameter;
if (l.Count == 0)
{
UserSettings.Default.LoadingMode = ELoadingMode.All;
goto case ELoadingMode.All;
}
if (l.Count < 1) return;
var directoryFilesToShow = l.Cast<FileItem>();
FilterDirectoryFilesToDisplay(cancellationToken, directoryFilesToShow);
@ -92,11 +81,6 @@ public class LoadCommand : ViewModelCommand<LoadingModesViewModel>
FilterNewOrModifiedFilesToDisplay(cancellationToken);
break;
}
case ELoadingMode.AllButPatched:
{
FilterPacthedFilesToDisplay(cancellationToken);
break;
}
default: throw new ArgumentOutOfRangeException();
}
@ -116,36 +100,42 @@ public class LoadCommand : ViewModelCommand<LoadingModesViewModel>
if (directoryFiles == null) filter = null;
else
{
filter = [];
filter = new HashSet<string>();
foreach (var directoryFile in directoryFiles)
{
if (!directoryFile.IsEnabled) continue;
if (!directoryFile.IsEnabled)
continue;
filter.Add(directoryFile.Name);
}
}
var hasFilter = filter != null && filter.Count != 0;
var entries = new List<GameFile>();
var entries = new List<VfsEntry>();
foreach (var asset in _applicationView.CUE4Parse.Provider.Files.Values)
{
cancellationToken.ThrowIfCancellationRequested(); // cancel if needed
if (asset.IsUePackagePayload) continue;
if (asset is not VfsEntry entry || entry.Path.EndsWith(".uexp") || entry.Path.EndsWith(".ubulk") || entry.Path.EndsWith(".uptnl"))
continue;
if (hasFilter)
{
if (asset is VfsEntry entry && filter.Contains(entry.Vfs.Name))
if (filter.Contains(entry.Vfs.Name))
{
entries.Add(asset);
entries.Add(entry);
_applicationView.Status.UpdateStatusLabel(entry.Vfs.Name);
}
}
else
{
entries.Add(asset);
entries.Add(entry);
_applicationView.Status.UpdateStatusLabel(entry.Vfs.Name);
}
}
_applicationView.Status.UpdateStatusLabel($"{entries.Count:### ### ###} Packages");
_applicationView.Status.UpdateStatusLabel("Folders & Packages");
_applicationView.CUE4Parse.AssetsFolder.BulkPopulate(entries);
}
@ -167,11 +157,11 @@ public class LoadCommand : ViewModelCommand<LoadingModesViewModel>
var mode = UserSettings.Default.LoadingMode;
var entries = ParseBackup(openFileDialog.FileName, mode, cancellationToken);
_applicationView.Status.UpdateStatusLabel($"{entries.Count:### ### ###} Packages");
_applicationView.Status.UpdateStatusLabel($"{mode.ToString()[6..]} Folders & Packages");
_applicationView.CUE4Parse.AssetsFolder.BulkPopulate(entries);
}
private List<GameFile> ParseBackup(string path, ELoadingMode mode, CancellationToken cancellationToken = default)
private List<VfsEntry> ParseBackup(string path, ELoadingMode mode, CancellationToken cancellationToken = default)
{
using var fileStream = new FileStream(path, FileMode.Open);
using var memoryStream = new MemoryStream();
@ -186,13 +176,13 @@ public class LoadCommand : ViewModelCommand<LoadingModesViewModel>
memoryStream.Position = 0;
using var archive = new FStreamArchive(fileStream.Name, memoryStream);
var entries = new List<GameFile>();
var entries = new List<VfsEntry>();
switch (mode)
{
case ELoadingMode.AllButNew:
{
var paths = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var paths = new HashSet<string>();
var magic = archive.Read<uint>();
if (magic != BackupManagerViewModel.FBKP_MAGIC)
{
@ -202,7 +192,7 @@ public class LoadCommand : ViewModelCommand<LoadingModesViewModel>
cancellationToken.ThrowIfCancellationRequested();
archive.Position += 29;
paths.Add(archive.ReadString()[1..]);
paths.Add(archive.ReadString().ToLower()[1..]);
archive.Position += 4;
}
}
@ -215,19 +205,18 @@ public class LoadCommand : ViewModelCommand<LoadingModesViewModel>
cancellationToken.ThrowIfCancellationRequested();
archive.Position += sizeof(long) + sizeof(byte);
var fullPath = archive.ReadString();
if (version < EBackupVersion.PerfectPath) fullPath = fullPath[1..];
paths.Add(fullPath);
paths.Add(archive.ReadString().ToLower()[1..]);
}
}
foreach (var (key, asset) in _applicationView.CUE4Parse.Provider.Files)
foreach (var (key, value) in _applicationView.CUE4Parse.Provider.Files)
{
cancellationToken.ThrowIfCancellationRequested();
if (asset.IsUePackagePayload || paths.Contains(key)) continue;
if (value is not VfsEntry entry || paths.Contains(key) || entry.Path.EndsWith(".uexp") ||
entry.Path.EndsWith(".ubulk") || entry.Path.EndsWith(".uptnl")) continue;
entries.Add(asset);
entries.Add(entry);
_applicationView.Status.UpdateStatusLabel(entry.Vfs.Name);
}
break;
@ -246,7 +235,7 @@ public class LoadCommand : ViewModelCommand<LoadingModesViewModel>
var uncompressedSize = archive.Read<long>();
var isEncrypted = archive.ReadFlag();
archive.Position += 4;
var fullPath = archive.ReadString()[1..];
var fullPath = archive.ReadString().ToLower()[1..];
archive.Position += 4;
AddEntry(fullPath, uncompressedSize, isEncrypted, entries);
@ -262,8 +251,7 @@ public class LoadCommand : ViewModelCommand<LoadingModesViewModel>
var uncompressedSize = archive.Read<long>();
var isEncrypted = archive.ReadFlag();
var fullPath = archive.ReadString();
if (version < EBackupVersion.PerfectPath) fullPath = fullPath[1..];
var fullPath = archive.ReadString().ToLower()[1..];
AddEntry(fullPath, uncompressedSize, isEncrypted, entries);
}
@ -275,34 +263,14 @@ public class LoadCommand : ViewModelCommand<LoadingModesViewModel>
return entries;
}
private void AddEntry(string path, long uncompressedSize, bool isEncrypted, List<GameFile> entries)
private void AddEntry(string path, long uncompressedSize, bool isEncrypted, List<VfsEntry> entries)
{
if (!_applicationView.CUE4Parse.Provider.Files.TryGetValue(path, out var asset) ||
asset.IsUePackagePayload || asset.Size == uncompressedSize && asset.IsEncrypted == isEncrypted)
if (path.EndsWith(".uexp") || path.EndsWith(".ubulk") || path.EndsWith(".uptnl") ||
!_applicationView.CUE4Parse.Provider.Files.TryGetValue(path, out var asset) || asset is not VfsEntry entry ||
entry.Size == uncompressedSize && entry.IsEncrypted == isEncrypted)
return;
entries.Add(asset);
}
private void FilterPacthedFilesToDisplay(CancellationToken cancellationToken)
{
var loaded = new Dictionary<string, GameFile>(_applicationView.CUE4Parse.Provider.PathComparer);
foreach (var (key, asset) in _applicationView.CUE4Parse.Provider.Files)
{
cancellationToken.ThrowIfCancellationRequested(); // cancel if needed
if (asset.IsUePackagePayload) continue;
if (asset is VfsEntry entry && loaded.TryGetValue(key, out var file) &&
file is VfsEntry existingEntry && entry.Vfs.ReadOrder < existingEntry.Vfs.ReadOrder)
{
continue;
}
loaded[key] = asset;
}
_applicationView.Status.UpdateStatusLabel($"{loaded.Count:### ### ###} Packages");
_applicationView.CUE4Parse.AssetsFolder.BulkPopulate(loaded.Values);
entries.Add(entry);
_applicationView.Status.UpdateStatusLabel(entry.Vfs.Name);
}
}

View File

@ -29,10 +29,9 @@ public class MenuCommand : ViewModelCommand<ApplicationViewModel>
Helper.OpenWindow<AdonisWindow>("AES Manager", () => new AesManager().Show());
break;
case "Directory_Backup":
Helper.OpenWindow<AdonisWindow>("Backup Manager", () => new BackupManager(contextViewModel.CUE4Parse.Provider.ProjectName).Show());
Helper.OpenWindow<AdonisWindow>("Backup Manager", () => new BackupManager(contextViewModel.CUE4Parse.Provider.InternalGameName).Show());
break;
case "Directory_ArchivesInfo":
ApplicationService.ApplicationView.IsAssetsExplorerVisible = false;
contextViewModel.CUE4Parse.TabControl.AddTab("Archives Info");
contextViewModel.CUE4Parse.TabControl.SelectedTab.Highlighter = AvalonExtensions.HighlighterSelector("json");
contextViewModel.CUE4Parse.TabControl.SelectedTab.SetDocumentText(JsonConvert.SerializeObject(contextViewModel.CUE4Parse.GameDirectory.DirectoryFiles, Formatting.Indented), false, false);

View File

@ -1,11 +1,8 @@
using System.Collections;
using System.Collections;
using System.Linq;
using System.Threading;
using CUE4Parse.FileProvider.Objects;
using FModel.Framework;
using FModel.Services;
using FModel.Settings;
using FModel.Views.Resources.Controls;
namespace FModel.ViewModels.Commands;
@ -22,192 +19,62 @@ public class RightClickMenuCommand : ViewModelCommand<ApplicationViewModel>
if (parameter is not object[] parameters || parameters[0] is not string trigger)
return;
var param = (parameters[1] as IEnumerable)?.OfType<object>().ToArray() ?? [];
if (param.Length == 0) return;
var assetItems = ((IList) parameters[1]).Cast<AssetItem>().ToArray();
if (!assetItems.Any()) return;
var folders = param.OfType<TreeItem>().ToArray();
var assets = param.SelectMany(item => item switch
{
GameFile gf => new[] { gf }, // search view passes GameFile directly
GameFileViewModel gvm => new[] { gvm.Asset },
_ => []
}).ToArray();
if (folders.Length == 0 && assets.Length == 0)
return;
var updateUi = assets.Length > 1 ? EBulkType.Auto : EBulkType.None;
var updateUi = assetItems.Length > 1 ? EBulkType.Auto : EBulkType.None;
await _threadWorkerView.Begin(cancellationToken =>
{
switch (trigger)
{
#region Asset Commands
case "Assets_Extract_New_Tab":
foreach (var entry in assets)
foreach (var asset in assetItems)
{
Thread.Yield();
cancellationToken.ThrowIfCancellationRequested();
contextViewModel.CUE4Parse.Extract(cancellationToken, entry, true);
}
break;
case "Assets_Show_Metadata":
foreach (var entry in assets)
{
Thread.Yield();
cancellationToken.ThrowIfCancellationRequested();
contextViewModel.CUE4Parse.ShowMetadata(entry);
}
break;
case "Assets_Show_References":
{
Thread.Yield();
cancellationToken.ThrowIfCancellationRequested();
contextViewModel.CUE4Parse.FindReferences(assets.FirstOrDefault());
}
break;
case "Assets_Decompile":
foreach (var entry in assets)
{
Thread.Yield();
cancellationToken.ThrowIfCancellationRequested();
contextViewModel.CUE4Parse.Decompile(entry);
contextViewModel.CUE4Parse.Extract(cancellationToken, asset.FullPath, true);
}
break;
case "Assets_Export_Data":
foreach (var entry in assets)
foreach (var asset in assetItems)
{
Thread.Yield();
cancellationToken.ThrowIfCancellationRequested();
contextViewModel.CUE4Parse.ExportData(entry);
contextViewModel.CUE4Parse.ExportData(asset.FullPath);
}
break;
case "Assets_Save_Properties":
foreach (var entry in assets)
foreach (var asset in assetItems)
{
Thread.Yield();
cancellationToken.ThrowIfCancellationRequested();
contextViewModel.CUE4Parse.Extract(cancellationToken, entry, false, EBulkType.Properties | updateUi);
contextViewModel.CUE4Parse.Extract(cancellationToken, asset.FullPath, false, EBulkType.Properties | updateUi);
}
break;
case "Assets_Save_Textures":
foreach (var entry in assets)
foreach (var asset in assetItems)
{
Thread.Yield();
cancellationToken.ThrowIfCancellationRequested();
contextViewModel.CUE4Parse.Extract(cancellationToken, entry, false, EBulkType.Textures | updateUi);
contextViewModel.CUE4Parse.Extract(cancellationToken, asset.FullPath, false, EBulkType.Textures | updateUi);
}
break;
case "Assets_Save_Models":
foreach (var entry in assets)
foreach (var asset in assetItems)
{
Thread.Yield();
cancellationToken.ThrowIfCancellationRequested();
contextViewModel.CUE4Parse.Extract(cancellationToken, entry, false, EBulkType.Meshes | updateUi);
contextViewModel.CUE4Parse.Extract(cancellationToken, asset.FullPath, false, EBulkType.Meshes | updateUi);
}
break;
case "Assets_Save_Animations":
foreach (var entry in assets)
foreach (var asset in assetItems)
{
Thread.Yield();
cancellationToken.ThrowIfCancellationRequested();
contextViewModel.CUE4Parse.Extract(cancellationToken, entry, false, EBulkType.Animations | updateUi);
contextViewModel.CUE4Parse.Extract(cancellationToken, asset.FullPath, false, EBulkType.Animations | updateUi);
}
break;
case "Assets_Save_Audio":
foreach (var entry in assets)
{
Thread.Yield();
cancellationToken.ThrowIfCancellationRequested();
contextViewModel.CUE4Parse.Extract(cancellationToken, entry, false, EBulkType.Audio | updateUi);
}
break;
#endregion
#region Folder Commands
case "Folders_Export_Data":
foreach (var folder in folders)
{
cancellationToken.ThrowIfCancellationRequested();
contextViewModel.CUE4Parse.ExportFolder(cancellationToken, folder);
FLogger.Append(ELog.Information, () =>
{
FLogger.Text("Successfully exported ", Constants.WHITE);
FLogger.Link(folder.PathAtThisPoint, UserSettings.Default.RawDataDirectory, true);
});
}
break;
case "Folders_Save_Properties":
foreach (var folder in folders)
{
Thread.Yield();
cancellationToken.ThrowIfCancellationRequested();
contextViewModel.CUE4Parse.SaveFolder(cancellationToken, folder);
FLogger.Append(ELog.Information, () =>
{
FLogger.Text("Successfully saved ", Constants.WHITE);
FLogger.Link(folder.PathAtThisPoint, UserSettings.Default.PropertiesDirectory, true);
});
}
break;
case "Folders_Save_Textures":
foreach (var folder in folders)
{
Thread.Yield();
cancellationToken.ThrowIfCancellationRequested();
contextViewModel.CUE4Parse.TextureFolder(cancellationToken, folder);
FLogger.Append(ELog.Information, () =>
{
FLogger.Text("Successfully saved textures from ", Constants.WHITE);
FLogger.Link(folder.PathAtThisPoint, UserSettings.Default.TextureDirectory, true);
});
}
break;
case "Folders_Save_Models":
foreach (var folder in folders)
{
Thread.Yield();
cancellationToken.ThrowIfCancellationRequested();
contextViewModel.CUE4Parse.ModelFolder(cancellationToken, folder);
FLogger.Append(ELog.Information, () =>
{
FLogger.Text("Successfully saved models from ", Constants.WHITE);
FLogger.Link(folder.PathAtThisPoint, UserSettings.Default.ModelDirectory, true);
});
}
break;
case "Folders_Save_Animations":
foreach (var folder in folders)
{
Thread.Yield();
cancellationToken.ThrowIfCancellationRequested();
contextViewModel.CUE4Parse.AnimationFolder(cancellationToken, folder);
FLogger.Append(ELog.Information, () =>
{
FLogger.Text("Successfully saved animations from ", Constants.WHITE);
FLogger.Link(folder.PathAtThisPoint, UserSettings.Default.ModelDirectory, true);
});
}
break;
case "Folders_Save_Audio":
foreach (var folder in folders)
{
Thread.Yield();
cancellationToken.ThrowIfCancellationRequested();
contextViewModel.CUE4Parse.AudioFolder(cancellationToken, folder);
FLogger.Append(ELog.Information, () =>
{
FLogger.Text("Successfully saved audio from ", Constants.WHITE);
FLogger.Link(folder.PathAtThisPoint, UserSettings.Default.AudioDirectory, true);
});
}
break;
#endregion
}
});
}

View File

@ -1,4 +1,4 @@
using System.Windows;
using System.Windows;
using AdonisUI.Controls;
using FModel.Framework;
using FModel.Services;
@ -15,7 +15,7 @@ public class TabCommand : ViewModelCommand<TabItem>
{
}
public override async void Execute(TabItem tabViewModel, object parameter)
public override async void Execute(TabItem contextViewModel, object parameter)
{
switch (parameter)
{
@ -23,62 +23,53 @@ public class TabCommand : ViewModelCommand<TabItem>
_applicationView.CUE4Parse.TabControl.RemoveTab(mdlClick);
break;
case "Close_Tab":
_applicationView.CUE4Parse.TabControl.RemoveTab(tabViewModel);
_applicationView.CUE4Parse.TabControl.RemoveTab(contextViewModel);
break;
case "Close_All_Tabs":
_applicationView.CUE4Parse.TabControl.RemoveAllTabs();
break;
case "Close_Other_Tabs":
_applicationView.CUE4Parse.TabControl.RemoveOtherTabs(tabViewModel);
break;
case "Find_References":
_applicationView.CUE4Parse.FindReferences(tabViewModel.Entry);
_applicationView.CUE4Parse.TabControl.RemoveOtherTabs(contextViewModel);
break;
case "Asset_Export_Data":
await _threadWorkerView.Begin(_ => _applicationView.CUE4Parse.ExportData(tabViewModel.Entry));
await _threadWorkerView.Begin(_ => _applicationView.CUE4Parse.ExportData(contextViewModel.FullPath));
break;
case "Asset_Save_Properties":
await _threadWorkerView.Begin(cancellationToken =>
{
_applicationView.CUE4Parse.Extract(cancellationToken, tabViewModel.Entry, false, EBulkType.Properties);
_applicationView.CUE4Parse.Extract(cancellationToken, contextViewModel.FullPath, false, EBulkType.Properties);
});
break;
case "Asset_Save_Textures":
await _threadWorkerView.Begin(cancellationToken =>
{
_applicationView.CUE4Parse.Extract(cancellationToken, tabViewModel.Entry, false, EBulkType.Textures);
_applicationView.CUE4Parse.Extract(cancellationToken, contextViewModel.FullPath, false, EBulkType.Textures);
});
break;
case "Asset_Save_Models":
await _threadWorkerView.Begin(cancellationToken =>
{
_applicationView.CUE4Parse.Extract(cancellationToken, tabViewModel.Entry, false, EBulkType.Meshes);
_applicationView.CUE4Parse.Extract(cancellationToken, contextViewModel.FullPath, false, EBulkType.Meshes | EBulkType.Auto);
});
break;
case "Asset_Save_Animations":
await _threadWorkerView.Begin(cancellationToken =>
{
_applicationView.CUE4Parse.Extract(cancellationToken, tabViewModel.Entry, false, EBulkType.Animations);
});
break;
case "Asset_Save_Audio":
await _threadWorkerView.Begin(cancellationToken =>
{
_applicationView.CUE4Parse.Extract(cancellationToken, tabViewModel.Entry, false, EBulkType.Audio);
_applicationView.CUE4Parse.Extract(cancellationToken, contextViewModel.FullPath, false, EBulkType.Animations | EBulkType.Auto);
});
break;
case "Open_Properties":
if (tabViewModel.Header == "New Tab" || tabViewModel.Document == null) return;
Helper.OpenWindow<AdonisWindow>(tabViewModel.Header + " (Properties)", () =>
if (contextViewModel.Header == "New Tab" || contextViewModel.Document == null) return;
Helper.OpenWindow<AdonisWindow>(contextViewModel.Header + " (Properties)", () =>
{
new PropertiesPopout(tabViewModel)
new PropertiesPopout(contextViewModel)
{
Title = tabViewModel.Header + " (Properties)"
Title = contextViewModel.Header + " (Properties)"
}.Show();
});
break;
case "Copy_Asset_Path":
Clipboard.SetText(tabViewModel.Entry.Path);
Clipboard.SetText(contextViewModel.FullPath);
break;
}
}

View File

@ -5,7 +5,6 @@ using System.Linq;
using System.Text.RegularExpressions;
using System.Windows;
using System.Windows.Data;
using CUE4Parse.Compression;
using CUE4Parse.UE4.IO;
using CUE4Parse.UE4.Objects.Core.Misc;
using CUE4Parse.UE4.VirtualFileSystem;
@ -70,13 +69,6 @@ public class FileItem : ViewModel
set => SetProperty(ref _guid, value);
}
private CompressionMethod[] _compressionMethods;
public CompressionMethod[] CompressionMethods
{
get => _compressionMethods;
set => SetProperty(ref _compressionMethods, value);
}
public FileItem(string name, long length)
{
Name = name;
@ -92,7 +84,6 @@ public class FileItem : ViewModel
IsEnabled = false;
Key = string.Empty;
FileCount = reader is IoStoreReader storeReader ? (int) storeReader.TocResource.Header.TocEntryCount - 1 : 0;
CompressionMethods = reader.CompressionMethods;
}
public override string ToString()

View File

@ -1,407 +0,0 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using CUE4Parse.FileProvider.Objects;
using CUE4Parse.GameTypes.FN.Assets.Exports.DataAssets;
using CUE4Parse.UE4.Assets;
using CUE4Parse.UE4.Assets.Exports.Animation;
using CUE4Parse.UE4.Assets.Exports.BuildData;
using CUE4Parse.UE4.Assets.Exports.Component;
using CUE4Parse.UE4.Assets.Exports.CriWare;
using CUE4Parse.UE4.Assets.Exports.Engine;
using CUE4Parse.UE4.Assets.Exports.Engine.Font;
using CUE4Parse.UE4.Assets.Exports.Fmod;
using CUE4Parse.UE4.Assets.Exports.Foliage;
using CUE4Parse.UE4.Assets.Exports.Internationalization;
using CUE4Parse.UE4.Assets.Exports.LevelSequence;
using CUE4Parse.UE4.Assets.Exports.Material;
using CUE4Parse.UE4.Assets.Exports.Material.Editor;
using CUE4Parse.UE4.Assets.Exports.Niagara;
using CUE4Parse.UE4.Assets.Exports.SkeletalMesh;
using CUE4Parse.UE4.Assets.Exports.Sound;
using CUE4Parse.UE4.Assets.Exports.StaticMesh;
using CUE4Parse.UE4.Assets.Exports.Texture;
using CUE4Parse.UE4.Assets.Exports.Wwise;
using CUE4Parse.UE4.Assets.Objects;
using CUE4Parse.UE4.Objects.Engine;
using CUE4Parse.UE4.Objects.Engine.Animation;
using CUE4Parse.UE4.Objects.Engine.Curves;
using CUE4Parse.UE4.Objects.MediaAssets;
using CUE4Parse.UE4.Objects.Niagara;
using CUE4Parse.UE4.Objects.PhysicsEngine;
using CUE4Parse.UE4.Objects.RigVM;
using CUE4Parse.UE4.Objects.UObject;
using CUE4Parse.UE4.Objects.UObject.Editor;
using CUE4Parse.Utils;
using CUE4Parse_Conversion.Textures;
using FModel.Framework;
using FModel.Services;
using FModel.Settings;
using Serilog;
using SkiaSharp;
using Svg.Skia;
namespace FModel.ViewModels;
public class GameFileViewModel(GameFile asset) : ViewModel
{
private const int MaxPreviewSize = 128;
private ApplicationViewModel _applicationView => ApplicationService.ApplicationView;
public EResolveCompute Resolved { get; private set; } = EResolveCompute.None;
public GameFile Asset { get; } = asset;
private string _resolvedAssetType = asset.Extension;
public string ResolvedAssetType
{
get => _resolvedAssetType;
private set => SetProperty(ref _resolvedAssetType, value);
}
private bool _isSelected;
public bool IsSelected
{
get => _isSelected;
set => SetProperty(ref _isSelected, value);
}
private EAssetCategory _assetCategory = EAssetCategory.All;
public EAssetCategory AssetCategory
{
get => _assetCategory;
private set
{
SetProperty(ref _assetCategory, value);
Resolved |= EResolveCompute.Category; // blindly assume category is resolved when set, even if unchanged
}
}
private ImageSource _previewImage;
public ImageSource PreviewImage
{
get => _previewImage;
private set
{
if (SetProperty(ref _previewImage, value))
{
Resolved |= EResolveCompute.Preview;
}
}
}
public Task ExtractAsync()
=> ApplicationService.ThreadWorkerView.Begin(cancellationToken =>
_applicationView.CUE4Parse.ExtractSelected(cancellationToken, [Asset]));
public Task ResolveAsync(EResolveCompute resolve)
{
try
{
return ResolveInternalAsync(resolve);
}
catch (Exception e)
{
Log.Error(e, "Failed to resolve asset {AssetName} ({Resolver})", Asset.Path, resolve.ToStringBitfield());
Resolved = EResolveCompute.All;
return Task.CompletedTask;
}
}
private Task ResolveInternalAsync(EResolveCompute resolve)
{
if (!_applicationView.IsAssetsExplorerVisible || !UserSettings.Default.PreviewTexturesAssetExplorer)
{
resolve &= ~EResolveCompute.Preview;
}
resolve &= ~Resolved;
if (resolve == EResolveCompute.None)
return Task.CompletedTask;
if (!Asset.IsUePackage || _applicationView.CUE4Parse is null)
return ResolveByExtensionAsync(resolve);
return ResolveByPackageAsync(resolve);
}
private Task ResolveByPackageAsync(EResolveCompute resolve)
{
if (Asset.Extension is "umap")
{
AssetCategory = EAssetCategory.World;
ResolvedAssetType = "World";
Resolved |= EResolveCompute.Preview;
return Task.CompletedTask;
}
if (Asset.NameWithoutExtension.EndsWith("_BuiltData"))
{
AssetCategory = EAssetCategory.BuildData;
ResolvedAssetType = "MapBuildDataRegistry";
Resolved |= EResolveCompute.Preview;
return Task.CompletedTask;
}
return Task.Run(() =>
{
// TODO: cache and reuse packages
var pkg = _applicationView.CUE4Parse?.Provider.LoadPackage(Asset);
if (pkg is null)
throw new InvalidOperationException($"Failed to load {Asset.Path} as UE package.");
var mainIndex = pkg.GetExportIndex(Asset.NameWithoutExtension, StringComparison.OrdinalIgnoreCase);
if (mainIndex < 0) mainIndex = pkg.GetExportIndex($"{Asset.NameWithoutExtension}_C", StringComparison.OrdinalIgnoreCase);
if (mainIndex < 0) mainIndex = 0;
var pointer = new FPackageIndex(pkg, mainIndex + 1).ResolvedObject;
if (pointer?.Object is null)
return;
var dummy = ((AbstractUePackage) pkg).ConstructObject(pointer.Class?.Object?.Value as UStruct, pkg);
ResolvedAssetType = dummy.ExportType;
AssetCategory = dummy switch
{
URigVMBlueprintGeneratedClass => EAssetCategory.RigVMBlueprintGeneratedClass,
UAnimBlueprintGeneratedClass => EAssetCategory.AnimBlueprintGeneratedClass,
UWidgetBlueprintGeneratedClass => EAssetCategory.WidgetBlueprintGeneratedClass,
UBlueprintGeneratedClass or UFunction => EAssetCategory.BlueprintGeneratedClass,
UUserDefinedEnum => EAssetCategory.UserDefinedEnum,
UUserDefinedStruct => EAssetCategory.UserDefinedStruct,
UBlueprintCore => EAssetCategory.Blueprint,
UClassCookedMetaData or UStructCookedMetaData or UEnumCookedMetaData => EAssetCategory.CookedMetaData,
UStaticMesh => EAssetCategory.StaticMesh,
USkeletalMesh => EAssetCategory.SkeletalMesh,
UPhysicsAsset => EAssetCategory.PhysicsAsset,
UTexture => EAssetCategory.Texture,
UMaterialInterface => EAssetCategory.Material,
UMaterialInterfaceEditorOnlyData => EAssetCategory.MaterialEditorData,
UMaterialFunction => EAssetCategory.MaterialFunction,
UMaterialParameterCollection => EAssetCategory.MaterialParameterCollection,
UAnimationAsset => EAssetCategory.Animation,
USkeleton => EAssetCategory.Skeleton,
UWorld => EAssetCategory.World,
UMapBuildDataRegistry => EAssetCategory.BuildData,
ULevelSequence => EAssetCategory.LevelSequence,
UFoliageType => EAssetCategory.Foliage,
UItemDefinitionBase => EAssetCategory.ItemDefinitionBase,
UDataAsset or UDataTable or UCurveTable or UStringTable => EAssetCategory.Data,
UCurveBase => EAssetCategory.CurveBase,
UWwiseAssetLibrary or USoundBase or UAkMediaAssetData or UAtomWaveBank or USoundAtomCue
or UAtomCueSheet or USoundAtomCueSheet or UFMODBank or UFMODEvent or UAkAudioType => EAssetCategory.Audio,
UFileMediaSource => EAssetCategory.Video,
UFont or UFontFace => EAssetCategory.Font,
UNiagaraSystem or UNiagaraScriptBase or UParticleSystem => EAssetCategory.Particle,
_ => EAssetCategory.All
};
switch (AssetCategory)
{
case EAssetCategory.Texture when pointer.Object.Value is UTexture texture:
{
if (!resolve.HasFlag(EResolveCompute.Preview))
break;
var img = texture.Decode(MaxPreviewSize, UserSettings.Default.CurrentDir.TexturePlatform);
if (img != null)
{
using var bitmap = img.ToSkBitmap();
using var image = bitmap.Encode(SKEncodedImageFormat.Png, 100);
SetPreviewImage(image);
}
break;
}
case EAssetCategory.ItemDefinitionBase:
if (!resolve.HasFlag(EResolveCompute.Preview))
break;
if (pointer.Object.Value is UItemDefinitionBase itemDef)
{
if (LookupPreview(itemDef.DataList)) break;
if (itemDef is UAthenaPickaxeItemDefinition pickaxe && pickaxe.WeaponDefinition.TryLoad(out UItemDefinitionBase weaponDef))
{
LookupPreview(weaponDef.DataList);
}
bool LookupPreview(FInstancedStruct[] dataList)
{
foreach (var data in dataList)
{
if (!data.NonConstStruct.TryGetValue(out FSoftObjectPath icon, "Icon", "LargeIcon") ||
!icon.TryLoad<UTexture2D>(out var texture))
continue;
var img = texture.Decode(MaxPreviewSize, UserSettings.Default.CurrentDir.TexturePlatform);
if (img == null) return false;
using var bitmap = img.ToSkBitmap();
using var image = bitmap.Encode(SKEncodedImageFormat.Png, 100);
SetPreviewImage(image);
return true;
}
return false;
}
}
break;
default:
Resolved |= EResolveCompute.Preview;
break;
}
});
}
private Task ResolveByExtensionAsync(EResolveCompute resolve)
{
Resolved |= EResolveCompute.Preview;
switch (Asset.Extension)
{
case "uproject":
case "uefnproject":
case "upluginmanifest":
case "uplugin":
case "ini":
case "locmeta":
case "locres":
case "verse":
case "lua":
case "luac":
case "json5":
case "json":
case "bin":
case "txt":
case "log":
case "pem":
case "xml":
AssetCategory = EAssetCategory.Data;
break;
case "wav":
case "bank":
case "bnk":
case "pck":
case "awb":
case "acb":
case "xvag":
case "flac":
case "at9":
case "wem":
case "ogg":
AssetCategory = EAssetCategory.Audio;
break;
case "ufont":
case "otf":
case "ttf":
AssetCategory = EAssetCategory.Font;
break;
case "mp4":
AssetCategory = EAssetCategory.Video;
break;
case "jpg":
case "png":
case "bmp":
case "svg":
{
Resolved |= ~EResolveCompute.Preview;
AssetCategory = EAssetCategory.Texture;
if (!resolve.HasFlag(EResolveCompute.Preview))
break;
return Task.Run(() =>
{
var data = _applicationView.CUE4Parse.Provider.SaveAsset(Asset);
using var stream = new MemoryStream(data);
stream.Position = 0;
SKBitmap bitmap;
if (Asset.Extension == "svg")
{
var svg = new SKSvg();
svg.Load(stream);
if (svg.Picture == null)
return;
bitmap = new SKBitmap(MaxPreviewSize, MaxPreviewSize);
using var canvas = new SKCanvas(bitmap);
canvas.Clear(SKColors.Transparent);
var bounds = svg.Picture.CullRect;
float scale = Math.Min(MaxPreviewSize / bounds.Width, MaxPreviewSize / bounds.Height);
canvas.Scale(scale);
canvas.Translate(-bounds.Left, -bounds.Top);
canvas.DrawPicture(svg.Picture);
}
else
{
bitmap = SKBitmap.Decode(stream);
}
using var image = bitmap.Encode(Asset.Extension == "jpg" ? SKEncodedImageFormat.Jpeg : SKEncodedImageFormat.Png, 100);
SetPreviewImage(image);
bitmap.Dispose();
});
}
default:
AssetCategory = EAssetCategory.All; // just so it sets resolved
break;
}
return Task.CompletedTask;
}
private void SetPreviewImage(SKData data)
{
using var ms = new MemoryStream(data.ToArray());
ms.Position = 0;
var bitmap = new BitmapImage();
bitmap.BeginInit();
bitmap.CacheOption = BitmapCacheOption.OnLoad;
bitmap.StreamSource = ms;
bitmap.EndInit();
bitmap.Freeze();
Application.Current.Dispatcher.InvokeAsync(() => PreviewImage = bitmap);
}
private CancellationTokenSource _previewCts;
public void OnIsVisible()
{
if (Resolved == EResolveCompute.All)
return;
_previewCts?.Cancel();
_previewCts = new CancellationTokenSource();
var token = _previewCts.Token;
Task.Delay(100, token).ContinueWith(t =>
{
if (t.IsCanceled) return;
ResolveAsync(EResolveCompute.All);
}, TaskScheduler.FromCurrentSynchronizationContext());
}
}
[Flags]
public enum EResolveCompute
{
None = 0,
Category = 1 << 0,
Preview = 1 << 1,
All = Category | Preview
}

View File

@ -1,3 +1,4 @@
using FModel.Extensions;
using FModel.Framework;
using Newtonsoft.Json;
using Serilog;
@ -9,7 +10,6 @@ using System.Linq;
using System.Text.RegularExpressions;
using CUE4Parse.UE4.Objects.Core.Serialization;
using CUE4Parse.UE4.Versions;
using CUE4Parse.Utils;
using FModel.Settings;
using FModel.ViewModels.ApiEndpoints.Models;
using Microsoft.Win32;
@ -84,11 +84,11 @@ public class GameSelectorViewModel : ViewModel
=> Enum.GetValues<EGame>()
.GroupBy(value => (int)value)
.Select(group => group.First())
.OrderBy(value => ((int)value & 0xFF) == 0);
.OrderBy(value => (int)value == ((int)value & ~0xF));
private IEnumerable<DirectorySettings> EnumerateDetectedGames()
{
yield return GetUnrealEngineGame("Fortnite", "\\FortniteGame\\Content\\Paks", EGame.GAME_UE5_7);
yield return DirectorySettings.Default("Fortnite [LIVE]", Constants._FN_LIVE_TRIGGER, ue: EGame.GAME_UE5_7);
yield return GetUnrealEngineGame("Fortnite", "\\FortniteGame\\Content\\Paks", EGame.GAME_UE5_5);
yield return DirectorySettings.Default("Fortnite [LIVE]", Constants._FN_LIVE_TRIGGER, ue: EGame.GAME_UE5_5);
yield return GetUnrealEngineGame("Pewee", "\\RogueCompany\\Content\\Paks", EGame.GAME_RogueCompany);
yield return GetUnrealEngineGame("Rosemallow", "\\Indiana\\Content\\Paks", EGame.GAME_UE4_21);
yield return GetUnrealEngineGame("Catnip", "\\OakGame\\Content\\Paks", EGame.GAME_Borderlands3);
@ -99,7 +99,7 @@ public class GameSelectorViewModel : ViewModel
yield return GetUnrealEngineGame("9361c8c6d2f34b42b5f2f61093eedf48", "\\TslGame\\Content\\Paks", EGame.GAME_PlayerUnknownsBattlegrounds);
yield return GetRiotGame("VALORANT", "ShooterGame\\Content\\Paks", EGame.GAME_Valorant);
yield return DirectorySettings.Default("VALORANT [LIVE]", Constants._VAL_LIVE_TRIGGER, ue: EGame.GAME_Valorant);
yield return GetSteamGame(381210, "\\DeadByDaylight\\Content\\Paks", EGame.GAME_DeadByDaylight, aesKey: "0x22b1639b548124925cf7b9cbaa09f9ac295fcf0324586d6b37ee1d42670b39b3"); // Dead By Daylight
yield return GetSteamGame(381210, "\\DeadByDaylight\\Content\\Paks", EGame.GAME_UE4_27); // Dead By Daylight
yield return GetSteamGame(578080, "\\TslGame\\Content\\Paks", EGame.GAME_PlayerUnknownsBattlegrounds); // PUBG
yield return GetSteamGame(1172380, "\\SwGame\\Content\\Paks", EGame.GAME_StarWarsJediFallenOrder); // STAR WARS Jedi: Fallen Order™
yield return GetSteamGame(677620, "\\PortalWars\\Content\\Paks", EGame.GAME_Splitgate); // Splitgate
@ -151,13 +151,13 @@ public class GameSelectorViewModel : ViewModel
return null;
}
private DirectorySettings GetSteamGame(int id, string pakDirectory, EGame ueVersion, string aesKey = "")
private DirectorySettings GetSteamGame(int id, string pakDirectory, EGame ueVersion)
{
var steamInfo = SteamDetection.GetSteamGameById(id);
if (steamInfo is not null)
{
Log.Debug("Found {GameName} in steam manifests", steamInfo.Name);
return DirectorySettings.Default(steamInfo.Name, $"{steamInfo.GameRoot}{pakDirectory}", ue: ueVersion, aes: aesKey);
return DirectorySettings.Default(steamInfo.Name, $"{steamInfo.GameRoot}{pakDirectory}", ue: ueVersion);
}
return null;

View File

@ -1,25 +1,16 @@
using System;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Windows.Data;
using CUE4Parse.FileProvider.Objects;
using CUE4Parse.UE4.VirtualFileSystem;
using FModel.Framework;
namespace FModel.ViewModels;
public class SearchViewModel : ViewModel
{
public enum ESortSizeMode
{
None,
Ascending,
Descending
}
private string _filterText = string.Empty;
private string _filterText;
public string FilterText
{
get => _filterText;
@ -40,106 +31,34 @@ public class SearchViewModel : ViewModel
set => SetProperty(ref _hasMatchCaseEnabled, value);
}
private ESortSizeMode _currentSortSizeMode = ESortSizeMode.None;
public ESortSizeMode CurrentSortSizeMode
{
get => _currentSortSizeMode;
set => SetProperty(ref _currentSortSizeMode, value);
}
private int _resultsCount = 0;
public int ResultsCount
{
get => _resultsCount;
private set => SetProperty(ref _resultsCount, value);
}
private GameFile _refFile;
public GameFile RefFile
{
get => _refFile;
private set => SetProperty(ref _refFile, value);
}
public RangeObservableCollection<GameFile> SearchResults { get; }
public ListCollectionView SearchResultsView { get; }
public int ResultsCount => SearchResults?.Count ?? 0;
public RangeObservableCollection<AssetItem> SearchResults { get; }
public ICollectionView SearchResultsView { get; }
public SearchViewModel()
{
SearchResults = [];
SearchResultsView = new ListCollectionView(SearchResults)
{
Filter = e => ItemFilter(e, FilterText.Trim().Split(' ', StringSplitOptions.RemoveEmptyEntries)),
};
ResultsCount = SearchResultsView.Count;
SearchResults = new RangeObservableCollection<AssetItem>();
SearchResultsView = new ListCollectionView(SearchResults);
}
public void RefreshFilter()
{
SearchResultsView.Refresh();
ResultsCount = SearchResultsView.Count;
}
public void ChangeCollection(IEnumerable<GameFile> files, GameFile refFile = null)
{
SearchResults.Clear();
SearchResults.AddRange(files);
RefFile = refFile;
ResultsCount = SearchResultsView.Count;
}
public async Task CycleSortSizeMode()
{
CurrentSortSizeMode = CurrentSortSizeMode switch
{
ESortSizeMode.None => ESortSizeMode.Descending,
ESortSizeMode.Descending => ESortSizeMode.Ascending,
_ => ESortSizeMode.None
};
var sorted = await Task.Run(() =>
{
var archiveDict = SearchResults
.OfType<VfsEntry>()
.Select(f => f.Vfs.Name)
.Distinct()
.Select((name, idx) => (name, idx))
.ToDictionary(x => x.name, x => x.idx);
var keyed = SearchResults.Select(f =>
{
int archiveKey = f is VfsEntry ve && archiveDict.TryGetValue(ve.Vfs.Name, out var key) ? key : -1;
return (File: f, f.Size, ArchiveKey: archiveKey);
});
return CurrentSortSizeMode switch
{
ESortSizeMode.Ascending => keyed
.OrderBy(x => x.Size).ThenBy(x => x.ArchiveKey)
.Select(x => x.File).ToList(),
ESortSizeMode.Descending => keyed
.OrderByDescending(x => x.Size).ThenBy(x => x.ArchiveKey)
.Select(x => x.File).ToList(),
_ => keyed
.OrderBy(x => x.ArchiveKey).ThenBy(x => x.File.Path, StringComparer.OrdinalIgnoreCase)
.Select(x => x.File).ToList()
};
});
SearchResults.Clear();
SearchResults.AddRange(sorted);
if (SearchResultsView.Filter == null)
SearchResultsView.Filter = e => ItemFilter(e, FilterText.Trim().Split(' '));
else
SearchResultsView.Refresh();
}
private bool ItemFilter(object item, IEnumerable<string> filters)
{
if (item is not GameFile entry)
if (item is not AssetItem assetItem)
return true;
if (!HasRegexEnabled)
return filters.All(x => entry.Path.Contains(x, HasMatchCaseEnabled ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase));
return filters.All(x => assetItem.FullPath.Contains(x, HasMatchCaseEnabled ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase));
var o = RegexOptions.None;
if (!HasMatchCaseEnabled) o |= RegexOptions.IgnoreCase;
return new Regex(FilterText, o).Match(entry.Path).Success;
if (HasMatchCaseEnabled) o |= RegexOptions.IgnoreCase;
return new Regex(FilterText, o).Match(assetItem.FullPath).Success;
}
}
}

View File

@ -2,14 +2,13 @@ using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using CUE4Parse.UE4.Assets.Exports.Material;
using CUE4Parse.UE4.Assets.Exports.Nanite;
using CUE4Parse.UE4.Assets.Exports.Texture;
using CUE4Parse.UE4.Objects.Core.Serialization;
using CUE4Parse.UE4.Versions;
using CUE4Parse_Conversion.Meshes;
using CUE4Parse_Conversion.Textures;
using CUE4Parse_Conversion.UEFormat.Enums;
using CUE4Parse.UE4.Assets.Exports.Material;
using FModel.Framework;
using FModel.Services;
using FModel.Settings;
@ -144,13 +143,6 @@ public class SettingsViewModel : ViewModel
set => SetProperty(ref _selectedLodExportFormat, value);
}
private ENaniteMeshFormat _selectedNaniteMeshExportFormat;
public ENaniteMeshFormat SelectedNaniteMeshExportFormat
{
get => _selectedNaniteMeshExportFormat;
set => SetProperty(ref _selectedNaniteMeshExportFormat, value);
}
private EMaterialFormat _selectedMaterialExportFormat;
public EMaterialFormat SelectedMaterialExportFormat
{
@ -165,13 +157,6 @@ public class SettingsViewModel : ViewModel
set => SetProperty(ref _selectedTextureExportFormat, value);
}
private ulong _criwareDecryptionKey;
public ulong CriwareDecryptionKey
{
get => _criwareDecryptionKey;
set => SetProperty(ref _criwareDecryptionKey, value);
}
public bool SocketSettingsEnabled => SelectedMeshExportFormat == EMeshFormat.ActorX;
public bool CompressionSettingsEnabled => SelectedMeshExportFormat == EMeshFormat.UEFormat;
@ -185,7 +170,6 @@ public class SettingsViewModel : ViewModel
public ReadOnlyObservableCollection<ESocketFormat> SocketExportFormats { get; private set; }
public ReadOnlyObservableCollection<EFileCompressionFormat> CompressionFormats { get; private set; }
public ReadOnlyObservableCollection<ELodFormat> LodExportFormats { get; private set; }
public ReadOnlyObservableCollection<ENaniteMeshFormat> NaniteMeshExportFormats { get; private set; }
public ReadOnlyObservableCollection<EMaterialFormat> MaterialExportFormats { get; private set; }
public ReadOnlyObservableCollection<ETextureFormat> TextureExportFormats { get; private set; }
public ReadOnlyObservableCollection<ETexturePlatform> Platforms { get; private set; }
@ -209,7 +193,6 @@ public class SettingsViewModel : ViewModel
private ESocketFormat _socketExportFormatSnapshot;
private EFileCompressionFormat _compressionFormatSnapshot;
private ELodFormat _lodExportFormatSnapshot;
private ENaniteMeshFormat _naniteMeshExportFormatSnapshot;
private EMaterialFormat _materialExportFormatSnapshot;
private ETextureFormat _textureExportFormatSnapshot;
@ -234,7 +217,6 @@ public class SettingsViewModel : ViewModel
_customVersionsSnapshot = UserSettings.Default.CurrentDir.Versioning.CustomVersions;
_optionsSnapshot = UserSettings.Default.CurrentDir.Versioning.Options;
_mapStructTypesSnapshot = UserSettings.Default.CurrentDir.Versioning.MapStructTypes;
_criwareDecryptionKey = UserSettings.Default.CurrentDir.CriwareDecryptionKey;
AesEndpoint = UserSettings.Default.CurrentDir.Endpoints[0];
MappingEndpoint = UserSettings.Default.CurrentDir.Endpoints[1];
@ -251,7 +233,6 @@ public class SettingsViewModel : ViewModel
_socketExportFormatSnapshot = UserSettings.Default.SocketExportFormat;
_compressionFormatSnapshot = UserSettings.Default.CompressionFormat;
_lodExportFormatSnapshot = UserSettings.Default.LodExportFormat;
_naniteMeshExportFormatSnapshot = UserSettings.Default.NaniteMeshExportFormat;
_materialExportFormatSnapshot = UserSettings.Default.MaterialExportFormat;
_textureExportFormatSnapshot = UserSettings.Default.TextureExportFormat;
@ -267,10 +248,8 @@ public class SettingsViewModel : ViewModel
SelectedSocketExportFormat = _socketExportFormatSnapshot;
SelectedCompressionFormat = _selectedCompressionFormat;
SelectedLodExportFormat = _lodExportFormatSnapshot;
SelectedNaniteMeshExportFormat = _naniteMeshExportFormatSnapshot;
SelectedMaterialExportFormat = _materialExportFormatSnapshot;
SelectedTextureExportFormat = _textureExportFormatSnapshot;
CriwareDecryptionKey = _criwareDecryptionKey;
SelectedAesReload = UserSettings.Default.AesReload;
SelectedDiscordRpc = UserSettings.Default.DiscordRpc;
@ -284,7 +263,6 @@ public class SettingsViewModel : ViewModel
SocketExportFormats = new ReadOnlyObservableCollection<ESocketFormat>(new ObservableCollection<ESocketFormat>(EnumerateSocketExportFormat()));
CompressionFormats = new ReadOnlyObservableCollection<EFileCompressionFormat>(new ObservableCollection<EFileCompressionFormat>(EnumerateCompressionFormat()));
LodExportFormats = new ReadOnlyObservableCollection<ELodFormat>(new ObservableCollection<ELodFormat>(EnumerateLodExportFormat()));
NaniteMeshExportFormats = new ReadOnlyObservableCollection<ENaniteMeshFormat>(new ObservableCollection<ENaniteMeshFormat>(EnumerateNaniteMeshExportFormat()));
MaterialExportFormats = new ReadOnlyObservableCollection<EMaterialFormat>(new ObservableCollection<EMaterialFormat>(EnumerateMaterialExportFormat()));
TextureExportFormats = new ReadOnlyObservableCollection<ETextureFormat>(new ObservableCollection<ETextureFormat>(EnumerateTextureExportFormat()));
Platforms = new ReadOnlyObservableCollection<ETexturePlatform>(new ObservableCollection<ETexturePlatform>(EnumerateUePlatforms()));
@ -317,7 +295,6 @@ public class SettingsViewModel : ViewModel
UserSettings.Default.CurrentDir.Versioning.CustomVersions = SelectedCustomVersions;
UserSettings.Default.CurrentDir.Versioning.Options = SelectedOptions;
UserSettings.Default.CurrentDir.Versioning.MapStructTypes = SelectedMapStructTypes;
UserSettings.Default.CurrentDir.CriwareDecryptionKey = CriwareDecryptionKey;
UserSettings.Default.AssetLanguage = SelectedAssetLanguage;
UserSettings.Default.CompressedAudioMode = SelectedCompressedAudio;
@ -326,7 +303,6 @@ public class SettingsViewModel : ViewModel
UserSettings.Default.SocketExportFormat = SelectedSocketExportFormat;
UserSettings.Default.CompressionFormat = SelectedCompressionFormat;
UserSettings.Default.LodExportFormat = SelectedLodExportFormat;
UserSettings.Default.NaniteMeshExportFormat = SelectedNaniteMeshExportFormat;
UserSettings.Default.MaterialExportFormat = SelectedMaterialExportFormat;
UserSettings.Default.TextureExportFormat = SelectedTextureExportFormat;
UserSettings.Default.AesReload = SelectedAesReload;
@ -342,7 +318,7 @@ public class SettingsViewModel : ViewModel
=> Enum.GetValues<EGame>()
.GroupBy(value => (int)value)
.Select(group => group.First())
.OrderBy(value => ((int)value & 0xFF) == 0);
.OrderBy(value => (int)value == ((int)value & ~0xF));
private IEnumerable<ELanguage> EnumerateAssetLanguages() => Enum.GetValues<ELanguage>();
private IEnumerable<EAesReload> EnumerateAesReloads() => Enum.GetValues<EAesReload>();
private IEnumerable<EDiscordRpc> EnumerateDiscordRpcs() => Enum.GetValues<EDiscordRpc>();
@ -352,7 +328,6 @@ public class SettingsViewModel : ViewModel
private IEnumerable<ESocketFormat> EnumerateSocketExportFormat() => Enum.GetValues<ESocketFormat>();
private IEnumerable<EFileCompressionFormat> EnumerateCompressionFormat() => Enum.GetValues<EFileCompressionFormat>();
private IEnumerable<ELodFormat> EnumerateLodExportFormat() => Enum.GetValues<ELodFormat>();
private IEnumerable<ENaniteMeshFormat> EnumerateNaniteMeshExportFormat() => Enum.GetValues<ENaniteMeshFormat>();
private IEnumerable<EMaterialFormat> EnumerateMaterialExportFormat() => Enum.GetValues<EMaterialFormat>();
private IEnumerable<ETextureFormat> EnumerateTextureExportFormat() => Enum.GetValues<ETextureFormat>();
private IEnumerable<ETexturePlatform> EnumerateUePlatforms() => Enum.GetValues<ETexturePlatform>();

View File

@ -8,6 +8,7 @@ using ICSharpCode.AvalonEdit.Document;
using ICSharpCode.AvalonEdit.Highlighting;
using Serilog;
using SkiaSharp;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
@ -15,15 +16,12 @@ using System.Windows;
using System.Windows.Media.Imaging;
using CUE4Parse.UE4.Assets.Exports.Texture;
using CUE4Parse_Conversion.Textures;
using CUE4Parse.FileProvider.Objects;
using CUE4Parse.Utils;
namespace FModel.ViewModels;
public class TabImage : ViewModel
{
public string ExportName { get; set; }
public string ExportName { get; }
public byte[] ImageBuffer { get; set; }
public TabImage(string name, bool rnn, SKBitmap img)
@ -33,13 +31,6 @@ public class TabImage : ViewModel
SetImage(img);
}
public TabImage(string name, bool rnn, CTexture img)
{
ExportName = name;
RenderNearestNeighbor = rnn;
SetImage(img);
}
private BitmapImage _image;
public BitmapImage Image
{
@ -79,42 +70,11 @@ public class TabImage : ViewModel
}
_bmp = bitmap;
ExportName += "." + (NoAlpha ? "jpg" : "png");
using var data = _bmp.Encode(NoAlpha ? SKEncodedImageFormat.Jpeg : SKEncodedImageFormat.Png, 100);
using var data = _bmp.Encode(NoAlpha ? ETextureFormat.Jpeg : UserSettings.Default.TextureExportFormat, 100);
using var stream = new MemoryStream(ImageBuffer = data.ToArray(), false);
var image = new BitmapImage();
image.BeginInit();
image.CacheOption = BitmapCacheOption.OnLoad;
image.StreamSource = stream;
image.EndInit();
image.Freeze();
Image = image;
}
private void SetImage(CTexture bitmap)
{
if (bitmap is null)
{
ImageBuffer = null;
Image = null;
if (UserSettings.Default.TextureExportFormat == ETextureFormat.Tga)
return;
}
_bmp = bitmap.ToSkBitmap();
byte[] imageData = _bmp.Encode(NoAlpha ? SKEncodedImageFormat.Jpeg : SKEncodedImageFormat.Png, 100).ToArray();
if (PixelFormatUtils.IsHDR(bitmap.PixelFormat) || (UserSettings.Default.TextureExportFormat != ETextureFormat.Jpeg && UserSettings.Default.TextureExportFormat != ETextureFormat.Png))
{
ImageBuffer = bitmap.Encode(UserSettings.Default.TextureExportFormat, UserSettings.Default.SaveHdrTexturesAsHdr, out var ext);
ExportName += "." + ext;
}
else
{
ImageBuffer = imageData;
ExportName += "." + (NoAlpha ? "jpg" : "png");
}
using var stream = new MemoryStream(imageData);
var image = new BitmapImage();
image.BeginInit();
image.CacheOption = BitmapCacheOption.OnLoad;
@ -132,28 +92,22 @@ public class TabItem : ViewModel
{
public string ParentExportType { get; private set; }
private GameFile _entry;
public GameFile Entry
private string _header;
public string Header
{
get => _entry;
set
{
SetProperty(ref _entry, value);
RaisePropertyChanged(nameof(Header));
}
get => _header;
set => SetProperty(ref _header, value);
}
private string _titleExtra;
public string TitleExtra
private string _directory;
public string Directory
{
get => _titleExtra;
set
{
SetProperty(ref _titleExtra, value);
RaisePropertyChanged(nameof(Header));
}
get => _directory;
set => SetProperty(ref _directory, value);
}
public string FullPath => this.Directory + "/" + this.Header;
private bool _hasSearchOpen;
public bool HasSearchOpen
{
@ -248,8 +202,6 @@ public class TabItem : ViewModel
}
}
public string Header => $"{Entry.Name}{(string.IsNullOrEmpty(TitleExtra) ? "" : $" ({TitleExtra})")}";
public bool HasImage => SelectedImage != null;
public bool HasMultipleImages => _images.Count > 1;
public string Page => $"{_images.IndexOf(_selectedImage) + 1} / {_images.Count}";
@ -265,17 +217,18 @@ public class TabItem : ViewModel
private GoToCommand _goToCommand;
public GoToCommand GoToCommand => _goToCommand ??= new GoToCommand(null);
public TabItem(GameFile entry, string parentExportType)
public TabItem(string header, string directory, string parentExportType)
{
Entry = entry;
Header = header;
Directory = directory;
ParentExportType = parentExportType;
_images = new ObservableCollection<TabImage>();
}
public void SoftReset(GameFile entry)
public void SoftReset(string header, string directory)
{
Entry = entry;
TitleExtra = string.Empty;
Header = header;
Directory = directory;
ParentExportType = string.Empty;
ScrollTrigger = null;
Application.Current.Dispatcher.Invoke(() =>
@ -292,7 +245,7 @@ public class TabItem : ViewModel
public void AddImage(UTexture texture, bool save, bool updateUi)
{
var appendLayerNumber = false;
var img = new CTexture[1];
var img = new SKBitmap[1];
if (texture is UTexture2DArray textureArray)
{
img = textureArray.DecodeTextureArray(UserSettings.Default.CurrentDir.TexturePlatform);
@ -303,7 +256,7 @@ public class TabItem : ViewModel
img[0] = texture.Decode(UserSettings.Default.CurrentDir.TexturePlatform);
if (texture is UTextureCube)
{
img[0] = img[0].ToPanorama();
img[0] = img[0]?.ToPanorama();
}
}
@ -318,29 +271,6 @@ public class TabItem : ViewModel
}
}
public void AddImage(string name, bool rnn, CTexture[] img, bool save, bool updateUi, bool appendLayerNumber = false)
{
for (var i = 0; i < img.Length; i++)
{
AddImage($"{name}{(appendLayerNumber ? $"_{i}" : "")}", rnn, img[i], save, updateUi);
}
}
public void AddImage(string name, bool rnn, CTexture img, bool save, bool updateUi)
{
Application.Current.Dispatcher.Invoke(() =>
{
var t = new TabImage(name, rnn, img);
if (save) SaveImage(t, updateUi);
if (!updateUi) return;
_images.Add(t);
SelectedImage ??= t;
RaisePropertyChanged("Page");
RaisePropertyChanged("HasMultipleImages");
});
}
public void AddImage(string name, bool rnn, SKBitmap img, bool save, bool updateUi)
{
Application.Current.Dispatcher.Invoke(() =>
@ -374,14 +304,23 @@ public class TabItem : ViewModel
public void SaveImage() => SaveImage(SelectedImage, true);
private void SaveImage(TabImage image, bool updateUi)
{
if (image == null)
return;
if (image == null) return;
var path = Path.Combine(UserSettings.Default.TextureDirectory, UserSettings.Default.KeepDirectoryStructure ? Entry.Directory : "", image.ExportName).Replace('\\', '/');
var ext = UserSettings.Default.TextureExportFormat switch
{
ETextureFormat.Png => ".png",
ETextureFormat.Jpeg => ".jpg",
ETextureFormat.Tga => ".tga",
_ => ".png"
};
Directory.CreateDirectory(path.SubstringBeforeLast('/'));
var fileName = image.ExportName + ext;
var path = Path.Combine(UserSettings.Default.TextureDirectory,
UserSettings.Default.KeepDirectoryStructure ? Directory : "", fileName!).Replace('\\', '/');
SaveImage(image, path, image.ExportName, updateUi);
System.IO.Directory.CreateDirectory(path.SubstringBeforeLast('/'));
SaveImage(image, path, fileName, updateUi);
}
private void SaveImage(TabImage image, string path, string fileName, bool updateUi)
@ -398,11 +337,11 @@ public class TabItem : ViewModel
public void SaveProperty(bool updateUi)
{
var fileName = Path.ChangeExtension(Entry.Name, ".json");
var fileName = Path.ChangeExtension(Header, ".json");
var directory = Path.Combine(UserSettings.Default.PropertiesDirectory,
UserSettings.Default.KeepDirectoryStructure ? Entry.Directory : "", fileName).Replace('\\', '/');
UserSettings.Default.KeepDirectoryStructure ? Directory : "", fileName).Replace('\\', '/');
Directory.CreateDirectory(directory.SubstringBeforeLast('/'));
System.IO.Directory.CreateDirectory(directory.SubstringBeforeLast('/'));
Application.Current.Dispatcher.Invoke(() => File.WriteAllText(directory, Document.Text));
SaveCheck(directory, fileName, updateUi);
@ -451,25 +390,28 @@ public class TabControlViewModel : ViewModel
public TabControlViewModel()
{
_tabItems = [];
_tabItems = new ObservableCollection<TabItem>(EnumerateTabs());
TabsItems = new ReadOnlyObservableCollection<TabItem>(_tabItems);
AddTab();
SelectedTab = TabsItems.FirstOrDefault();
}
public void AddTab() => AddTab("New Tab");
public void AddTab(string title) => AddTab(new FakeGameFile(title));
public void AddTab(GameFile entry, string parentExportType = null)
public void AddTab(string header = null, string directory = null, string parentExportType = null)
{
if (SelectedTab?.Header == "New Tab")
if (!CanAddTabs) return;
var h = header ?? "New Tab";
var d = directory ?? string.Empty;
var p = parentExportType ?? string.Empty;
if (SelectedTab is { Header : "New Tab" })
{
SelectedTab.Entry = entry;
SelectedTab.Header = h;
SelectedTab.Directory = d;
return;
}
if (!CanAddTabs) return;
Application.Current.Dispatcher.Invoke(() =>
{
_tabItems.Add(new TabItem(entry, parentExportType ?? string.Empty));
_tabItems.Add(new TabItem(h, d, p));
SelectedTab = _tabItems.Last();
});
}
@ -528,4 +470,9 @@ public class TabControlViewModel : ViewModel
_tabItems.Clear();
});
}
private static IEnumerable<TabItem> EnumerateTabs()
{
yield return new TabItem("New Tab", string.Empty, string.Empty);
}
}

View File

@ -40,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()
{
@ -100,7 +104,37 @@ public class ThreadWorkerViewModel : ViewModel
CurrentCancellationTokenSource = null; // kill token
Log.Error("{Exception}", e);
FLogger.Append(e);
FLogger.Append(ELog.Error, () =>
{
if ((e.InnerException ?? e) is { TargetSite.DeclaringType: not null } exception)
{
if (exception.TargetSite.ToString() == "CUE4Parse.FileProvider.GameFile get_Item(System.String)")
{
FLogger.Text(e.Message, Constants.WHITE, true);
}
else
{
var t = exception.GetType();
FLogger.Text(t.Namespace + _dot, Constants.GRAY);
FLogger.Text(t.Name, Constants.WHITE);
FLogger.Text(_colon + " ", Constants.GRAY);
FLogger.Text(exception.Message, Constants.RED, true);
FLogger.Text(_at, _gray);
FLogger.Text(exception.TargetSite.DeclaringType.FullName + _dot, Constants.GRAY);
FLogger.Text(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.Text("(" + string.Join(", ", parameters) + ")", Constants.GRAY, true);
}
}
});
return;
}
}

View File

@ -1,11 +1,9 @@
using System;
using System.Collections.Generic;
using System;
using System.ComponentModel;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Windows.Data;
using CUE4Parse.Utils;
using FModel.Extensions;
using FModel.Framework;
using FModel.Services;
using FModel.Settings;
@ -15,7 +13,7 @@ using FModel.Views.Resources.Converters;
namespace FModel.ViewModels;
public partial class UpdateViewModel : ViewModel
public class UpdateViewModel : ViewModel
{
private ApiEndpointViewModel _apiEndpointView => ApplicationService.ApiEndpointView;
@ -27,7 +25,7 @@ public partial class UpdateViewModel : ViewModel
public UpdateViewModel()
{
Commits = [];
Commits = new RangeObservableCollection<GitHubCommit>();
CommitsView = new ListCollectionView(Commits)
{
GroupDescriptions = { new PropertyGroupDescription("Commit.Author.Date", new DateTimeToDateConverter()) }
@ -37,121 +35,40 @@ public partial class UpdateViewModel : ViewModel
RemindMeCommand.Execute(this, null);
}
public async Task LoadAsync()
public async Task Load()
{
var commits = await _apiEndpointView.GitHubApi.GetCommitHistoryAsync();
if (commits == null || commits.Length == 0)
return;
Commits.AddRange(await _apiEndpointView.GitHubApi.GetCommitHistoryAsync());
Commits.AddRange(commits);
var qa = await _apiEndpointView.GitHubApi.GetReleaseAsync("qa");
qa.Assets.OrderByDescending(x => x.CreatedAt).First().IsLatest = true;
try
foreach (var asset in qa.Assets)
{
_ = LoadCoAuthors();
_ = LoadAssets();
}
catch
{
//
}
}
private Task LoadCoAuthors()
{
return Task.Run(async () =>
{
var coAuthorMap = new Dictionary<GitHubCommit, HashSet<string>>();
foreach (var commit in Commits)
var commitSha = asset.Name.SubstringBeforeLast(".zip");
var commit = Commits.FirstOrDefault(x => x.Sha == commitSha);
if (commit != null)
{
if (!commit.Commit.Message.Contains("Co-authored-by"))
continue;
var regex = GetCoAuthorRegex();
var matches = regex.Matches(commit.Commit.Message);
if (matches.Count == 0) continue;
commit.Commit.Message = regex.Replace(commit.Commit.Message, string.Empty).Trim();
coAuthorMap[commit] = [];
foreach (Match match in matches)
{
if (match.Groups.Count < 3) continue;
coAuthorMap[commit].Add(match.Groups[1].Value);
}
commit.Asset = asset;
}
if (coAuthorMap.Count == 0) return;
var uniqueUsernames = coAuthorMap.Values.SelectMany(x => x).Distinct().ToArray();
var authorCache = new Dictionary<string, Author>();
foreach (var username in uniqueUsernames)
else
{
try
Commits.Add(new GitHubCommit
{
var author = await _apiEndpointView.GitHubApi.GetUserAsync(username);
if (author != null)
authorCache[username] = author;
}
catch
{
//
}
}
foreach (var (commit, usernames) in coAuthorMap)
{
var coAuthors = usernames
.Where(username => authorCache.ContainsKey(username))
.Select(username => authorCache[username])
.ToArray();
if (coAuthors.Length > 0)
commit.CoAuthors = coAuthors;
}
});
}
private Task LoadAssets()
{
return Task.Run(async () =>
{
var qa = await _apiEndpointView.GitHubApi.GetReleaseAsync("qa");
var assets = qa.Assets.OrderByDescending(x => x.CreatedAt).ToList();
for (var i = 0; i < assets.Count; i++)
{
var asset = assets[i];
asset.IsLatest = i == 0;
var commitSha = asset.Name.SubstringBeforeLast(".zip");
var commit = Commits.FirstOrDefault(x => x.Sha == commitSha);
if (commit != null)
{
commit.Asset = asset;
}
else
{
Commits.Add(new GitHubCommit
Sha = commitSha,
Commit = new Commit
{
Sha = commitSha,
Commit = new Commit
{
Message = $"FModel ({commitSha[..7]})",
Author = new Author { Name = asset.Uploader.Login, Date = asset.CreatedAt }
},
Author = asset.Uploader,
Asset = asset
});
}
Message = $"FModel ({commitSha[..7]})",
Author = new Author { Name = asset.Uploader.Login, Date = asset.CreatedAt }
},
Author = asset.Uploader,
Asset = asset
});
}
});
}
}
public void DownloadLatest()
{
Commits.FirstOrDefault(x => x.IsDownloadable && x.Asset.IsLatest)?.Download();
Commits.FirstOrDefault(x => x.Asset.IsLatest)?.Download();
}
[GeneratedRegex(@"Co-authored-by:\s*(.+?)\s*<(.+?)>", RegexOptions.IgnoreCase | RegexOptions.Multiline, "en-US")]
private static partial Regex GetCoAuthorRegex();
}

View File

@ -11,6 +11,13 @@
<Setter Property="Title" Value="About" />
</Style>
</adonisControls:AdonisWindow.Style>
<adonisControls:AdonisWindow.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Resources/Resources.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</adonisControls:AdonisWindow.Resources>
<StackPanel Margin="30 10">
<StackPanel HorizontalAlignment="Center" Margin="0 0 0 30">
<TextBlock Text="{Binding Source={x:Static local:Constants.APP_VERSION}, StringFormat={}FModel {0}}" FontSize="15" FontWeight="500" Foreground="#9DA3DD" FontStretch="Expanded" />

View File

@ -14,6 +14,13 @@
<Setter Property="Title" Value="AES Manager" />
</Style>
</adonisControls:AdonisWindow.Style>
<adonisControls:AdonisWindow.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Resources/Resources.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</adonisControls:AdonisWindow.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>

View File

@ -16,6 +16,13 @@
<Setter Property="Title" Value="Audio Player" />
</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" />

View File

@ -12,6 +12,13 @@
<Setter Property="Title" Value="Backup Manager" />
</Style>
</adonisControls:AdonisWindow.Style>
<adonisControls:AdonisWindow.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Resources/Resources.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</adonisControls:AdonisWindow.Resources>
<Grid Column="2" adonisExtensions:LayerExtension.Layer="2" Margin="10" Background="Transparent">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />

View File

@ -14,6 +14,13 @@
<Setter Property="Title" Value="Directory Selector" />
</Style>
</adonisControls:AdonisWindow.Style>
<adonisControls:AdonisWindow.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Resources/Resources.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</adonisControls:AdonisWindow.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>

View File

@ -14,6 +14,14 @@
<Setter Property="Title" Value="Image Merger"/>
</Style>
</adonisControls:AdonisWindow.Style>
<adonisControls:AdonisWindow.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Resources/Resources.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</adonisControls:AdonisWindow.Resources>
<Grid Margin="10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>

View File

@ -1,35 +0,0 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<SolidColorBrush x:Key="NeutralBrush" Color="White" />
<SolidColorBrush x:Key="BlueprintBrush" Color="DodgerBlue" />
<SolidColorBrush x:Key="BlueprintWidgetBrush" Color="DarkViolet" />
<SolidColorBrush x:Key="BlueprintAnimBrush" Color="Crimson" />
<SolidColorBrush x:Key="BlueprintRigVMBrush" Color="Teal" />
<SolidColorBrush x:Key="CookedMetaDataBrush" Color="Yellow" />
<SolidColorBrush x:Key="UserDefinedEnumBrush" Color="DarkGoldenrod" />
<SolidColorBrush x:Key="UserDefinedStructBrush" Color="Tan" />
<SolidColorBrush x:Key="MaterialBrush" Color="BurlyWood" />
<SolidColorBrush x:Key="MaterialEditorBrush" Color="Yellow" />
<SolidColorBrush x:Key="BinaryBrush" Color="Yellow" />
<SolidColorBrush x:Key="TextureBrush" Color="MediumPurple" />
<SolidColorBrush x:Key="ConfigBrush" Color="LightSlateGray" />
<SolidColorBrush x:Key="AudioBrush" Color="MediumSeaGreen" />
<SolidColorBrush x:Key="VideoBrush" Color="IndianRed" />
<SolidColorBrush x:Key="DataTableBrush" Color="SteelBlue" />
<SolidColorBrush x:Key="CurveBrush" Color="HotPink" />
<SolidColorBrush x:Key="PluginBrush" Color="GreenYellow" />
<SolidColorBrush x:Key="ProjectBrush" Color="DeepSkyBlue" />
<SolidColorBrush x:Key="LocalizationBrush" Color="CornflowerBlue" />
<SolidColorBrush x:Key="WorldBrush" Color="Orange" />
<SolidColorBrush x:Key="BuildDataBrush" Color="Tomato" />
<SolidColorBrush x:Key="LevelSequenceBrush" Color="Coral" />
<SolidColorBrush x:Key="FoliageBrush" Color="ForestGreen" />
<SolidColorBrush x:Key="ParticleBrush" Color="Gold" />
<SolidColorBrush x:Key="AnimationBrush" Color="Coral" />
<SolidColorBrush x:Key="LuaBrush" Color="DarkBlue" />
<SolidColorBrush x:Key="JsonXmlBrush" Color="LightGreen" />
</ResourceDictionary>

View File

@ -3,7 +3,6 @@ using System.Text.RegularExpressions;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.TextFormatting;
using CUE4Parse.Utils;
using FModel.Extensions;
using FModel.Services;
using FModel.ViewModels;
@ -70,24 +69,31 @@ public class GamePathVisualLineText : VisualLineText
{
var obj = gamePath.SubstringAfterLast('.');
var package = gamePath.SubstringBeforeLast('.');
var fullPath = _applicationView.CUE4Parse.Provider.FixPath(package);
var firstLine = a.ParentVisualLine.Document.GetLineByNumber(2);
if (a.ParentVisualLine.Document.FileName.Equals(fullPath.SubstringBeforeLast('.'), StringComparison.OrdinalIgnoreCase) &&
!a.ParentVisualLine.Document.GetText(firstLine.Offset, firstLine.Length).Equals(" \"Summary\": {")) // Show Metadata case
var fullPath = _applicationView.CUE4Parse.Provider.FixPath(package, StringComparison.Ordinal);
if (a.ParentVisualLine.Document.FileName.Equals(fullPath.SubstringBeforeLast('.'), StringComparison.OrdinalIgnoreCase))
{
var lineNumber = a.ParentVisualLine.Document.Text.GetNameLineNumber(obj);
if (lineNumber > -1)
{
var line = a.ParentVisualLine.Document.GetLineByNumber(lineNumber);
AvalonEditor.YesWeEditor.Select(line.Offset, line.Length);
AvalonEditor.YesWeEditor.ScrollToLine(lineNumber);
return;
}
}
int lineNumber;
DocumentLine line;
await _threadWorkerView.Begin(cancellationToken =>
_applicationView.CUE4Parse.ExtractAndScroll(cancellationToken, fullPath, obj, parentExportType));
if (Regex.IsMatch(obj, @"^(.+)\[(\d+)\]$"))
{
lineNumber = a.ParentVisualLine.Document.Text.GetKismetLineNumber(obj);
line = a.ParentVisualLine.Document.GetLineByNumber(lineNumber);
}
else
{
lineNumber = a.ParentVisualLine.Document.Text.GetNameLineNumber(obj);
line = a.ParentVisualLine.Document.GetLineByNumber(lineNumber);
}
AvalonEditor.YesWeEditor.Select(line.Offset, line.Length);
AvalonEditor.YesWeEditor.ScrollToLine(lineNumber);
}
else
{
await _threadWorkerView.Begin(cancellationToken =>
_applicationView.CUE4Parse.ExtractAndScroll(cancellationToken, fullPath, obj, parentExportType));
}
};
return a;
}

View File

@ -1,34 +0,0 @@
using System.Text.RegularExpressions;
using ICSharpCode.AvalonEdit.Rendering;
namespace FModel.Views.Resources.Controls;
public class JumpElementGenerator : VisualLineElementGenerator
{
private readonly Regex _JumpRegex = new(
@"\b(?:goto\s+Label_(?'target'\d+);)",
RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
private Match FindMatch(int startOffset)
{
var endOffset = CurrentContext.VisualLine.LastDocumentLine.EndOffset;
var relevantText = CurrentContext.Document.GetText(startOffset, endOffset - startOffset);
return _JumpRegex.Match(relevantText);
}
public override int GetFirstInterestedOffset(int startOffset)
{
var m = FindMatch(startOffset);
return m.Success ? startOffset + m.Index : -1;
}
public override VisualLineElement ConstructElement(int offset)
{
var m = FindMatch(offset);
if (!m.Success || m.Index != 0 ||
!m.Groups.TryGetValue("target", out var g))
return null;
return new JumpVisualLineText(g.Value, CurrentContext.VisualLine, g.Length + g.Index + 1);
}
}

View File

@ -1,75 +0,0 @@
using System;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.TextFormatting;
using FModel.Extensions;
using FModel.Services;
using FModel.ViewModels;
using ICSharpCode.AvalonEdit.Rendering;
namespace FModel.Views.Resources.Controls;
public class JumpVisualLineText : VisualLineText
{
public delegate void JumpOnClick(string Jump);
public event JumpOnClick OnJumpClicked;
private readonly string _jump;
public JumpVisualLineText(string jump, VisualLine parentVisualLine, int length) : base(parentVisualLine, length)
{
_jump = jump;
}
public override TextRun CreateTextRun(int startVisualColumn, ITextRunConstructionContext context)
{
if (context == null)
throw new ArgumentNullException(nameof(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 JumpIsClickable() => !string.IsNullOrEmpty(_jump) && Keyboard.Modifiers == ModifierKeys.None;
protected override void OnQueryCursor(QueryCursorEventArgs e)
{
if (!JumpIsClickable())
return;
e.Handled = true;
e.Cursor = Cursors.Hand;
}
protected override void OnMouseDown(MouseButtonEventArgs e)
{
if (e.ChangedButton != MouseButton.Left || !JumpIsClickable())
return;
if (e.Handled || OnJumpClicked == null)
return;
OnJumpClicked(_jump);
e.Handled = true;
}
protected override VisualLineText CreateInstance(int length)
{
var a = new JumpVisualLineText(_jump, ParentVisualLine, length);
a.OnJumpClicked += jump =>
{
var lineNumber = a.ParentVisualLine.Document.Text.GetNameLineNumberText($" Label_{jump}:"); // impossible for different indentation
if (lineNumber > -1)
{
var line = a.ParentVisualLine.Document.GetLineByNumber(lineNumber);
AvalonEditor.YesWeEditor.Select(line.Offset, line.Length);
AvalonEditor.YesWeEditor.ScrollToLine(lineNumber);
}
};
return a;
}
}

View File

@ -5,6 +5,13 @@
xmlns:avalonEdit="http://icsharpcode.net/sharpdevelop/avalonedit"
xmlns:adonisExtensions="clr-namespace:AdonisUI.Extensions;assembly=AdonisUI"
PreviewKeyDown="OnPreviewKeyDown">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="../Resources.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />

View File

@ -43,7 +43,6 @@ public partial class AvalonEditor
MyAvalonEditor.TextArea.TextView.LinkTextBackgroundBrush = null;
MyAvalonEditor.TextArea.TextView.LinkTextForegroundBrush = Brushes.Cornsilk;
MyAvalonEditor.TextArea.TextView.ElementGenerators.Add(new GamePathElementGenerator());
MyAvalonEditor.TextArea.TextView.ElementGenerators.Add(new JumpElementGenerator());
MyAvalonEditor.TextArea.TextView.ElementGenerators.Add(new HexColorElementGenerator());
ApplicationService.ApplicationView.CUE4Parse.TabControl.OnTabRemove += OnTabClose;
@ -120,7 +119,7 @@ public partial class AvalonEditor
if (sender is not TextEditor avalonEditor || DataContext is not TabItem tabItem ||
avalonEditor.Document == null || string.IsNullOrEmpty(avalonEditor.Document.Text))
return;
avalonEditor.Document.FileName = tabItem.Entry.PathWithoutExtension;
avalonEditor.Document.FileName = tabItem.Directory + '/' + StringExtensions.SubstringBeforeLast(tabItem.Header, '.');
if (!_savedCarets.ContainsKey(avalonEditor.Document.FileName))
_ignoreCaret = true;
@ -128,8 +127,6 @@ public partial class AvalonEditor
if (!tabItem.ShouldScroll) return;
var lineNumber = avalonEditor.Document.Text.GetNameLineNumber(tabItem.ScrollTrigger);
if (lineNumber == -1) lineNumber = 1;
var line = avalonEditor.Document.GetLineByNumber(lineNumber);
avalonEditor.Select(line.Offset, line.Length);
avalonEditor.ScrollToLine(lineNumber);

View File

@ -2,5 +2,12 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
DataContextChanged="OnDataContextChanged">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="../Resources.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<StackPanel x:Name="InMeDaddy" Orientation="Horizontal" HorizontalAlignment="Right" Height="24" />
</UserControl>

View File

@ -1,4 +1,4 @@
using System.Linq;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
@ -10,7 +10,7 @@ namespace FModel.Views.Resources.Controls;
public partial class Breadcrumb
{
private const string NavigateNext = "M9.31 6.71c-.39.39-.39 1.02 0 1.41L13.19 12l-3.88 3.88c-.39.39-.39 1.02 0 1.41.39.39 1.02.39 1.41 0l4.59-4.59c.39-.39.39-1.02 0-1.41L10.72 6.7c-.38-.38-1.02-.38-1.41.01z";
private const string _NAVIGATE_NEXT = "M9.31 6.71c-.39.39-.39 1.02 0 1.41L13.19 12l-3.88 3.88c-.39.39-.39 1.02 0 1.41.39.39 1.02.39 1.41 0l4.59-4.59c.39-.39.39-1.02 0-1.41L10.72 6.7c-.38-.38-1.02-.38-1.41.01z";
public Breadcrumb()
{
@ -25,27 +25,17 @@ public partial class Breadcrumb
var folders = pathAtThisPoint.Split('/');
for (var i = 0; i < folders.Length; i++)
{
var border = new Border
var textBlock = new TextBlock
{
BorderThickness = new Thickness(1),
BorderBrush = Brushes.Transparent,
Text = folders[i],
Background = Brushes.Transparent,
Padding = new Thickness(6, 3, 6, 3),
Cursor = Cursors.Hand,
Tag = i + 1,
IsEnabled = i < folders.Length - 1,
Child = new TextBlock
{
Text = folders[i],
VerticalAlignment = VerticalAlignment.Center
}
Margin = new Thickness(0, 3, 0, 0)
};
textBlock.MouseUp += OnMouseClick;
border.MouseEnter += OnMouseEnter;
border.MouseLeave += OnMouseLeave;
border.MouseUp += OnMouseClick;
InMeDaddy.Children.Add(border);
InMeDaddy.Children.Add(textBlock);
if (i >= folders.Length - 1) continue;
InMeDaddy.Children.Add(new Viewbox
@ -62,8 +52,7 @@ public partial class Breadcrumb
new Path
{
Fill = Brushes.White,
Data = Geometry.Parse(NavigateNext),
Opacity = 0.6
Data = Geometry.Parse(_NAVIGATE_NEXT)
}
}
}
@ -71,31 +60,13 @@ public partial class Breadcrumb
}
}
private void OnMouseEnter(object sender, MouseEventArgs e)
{
if (sender is Border border)
{
border.BorderBrush = new SolidColorBrush(Color.FromRgb(127, 127, 144));
border.Background = new SolidColorBrush(Color.FromRgb(72, 73, 92));
}
}
private void OnMouseLeave(object sender, MouseEventArgs e)
{
if (sender is Border border)
{
border.BorderBrush = Brushes.Transparent;
border.Background = Brushes.Transparent;
}
}
private void OnMouseClick(object sender, MouseButtonEventArgs e)
{
if (sender is not Border { DataContext: string pathAtThisPoint, Tag: int index }) return;
if (sender is not TextBlock { DataContext: string pathAtThisPoint, Tag: int index }) return;
var directory = string.Join('/', pathAtThisPoint.Split('/').Take(index));
if (pathAtThisPoint.Equals(directory)) return;
ApplicationService.ApplicationView.CustomDirectories.GoToCommand.JumpTo(directory);
}
}
}

View File

@ -4,6 +4,13 @@
xmlns:converters="clr-namespace:FModel.Views.Resources.Converters"
xmlns:adonisUi="clr-namespace:AdonisUI;assembly=AdonisUI"
xmlns:controls="clr-namespace:FModel.Views.Resources.Controls">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="../Resources.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<Border BorderThickness="1" CornerRadius="0.5"
BorderBrush="{DynamicResource {x:Static adonisUi:Brushes.Layer4BackgroundBrush}}">
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
@ -35,33 +42,22 @@
<Grid Grid.Row="3">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="16"/>
<ColumnDefinition Width="5"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="5"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<ItemsControl Grid.Column="0" ItemsSource="{Binding Authors}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Ellipse Width="16" Height="16" Margin="0,0,2,0">
<Ellipse.Fill>
<ImageBrush ImageSource="{Binding AvatarUrl}" />
</Ellipse.Fill>
</Ellipse>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Ellipse Grid.Column="0">
<Ellipse.Fill>
<ImageBrush ImageSource="{Binding Author.AvatarUrl}" />
</Ellipse.Fill>
</Ellipse>
<TextBlock Grid.Column="2" FontSize="11">
<TextBlock.Text>
<MultiBinding StringFormat="{}{0} committed {1}">
<Binding Path="AuthorNames" />
<Binding Path="Author.Login" />
<Binding Path="Commit.Author.Date" Converter="{x:Static converters:RelativeDateTimeConverter.Instance}" />
</MultiBinding>
</TextBlock.Text>
@ -69,7 +65,7 @@
</Grid>
</Grid>
<Grid Grid.Column="1" MaxHeight="96">
<Grid Grid.Column="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="15" />
@ -132,3 +128,4 @@
</Grid>
</Border>
</UserControl>

View File

@ -3,6 +3,13 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converters="clr-namespace:FModel.Views.Resources.Converters"
xmlns:adonisUi="clr-namespace:AdonisUI;assembly=AdonisUI">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="../Resources.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />

View File

@ -1,305 +0,0 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:adonisUi="clr-namespace:AdonisUI;assembly=AdonisUI"
xmlns:settings="clr-namespace:FModel.Settings"
xmlns:converters="clr-namespace:FModel.Views.Resources.Converters">
<ContextMenu x:Key="FileContextMenu" x:Shared="False"
DataContext="{Binding DataContext, RelativeSource={RelativeSource AncestorType=Window}}">
<MenuItem Header="Extract in New Tab" Command="{Binding RightClickMenuCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="Assets_Extract_New_Tab" />
<Binding Path="PlacementTarget.SelectedItems" RelativeSource="{RelativeSource AncestorType=ContextMenu}" />
</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 ExtractIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Show Metadata" Command="{Binding RightClickMenuCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="Assets_Show_Metadata" />
<Binding Path="PlacementTarget.SelectedItems" RelativeSource="{RelativeSource AncestorType=ContextMenu}" />
</MultiBinding>
</MenuItem.CommandParameter>
<MenuItem.IsEnabled>
<Binding Path="PlacementTarget.SelectedItems" RelativeSource="{RelativeSource AncestorType=ContextMenu}">
<Binding.Converter>
<converters:AnyItemMeetsConditionConverter>
<converters:AnyItemMeetsConditionConverter.Conditions>
<converters:ItemIsUePackageCondition />
</converters:AnyItemMeetsConditionConverter.Conditions>
</converters:AnyItemMeetsConditionConverter>
</Binding.Converter>
</Binding>
</MenuItem.IsEnabled>
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource InfoIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Find References" Command="{Binding RightClickMenuCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="Assets_Show_References" />
<Binding Path="PlacementTarget.SelectedItems" RelativeSource="{RelativeSource AncestorType=ContextMenu}" />
</MultiBinding>
</MenuItem.CommandParameter>
<MenuItem.IsEnabled>
<Binding Path="PlacementTarget.SelectedItems" RelativeSource="{RelativeSource AncestorType=ContextMenu}">
<Binding.Converter>
<converters:AnyItemMeetsConditionConverter>
<converters:AnyItemMeetsConditionConverter.Conditions>
<converters:ItemIsUePackageCondition />
<converters:ItemIsIoStoreCondition />
</converters:AnyItemMeetsConditionConverter.Conditions>
</converters:AnyItemMeetsConditionConverter>
</Binding.Converter>
</Binding>
</MenuItem.IsEnabled>
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource SearchIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Decompile Blueprint"
Command="{Binding RightClickMenuCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="Assets_Decompile" />
<Binding Path="PlacementTarget.SelectedItems" RelativeSource="{RelativeSource AncestorType=ContextMenu}" />
</MultiBinding>
</MenuItem.CommandParameter>
<MenuItem.IsEnabled>
<Binding Path="PlacementTarget.SelectedItems" RelativeSource="{RelativeSource AncestorType=ContextMenu}">
<Binding.Converter>
<converters:AnyItemMeetsConditionConverter>
<converters:AnyItemMeetsConditionConverter.Conditions>
<converters:ItemCategoryCondition Category="Blueprint" />
</converters:AnyItemMeetsConditionConverter.Conditions>
</converters:AnyItemMeetsConditionConverter>
</Binding.Converter>
</Binding>
</MenuItem.IsEnabled>
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource CppIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
<MenuItem.Style>
<Style TargetType="{x:Type MenuItem}" BasedOn="{StaticResource {x:Type MenuItem}}">
<Style.Triggers>
<DataTrigger Binding="{Binding ShowDecompileOption, Source={x:Static settings:UserSettings.Default}}" Value="False">
<Setter Property="Visibility" Value="Collapsed" />
</DataTrigger>
</Style.Triggers>
</Style>
</MenuItem.Style>
</MenuItem>
<Separator />
<MenuItem Command="{Binding RightClickMenuCommand}">
<MenuItem.Header>
<TextBlock
Text="{Binding PlacementTarget.SelectedItem.Asset.Extension,
FallbackValue='Export Raw Data',
StringFormat='Export Raw Data (.{0})',
RelativeSource={RelativeSource AncestorType=ContextMenu}}" />
</MenuItem.Header>
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="Assets_Export_Data" />
<Binding Path="PlacementTarget.SelectedItems" RelativeSource="{RelativeSource AncestorType=ContextMenu}" />
</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 Properties (.json)" Command="{Binding RightClickMenuCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="Assets_Save_Properties" />
<Binding Path="PlacementTarget.SelectedItems" RelativeSource="{RelativeSource AncestorType=ContextMenu}" />
</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_Textures" />
<Binding Path="PlacementTarget.SelectedItems" RelativeSource="{RelativeSource AncestorType=ContextMenu}" />
</MultiBinding>
</MenuItem.CommandParameter>
<MenuItem.IsEnabled>
<Binding Path="PlacementTarget.SelectedItems" RelativeSource="{RelativeSource AncestorType=ContextMenu}">
<Binding.Converter>
<converters:AnyItemMeetsConditionConverter>
<converters:AnyItemMeetsConditionConverter.Conditions>
<converters:ItemCategoryCondition Category="Texture" />
</converters:AnyItemMeetsConditionConverter.Conditions>
</converters:AnyItemMeetsConditionConverter>
</Binding.Converter>
</Binding>
</MenuItem.IsEnabled>
<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 Model" Command="{Binding RightClickMenuCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="Assets_Save_Models" />
<Binding Path="PlacementTarget.SelectedItems" RelativeSource="{RelativeSource AncestorType=ContextMenu}" />
</MultiBinding>
</MenuItem.CommandParameter>
<MenuItem.IsEnabled>
<Binding Path="PlacementTarget.SelectedItems" RelativeSource="{RelativeSource AncestorType=ContextMenu}">
<Binding.Converter>
<converters:AnyItemMeetsConditionConverter>
<converters:AnyItemMeetsConditionConverter.Conditions>
<converters:ItemCategoryCondition Category="Mesh" />
</converters:AnyItemMeetsConditionConverter.Conditions>
</converters:AnyItemMeetsConditionConverter>
</Binding.Converter>
</Binding>
</MenuItem.IsEnabled>
<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" Command="{Binding RightClickMenuCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="Assets_Save_Animations" />
<Binding Path="PlacementTarget.SelectedItems" RelativeSource="{RelativeSource AncestorType=ContextMenu}" />
</MultiBinding>
</MenuItem.CommandParameter>
<MenuItem.IsEnabled>
<Binding Path="PlacementTarget.SelectedItems" RelativeSource="{RelativeSource AncestorType=ContextMenu}">
<Binding.Converter>
<converters:AnyItemMeetsConditionConverter>
<converters:AnyItemMeetsConditionConverter.Conditions>
<converters:ItemCategoryCondition Category="Animation" />
</converters:AnyItemMeetsConditionConverter.Conditions>
</converters:AnyItemMeetsConditionConverter>
</Binding.Converter>
</Binding>
</MenuItem.IsEnabled>
<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>
<MenuItem Header="Save Audio" Command="{Binding RightClickMenuCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="Assets_Save_Audio" />
<Binding Path="PlacementTarget.SelectedItems" RelativeSource="{RelativeSource AncestorType=ContextMenu}" />
</MultiBinding>
</MenuItem.CommandParameter>
<MenuItem.IsEnabled>
<Binding Path="PlacementTarget.SelectedItems" RelativeSource="{RelativeSource AncestorType=ContextMenu}">
<Binding.Converter>
<converters:AnyItemMeetsConditionConverter>
<converters:AnyItemMeetsConditionConverter.Conditions>
<converters:ItemCategoryCondition Category="Audio" />
</converters:AnyItemMeetsConditionConverter.Conditions>
</converters:AnyItemMeetsConditionConverter>
</Binding.Converter>
</Binding>
</MenuItem.IsEnabled>
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource AudioIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<Separator />
<MenuItem Header="Copy">
<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 Header="Package Path" Command="{Binding CopyCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="File_Path" />
<Binding Path="PlacementTarget.SelectedItems" RelativeSource="{RelativeSource AncestorType=ContextMenu}" />
</MultiBinding>
</MenuItem.CommandParameter>
</MenuItem>
<MenuItem Header="Package Name" Command="{Binding CopyCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="File_Name" />
<Binding Path="PlacementTarget.SelectedItems" RelativeSource="{RelativeSource AncestorType=ContextMenu}" />
</MultiBinding>
</MenuItem.CommandParameter>
</MenuItem>
<MenuItem Header="Directory Path" Command="{Binding CopyCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="Directory_Path" />
<Binding Path="PlacementTarget.SelectedItems" RelativeSource="{RelativeSource AncestorType=ContextMenu}" />
</MultiBinding>
</MenuItem.CommandParameter>
</MenuItem>
<MenuItem Header="Package Path w/o Extension" Command="{Binding CopyCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="File_Path_No_Extension" />
<Binding Path="PlacementTarget.SelectedItems" RelativeSource="{RelativeSource AncestorType=ContextMenu}" />
</MultiBinding>
</MenuItem.CommandParameter>
</MenuItem>
<MenuItem Header="Package Name w/o Extension" Command="{Binding CopyCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="File_Name_No_Extension" />
<Binding Path="PlacementTarget.SelectedItems" RelativeSource="{RelativeSource AncestorType=ContextMenu}" />
</MultiBinding>
</MenuItem.CommandParameter>
</MenuItem>
</MenuItem>
</ContextMenu>
</ResourceDictionary>

View File

@ -1,150 +0,0 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:adonisUi="clr-namespace:AdonisUI;assembly=AdonisUI"
xmlns:converters="clr-namespace:FModel.Views.Resources.Converters"
x:Class="FModel.Views.Resources.Controls.ContextMenus.FolderContextMenuDictionary">
<ContextMenu x:Key="FolderContextMenu" x:Shared="False"
Opened="FolderContextMenu_OnOpened">
<MenuItem Header="Export Folder's Packages Raw Data (.uasset)"
Command="{Binding RightClickMenuCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="Folders_Export_Data" />
<Binding Path="Tag"
RelativeSource="{RelativeSource AncestorType=ContextMenu}" />
</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 Folder's Packages Properties (.json)"
Command="{Binding RightClickMenuCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="Folders_Save_Properties" />
<Binding Path="Tag"
RelativeSource="{RelativeSource AncestorType=ContextMenu}" />
</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 Folder's Packages Textures"
Command="{Binding RightClickMenuCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="Folders_Save_Textures" />
<Binding Path="Tag"
RelativeSource="{RelativeSource AncestorType=ContextMenu}" />
</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="Save Folder's Packages Models"
Command="{Binding RightClickMenuCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="Folders_Save_Models" />
<Binding Path="Tag"
RelativeSource="{RelativeSource AncestorType=ContextMenu}" />
</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 Folder's Packages Animations"
Command="{Binding RightClickMenuCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="Folders_Save_Animations" />
<Binding Path="Tag"
RelativeSource="{RelativeSource AncestorType=ContextMenu}" />
</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>
<MenuItem Header="Save Folder's Packages Audio"
Command="{Binding RightClickMenuCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="Folders_Save_Audio" />
<Binding Path="Tag"
RelativeSource="{RelativeSource AncestorType=ContextMenu}" />
</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 AudioIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<Separator />
<MenuItem Header="Favorite Directory" Click="OnFavoriteDirectoryClick"
CommandParameter="{Binding Tag, RelativeSource={RelativeSource AncestorType=ContextMenu}}">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource DirectoriesAddIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Copy Directory Path" Click="OnCopyDirectoryPathClick"
CommandParameter="{Binding Tag, RelativeSource={RelativeSource AncestorType=ContextMenu}}">
<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>
</ContextMenu>
</ResourceDictionary>

View File

@ -1,70 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using FModel.Services;
using FModel.Settings;
using FModel.ViewModels;
namespace FModel.Views.Resources.Controls.ContextMenus;
public partial class FolderContextMenuDictionary
{
private ApplicationViewModel _applicationView => ApplicationService.ApplicationView;
public FolderContextMenuDictionary()
{
InitializeComponent();
}
private void FolderContextMenu_OnOpened(object sender, RoutedEventArgs e)
{
if (sender is not ContextMenu { PlacementTarget: FrameworkElement fe } menu)
return;
var listBox = FindAncestor<ListBox>(fe);
if (listBox != null)
{
menu.DataContext = listBox.DataContext;
menu.Tag = listBox.SelectedItems;
return;
}
var treeView = FindAncestor<TreeView>(fe);
if (treeView != null)
{
menu.DataContext = treeView.DataContext;
menu.Tag = new[] { treeView.SelectedItem }.ToList();
}
}
private static T FindAncestor<T>(DependencyObject current) where T : DependencyObject
{
while (current != null)
{
if (current is T t)
return t;
current = VisualTreeHelper.GetParent(current);
}
return null;
}
private void OnFavoriteDirectoryClick(object sender, RoutedEventArgs e)
{
if (sender is not MenuItem { CommandParameter: IEnumerable<object> list } || list.FirstOrDefault() is not TreeItem folder)
return;
_applicationView.CustomDirectories.Add(new CustomDirectory(folder.Header, folder.PathAtThisPoint));
FLogger.Append(ELog.Information, () =>
FLogger.Text($"Successfully saved '{folder.PathAtThisPoint}' as a new favorite directory", Constants.WHITE, true));
}
private void OnCopyDirectoryPathClick(object sender, RoutedEventArgs e)
{
if (sender is not MenuItem { CommandParameter: IEnumerable<object> list } || list.FirstOrDefault() is not TreeItem folder)
return;
Clipboard.SetText(folder.PathAtThisPoint);
}
}

View File

@ -9,6 +9,13 @@
WindowStartupLocation="CenterScreen" IconVisibility="Collapsed" ResizeMode="NoResize" SizeToContent="Width"
MinWidth="{Binding Source={x:Static SystemParameters.MaximizedPrimaryScreenWidth}, Converter={converters:RatioConverter}, ConverterParameter='0.30'}"
Height="{Binding Source={x:Static SystemParameters.MaximizedPrimaryScreenWidth}, Converter={converters:RatioConverter}, ConverterParameter='0.20'}">
<adonisControls:AdonisWindow.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="../Resources.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</adonisControls:AdonisWindow.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>

View File

@ -9,6 +9,13 @@
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="*"/>

View File

@ -4,13 +4,18 @@
xmlns:controls="clr-namespace:FModel.Views.Resources.Controls"
xmlns:adonisControls="clr-namespace:AdonisUI.Controls;assembly=AdonisUI"
WindowStartupLocation="CenterScreen" IconVisibility="Collapsed">
<adonisControls:AdonisWindow.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="../Resources.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</adonisControls:AdonisWindow.Resources>
<DockPanel HorizontalAlignment="Center" VerticalAlignment="Center">
<controls:MagnifierManager.Magnifier>
<controls:Magnifier Radius="150" ZoomFactor=".7" />
</controls:MagnifierManager.Magnifier>
<Border BorderBrush="#3b3d4a" BorderThickness="1" HorizontalAlignment="Center" VerticalAlignment="Center">
<Image x:Name="ImageCtrl" UseLayoutRounding="True" />
</Border>
<Image x:Name="ImageCtrl" UseLayoutRounding="True" />
</DockPanel>
</adonisControls:AdonisWindow>

View File

@ -1,41 +0,0 @@
<UserControl x:Class="FModel.Views.Resources.Controls.Inputs.SearchTextBox"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignWidth="300"
x:Name="Root"
xmlns:adonisUi="clr-namespace:AdonisUI;assembly=AdonisUI"
xmlns:adonisExtensions="clr-namespace:AdonisUI.Extensions;assembly=AdonisUI">
<Grid>
<Grid.ColumnDefinitions>
<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">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource SearchIcon}" />
</Canvas>
</Viewbox>
</Grid>
<TextBox x:Name="TextBox" Grid.Column="0" Grid.ColumnSpan="2" AcceptsTab="False" AcceptsReturn="False"
Text="{Binding Text, ElementName=Root, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
Padding="25 0 0 0" HorizontalAlignment="Stretch"
adonisExtensions:WatermarkExtension.Watermark="{Binding Watermark, ElementName=Root}" />
<Button Grid.Column="1" ToolTip="Clear Search Filter" Padding="5"
Click="OnClearButtonClick"
Style="{DynamicResource {x:Static adonisUi:Styles.ToolbarButton}}">
<Viewbox Width="16" Height="16" HorizontalAlignment="Center">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}"
Data="{StaticResource BackspaceIcon}"/>
</Canvas>
</Viewbox>
</Button>
</Grid>
</UserControl>

View File

@ -1,48 +0,0 @@
using System.Windows;
using System.Windows.Controls;
namespace FModel.Views.Resources.Controls.Inputs;
public partial class SearchTextBox : UserControl
{
public static readonly DependencyProperty TextProperty =
DependencyProperty.Register(nameof(Text), typeof(string), typeof(SearchTextBox),
new FrameworkPropertyMetadata(string.Empty, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
public static readonly DependencyProperty WatermarkProperty =
DependencyProperty.Register(nameof(Watermark), typeof(string), typeof(SearchTextBox),
new PropertyMetadata("Search by name..."));
public static readonly RoutedEvent ClearButtonClickEvent =
EventManager.RegisterRoutedEvent(nameof(ClearButtonClick), RoutingStrategy.Bubble,
typeof(RoutedEventHandler), typeof(SearchTextBox));
public string Text
{
get => (string)GetValue(TextProperty);
set => SetValue(TextProperty, value);
}
public string Watermark
{
get => (string)GetValue(WatermarkProperty);
set => SetValue(WatermarkProperty, value);
}
public event RoutedEventHandler ClearButtonClick
{
add => AddHandler(ClearButtonClickEvent, value);
remove => RemoveHandler(ClearButtonClickEvent, value);
}
public SearchTextBox()
{
InitializeComponent();
}
private void OnClearButtonClick(object sender, RoutedEventArgs e)
{
Text = string.Empty;
RaiseEvent(new RoutedEventArgs(ClearButtonClickEvent, this));
}
}

View File

@ -1,42 +0,0 @@
using System.Windows;
using System.Windows.Controls;
namespace FModel.Views.Resources.Controls;
public sealed class ListBoxItemBehavior
{
public static bool GetIsBroughtIntoViewWhenSelected(ListBoxItem listBoxItem)
{
return (bool) listBoxItem.GetValue(IsBroughtIntoViewWhenSelectedProperty);
}
public static void SetIsBroughtIntoViewWhenSelected(ListBoxItem listBoxItem, bool value)
{
listBoxItem.SetValue(IsBroughtIntoViewWhenSelectedProperty, value);
}
public static readonly DependencyProperty IsBroughtIntoViewWhenSelectedProperty =
DependencyProperty.RegisterAttached("IsBroughtIntoViewWhenSelected", typeof(bool), typeof(ListBoxItemBehavior),
new UIPropertyMetadata(false, OnIsBroughtIntoViewWhenSelectedChanged));
private static void OnIsBroughtIntoViewWhenSelectedChanged(DependencyObject depObj, DependencyPropertyChangedEventArgs e)
{
if (depObj is not ListBoxItem item)
return;
if (e.NewValue is not bool value)
return;
if (value)
item.Selected += OnListBoxItemSelected;
else
item.Selected -= OnListBoxItemSelected;
}
private static void OnListBoxItemSelected(object sender, RoutedEventArgs e)
{
if (e.OriginalSource is ListBoxItem item)
item.BringIntoView();
}
}

View File

@ -7,6 +7,13 @@
xmlns:adonisControls="clr-namespace:AdonisUI.Controls;assembly=AdonisUI"
WindowStartupLocation="CenterScreen" IconVisibility="Collapsed" PreviewKeyDown="OnPreviewKeyDown"
Width="{Binding Source={x:Static SystemParameters.MaximizedPrimaryScreenWidth}, Converter={converters:RatioConverter}, ConverterParameter='0.40'}">
<adonisControls:AdonisWindow.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="../Resources.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</adonisControls:AdonisWindow.Resources>
<DockPanel HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<avalonEdit:TextEditor x:Name="MyAvalonEditor" Background="{DynamicResource {x:Static adonisUi:Brushes.Layer3BackgroundBrush}}"
FontFamily="Consolas" FontSize="8pt" IsReadOnly="True" ShowLineNumbers="True" Foreground="#DAE5F2"

View File

@ -1,8 +1,8 @@
using System;
using System;
using System.Text.RegularExpressions;
using System.Windows.Input;
using System.Windows.Media;
using CUE4Parse.Utils;
using FModel.Extensions;
using FModel.ViewModels;
using ICSharpCode.AvalonEdit;
using ICSharpCode.AvalonEdit.Document;
@ -24,7 +24,7 @@ public partial class PropertiesPopout
MyAvalonEditor.Document = new TextDocument
{
Text = contextViewModel.Document.Text,
FileName = contextViewModel.Entry.PathWithoutExtension
FileName = contextViewModel.Directory + '/' + contextViewModel.Header.SubstringBeforeLast('.')
};
MyAvalonEditor.FontSize = contextViewModel.FontSize;
MyAvalonEditor.SyntaxHighlighting = contextViewModel.Highlighter;
@ -32,7 +32,6 @@ public partial class PropertiesPopout
MyAvalonEditor.TextArea.TextView.LinkTextBackgroundBrush = null;
MyAvalonEditor.TextArea.TextView.LinkTextForegroundBrush = Brushes.Cornsilk;
MyAvalonEditor.TextArea.TextView.ElementGenerators.Add(new GamePathElementGenerator());
MyAvalonEditor.TextArea.TextView.ElementGenerators.Add(new JumpElementGenerator());
MyAvalonEditor.TextArea.TextView.ElementGenerators.Add(new HexColorElementGenerator());
_manager = new JsonFoldingStrategies(MyAvalonEditor);
_manager.UpdateFoldings(MyAvalonEditor.Document);

View File

@ -6,7 +6,6 @@ using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Threading;
namespace FModel.Views.Resources.Controls;
@ -34,11 +33,6 @@ public class FLogger : ITextFormatter
private static readonly BrushConverter _brushConverter = new();
private static int _previous;
private const string _at = " at ";
private const char _dot = '.';
private const char _colon = ':';
private const string _gray = "#999";
public static void Append(ELog type, Action job)
{
Application.Current.Dispatcher.Invoke(delegate
@ -60,45 +54,6 @@ public class FLogger : ITextFormatter
}
job();
}, DispatcherPriority.Background);
}
public static void Append(Exception e)
{
Append(ELog.Error, () =>
{
if ((e.InnerException ?? e) is { TargetSite.DeclaringType: not null } exception)
{
if (exception.TargetSite.ToString() == "CUE4Parse.FileProvider.GameFile get_Item(System.String)")
{
Text(e.Message, Constants.WHITE, true);
}
else
{
var t = exception.GetType();
Text(t.Namespace + _dot, Constants.GRAY);
Text(t.Name, Constants.WHITE);
Text(_colon + " ", Constants.GRAY);
Text(exception.Message, Constants.RED, true);
Text(_at, _gray);
Text(exception.TargetSite.DeclaringType.FullName + _dot, Constants.GRAY);
Text(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;
}
Text("(" + string.Join(", ", parameters) + ")", Constants.GRAY, true);
}
}
else
{
Text(e.Message, Constants.WHITE, true);
}
});
}

View File

@ -1,81 +0,0 @@
<UserControl x:Class="FModel.Views.Resources.Controls.TiledExplorer.FileButton2"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d" d:DesignWidth="128" d:DesignHeight="192"
d:DataContext="{d:DesignInstance Type=vm:GameFileViewModel, IsDesignTimeCreatable=False}"
xmlns:vm="clr-namespace:FModel.ViewModels"
xmlns:converters="clr-namespace:FModel.Views.Resources.Converters"
xmlns:adonisUi="clr-namespace:AdonisUI;assembly=AdonisUI"
Width="128" Height="192"
Background="{DynamicResource {x:Static adonisUi:Brushes.Layer4BackgroundBrush}}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="2" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid Grid.Row="0" Height="128" Background="{DynamicResource {x:Static adonisUi:Brushes.Layer3BorderBrush}}">
<Image Stretch="Uniform" Source="{Binding PreviewImage}" />
<Path x:Name="FallbackIcon" Width="64" Stretch="Uniform">
<Path.Style>
<Style TargetType="{x:Type Path}">
<Setter Property="Visibility" Value="Collapsed" />
<Style.Triggers>
<DataTrigger Binding="{Binding PreviewImage}" Value="{x:Null}">
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
</Style>
</Path.Style>
<Path.Data>
<MultiBinding Converter="{x:Static converters:FileToGeometryConverter.Instance}">
<Binding Path="AssetCategory" />
<Binding Path="ResolvedAssetType" />
</MultiBinding>
</Path.Data>
<Path.Fill>
<MultiBinding Converter="{x:Static converters:FileToGeometryConverter.Instance}">
<Binding Path="AssetCategory" />
<Binding Path="ResolvedAssetType" />
</MultiBinding>
</Path.Fill>
</Path>
</Grid>
<Rectangle Grid.Row="1" Fill="{Binding Fill, ElementName=FallbackIcon, FallbackValue=Red}" />
<Grid Grid.Row="2" Margin="5">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="{Binding Asset.NameWithoutExtension, FallbackValue=Asset Name}"
FontSize="13" TextWrapping="Wrap" TextTrimming="CharacterEllipsis" FontWeight="DemiBold"
TextAlignment="Left" HorizontalAlignment="Left" VerticalAlignment="Top"
Foreground="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}"/>
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="5" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="{Binding ResolvedAssetType, FallbackValue=Asset Type}"
FontSize="9" TextWrapping="NoWrap" TextTrimming="CharacterEllipsis" FontWeight="Normal"
TextAlignment="Left" HorizontalAlignment="Left" VerticalAlignment="Bottom"
Foreground="{DynamicResource {x:Static adonisUi:Brushes.DisabledForegroundBrush}}" />
<TextBlock Grid.Column="2" Text="{Binding Asset.Size, Converter={x:Static converters:SizeToStringConverter.Instance}, FallbackValue=0 B}"
FontSize="9" TextWrapping="NoWrap" TextTrimming="CharacterEllipsis" FontWeight="Normal"
TextAlignment="Right" HorizontalAlignment="Right" VerticalAlignment="Bottom"
Foreground="{DynamicResource {x:Static adonisUi:Brushes.DisabledForegroundBrush}}" />
</Grid>
</Grid>
</Grid>
</UserControl>

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