From 5a7fa36d6597e9f3fb5ecbb263cf94d64ef1a585 Mon Sep 17 00:00:00 2001 From: Metious <71298690+Metious@users.noreply.github.com> Date: Fri, 13 Sep 2024 19:48:04 +0330 Subject: [PATCH] feat: Custom FMOD support for PlayableDirector (#554) * Added attaching functionality for FMOD channel * Added FMOD support for playable behaviour --- Nautilus/Handlers/CustomSoundHandler.cs | 37 ++++ Nautilus/Nautilus.csproj | 1 + Nautilus/Patchers/CustomSoundPatcher.cs | 216 +++++++++++++++++++++++- 3 files changed, 252 insertions(+), 2 deletions(-) diff --git a/Nautilus/Handlers/CustomSoundHandler.cs b/Nautilus/Handlers/CustomSoundHandler.cs index 72fa7e90..b9020c16 100644 --- a/Nautilus/Handlers/CustomSoundHandler.cs +++ b/Nautilus/Handlers/CustomSoundHandler.cs @@ -169,4 +169,41 @@ public static bool TryGetCustomSoundChannel(int id, out Channel channel) { return CustomSoundPatcher.EmitterPlayedChannels.TryGetValue(id, out channel); } + + + /// + /// Attaches the specified channel to the given transform. This results in the sound position following the . + /// + /// The channel to attach. + /// The transform which the channel will follow. + public static void AttachChannelToGameObject(Channel channel, Transform transform) + { + var index = CustomSoundPatcher.AttachedChannels.FindIndex(x => x.Channel.handle == channel.handle); + var attachedChannel = new CustomSoundPatcher.AttachedChannel(channel, transform); + if (index == -1) + { + CustomSoundPatcher.AttachedChannels.Add(attachedChannel); + } + else + { + CustomSoundPatcher.AttachedChannels[index] = attachedChannel; + } + + CustomSoundPatcher.SetChannel3DAttributes(channel, transform); + } + + /// + /// Detaches the specified channel from any game object. + /// + /// The channel to detach. + public static void DetachChannelFromGameObject(Channel channel) + { + var index = CustomSoundPatcher.AttachedChannels.FindIndex(x => x.Channel.handle == channel.handle); + if (index == -1) + { + InternalLogger.Warn($"{nameof(CustomSoundHandler)}: The specified channel is not attached to any game object."); + } + + CustomSoundPatcher.AttachedChannels.RemoveAt(index); + } } \ No newline at end of file diff --git a/Nautilus/Nautilus.csproj b/Nautilus/Nautilus.csproj index ae1db35c..2cecc44b 100644 --- a/Nautilus/Nautilus.csproj +++ b/Nautilus/Nautilus.csproj @@ -46,6 +46,7 @@ + diff --git a/Nautilus/Patchers/CustomSoundPatcher.cs b/Nautilus/Patchers/CustomSoundPatcher.cs index 6ca5378b..7d4a2de7 100644 --- a/Nautilus/Patchers/CustomSoundPatcher.cs +++ b/Nautilus/Patchers/CustomSoundPatcher.cs @@ -4,19 +4,26 @@ using FMODUnity; using HarmonyLib; using Nautilus.FMod.Interfaces; +using Nautilus.Handlers; using Nautilus.Utility; using UnityEngine; +using UnityEngine.Playables; namespace Nautilus.Patchers; internal class CustomSoundPatcher { + internal record struct AttachedChannel(Channel Channel, Transform Transform); + internal static readonly SelfCheckingDictionary CustomSounds = new("CustomSounds"); internal static readonly SelfCheckingDictionary CustomSoundBuses = new("CustomSoundBuses"); internal static readonly SelfCheckingDictionary CustomFModSounds = new("CustoomFModSounds"); internal static readonly Dictionary EmitterPlayedChannels = new(); + internal static List AttachedChannels = new(); private static readonly Dictionary PlayedChannels = new(); + private static readonly Dictionary PlayableBehaviorChannels = new(); + private static readonly List _attachedChannelsToRemove = new(); internal static void Patch(Harmony harmony) { @@ -68,7 +75,212 @@ public static bool FMODExtension_GetLength_Prefix(string path, ref int __result) __result = 0; return false; } + + [HarmonyPatch(typeof(FMODEventPlayableBehavior), nameof(FMODEventPlayableBehavior.PerformSeek))] + [HarmonyPrefix] + public static bool FMODEventPlayableBehavior_PerformSeek_Prefix(FMODEventPlayableBehavior __instance) + { + if (!PlayableBehaviorChannels.TryGetValue(__instance, out var channel)) + { + return true; + } + + if (__instance.seek < 0) + { + return true; + } + + channel.setPosition((uint)__instance.seek, TIMEUNIT.MS); + __instance.seek = -1; + return false; + } + + [HarmonyPatch(typeof(FMODEventPlayableBehavior), nameof(FMODEventPlayableBehavior.PlayEvent))] + [HarmonyPrefix] + public static bool FMODEventPlayableBehavior_PlayEvent_Prefix(FMODEventPlayableBehavior __instance) + { + if (string.IsNullOrEmpty(__instance.eventName)) + { + return true; + } + + if (string.IsNullOrEmpty(__instance.eventName) || !CustomSounds.TryGetValue(__instance.eventName, out Sound soundEvent) + && !CustomFModSounds.ContainsKey(__instance.eventName)) return true; + + Channel channel; + if (CustomFModSounds.TryGetValue(__instance.eventName, out var fModSound)) + { + if (!fModSound.TryPlaySound(out channel)) + return false; + } + else if (CustomSoundBuses.TryGetValue(__instance.eventName, out Bus bus)) + { + if (!AudioUtils.TryPlaySound(soundEvent, bus, out channel)) + return false; + } + else + { + return false; + } + + PlayableBehaviorChannels[__instance] = channel; + + channel.setPaused(true); + + __instance.PerformSeek(); + + if (__instance.TrackTargetObject) + { + CustomSoundHandler.AttachChannelToGameObject(channel, __instance.TrackTargetObject.transform); + } + else + { + SetChannel3DAttributes(channel, Vector3.zero); + } + + channel.setVolume(__instance.currentVolume); + + channel.setPaused(false); + + return false; + } + + [HarmonyPatch(typeof(FMODEventPlayableBehavior), nameof(FMODEventPlayableBehavior.OnExit))] + [HarmonyPrefix] + public static bool FMODEventPlayableBehavior_OnExit_Prefix(FMODEventPlayableBehavior __instance) + { + if (!PlayableBehaviorChannels.TryGetValue(__instance, out var channel)) + { + return true; + } + + if (!__instance.isPlayheadInside) + { + return false; + } + + if (__instance.stopType != FMODUnity.STOP_MODE.None) + { + channel.stop(); + } + + PlayableBehaviorChannels.Remove(__instance); + __instance.isPlayheadInside = false; + + return false; + } + [HarmonyPatch(typeof(FMODEventPlayableBehavior), nameof(FMODEventPlayableBehavior.ProcessFrame))] + [HarmonyPrefix] + public static bool FMODEventPlayableBehavior_ProcessFrame_Prefix(FMODEventPlayableBehavior __instance) + { + return !PlayableBehaviorChannels.ContainsKey(__instance); + } + + [HarmonyPatch(typeof(FMODEventPlayableBehavior), nameof(FMODEventPlayableBehavior.UpdateBehavior))] + [HarmonyPrefix] + public static bool FMODEventPlayableBehavior_UpdateBehavior_Prefix(FMODEventPlayableBehavior __instance, float time, float volume) + { + if (!PlayableBehaviorChannels.TryGetValue(__instance, out var channel)) + { + return true; + } + + if (volume != __instance.currentVolume) + { + __instance.currentVolume = volume; + channel.setVolume(volume); + } + + if (time >= __instance.OwningClip.start && time < __instance.OwningClip.end) + { + __instance.OnEnter(); + } + else + { + __instance.OnExit(); + } + + return false; + } + + [HarmonyPatch(typeof(FMODEventPlayableBehavior), nameof(FMODEventPlayableBehavior.OnGraphStop))] + [HarmonyPostfix] + public static void FMODEventPlayableBehavior_OnGraphStop_Postfix(FMODEventPlayableBehavior __instance) + { + if (!PlayableBehaviorChannels.TryGetValue(__instance, out var channel)) + { + channel.stop(); + PlayableBehaviorChannels.Remove(__instance); + } + } + + [HarmonyPatch(typeof(FMODEventPlayableBehavior), nameof(FMODEventPlayableBehavior.Evaluate))] + [HarmonyPrefix] + public static bool FMODEventPlayableBehavior_Evaluate_Postfix(FMODEventPlayableBehavior __instance, double time, FrameData info, bool evaluate) + { + if (!PlayableBehaviorChannels.TryGetValue(__instance, out var channel)) + { + return true; + } + + if (!info.timeHeld && time >= __instance.OwningClip.start && time < __instance.OwningClip.end) + { + if (!__instance.isPlayheadInside) + { + if (time - __instance.OwningClip.start > 0.1) + { + __instance.seek = __instance.GetPosition(time); + } + __instance.OnEnter(); + return false; + } + if ((evaluate || info.seekOccurred || info.timeLooped || info.evaluationType == FrameData.EvaluationType.Evaluate)) + { + __instance.seek = __instance.GetPosition(time); + __instance.PerformSeek(); + return false; + } + } + else + { + __instance.OnExit(); + } + + return false; + } + + [HarmonyPatch(typeof(RuntimeManager), nameof(RuntimeManager.Update))] + [HarmonyPostfix] + public static void RuntimeManager_Update_Postfix(RuntimeManager __instance) + { + if (!__instance.studioSystem.isValid()) + { + return; + } + + foreach (var attachedChannel in AttachedChannels) + { + attachedChannel.Channel.isPlaying(out var isPlaying); + if (!isPlaying || !attachedChannel.Transform) + { + _attachedChannelsToRemove.Add(attachedChannel); + continue; + } + + SetChannel3DAttributes(attachedChannel.Channel, attachedChannel.Transform); + } + + if (_attachedChannelsToRemove.Count > 0) + { + foreach (var toRemove in _attachedChannelsToRemove) + { + AttachedChannels.Remove(toRemove); + } + _attachedChannelsToRemove.Clear(); + } + } + #if SUBNAUTICA [HarmonyPatch(typeof(FMODUWE), nameof(FMODUWE.PlayOneShotImpl))] @@ -700,13 +912,13 @@ public static bool FMOD_CustomLoopingEmitter_OnPlay_Prefix(FMOD_CustomLoopingEmi } #endif - private static void SetChannel3DAttributes(Channel channel, Transform transform) + internal static void SetChannel3DAttributes(Channel channel, Transform transform) { ATTRIBUTES_3D attributes = transform.To3DAttributes(); channel.set3DAttributes(ref attributes.position, ref attributes.velocity); } - private static void SetChannel3DAttributes(Channel channel, Vector3 position) + internal static void SetChannel3DAttributes(Channel channel, Vector3 position) { ATTRIBUTES_3D attributes = position.To3DAttributes(); channel.set3DAttributes(ref attributes.position, ref attributes.velocity);