Merge remote-tracking branch 'upstream/dev' into ExportingFix

This commit is contained in:
Krowe Moh 2026-04-09 05:52:54 +10:00
commit d8d8757768
8 changed files with 164 additions and 47 deletions

@ -1 +1 @@
Subproject commit 39d1b4b1fb57cc7cf47e10b003a1dca3961661c3
Subproject commit a3821bdc34ef75f10a2003092ad2502dc648e53a

View File

@ -161,4 +161,5 @@ public enum EAssetCategory : uint
GameSpecific = AssetCategoryExtensions.CategoryBase + (10 << 16),
Borderlands = GameSpecific + 1,
Aion2 = GameSpecific + 2,
RocoKingdomWorld = GameSpecific + 3,
}

View File

@ -1,4 +1,5 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
@ -702,23 +703,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)
{
@ -751,10 +740,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);
@ -784,4 +773,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,12 @@ using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
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 +20,13 @@ 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.RocoKingdomWorld.Assets.Objects;
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 +57,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 +70,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;
@ -695,6 +685,11 @@ public class CUE4ParseViewModel : ViewModel
ProcessAion2DatFile(entry, updateUi, saveProperties);
break;
}
case "bytes" when Provider.Versions.Game is EGame.GAME_RocoKingdomWorld:
{
ProcessRocoBinFile(entry, updateUi, saveProperties);
break;
}
case "dbc" when Provider.Versions.Game is EGame.GAME_AshesOfCreation:
{
ProcessCacheDBFile(entry, updateUi, saveProperties);
@ -981,6 +976,40 @@ public class CUE4ParseViewModel : ViewModel
}
}
// Roco Kingdom: World
void ProcessRocoBinFile(GameFile entry, bool updateUi, bool saveProperties)
{
TabControl.SelectedTab.Highlighter = AvalonExtensions.HighlighterSelector("json");
var nonFileName = "/" + entry.NameWithoutExtension + ".non";
var nonPath = Provider.Files.Keys.FirstOrDefault(k => k.EndsWith(nonFileName, StringComparison.OrdinalIgnoreCase));
// I will only get one localization file because they did not translate any languages, lol
var locPathKey = entry.Path.Replace("/BinData/", "/BinLocalize/en_US/").Replace("/BinDataCompressed/", "/BinLocalize/en_US/");
var locFileFound = Provider.Files.TryGetValue(locPathKey, out var locEntry);
if (!string.IsNullOrEmpty(nonPath) && Provider.Files.TryGetValue(nonPath, out var nonEntry))
{
string json = Encoding.UTF8.GetString(nonEntry.Read());
var schema = JsonConvert.DeserializeObject<FRocoSchema>(json);
var archive = entry.CreateReader();
var locArchive = locFileFound ? new FRocoBinData(locEntry.CreateReader(), null, ERocoBinDataType.BinLocalize) : null;
var data = entry.PathWithoutExtension switch
{
var p when p.Contains("BinDataCompressed") => new FRocoBinData(archive, schema, ERocoBinDataType.BinDataCompressed, locArchive),
var p when p.Contains("BinData") => new FRocoBinData(archive, schema, ERocoBinDataType.BinData, locArchive),
var p when p.Contains("BinLocalize") => new FRocoBinData(archive, null, ERocoBinDataType.BinLocalize),
_ => null
};
TabControl.SelectedTab.SetDocumentText(JsonConvert.SerializeObject(data, Formatting.Indented), saveProperties, updateUi);
}
else if (entry.PathWithoutExtension.Contains("/Bin/"))
{
throw new Exception($"Could not find associated .non file for {entry.Name}");
}
}
void ProcessAion2DatFile(GameFile entry, bool updateUi, bool saveProperties)
{
TabControl.SelectedTab.Highlighter = AvalonExtensions.HighlighterSelector("json");
@ -1171,20 +1200,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 +1236,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 +1488,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 +1500,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),
@ -358,6 +359,7 @@ public class GameFileViewModel(GameFile asset) : ViewModel
break;
case "stinfo":
case "ushaderbytecode":
case "upipelinecache":
AssetCategory = EAssetCategory.ByteCode;
break;
case "wav":
@ -439,6 +441,11 @@ public class GameFileViewModel(GameFile asset) : ViewModel
case "dat" when GameVersion is EGame.GAME_Aion2:
AssetCategory = EAssetCategory.Aion2;
break;
case "bytes" when GameVersion is EGame.GAME_RocoKingdomWorld:
case "non" when GameVersion is EGame.GAME_RocoKingdomWorld:
case "cam" when GameVersion is EGame.GAME_RocoKingdomWorld:
AssetCategory = EAssetCategory.RocoKingdomWorld;
break;
default:
AssetCategory = EAssetCategory.All; // just so it sets resolved
break;

View File

@ -39,7 +39,7 @@
<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="LuaBrush" Color="Blue" />
<SolidColorBrush x:Key="JsonXmlBrush" Color="LightGreen" />
<SolidColorBrush x:Key="CodeBrush" Color="SandyBrown" />
<SolidColorBrush x:Key="HtmlBrush" Color="Tomato" />
@ -52,4 +52,5 @@
<!-- For specific games -->
<SolidColorBrush x:Key="BorderlandsBrush" Color="Yellow"></SolidColorBrush>
<SolidColorBrush x:Key="AionBrush" Color="DeepSkyBlue"></SolidColorBrush>
<SolidColorBrush x:Key="RocoKingdomWorldBrush" Color="#fecf4d"></SolidColorBrush>
</ResourceDictionary>

View File

@ -91,6 +91,7 @@ public class FileToGeometryConverter : IMultiValueConverter
EAssetCategory.Borderlands => ("BorderlandsIcon", "BorderlandsBrush"),
EAssetCategory.Aion2 => ("AionIcon", "AionBrush"),
EAssetCategory.RocoKingdomWorld => ("RocoKingdomWorldIcon", "RocoKingdomWorldBrush"),
_ => ("AssetIcon", "NeutralBrush")
};

File diff suppressed because one or more lines are too long