diff --git a/HKMP/Game/Client/ClientManager.cs b/HKMP/Game/Client/ClientManager.cs index 6704d69..782a6d2 100644 --- a/HKMP/Game/Client/ClientManager.cs +++ b/HKMP/Game/Client/ClientManager.cs @@ -193,8 +193,7 @@ ModSettings modSettings new GamePatcher(netClient).RegisterHooks(); new FsmPatcher().RegisterHooks(); - var customHooks = new CustomHooks(); - customHooks.Initialize(); + CustomHooks.Initialize(); _commandManager = new ClientCommandManager(); var eventAggregator = new EventAggregator(); @@ -262,7 +261,7 @@ ModSettings modSettings On.HeroController.Start += OnHeroControllerStart; On.HeroController.Update += OnPlayerUpdate; - customHooks.AfterEnterSceneHeroTransformed += OnEnterScene; + CustomHooks.AfterEnterSceneHeroTransformed += OnEnterScene; // Register client connect and timeout handler netClient.ConnectEvent += OnClientConnect; diff --git a/HKMP/Game/Client/CustomHooks.cs b/HKMP/Game/Client/CustomHooks.cs index 3bb6855..cbc3456 100644 --- a/HKMP/Game/Client/CustomHooks.cs +++ b/HKMP/Game/Client/CustomHooks.cs @@ -1,18 +1,21 @@ using System; using System.Reflection; using Hkmp.Logging; +using HutongGames.PlayMaker; +using HutongGames.PlayMaker.Actions; using Mono.Cecil.Cil; using MonoMod.Cil; using MonoMod.RuntimeDetour; +using UnityEngine.Audio; namespace Hkmp.Game.Client; // TODO: create method for de-registering the hooks /// -/// Class that manages and exposes custom hooks that are not possible with On hooks or ModHooks. Uses IL modification +/// Static class that manages and exposes custom hooks that are not possible with On hooks or ModHooks. Uses IL modification /// to embed event calls in certain methods. /// -public class CustomHooks { +public static class CustomHooks { /// /// The binding flags for obtaining certain types for hooking. /// @@ -34,21 +37,32 @@ public class CustomHooks { /// /// IL Hook instance for the HeroController EnterScene hook. /// - private ILHook _heroControllerEnterSceneIlHook; + private static ILHook _heroControllerEnterSceneIlHook; /// /// IL Hook instance for the HeroController Respawn hook. /// - private ILHook _heroControllerRespawnIlHook; + private static ILHook _heroControllerRespawnIlHook; /// /// Event for when the player object is done being transformed (changed position, scale) after entering a scene. /// - public event Action AfterEnterSceneHeroTransformed; + public static event Action AfterEnterSceneHeroTransformed; + + /// + /// Event for when the AudioManager.ApplyMusicCue method is called from the ApplyMusicCue FSM action. + /// + public static event Action ApplyMusicCueFromFsmAction; + + /// + /// Event for when the AudioMixerSnapshot.TransitionTo method is called from the TransitionToAudioSnapshot FSM + /// action. + /// + public static event Action TransitionToAudioSnapshotFromFsmAction; /// /// Initialize the class by registering the IL hooks. /// - public void Initialize() { + public static void Initialize() { IL.HeroController.Start += HeroControllerOnStart; IL.HeroController.EnterSceneDreamGate += HeroControllerOnEnterSceneDreamGate; @@ -57,12 +71,15 @@ public void Initialize() { type = typeof(HeroController).GetNestedType("d__473", BindingFlags); _heroControllerRespawnIlHook = new ILHook(type.GetMethod("MoveNext", BindingFlags), HeroControllerOnRespawn); + + IL.HutongGames.PlayMaker.Actions.ApplyMusicCue.OnEnter += ApplyMusicCueOnEnter; + IL.HutongGames.PlayMaker.Actions.TransitionToAudioSnapshot.OnEnter += TransitionToAudioSnapshotOnEnter; } /// /// IL Hook for the HeroController Start method. Calls an event within the method. /// - private void HeroControllerOnStart(ILContext il) { + private static void HeroControllerOnStart(ILContext il) { try { // Create a cursor for this context var c = new ILCursor(il); @@ -76,7 +93,7 @@ private void HeroControllerOnStart(ILContext il) { /// /// IL Hook for the HeroController EnterSceneDreamGate method. Calls an event within the method. /// - private void HeroControllerOnEnterSceneDreamGate(ILContext il) { + private static void HeroControllerOnEnterSceneDreamGate(ILContext il) { try { // Create a cursor for this context var c = new ILCursor(il); @@ -90,7 +107,7 @@ private void HeroControllerOnEnterSceneDreamGate(ILContext il) { /// /// IL Hook for the HeroController EnterScene method. Calls an event multiple times within the method. /// - private void HeroControllerOnEnterScene(ILContext il) { + private static void HeroControllerOnEnterScene(ILContext il) { try { // Create a cursor for this context var c = new ILCursor(il); @@ -131,7 +148,7 @@ private void HeroControllerOnEnterScene(ILContext il) { /// /// IL Hook for the HeroController Respawn method. Calls an event multiple times within the method. /// - private void HeroControllerOnRespawn(ILContext il) { + private static void HeroControllerOnRespawn(ILContext il) { try { // Create a cursor for this context var c = new ILCursor(il); @@ -149,7 +166,7 @@ private void HeroControllerOnRespawn(ILContext il) { /// 'HeroInPosition' instructions. /// /// The IL cursor on which to match the instructions and emit the delegate. - private void EmitAfterEnterSceneEventHeroInPosition(ILCursor c) { + private static void EmitAfterEnterSceneEventHeroInPosition(ILCursor c) { c.GotoNext( MoveType.After, HeroInPositionInstructions @@ -157,4 +174,58 @@ private void EmitAfterEnterSceneEventHeroInPosition(ILCursor c) { c.EmitDelegate(() => { AfterEnterSceneHeroTransformed?.Invoke(); }); } + + /// + /// IL Hook for the ApplyMusicCue OnEnter method. Calls an event in the method after the ApplyMusicCue call is + /// made. + /// + private static void ApplyMusicCueOnEnter(ILContext il) { + try { + // Create a cursor for this context + var c = new ILCursor(il); + + // IL_005d: ldc.i4.0 + // IL_005e: callvirt instance void AudioManager::ApplyMusicCue(class MusicCue, float32, float32, bool) + c.GotoNext( + MoveType.After, + i => i.MatchLdcI4(0), + i => i.MatchCallvirt(typeof(AudioManager), "ApplyMusicCue") + ); + + // Put the instance of the ApplyMusicCue class onto the stack + c.Emit(OpCodes.Ldarg_0); + + // Emit a delegate for firing the event with the ApplyMusicCue instance + c.EmitDelegate>(action => { ApplyMusicCueFromFsmAction?.Invoke(action); }); + } catch (Exception e) { + Logger.Error($"Could not change ApplyMusicCueOnEnter IL: \n{e}"); + } + } + + /// + /// IL Hook for the TransitionToAudioSnapshot OnEnter method. Calls an event in the method after the TransitionTo + /// call is made. + /// + private static void TransitionToAudioSnapshotOnEnter(ILContext il) { + try { + // Create a cursor for this context + var c = new ILCursor(il); + + // IL_0021: callvirt instance float32 [PlayMaker]HutongGames.PlayMaker.FsmFloat::get_Value() + // IL_0026: callvirt instance void [UnityEngine.AudioModule]UnityEngine.Audio.AudioMixerSnapshot::TransitionTo(float32) + c.GotoNext( + MoveType.After, + i => i.MatchCallvirt(typeof(FsmFloat), "get_Value"), + i => i.MatchCallvirt(typeof(AudioMixerSnapshot), "TransitionTo") + ); + + // Put the instance of the TransitionToAudioSnapshot class onto the stack + c.Emit(OpCodes.Ldarg_0); + + // Emit a delegate for firing the event with the TransitionToAudioSnapshot instance + c.EmitDelegate>(action => { TransitionToAudioSnapshotFromFsmAction?.Invoke(action); }); + } catch (Exception e) { + Logger.Error($"Could not change TransitionToAudioSnapshotOnEnter IL: \n{e}"); + } + } } diff --git a/HKMP/Game/Client/Entity/Component/MusicComponent.cs b/HKMP/Game/Client/Entity/Component/MusicComponent.cs index 536daee..ec964ff 100644 --- a/HKMP/Game/Client/Entity/Component/MusicComponent.cs +++ b/HKMP/Game/Client/Entity/Component/MusicComponent.cs @@ -21,14 +21,35 @@ internal class MusicComponent : EntityComponent { /// private const string MusicDataFilePath = "Hkmp.Resource.music-data.json"; + /// + /// Static list of MusicCueData instances that is loaded from an embedded JSON file. + /// Used for coupling IDs to music cues that can then be used for bidirectional lookups. + /// private static readonly List MusicCueDataList; + /// + /// Static list of AudioMixerSnapshotData instances that is loaded from an embedded JSON file. + /// Used for coupling IDs to audio snapshots that can then be used for bidirectional lookups. + /// private static readonly List SnapshotDataList; + /// + /// The singleton instance of MusicComponent to ensure we only have one MusicComponent responsible for + /// synchronising music in a scene. + /// private static MusicComponent _instance; + /// + /// The index of the last played music cue, so we don't restart them unnecessarily. + /// private byte _lastMusicCueIndex; + /// + /// The index of the last played audio snapshot, so we don't restart them unnecessarily. + /// private byte _lastSnapshotIndex; + /// + /// Static constructor responsible for loading data from the JSON and registering static hooks. + /// static MusicComponent() { var dataPair = FileUtil.LoadObjectFromEmbeddedJson< (List, List) @@ -49,6 +70,15 @@ static MusicComponent() { On.PlayMakerFSM.OnEnable += OnFsmEnable; } + /// + /// Try to create a new instance of MusicComponent if it doesn't exist yet. This will prevent the creation of + /// more instances by keeping track of a singleton instance. + /// + /// The NetClient instance for networking data. + /// The entity ID that this component is attached to. + /// The host-client pair of game objects of the entity. + /// The created instance of MusicComponent if successful, otherwise null. + /// True if a new component could be created, false if a component already existed. public static bool CreateInstance( NetClient netClient, ushort entityId, @@ -65,10 +95,21 @@ out MusicComponent musicComponent return false; } + /// + /// Clear the current singleton instance of the component. + /// public static void ClearInstance() { _instance = null; } + /// + /// Get the MusicCueData instance from the list for which the given predicate holds. + /// + /// The predicate function that should return true for the MusicCueData that is + /// requested. + /// The MusicCueData for which the predicate holds, or null if no such instance could + /// be found. + /// True if the MusicCueData was found, false otherwise. private static bool GetMusicCueData(Func predicate, out MusicCueData musicCueData) { foreach (var data in MusicCueDataList) { if (predicate.Invoke(data)) { @@ -81,6 +122,14 @@ private static bool GetMusicCueData(Func predicate, out Musi return false; } + /// + /// Get the AudioMixerSnapshotData instance from the list for which the given predicate holds. + /// + /// The predicate function that should return true for the AudioMixerSnapshotData that is + /// requested. + /// The AudioMixerSnapshotData for which the predicate holds, or null if no such + /// instance could be found. + /// True if the AudioMixerSnapshotData was found, false otherwise. private static bool GetAudioMixerSnapshotData(Func predicate, out AudioMixerSnapshotData snapshotData) { foreach (var data in SnapshotDataList) { if (predicate.Invoke(data)) { @@ -98,24 +147,23 @@ private MusicComponent( ushort entityId, HostClientPair gameObject ) : base(netClient, entityId, gameObject) { - On.HutongGames.PlayMaker.Actions.ApplyMusicCue.OnEnter += ApplyMusicCueOnEnter; - On.HutongGames.PlayMaker.Actions.TransitionToAudioSnapshot.OnEnter += TransitionToAudioSnapshotOnEnter; + CustomHooks.ApplyMusicCueFromFsmAction += OnApplyMusicCue; + CustomHooks.TransitionToAudioSnapshotFromFsmAction += OnTransitionToAudioSnapshot; } - private void ApplyMusicCueOnEnter( - On.HutongGames.PlayMaker.Actions.ApplyMusicCue.orig_OnEnter orig, - ApplyMusicCue self - ) { - - Logger.Debug($"ApplyMusicCueOnEnter: {self.Fsm.GameObject.gameObject.name}, {self.Fsm.Name}"); + /// + /// Hook that is called when the AudioManager.ApplyMusicCue is called from an ApplyMusicCue FSM action. + /// Used to network the starting of a music cue for the scene host. + /// + /// The ApplyMusicCue FSM action responsible for the call. + private void OnApplyMusicCue(ApplyMusicCue action) { + Logger.Debug($"OnApplyMusicCue: {action.Fsm.GameObject.gameObject.name}, {action.Fsm.Name}"); if (IsControlled) { return; } - orig(self); - - var musicCue = self.musicCue.Value; + var musicCue = action.musicCue.Value; if (musicCue == null) { return; } @@ -139,19 +187,24 @@ ApplyMusicCue self } } - private void TransitionToAudioSnapshotOnEnter( - On.HutongGames.PlayMaker.Actions.TransitionToAudioSnapshot.orig_OnEnter orig, - TransitionToAudioSnapshot self - ) { - Logger.Debug($"TransitionToAudioSnapshotOnEnter: {self.Fsm.GameObject.gameObject.name}, {self.Fsm.Name}"); + /// + /// Hook that is called when the AudioMixerSnapshot.TransitionTo is called from an TransitionToAudioSnapshot FSM + /// action. Used to network the starting of a audio snapshot for the scene host. + /// + /// The TransitionToAudioSnapshot FSM action responsible for the call. + private void OnTransitionToAudioSnapshot(TransitionToAudioSnapshot action) { + Logger.Debug($"OnTransitionToAudioSnapshot: {action.Fsm.GameObject.gameObject.name}, {action.Fsm.Name}"); + + if (action.Fsm.Name.Equals("Door Control")) { + Logger.Debug(" Was door control, allowing"); + return; + } if (IsControlled) { return; } - orig(self); - - var snapshot = self.snapshot.Value; + var snapshot = action.snapshot.Value; if (snapshot == null) { return; } @@ -191,7 +244,7 @@ public override void Update(EntityNetworkData data, bool alreadyInSceneUpdate) { var musicCueIndex = data.Packet.ReadByte(); var snapshotIndex = data.Packet.ReadByte(); - Logger.Debug($"Applying entity network data for music component with indices: {musicCueIndex}, {snapshotIndex}"); + Logger.Debug($"Applying entity network data for music component with indices: {musicCueIndex}, {snapshotIndex}"); if (musicCueIndex != _lastMusicCueIndex) { ApplyIndex(musicCueIndex); @@ -240,10 +293,14 @@ void ApplyIndex(byte index) { /// public override void Destroy() { - On.HutongGames.PlayMaker.Actions.ApplyMusicCue.OnEnter -= ApplyMusicCueOnEnter; - On.HutongGames.PlayMaker.Actions.TransitionToAudioSnapshot.OnEnter -= TransitionToAudioSnapshotOnEnter; + CustomHooks.ApplyMusicCueFromFsmAction -= OnApplyMusicCue; + CustomHooks.TransitionToAudioSnapshotFromFsmAction -= OnTransitionToAudioSnapshot; } + /// + /// Hook for when an FSM becomes enabled. Used to check for ApplyMusicCue or TransitionToAudioSnapshot actions + /// such that their audio data can be added to the lists of data. + /// private static void OnFsmEnable(On.PlayMakerFSM.orig_OnEnable orig, PlayMakerFSM self) { orig(self); @@ -283,7 +340,10 @@ out var snapshotData } } } - + + /// + /// Data for music cues, used for looking up the index or the music cue from index for networking purposes. + /// private class MusicCueData { public MusicCueType Type { get; set; } public string Name { get; set; } @@ -293,6 +353,10 @@ private class MusicCueData { public MusicCue MusicCue { get; set; } } + /// + /// Data for audio snapshots, used for looking up the index or the audio snapshot from index for networking + /// purposes. + /// private class AudioMixerSnapshotData { public AudioMixerSnapshotType Type { get; set; } public string Name { get; set; } @@ -302,6 +366,9 @@ private class AudioMixerSnapshotData { public AudioMixerSnapshot Snapshot { get; set; } } + /// + /// Enum for music cue types. + /// [JsonConverter(typeof(StringEnumConverter))] private enum MusicCueType { None, @@ -324,6 +391,9 @@ private enum MusicCueType { Waterways } + /// + /// Enum for audio snapshot types. + /// [JsonConverter(typeof(StringEnumConverter))] private enum AudioMixerSnapshotType { Silent,