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