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,