Merge remote-tracking branch 'origin/dev' into feature/new-viewer

# Conflicts:
#	CUE4Parse
#	FModel/MainWindow.xaml
#	FModel/ViewModels/CUE4ParseViewModel.cs
#	FModel/Views/Snooper/Animations/Skeleton.cs
#	FModel/Views/Snooper/Models/SkeletalModel.cs
#	FModel/Views/Snooper/Models/StaticModel.cs
#	FModel/Views/Snooper/Renderer.cs
This commit is contained in:
Asval
2026-09-16 15:58:40 +02:00
22 changed files with 407 additions and 80 deletions

View File

@@ -32,6 +32,7 @@ public class BaseIcon : UCreator
// rarity // rarity
if (Object.TryGetValue(out FPackageIndex series, "Series")) GetSeries(series); if (Object.TryGetValue(out FPackageIndex series, "Series")) GetSeries(series);
else if (Object.TryGetValue(out FStructFallback componentContainer, "ComponentContainer")) GetSeries(componentContainer); else if (Object.TryGetValue(out FStructFallback componentContainer, "ComponentContainer")) GetSeries(componentContainer);
else if (Object.TryGetValue(out EFortRarity hackRarity, "HackRarity")) GetRarity(hackRarity);
else GetRarity(Object.GetOrDefault("Rarity", EFortRarity.Uncommon)); // default is uncommon else GetRarity(Object.GetOrDefault("Rarity", EFortRarity.Uncommon)); // default is uncommon
if (Object.TryGetValue(out FInstancedStruct[] dataList, "DataList")) if (Object.TryGetValue(out FInstancedStruct[] dataList, "DataList"))
@@ -48,7 +49,7 @@ public class BaseIcon : UCreator
Preview = preview; Preview = preview;
else if (Object.TryGetValue(out FPackageIndex itemDefinition, "HeroDefinition", "WeaponDefinition")) else if (Object.TryGetValue(out FPackageIndex itemDefinition, "HeroDefinition", "WeaponDefinition"))
Preview = Utils.GetBitmap(itemDefinition); Preview = Utils.GetBitmap(itemDefinition);
else if (Object.TryGetValue(out FSoftObjectPath largePreview, "LargePreviewImage", "EntryListIcon", "SmallPreviewImage", "BundleImage", "ItemDisplayAsset", "LargeIcon", "ToastIcon", "SmallIcon")) else if (Object.TryGetValue(out FSoftObjectPath largePreview, "LargePreviewImage", "EntryListIcon", "SmallPreviewImage", "BundleImage", "ItemDisplayAsset", "LargeIcon", "ToastIcon", "SmallIcon", "CartridgeIcon", "EffectIcon"))
Preview = Utils.GetBitmap(largePreview); Preview = Utils.GetBitmap(largePreview);
else if (Object.TryGetValue(out string s, "LargePreviewImage") && !string.IsNullOrEmpty(s)) else if (Object.TryGetValue(out string s, "LargePreviewImage") && !string.IsNullOrEmpty(s))
Preview = Utils.GetBitmap(s); Preview = Utils.GetBitmap(s);
@@ -66,9 +67,9 @@ public class BaseIcon : UCreator
} }
// text // text
if (Object.TryGetValue(out FText displayName, "DisplayName", "ItemName", "BundleName", "DefaultHeaderText", "UIDisplayName", "EntryName", "EventCalloutTitle")) if (Object.TryGetValue(out FText displayName, "DisplayName", "ItemName", "BundleName", "DefaultHeaderText", "UIDisplayName", "EntryName", "EventCalloutTitle", "KeyName", "HackDisplayName"))
DisplayName = displayName.Text; 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", "SetDescription", "BundleDescription", "GeneralDescription", "DefaultBodyText", "UIDescription", "UIDisplayDescription", "EntryDescription", "EventCalloutDescription", "KeyDescription", "HackDescription"))
Description = description.Text; Description = description.Text;
else if (Object.TryGetValue(out FText[] descriptions, "Description")) else if (Object.TryGetValue(out FText[] descriptions, "Description"))
Description = string.Join('\n', descriptions.Select(x => x.Text)); Description = string.Join('\n', descriptions.Select(x => x.Text));

View File

@@ -65,6 +65,7 @@ public class CreatorPackage : IDisposable
case "AthenaEmojiItemDefinition": case "AthenaEmojiItemDefinition":
case "AthenaItemWrapDefinition": case "AthenaItemWrapDefinition":
case "AthenaToyItemDefinition": case "AthenaToyItemDefinition":
case "FortHackData":
case "FortHeroType": case "FortHeroType":
case "FortTokenType": case "FortTokenType":
case "FortAbilityKit": case "FortAbilityKit":
@@ -72,7 +73,9 @@ public class CreatorPackage : IDisposable
case "FortMissionInfo": case "FortMissionInfo":
case "RewardGraphToken": case "RewardGraphToken":
case "JunoKnowledgeBundle": case "JunoKnowledgeBundle":
case "FortHackDataMinimal":
case "FortBannerTokenType": case "FortBannerTokenType":
case "FortHackKeyCoreData":
case "FortVariantTokenType": case "FortVariantTokenType":
case "FortDecoItemDefinition": case "FortDecoItemDefinition":
case "FortStatItemDefinition": case "FortStatItemDefinition":
@@ -88,6 +91,7 @@ public class CreatorPackage : IDisposable
case "SparksBassItemDefinition": case "SparksBassItemDefinition":
case "FortGadgetItemDefinition": case "FortGadgetItemDefinition":
case "AthenaCharmItemDefinition": case "AthenaCharmItemDefinition":
case "FortHackKeyItemDefinition":
case "FortPlaysetItemDefinition": case "FortPlaysetItemDefinition":
case "FortGiftBoxItemDefinition": case "FortGiftBoxItemDefinition":
case "FortOutpostItemDefinition": case "FortOutpostItemDefinition":
@@ -124,6 +128,7 @@ public class CreatorPackage : IDisposable
case "JunoRecipeBundleItemDefinition": case "JunoRecipeBundleItemDefinition":
case "FortHomebaseNodeItemDefinition": case "FortHomebaseNodeItemDefinition":
case "FortNeverPersistItemDefinition": case "FortNeverPersistItemDefinition":
case "FortHackCartridgeItemDefinition":
case "FortPlayerAugmentItemDefinition": case "FortPlayerAugmentItemDefinition":
case "FortSmartBuildingItemDefinition": case "FortSmartBuildingItemDefinition":
case "FortGiftBoxUnlockItemDefinition": case "FortGiftBoxUnlockItemDefinition":

View File

@@ -131,6 +131,7 @@ public enum EAssetCategory : uint
CustomizableObject = Mesh + 3, CustomizableObject = Mesh + 3,
NaniteDisplacedMesh = Mesh + 4, NaniteDisplacedMesh = Mesh + 4,
GeometryCollection = Mesh + 5, GeometryCollection = Mesh + 5,
ChaosClothAsset = Mesh + 6,
Texture = AssetCategoryExtensions.CategoryBase + (3 << 16), Texture = AssetCategoryExtensions.CategoryBase + (3 << 16),
Materials = AssetCategoryExtensions.CategoryBase + (4 << 16), Materials = AssetCategoryExtensions.CategoryBase + (4 << 16),
Material = Materials + 1, Material = Materials + 1,
@@ -167,6 +168,15 @@ public enum EAssetCategory : uint
DeltaForce = GameSpecific + 4, DeltaForce = GameSpecific + 4,
LegoBatman = GameSpecific + 5, LegoBatman = GameSpecific + 5,
ArcSys = GameSpecific + 6, ArcSys = GameSpecific + 6,
GothamKnights = GameSpecific + 7,
}
public enum EAssetFamily
{
None,
Criware,
Wwise,
FMod,
} }
public enum EUnluacMode public enum EUnluacMode

View File

@@ -11,7 +11,9 @@ public static class VisualTreeExtensions
{ {
if (current is T t) if (current is T t)
return t; return t;
current = VisualTreeHelper.GetParent(current); current = current is FrameworkContentElement contentElement
? contentElement.Parent
: VisualTreeHelper.GetParent(current);
} }
return null; return null;
} }

View File

@@ -820,6 +820,33 @@
<TextBlock VerticalAlignment="Center" Text="{Binding PendingRequest.DisplayText, Source={x:Static viewModels:SnooperViewModel.Instance}, Mode=OneWay, StringFormat={}{0}: double-click one}" /> <TextBlock VerticalAlignment="Center" Text="{Binding PendingRequest.DisplayText, Source={x:Static viewModels:SnooperViewModel.Instance}, Mode=OneWay, StringFormat={}{0}: double-click one}" />
</StatusBarItem> </StatusBarItem>
<StatusBarItem Margin="0 0 10 0"
Padding="0"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch"
Visibility="{Binding IsUpdateAvailable, Converter={StaticResource BoolToVisibilityConverter}}">
<Button HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Center"
Style="{StaticResource StatusBarButton}"
Background="{DynamicResource {x:Static adonisUi:Brushes.AlertBrush}}"
ToolTip="A new version of FModel is available. Click to view it."
Command="{Binding MenuCommand}"
CommandParameter="Help_Releases">
<StackPanel Orientation="Horizontal"
VerticalAlignment="Center">
<Viewbox Width="16"
Height="16">
<Canvas Width="24"
Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.AccentForegroundBrush}}"
Data="{StaticResource UpdateIcon}" />
</Canvas>
</Viewbox>
<TextBlock Margin="10 0 0 0"
Text="Update Available" />
</StackPanel>
</Button>
</StatusBarItem>
<StatusBarItem Margin="0 0 10 0" Padding="0" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch"> <StatusBarItem Margin="0 0 10 0" Padding="0" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch">
<Button x:Name="ExportSessionButton" <Button x:Name="ExportSessionButton"
HorizontalContentAlignment="Stretch" HorizontalContentAlignment="Stretch"

View File

@@ -131,6 +131,7 @@ public class FModelApiEndpoint : AbstractApiProvider
UserSettings.Default.LastUpdateCheck = DateTime.Now; UserSettings.Default.LastUpdateCheck = DateTime.Now;
var targetHash = ((CustomMandatory) args.Mandatory).CommitHash; var targetHash = ((CustomMandatory) args.Mandatory).CommitHash;
_applicationView.IsUpdateAvailable = targetHash != Constants.APP_COMMIT_ID;
if (targetHash == Constants.APP_COMMIT_ID) if (targetHash == Constants.APP_COMMIT_ID)
{ {
if (UserSettings.Default.ShowChangelog) if (UserSettings.Default.ShowChangelog)

View File

@@ -61,6 +61,13 @@ public class ApplicationViewModel : ViewModel
} }
} }
private bool _isUpdateAvailable;
public bool IsUpdateAvailable
{
get => _isUpdateAvailable;
internal set => SetProperty(ref _isUpdateAvailable, value);
}
private int _selectedLeftTabIndex; private int _selectedLeftTabIndex;
public int SelectedLeftTabIndex public int SelectedLeftTabIndex
{ {
@@ -139,7 +146,7 @@ public class ApplicationViewModel : ViewModel
public DirectorySettings AvoidEmptyGameDirectory(bool bAlreadyLaunched) public DirectorySettings AvoidEmptyGameDirectory(bool bAlreadyLaunched)
{ {
var gameDirectory = UserSettings.Default.GameDirectory; var gameDirectory = UserSettings.Default.GameDirectory;
if (!bAlreadyLaunched && UserSettings.Default.PerDirectory.TryGetValue(gameDirectory, out var currentDir)) if (!bAlreadyLaunched && GameSelectorViewModel.IsGameDirectoryAvailable(gameDirectory) && UserSettings.Default.PerDirectory.TryGetValue(gameDirectory, out var currentDir))
return currentDir; return currentDir;
Status.SetStatus(EStatusKind.Configuring); Status.SetStatus(EStatusKind.Configuring);

View File

@@ -14,9 +14,9 @@ using CSCore.CoreAudioAPI;
using CSCore.DSP; using CSCore.DSP;
using CSCore.SoundOut; using CSCore.SoundOut;
using CSCore.Streams; using CSCore.Streams;
using CUE4Parse.UE4.CriWare.Decoders; using CUE4Parse.UE4.Criware.Decoders;
using CUE4Parse.UE4.CriWare.Decoders.ADX; using CUE4Parse.UE4.Criware.Decoders.ADX;
using CUE4Parse.UE4.CriWare.Decoders.HCA; using CUE4Parse.UE4.Criware.Decoders.HCA;
using CUE4Parse.Utils; using CUE4Parse.Utils;
using FModel.Extensions; using FModel.Extensions;
using FModel.Framework; using FModel.Framework;

View File

@@ -10,6 +10,8 @@ using System.Text.RegularExpressions;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using AdonisUI.Controls; using AdonisUI.Controls;
using CUE4Parse_Conversion.Exporters;
using CUE4Parse_Conversion.Sounds;
using CUE4Parse; using CUE4Parse;
using CUE4Parse.Compression; using CUE4Parse.Compression;
using CUE4Parse.Encryption.Aes; using CUE4Parse.Encryption.Aes;
@@ -22,15 +24,17 @@ using CUE4Parse.GameTypes.AshEchoes.FileProvider;
using CUE4Parse.GameTypes.Borderlands3.Assets.Exports; using CUE4Parse.GameTypes.Borderlands3.Assets.Exports;
using CUE4Parse.GameTypes.Borderlands4.Assets.Exports; using CUE4Parse.GameTypes.Borderlands4.Assets.Exports;
using CUE4Parse.GameTypes.Borderlands4.Wwise; using CUE4Parse.GameTypes.Borderlands4.Wwise;
using CUE4Parse.GameTypes.Dawnwalker.Assets.Exports;
using CUE4Parse.GameTypes.DFHO.Assets.Objects; using CUE4Parse.GameTypes.DFHO.Assets.Objects;
using CUE4Parse.GameTypes.HonorOfKings.FileProvider; using CUE4Parse.GameTypes.HonorOfKings.FileProvider;
using CUE4Parse.GameTypes.KRD.Assets.Exports; using CUE4Parse.GameTypes.KRD.Assets.Exports;
using CUE4Parse.GameTypes.LegoBatman.Assets; using CUE4Parse.GameTypes.LegoBatman.Assets;
using CUE4Parse.GameTypes.LordOfMysteries.FileProvider; using CUE4Parse.GameTypes.LordOfMysteries.FileProvider;
using CUE4Parse.GameTypes.RocoKingdomWorld.Assets.Objects; using CUE4Parse.GameTypes.Tencent.RocoKingdomWorld.Assets.Objects;
using CUE4Parse.GameTypes.SMG.UE4.Assets.Exports.Wwise; using CUE4Parse.GameTypes.SMG.UE4.Assets.Exports.Wwise;
using CUE4Parse.GameTypes.SquareEnix.UE4.Assets.Exports; using CUE4Parse.GameTypes.SquareEnix.UE4.Assets.Exports;
using CUE4Parse.GameTypes.Theia.FileProvider; using CUE4Parse.GameTypes.Theia.FileProvider;
using CUE4Parse.GameTypes.WarnerBros.GothamKnights.Assets.Exports.Wwise;
using CUE4Parse.MappingsProvider; using CUE4Parse.MappingsProvider;
using CUE4Parse.MappingsProvider.Jmap; using CUE4Parse.MappingsProvider.Jmap;
using CUE4Parse.MappingsProvider.Usmap; using CUE4Parse.MappingsProvider.Usmap;
@@ -38,10 +42,11 @@ using CUE4Parse.UE4.AssetRegistry;
using CUE4Parse.UE4.Assets; using CUE4Parse.UE4.Assets;
using CUE4Parse.UE4.Assets.Exports; using CUE4Parse.UE4.Assets.Exports;
using CUE4Parse.UE4.Assets.Exports.Animation; using CUE4Parse.UE4.Assets.Exports.Animation;
using CUE4Parse.UE4.Assets.Exports.CriWare; using CUE4Parse.UE4.Assets.Exports.Criware;
using CUE4Parse.UE4.Assets.Exports.Engine;
using CUE4Parse.UE4.Assets.Exports.Fmod; using CUE4Parse.UE4.Assets.Exports.Fmod;
using CUE4Parse.UE4.Assets.Exports.GeometryCollection;
using CUE4Parse.UE4.Assets.Exports.Material; using CUE4Parse.UE4.Assets.Exports.Material;
using CUE4Parse.UE4.Assets.Exports.SkeletalMesh;
using CUE4Parse.UE4.Assets.Exports.Sound; using CUE4Parse.UE4.Assets.Exports.Sound;
using CUE4Parse.UE4.Assets.Exports.StaticMesh; using CUE4Parse.UE4.Assets.Exports.StaticMesh;
using CUE4Parse.UE4.Assets.Exports.Texture; using CUE4Parse.UE4.Assets.Exports.Texture;
@@ -49,8 +54,8 @@ using CUE4Parse.UE4.Assets.Exports.Verse;
using CUE4Parse.UE4.Assets.Exports.Wwise; using CUE4Parse.UE4.Assets.Exports.Wwise;
using CUE4Parse.UE4.Assets.Objects; using CUE4Parse.UE4.Assets.Objects;
using CUE4Parse.UE4.BinaryConfig; using CUE4Parse.UE4.BinaryConfig;
using CUE4Parse.UE4.CriWare; using CUE4Parse.UE4.Criware;
using CUE4Parse.UE4.CriWare.Readers; using CUE4Parse.UE4.Criware.Readers;
using CUE4Parse.UE4.FMod; using CUE4Parse.UE4.FMod;
using CUE4Parse.UE4.GameFeatures; using CUE4Parse.UE4.GameFeatures;
using CUE4Parse.UE4.IO; using CUE4Parse.UE4.IO;
@@ -1300,6 +1305,11 @@ public class CUE4ParseViewModel : ViewModel
TabControl.SelectedTab.AddImage(sourceFile.SubstringAfterLast('/'), false, bitmap, false, updateUi); TabControl.SelectedTab.AddImage(sourceFile.SubstringAfterLast('/'), false, bitmap, false, updateUi);
return false; return false;
} }
case UBiomesMaskTextureData when (isNone || saveTextures) && pointer.Object.Value is UBiomesMaskTextureData biomes && biomes.MaskData is { } maskData:
{
TabControl.SelectedTab.AddImage(biomes.Name, false, SKBitmap.Decode(maskData.Value), false, updateUi);
return false;
}
// Supermassive Games (for example - The Dark Pictures Anthology: House of Ashes etc.) // Supermassive Games (for example - The Dark Pictures Anthology: House of Ashes etc.)
case UExternalSource when (isNone || saveAudio) && pointer.Object.Value is UExternalSource externalSource: case UExternalSource when (isNone || saveAudio) && pointer.Object.Value is UExternalSource externalSource:
{ {
@@ -1506,12 +1516,37 @@ public class CUE4ParseViewModel : ViewModel
} }
return false; return false;
} }
// Gotham Knights
case UOrpheusBank when (isNone || saveAudio) && pointer.Object.Value is UOrpheusBank orpheusBank:
{
if (orpheusBank.SoundBank is null)
return false;
var extractedSounds = WwiseProvider.ExtractBankSounds(orpheusBank.SoundBank);
foreach (var sound in extractedSounds)
{
SaveAndPlaySound(cancellationToken, sound.OutputPath, sound.Extension, sound.Data?.GetData() ?? [], saveAudio, updateUi);
}
return false;
}
// Gotham Knights
case UOrpheusEvent when (isNone || saveAudio) && pointer.Object.Value is UOrpheusEvent orpheusEvent:
{
var extractedSounds = WwiseProvider.ExtractGothamKnightsAudioEventSounds(orpheusEvent);
foreach (var sound in extractedSounds)
{
SaveAndPlaySound(cancellationToken, sound.OutputPath, sound.Extension, sound.Data?.GetData() ?? [], saveAudio, updateUi);
}
return false;
}
case UWorld when isNone && UserSettings.Default.PreviewWorlds: case UWorld when isNone && UserSettings.Default.PreviewWorlds:
case UBlueprintGeneratedClass when isNone && UserSettings.Default.PreviewWorlds: case UBlueprintGeneratedClass when isNone && UserSettings.Default.PreviewWorlds:
// case UPaperSprite when isNone && UserSettings.Default.PreviewMaterials: // case UPaperSprite when isNone && UserSettings.Default.PreviewMaterials:
case UStaticMesh when isNone && UserSettings.Default.PreviewStaticMeshes: case UStaticMesh when isNone && UserSettings.Default.PreviewStaticMeshes:
case UGeometryCollection when isNone && UserSettings.Default.PreviewStaticMeshes: case UGeometryCollection when isNone && UserSettings.Default.PreviewStaticMeshes:
case USkeletalMesh when isNone && UserSettings.Default.PreviewSkeletalMeshes: case USkinnedAsset when isNone && UserSettings.Default.PreviewSkeletalMeshes:
case USkeleton when isNone && UserSettings.Default.SaveSkeletonAsMesh: case USkeleton when isNone && UserSettings.Default.SaveSkeletonAsMesh:
case UAnimationAsset when isNone && UserSettings.Default.PreviewAnimations: case UAnimationAsset when isNone && UserSettings.Default.PreviewAnimations:
// case UMaterialInstance when isNone && UserSettings.Default.PreviewMaterials && // case UMaterialInstance when isNone && UserSettings.Default.PreviewMaterials &&
@@ -1524,7 +1559,8 @@ public class CUE4ParseViewModel : ViewModel
return true; return true;
} }
case UStaticMesh when HasFlag(bulk, EBulkType.Meshes): case UStaticMesh when HasFlag(bulk, EBulkType.Meshes):
case USkeletalMesh when HasFlag(bulk, EBulkType.Meshes): case UGeometryCollection when HasFlag(bulk, EBulkType.Meshes):
case USkinnedAsset when HasFlag(bulk, EBulkType.Meshes):
case USkeleton when UserSettings.Default.SaveSkeletonAsMesh && HasFlag(bulk, EBulkType.Meshes): case USkeleton when UserSettings.Default.SaveSkeletonAsMesh && HasFlag(bulk, EBulkType.Meshes):
// case UMaterialInterface when HasFlag(bulk, EBulkType.Materials): // case UMaterialInterface when HasFlag(bulk, EBulkType.Materials):
case UAnimationAsset when HasFlag(bulk, EBulkType.Animations): case UAnimationAsset when HasFlag(bulk, EBulkType.Animations):

View File

@@ -148,7 +148,7 @@ public class LoadCommand : ViewModelCommand<LoadingModesViewModel>
{ {
entries.Add(asset); entries.Add(asset);
} }
else if (includeLooseFiles && asset is OsGameFile) else if (includeLooseFiles && asset is not VfsEntry)
{ {
entries.Add(asset); entries.Add(asset);
} }

View File

@@ -5,7 +5,7 @@ using System.Threading.Tasks;
using System.Windows; using System.Windows;
using System.Windows.Media; using System.Windows.Media;
using System.Windows.Media.Imaging; using System.Windows.Media.Imaging;
using CUE4Parse_Conversion.Textures;
using CUE4Parse.FileProvider.Objects; using CUE4Parse.FileProvider.Objects;
using CUE4Parse.GameTypes.Borderlands3.Assets.Exports; using CUE4Parse.GameTypes.Borderlands3.Assets.Exports;
using CUE4Parse.GameTypes.Borderlands4.Assets.Exports; using CUE4Parse.GameTypes.Borderlands4.Assets.Exports;
@@ -15,12 +15,15 @@ using CUE4Parse.GameTypes.RED.Assets.Exports;
using CUE4Parse.GameTypes.SMG.UE4.Assets.Exports.Wwise; using CUE4Parse.GameTypes.SMG.UE4.Assets.Exports.Wwise;
using CUE4Parse.GameTypes.SMG.UE4.Assets.Objects; using CUE4Parse.GameTypes.SMG.UE4.Assets.Objects;
using CUE4Parse.GameTypes.SquareEnix.UE4.Assets.Exports; using CUE4Parse.GameTypes.SquareEnix.UE4.Assets.Exports;
using CUE4Parse.GameTypes.WarnerBros.GothamKnights.Assets.Exports.Wwise;
using CUE4Parse.UE4.Assets; using CUE4Parse.UE4.Assets;
using CUE4Parse.UE4.Assets.Exports; using CUE4Parse.UE4.Assets.Exports;
using CUE4Parse.UE4.Assets.Exports.Animation; using CUE4Parse.UE4.Assets.Exports.Animation;
using CUE4Parse.UE4.Assets.Exports.BuildData; using CUE4Parse.UE4.Assets.Exports.BuildData;
using CUE4Parse.UE4.Assets.Exports.ChaosClothAsset;
using CUE4Parse.UE4.Assets.Exports.Component; using CUE4Parse.UE4.Assets.Exports.Component;
using CUE4Parse.UE4.Assets.Exports.CriWare; using CUE4Parse.UE4.Assets.Exports.Criware;
using CUE4Parse.UE4.Assets.Exports.Criware.Media;
using CUE4Parse.UE4.Assets.Exports.CustomizableObject; using CUE4Parse.UE4.Assets.Exports.CustomizableObject;
using CUE4Parse.UE4.Assets.Exports.Engine; using CUE4Parse.UE4.Assets.Exports.Engine;
using CUE4Parse.UE4.Assets.Exports.Engine.Font; using CUE4Parse.UE4.Assets.Exports.Engine.Font;
@@ -51,17 +54,11 @@ using CUE4Parse.UE4.Objects.UObject;
using CUE4Parse.UE4.Objects.UObject.Editor; using CUE4Parse.UE4.Objects.UObject.Editor;
using CUE4Parse.UE4.Versions; using CUE4Parse.UE4.Versions;
using CUE4Parse.Utils; using CUE4Parse.Utils;
using CUE4Parse_Conversion.Textures;
using FModel.Framework; using FModel.Framework;
using FModel.Services; using FModel.Services;
using FModel.Settings; using FModel.Settings;
using Serilog; using Serilog;
using SkiaSharp; using SkiaSharp;
using Svg.Skia; using Svg.Skia;
namespace FModel.ViewModels; namespace FModel.ViewModels;
@@ -101,6 +98,13 @@ public class GameFileViewModel(GameFile asset) : ViewModel
} }
} }
private EAssetFamily _assetFamily = EAssetFamily.None;
public EAssetFamily AssetFamily
{
get => _assetFamily;
private set => SetProperty(ref _assetFamily, value);
}
private EBulkType _assetActions = EBulkType.None; private EBulkType _assetActions = EBulkType.None;
public EBulkType AssetActions public EBulkType AssetActions
{ {
@@ -204,6 +208,20 @@ public class GameFileViewModel(GameFile asset) : ViewModel
var dummy = ((AbstractUePackage) pkg).ConstructObject(pointer.Class, pkg); var dummy = ((AbstractUePackage) pkg).ConstructObject(pointer.Class, pkg);
ResolvedAssetType = dummy.ExportType; ResolvedAssetType = dummy.ExportType;
AssetFamily = dummy switch
{
UAtomSoundBase or UAtomSoundBank or USoundAtomCue or USoundAtomCueSheet
or USoundAtomConfig or UAtomDspBusSetting or UAtomRackBase => EAssetFamily.Criware,
UAkAudioType or UAkAssetData or UAkAssetPlatformData or UAkMediaAssetData
or UAkMediaAsset or UWwiseAssetLibrary or UExternalSource or UExternalSourceBank => EAssetFamily.Wwise,
UFMODEvent or UFMODBank or UFMODBankLookup or UFMODBus
or UFMODSnapshot or UFMODSnapshotReverb or UFMODVCA => EAssetFamily.FMod,
_ => EAssetFamily.None,
};
(AssetCategory, AssetActions) = dummy switch (AssetCategory, AssetActions) = dummy switch
{ {
URigVMBlueprintGeneratedClass => (EAssetCategory.RigVMBlueprintGeneratedClass, EBulkType.Code), URigVMBlueprintGeneratedClass => (EAssetCategory.RigVMBlueprintGeneratedClass, EBulkType.Code),
@@ -217,9 +235,10 @@ public class GameFileViewModel(GameFile asset) : ViewModel
UStaticMesh => (EAssetCategory.StaticMesh, EBulkType.Meshes), UStaticMesh => (EAssetCategory.StaticMesh, EBulkType.Meshes),
USkeletalMesh => (EAssetCategory.SkeletalMesh, EBulkType.Meshes), USkeletalMesh => (EAssetCategory.SkeletalMesh, EBulkType.Meshes),
UChaosClothAsset => (EAssetCategory.ChaosClothAsset, EBulkType.Meshes),
UCustomizableObject => (EAssetCategory.CustomizableObject, EBulkType.None), UCustomizableObject => (EAssetCategory.CustomizableObject, EBulkType.None),
UNaniteDisplacedMesh => (EAssetCategory.NaniteDisplacedMesh, EBulkType.None), UNaniteDisplacedMesh => (EAssetCategory.NaniteDisplacedMesh, EBulkType.None),
UGeometryCollection => (EAssetCategory.GeometryCollection, EBulkType.None), UGeometryCollection => (EAssetCategory.GeometryCollection, EBulkType.Meshes),
UTexture => (EAssetCategory.Texture, EBulkType.Textures), UTexture => (EAssetCategory.Texture, EBulkType.Textures),
@@ -245,18 +264,23 @@ public class GameFileViewModel(GameFile asset) : ViewModel
UObjectRedirector => (EAssetCategory.ObjectRedirector, EBulkType.None), UObjectRedirector => (EAssetCategory.ObjectRedirector, EBulkType.None),
UPhysicalMaterial => (EAssetCategory.PhysicalMaterial, EBulkType.None), UPhysicalMaterial => (EAssetCategory.PhysicalMaterial, EBulkType.None),
USoundAtomCue or UAkAudioEvent or USoundCue or UFMODEvent USoundAtomCue or UAtomSoundCue or UAkAudioEvent or USoundCue or UFMODEvent
or UAkAssetData or UAkAssetPlatformData => (EAssetCategory.AudioEvent, EBulkType.Audio), or UAkAssetData or UAkAssetPlatformData => (EAssetCategory.AudioEvent, EBulkType.Audio),
UFMODBankLookup => (EAssetCategory.Data, EBulkType.None), UFileManaMovie => (EAssetCategory.Video, EBulkType.None),
UFMODBus or UFMODSnapshot or UFMODSnapshotReverb or UFMODVCA or USQEXSEADSoundAttenuation => (EAssetCategory.Audio, EBulkType.None), UFMODBankLookup or USoundAtomConfig or UFMODSnapshotReverb or UFMODBus or UFMODSnapshot
or UFMODVCA or UAtomDspBusSetting => (EAssetCategory.Data, EBulkType.None),
UFMODBank or UAkAudioBank or UAtomWaveBank or UAkInitBank or USQEXSEADSoundBank => (EAssetCategory.SoundBank, EBulkType.Audio), USQEXSEADSoundAttenuation => (EAssetCategory.Audio, EBulkType.None),
UWwiseAssetLibrary or USoundBase or UAkMediaAssetData or UAtomCueSheet UFMODBank or UAkAudioBank or UAtomSoundBank or USoundAtomCueSheet
or USoundAtomCueSheet or UAkAudioType or UExternalSource or UExternalSourceBank or UAkInitBank or USQEXSEADSoundBank => (EAssetCategory.SoundBank, EBulkType.Audio),
or UAkMediaAsset => (EAssetCategory.Audio, EBulkType.Audio),
UWwiseAssetLibrary or UAtomSoundBase or USoundBase or UAkMediaAssetData
or UExternalSource or UExternalSourceBank or UAkMediaAsset => (EAssetCategory.Audio, EBulkType.Audio),
UAkAudioType => (EAssetCategory.Data, EBulkType.None),
UFileMediaSource => (EAssetCategory.Video, EBulkType.None), UFileMediaSource => (EAssetCategory.Video, EBulkType.None),
UFont or UFontFace or USMGLocaleFontUMG => (EAssetCategory.Font, EBulkType.None), UFont or UFontFace or USMGLocaleFontUMG => (EAssetCategory.Font, EBulkType.None),
@@ -269,6 +293,7 @@ public class GameFileViewModel(GameFile asset) : ViewModel
UFaceFXAnimSet when GameVersion is EGame.GAME_Borderlands4 => (EAssetCategory.Borderlands, EBulkType.Audio), // Borderlands 4; UFaceFXAnimSet when GameVersion is EGame.GAME_Borderlands4 => (EAssetCategory.Borderlands, EBulkType.Audio), // Borderlands 4;
UWubAudioEvent or UWubDialogueEvent when GameVersion is EGame.GAME_LEGOBatmanLegacyoftheDarkKnight => (EAssetCategory.LegoBatman, EBulkType.Audio), // Lego Batman: Legacy of the Dark Knight; UWubAudioEvent or UWubDialogueEvent when GameVersion is EGame.GAME_LEGOBatmanLegacyoftheDarkKnight => (EAssetCategory.LegoBatman, EBulkType.Audio), // Lego Batman: Legacy of the Dark Knight;
UREDBinaryObject => (EAssetCategory.ArcSys, EBulkType.None), // Arc System Works games; UREDBinaryObject => (EAssetCategory.ArcSys, EBulkType.None), // Arc System Works games;
UOrpheusBank or UOrpheusEvent => (EAssetCategory.GothamKnights, EBulkType.Audio), // Gotham Knights;
_ => (EAssetCategory.All, EBulkType.None), _ => (EAssetCategory.All, EBulkType.None),
}; };
@@ -336,6 +361,15 @@ public class GameFileViewModel(GameFile asset) : ViewModel
{ {
Resolved |= EResolveCompute.Preview; Resolved |= EResolveCompute.Preview;
var lowercaseExtension = Asset.Extension.ToLowerInvariant(); var lowercaseExtension = Asset.Extension.ToLowerInvariant();
AssetFamily = lowercaseExtension switch
{
"acb" or "acf" or "adx" or "awb" or "hca" or "usm" => EAssetFamily.Criware,
"bnk" or "pck" or "wem" => EAssetFamily.Wwise,
"bank" or "fsb" => EAssetFamily.FMod,
_ => EAssetFamily.None,
};
switch (lowercaseExtension) switch (lowercaseExtension)
{ {
case "uproject": case "uproject":
@@ -365,6 +399,7 @@ public class GameFileViewModel(GameFile asset) : ViewModel
case "sql": case "sql":
case "py": case "py":
case "cs": case "cs":
case "acf":
AssetCategory = EAssetCategory.Data; AssetCategory = EAssetCategory.Data;
break; break;
case "stinfo": case "stinfo":
@@ -375,6 +410,8 @@ public class GameFileViewModel(GameFile asset) : ViewModel
break; break;
case "wav": case "wav":
case "awb": // This is technically soundbank and should be below but I want it to be distinguishable from "acb" case "awb": // This is technically soundbank and should be below but I want it to be distinguishable from "acb"
case "adx":
case "hca":
case "xvag": case "xvag":
case "flac": case "flac":
case "at9": case "at9":
@@ -385,6 +422,7 @@ public class GameFileViewModel(GameFile asset) : ViewModel
break; break;
case "acb": case "acb":
case "bank": case "bank":
case "fsb":
case "bnk": case "bnk":
case "pck": case "pck":
AssetCategory = EAssetCategory.SoundBank; AssetCategory = EAssetCategory.SoundBank;
@@ -395,7 +433,9 @@ public class GameFileViewModel(GameFile asset) : ViewModel
case "ttf": case "ttf":
AssetCategory = EAssetCategory.Font; AssetCategory = EAssetCategory.Font;
break; break;
case "webm":
case "mp4": case "mp4":
case "usm":
AssetCategory = EAssetCategory.Video; AssetCategory = EAssetCategory.Video;
break; break;
case "jpg": case "jpg":

View File

@@ -58,7 +58,7 @@ public class GameSelectorViewModel : ViewModel
if (DetectedDirectories.FirstOrDefault(x => x.GameDirectory == gameDirectory) is { } detectedGame) if (DetectedDirectories.FirstOrDefault(x => x.GameDirectory == gameDirectory) is { } detectedGame)
SelectedDirectory = detectedGame; SelectedDirectory = detectedGame;
else if (!string.IsNullOrEmpty(gameDirectory)) else if (IsGameDirectoryAvailable(gameDirectory))
AddUndetectedDir(gameDirectory); AddUndetectedDir(gameDirectory);
else else
SelectedDirectory = DetectedDirectories.FirstOrDefault(); SelectedDirectory = DetectedDirectories.FirstOrDefault();
@@ -189,6 +189,39 @@ public class GameSelectorViewModel : ViewModel
SelectedDirectory = DetectedDirectories.Last(); SelectedDirectory = DetectedDirectories.Last();
} }
public int ClearMissingDirectories()
{
var removedDirectories = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var missingDirectories = UserSettings.Default.PerDirectory
.Where(x => !IsGameDirectoryAvailable(x.Value.GameDirectory))
.Select(x => new KeyValuePair<string, string>(x.Key, x.Value.GameDirectory))
.ToArray();
foreach (var (key, gameDirectory) in missingDirectories)
{
UserSettings.Default.PerDirectory.Remove(key);
removedDirectories.Add(gameDirectory);
}
var missingDetectedDirectories = _detectedDirectories
.Where(x => !IsGameDirectoryAvailable(x.GameDirectory))
.ToArray();
foreach (var directory in missingDetectedDirectories)
{
_detectedDirectories.Remove(directory);
removedDirectories.Add(directory.GameDirectory);
}
if (SelectedDirectory is null || !_detectedDirectories.Contains(SelectedDirectory))
SelectedDirectory = DetectedDirectories.FirstOrDefault();
return removedDirectories.Count;
}
public static bool IsGameDirectoryAvailable(string gameDirectory)
=> gameDirectory is Constants._FN_LIVE_TRIGGER or Constants._VAL_LIVE_TRIGGER || Directory.Exists(gameDirectory);
private IEnumerable<EGame> EnumerateUeGames() private IEnumerable<EGame> EnumerateUeGames()
=> Enum.GetValues<EGame>() => Enum.GetValues<EGame>()
.GroupBy(value => (int)value) .GroupBy(value => (int)value)
@@ -196,8 +229,8 @@ public class GameSelectorViewModel : ViewModel
.OrderBy(value => ((int)value & 0xFF) == 0); .OrderBy(value => ((int)value & 0xFF) == 0);
private IEnumerable<DirectorySettings> EnumerateDetectedGames() private IEnumerable<DirectorySettings> EnumerateDetectedGames()
{ {
yield return GetUnrealEngineGame("Fortnite", "\\FortniteGame\\Content\\Paks", EGame.GAME_UE5_8); yield return GetUnrealEngineGame("Fortnite", "\\FortniteGame\\Content\\Paks", EGame.GAME_UE6_0);
yield return DirectorySettings.Default("Fortnite [LIVE]", Constants._FN_LIVE_TRIGGER, ue: EGame.GAME_UE5_8); yield return DirectorySettings.Default("Fortnite [LIVE]", Constants._FN_LIVE_TRIGGER, ue: EGame.GAME_UE6_0);
yield return GetUnrealEngineGame("Pewee", "\\RogueCompany\\Content\\Paks", EGame.GAME_RogueCompany); yield return GetUnrealEngineGame("Pewee", "\\RogueCompany\\Content\\Paks", EGame.GAME_RogueCompany);
yield return GetUnrealEngineGame("Rosemallow", "\\Indiana\\Content\\Paks", EGame.GAME_UE4_21); yield return GetUnrealEngineGame("Rosemallow", "\\Indiana\\Content\\Paks", EGame.GAME_UE4_21);
yield return GetUnrealEngineGame("Catnip", "\\OakGame\\Content\\Paks", EGame.GAME_Borderlands3); yield return GetUnrealEngineGame("Catnip", "\\OakGame\\Content\\Paks", EGame.GAME_Borderlands3);

View File

@@ -130,14 +130,18 @@
adonisExtensions:LayerExtension.IncreaseLayer="True"> adonisExtensions:LayerExtension.IncreaseLayer="True">
<Grid Margin="30, 12, 6, 12"> <Grid Margin="30, 12, 6, 12">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/> <ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/> <ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/> <ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<Button Grid.Column="1" MinWidth="78" Margin="0 0 12 0" IsDefault="True" IsCancel="False" <Button Grid.Column="0" MinWidth="78" Margin="0 0 12 0"
HorizontalAlignment="Left" VerticalAlignment="Bottom" Content="Clear"
ToolTip="Remove game directories that no longer exist" Click="OnClearDirectories" />
<Button Grid.Column="2" MinWidth="78" Margin="0 0 12 0" IsDefault="True" IsCancel="False"
HorizontalAlignment="Right" VerticalAlignment="Bottom" Content="OK" Click="OnClick" /> HorizontalAlignment="Right" VerticalAlignment="Bottom" Content="OK" Click="OnClick" />
<Button Grid.Column="2" MinWidth="78" Margin="0 0 12 0" IsDefault="False" IsCancel="True" <Button Grid.Column="3" MinWidth="78" Margin="0 0 12 0" IsDefault="False" IsCancel="True"
HorizontalAlignment="Right" VerticalAlignment="Bottom" Content="Cancel" /> HorizontalAlignment="Right" VerticalAlignment="Bottom" Content="Cancel" />
</Grid> </Grid>
</Border> </Border>

View File

@@ -1,6 +1,13 @@
using FModel.ViewModels; using FModel.ViewModels;
using FModel.Views.Resources.Controls;
using Ookii.Dialogs.Wpf; using Ookii.Dialogs.Wpf;
using Serilog;
using System.Windows; using System.Windows;
using MessageBox = AdonisUI.Controls.MessageBox;
using MessageBoxButtons = AdonisUI.Controls.MessageBoxButtons;
using MessageBoxImage = AdonisUI.Controls.MessageBoxImage;
using MessageBoxModel = AdonisUI.Controls.MessageBoxModel;
using MessageBoxResult = AdonisUI.Controls.MessageBoxResult;
namespace FModel.Views; namespace FModel.Views;
@@ -63,6 +70,37 @@ public partial class DirectorySelector
gameLauncherViewModel.DeleteSelectedGame(); gameLauncherViewModel.DeleteSelectedGame();
} }
private void OnClearDirectories(object sender, RoutedEventArgs e)
{
if (DataContext is not GameSelectorViewModel gameSelectorViewModel)
return;
var messageBox = new MessageBoxModel
{
Text = "Remove all saved game directories that no longer exist?",
Caption = "Clear Empty Directories",
Icon = MessageBoxImage.Question,
Buttons = MessageBoxButtons.YesNo(),
IsSoundEnabled = false
};
MessageBox.Show(messageBox);
if (messageBox.Result != MessageBoxResult.Yes)
return;
var removedCount = gameSelectorViewModel.ClearMissingDirectories();
var message = removedCount switch
{
0 => "No empty directories were found",
1 => "Removed 1 empty game directory",
_ => $"Removed {removedCount} empty game directories"
};
Log.Information("{Message}", message);
if (FLogger.Logger is not null) // When only directory selector is open, FLogger.Logger is null
FLogger.Append(ELog.Information, () => FLogger.Text(message, Constants.WHITE, true));
}
public void AddManualGame(string directory) public void AddManualGame(string directory)
{ {
ManualGameExpander.IsExpanded = true; ManualGameExpander.IsExpanded = true;

View File

@@ -11,6 +11,8 @@
<SolidColorBrush x:Key="UserDefinedEnumBrush" Color="DarkGoldenrod" /> <SolidColorBrush x:Key="UserDefinedEnumBrush" Color="DarkGoldenrod" />
<SolidColorBrush x:Key="UserDefinedStructBrush" Color="Tan" /> <SolidColorBrush x:Key="UserDefinedStructBrush" Color="Tan" />
<SolidColorBrush x:Key="GroomAssetBrush" Color="RosyBrown " />
<SolidColorBrush x:Key="ChaosClothBrush" Color="LightSlateGray" />
<SolidColorBrush x:Key="CustomizableObjectBrush" Color="DarkRed" /> <SolidColorBrush x:Key="CustomizableObjectBrush" Color="DarkRed" />
<SolidColorBrush x:Key="NaniteDisplacedMeshBrush" Color="ForestGreen" /> <SolidColorBrush x:Key="NaniteDisplacedMeshBrush" Color="ForestGreen" />
@@ -22,7 +24,7 @@
<SolidColorBrush x:Key="BinaryBrush" Color="Yellow" /> <SolidColorBrush x:Key="BinaryBrush" Color="Yellow" />
<SolidColorBrush x:Key="TextureBrush" Color="MediumPurple" /> <SolidColorBrush x:Key="TextureBrush" Color="MediumPurple" />
<SolidColorBrush x:Key="ConfigBrush" Color="LightSlateGray" /> <SolidColorBrush x:Key="ConfigBrush" Color="LightSlateGray" />
<SolidColorBrush x:Key="AudioBrush" Color="MediumSeaGreen" /> <SolidColorBrush x:Key="AudioBrush" Color="MediumSeaGreen" />
<SolidColorBrush x:Key="SoundBankBrush" Color="DarkSeaGreen" /> <SolidColorBrush x:Key="SoundBankBrush" Color="DarkSeaGreen" />
<SolidColorBrush x:Key="AudioEventBrush" Color="LightSeaGreen" /> <SolidColorBrush x:Key="AudioEventBrush" Color="LightSeaGreen" />
@@ -52,6 +54,11 @@
<SolidColorBrush x:Key="PythonBrush" Color="#3d77a8" /> <SolidColorBrush x:Key="PythonBrush" Color="#3d77a8" />
<SolidColorBrush x:Key="CSharpBrush" Color="#9e559a" /> <SolidColorBrush x:Key="CSharpBrush" Color="#9e559a" />
<!-- Asset family brushes -->
<SolidColorBrush x:Key="CriwareBrush" Color="#005bab" />
<SolidColorBrush x:Key="WwiseBrush" Color="#0152a1" />
<SolidColorBrush x:Key="FModBrush" Color="#00758f" />
<!-- For specific games --> <!-- For specific games -->
<SolidColorBrush x:Key="BorderlandsBrush" Color="Yellow"></SolidColorBrush> <SolidColorBrush x:Key="BorderlandsBrush" Color="Yellow"></SolidColorBrush>
<SolidColorBrush x:Key="AionBrush" Color="DeepSkyBlue"></SolidColorBrush> <SolidColorBrush x:Key="AionBrush" Color="DeepSkyBlue"></SolidColorBrush>

View File

@@ -44,12 +44,14 @@
<MultiBinding Converter="{x:Static converters:FileToGeometryConverter.Instance}"> <MultiBinding Converter="{x:Static converters:FileToGeometryConverter.Instance}">
<Binding Path="AssetCategory" /> <Binding Path="AssetCategory" />
<Binding Path="ResolvedAssetType" /> <Binding Path="ResolvedAssetType" />
<Binding Path="AssetFamily" />
</MultiBinding> </MultiBinding>
</Path.Data> </Path.Data>
<Path.Fill> <Path.Fill>
<MultiBinding Converter="{x:Static converters:FileToGeometryConverter.Instance}"> <MultiBinding Converter="{x:Static converters:FileToGeometryConverter.Instance}">
<Binding Path="AssetCategory" /> <Binding Path="AssetCategory" />
<Binding Path="ResolvedAssetType" /> <Binding Path="ResolvedAssetType" />
<Binding Path="AssetFamily" />
</MultiBinding> </MultiBinding>
</Path.Fill> </Path.Fill>
</Path> </Path>

View File

@@ -35,12 +35,14 @@
<MultiBinding Converter="{x:Static converters:FileToGeometryConverter.Instance}"> <MultiBinding Converter="{x:Static converters:FileToGeometryConverter.Instance}">
<Binding Path="AssetCategory" /> <Binding Path="AssetCategory" />
<Binding Path="ResolvedAssetType" /> <Binding Path="ResolvedAssetType" />
<Binding Path="AssetFamily" />
</MultiBinding> </MultiBinding>
</Path.Data> </Path.Data>
<Path.Fill> <Path.Fill>
<MultiBinding Converter="{x:Static converters:FileToGeometryConverter.Instance}"> <MultiBinding Converter="{x:Static converters:FileToGeometryConverter.Instance}">
<Binding Path="AssetCategory" /> <Binding Path="AssetCategory" />
<Binding Path="ResolvedAssetType" /> <Binding Path="ResolvedAssetType" />
<Binding Path="AssetFamily" />
</MultiBinding> </MultiBinding>
</Path.Fill> </Path.Fill>
</Path> </Path>

View File

@@ -5,6 +5,7 @@ using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows.Data; using System.Windows.Data;
using System.Windows.Input; using System.Windows.Input;
using System.Windows.Threading;
namespace FModel.Views.Resources.Controls; namespace FModel.Views.Resources.Controls;
@@ -17,8 +18,9 @@ public class FilterableComboBox : ComboBox
/// If true, on lost focus or enter key pressed, checks the text in the combobox. If the text is not present /// If true, on lost focus or enter key pressed, checks the text in the combobox. If the text is not present
/// in the list, it leaves it blank. /// in the list, it leaves it blank.
/// </summary> /// </summary>
public bool OnlyValuesInList { public bool OnlyValuesInList
get => (bool)GetValue(OnlyValuesInListProperty); {
get => (bool) GetValue(OnlyValuesInListProperty);
set => SetValue(OnlyValuesInListProperty, value); set => SetValue(OnlyValuesInListProperty, value);
} }
public static readonly DependencyProperty OnlyValuesInListProperty = public static readonly DependencyProperty OnlyValuesInListProperty =
@@ -27,8 +29,9 @@ public class FilterableComboBox : ComboBox
/// <summary> /// <summary>
/// Selected item, changes only on lost focus or enter key pressed /// Selected item, changes only on lost focus or enter key pressed
/// </summary> /// </summary>
public object EffectivelySelectedItem { public object EffectivelySelectedItem
get => (bool)GetValue(EffectivelySelectedItemProperty); {
get => (bool) GetValue(EffectivelySelectedItemProperty);
set => SetValue(EffectivelySelectedItemProperty, value); set => SetValue(EffectivelySelectedItemProperty, value);
} }
public static readonly DependencyProperty EffectivelySelectedItemProperty = public static readonly DependencyProperty EffectivelySelectedItemProperty =
@@ -38,6 +41,7 @@ public class FilterableComboBox : ComboBox
private bool TextBoxFreezed; private bool TextBoxFreezed;
protected TextBox EditableTextBox => GetTemplateChild("PART_EditableTextBox") as TextBox; protected TextBox EditableTextBox => GetTemplateChild("PART_EditableTextBox") as TextBox;
private UserChange<bool> IsDropDownOpenUC; private UserChange<bool> IsDropDownOpenUC;
private readonly DispatcherTimer FilterTimer;
/// <summary> /// <summary>
/// Triggers on lost focus or enter key pressed, if the selected item changed since the last time focus was lost or enter was pressed. /// Triggers on lost focus or enter key pressed, if the selected item changed since the last time focus was lost or enter was pressed.
@@ -47,6 +51,15 @@ public class FilterableComboBox : ComboBox
public FilterableComboBox() public FilterableComboBox()
{ {
IsDropDownOpenUC = new UserChange<bool>(v => IsDropDownOpen = v); IsDropDownOpenUC = new UserChange<bool>(v => IsDropDownOpen = v);
FilterTimer = new DispatcherTimer(DispatcherPriority.Background, Dispatcher)
{
Interval = TimeSpan.FromMilliseconds(125)
};
FilterTimer.Tick += (_, _) =>
{
FilterTimer.Stop();
RefreshFilter();
};
DropDownOpened += FilteredComboBox_DropDownOpened; DropDownOpened += FilteredComboBox_DropDownOpened;
Focusable = true; Focusable = true;
@@ -55,7 +68,8 @@ public class FilterableComboBox : ComboBox
StaysOpenOnEdit = true; StaysOpenOnEdit = true;
IsReadOnly = false; IsReadOnly = false;
Loaded += (s, e) => { Loaded += (s, e) =>
{
if (EditableTextBox != null) if (EditableTextBox != null)
new TextBoxBaseUserChangeTracker(EditableTextBox).UserTextChanged += FilteredComboBox_UserTextChange; new TextBoxBaseUserChangeTracker(EditableTextBox).UserTextChanged += FilteredComboBox_UserTextChange;
}; };
@@ -67,25 +81,53 @@ public class FilterableComboBox : ComboBox
protected override void OnPreviewKeyDown(KeyEventArgs e) protected override void OnPreviewKeyDown(KeyEventArgs e)
{ {
if (IsDropDownOpen && Keyboard.Modifiers == ModifierKeys.None && e.Key is Key.Up or Key.Down)
{
MoveSelection(e.Key);
e.Handled = true;
return;
}
base.OnPreviewKeyDown(e); base.OnPreviewKeyDown(e);
if (e.Key == Key.Down && !IsDropDownOpen) { if (e.Key == Key.Down && !IsDropDownOpen)
{
ApplyPendingFilter();
IsDropDownOpen = true; IsDropDownOpen = true;
e.Handled = true; e.Handled = true;
} }
else if (e.Key == Key.Escape) { else if (e.Key == Key.Escape)
{
ClearFilter(); ClearFilter();
Text = ""; Text = "";
IsDropDownOpen = true; IsDropDownOpen = true;
} }
else if (e.Key == Key.Enter || e.Key == Key.Tab) { else if (e.Key == Key.Enter || e.Key == Key.Tab)
{
ApplyPendingFilter();
CheckSelectedItem(); CheckSelectedItem();
TriggerSelectedItemChanged(); TriggerSelectedItemChanged();
} }
} }
private void MoveSelection(Key key)
{
ApplyPendingFilter();
if (Items.Count == 0)
return;
var index = SelectedItem == null ? -1 : Items.IndexOf(SelectedItem);
index = index < 0
? key == Key.Up ? Items.Count - 1 : 0
: Math.Clamp(index + (key == Key.Up ? -1 : 1), 0, Items.Count - 1);
SelectedItem = Items[index];
(ItemContainerGenerator.ContainerFromIndex(index) as ComboBoxItem)?.BringIntoView();
}
protected override void OnPreviewLostKeyboardFocus(KeyboardFocusChangedEventArgs e) protected override void OnPreviewLostKeyboardFocus(KeyboardFocusChangedEventArgs e)
{ {
base.OnPreviewLostKeyboardFocus(e); base.OnPreviewLostKeyboardFocus(e);
ApplyPendingFilter();
CheckSelectedItem(); CheckSelectedItem();
if ((e.OldFocus == this || e.OldFocus == EditableTextBox) && e.NewFocus != this && e.NewFocus != EditableTextBox) if ((e.OldFocus == this || e.OldFocus == EditableTextBox) && e.NewFocus != this && e.NewFocus != EditableTextBox)
TriggerSelectedItemChanged(); TriggerSelectedItemChanged();
@@ -100,7 +142,8 @@ public class FilterableComboBox : ComboBox
private bool shouldTriggerSelectedItemChanged = false; private bool shouldTriggerSelectedItemChanged = false;
private void TriggerSelectedItemChanged() private void TriggerSelectedItemChanged()
{ {
if (shouldTriggerSelectedItemChanged) { if (shouldTriggerSelectedItemChanged)
{
SelectionEffectivelyChanged?.Invoke(this, SelectedItem); SelectionEffectivelyChanged?.Invoke(this, SelectedItem);
shouldTriggerSelectedItemChanged = false; shouldTriggerSelectedItemChanged = false;
} }
@@ -108,9 +151,12 @@ public class FilterableComboBox : ComboBox
public void ClearFilter() public void ClearFilter()
{ {
if (string.IsNullOrEmpty(CurrentFilter)) return; FilterTimer.Stop();
if (string.IsNullOrEmpty(CurrentFilter))
return;
CurrentFilter = ""; CurrentFilter = "";
CollectionViewSource.GetDefaultView(ItemsSource).Refresh(); if (ItemsSource != null)
CollectionViewSource.GetDefaultView(ItemsSource).Refresh();
} }
private void FilteredComboBox_DropDownOpened(object sender, EventArgs e) private void FilteredComboBox_DropDownOpened(object sender, EventArgs e)
@@ -121,47 +167,64 @@ public class FilterableComboBox : ComboBox
private void FilteredComboBox_UserTextChange(object sender, EventArgs e) private void FilteredComboBox_UserTextChange(object sender, EventArgs e)
{ {
if (TextBoxFreezed) return; if (TextBoxFreezed)
return;
var tb = EditableTextBox; var tb = EditableTextBox;
if (tb.SelectionStart + tb.SelectionLength == tb.Text.Length) if (tb.SelectionStart + tb.SelectionLength == tb.Text.Length)
CurrentFilter = tb.Text.Substring(0, tb.SelectionStart).ToLower(); CurrentFilter = tb.Text.Substring(0, tb.SelectionStart);
else else
CurrentFilter = tb.Text.ToLower(); CurrentFilter = tb.Text;
RefreshFilter();
FilterTimer.Stop();
FilterTimer.Start();
} }
protected override void OnItemsSourceChanged(IEnumerable oldValue, IEnumerable newValue) protected override void OnItemsSourceChanged(IEnumerable oldValue, IEnumerable newValue)
{ {
if (newValue != null) { FilterTimer.Stop();
if (newValue != null)
{
var view = CollectionViewSource.GetDefaultView(newValue); var view = CollectionViewSource.GetDefaultView(newValue);
view.Filter += FilterItem; view.Filter += FilterItem;
} }
if (oldValue != null) { if (oldValue != null)
{
var view = CollectionViewSource.GetDefaultView(oldValue); var view = CollectionViewSource.GetDefaultView(oldValue);
if (view != null) view.Filter -= FilterItem; if (view != null)
view.Filter -= FilterItem;
} }
base.OnItemsSourceChanged(oldValue, newValue); base.OnItemsSourceChanged(oldValue, newValue);
} }
private void ApplyPendingFilter()
{
if (!FilterTimer.IsEnabled)
return;
FilterTimer.Stop();
RefreshFilter();
}
private void RefreshFilter() private void RefreshFilter()
{ {
if (ItemsSource == null) return; if (ItemsSource == null)
return;
var view = CollectionViewSource.GetDefaultView(ItemsSource); var view = CollectionViewSource.GetDefaultView(ItemsSource);
FreezTextBoxState(() => { FreezTextBoxState(() =>
var isDropDownOpen = IsDropDownOpen; {
//always hide because showing it enables the user to pick with up and down keys, otherwise it's not working because of the glitch in view.Refresh()
IsDropDownOpenUC.Set(false);
view.Refresh(); view.Refresh();
if (!string.IsNullOrEmpty(CurrentFilter) || isDropDownOpen) if (!IsDropDownOpen && !string.IsNullOrEmpty(CurrentFilter))
IsDropDownOpenUC.Set(true); IsDropDownOpenUC.Set(true);
if (SelectedItem == null) { if (SelectedItem == null)
{
foreach (var itm in ItemsSource) foreach (var itm in ItemsSource)
if (itm.ToString() == Text) { if (itm.ToString() == Text)
{
SelectedItem = itm; SelectedItem = itm;
break; break;
} }
@@ -185,10 +248,16 @@ public class FilterableComboBox : ComboBox
private bool FilterItem(object value) private bool FilterItem(object value)
{ {
if (value == null) return false; if (value == null)
if (CurrentFilter.Length == 0) return true; return false;
if (CurrentFilter.Length == 0)
return true;
return value.ToString().ToLower().Contains(CurrentFilter); var text = value.ToString();
if (text?.StartsWith("GAME_", StringComparison.OrdinalIgnoreCase) == true)
text = text[5..];
return text?.Contains(CurrentFilter, StringComparison.OrdinalIgnoreCase) == true;
} }
private class TextBoxBaseUserChangeTracker private class TextBoxBaseUserChangeTracker
@@ -205,11 +274,13 @@ public class FilterableComboBox : ComboBox
TextBoxBase = textBoxBase; TextBoxBase = textBoxBase;
LastText = TextBoxBase.ToString(); LastText = TextBoxBase.ToString();
textBoxBase.PreviewTextInput += (s, e) => { textBoxBase.PreviewTextInput += (s, e) =>
{
IsTextInput = true; IsTextInput = true;
}; };
textBoxBase.TextChanged += (s, e) => { textBoxBase.TextChanged += (s, e) =>
{
var isUserChange = PressedKeys.Count > 0 || IsTextInput || LastText == TextBoxBase.ToString(); var isUserChange = PressedKeys.Count > 0 || IsTextInput || LastText == TextBoxBase.ToString();
IsTextInput = false; IsTextInput = false;
LastText = TextBoxBase.ToString(); LastText = TextBoxBase.ToString();
@@ -217,17 +288,21 @@ public class FilterableComboBox : ComboBox
UserTextChanged?.Invoke(this, e); UserTextChanged?.Invoke(this, e);
}; };
textBoxBase.PreviewKeyDown += (s, e) => { textBoxBase.PreviewKeyDown += (s, e) =>
switch (e.Key) { {
switch (e.Key)
{
case Key.Back: case Key.Back:
case Key.Space: case Key.Space:
if (!PressedKeys.Contains(e.Key)) if (!PressedKeys.Contains(e.Key))
PressedKeys.Add(e.Key); PressedKeys.Add(e.Key);
break; break;
} }
if (e.Key == Key.Back) { if (e.Key == Key.Back)
{
var textBox = textBoxBase as TextBox; var textBox = textBoxBase as TextBox;
if (textBox.SelectionStart > 0 && textBox.SelectionLength > 0 && (textBox.SelectionStart + textBox.SelectionLength) == textBox.Text.Length) { if (textBox.SelectionStart > 0 && textBox.SelectionLength > 0 && (textBox.SelectionStart + textBox.SelectionLength) == textBox.Text.Length)
{
textBox.SelectionStart--; textBox.SelectionStart--;
textBox.SelectionLength++; textBox.SelectionLength++;
e.Handled = true; e.Handled = true;
@@ -236,12 +311,14 @@ public class FilterableComboBox : ComboBox
} }
}; };
textBoxBase.PreviewKeyUp += (s, e) => { textBoxBase.PreviewKeyUp += (s, e) =>
{
if (PressedKeys.Contains(e.Key)) if (PressedKeys.Contains(e.Key))
PressedKeys.Remove(e.Key); PressedKeys.Remove(e.Key);
}; };
textBoxBase.LostFocus += (s, e) => { textBoxBase.LostFocus += (s, e) =>
{
PressedKeys.Clear(); PressedKeys.Clear();
IsTextInput = false; IsTextInput = false;
}; };
@@ -261,11 +338,13 @@ public class FilterableComboBox : ComboBox
public void Set(T val) public void Set(T val)
{ {
try { try
{
IsUserChange = false; IsUserChange = false;
action(val); action(val);
} }
finally { finally
{
IsUserChange = true; IsUserChange = true;
} }
} }

View File

@@ -12,7 +12,7 @@ public class FileToGeometryConverter : IMultiValueConverter
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{ {
if (values.Length < 2 || values[0] is not EAssetCategory category || values[1] is not string resolvedAssetType) if (values.Length < 3 || values[0] is not EAssetCategory category || values[1] is not string resolvedAssetType || values[2] is not EAssetFamily family)
return null; return null;
resolvedAssetType = resolvedAssetType.ToLowerInvariant(); resolvedAssetType = resolvedAssetType.ToLowerInvariant();
@@ -32,6 +32,7 @@ public class FileToGeometryConverter : IMultiValueConverter
EAssetCategory.StaticMesh => ("StaticMeshIconAlt", "NeutralBrush"), EAssetCategory.StaticMesh => ("StaticMeshIconAlt", "NeutralBrush"),
EAssetCategory.SkeletalMesh => ("SkeletalMeshIconAlt", "NeutralBrush"), EAssetCategory.SkeletalMesh => ("SkeletalMeshIconAlt", "NeutralBrush"),
EAssetCategory.ChaosClothAsset => ("ChaosClothIcon", "ChaosClothBrush"),
EAssetCategory.CustomizableObject => ("StaticMeshIconAlt", "CustomizableObjectBrush"), EAssetCategory.CustomizableObject => ("StaticMeshIconAlt", "CustomizableObjectBrush"),
EAssetCategory.NaniteDisplacedMesh => ("StaticMeshIconAlt", "NaniteDisplacedMeshBrush"), EAssetCategory.NaniteDisplacedMesh => ("StaticMeshIconAlt", "NaniteDisplacedMeshBrush"),
EAssetCategory.GeometryCollection => ("GeometryCollectionIcon", "NeutralBrush"), EAssetCategory.GeometryCollection => ("GeometryCollectionIcon", "NeutralBrush"),
@@ -99,12 +100,26 @@ public class FileToGeometryConverter : IMultiValueConverter
EAssetCategory.Aion2 => ("AionIcon", "AionBrush"), EAssetCategory.Aion2 => ("AionIcon", "AionBrush"),
EAssetCategory.RocoKingdomWorld => ("RocoKingdomWorldIcon", "RocoKingdomWorldBrush"), EAssetCategory.RocoKingdomWorld => ("RocoKingdomWorldIcon", "RocoKingdomWorldBrush"),
EAssetCategory.DeltaForce => ("DeltaForceIcon", "DeltaForceBrush"), EAssetCategory.DeltaForce => ("DeltaForceIcon", "DeltaForceBrush"),
EAssetCategory.LegoBatman => ("BatmanIcon", "BatmanBrush"), EAssetCategory.LegoBatman or EAssetCategory.GothamKnights => ("BatmanIcon", "BatmanBrush"),
EAssetCategory.ArcSys => ("ArcSysIcon", "ArcSysBrush"), EAssetCategory.ArcSys => ("ArcSysIcon", "ArcSysBrush"),
_ => ("AssetIcon", "NeutralBrush") _ => ("AssetIcon", "NeutralBrush")
}; };
(geometry, brush) = (family, category) switch
{
(EAssetFamily.Criware, EAssetCategory.Data) => ("CriwareIcon", "CriwareBrush"),
(EAssetFamily.Criware, _) => ("CriwareIcon", brush),
(EAssetFamily.Wwise, EAssetCategory.Data) => ("WwiseIcon", "WwiseBrush"),
(EAssetFamily.Wwise, _) => ("WwiseIcon", brush),
(EAssetFamily.FMod, EAssetCategory.Data) => ("FModIcon", "FModBrush"),
(EAssetFamily.FMod, _) => ("FModIcon", brush),
_ => (geometry, brush),
};
if (targetType == typeof(Geometry)) if (targetType == typeof(Geometry))
return Application.Current.FindResource(geometry) as Geometry; return Application.Current.FindResource(geometry) as Geometry;
if (targetType == typeof(Brush)) if (targetType == typeof(Brush))

File diff suppressed because one or more lines are too long

View File

@@ -1682,6 +1682,16 @@
<Style x:Key="UComboBox" TargetType="ComboBox" BasedOn="{StaticResource {x:Type ComboBox}}"> <Style x:Key="UComboBox" TargetType="ComboBox" BasedOn="{StaticResource {x:Type ComboBox}}">
<Setter Property="adonisExtensions:WatermarkExtension.Watermark" Value="UE5 / UE4 / GameName..." /> <Setter Property="adonisExtensions:WatermarkExtension.Watermark" Value="UE5 / UE4 / GameName..." />
<Setter Property="ScrollViewer.CanContentScroll" Value="True" />
<Setter Property="VirtualizingPanel.IsVirtualizing" Value="True" />
<Setter Property="VirtualizingPanel.VirtualizationMode" Value="Recycling" />
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<VirtualizingStackPanel />
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
<Setter Property="Template"> <Setter Property="Template">
<Setter.Value> <Setter.Value>
<ControlTemplate TargetType="ComboBox"> <ControlTemplate TargetType="ComboBox">