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/27] 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 class CUE4ParseViewModel : ViewModel 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/27] 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 class CustomDirectory : ViewModel 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 @@ public class GameSelectorViewModel : ViewModel 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 @@ public class GameSelectorViewModel : ViewModel 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/27] 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 SkiaSharp; 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 class CUE4ParseViewModel : ViewModel { 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/27] 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 @@ public class CUE4ParseViewModel : ViewModel 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/27] 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/27] 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 class CUE4ParseViewModel : ViewModel 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/27] 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 @@ public class CUE4ParseViewModel : ViewModel 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 class CUE4ParseViewModel : ViewModel 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/27] 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 @@ public class CUE4ParseViewModel : ViewModel 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 @@ public class CUE4ParseViewModel : ViewModel } } - 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/27] 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/27] 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.Oodle.Objects; 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 SkiaSharp; 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 class CUE4ParseViewModel : ViewModel 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 class CUE4ParseViewModel : ViewModel 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 @@ public class CUE4ParseViewModel : ViewModel 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 @@ public class CUE4ParseViewModel : ViewModel 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 class CUE4ParseViewModel : ViewModel 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/27] 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 SkiaSharp; 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 class CUE4ParseViewModel : ViewModel 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 class CUE4ParseViewModel : ViewModel } 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 class CUE4ParseViewModel : ViewModel 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 class CUE4ParseViewModel : ViewModel } // 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 @@ public class CUE4ParseViewModel : ViewModel 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 @@ public class CUE4ParseViewModel : ViewModel } 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 @@ public class CUE4ParseViewModel : ViewModel ? 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 @@ public class CUE4ParseViewModel : ViewModel { 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 @@ public class CUE4ParseViewModel : ViewModel 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 @@ public class CUE4ParseViewModel : ViewModel } 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/27] 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.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_Conversion; using CUE4Parse_Conversion.Sounds; @@ -61,7 +62,6 @@ using SkiaSharp; 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 class CUE4ParseViewModel : ViewModel 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 class CUE4ParseViewModel : ViewModel { 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 @@ public class CUE4ParseViewModel : ViewModel 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 @@ public class CUE4ParseViewModel : ViewModel 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/27] 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/27] 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.Oodle.Objects; 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 class CUE4ParseViewModel : ViewModel 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 class CUE4ParseViewModel : ViewModel 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 class CUE4ParseViewModel : ViewModel 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; + WwiseProvider ??= new WwiseProvider(Provider, audioEvent); - 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; - - 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 class CUE4ParseViewModel : ViewModel 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/27] 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/27] 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 class CUE4ParseViewModel : ViewModel 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 @@ public class CUE4ParseViewModel : ViewModel } } - 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 class CUE4ParseViewModel : ViewModel } 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); From ff58050fe94536f2a3e143da93fe4f994a49600c Mon Sep 17 00:00:00 2001 From: Masusder <59669685+Masusder@users.noreply.github.com> Date: Sun, 22 Jun 2025 16:08:17 +0200 Subject: [PATCH 17/27] Bump --- CUE4Parse | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CUE4Parse b/CUE4Parse index 80c90735..62ffed7e 160000 --- a/CUE4Parse +++ b/CUE4Parse @@ -1 +1 @@ -Subproject commit 80c9073527c1588c9d28159e8672c94a96ec510b +Subproject commit 62ffed7e682a3dc83f1c6c53778640b9d4bfd669 From 233b7ab03b24b50ab98937a82f30e392017a30a3 Mon Sep 17 00:00:00 2001 From: LongerWarrior Date: Mon, 7 Jul 2025 00:17:56 +0300 Subject: [PATCH 18/27] Need For Speed Mobile and Game For Peace fixes --- CUE4Parse | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CUE4Parse b/CUE4Parse index 566bc1f7..8389f31d 160000 --- a/CUE4Parse +++ b/CUE4Parse @@ -1 +1 @@ -Subproject commit 566bc1f731b993a09b50772fef8de4d9d5e36243 +Subproject commit 8389f31d4ca4add18377ca5ba7bcd93066b13362 From 62b103f958865b2c7681ef2220c8a2ed844551b3 Mon Sep 17 00:00:00 2001 From: Chompster86 Date: Sun, 6 Jul 2025 23:10:14 -0400 Subject: [PATCH 19/27] Fixed Burst Weapon's Fire Rates (#575) * Update BaseIconStats.cs Fixed Burst Fire Rates * Hides fire rate if it = 0 Made it so if the equation = 0, it hides fire rate * Fixed if firing rate = 1 Made it so if firing rate = 1, it hides the burst firing rate equation and just shows the firing rate. * Neverness to Everness and Terminull Brigade support Add new loading mode that skips patched assets * BinaryConfig.ini support * Update CUE4Parse --------- Co-authored-by: GhostScissors <79089473+GhostScissors@users.noreply.github.com> --- CUE4Parse | 2 +- FModel/Creator/Bases/FN/BaseIconStats.cs | 9 ++++++++- FModel/ViewModels/CUE4ParseViewModel.cs | 11 +++++++++++ 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/CUE4Parse b/CUE4Parse index 8389f31d..5a03d963 160000 --- a/CUE4Parse +++ b/CUE4Parse @@ -1 +1 @@ -Subproject commit 8389f31d4ca4add18377ca5ba7bcd93066b13362 +Subproject commit 5a03d96378c214e924e0cacfaa02969ca4a27236 diff --git a/FModel/Creator/Bases/FN/BaseIconStats.cs b/FModel/Creator/Bases/FN/BaseIconStats.cs index c7c1e485..a297b0e0 100644 --- a/FModel/Creator/Bases/FN/BaseIconStats.cs +++ b/FModel/Creator/Bases/FN/BaseIconStats.cs @@ -95,6 +95,8 @@ public class BaseIconStats : BaseIcon weaponRowValue.TryGetValue(out float heatMax, "OverheatingMaxValue"); //Maximum heat overheating weapons can hold before they need to cool off weaponRowValue.TryGetValue(out float heatPerShot, "OverheatHeatingValue"); //Heat generated per shot on overheat weapons weaponRowValue.TryGetValue(out float overheatCooldown, "OverheatedCooldownDelay"); //Cooldown after a weapon reaches its maximum heat capacity + weaponRowValue.TryGetValue(out int cartridgePerFire, "CartridgePerFire"); //Amount of bullets shot after pressing the fire button once + weaponRowValue.TryGetValue(out float burstFiringRate, "BurstFiringRate"); //Item firing rate during a burst, value is shots per second { var multiplier = bpc != 0f ? bpc : 1; if (dmgPb != 0f) @@ -122,7 +124,12 @@ public class BaseIconStats : BaseIcon _statistics.Add(new IconStat(Utils.GetLocalizedResource("", "068239DD4327B36124498C9C5F61C038", "Magazine Size"), clipSize, 40)); } - if (firingRate != 0f) + var burstEquation = cartridgePerFire / (((cartridgePerFire - 1f) / burstFiringRate) + (1f / firingRate)); + if (burstEquation != 0f) + { + _statistics.Add(new IconStat(Utils.GetLocalizedResource("", "27B80BA44805ABD5A2D2BAB2902B250C", "Fire Rate"), burstEquation, 11)); + } + else if (firingRate != 0f) { _statistics.Add(new IconStat(Utils.GetLocalizedResource("", "27B80BA44805ABD5A2D2BAB2902B250C", "Fire Rate"), firingRate, 11)); } diff --git a/FModel/ViewModels/CUE4ParseViewModel.cs b/FModel/ViewModels/CUE4ParseViewModel.cs index c99133ba..275d5b16 100644 --- a/FModel/ViewModels/CUE4ParseViewModel.cs +++ b/FModel/ViewModels/CUE4ParseViewModel.cs @@ -39,6 +39,7 @@ using CUE4Parse_Conversion; using CUE4Parse_Conversion.Sounds; using CUE4Parse.FileProvider.Objects; using CUE4Parse.UE4.Assets; +using CUE4Parse.UE4.BinaryConfig; using CUE4Parse.UE4.Objects.UObject; using CUE4Parse.Utils; using EpicManifestParser; @@ -580,6 +581,16 @@ public class CUE4ParseViewModel : ViewModel break; } + case "ini" when entry.Name.Contains("BinaryConfig"): + { + var ar = entry.CreateReader(); + var configCache = new FConfigCacheIni(ar); + + TabControl.SelectedTab.Highlighter = AvalonExtensions.HighlighterSelector("json"); + TabControl.SelectedTab.SetDocumentText(JsonConvert.SerializeObject(configCache, Formatting.Indented), saveProperties, updateUi); + + break; + } case "upluginmanifest": case "uproject": case "manifest": From 1f1d2ae3c2cf3a47fadfe0fefd11c0e161fa57db Mon Sep 17 00:00:00 2001 From: LongerWarrior Date: Tue, 8 Jul 2025 16:59:09 +0300 Subject: [PATCH 20/27] =?UTF-8?q?Tony=20Hawk's=E2=84=A2=20Pro=20Skater?= =?UTF-8?q?=E2=84=A2=203=20+=204=20support?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CUE4Parse | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CUE4Parse b/CUE4Parse index 5a03d963..7bf781ff 160000 --- a/CUE4Parse +++ b/CUE4Parse @@ -1 +1 @@ -Subproject commit 5a03d96378c214e924e0cacfaa02969ca4a27236 +Subproject commit 7bf781ffd8871c48fdca8be86576b313e023ce7c From cfbee11e584975053a2658293a84235dd737da8b Mon Sep 17 00:00:00 2001 From: Asval Date: Thu, 10 Jul 2025 19:24:51 +0200 Subject: [PATCH 21/27] added user setting --- CUE4Parse | 2 +- FModel/Settings/UserSettings.cs | 7 +++++++ FModel/ViewModels/CUE4ParseViewModel.cs | 27 +++++++++++++------------ FModel/Views/SettingsView.xaml | 6 ++++++ 4 files changed, 28 insertions(+), 14 deletions(-) diff --git a/CUE4Parse b/CUE4Parse index 62ffed7e..3a333f2d 160000 --- a/CUE4Parse +++ b/CUE4Parse @@ -1 +1 @@ -Subproject commit 62ffed7e682a3dc83f1c6c53778640b9d4bfd669 +Subproject commit 3a333f2d95ec77ce162ea80ab05be0b279e43807 diff --git a/FModel/Settings/UserSettings.cs b/FModel/Settings/UserSettings.cs index 66c126bf..6abd1d1e 100644 --- a/FModel/Settings/UserSettings.cs +++ b/FModel/Settings/UserSettings.cs @@ -439,6 +439,13 @@ namespace FModel.Settings set => SetProperty(ref _cameraMode, value); } + private int _wwiseMaxBnkPrefetch; + public int WwiseMaxBnkPrefetch + { + get => _wwiseMaxBnkPrefetch; + set => SetProperty(ref _wwiseMaxBnkPrefetch, value); + } + private int _previewMaxTextureSize = 1024; public int PreviewMaxTextureSize { diff --git a/FModel/ViewModels/CUE4ParseViewModel.cs b/FModel/ViewModels/CUE4ParseViewModel.cs index fa23482b..11a43195 100644 --- a/FModel/ViewModels/CUE4ParseViewModel.cs +++ b/FModel/ViewModels/CUE4ParseViewModel.cs @@ -41,7 +41,6 @@ using CUE4Parse.FileProvider.Objects; using CUE4Parse.UE4.Assets; using CUE4Parse.UE4.BinaryConfig; using CUE4Parse.UE4.Objects.UObject; -using CUE4Parse.UE4.Objects.UObject; using CUE4Parse.Utils; using EpicManifestParser; using EpicManifestParser.UE; @@ -117,8 +116,8 @@ public class CUE4ParseViewModel : ViewModel public AssetsFolderViewModel AssetsFolder { get; } public SearchViewModel SearchVm { get; } public TabControlViewModel TabControl { get; } - public ConfigIni IoStoreOnDemand { get; } - private Lazy _wwiseProviderLazy; + public ConfigIni IoStoreOnDemand { get; } + private Lazy _wwiseProviderLazy; public WwiseProvider WwiseProvider => _wwiseProviderLazy.Value; public CUE4ParseViewModel() @@ -268,8 +267,8 @@ public class CUE4ParseViewModel : ViewModel } } - Provider.Initialize(); - _wwiseProviderLazy = new Lazy(() => new WwiseProvider(Provider)); + Provider.Initialize(); + _wwiseProviderLazy = new Lazy(() => new WwiseProvider(Provider, UserSettings.Default.WwiseMaxBnkPrefetch)); Log.Information($"{Provider.Versions.Game} ({Provider.Versions.Platform}) | Archives: x{Provider.UnloadedVfs.Count} | AES: x{Provider.RequiredKeys.Count} | Loose Files: x{Provider.Files.Count}"); }); } @@ -674,9 +673,11 @@ public class CUE4ParseViewModel : ViewModel var archive = entry.CreateReader(); var wwise = new WwiseReader(archive); TabControl.SelectedTab.SetDocumentText(JsonConvert.SerializeObject(wwise, Formatting.Indented), saveProperties, updateUi); - foreach (var (name, data) in wwise.WwiseEncodedMedias) + + var medias = WwiseProvider.ExtractBankSounds(wwise); + foreach (var media in medias) { - SaveAndPlaySound(entry.Path.SubstringBeforeWithLast('/') + name, "WEM", data); + SaveAndPlaySound(media.OutputPath, media.Extension, media.Data); } break; @@ -857,12 +858,12 @@ public class CUE4ParseViewModel : ViewModel return false; } case UAkAudioEvent when isNone && pointer.Object.Value is UAkAudioEvent audioEvent: - { - var extractedSounds = WwiseProvider.ExtractAudioEventSounds(audioEvent); - foreach (var sound in extractedSounds) - { - SaveAndPlaySound(sound.OutputPath, sound.Extension, sound.Data); - } + { + var extractedSounds = WwiseProvider.ExtractAudioEventSounds(audioEvent); + foreach (var sound in extractedSounds) + { + SaveAndPlaySound(sound.OutputPath, sound.Extension, sound.Data); + } return false; } case UAkMediaAssetData when isNone: diff --git a/FModel/Views/SettingsView.xaml b/FModel/Views/SettingsView.xaml index d8a79e24..b59ad295 100644 --- a/FModel/Views/SettingsView.xaml +++ b/FModel/Views/SettingsView.xaml @@ -44,6 +44,7 @@ + @@ -227,6 +228,11 @@ + + + From 364df7f40257ac48379dd355e1e3f5e7b808ed77 Mon Sep 17 00:00:00 2001 From: Asval Date: Tue, 15 Jul 2025 22:30:46 +0200 Subject: [PATCH 22/27] fixed opengl being triggered when not needed --- FModel/ViewModels/CUE4ParseViewModel.cs | 9 ++++++++- FModel/Views/Snooper/Options.cs | 3 +-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/FModel/ViewModels/CUE4ParseViewModel.cs b/FModel/ViewModels/CUE4ParseViewModel.cs index 11a43195..0ddfe660 100644 --- a/FModel/ViewModels/CUE4ParseViewModel.cs +++ b/FModel/ViewModels/CUE4ParseViewModel.cs @@ -78,6 +78,13 @@ public class CUE4ParseViewModel : ViewModel set => SetProperty(ref _modelIsOverwritingMaterial, value); } + private bool _modelIsWaitingAnimation; + public bool ModelIsWaitingAnimation + { + get => _modelIsWaitingAnimation; + set => SetProperty(ref _modelIsWaitingAnimation, value); + } + public bool IsSnooperOpen => _snooper is { Exists: true, IsVisible: true }; private Snooper _snooper; public Snooper SnooperViewer @@ -909,7 +916,7 @@ public class CUE4ParseViewModel : ViewModel SnooperViewer.Run(); return true; } - case UAnimSequenceBase when isNone && UserSettings.Default.PreviewAnimations || SnooperViewer.Renderer.Options.ModelIsWaitingAnimation: + case UAnimSequenceBase when isNone && UserSettings.Default.PreviewAnimations || ModelIsWaitingAnimation: { // animate all animations using their specified skeleton or when we explicitly asked for a loaded model to be animated (ignoring whether we wanted to preview animations) SnooperViewer.Renderer.Animate(pointer.Object.Value); diff --git a/FModel/Views/Snooper/Options.cs b/FModel/Views/Snooper/Options.cs index 0f55dd27..7b4f434b 100644 --- a/FModel/Views/Snooper/Options.cs +++ b/FModel/Views/Snooper/Options.cs @@ -16,7 +16,6 @@ namespace FModel.Views.Snooper; public class Options { public FGuid SelectedModel { get; private set; } - public bool ModelIsWaitingAnimation { get; private set; } public int SelectedSection { get; private set; } public int SelectedMorph { get; private set; } public int SelectedAnimation{ get; private set; } @@ -238,7 +237,7 @@ public class Options public void AnimateMesh(bool value) { - ModelIsWaitingAnimation = value; + Services.ApplicationService.ApplicationView.CUE4Parse.ModelIsWaitingAnimation = value; } public void ResetModelsLightsAnimations() From e6ef05092e69d2a588582bcdbceb30e082d384b3 Mon Sep 17 00:00:00 2001 From: Asval Date: Tue, 15 Jul 2025 22:58:54 +0200 Subject: [PATCH 23/27] added back transforms manual inputs --- FModel/Views/Snooper/Renderer.cs | 2 +- FModel/Views/Snooper/SnimGui.cs | 3 ++ FModel/Views/Snooper/Transform.cs | 51 +++++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 1 deletion(-) diff --git a/FModel/Views/Snooper/Renderer.cs b/FModel/Views/Snooper/Renderer.cs index a2317032..0a3a6795 100644 --- a/FModel/Views/Snooper/Renderer.cs +++ b/FModel/Views/Snooper/Renderer.cs @@ -123,7 +123,7 @@ public class Renderer : IDisposable public void Animate(UObject anim) { - if (!Options.ModelIsWaitingAnimation) + if (!Services.ApplicationService.ApplicationView.CUE4Parse.ModelIsWaitingAnimation) { if (anim is UAnimSequenceBase animBase) { diff --git a/FModel/Views/Snooper/SnimGui.cs b/FModel/Views/Snooper/SnimGui.cs index c8cf6f40..27112d6f 100644 --- a/FModel/Views/Snooper/SnimGui.cs +++ b/FModel/Views/Snooper/SnimGui.cs @@ -624,6 +624,9 @@ Snooper aims to give an accurate preview of models, materials, skeletal animatio ImGui.EndTable(); } + ImGui.SeparatorText("Manual Inputs"); + model.Transforms[model.SelectedInstance].ImGuiTransform(s.Renderer.CameraOp.Speed / 100f); + ImGui.EndTabItem(); } diff --git a/FModel/Views/Snooper/Transform.cs b/FModel/Views/Snooper/Transform.cs index ec9d270a..5f81874f 100644 --- a/FModel/Views/Snooper/Transform.cs +++ b/FModel/Views/Snooper/Transform.cs @@ -1,5 +1,6 @@ using System.Numerics; using CUE4Parse.UE4.Objects.Core.Math; +using ImGuiNET; namespace FModel.Views.Snooper; @@ -48,5 +49,55 @@ public class Transform ModifyLocal(_saved.Value); } + public void ImGuiTransform(float speed) + { + const float width = 100f; + + if (ImGui.TreeNode("Position")) + { + ImGui.SetNextItemWidth(width); + ImGui.DragFloat("X", ref Position.X, speed, 0f, 0f, "%.2f m"); + + ImGui.SetNextItemWidth(width); + ImGui.DragFloat("Y", ref Position.Y, speed, 0f, 0f, "%.2f m"); + + ImGui.SetNextItemWidth(width); + ImGui.DragFloat("Z", ref Position.Z, speed, 0f, 0f, "%.2f m"); + + ImGui.TreePop(); + } + + if (ImGui.TreeNode("Rotation")) + { + ImGui.SetNextItemWidth(width); + ImGui.DragFloat("W", ref Rotation.W, .005f, 0f, 0f, "%.3f rad"); + + ImGui.SetNextItemWidth(width); + ImGui.DragFloat("X", ref Rotation.X, .005f, 0f, 0f, "%.3f rad"); + + ImGui.SetNextItemWidth(width); + ImGui.DragFloat("Y", ref Rotation.Y, .005f, 0f, 0f, "%.3f rad"); + + ImGui.SetNextItemWidth(width); + ImGui.DragFloat("Z", ref Rotation.Z, .005f, 0f, 0f, "%.3f rad"); + + ImGui.TreePop(); + } + + if (ImGui.TreeNode("Scale")) + { + ImGui.SetNextItemWidth(width); + ImGui.DragFloat("X", ref Scale.X, speed, 0f, 0f, "%.3f"); + + ImGui.SetNextItemWidth(width); + ImGui.DragFloat("Y", ref Scale.Y, speed, 0f, 0f, "%.3f"); + + ImGui.SetNextItemWidth(width); + ImGui.DragFloat("Z", ref Scale.Z, speed, 0f, 0f, "%.3f"); + + ImGui.TreePop(); + } + } + public override string ToString() => Matrix.Translation.ToString(); } From 1f572792063c510b0ebd261c2f460cdbbbd1c126 Mon Sep 17 00:00:00 2001 From: LongerWarrior Date: Wed, 16 Jul 2025 17:25:27 +0300 Subject: [PATCH 24/27] AshEchoes, Ashen, Dauntless support --- CUE4Parse | 2 +- FModel/ViewModels/CUE4ParseViewModel.cs | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CUE4Parse b/CUE4Parse index 3a333f2d..e9b676e9 160000 --- a/CUE4Parse +++ b/CUE4Parse @@ -1 +1 @@ -Subproject commit 3a333f2d95ec77ce162ea80ab05be0b279e43807 +Subproject commit e9b676e97985f3d452d21e844202b546472d9bff diff --git a/FModel/ViewModels/CUE4ParseViewModel.cs b/FModel/ViewModels/CUE4ParseViewModel.cs index 0ddfe660..0f3bdad2 100644 --- a/FModel/ViewModels/CUE4ParseViewModel.cs +++ b/FModel/ViewModels/CUE4ParseViewModel.cs @@ -164,6 +164,7 @@ public class CUE4ParseViewModel : ViewModel [ new(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData) + "\\KONAMI\\eFootball\\ST\\Download") ], SearchOption.AllDirectories, versionContainer, pathComparer), + _ when versionContainer.Game is EGame.GAME_AshEchoes => new AEDefaultFileProvider(gameDirectory, SearchOption.AllDirectories, versionContainer, pathComparer), _ => new DefaultFileProvider(gameDirectory, SearchOption.AllDirectories, versionContainer, pathComparer) }; @@ -607,6 +608,7 @@ public class CUE4ParseViewModel : ViewModel case "uplugin": case "archive": case "dnearchive": // Banishers: Ghosts of New Eden + case "stUMeta": // LIS: Double Exposure case "vmodule": case "uparam": // Steel Hunters case "verse": @@ -758,6 +760,8 @@ public class CUE4ParseViewModel : ViewModel break; } case "res": // just skip + case "luac": // compiled lua + case "bytes": // wuthering waves break; default: { From 4bc93c05e3f3e204846e4630b347aed9b54b209c Mon Sep 17 00:00:00 2001 From: LongerWarrior Date: Wed, 16 Jul 2025 17:34:46 +0300 Subject: [PATCH 25/27] AshEchoes, Ashen, Dauntless support --- FModel/ViewModels/CUE4ParseViewModel.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/FModel/ViewModels/CUE4ParseViewModel.cs b/FModel/ViewModels/CUE4ParseViewModel.cs index 0f3bdad2..c3279387 100644 --- a/FModel/ViewModels/CUE4ParseViewModel.cs +++ b/FModel/ViewModels/CUE4ParseViewModel.cs @@ -30,7 +30,8 @@ using CUE4Parse.UE4.IO; using CUE4Parse.UE4.Localization; using CUE4Parse.UE4.Objects.Core.Serialization; using CUE4Parse.UE4.Objects.Engine; -using CUE4Parse.UE4.Oodle.Objects; +using CUE4Parse.UE4.Oodle.Objects; +using CUE4Parse.UE4.Pak; using CUE4Parse.UE4.Readers; using CUE4Parse.UE4.Shaders; using CUE4Parse.UE4.Versions; From 66bdddd6865a45a220e972a0f20e997271d3217c Mon Sep 17 00:00:00 2001 From: "Marcel K." <106357974+Ka1serM@users.noreply.github.com> Date: Thu, 17 Jul 2025 13:10:35 +0200 Subject: [PATCH 26/27] make HDR export optional --- CUE4Parse | 2 +- FModel/Settings/UserSettings.cs | 10 +++++++++- FModel/ViewModels/TabControlViewModel.cs | 2 +- FModel/Views/SettingsView.xaml | 20 +++++++++++++------- 4 files changed, 24 insertions(+), 10 deletions(-) diff --git a/CUE4Parse b/CUE4Parse index e9b676e9..ff2e96ba 160000 --- a/CUE4Parse +++ b/CUE4Parse @@ -1 +1 @@ -Subproject commit e9b676e97985f3d452d21e844202b546472d9bff +Subproject commit ff2e96ba05bd732564bb7db2d995334bf68062e7 diff --git a/FModel/Settings/UserSettings.cs b/FModel/Settings/UserSettings.cs index 6abd1d1e..b04b2831 100644 --- a/FModel/Settings/UserSettings.cs +++ b/FModel/Settings/UserSettings.cs @@ -73,7 +73,8 @@ namespace FModel.Settings CompressionFormat = Default.CompressionFormat, Platform = Default.CurrentDir.TexturePlatform, ExportMorphTargets = Default.SaveMorphTargets, - ExportMaterials = Default.SaveEmbeddedMaterials + ExportMaterials = Default.SaveEmbeddedMaterials, + ExportHdrTexturesAsHdr = Default.SaveHdrTexturesAsHdr }; private bool _showChangelog = true; @@ -508,5 +509,12 @@ namespace FModel.Settings get => _saveSkeletonAsMesh; set => SetProperty(ref _saveSkeletonAsMesh, value); } + + private bool _saveHdrTexturesAsHdr = true; + public bool SaveHdrTexturesAsHdr + { + get => _saveHdrTexturesAsHdr; + set => SetProperty(ref _saveHdrTexturesAsHdr, value); + } } } diff --git a/FModel/ViewModels/TabControlViewModel.cs b/FModel/ViewModels/TabControlViewModel.cs index f9f9eb96..9131169a 100644 --- a/FModel/ViewModels/TabControlViewModel.cs +++ b/FModel/ViewModels/TabControlViewModel.cs @@ -105,7 +105,7 @@ public class TabImage : ViewModel if (PixelFormatUtils.IsHDR(bitmap.PixelFormat) || (UserSettings.Default.TextureExportFormat != ETextureFormat.Jpeg && UserSettings.Default.TextureExportFormat != ETextureFormat.Png)) { - ImageBuffer = bitmap.Encode(UserSettings.Default.TextureExportFormat, out var ext); + ImageBuffer = bitmap.Encode(UserSettings.Default.TextureExportFormat, UserSettings.Default.SaveHdrTexturesAsHdr, out var ext); ExportName += "." + ext; } else diff --git a/FModel/Views/SettingsView.xaml b/FModel/Views/SettingsView.xaml index b59ad295..4c85c8fd 100644 --- a/FModel/Views/SettingsView.xaml +++ b/FModel/Views/SettingsView.xaml @@ -323,6 +323,7 @@ + @@ -464,10 +465,15 @@ IsChecked="{Binding SaveSkeletonAsMesh, Source={x:Static local:Settings.UserSettings.Default}, Mode=TwoWay}" Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" Margin="0 5 0 0"/> - + + - - + + + @@ -476,8 +482,8 @@ - - + @@ -486,8 +492,8 @@ - - + From 75bdfc17973e08c2ec4c2f35c5964b7136f12759 Mon Sep 17 00:00:00 2001 From: Asval Date: Mon, 21 Jul 2025 18:11:12 +0200 Subject: [PATCH 27/27] automated GetInternalSID using Athena_SeasonTitles --- CUE4Parse | 2 +- FModel/Creator/Bases/FN/BaseCommunity.cs | 12 +--- FModel/Creator/Bases/FN/BaseIcon.cs | 47 ++++------------ FModel/ViewModels/CUE4ParseViewModel.cs | 3 +- FModel/Views/SettingsView.xaml | 70 ++++++++++++------------ 5 files changed, 53 insertions(+), 81 deletions(-) diff --git a/CUE4Parse b/CUE4Parse index ff2e96ba..b5a3fd7f 160000 --- a/CUE4Parse +++ b/CUE4Parse @@ -1 +1 @@ -Subproject commit ff2e96ba05bd732564bb7db2d995334bf68062e7 +Subproject commit b5a3fd7fc4463740e885ea1d17dc9a1697b3b9b9 diff --git a/FModel/Creator/Bases/FN/BaseCommunity.cs b/FModel/Creator/Bases/FN/BaseCommunity.cs index 2492426d..39c91fa4 100644 --- a/FModel/Creator/Bases/FN/BaseCommunity.cs +++ b/FModel/Creator/Bases/FN/BaseCommunity.cs @@ -123,15 +123,9 @@ public class BaseCommunity : BaseIcon { if (!bShort) return base.GetCosmeticSeason(seasonNumber); var s = seasonNumber["Cosmetics.Filter.Season.".Length..]; - (int chapterIdx, int seasonIdx) = GetInternalSID(int.Parse(s)); - return s switch - { - "10" => $"C{chapterIdx} SX", - "27" => $"Fortnite: OG", - "32" => $"Fortnite: Remix", - "35" => $"C{chapterIdx} MS1", - _ => $"C{chapterIdx} S{seasonIdx}" - }; + (string chapterIdx, string seasonIdx, bool onlySeason) = GetInternalSID(s); + var prefix = int.TryParse(seasonIdx, out _) ? "S" : ""; + return onlySeason ? $"{prefix}{seasonIdx}" : $"C{chapterIdx} {prefix}{seasonIdx}"; } private new void DrawBackground(SKCanvas c) diff --git a/FModel/Creator/Bases/FN/BaseIcon.cs b/FModel/Creator/Bases/FN/BaseIcon.cs index e131f358..00c1e675 100644 --- a/FModel/Creator/Bases/FN/BaseIcon.cs +++ b/FModel/Creator/Bases/FN/BaseIcon.cs @@ -221,55 +221,32 @@ public class BaseIcon : UCreator return Utils.RemoveHtmlTags(string.Format(format, name)); } - protected (int, int) GetInternalSID(int number) + protected (string, string, bool) GetInternalSID(string number) { - static int GetSeasonsInChapter(int chapter) => chapter switch - { - 1 => 10, - 2 => 8, - 3 => 4, - 4 => 5, - 5 => 5, - _ => 10 - }; + if (!Utils.TryLoadObject("FortniteGame/Plugins/GameFeatures/BattlePassBase/Content/DataTables/Athena_SeasonTitles.Athena_SeasonTitles", out UDataTable seasonTitles) || + !seasonTitles.TryGetDataTableRow(number, StringComparison.InvariantCulture, out var row) || + !row.TryGetValue(out FText chapterText, "DisplayChapterText") || + !row.TryGetValue(out FText seasonText, "DisplaySeasonText") || + !row.TryGetValue(out FName displayType, "DisplayType")) + return (string.Empty, string.Empty, true); - var chapterIdx = 0; - var seasonIdx = 0; - while (number > 0) - { - var seasonsInChapter = GetSeasonsInChapter(++chapterIdx); - if (number > seasonsInChapter) - number -= seasonsInChapter; - else - { - seasonIdx = number; - number = 0; - } - } - return (chapterIdx, seasonIdx); + var onlySeason = displayType.Text.EndsWith("::OnlySeason") || (chapterText.Text == seasonText.Text && !int.TryParse(seasonText.Text, out _)); + return (chapterText.Text, seasonText.Text, onlySeason); } protected string GetCosmeticSeason(string seasonNumber) { var s = seasonNumber["Cosmetics.Filter.Season.".Length..]; - var initial = int.Parse(s); - (int chapterIdx, int seasonIdx) = GetInternalSID(initial); + (string chapterIdx, string seasonIdx, bool onlySeason) = GetInternalSID(s); var season = Utils.GetLocalizedResource("AthenaSeasonItemDefinitionInternal", "SeasonTextFormat", "Season {0}"); var introduced = Utils.GetLocalizedResource("Fort.Cosmetics", "CosmeticItemDescription_Season", "\nIntroduced in {0}."); - if (s == "10") return Utils.RemoveHtmlTags(string.Format(introduced, string.Format(season, "X"))); - if (initial <= 10) return Utils.RemoveHtmlTags(string.Format(introduced, string.Format(season, s))); + if (onlySeason) return Utils.RemoveHtmlTags(string.Format(introduced, string.Format(season, seasonIdx))); var chapter = Utils.GetLocalizedResource("AthenaSeasonItemDefinitionInternal", "ChapterTextFormat", "Chapter {0}"); var chapterFormat = Utils.GetLocalizedResource("AthenaSeasonItemDefinitionInternal", "ChapterSeasonTextFormat", "{0}, {1}"); var d = string.Format(chapterFormat, string.Format(chapter, chapterIdx), string.Format(season, seasonIdx)); - return s switch - { - "27" => Utils.RemoveHtmlTags(string.Format(introduced, string.Format("Fortnite: OG"))), - "32" => Utils.RemoveHtmlTags(string.Format(introduced, string.Format("Fortnite: Remix"))), - "35" => Utils.RemoveHtmlTags(string.Format(introduced, string.Format(chapterFormat, string.Format(chapter, chapterIdx), string.Format("MS1")))), - _ => Utils.RemoveHtmlTags(string.Format(introduced, d)) - }; + return Utils.RemoveHtmlTags(string.Format(introduced, d)); } protected void CheckGameplayTags(FInstancedStruct[] dataList) diff --git a/FModel/ViewModels/CUE4ParseViewModel.cs b/FModel/ViewModels/CUE4ParseViewModel.cs index c3279387..8716cd98 100644 --- a/FModel/ViewModels/CUE4ParseViewModel.cs +++ b/FModel/ViewModels/CUE4ParseViewModel.cs @@ -30,7 +30,7 @@ using CUE4Parse.UE4.IO; using CUE4Parse.UE4.Localization; using CUE4Parse.UE4.Objects.Core.Serialization; using CUE4Parse.UE4.Objects.Engine; -using CUE4Parse.UE4.Oodle.Objects; +using CUE4Parse.UE4.Oodle.Objects; using CUE4Parse.UE4.Pak; using CUE4Parse.UE4.Readers; using CUE4Parse.UE4.Shaders; @@ -39,6 +39,7 @@ using CUE4Parse.UE4.Wwise; using CUE4Parse_Conversion; using CUE4Parse_Conversion.Sounds; using CUE4Parse.FileProvider.Objects; +using CUE4Parse.GameTypes.AshEchoes.FileProvider; using CUE4Parse.UE4.Assets; using CUE4Parse.UE4.BinaryConfig; using CUE4Parse.UE4.Objects.UObject; diff --git a/FModel/Views/SettingsView.xaml b/FModel/Views/SettingsView.xaml index 4c85c8fd..00a7c1f1 100644 --- a/FModel/Views/SettingsView.xaml +++ b/FModel/Views/SettingsView.xaml @@ -465,42 +465,42 @@ IsChecked="{Binding SaveSkeletonAsMesh, Source={x:Static local:Settings.UserSettings.Default}, Mode=TwoWay}" Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" Margin="0 5 0 0"/> - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" Margin="0 5 0 5"/>