Merge pull request #564 from Masusder/dev
Some checks are pending
FModel QA Builder / build (push) Waiting to run

Support for audio events from Wwise soundbanks
This commit is contained in:
Valentin 2025-07-15 23:01:16 +02:00 committed by GitHub
commit 4a9a44139d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 109 additions and 26 deletions

@ -1 +1 @@
Subproject commit 7bf781ffd8871c48fdca8be86576b313e023ce7c
Subproject commit 3a333f2d95ec77ce162ea80ab05be0b279e43807

View File

@ -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<CustomDirectory>
{
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<CustomDirectory>();
}

View File

@ -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
{

View File

@ -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
@ -117,6 +124,8 @@ public class CUE4ParseViewModel : ViewModel
public SearchViewModel SearchVm { get; }
public TabControlViewModel TabControl { get; }
public ConfigIni IoStoreOnDemand { get; }
private Lazy<WwiseProvider> _wwiseProviderLazy;
public WwiseProvider WwiseProvider => _wwiseProviderLazy.Value;
public CUE4ParseViewModel()
{
@ -266,6 +275,7 @@ public class CUE4ParseViewModel : ViewModel
}
Provider.Initialize();
_wwiseProviderLazy = new Lazy<WwiseProvider>(() => 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}");
});
}
@ -670,9 +680,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;
@ -852,22 +864,12 @@ 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:
{
foreach (var kvp in wwiseData.EventLanguageMap)
var extractedSounds = WwiseProvider.ExtractAudioEventSounds(audioEvent);
foreach (var sound in extractedSounds)
{
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);
}
SaveAndPlaySound(sound.OutputPath, sound.Extension, sound.Data);
}
return false;
}
@ -914,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);
@ -961,7 +963,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()}";

View File

@ -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;

View File

@ -1,4 +1,4 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:FModel"
xmlns:system="clr-namespace:System;assembly=mscorlib"

View File

@ -44,6 +44,7 @@
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
@ -227,6 +228,11 @@
<TextBlock Grid.Row="17" Grid.Column="0" Text="Serialize Inlined Shader Maps" VerticalAlignment="Center" Margin="0 0 0 5" />
<CheckBox Grid.Row="17" Grid.Column="2" Content="{Binding IsChecked, RelativeSource={RelativeSource Self}, Converter={x:Static converters:BoolToToggleConverter.Instance}}"
IsChecked="{Binding ReadShaderMaps, Source={x:Static local:Settings.UserSettings.Default}, Mode=TwoWay}" Margin="0 5 0 10"/>
<TextBlock Grid.Row="18" Grid.Column="0" Text="Max Wwise Bank (.BNK) Prefetch" VerticalAlignment="Center" Margin="0 0 0 5" />
<Slider Grid.Row="18" Grid.Column="2" Grid.ColumnSpan="5" TickPlacement="None" Minimum="0" Maximum="512" Ticks="0,2,4,8,16,32,64,128,256,512"
AutoToolTipPlacement="BottomRight" IsMoveToPointEnabled="True" IsSnapToTickEnabled="True" Margin="0 5 0 5"
Value="{Binding WwiseMaxBnkPrefetch, Source={x:Static local:Settings.UserSettings.Default}, Mode=TwoWay}"/>
</Grid>
</DataTemplate>
<DataTemplate x:Key="CreatorTemplate">

View File

@ -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()

View File

@ -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)
{

View File

@ -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();
}

View File

@ -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();
}