From 39f8ea57023161d3318cb2c0ad0c90fb527124f4 Mon Sep 17 00:00:00 2001 From: Masusder <59669685+Masusder@users.noreply.github.com> Date: Wed, 16 Apr 2025 18:39:57 +0200 Subject: [PATCH 01/16] Generalized wwiseaudio dir lookup --- FModel/ViewModels/CUE4ParseViewModel.cs | 36 ++++++++++++++++++------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/FModel/ViewModels/CUE4ParseViewModel.cs b/FModel/ViewModels/CUE4ParseViewModel.cs index cc4bb7ce..cdec8acb 100644 --- a/FModel/ViewModels/CUE4ParseViewModel.cs +++ b/FModel/ViewModels/CUE4ParseViewModel.cs @@ -842,15 +842,33 @@ public void ExtractAndScroll(CancellationToken cancellationToken, string fullPat if (!kvp.Value.HasValue) continue; foreach (var media in kvp.Value.Value.Media) - { - if (!Provider.TrySaveAsset(Path.Combine("Game/WwiseAudio/", media.MediaPathName.Text), out var data)) continue; - - var namedPath = string.Concat( - Provider.ProjectName, "/Content/WwiseAudio/", - media.DebugName.Text.SubstringBeforeLast('.').Replace('\\', '/'), - " (", kvp.Key.LanguageName.Text, ")"); - SaveAndPlaySound(namedPath, media.MediaPathName.Text.SubstringAfterLast('.'), data); - } + { + var mediaRelativePath = media.MediaPathName.Text.Replace('\\', '/'); + var projectName = string.IsNullOrEmpty(Provider.ProjectName) ? "Game" : Provider.ProjectName; + var baseWwiseAudioPath = Path.Combine(projectName, "Content", "WwiseAudio"); + var candidatePath = Path.Combine(baseWwiseAudioPath, "Cooked", media.MediaPathName.Text); + if (!Provider.TrySaveAsset(candidatePath, out byte[] data)) + { + candidatePath = Path.Combine(baseWwiseAudioPath, mediaRelativePath); + if (!Provider.TrySaveAsset(candidatePath, out data)) + { + continue; + } + } + + var debugName = !string.IsNullOrEmpty(media.DebugName.Text) + ? media.DebugName.Text.SubstringBeforeLast('.') + : Path.GetFileNameWithoutExtension(mediaRelativePath); + + var namedPath = Path.Combine( + projectName, + "Content", + "WwiseAudio", + $"{debugName.Replace('\\', '/')} ({kvp.Key.LanguageName.Text})" + ); + + SaveAndPlaySound(namedPath, Path.GetExtension(mediaRelativePath).TrimStart('.'), data); + } } return false; } From e943ea314aabdeb0189d7842c2acad1ebdf3b0fd Mon Sep 17 00:00:00 2001 From: Masusder <59669685+Masusder@users.noreply.github.com> Date: Sun, 20 Apr 2025 14:34:59 +0200 Subject: [PATCH 02/16] Custom directories for dbd AES key wasn't changed since they moved to UE5, so I see no reason not to include it --- FModel/Settings/CustomDirectory.cs | 15 +++++++++++++++ FModel/ViewModels/GameSelectorViewModel.cs | 6 +++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/FModel/Settings/CustomDirectory.cs b/FModel/Settings/CustomDirectory.cs index 14b4a388..f368a7c4 100644 --- a/FModel/Settings/CustomDirectory.cs +++ b/FModel/Settings/CustomDirectory.cs @@ -30,6 +30,21 @@ public static IList Default(string gameName) new("Shop Backgrounds", "ShooterGame/Content/UI/OutOfGame/MainMenu/Store/Shared/Textures/"), new("Weapon Renders", "ShooterGame/Content/UI/Screens/OutOfGame/MainMenu/Collection/Assets/Large/") }; + case "Dead by Daylight": + return new List + { + new("Characters V1", "DeadByDaylight/Plugins/DBDCharacters/"), + new("Characters V2", "DeadByDaylight/Plugins/Runtime/Bhvr/DBDCharacters/"), + new("Characters (Deprecated)", "DeadbyDaylight/Content/Characters/"), + new("Meshes", "DeadByDaylight/Content/Meshes/"), + new("Textures", "DeadByDaylight/Content/Textures/"), + new("Icons", "DeadByDaylight/Content/UI/UMGAssets/Icons/"), + new("Blueprints", "DeadByDaylight/Content/Blueprints/"), + new("Audio Events", "DeadByDaylight/Content/Audio/Events/"), + new("Audio", "DeadByDaylight/Content/WwiseAudio/Cooked/"), + new("Data Tables", "DeadByDaylight/Content/Data/"), + new("Localization", "DeadByDaylight/Content/Localization/") + }; default: return new List(); } diff --git a/FModel/ViewModels/GameSelectorViewModel.cs b/FModel/ViewModels/GameSelectorViewModel.cs index 870160bf..b30ebf6d 100644 --- a/FModel/ViewModels/GameSelectorViewModel.cs +++ b/FModel/ViewModels/GameSelectorViewModel.cs @@ -99,7 +99,7 @@ private IEnumerable EnumerateDetectedGames() yield return GetUnrealEngineGame("9361c8c6d2f34b42b5f2f61093eedf48", "\\TslGame\\Content\\Paks", EGame.GAME_PlayerUnknownsBattlegrounds); yield return GetRiotGame("VALORANT", "ShooterGame\\Content\\Paks", EGame.GAME_Valorant); yield return DirectorySettings.Default("VALORANT [LIVE]", Constants._VAL_LIVE_TRIGGER, ue: EGame.GAME_Valorant); - yield return GetSteamGame(381210, "\\DeadByDaylight\\Content\\Paks", EGame.GAME_UE4_27); // Dead By Daylight + yield return GetSteamGame(381210, "\\DeadByDaylight\\Content\\Paks", EGame.GAME_DeadByDaylight, aesKey: "0x22b1639b548124925cf7b9cbaa09f9ac295fcf0324586d6b37ee1d42670b39b3"); // Dead By Daylight yield return GetSteamGame(578080, "\\TslGame\\Content\\Paks", EGame.GAME_PlayerUnknownsBattlegrounds); // PUBG yield return GetSteamGame(1172380, "\\SwGame\\Content\\Paks", EGame.GAME_StarWarsJediFallenOrder); // STAR WARS Jedi: Fallen Order™ yield return GetSteamGame(677620, "\\PortalWars\\Content\\Paks", EGame.GAME_Splitgate); // Splitgate @@ -151,13 +151,13 @@ private DirectorySettings GetRiotGame(string gameName, string pakDirectory, EGam return null; } - private DirectorySettings GetSteamGame(int id, string pakDirectory, EGame ueVersion) + private DirectorySettings GetSteamGame(int id, string pakDirectory, EGame ueVersion, string aesKey = "") { var steamInfo = SteamDetection.GetSteamGameById(id); if (steamInfo is not null) { Log.Debug("Found {GameName} in steam manifests", steamInfo.Name); - return DirectorySettings.Default(steamInfo.Name, $"{steamInfo.GameRoot}{pakDirectory}", ue: ueVersion); + return DirectorySettings.Default(steamInfo.Name, $"{steamInfo.GameRoot}{pakDirectory}", ue: ueVersion, aes: aesKey); } return null; From 0d9a2a34e935adc96920d377c833566c2775d629 Mon Sep 17 00:00:00 2001 From: Masusder <59669685+Masusder@users.noreply.github.com> Date: Mon, 28 Apr 2025 11:55:22 +0200 Subject: [PATCH 03/16] Bnk audio events support --- CUE4Parse | 2 +- FModel/ViewModels/CUE4ParseViewModel.cs | 140 +++++++++++++++++++----- 2 files changed, 114 insertions(+), 28 deletions(-) diff --git a/CUE4Parse b/CUE4Parse index 72eaf410..62376087 160000 --- a/CUE4Parse +++ b/CUE4Parse @@ -1 +1 @@ -Subproject commit 72eaf4101268bd971281f3cd8769a57be90caedb +Subproject commit 62376087940c3fb9d8c8c12e8bb8adb8203bc7b6 diff --git a/FModel/ViewModels/CUE4ParseViewModel.cs b/FModel/ViewModels/CUE4ParseViewModel.cs index cdec8acb..cdb6302d 100644 --- a/FModel/ViewModels/CUE4ParseViewModel.cs +++ b/FModel/ViewModels/CUE4ParseViewModel.cs @@ -60,6 +60,7 @@ using UE4Config.Parsing; using Application = System.Windows.Application; using FGuid = CUE4Parse.UE4.Objects.Core.Misc.FGuid; +using CUE4Parse.UE4.Wwise.Objects; namespace FModel.ViewModels; @@ -839,36 +840,121 @@ public void ExtractAndScroll(CancellationToken cancellationToken, string fullPat { foreach (var kvp in wwiseData.EventLanguageMap) { - if (!kvp.Value.HasValue) continue; + if (!kvp.Value.HasValue) + continue; + + var projectName = string.IsNullOrEmpty(Provider.ProjectName) ? "Game" : Provider.ProjectName; + var baseWwiseAudioPath = Path.Combine(projectName, "Content", "WwiseAudio", "Cooked"); + var audioEventPath = pointer.Object.Value.GetPathName().Replace("Game", projectName); + + foreach (var soundBank in kvp.Value.Value.SoundBanks) + { + if (!soundBank.bContainsMedia) + continue; + + var soundBankName = soundBank.SoundBankPathName.ToString(); + var soundBankPath = Path.Combine(baseWwiseAudioPath, soundBankName); + var audioEventId = kvp.Value.Value.EventId.ToString(); + + if (!Provider.TrySaveAsset(soundBankPath, out byte[] data)) + continue; + + using var ar = new FByteArchive(soundBankName, data); + var wwiseReader = new WwiseReader(ar); + + var hierarchyTable = new Dictionary(); + foreach (var hierarchy in wwiseReader.Hierarchies) + { + uint id = hierarchy.Data.Id; + + if (!hierarchyTable.ContainsKey(id)) + { + hierarchyTable.Add(id, hierarchy); + } + } + + long parsedId = long.Parse(audioEventId); + uint parsedAudioEventId = (uint) parsedId; + if (hierarchyTable.TryGetValue(parsedAudioEventId, out var eventHierarchy) && + eventHierarchy.Data is HierarchyEvent hierarchyEvent) + { + foreach (var actionId in hierarchyEvent.EventActionIds) + { + if (!hierarchyTable.TryGetValue(actionId, out var actionHierarchy) || + actionHierarchy.Data is not HierarchyEventAction eventAction) + continue; + + TraverseAndSave(eventAction.ReferencedId); + } + } + + void TraverseAndSave(uint id) + { + if (!hierarchyTable.TryGetValue(id, out var hierarchy)) + return; + + switch (hierarchy.Data) + { + case HierarchySoundSfxVoice soundSfx: + SaveWemSound(soundSfx); + break; + + case HierarchyRandomSequenceContainer randomContainer: + foreach (var childId in randomContainer.ChildIDs) + TraverseAndSave(childId); + break; + + case HierarchySwitchContainer switchContainer: + foreach (var childId in switchContainer.ChildIDs) + TraverseAndSave(childId); + break; + + case HierarchyLayerContainer layerContainer: + foreach (var childId in layerContainer.ChildIDs) + TraverseAndSave(childId); + break; + } + } + + void SaveWemSound(HierarchySoundSfxVoice soundSfx) + { + var wemId = soundSfx.SourceId; + if (wwiseReader.WwiseEncodedMedias.TryGetValue(wemId.ToString(), out var wemData)) + { + var debugName = kvp.Value.Value.DebugName.ToString(); + var outputPath = Path.Combine(audioEventPath.Replace($".{debugName}", ""), $"{debugName.Replace('\\', '/')} ({wemId})"); + SaveAndPlaySound(outputPath, "WEM", wemData); + } + } + } foreach (var media in kvp.Value.Value.Media) - { - var mediaRelativePath = media.MediaPathName.Text.Replace('\\', '/'); - var projectName = string.IsNullOrEmpty(Provider.ProjectName) ? "Game" : Provider.ProjectName; - var baseWwiseAudioPath = Path.Combine(projectName, "Content", "WwiseAudio"); - var candidatePath = Path.Combine(baseWwiseAudioPath, "Cooked", media.MediaPathName.Text); - if (!Provider.TrySaveAsset(candidatePath, out byte[] data)) - { - candidatePath = Path.Combine(baseWwiseAudioPath, mediaRelativePath); - if (!Provider.TrySaveAsset(candidatePath, out data)) - { - continue; - } - } - - var debugName = !string.IsNullOrEmpty(media.DebugName.Text) - ? media.DebugName.Text.SubstringBeforeLast('.') - : Path.GetFileNameWithoutExtension(mediaRelativePath); - - var namedPath = Path.Combine( - projectName, - "Content", - "WwiseAudio", - $"{debugName.Replace('\\', '/')} ({kvp.Key.LanguageName.Text})" - ); - - SaveAndPlaySound(namedPath, Path.GetExtension(mediaRelativePath).TrimStart('.'), data); + { + var candidatePath = Path.Combine(baseWwiseAudioPath, media.MediaPathName.Text); + var mediaRelativePath = media.MediaPathName.Text.Replace('\\', '/'); + + if (!Provider.TrySaveAsset(candidatePath, out byte[] data)) + { + candidatePath = Path.Combine(baseWwiseAudioPath, mediaRelativePath); + if (!Provider.TrySaveAsset(candidatePath, out data)) + { + continue; + } } + + var debugName = !string.IsNullOrEmpty(media.DebugName.Text) + ? media.DebugName.Text.SubstringBeforeLast('.') + : Path.GetFileNameWithoutExtension(mediaRelativePath); + + var namedPath = Path.Combine( + projectName, + "Content", + "WwiseAudio", + $"{debugName.Replace('\\', '/')} ({kvp.Key.LanguageName.Text})" + ); + + SaveAndPlaySound(namedPath, Path.GetExtension(mediaRelativePath).TrimStart('.'), data); + } } return false; } From 7581310ef3c685dd527ae08d3741cadbaa1af453 Mon Sep 17 00:00:00 2001 From: Masusder <59669685+Masusder@users.noreply.github.com> Date: Mon, 28 Apr 2025 13:18:48 +0200 Subject: [PATCH 04/16] Too long audio path fix --- FModel/ViewModels/CUE4ParseViewModel.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/FModel/ViewModels/CUE4ParseViewModel.cs b/FModel/ViewModels/CUE4ParseViewModel.cs index cdb6302d..70f0ed94 100644 --- a/FModel/ViewModels/CUE4ParseViewModel.cs +++ b/FModel/ViewModels/CUE4ParseViewModel.cs @@ -921,8 +921,17 @@ void SaveWemSound(HierarchySoundSfxVoice soundSfx) var wemId = soundSfx.SourceId; if (wwiseReader.WwiseEncodedMedias.TryGetValue(wemId.ToString(), out var wemData)) { - var debugName = kvp.Value.Value.DebugName.ToString(); - var outputPath = Path.Combine(audioEventPath.Replace($".{debugName}", ""), $"{debugName.Replace('\\', '/')} ({wemId})"); + var debugName = kvp.Value.Value.DebugName.ToString(); + var fileName = $"{debugName.Replace('\\', '/')} ({wemId})"; + var outputPath = Path.Combine(audioEventPath.Replace($".{debugName}", ""), fileName); + + // If file path is too long, audio player will fail + if (outputPath.StartsWith('/')) outputPath = outputPath[1..]; + if (Path.Combine(UserSettings.Default.AudioDirectory, outputPath).Length >= 250) + { + outputPath = Path.Combine(projectName, fileName); + } + SaveAndPlaySound(outputPath, "WEM", wemData); } } From 48d172446eca070be2538923a65b2527483780f0 Mon Sep 17 00:00:00 2001 From: Masusder <59669685+Masusder@users.noreply.github.com> Date: Mon, 28 Apr 2025 13:22:46 +0200 Subject: [PATCH 05/16] Update CUE4Parse --- CUE4Parse | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CUE4Parse b/CUE4Parse index 62376087..bd2c5179 160000 --- a/CUE4Parse +++ b/CUE4Parse @@ -1 +1 @@ -Subproject commit 62376087940c3fb9d8c8c12e8bb8adb8203bc7b6 +Subproject commit bd2c5179997c4507de84f7de76419f3bcdd8c2c1 From 9318838ea7671e0d65cdd736f5f2a27dfa0342e6 Mon Sep 17 00:00:00 2001 From: Masusder <59669685+Masusder@users.noreply.github.com> Date: Mon, 28 Apr 2025 17:28:04 +0200 Subject: [PATCH 06/16] Generic search for wwise directory --- FModel/ViewModels/CUE4ParseViewModel.cs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/FModel/ViewModels/CUE4ParseViewModel.cs b/FModel/ViewModels/CUE4ParseViewModel.cs index 70f0ed94..cbea2176 100644 --- a/FModel/ViewModels/CUE4ParseViewModel.cs +++ b/FModel/ViewModels/CUE4ParseViewModel.cs @@ -837,14 +837,26 @@ public void ExtractAndScroll(CancellationToken cancellationToken, string fullPat return false; } case UAkAudioEvent when isNone && pointer.Object.Value is UAkAudioEvent { EventCookedData: { } wwiseData }: - { + { + var files = Provider.Files.Values.ToList(); + + var bnkFile = files.FirstOrDefault(f => f.Path.Contains("/WwiseAudio/") && f.Path.EndsWith(".bnk", StringComparison.OrdinalIgnoreCase)); + string bnkDirectory = bnkFile != null ? Path.GetDirectoryName(bnkFile.Path.Replace('/', Path.DirectorySeparatorChar)) : null; + foreach (var kvp in wwiseData.EventLanguageMap) { if (!kvp.Value.HasValue) continue; var projectName = string.IsNullOrEmpty(Provider.ProjectName) ? "Game" : Provider.ProjectName; - var baseWwiseAudioPath = Path.Combine(projectName, "Content", "WwiseAudio", "Cooked"); + var baseWwiseAudioPath = Path.Combine(projectName, "Content", "WwiseAudio", "Cooked"); + + // If .bnk was found we will use that for base wwise directory + if (!string.IsNullOrEmpty(bnkDirectory)) + { + baseWwiseAudioPath = bnkDirectory; + } + var audioEventPath = pointer.Object.Value.GetPathName().Replace("Game", projectName); foreach (var soundBank in kvp.Value.Value.SoundBanks) From adc19388c9a7272879f80e6d8fcac4e60a5bb02e Mon Sep 17 00:00:00 2001 From: Masusder <59669685+Masusder@users.noreply.github.com> Date: Thu, 1 May 2025 19:06:52 +0200 Subject: [PATCH 07/16] Generic search for wwise directory (v2) and cute icon --- FModel/FModel.csproj | 2 + FModel/Resources/deadbydaylight.png | Bin 0 -> 680 bytes FModel/ViewModels/CUE4ParseViewModel.cs | 64 +++++++++++++++--------- FModel/Views/Resources/Resources.xaml | 5 +- 4 files changed, 47 insertions(+), 24 deletions(-) create mode 100644 FModel/Resources/deadbydaylight.png diff --git a/FModel/FModel.csproj b/FModel/FModel.csproj index dc171282..031da686 100644 --- a/FModel/FModel.csproj +++ b/FModel/FModel.csproj @@ -73,6 +73,7 @@ + @@ -181,6 +182,7 @@ + diff --git a/FModel/Resources/deadbydaylight.png b/FModel/Resources/deadbydaylight.png new file mode 100644 index 0000000000000000000000000000000000000000..45aeb77dee79519aac2f52a63740392ac3d94df5 GIT binary patch literal 680 zcmV;Z0$2TsP)Px%Vo5|nR5(wSluxMDRTPE4&wBRfm1dxrI4Y7HND=5jK`^sP(Ll063MvhI2{;H0tN%R+q%S~{dB9oV zJ@sUge}E@|QT3%HzXJ<_`RboZ=BQWu0SyB94EPW@r!Gr!6j%%FS3gK{9(Wm;p{4ws@dX!VCA zUjZfbW5BJ}!3}k|FGWc<0B-?r01x#Mt0BH zTzDS%zmoI;906WYAFjP5H-K$wS$9ezzYZJ+9tU0kjx@lcV@6Mg;{H2eF7STQIaPpV zZ4SCNd=mHrc&bzRDd4vz^2B`A$v)sq^-^sPvgxzHFY29Y+f-js7qqwxu(t-WL+3NV z+d$1miC&bx3T*ATaAXib`T3gqNVmi!m(>C;z(qBPo$1}y;$6TA_1u&wYM1UNZ1Lwc z#qGN)-J$Mo@oT_O>XqpL>`{02bg)nTs3%_9tvJczMrpZjau>C#wyU4^B;2Id6LM!I zsh9M6`<`3R_{8J`MQ)cCt3UT7d{G_km!wQkkKVKZCLT^wmaRFM*18QnBn;J_=P+pi O0000 f.Path.Contains("/WwiseAudio/") && f.Path.EndsWith(".bnk", StringComparison.OrdinalIgnoreCase)); - string bnkDirectory = bnkFile != null ? Path.GetDirectoryName(bnkFile.Path.Replace('/', Path.DirectorySeparatorChar)) : null; - foreach (var kvp in wwiseData.EventLanguageMap) { if (!kvp.Value.HasValue) continue; - var projectName = string.IsNullOrEmpty(Provider.ProjectName) ? "Game" : Provider.ProjectName; - var baseWwiseAudioPath = Path.Combine(projectName, "Content", "WwiseAudio", "Cooked"); - - // If .bnk was found we will use that for base wwise directory - if (!string.IsNullOrEmpty(bnkDirectory)) - { - baseWwiseAudioPath = bnkDirectory; - } - + var projectName = string.IsNullOrEmpty(Provider.ProjectName) ? "Game" : Provider.ProjectName; + var baseWwiseAudioPath = DetermineBaseWwiseAudioPath(projectName, kvp.Value.Value); var audioEventPath = pointer.Object.Value.GetPathName().Replace("Game", projectName); foreach (var soundBank in kvp.Value.Value.SoundBanks) @@ -951,16 +939,11 @@ void SaveWemSound(HierarchySoundSfxVoice soundSfx) foreach (var media in kvp.Value.Value.Media) { - var candidatePath = Path.Combine(baseWwiseAudioPath, media.MediaPathName.Text); - var mediaRelativePath = media.MediaPathName.Text.Replace('\\', '/'); + var mediaRelativePath = Path.Combine(baseWwiseAudioPath, media.MediaPathName.Text.Replace('\\', '/')); - if (!Provider.TrySaveAsset(candidatePath, out byte[] data)) - { - candidatePath = Path.Combine(baseWwiseAudioPath, mediaRelativePath); - if (!Provider.TrySaveAsset(candidatePath, out data)) - { - continue; - } + if (!Provider.TrySaveAsset(mediaRelativePath, out byte[] data)) + { + continue; } var debugName = !string.IsNullOrEmpty(media.DebugName.Text) @@ -1154,5 +1137,40 @@ public void ExportData(GameFile entry, bool updateUi = true) private static bool HasFlag(EBulkType a, EBulkType b) { return (a & b) == b; + } + + private string DetermineBaseWwiseAudioPath(string projectName, FWwiseEventCookedData value) + { + var files = Provider.Files.Values.ToList(); + + // Most common directory + var baseWwiseAudioPath = Path.Combine(projectName, "Content", "WwiseAudio"); + + var soundBankName = value.SoundBanks.FirstOrDefault().SoundBankPathName.ToString() ?? string.Empty; + var mediaPathName = value.Media.FirstOrDefault().MediaPathName.Text ?? string.Empty; + + if (!string.IsNullOrEmpty(soundBankName)) + { + var matchingFile = files.FirstOrDefault(f => f.Path.Contains(soundBankName)); + if (matchingFile != null) + { + var matchingDirectory = matchingFile.Path[..matchingFile.Path.LastIndexOf(soundBankName)]; + baseWwiseAudioPath = matchingDirectory.Replace('/', Path.DirectorySeparatorChar); + return baseWwiseAudioPath; + } + } + + if (!string.IsNullOrEmpty(mediaPathName)) + { + var matchingFile = files.FirstOrDefault(f => f.Path.Contains(mediaPathName)); + if (matchingFile != null) + { + var matchingDirectory = matchingFile.Path[..matchingFile.Path.LastIndexOf(mediaPathName)]; + baseWwiseAudioPath = matchingDirectory.Replace('/', Path.DirectorySeparatorChar); + return baseWwiseAudioPath; + } + } + + return baseWwiseAudioPath; } } diff --git a/FModel/Views/Resources/Resources.xaml b/FModel/Views/Resources/Resources.xaml index 1d1500ed..be269162 100644 --- a/FModel/Views/Resources/Resources.xaml +++ b/FModel/Views/Resources/Resources.xaml @@ -1,4 +1,4 @@ - + + + From c5e78e7ba754a0a729a91a45e7ba83edc964e731 Mon Sep 17 00:00:00 2001 From: Masusder <59669685+Masusder@users.noreply.github.com> Date: Fri, 2 May 2025 01:01:09 +0200 Subject: [PATCH 08/16] Music Track/Segment --- FModel/ViewModels/CUE4ParseViewModel.cs | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/FModel/ViewModels/CUE4ParseViewModel.cs b/FModel/ViewModels/CUE4ParseViewModel.cs index 443cf91b..a825cdfd 100644 --- a/FModel/ViewModels/CUE4ParseViewModel.cs +++ b/FModel/ViewModels/CUE4ParseViewModel.cs @@ -895,10 +895,20 @@ void TraverseAndSave(uint id) switch (hierarchy.Data) { - case HierarchySoundSfxVoice soundSfx: - SaveWemSound(soundSfx); - break; - + case HierarchySoundSfxVoice soundSfx: + SaveWemSound(soundSfx.SourceId); + break; + + case HierarchyMusicTrack musicTrack: + foreach (var playlist in musicTrack.Playlist) + SaveWemSound((uint) playlist.SourceID); + break; + + case HierarchyMusicSegment musicSegment: + foreach (var childId in musicSegment.ChildIDs) + TraverseAndSave(childId); + break; + case HierarchyRandomSequenceContainer randomContainer: foreach (var childId in randomContainer.ChildIDs) TraverseAndSave(childId); @@ -916,9 +926,8 @@ void TraverseAndSave(uint id) } } - void SaveWemSound(HierarchySoundSfxVoice soundSfx) + void SaveWemSound(uint wemId) { - var wemId = soundSfx.SourceId; if (wwiseReader.WwiseEncodedMedias.TryGetValue(wemId.ToString(), out var wemData)) { var debugName = kvp.Value.Value.DebugName.ToString(); From 31382e3dbe1d188425c588ec62fc79caffd02ed0 Mon Sep 17 00:00:00 2001 From: Masusder <59669685+Masusder@users.noreply.github.com> Date: Fri, 2 May 2025 21:15:04 +0200 Subject: [PATCH 09/16] In case project has "Game" in its name --- CUE4Parse | 2 +- FModel/Resources/deadbydaylight.png | Bin 680 -> 0 bytes FModel/ViewModels/CUE4ParseViewModel.cs | 4 ++-- FModel/Views/Resources/Resources.xaml | 3 --- 4 files changed, 3 insertions(+), 6 deletions(-) delete mode 100644 FModel/Resources/deadbydaylight.png diff --git a/CUE4Parse b/CUE4Parse index bd2c5179..8304388d 160000 --- a/CUE4Parse +++ b/CUE4Parse @@ -1 +1 @@ -Subproject commit bd2c5179997c4507de84f7de76419f3bcdd8c2c1 +Subproject commit 8304388d6bec644d298382942fe02b3e0ee1e7d0 diff --git a/FModel/Resources/deadbydaylight.png b/FModel/Resources/deadbydaylight.png deleted file mode 100644 index 45aeb77dee79519aac2f52a63740392ac3d94df5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 680 zcmV;Z0$2TsP)Px%Vo5|nR5(wSluxMDRTPE4&wBRfm1dxrI4Y7HND=5jK`^sP(Ll063MvhI2{;H0tN%R+q%S~{dB9oV zJ@sUge}E@|QT3%HzXJ<_`RboZ=BQWu0SyB94EPW@r!Gr!6j%%FS3gK{9(Wm;p{4ws@dX!VCA zUjZfbW5BJ}!3}k|FGWc<0B-?r01x#Mt0BH zTzDS%zmoI;906WYAFjP5H-K$wS$9ezzYZJ+9tU0kjx@lcV@6Mg;{H2eF7STQIaPpV zZ4SCNd=mHrc&bzRDd4vz^2B`A$v)sq^-^sPvgxzHFY29Y+f-js7qqwxu(t-WL+3NV z+d$1miC&bx3T*ATaAXib`T3gqNVmi!m(>C;z(qBPo$1}y;$6TA_1u&wYM1UNZ1Lwc z#qGN)-J$Mo@oT_O>XqpL>`{02bg)nTs3%_9tvJczMrpZjau>C#wyU4^B;2Id6LM!I zsh9M6`<`3R_{8J`MQ)cCt3UT7d{G_km!wQkkKVKZCLT^wmaRFM*18QnBn;J_=P+pi O0000 - - - From 60292cb24f26aa51c25fae3e936ac94804808fa8 Mon Sep 17 00:00:00 2001 From: Masusder <59669685+Masusder@users.noreply.github.com> Date: Fri, 9 May 2025 11:46:26 +0200 Subject: [PATCH 10/16] Music switch/rnd --- CUE4Parse | 2 +- FModel/FModel.csproj | 2 -- FModel/ViewModels/CUE4ParseViewModel.cs | 38 +++++++++++++++++-------- 3 files changed, 27 insertions(+), 15 deletions(-) diff --git a/CUE4Parse b/CUE4Parse index 8304388d..8d95611f 160000 --- a/CUE4Parse +++ b/CUE4Parse @@ -1 +1 @@ -Subproject commit 8304388d6bec644d298382942fe02b3e0ee1e7d0 +Subproject commit 8d95611f407181e07d849ebe594f0133e88c55f9 diff --git a/FModel/FModel.csproj b/FModel/FModel.csproj index 031da686..dc171282 100644 --- a/FModel/FModel.csproj +++ b/FModel/FModel.csproj @@ -73,7 +73,6 @@ - @@ -182,7 +181,6 @@ - diff --git a/FModel/ViewModels/CUE4ParseViewModel.cs b/FModel/ViewModels/CUE4ParseViewModel.cs index 3bbea72b..c32b5781 100644 --- a/FModel/ViewModels/CUE4ParseViewModel.cs +++ b/FModel/ViewModels/CUE4ParseViewModel.cs @@ -34,12 +34,13 @@ using CUE4Parse.UE4.Readers; using CUE4Parse.UE4.Shaders; using CUE4Parse.UE4.Versions; -using CUE4Parse.UE4.Wwise; +using CUE4Parse.UE4.Wwise; +using CUE4Parse.UE4.Wwise.Objects.HIRC; using CUE4Parse_Conversion; using CUE4Parse_Conversion.Sounds; using CUE4Parse.FileProvider.Objects; using CUE4Parse.UE4.Assets; -using CUE4Parse.UE4.Objects.UObject; +using CUE4Parse.UE4.Objects.UObject; using CUE4Parse.Utils; using EpicManifestParser; using EpicManifestParser.UE; @@ -60,7 +61,6 @@ using UE4Config.Parsing; using Application = System.Windows.Application; using FGuid = CUE4Parse.UE4.Objects.Core.Misc.FGuid; -using CUE4Parse.UE4.Wwise.Objects; namespace FModel.ViewModels; @@ -845,7 +845,9 @@ public void ExtractAndScroll(CancellationToken cancellationToken, string fullPat var projectName = string.IsNullOrEmpty(Provider.ProjectName) ? "Game" : Provider.ProjectName; var baseWwiseAudioPath = DetermineBaseWwiseAudioPath(projectName, kvp.Value.Value); - var audioEventPath = pointer.Object.Value.GetPathName().StartsWith("Game") ? pointer.Object.Value.GetPathName().Replace("Game", projectName) : pointer.Object.Value.GetPathName(); + var audioEventPath = pointer.Object.Value.GetPathName().StartsWith("/Game") + ? string.Concat(projectName, pointer.Object.Value.GetPathName().AsSpan(5)) + : pointer.Object.Value.GetPathName(); foreach (var soundBank in kvp.Value.Value.SoundBanks) { @@ -886,8 +888,10 @@ public void ExtractAndScroll(CancellationToken cancellationToken, string fullPat TraverseAndSave(eventAction.ReferencedId); } - } - + } + + // TODO: If EventActionPlay points to different soundbank ID than we're currently in, use `wwiseReader.IdToString` to convert to bank name, serialize it, and continue traversing from there + // TODO: It's possible for switch container to point to a different soundbank without referencing it in any way. I don't know how to handle that yet void TraverseAndSave(uint id) { if (!hierarchyTable.TryGetValue(id, out var hierarchy)) @@ -896,7 +900,17 @@ void TraverseAndSave(uint id) switch (hierarchy.Data) { case HierarchySoundSfxVoice soundSfx: - SaveWemSound(soundSfx.SourceId); + SaveWemSound(soundSfx.Source.SourceId); + break; + + case HierarchyMusicRandomSequenceContainer musicRandomSequenceContainer: + foreach (var childId in musicRandomSequenceContainer.ChildIds) + TraverseAndSave(childId); + break; + + case HierarchyMusicSwitchContainer musicSwitchContainer: + foreach (var childId in musicSwitchContainer.ChildIds) + TraverseAndSave(childId); break; case HierarchyMusicTrack musicTrack: @@ -905,22 +919,22 @@ void TraverseAndSave(uint id) break; case HierarchyMusicSegment musicSegment: - foreach (var childId in musicSegment.ChildIDs) + foreach (var childId in musicSegment.ChildIds) TraverseAndSave(childId); break; case HierarchyRandomSequenceContainer randomContainer: - foreach (var childId in randomContainer.ChildIDs) + foreach (var childId in randomContainer.ChildIds) TraverseAndSave(childId); break; case HierarchySwitchContainer switchContainer: - foreach (var childId in switchContainer.ChildIDs) + foreach (var childId in switchContainer.ChildIds) TraverseAndSave(childId); break; case HierarchyLayerContainer layerContainer: - foreach (var childId in layerContainer.ChildIDs) + foreach (var childId in layerContainer.ChildIds) TraverseAndSave(childId); break; } @@ -1061,7 +1075,7 @@ public void ShowMetadata(GameFile entry) private void SaveAndPlaySound(string fullPath, string ext, byte[] data) { - if (fullPath.StartsWith("/")) fullPath = fullPath[1..]; + if (fullPath.StartsWith('/')) fullPath = fullPath[1..]; var savedAudioPath = Path.Combine(UserSettings.Default.AudioDirectory, UserSettings.Default.KeepDirectoryStructure ? fullPath : fullPath.SubstringAfterLast('/')).Replace('\\', '/') + $".{ext.ToLowerInvariant()}"; From cf1f19f615a4965ce2f023ec5b21d69d39b59522 Mon Sep 17 00:00:00 2001 From: Masusder <59669685+Masusder@users.noreply.github.com> Date: Wed, 14 May 2025 22:23:45 +0200 Subject: [PATCH 11/16] Semi-support for cross-soundbanks audio events Unfortunately in case some game splits audio events across multiple soundbanks and given game has thousands of them, custom implementation for that game would be required --- CUE4Parse | 2 +- FModel/ViewModels/CUE4ParseViewModel.cs | 162 ++++++++++++++++++------ 2 files changed, 125 insertions(+), 39 deletions(-) diff --git a/CUE4Parse b/CUE4Parse index 8d95611f..330f706c 160000 --- a/CUE4Parse +++ b/CUE4Parse @@ -1 +1 @@ -Subproject commit 8d95611f407181e07d849ebe594f0133e88c55f9 +Subproject commit 330f706c1f04389dc4da37382365d727b5fc09a0 diff --git a/FModel/ViewModels/CUE4ParseViewModel.cs b/FModel/ViewModels/CUE4ParseViewModel.cs index c32b5781..c5520992 100644 --- a/FModel/ViewModels/CUE4ParseViewModel.cs +++ b/FModel/ViewModels/CUE4ParseViewModel.cs @@ -61,6 +61,7 @@ using UE4Config.Parsing; using Application = System.Windows.Application; using FGuid = CUE4Parse.UE4.Objects.Core.Misc.FGuid; +using CUE4Parse.UE4.Wwise.Objects; namespace FModel.ViewModels; @@ -759,7 +760,12 @@ public void ExtractAndScroll(CancellationToken cancellationToken, string fullPat if (CheckExport(cancellationToken, result.Package, i)) break; } - } + } + + private readonly Dictionary _wwiseHierarchyTables = []; + private readonly Dictionary _wwiseEncodedMedia = []; + private readonly List _wwiseLoadedSoundBanks = []; + private bool _completedWwiseFullBnkInit = false; private bool CheckExport(CancellationToken cancellationToken, IPackage pkg, int index, EBulkType bulk = EBulkType.None) // return true once you wanna stop searching for exports { @@ -838,17 +844,20 @@ public void ExtractAndScroll(CancellationToken cancellationToken, string fullPat } case UAkAudioEvent when isNone && pointer.Object.Value is UAkAudioEvent { EventCookedData: { } wwiseData }: { + var visitedWemIds = new HashSet(); // To prevent duplicates foreach (var kvp in wwiseData.EventLanguageMap) { if (!kvp.Value.HasValue) - continue; - + continue; + var projectName = string.IsNullOrEmpty(Provider.ProjectName) ? "Game" : Provider.ProjectName; var baseWwiseAudioPath = DetermineBaseWwiseAudioPath(projectName, kvp.Value.Value); var audioEventPath = pointer.Object.Value.GetPathName().StartsWith("/Game") ? string.Concat(projectName, pointer.Object.Value.GetPathName().AsSpan(5)) : pointer.Object.Value.GetPathName(); + BulkInitializeWwiseSoundBanks(baseWwiseAudioPath); + foreach (var soundBank in kvp.Value.Value.SoundBanks) { if (!soundBank.bContainsMedia) @@ -856,33 +865,19 @@ public void ExtractAndScroll(CancellationToken cancellationToken, string fullPat var soundBankName = soundBank.SoundBankPathName.ToString(); var soundBankPath = Path.Combine(baseWwiseAudioPath, soundBankName); - var audioEventId = kvp.Value.Value.EventId.ToString(); - - if (!Provider.TrySaveAsset(soundBankPath, out byte[] data)) - continue; - - using var ar = new FByteArchive(soundBankName, data); - var wwiseReader = new WwiseReader(ar); - - var hierarchyTable = new Dictionary(); - foreach (var hierarchy in wwiseReader.Hierarchies) - { - uint id = hierarchy.Data.Id; - - if (!hierarchyTable.ContainsKey(id)) - { - hierarchyTable.Add(id, hierarchy); - } - } - + var audioEventId = kvp.Value.Value.EventId.ToString(); + + TryLoadAndCacheSoundBank(soundBankPath, soundBankName, out _); + + var visitedDecisionNodes = new HashSet<(uint parentHierarchyId, uint audioNodeId)>(); // To prevent infinite loops long parsedId = long.Parse(audioEventId); uint parsedAudioEventId = (uint) parsedId; - if (hierarchyTable.TryGetValue(parsedAudioEventId, out var eventHierarchy) && + if (_wwiseHierarchyTables.TryGetValue(parsedAudioEventId, out var eventHierarchy) && eventHierarchy.Data is HierarchyEvent hierarchyEvent) { foreach (var actionId in hierarchyEvent.EventActionIds) { - if (!hierarchyTable.TryGetValue(actionId, out var actionHierarchy) || + if (!_wwiseHierarchyTables.TryGetValue(actionId, out var actionHierarchy) || actionHierarchy.Data is not HierarchyEventAction eventAction) continue; @@ -891,10 +886,9 @@ public void ExtractAndScroll(CancellationToken cancellationToken, string fullPat } // TODO: If EventActionPlay points to different soundbank ID than we're currently in, use `wwiseReader.IdToString` to convert to bank name, serialize it, and continue traversing from there - // TODO: It's possible for switch container to point to a different soundbank without referencing it in any way. I don't know how to handle that yet void TraverseAndSave(uint id) { - if (!hierarchyTable.TryGetValue(id, out var hierarchy)) + if (!_wwiseHierarchyTables.TryGetValue(id, out var hierarchy)) return; switch (hierarchy.Data) @@ -911,11 +905,28 @@ void TraverseAndSave(uint id) case HierarchyMusicSwitchContainer musicSwitchContainer: foreach (var childId in musicSwitchContainer.ChildIds) TraverseAndSave(childId); - break; + + foreach (var node in musicSwitchContainer.DecisionTree.Nodes) + foreach (var nodeChild in node.Children) + TraverseDecisionTreeNode(nodeChild, musicSwitchContainer.Id); + + void TraverseDecisionTreeNode(AkDecisionTreeNode node, uint parentHierarchyId) + { + var key = (parentHierarchyId, node.AudioNodeId); + if (!visitedDecisionNodes.Add(key)) + return; + + foreach (var nodeChildTraverse in node.Children) + { + TraverseAndSave(nodeChildTraverse.AudioNodeId); + TraverseDecisionTreeNode(nodeChildTraverse, parentHierarchyId); + } + } + break; case HierarchyMusicTrack musicTrack: foreach (var playlist in musicTrack.Playlist) - SaveWemSound((uint) playlist.SourceID); + SaveWemSound(playlist.SourceId); break; case HierarchyMusicSegment musicSegment: @@ -941,8 +952,11 @@ void TraverseAndSave(uint id) } void SaveWemSound(uint wemId) - { - if (wwiseReader.WwiseEncodedMedias.TryGetValue(wemId.ToString(), out var wemData)) + { + if (!visitedWemIds.Add(wemId)) + return; + + if (_wwiseEncodedMedia.TryGetValue(wemId.ToString(), out var wemData)) { var debugName = kvp.Value.Value.DebugName.ToString(); var fileName = $"{debugName.Replace('\\', '/')} ({wemId})"; @@ -973,10 +987,8 @@ void SaveWemSound(uint wemId) ? media.DebugName.Text.SubstringBeforeLast('.') : Path.GetFileNameWithoutExtension(mediaRelativePath); - var namedPath = Path.Combine( - projectName, - "Content", - "WwiseAudio", + var namedPath = Path.Combine( + baseWwiseAudioPath, $"{debugName.Replace('\\', '/')} ({kvp.Key.LanguageName.Text})" ); @@ -1166,15 +1178,14 @@ private string DetermineBaseWwiseAudioPath(string projectName, FWwiseEventCooked { var files = Provider.Files.Values.ToList(); - // Most common directory - var baseWwiseAudioPath = Path.Combine(projectName, "Content", "WwiseAudio"); + var baseWwiseAudioPath = Path.Combine(projectName, "Content", "WwiseAudio"); // Most common directory var soundBankName = value.SoundBanks.FirstOrDefault().SoundBankPathName.ToString() ?? string.Empty; var mediaPathName = value.Media.FirstOrDefault().MediaPathName.Text ?? string.Empty; if (!string.IsNullOrEmpty(soundBankName)) { - var matchingFile = files.FirstOrDefault(f => f.Path.Contains(soundBankName)); + GameFile matchingFile = files.FirstOrDefault(f => f.Path.Contains(soundBankName)); if (matchingFile != null) { var matchingDirectory = matchingFile.Path[..matchingFile.Path.LastIndexOf(soundBankName)]; @@ -1185,7 +1196,7 @@ private string DetermineBaseWwiseAudioPath(string projectName, FWwiseEventCooked if (!string.IsNullOrEmpty(mediaPathName)) { - var matchingFile = files.FirstOrDefault(f => f.Path.Contains(mediaPathName)); + GameFile matchingFile = files.FirstOrDefault(f => f.Path.Contains(mediaPathName)); if (matchingFile != null) { var matchingDirectory = matchingFile.Path[..matchingFile.Path.LastIndexOf(mediaPathName)]; @@ -1195,5 +1206,80 @@ private string DetermineBaseWwiseAudioPath(string projectName, FWwiseEventCooked } return baseWwiseAudioPath; + } + + private void BulkInitializeWwiseSoundBanks(string baseWwiseAudioPath) + { + if (_completedWwiseFullBnkInit) + return; + + // Important note: If game splits audio event hierarchies across multiple soundbanks and either of these limits is reached, given game requires custom loading implementation! + const long MAX_TOTAL_WWISE_SIZE = 2L * 1024 * 1024 * 1024; // 2 GB + const int MAX_BANK_FILES = 500; + + long totalLoadedSize = 0; + int totalLoadedBanks = 0; + + IEnumerable soundBankFiles = Provider.Files.Values + .Where(file => string.Equals(file.Extension, "bnk", StringComparison.OrdinalIgnoreCase)) + .Where(file => file.Path.StartsWith(baseWwiseAudioPath.Replace("\\", "/"), StringComparison.OrdinalIgnoreCase)); + + foreach (var soundbank in soundBankFiles) + { + if (totalLoadedBanks >= MAX_BANK_FILES) + break; + + string fullPath = soundbank.Path; + string relPath = fullPath[baseWwiseAudioPath.Length..].TrimStart('/', '\\'); + + if (!TryLoadAndCacheSoundBank(fullPath, relPath, out var size)) + continue; + + if (totalLoadedSize + size > MAX_TOTAL_WWISE_SIZE) + break; + + totalLoadedSize += size; + totalLoadedBanks += 1; + } + + _completedWwiseFullBnkInit = true; + } + + private bool TryLoadAndCacheSoundBank(string fullAbsolutePath, string relativePath, out long fileSize) + { + fileSize = 0; + + if (_wwiseLoadedSoundBanks.Contains(relativePath)) + return false; + + if (!Provider.TrySaveAsset(fullAbsolutePath, out byte[] data)) + return false; + + fileSize = data.LongLength; + + using var archive = new FByteArchive(relativePath, data); + var wwiseReader = new WwiseReader(archive); + + if (wwiseReader.Hierarchies != null) + { + foreach (var h in wwiseReader.Hierarchies) + { + uint id = h.Data.Id; + if (!_wwiseHierarchyTables.ContainsKey(id)) + _wwiseHierarchyTables[id] = h; + } + } + + if (wwiseReader.WwiseEncodedMedias != null) + { + foreach (var kv in wwiseReader.WwiseEncodedMedias) + { + if (!_wwiseEncodedMedia.ContainsKey(kv.Key)) + _wwiseEncodedMedia[kv.Key] = kv.Value; + } + } + + _wwiseLoadedSoundBanks.Add(relativePath); + return true; } } From c10ff9dfc239d2bb6ad36e1c1af9bcdd7dde045f Mon Sep 17 00:00:00 2001 From: Masusder <59669685+Masusder@users.noreply.github.com> Date: Sun, 18 May 2025 13:34:29 +0200 Subject: [PATCH 12/16] Debug helper --- CUE4Parse | 2 +- FModel/ViewModels/CUE4ParseViewModel.cs | 39 ++++++++++++++++++++++--- 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/CUE4Parse b/CUE4Parse index 330f706c..eba3ca67 160000 --- a/CUE4Parse +++ b/CUE4Parse @@ -1 +1 @@ -Subproject commit 330f706c1f04389dc4da37382365d727b5fc09a0 +Subproject commit eba3ca676b54d3cd07770b6f5e90ac25e7db7aa9 diff --git a/FModel/ViewModels/CUE4ParseViewModel.cs b/FModel/ViewModels/CUE4ParseViewModel.cs index c5520992..23652770 100644 --- a/FModel/ViewModels/CUE4ParseViewModel.cs +++ b/FModel/ViewModels/CUE4ParseViewModel.cs @@ -35,6 +35,7 @@ using CUE4Parse.UE4.Shaders; using CUE4Parse.UE4.Versions; using CUE4Parse.UE4.Wwise; +using CUE4Parse.UE4.Wwise.Objects; using CUE4Parse.UE4.Wwise.Objects.HIRC; using CUE4Parse_Conversion; using CUE4Parse_Conversion.Sounds; @@ -61,7 +62,6 @@ using UE4Config.Parsing; using Application = System.Windows.Application; using FGuid = CUE4Parse.UE4.Objects.Core.Misc.FGuid; -using CUE4Parse.UE4.Wwise.Objects; namespace FModel.ViewModels; @@ -869,7 +869,7 @@ public void ExtractAndScroll(CancellationToken cancellationToken, string fullPat TryLoadAndCacheSoundBank(soundBankPath, soundBankName, out _); - var visitedDecisionNodes = new HashSet<(uint parentHierarchyId, uint audioNodeId)>(); // To prevent infinite loops + var visitedDecisionNodes = new HashSet<(uint parentHierarchyId, uint audioNodeId)>(); // To prevent infinite loops (shouldn't happen, just in case) long parsedId = long.Parse(audioEventId); uint parsedAudioEventId = (uint) parsedId; if (_wwiseHierarchyTables.TryGetValue(parsedAudioEventId, out var eventHierarchy) && @@ -879,13 +879,26 @@ public void ExtractAndScroll(CancellationToken cancellationToken, string fullPat { if (!_wwiseHierarchyTables.TryGetValue(actionId, out var actionHierarchy) || actionHierarchy.Data is not HierarchyEventAction eventAction) - continue; + continue; + + // TODO: If EventActionPlay points to different soundbank ID than we're currently in, use `wwiseReader.IdToString` to convert to bank name, serialize it, and continue traversing from there + // This isn't needed if all soundbanks are loaded anyway + + //if (eventAction.EventActionType == EEventActionType.Play) + //{ + // var playActionData = (AkActionPlay) eventAction.ActionData; + // var bankId = playActionData.BankId; + // if (bankId != referencedSoundBankId) // I need to know what soundbank I'm currently in + // { + // var soundbankConvertedName = IdToString[referencedSoundBankId]; // I need IdToString from given soundbank + // TryLoadAndCacheSoundBank(Path.Combine(baseWwiseAudioPath, soundbankConvertedName + ".bnk"), soundbankConvertedName, out _); + // } + //} TraverseAndSave(eventAction.ReferencedId); } } - // TODO: If EventActionPlay points to different soundbank ID than we're currently in, use `wwiseReader.IdToString` to convert to bank name, serialize it, and continue traversing from there void TraverseAndSave(uint id) { if (!_wwiseHierarchyTables.TryGetValue(id, out var hierarchy)) @@ -1227,7 +1240,16 @@ private void BulkInitializeWwiseSoundBanks(string baseWwiseAudioPath) foreach (var soundbank in soundBankFiles) { if (totalLoadedBanks >= MAX_BANK_FILES) + { +#if DEBUG + Log.Debug("Reached maximum number of soundbank files to load. This game might require custom loading implementation (only necessary if audio event hierarchies are split across multiple soundbanks)."); + FLogger.Append(ELog.Debug, () => + { + FLogger.Text("Max soundbank files loaded. Custom loading may be required if hierarchies are split across multiple banks.", Constants.WHITE); + }); +#endif break; + } string fullPath = soundbank.Path; string relPath = fullPath[baseWwiseAudioPath.Length..].TrimStart('/', '\\'); @@ -1236,7 +1258,16 @@ private void BulkInitializeWwiseSoundBanks(string baseWwiseAudioPath) continue; if (totalLoadedSize + size > MAX_TOTAL_WWISE_SIZE) + { +#if DEBUG + Log.Debug("Reached maximum total size of soundbank files to load. This game might require custom loading implementation (only necessary if audio event hierarchies are split across multiple soundbanks)."); + FLogger.Append(ELog.Debug, () => + { + FLogger.Text("Reached max total soundbank size. Custom loading may be required if hierarchies are split across multiple banks.", Constants.WHITE); + }); +#endif break; + } totalLoadedSize += size; totalLoadedBanks += 1; From 746107875c0c97d9105a96424f916b865451d148 Mon Sep 17 00:00:00 2001 From: Masusder <59669685+Masusder@users.noreply.github.com> Date: Sun, 18 May 2025 15:36:35 +0200 Subject: [PATCH 13/16] Last bump --- CUE4Parse | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CUE4Parse b/CUE4Parse index eba3ca67..1c747072 160000 --- a/CUE4Parse +++ b/CUE4Parse @@ -1 +1 @@ -Subproject commit eba3ca676b54d3cd07770b6f5e90ac25e7db7aa9 +Subproject commit 1c7470728d264e81546c86a9044f6b15ad8f7908 From b75413306fa2063ec93ad0659f62e55e9e73eddb Mon Sep 17 00:00:00 2001 From: Masusder <59669685+Masusder@users.noreply.github.com> Date: Wed, 21 May 2025 11:55:33 +0200 Subject: [PATCH 14/16] Moved logic to CUE4Parse --- CUE4Parse | 2 +- FModel/ViewModels/CUE4ParseViewModel.cs | 312 +----------------------- 2 files changed, 12 insertions(+), 302 deletions(-) diff --git a/CUE4Parse b/CUE4Parse index 1c747072..f5d301f6 160000 --- a/CUE4Parse +++ b/CUE4Parse @@ -1 +1 @@ -Subproject commit 1c7470728d264e81546c86a9044f6b15ad8f7908 +Subproject commit f5d301f62824e15ca9bd00d0d7242f870164d87a diff --git a/FModel/ViewModels/CUE4ParseViewModel.cs b/FModel/ViewModels/CUE4ParseViewModel.cs index 214df160..16950b14 100644 --- a/FModel/ViewModels/CUE4ParseViewModel.cs +++ b/FModel/ViewModels/CUE4ParseViewModel.cs @@ -34,9 +34,7 @@ using CUE4Parse.UE4.Readers; using CUE4Parse.UE4.Shaders; using CUE4Parse.UE4.Versions; -using CUE4Parse.UE4.Wwise; -using CUE4Parse.UE4.Wwise.Objects; -using CUE4Parse.UE4.Wwise.Objects.HIRC; +using CUE4Parse.UE4.Wwise; using CUE4Parse_Conversion; using CUE4Parse_Conversion.Sounds; using CUE4Parse.FileProvider.Objects; @@ -117,7 +115,8 @@ public Snooper SnooperViewer public AssetsFolderViewModel AssetsFolder { get; } public SearchViewModel SearchVm { get; } public TabControlViewModel TabControl { get; } - public ConfigIni IoStoreOnDemand { get; } + public ConfigIni IoStoreOnDemand { get; } + public WwiseProvider WwiseProvider { get; set; } public CUE4ParseViewModel() { @@ -765,12 +764,7 @@ public void ExtractAndScroll(CancellationToken cancellationToken, string fullPat if (CheckExport(cancellationToken, result.Package, i)) break; } - } - - private readonly Dictionary _wwiseHierarchyTables = []; - private readonly Dictionary _wwiseEncodedMedia = []; - private readonly List _wwiseLoadedSoundBanks = []; - private bool _completedWwiseFullBnkInit = false; + } private bool CheckExport(CancellationToken cancellationToken, IPackage pkg, int index, EBulkType bulk = EBulkType.None) // return true once you wanna stop searching for exports { @@ -847,172 +841,15 @@ public void ExtractAndScroll(CancellationToken cancellationToken, string fullPat TabControl.SelectedTab.AddImage(sourceFile.SubstringAfterLast('/'), false, bitmap, false, updateUi); return false; } - case UAkAudioEvent when isNone && pointer.Object.Value is UAkAudioEvent { EventCookedData: { } wwiseData }: + case UAkAudioEvent when isNone && pointer.Object.Value is UAkAudioEvent audioEvent: { - var visitedWemIds = new HashSet(); // To prevent duplicates - foreach (var kvp in wwiseData.EventLanguageMap) - { - if (!kvp.Value.HasValue) - continue; - - var projectName = string.IsNullOrEmpty(Provider.ProjectName) ? "Game" : Provider.ProjectName; - var baseWwiseAudioPath = DetermineBaseWwiseAudioPath(projectName, kvp.Value.Value); - var audioEventPath = pointer.Object.Value.GetPathName().StartsWith("/Game") - ? string.Concat(projectName, pointer.Object.Value.GetPathName().AsSpan(5)) - : pointer.Object.Value.GetPathName(); - - BulkInitializeWwiseSoundBanks(baseWwiseAudioPath); - - foreach (var soundBank in kvp.Value.Value.SoundBanks) - { - if (!soundBank.bContainsMedia) - continue; - - var soundBankName = soundBank.SoundBankPathName.ToString(); - var soundBankPath = Path.Combine(baseWwiseAudioPath, soundBankName); - var audioEventId = kvp.Value.Value.EventId.ToString(); - - TryLoadAndCacheSoundBank(soundBankPath, soundBankName, out _); - - var visitedDecisionNodes = new HashSet<(uint parentHierarchyId, uint audioNodeId)>(); // To prevent infinite loops (shouldn't happen, just in case) - long parsedId = long.Parse(audioEventId); - uint parsedAudioEventId = (uint) parsedId; - if (_wwiseHierarchyTables.TryGetValue(parsedAudioEventId, out var eventHierarchy) && - eventHierarchy.Data is HierarchyEvent hierarchyEvent) - { - foreach (var actionId in hierarchyEvent.EventActionIds) - { - if (!_wwiseHierarchyTables.TryGetValue(actionId, out var actionHierarchy) || - actionHierarchy.Data is not HierarchyEventAction eventAction) - continue; - - // TODO: If EventActionPlay points to different soundbank ID than we're currently in, use `wwiseReader.IdToString` to convert to bank name, serialize it, and continue traversing from there - // This isn't needed if all soundbanks are loaded anyway - - //if (eventAction.EventActionType == EEventActionType.Play) - //{ - // var playActionData = (AkActionPlay) eventAction.ActionData; - // var bankId = playActionData.BankId; - // if (bankId != referencedSoundBankId) // I need to know what soundbank I'm currently in - // { - // var soundbankConvertedName = IdToString[referencedSoundBankId]; // I need IdToString from given soundbank - // TryLoadAndCacheSoundBank(Path.Combine(baseWwiseAudioPath, soundbankConvertedName + ".bnk"), soundbankConvertedName, out _); - // } - //} - - TraverseAndSave(eventAction.ReferencedId); - } - } - - void TraverseAndSave(uint id) - { - if (!_wwiseHierarchyTables.TryGetValue(id, out var hierarchy)) - return; - - switch (hierarchy.Data) - { - case HierarchySoundSfxVoice soundSfx: - SaveWemSound(soundSfx.Source.SourceId); - break; - - case HierarchyMusicRandomSequenceContainer musicRandomSequenceContainer: - foreach (var childId in musicRandomSequenceContainer.ChildIds) - TraverseAndSave(childId); - break; + WwiseProvider ??= new WwiseProvider(Provider, audioEvent); - case HierarchyMusicSwitchContainer musicSwitchContainer: - foreach (var childId in musicSwitchContainer.ChildIds) - TraverseAndSave(childId); - - foreach (var node in musicSwitchContainer.DecisionTree.Nodes) - foreach (var nodeChild in node.Children) - TraverseDecisionTreeNode(nodeChild, musicSwitchContainer.Id); - - void TraverseDecisionTreeNode(AkDecisionTreeNode node, uint parentHierarchyId) - { - var key = (parentHierarchyId, node.AudioNodeId); - if (!visitedDecisionNodes.Add(key)) - return; - - foreach (var nodeChildTraverse in node.Children) - { - TraverseAndSave(nodeChildTraverse.AudioNodeId); - TraverseDecisionTreeNode(nodeChildTraverse, parentHierarchyId); - } - } - break; - - case HierarchyMusicTrack musicTrack: - foreach (var playlist in musicTrack.Playlist) - SaveWemSound(playlist.SourceId); - break; - - case HierarchyMusicSegment musicSegment: - foreach (var childId in musicSegment.ChildIds) - TraverseAndSave(childId); - break; - - case HierarchyRandomSequenceContainer randomContainer: - foreach (var childId in randomContainer.ChildIds) - TraverseAndSave(childId); - break; - - case HierarchySwitchContainer switchContainer: - foreach (var childId in switchContainer.ChildIds) - TraverseAndSave(childId); - break; - - case HierarchyLayerContainer layerContainer: - foreach (var childId in layerContainer.ChildIds) - TraverseAndSave(childId); - break; - } - } - - void SaveWemSound(uint wemId) - { - if (!visitedWemIds.Add(wemId)) - return; - - if (_wwiseEncodedMedia.TryGetValue(wemId.ToString(), out var wemData)) - { - var debugName = kvp.Value.Value.DebugName.ToString(); - var fileName = $"{debugName.Replace('\\', '/')} ({wemId})"; - var outputPath = Path.Combine(audioEventPath.Replace($".{debugName}", ""), fileName); - - // If file path is too long, audio player will fail - if (outputPath.StartsWith('/')) outputPath = outputPath[1..]; - if (Path.Combine(UserSettings.Default.AudioDirectory, outputPath).Length >= 250) - { - outputPath = Path.Combine(projectName, fileName); - } - - SaveAndPlaySound(outputPath, "WEM", wemData); - } - } - } - - foreach (var media in kvp.Value.Value.Media) - { - var mediaRelativePath = Path.Combine(baseWwiseAudioPath, media.MediaPathName.Text.Replace('\\', '/')); - - if (!Provider.TrySaveAsset(mediaRelativePath, out byte[] data)) - { - continue; - } - - var debugName = !string.IsNullOrEmpty(media.DebugName.Text) - ? media.DebugName.Text.SubstringBeforeLast('.') - : Path.GetFileNameWithoutExtension(mediaRelativePath); - - var namedPath = Path.Combine( - baseWwiseAudioPath, - $"{debugName.Replace('\\', '/')} ({kvp.Key.LanguageName.Text})" - ); - - SaveAndPlaySound(namedPath, Path.GetExtension(mediaRelativePath).TrimStart('.'), data); - } - } + var extractedSounds = WwiseProvider.ExtractAudioEventSounds(audioEvent, UserSettings.Default.AudioDirectory); + foreach (var sound in extractedSounds) + { + SaveAndPlaySound(sound.OutputPath, sound.Extension, sound.Data); + } return false; } case UAkMediaAssetData when isNone: @@ -1190,132 +1027,5 @@ public void ExportData(GameFile entry, bool updateUi = true) private static bool HasFlag(EBulkType a, EBulkType b) { return (a & b) == b; - } - - private string DetermineBaseWwiseAudioPath(string projectName, FWwiseEventCookedData value) - { - var files = Provider.Files.Values.ToList(); - - var baseWwiseAudioPath = Path.Combine(projectName, "Content", "WwiseAudio"); // Most common directory - - var soundBankName = value.SoundBanks.FirstOrDefault().SoundBankPathName.ToString() ?? string.Empty; - var mediaPathName = value.Media.FirstOrDefault().MediaPathName.Text ?? string.Empty; - - if (!string.IsNullOrEmpty(soundBankName)) - { - GameFile matchingFile = files.FirstOrDefault(f => f.Path.Contains(soundBankName)); - if (matchingFile != null) - { - var matchingDirectory = matchingFile.Path[..matchingFile.Path.LastIndexOf(soundBankName)]; - baseWwiseAudioPath = matchingDirectory.Replace('/', Path.DirectorySeparatorChar); - return baseWwiseAudioPath; - } - } - - if (!string.IsNullOrEmpty(mediaPathName)) - { - GameFile matchingFile = files.FirstOrDefault(f => f.Path.Contains(mediaPathName)); - if (matchingFile != null) - { - var matchingDirectory = matchingFile.Path[..matchingFile.Path.LastIndexOf(mediaPathName)]; - baseWwiseAudioPath = matchingDirectory.Replace('/', Path.DirectorySeparatorChar); - return baseWwiseAudioPath; - } - } - - return baseWwiseAudioPath; - } - - private void BulkInitializeWwiseSoundBanks(string baseWwiseAudioPath) - { - if (_completedWwiseFullBnkInit) - return; - - // Important note: If game splits audio event hierarchies across multiple soundbanks and either of these limits is reached, given game requires custom loading implementation! - const long MAX_TOTAL_WWISE_SIZE = 2L * 1024 * 1024 * 1024; // 2 GB - const int MAX_BANK_FILES = 500; - - long totalLoadedSize = 0; - int totalLoadedBanks = 0; - - IEnumerable soundBankFiles = Provider.Files.Values - .Where(file => string.Equals(file.Extension, "bnk", StringComparison.OrdinalIgnoreCase)) - .Where(file => file.Path.StartsWith(baseWwiseAudioPath.Replace("\\", "/"), StringComparison.OrdinalIgnoreCase)); - - foreach (var soundbank in soundBankFiles) - { - if (totalLoadedBanks >= MAX_BANK_FILES) - { -#if DEBUG - Log.Debug("Reached maximum number of soundbank files to load. This game might require custom loading implementation (only necessary if audio event hierarchies are split across multiple soundbanks)."); - FLogger.Append(ELog.Debug, () => - { - FLogger.Text("Max soundbank files loaded. Custom loading may be required if hierarchies are split across multiple banks.", Constants.WHITE); - }); -#endif - break; - } - - string fullPath = soundbank.Path; - string relPath = fullPath[baseWwiseAudioPath.Length..].TrimStart('/', '\\'); - - if (!TryLoadAndCacheSoundBank(fullPath, relPath, out var size)) - continue; - - if (totalLoadedSize + size > MAX_TOTAL_WWISE_SIZE) - { -#if DEBUG - Log.Debug("Reached maximum total size of soundbank files to load. This game might require custom loading implementation (only necessary if audio event hierarchies are split across multiple soundbanks)."); - FLogger.Append(ELog.Debug, () => - { - FLogger.Text("Reached max total soundbank size. Custom loading may be required if hierarchies are split across multiple banks.", Constants.WHITE); - }); -#endif - break; - } - - totalLoadedSize += size; - totalLoadedBanks += 1; - } - - _completedWwiseFullBnkInit = true; - } - - private bool TryLoadAndCacheSoundBank(string fullAbsolutePath, string relativePath, out long fileSize) - { - fileSize = 0; - - if (_wwiseLoadedSoundBanks.Contains(relativePath)) - return false; - - if (!Provider.TrySaveAsset(fullAbsolutePath, out byte[] data)) - return false; - - fileSize = data.LongLength; - - using var archive = new FByteArchive(relativePath, data); - var wwiseReader = new WwiseReader(archive); - - if (wwiseReader.Hierarchies != null) - { - foreach (var h in wwiseReader.Hierarchies) - { - uint id = h.Data.Id; - if (!_wwiseHierarchyTables.ContainsKey(id)) - _wwiseHierarchyTables[id] = h; - } - } - - if (wwiseReader.WwiseEncodedMedias != null) - { - foreach (var kv in wwiseReader.WwiseEncodedMedias) - { - if (!_wwiseEncodedMedia.ContainsKey(kv.Key)) - _wwiseEncodedMedia[kv.Key] = kv.Value; - } - } - - _wwiseLoadedSoundBanks.Add(relativePath); - return true; } } From 06416cb7f88fc547de951d1c411c3322f3e70f48 Mon Sep 17 00:00:00 2001 From: Masusder <59669685+Masusder@users.noreply.github.com> Date: Sun, 25 May 2025 21:03:21 +0200 Subject: [PATCH 15/16] Bump CUE4Parse --- CUE4Parse | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CUE4Parse b/CUE4Parse index f5d301f6..2c550160 160000 --- a/CUE4Parse +++ b/CUE4Parse @@ -1 +1 @@ -Subproject commit f5d301f62824e15ca9bd00d0d7242f870164d87a +Subproject commit 2c550160f238380d0ce5712450f92adad0d044fc From 61da5a9ae08e95574bb03618473c47a21cbb0fd7 Mon Sep 17 00:00:00 2001 From: Masusder <59669685+Masusder@users.noreply.github.com> Date: Tue, 3 Jun 2025 18:47:11 +0200 Subject: [PATCH 16/16] Lazy loaded provider --- CUE4Parse | 2 +- FModel/ViewModels/CUE4ParseViewModel.cs | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CUE4Parse b/CUE4Parse index 2c550160..80c90735 160000 --- a/CUE4Parse +++ b/CUE4Parse @@ -1 +1 @@ -Subproject commit 2c550160f238380d0ce5712450f92adad0d044fc +Subproject commit 80c9073527c1588c9d28159e8672c94a96ec510b diff --git a/FModel/ViewModels/CUE4ParseViewModel.cs b/FModel/ViewModels/CUE4ParseViewModel.cs index 16950b14..9b468d16 100644 --- a/FModel/ViewModels/CUE4ParseViewModel.cs +++ b/FModel/ViewModels/CUE4ParseViewModel.cs @@ -116,7 +116,8 @@ public Snooper SnooperViewer public SearchViewModel SearchVm { get; } public TabControlViewModel TabControl { get; } public ConfigIni IoStoreOnDemand { get; } - public WwiseProvider WwiseProvider { get; set; } + private Lazy _wwiseProviderLazy; + public WwiseProvider WwiseProvider => _wwiseProviderLazy.Value; public CUE4ParseViewModel() { @@ -264,7 +265,8 @@ await _threadWorkerView.Begin(cancellationToken => } } - Provider.Initialize(); + Provider.Initialize(); + _wwiseProviderLazy = new Lazy(() => new WwiseProvider(Provider)); Log.Information($"{Provider.Versions.Game} ({Provider.Versions.Platform}) | Archives: x{Provider.UnloadedVfs.Count} | AES: x{Provider.RequiredKeys.Count} | Loose Files: x{Provider.Files.Count}"); }); } @@ -843,9 +845,7 @@ public void ExtractAndScroll(CancellationToken cancellationToken, string fullPat } case UAkAudioEvent when isNone && pointer.Object.Value is UAkAudioEvent audioEvent: { - WwiseProvider ??= new WwiseProvider(Provider, audioEvent); - - var extractedSounds = WwiseProvider.ExtractAudioEventSounds(audioEvent, UserSettings.Default.AudioDirectory); + var extractedSounds = WwiseProvider.ExtractAudioEventSounds(audioEvent); foreach (var sound in extractedSounds) { SaveAndPlaySound(sound.OutputPath, sound.Extension, sound.Data);