Square Enix audio support, Marvel Rivals asset registry fix, DBD update
Some checks are pending
FModel QA Builder / build (push) Waiting to run

Co-Authored-By: LongerWarrior <37636768+LongerWarrior@users.noreply.github.com>
Co-Authored-By: GhostScissors <79089473+GhostScissors@users.noreply.github.com>
This commit is contained in:
Masusder 2026-04-07 19:48:04 +02:00
parent e723149db8
commit 02cd52ac0f
4 changed files with 112 additions and 46 deletions

@ -1 +1 @@
Subproject commit 166d67076273f6e717adcc15cd57b759abf8e987
Subproject commit 75f3878b7a348cbda3927048b142a0a6923e79c0

View File

@ -1,4 +1,5 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
@ -664,23 +665,11 @@ public class AudioPlayerViewModel : ViewModel, ISource, IDisposable
public static bool TryConvert(string inputFilePath, byte[] inputFileData, out string wavFilePath, bool updateUi = false)
{
wavFilePath = string.Empty;
var vgmFilePath = Path.Combine(UserSettings.Default.OutputDirectory, ".data", "test.exe");
if (!File.Exists(vgmFilePath))
{
vgmFilePath = Path.Combine(UserSettings.Default.OutputDirectory, ".data", "vgmstream-cli.exe");
if (!File.Exists(vgmFilePath))
{
Log.Error("Failed to convert {InputFilePath}, vgmstream is missing", inputFilePath);
FLogger.Append(ELog.Error, () =>
{
FLogger.Text("Failed to convert audio because vgmstream is missing. See: ", Constants.WHITE);
FLogger.Link("→ link ←", Constants.AUDIO_ISSUE_LINK, true);
});
return false;
}
}
var vgmStreamPath = TryGetVgmstreamPath();
if (string.IsNullOrEmpty(vgmStreamPath))
return false;
var success = TryConvertToWAV(inputFilePath, inputFileData, vgmFilePath, true, out wavFilePath);
var success = TryConvertToWav(inputFilePath, inputFileData, vgmStreamPath, true, out wavFilePath);
if (!success)
{
@ -713,10 +702,10 @@ public class AudioPlayerViewModel : ViewModel, ISource, IDisposable
return false;
}
return TryConvertToWAV(SelectedAudioFile.FilePath, SelectedAudioFile.Data, decoderPath, false, out rawFilePath);
return TryConvertToWav(SelectedAudioFile.FilePath, SelectedAudioFile.Data, decoderPath, false, out rawFilePath);
}
private static bool TryConvertToWAV(string inputFilePath, byte[] inputFileData, string converterPath, bool usevgmstream, out string wavFilePath)
private static bool TryConvertToWav(string inputFilePath, byte[] inputFileData, string converterPath, bool usevgmstream, out string wavFilePath)
{
wavFilePath = Path.ChangeExtension(inputFilePath, ".wav");
var directory = Path.GetDirectoryName(inputFilePath);
@ -746,4 +735,74 @@ public class AudioPlayerViewModel : ViewModel, ISource, IDisposable
return success;
}
private static string TryGetVgmstreamPath()
{
var vgmFilePath = Path.Combine(UserSettings.Default.OutputDirectory, ".data", "test.exe");
if (!File.Exists(vgmFilePath))
{
vgmFilePath = Path.Combine(UserSettings.Default.OutputDirectory, ".data", "vgmstream-cli.exe");
if (!File.Exists(vgmFilePath))
{
Log.Error("Failed to convert audio, vgmstream is missing");
FLogger.Append(ELog.Error, () =>
{
FLogger.Text("Failed to convert audio because vgmstream is missing. See: ", Constants.WHITE);
FLogger.Link("→ link ←", Constants.AUDIO_ISSUE_LINK, true);
});
return string.Empty;
}
}
return vgmFilePath;
}
// Since Square Enix soundbanks are pretty niche, let's just use vgmstream to extract them
public static List<string> ExtractSquareEnixAudio(string sabPath, byte[] sqexData)
{
var vgmStreamPath = TryGetVgmstreamPath();
if (string.IsNullOrEmpty(vgmStreamPath))
return [];
if (sqexData.Length == 0)
return [];
var extractionDir = Path.GetDirectoryName(sabPath);
Directory.CreateDirectory(extractionDir);
// There's no clean way to know what was extracted with vgmstream (it's a soundbank, might contain multiple sounds) so we're monitoring extraction directory
var capturedFiles = new ConcurrentBag<string>();
using var watcher = new FileSystemWatcher(extractionDir)
{
Filter = "*.wav",
NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite | NotifyFilters.CreationTime
};
void handler(object s, FileSystemEventArgs e) => capturedFiles.Add(e.FullPath);
watcher.Created += handler;
watcher.Changed += handler;
watcher.EnableRaisingEvents = true;
var tempSab = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName() + ".sab");
File.WriteAllBytes(tempSab, sqexData);
var startInfo = new ProcessStartInfo
{
FileName = vgmStreamPath,
Arguments = $"-S 0 -o \"{extractionDir}\\?n_?s.wav\" \"{tempSab}\"",
UseShellExecute = false,
CreateNoWindow = true
};
using (var process = Process.Start(startInfo))
{
process?.WaitForExit(15000);
}
File.Delete(tempSab);
watcher.EnableRaisingEvents = false;
return [.. capturedFiles.Distinct()];
}
}

View File

@ -5,14 +5,11 @@ using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using AdonisUI.Controls;
using CUE4Parse;
using CUE4Parse.Compression;
using CUE4Parse.Encryption.Aes;
@ -22,11 +19,12 @@ using CUE4Parse.FileProvider.Vfs;
using CUE4Parse.GameTypes.Aion2.Objects;
using CUE4Parse.GameTypes.AoC.Objects;
using CUE4Parse.GameTypes.AshEchoes.FileProvider;
using CUE4Parse.GameTypes.SMG.UE4.Assets.Exports.Wwise;
using CUE4Parse.GameTypes.KRD.Assets.Exports;
using CUE4Parse.GameTypes.Borderlands3.Assets.Exports;
using CUE4Parse.GameTypes.Borderlands4.Assets.Exports;
using CUE4Parse.GameTypes.Borderlands4.Wwise;
using CUE4Parse.GameTypes.Borderlands3.Assets.Exports;
using CUE4Parse.GameTypes.KRD.Assets.Exports;
using CUE4Parse.GameTypes.SMG.UE4.Assets.Exports.Wwise;
using CUE4Parse.GameTypes.SquareEnix.UE4.Assets.Exports;
using CUE4Parse.MappingsProvider;
using CUE4Parse.UE4.AssetRegistry;
using CUE4Parse.UE4.Assets;
@ -57,14 +55,11 @@ using CUE4Parse.UE4.Shaders;
using CUE4Parse.UE4.Versions;
using CUE4Parse.UE4.Wwise;
using CUE4Parse.Utils;
using CUE4Parse_Conversion;
using CUE4Parse_Conversion.Sounds;
using EpicManifestParser;
using EpicManifestParser.UE;
using EpicManifestParser.ZlibngDotNetDecompressor;
using FModel.Creator;
using FModel.Extensions;
using FModel.Framework;
@ -73,21 +68,14 @@ using FModel.Settings;
using FModel.Views;
using FModel.Views.Resources.Controls;
using FModel.Views.Snooper;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using OpenTK.Windowing.Common;
using OpenTK.Windowing.Desktop;
using Serilog;
using SkiaSharp;
using Svg.Skia;
using UE4Config.Parsing;
using Application = System.Windows.Application;
using FGuid = CUE4Parse.UE4.Objects.Core.Misc.FGuid;
@ -1171,20 +1159,20 @@ public class CUE4ParseViewModel : ViewModel
case UFMODEvent when (isNone || saveAudio) && pointer.Object.Value is UFMODEvent fmodEvent:
{
var extractedSounds = FmodProvider.ExtractEventSounds(fmodEvent);
var directory = Path.GetDirectoryName(fmodEvent.Owner?.Name) ?? "/FMOD/Desktop/";
var directory = Path.GetDirectoryName(Provider.FixPath(fmodEvent.Owner?.Name ?? "/FMOD/Desktop/"));
foreach (var sound in extractedSounds)
{
SaveAndPlaySound(cancellationToken, Path.Combine(directory, sound.Name), sound.Extension, sound.Data, saveAudio, updateUi);
SaveAndPlaySound(cancellationToken, Path.Combine(directory, sound.Name).Replace("\\", "/"), sound.Extension, sound.Data, saveAudio, updateUi);
}
return false;
}
case UFMODBank when (isNone || saveAudio) && pointer.Object.Value is UFMODBank fmodBank:
{
var extractedSounds = FmodProvider.ExtractBankSounds(fmodBank);
var directory = Path.GetDirectoryName(fmodBank.Owner?.Name) ?? "/FMOD/Desktop/";
var directory = Path.GetDirectoryName(Provider.FixPath(fmodBank.Owner?.Name ?? "/FMOD/Desktop/"));
foreach (var sound in extractedSounds)
{
SaveAndPlaySound(cancellationToken, Path.Combine(directory, sound.Name), sound.Extension, sound.Data, saveAudio, updateUi);
SaveAndPlaySound(cancellationToken, Path.Combine(directory, sound.Name).Replace("\\", "/"), sound.Extension, sound.Data, saveAudio, updateUi);
}
return false;
}
@ -1207,6 +1195,22 @@ public class CUE4ParseViewModel : ViewModel
}
return false;
}
case USQEXSEADSoundBank or USQEXSEADSound when (isNone || saveAudio) && pointer.Object.Value is UObject squareEnixObject:
{
var data = squareEnixObject switch
{
USQEXSEADSoundBank sqexSoundBank => sqexSoundBank.SQEXSoundBankData?.Data ?? [],
USQEXSEADSound sqexSound => sqexSound.SQEXSoundData?.Data ?? [],
_ => [],
};
var sabPath = Path.Combine(TabControl.SelectedTab.Entry.PathWithoutExtension.Replace('\\', '/').SubstringBeforeLast('/'), squareEnixObject.Name);
var extractedSounds = AudioPlayerViewModel.ExtractSquareEnixAudio(sabPath, data);
foreach (var soundPath in extractedSounds)
{
SaveAndPlaySound(cancellationToken, soundPath, "wav", File.ReadAllBytes(soundPath), saveAudio, updateUi);
}
return false;
}
case UAkMediaAssetData when isNone || saveAudio:
case USoundWave when isNone || saveAudio:
{
@ -1443,8 +1447,10 @@ public class CUE4ParseViewModel : ViewModel
private void SaveAndPlaySound(CancellationToken cancellationToken, string fullPath, string ext, byte[] data, bool saveAudio, bool updateUi)
{
if (fullPath.StartsWith('/')) fullPath = fullPath[1..];
var savedAudioPath = Path.Combine(UserSettings.Default.AudioDirectory,
UserSettings.Default.KeepDirectoryStructure ? fullPath : fullPath.SubstringAfterLast('/')).Replace('\\', '/') + $".{ext.ToLowerInvariant()}";
var extLower = ext.ToLowerInvariant();
var baseFilePath = UserSettings.Default.KeepDirectoryStructure ? fullPath : fullPath.SubstringAfterLast('/');
var combinedPath = Path.Combine(UserSettings.Default.AudioDirectory, baseFilePath);
var savedAudioPath = Path.ChangeExtension(combinedPath, extLower).Replace('\\', '/');
if (saveAudio)
{
@ -1453,7 +1459,7 @@ public class CUE4ParseViewModel : ViewModel
Directory.CreateDirectory(directory);
bool conversionSuccess = true;
if (UserSettings.Default.ConvertAudioOnBulkExport)
if (UserSettings.Default.ConvertAudioOnBulkExport && extLower is not "wav")
{
if (AudioPlayerViewModel.TryConvert(savedAudioPath, data, out string wavFilePath))
savedAudioPath = wavFilePath;

View File

@ -12,6 +12,7 @@ using CUE4Parse.GameTypes.Borderlands4.Assets.Exports;
using CUE4Parse.GameTypes.FN.Assets.Exports.DataAssets;
using CUE4Parse.GameTypes.SMG.UE4.Assets.Exports.Wwise;
using CUE4Parse.GameTypes.SMG.UE4.Assets.Objects;
using CUE4Parse.GameTypes.SquareEnix.UE4.Assets.Exports;
using CUE4Parse.UE4.Assets;
using CUE4Parse.UE4.Assets.Exports;
using CUE4Parse.UE4.Assets.Exports.Animation;
@ -245,9 +246,9 @@ public class GameFileViewModel(GameFile asset) : ViewModel
UFMODBankLookup => (EAssetCategory.Data, EBulkType.None),
UFMODBus or UFMODSnapshot or UFMODSnapshotReverb or UFMODVCA => (EAssetCategory.Audio, EBulkType.None),
UFMODBus or UFMODSnapshot or UFMODSnapshotReverb or UFMODVCA or USQEXSEADSoundAttenuation => (EAssetCategory.Audio, EBulkType.None),
UFMODBank or UAkAudioBank or UAtomWaveBank or UAkInitBank => (EAssetCategory.SoundBank, EBulkType.Audio),
UFMODBank or UAkAudioBank or UAtomWaveBank or UAkInitBank or USQEXSEADSoundBank => (EAssetCategory.SoundBank, EBulkType.Audio),
UWwiseAssetLibrary or USoundBase or UAkMediaAssetData or UAtomCueSheet
or USoundAtomCueSheet or UAkAudioType or UExternalSource or UExternalSourceBank
@ -259,8 +260,8 @@ public class GameFileViewModel(GameFile asset) : ViewModel
UNiagaraSystem or UNiagaraScriptBase or UParticleSystem => (EAssetCategory.Particle, EBulkType.None),
// Game specific assets below
UBorderlandsDialogObject => (EAssetCategory.Borderlands, EBulkType.None), // Borderlands 3;
UGbxGraphAsset or UDialogScriptData or UDialogPerformanceData => (EAssetCategory.Borderlands, EBulkType.Audio), // Borderlands 4; Borderlands 3;
UBorderlandsDialogObject when GameVersion is EGame.GAME_Borderlands3 => (EAssetCategory.Borderlands, EBulkType.None), // Borderlands 3;
UGbxGraphAsset or UDialogScriptData or UDialogPerformanceData when GameVersion is EGame.GAME_Borderlands4 or EGame.GAME_Borderlands3 => (EAssetCategory.Borderlands, EBulkType.Audio), // Borderlands 4; Borderlands 3;
UFaceFXAnimSet when GameVersion is EGame.GAME_Borderlands4 => (EAssetCategory.Borderlands, EBulkType.Audio), // Borderlands 4;
_ => (EAssetCategory.All, EBulkType.None),