FModel/FModel/ViewModels/GameFileViewModel.cs
2026-02-08 12:49:50 +01:00

462 lines
17 KiB
C#

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.GameTypes.SMG.UE4.Assets.Exports.Wwise;
using CUE4Parse.GameTypes.SMG.UE4.Assets.Objects;
using CUE4Parse.UE4.Assets;
using CUE4Parse.UE4.Assets.Exports;
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.CustomizableObject;
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.Nanite;
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 EBulkType _assetActions = EBulkType.None;
public EBulkType AssetActions
{
get => _assetActions;
private set
{
SetProperty(ref _assetActions, value);
}
}
private ImageSource _previewImage;
public ImageSource PreviewImage
{
get => _previewImage;
private set
{
if (SetProperty(ref _previewImage, value))
{
Resolved |= EResolveCompute.Preview;
}
}
}
private int _numTextures = 0;
public int NumTextures
{
get => _numTextures;
private set => SetProperty(ref _numTextures, value);
}
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;
AssetActions = EBulkType.Meshes | EBulkType.Textures | EBulkType.Audio | EBulkType.Code;
ResolvedAssetType = "World";
Resolved |= EResolveCompute.Preview;
return Task.CompletedTask;
}
if (Asset.NameWithoutExtension.EndsWith("_BuiltData"))
{
AssetCategory = EAssetCategory.BuildData;
AssetActions = EBulkType.Textures;
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, AssetActions) = dummy switch
{
URigVMBlueprintGeneratedClass => (EAssetCategory.RigVMBlueprintGeneratedClass, EBulkType.Code),
UAnimBlueprintGeneratedClass => (EAssetCategory.AnimBlueprintGeneratedClass, EBulkType.Code),
UWidgetBlueprintGeneratedClass => (EAssetCategory.WidgetBlueprintGeneratedClass, EBulkType.Code),
UBlueprintGeneratedClass or UFunction => (EAssetCategory.BlueprintGeneratedClass, EBulkType.Code),
UUserDefinedEnum => (EAssetCategory.UserDefinedEnum, EBulkType.None),
UUserDefinedStruct => (EAssetCategory.UserDefinedStruct, EBulkType.Code),
UBlueprintCore => (EAssetCategory.Blueprint, EBulkType.Code),
UClassCookedMetaData or UStructCookedMetaData or UEnumCookedMetaData => (EAssetCategory.CookedMetaData, EBulkType.None),
UStaticMesh => (EAssetCategory.StaticMesh, EBulkType.Meshes),
USkeletalMesh => (EAssetCategory.SkeletalMesh, EBulkType.Meshes),
UCustomizableObject => (EAssetCategory.CustomizableObject, EBulkType.None),
UNaniteDisplacedMesh => (EAssetCategory.NaniteDisplacedMesh, EBulkType.None),
UTexture => (EAssetCategory.Texture, EBulkType.Textures),
UMaterialInterface => (EAssetCategory.Material, EBulkType.None),
UMaterialInterfaceEditorOnlyData => (EAssetCategory.MaterialEditorData, EBulkType.None),
UMaterialFunction => (EAssetCategory.MaterialFunction, EBulkType.None),
UMaterialFunctionEditorOnlyData => (EAssetCategory.MaterialFunctionEditorData, EBulkType.None),
UMaterialParameterCollection => (EAssetCategory.MaterialParameterCollection, EBulkType.None),
UAnimationAsset => (EAssetCategory.Animation, EBulkType.Animations),
USkeleton => (EAssetCategory.Skeleton, EBulkType.Meshes),
URig => (EAssetCategory.Rig, EBulkType.None),
UWorld => (EAssetCategory.World, EBulkType.Meshes | EBulkType.Textures | EBulkType.Audio | EBulkType.Code),
UMapBuildDataRegistry => (EAssetCategory.BuildData, EBulkType.Textures),
ULevelSequence => (EAssetCategory.LevelSequence, EBulkType.Code),
UFoliageType => (EAssetCategory.Foliage, EBulkType.None),
UItemDefinitionBase => (EAssetCategory.ItemDefinitionBase, EBulkType.Textures),
UDataAsset or UDataTable or UCurveTable or UStringTable => (EAssetCategory.Data, EBulkType.None),
UCurveBase => (EAssetCategory.CurveBase, EBulkType.None),
UPhysicsAsset => (EAssetCategory.PhysicsAsset, EBulkType.None),
UObjectRedirector => (EAssetCategory.ObjectRedirector, EBulkType.None),
UPhysicalMaterial => (EAssetCategory.PhysicalMaterial, EBulkType.None),
USoundAtomCue or UAkAudioEvent or USoundCue or UFMODEvent
or UAkAssetData or UAkAssetPlatformData => (EAssetCategory.AudioEvent, EBulkType.Audio),
UFMODBank or UAkAudioBank or UAtomWaveBank or UAkInitBank => (EAssetCategory.SoundBank, EBulkType.Audio),
UWwiseAssetLibrary or USoundBase or UAkMediaAssetData or UAtomCueSheet
or USoundAtomCueSheet or UAkAudioType or UExternalSource or UExternalSourceBank
or UAkMediaAsset => (EAssetCategory.Audio, EBulkType.Audio),
UFileMediaSource => (EAssetCategory.Video, EBulkType.None),
UFont or UFontFace or USMGLocaleFontUMG => (EAssetCategory.Font, EBulkType.None),
UNiagaraSystem or UNiagaraScriptBase or UParticleSystem => (EAssetCategory.Particle, EBulkType.None),
_ => (EAssetCategory.All, EBulkType.None),
};
switch (AssetCategory)
{
case EAssetCategory.Texture when pointer.Object.Value is UTexture texture:
{
if (!resolve.HasFlag(EResolveCompute.Preview))
break;
if (pointer.Object.Value is UTexture2DArray textureArray && textureArray.GetFirstMip() is { SizeZ: > 1 } firstMip)
NumTextures = firstMip.SizeZ;
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 "ushaderbytecode":
AssetCategory = EAssetCategory.ByteCode;
break;
case "wav":
case "awb": // This is technically soundbank and should be below but I want it to be distinguishable from "acb"
case "xvag":
case "flac":
case "at9":
case "wem":
case "ogg":
AssetCategory = EAssetCategory.Audio;
AssetActions = EBulkType.Audio;
break;
case "acb":
case "bank":
case "bnk":
case "pck":
AssetCategory = EAssetCategory.SoundBank;
AssetActions = EBulkType.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;
AssetActions = EBulkType.Textures;
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
}