diff --git a/EXILED/Exiled.Events/EventArgs/Player/ReceivingGunSoundEventArgs.cs b/EXILED/Exiled.Events/EventArgs/Player/ReceivingGunSoundEventArgs.cs
new file mode 100644
index 000000000..b12fe1197
--- /dev/null
+++ b/EXILED/Exiled.Events/EventArgs/Player/ReceivingGunSoundEventArgs.cs
@@ -0,0 +1,101 @@
+// -----------------------------------------------------------------------
+//
+// Copyright (c) ExMod Team. All rights reserved.
+// Licensed under the CC BY-SA 3.0 license.
+//
+// -----------------------------------------------------------------------
+
+namespace Exiled.Events.EventArgs.Player
+{
+ using API.Features;
+ using API.Features.Items;
+
+ using AudioPooling;
+
+ using Exiled.Events.EventArgs.Interfaces;
+
+ using UnityEngine;
+
+ ///
+ /// Contains all information before a player receive gun sound.
+ ///
+ public class ReceivingGunSoundEventArgs : IPlayerEvent, IDeniableEvent, IFirearmEvent
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The referencehub who will receive gun sound.
+ /// The internal firearm instance.
+ /// The index of the audio clip to be played.
+ /// The audio mixer channel.
+ /// The audible range of the sound.
+ /// The pitch of the sound.
+ /// The audio owner position.
+ /// The audio owner is visible for this player.
+ public ReceivingGunSoundEventArgs(ReferenceHub hub, InventorySystem.Items.Firearms.Firearm firearm, int audioIndex, MixerChannel mixerChannel, float range, float pitch, Vector3 ownPos, bool isSenderVisible)
+ {
+ Player = Player.Get(hub);
+ Firearm = Item.Get(firearm);
+ Sender = Firearm.Owner;
+ Range = range;
+ Pitch = pitch;
+ AudioIndex = audioIndex;
+ MixerChannel = mixerChannel;
+ SenderPosition = ownPos;
+ SenderVisible = isSenderVisible;
+ }
+
+ ///
+ /// Gets the player who will receive gun sound.
+ ///
+ public Player Player { get; }
+
+ ///
+ /// Gets the player who owns the Firearm.
+ ///
+ public Player Sender { get; }
+
+ ///
+ public Item Item => Firearm;
+
+ ///
+ /// Gets the firearm that was the source of the sound.
+ ///
+ public Firearm Firearm { get; }
+
+ ///
+ /// Gets or sets the index of the audio clip to be played from the firearm's audio list.
+ ///
+ public int AudioIndex { get; set; }
+
+ ///
+ /// Gets or sets the mixer channel through which the sound will be played.
+ ///
+ public MixerChannel MixerChannel { get; set; }
+
+ ///
+ /// Gets or sets the max audible distance of the gun sound.
+ ///
+ public float Range { get; set; }
+
+ ///
+ /// Gets or sets the pitch of the gun sound.
+ ///
+ public float Pitch { get; set; }
+
+ ///
+ /// Gets or sets the virtual origin point used(Only works when SenderVisible is false).
+ ///
+ public Vector3 SenderPosition { get; set; }
+
+ ///
+ /// Gets a value indicating whether Sender is visible for this player.
+ ///
+ public bool SenderVisible { get; }
+
+ ///
+ /// Gets or sets a value indicating whether the gun sound should be sent.
+ ///
+ public bool IsAllowed { get; set; } = true;
+ }
+}
diff --git a/EXILED/Exiled.Events/EventArgs/Player/SendingGunSoundEventArgs.cs b/EXILED/Exiled.Events/EventArgs/Player/SendingGunSoundEventArgs.cs
new file mode 100644
index 000000000..c1041453a
--- /dev/null
+++ b/EXILED/Exiled.Events/EventArgs/Player/SendingGunSoundEventArgs.cs
@@ -0,0 +1,86 @@
+// -----------------------------------------------------------------------
+//
+// Copyright (c) ExMod Team. All rights reserved.
+// Licensed under the CC BY-SA 3.0 license.
+//
+// -----------------------------------------------------------------------
+namespace Exiled.Events.EventArgs.Player
+{
+ using API.Features;
+ using API.Features.Items;
+
+ using AudioPooling;
+
+ using Exiled.Events.EventArgs.Interfaces;
+
+ using UnityEngine;
+
+ ///
+ /// Contains all information before a gun sound is sent to players.
+ ///
+ public class SendingGunSoundEventArgs : IPlayerEvent, IDeniableEvent, IFirearmEvent
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The internal firearm instance.
+ /// The index of the audio clip to be played.
+ /// The audio mixer channel.
+ /// The audible range of the sound.
+ /// The pitch of the sound.
+ /// The audio owner position.
+ public SendingGunSoundEventArgs(InventorySystem.Items.Firearms.Firearm firearm, int audioIndex, MixerChannel mixerChannel, float range, float pitch, Vector3 ownPos)
+ {
+ Firearm = Item.Get(firearm);
+ Player = Firearm.Owner;
+ Range = range;
+ Pitch = pitch;
+ AudioIndex = audioIndex;
+ MixerChannel = mixerChannel;
+ SendingPosition = ownPos;
+ }
+
+ ///
+ /// Gets the player who owns the Firearm.
+ ///
+ public Player Player { get; }
+
+ ///
+ public Item Item => Firearm;
+
+ ///
+ /// Gets the firearm that was the source of the sound.
+ ///
+ public Firearm Firearm { get; }
+
+ ///
+ /// Gets or sets the index of the audio clip to be played from the firearm's audio list.
+ ///
+ public int AudioIndex { get; set; }
+
+ ///
+ /// Gets or sets the mixer channel through which the sound will be played.
+ ///
+ public MixerChannel MixerChannel { get; set; }
+
+ ///
+ /// Gets or sets the max audible distance of the gun sound.
+ ///
+ public float Range { get; set; }
+
+ ///
+ /// Gets or sets the pitch of the gun sound.
+ ///
+ public float Pitch { get; set; }
+
+ ///
+ /// Gets or sets the virtual origin point used(Only will work when this player not visible for sending player).
+ ///
+ public Vector3 SendingPosition { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether the gun sound should be sent.
+ ///
+ public bool IsAllowed { get; set; } = true;
+ }
+}
diff --git a/EXILED/Exiled.Events/Handlers/Player.cs b/EXILED/Exiled.Events/Handlers/Player.cs
index 6965e3e1c..c03bcebeb 100644
--- a/EXILED/Exiled.Events/Handlers/Player.cs
+++ b/EXILED/Exiled.Events/Handlers/Player.cs
@@ -279,6 +279,16 @@ public class Player
///
public static Event Shooting { get; set; } = new();
+ ///
+ /// Invoked before a sends a gun sound to nearby players.
+ ///
+ public static Event SendingGunSound { get; set; } = new();
+
+ ///
+ /// Invoked before a receives a gun sound.
+ ///
+ public static Event ReceivingGunSound { get; set; } = new();
+
///
/// Invoked before a enters the pocket dimension.
///
@@ -900,6 +910,18 @@ public static void OnRoomChanged(RoomChangedEventArgs ev)
/// The instance.
public static void OnShooting(ShootingEventArgs ev) => Shooting.InvokeSafely(ev);
+ ///
+ /// Called before the server sends a gun sound to nearby players.
+ ///
+ /// The instance.
+ public static void OnSendingGunSound(SendingGunSoundEventArgs ev) => SendingGunSound.InvokeSafely(ev);
+
+ ///
+ /// Called when a receives a gun sound.
+ ///
+ /// The instance.
+ public static void OnReceivingGunSound(ReceivingGunSoundEventArgs ev) => ReceivingGunSound.InvokeSafely(ev);
+
///
/// Called before a enters the pocket dimension.
///
diff --git a/EXILED/Exiled.Events/Patches/Events/Player/ReceivingGunSound.cs b/EXILED/Exiled.Events/Patches/Events/Player/ReceivingGunSound.cs
new file mode 100644
index 000000000..46881a8d6
--- /dev/null
+++ b/EXILED/Exiled.Events/Patches/Events/Player/ReceivingGunSound.cs
@@ -0,0 +1,186 @@
+// -----------------------------------------------------------------------
+//
+// Copyright (c) ExMod Team. All rights reserved.
+// Licensed under the CC BY-SA 3.0 license.
+//
+// -----------------------------------------------------------------------
+namespace Exiled.Events.Patches.Events.Player
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Reflection;
+ using System.Reflection.Emit;
+
+ using Exiled.API.Features;
+ using Exiled.API.Features.Pools;
+ using Exiled.Events.Attributes;
+ using Exiled.Events.EventArgs.Player;
+
+ using HarmonyLib;
+
+ using InventorySystem.Items.Firearms;
+ using InventorySystem.Items.Firearms.Modules;
+
+ using static HarmonyLib.AccessTools;
+
+ ///
+ /// Patches AudioModule.ServerSendToNearbyPlayers to add event.
+ ///
+ [EventPatch(typeof(Handlers.Player), nameof(Handlers.Player.ReceivingGunSound))]
+ [HarmonyPatch]
+ public class ReceivingGunSound
+ {
+ private static Type displayClassType;
+ private static Type displayClassTypeForeach;
+
+ private static MethodBase TargetMethod()
+ {
+ Type innerType = Inner(typeof(AudioModule), "<>c__DisplayClass31_1");
+
+ return Method(innerType, "b__0");
+ }
+
+ private static bool Prepare()
+ {
+ displayClassType = Inner(typeof(AudioModule), "<>c__DisplayClass31_0");
+ displayClassTypeForeach = Inner(typeof(AudioModule), "<>c__DisplayClass31_1");
+
+ if (displayClassType == null || displayClassTypeForeach == null)
+ {
+ Log.Error("`<>c__DisplayClass31` _1 or _0 cannot found on ReceivingGunSound class. Class changed skipping patch.");
+ return false;
+ }
+
+ return true;
+ }
+
+ private static IEnumerable Transpiler(IEnumerable instructions, ILGenerator generator)
+ {
+ List newInstructions = ListPool.Pool.Get(instructions);
+
+ LocalBuilder ev = generator.DeclareLocal(typeof(ReceivingGunSoundEventArgs));
+
+ Label ret = generator.DefineLabel();
+
+ int offset = 1;
+ int index = newInstructions.FindLastIndex(x => x.opcode == OpCodes.Stloc_0) + offset;
+
+ newInstructions.InsertRange(
+ index,
+ [
+
+ // this.receiver
+ new(OpCodes.Ldarg_0),
+ new(OpCodes.Ldfld, Field(displayClassTypeForeach, "receiver")),
+
+ // this.locals1.Firearm
+ new(OpCodes.Ldarg_0),
+ new(OpCodes.Ldfld, Field(displayClassTypeForeach, "CS$<>8__locals1")),
+ new(OpCodes.Ldfld, Field(displayClassType, "<>4__this")),
+ new(OpCodes.Call, PropertyGetter(typeof(FirearmSubcomponentBase), nameof(FirearmSubcomponentBase.Firearm))),
+
+ // this.locals1.index
+ new(OpCodes.Ldarg_0),
+ new(OpCodes.Ldfld, Field(displayClassTypeForeach, "CS$<>8__locals1")),
+ new(OpCodes.Ldfld, Field(displayClassType, "index")),
+
+ // this.locals1.channel
+ new(OpCodes.Ldarg_0),
+ new(OpCodes.Ldfld, Field(displayClassTypeForeach, "CS$<>8__locals1")),
+ new(OpCodes.Ldfld, Field(displayClassType, "channel")),
+
+ // this.locals1.audioRange
+ new(OpCodes.Ldarg_0),
+ new(OpCodes.Ldfld, Field(displayClassTypeForeach, "CS$<>8__locals1")),
+ new(OpCodes.Ldfld, Field(displayClassType, "audioRange")),
+
+ // this.locals1.pitch
+ new(OpCodes.Ldarg_0),
+ new(OpCodes.Ldfld, Field(displayClassTypeForeach, "CS$<>8__locals1")),
+ new(OpCodes.Ldfld, Field(displayClassType, "pitch")),
+
+ // this.locals1.ownPos
+ new(OpCodes.Ldarg_0),
+ new(OpCodes.Ldfld, Field(displayClassTypeForeach, "CS$<>8__locals1")),
+ new(OpCodes.Ldfld, Field(displayClassType, "ownPos")),
+
+ // visibility flag
+ new(OpCodes.Ldloc_0),
+
+ // new(receiver, firearm, audioIndex, mixerChannel, range, pitch, ownPos, isSenderVisible)
+ new(OpCodes.Newobj, GetDeclaredConstructors(typeof(ReceivingGunSoundEventArgs))[0]),
+ new(OpCodes.Dup),
+ new(OpCodes.Dup),
+ new(OpCodes.Stloc, ev.LocalIndex),
+
+ // Handlers.Player.OnReceivingGunSound(ev);
+ new(OpCodes.Call, Method(typeof(Handlers.Player), nameof(Handlers.Player.OnReceivingGunSound))),
+
+ // if (!ev.IsAllowed)
+ // return;
+ new(OpCodes.Callvirt, PropertyGetter(typeof(ReceivingGunSoundEventArgs), nameof(ReceivingGunSoundEventArgs.IsAllowed))),
+ new CodeInstruction(OpCodes.Brfalse_S, ret),
+ ]);
+
+ offset = -2;
+ const int count = 3;
+
+ // index = ev.AudioIndex;
+ index = newInstructions.FindLastIndex(x => x.LoadsField(Field(displayClassType, "index"))) + offset;
+ newInstructions.RemoveRange(index, count);
+ newInstructions.InsertRange(
+ index,
+ [
+ new(OpCodes.Ldloc, ev.LocalIndex),
+ new(OpCodes.Callvirt, PropertyGetter(typeof(ReceivingGunSoundEventArgs), nameof(ReceivingGunSoundEventArgs.AudioIndex))),
+ ]);
+
+ // pitch = ev.Pitch;
+ index = newInstructions.FindLastIndex(x => x.LoadsField(Field(displayClassType, "pitch"))) + offset;
+ newInstructions.RemoveRange(index, count);
+ newInstructions.InsertRange(
+ index,
+ [
+ new(OpCodes.Ldloc, ev.LocalIndex),
+ new(OpCodes.Callvirt, PropertyGetter(typeof(ReceivingGunSoundEventArgs), nameof(ReceivingGunSoundEventArgs.Pitch))),
+ ]);
+
+ // channel = ev.MixerChannel;
+ index = newInstructions.FindLastIndex(x => x.LoadsField(Field(displayClassType, "channel"))) + offset;
+ newInstructions.RemoveRange(index, count);
+ newInstructions.InsertRange(
+ index,
+ [
+ new(OpCodes.Ldloc, ev.LocalIndex),
+ new(OpCodes.Callvirt, PropertyGetter(typeof(ReceivingGunSoundEventArgs), nameof(ReceivingGunSoundEventArgs.MixerChannel))),
+ ]);
+
+ // audioRange = ev.Range;
+ index = newInstructions.FindLastIndex(x => x.LoadsField(Field(displayClassType, "audioRange"))) + offset;
+ newInstructions.RemoveRange(index, count);
+ newInstructions.InsertRange(
+ index,
+ [
+ new(OpCodes.Ldloc, ev.LocalIndex),
+ new(OpCodes.Callvirt, PropertyGetter(typeof(ReceivingGunSoundEventArgs), nameof(ReceivingGunSoundEventArgs.Range))),
+ ]);
+
+ // ownPos = ev.SenderPosition;
+ index = newInstructions.FindLastIndex(x => x.LoadsField(Field(displayClassType, "ownPos"))) + offset;
+ newInstructions.RemoveRange(index, count);
+ newInstructions.InsertRange(
+ index,
+ [
+ new(OpCodes.Ldloc, ev.LocalIndex),
+ new(OpCodes.Callvirt, PropertyGetter(typeof(ReceivingGunSoundEventArgs), nameof(ReceivingGunSoundEventArgs.SenderPosition))),
+ ]);
+
+ newInstructions[newInstructions.Count - 1].WithLabels(ret);
+
+ for (int z = 0; z < newInstructions.Count; z++)
+ yield return newInstructions[z];
+
+ ListPool.Pool.Return(newInstructions);
+ }
+ }
+}
diff --git a/EXILED/Exiled.Events/Patches/Events/Player/SendingGunSound.cs b/EXILED/Exiled.Events/Patches/Events/Player/SendingGunSound.cs
new file mode 100644
index 000000000..203a31dc5
--- /dev/null
+++ b/EXILED/Exiled.Events/Patches/Events/Player/SendingGunSound.cs
@@ -0,0 +1,141 @@
+// -----------------------------------------------------------------------
+//
+// Copyright (c) ExMod Team. All rights reserved.
+// Licensed under the CC BY-SA 3.0 license.
+//
+// -----------------------------------------------------------------------
+namespace Exiled.Events.Patches.Events.Player
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Reflection.Emit;
+
+ using Exiled.API.Features;
+ using Exiled.API.Features.Pools;
+ using Exiled.Events.Attributes;
+ using Exiled.Events.EventArgs.Player;
+
+ using HarmonyLib;
+
+ using InventorySystem.Items.Firearms;
+ using InventorySystem.Items.Firearms.Modules;
+
+ using static HarmonyLib.AccessTools;
+
+ ///
+ /// Patches AudioModule.ServerSendToNearbyPlayers to add event.
+ ///
+ [EventPatch(typeof(Handlers.Player), nameof(Handlers.Player.SendingGunSound))]
+ [HarmonyPatch(typeof(AudioModule), nameof(AudioModule.ServerSendToNearbyPlayers))]
+ public class SendingGunSound
+ {
+ private static Type displayClassType;
+
+ private static bool Prepare()
+ {
+ displayClassType = Inner(typeof(AudioModule), "<>c__DisplayClass31_0");
+
+ if (displayClassType == null)
+ {
+ Log.Error("`<>c__DisplayClass31_0` cannot found on SendingGunSound class. Class changed skipping patch.");
+ return false;
+ }
+
+ return true;
+ }
+
+ private static IEnumerable Transpiler(IEnumerable instructions, ILGenerator generator)
+ {
+ List newInstructions = ListPool.Pool.Get(instructions);
+
+ LocalBuilder ev = generator.DeclareLocal(typeof(SendingGunSoundEventArgs));
+
+ Label ret = generator.DefineLabel();
+
+ int index = newInstructions.FindLastIndex(x => x.Calls(PropertyGetter(typeof(ReferenceHub), nameof(ReferenceHub.AllHubs))));
+
+ newInstructions.InsertRange(
+ index,
+ [
+
+ // this.Firearm
+ new(OpCodes.Ldarg_0),
+ new(OpCodes.Call, PropertyGetter(typeof(FirearmSubcomponentBase), nameof(FirearmSubcomponentBase.Firearm))),
+
+ // index
+ new(OpCodes.Ldarg_1),
+
+ // channel
+ new(OpCodes.Ldarg_2),
+
+ // audioRange
+ new(OpCodes.Ldarg_3),
+
+ // pitch
+ new(OpCodes.Ldarg_S, 4),
+
+ // ownPos
+ new(OpCodes.Ldloc_0),
+ new(OpCodes.Ldfld, Field(displayClassType, "ownPos")),
+
+ // new(firearm, audioIndex, mixerChannel, range, pitch, ownPos)
+ new(OpCodes.Newobj, GetDeclaredConstructors(typeof(SendingGunSoundEventArgs))[0]),
+ new(OpCodes.Dup),
+ new(OpCodes.Dup),
+ new(OpCodes.Stloc, ev.LocalIndex),
+
+ // Handlers.Player.OnSendingGunShotSound(ev);
+ new(OpCodes.Call, Method(typeof(Handlers.Player), nameof(Handlers.Player.OnSendingGunSound))),
+
+ // if (!ev.IsAllowed)
+ // return;
+ new(OpCodes.Callvirt, PropertyGetter(typeof(SendingGunSoundEventArgs), nameof(SendingGunSoundEventArgs.IsAllowed))),
+ new(OpCodes.Brfalse_S, ret),
+
+ // index = ev.AudioIndex;
+ new(OpCodes.Ldloc_0),
+ new(OpCodes.Ldloc, ev.LocalIndex),
+ new(OpCodes.Callvirt, PropertyGetter(typeof(SendingGunSoundEventArgs), nameof(SendingGunSoundEventArgs.AudioIndex))),
+ new(OpCodes.Stfld, Field(displayClassType, "index")),
+
+ // channel = ev.MixerChannel;
+ new(OpCodes.Ldloc_0),
+ new(OpCodes.Ldloc, ev.LocalIndex),
+ new(OpCodes.Callvirt, PropertyGetter(typeof(SendingGunSoundEventArgs), nameof(SendingGunSoundEventArgs.MixerChannel))),
+ new(OpCodes.Stfld, Field(displayClassType, "channel")),
+
+ // audioRange = ev.Range;
+ new(OpCodes.Ldloc_0),
+ new(OpCodes.Ldloc, ev.LocalIndex),
+ new(OpCodes.Callvirt, PropertyGetter(typeof(SendingGunSoundEventArgs), nameof(SendingGunSoundEventArgs.Range))),
+ new(OpCodes.Stfld, Field(displayClassType, "audioRange")),
+ new(OpCodes.Ldloc, ev.LocalIndex),
+ new(OpCodes.Callvirt, PropertyGetter(typeof(SendingGunSoundEventArgs), nameof(SendingGunSoundEventArgs.Range))),
+ new(OpCodes.Ldc_R4, AudioModule.SendDistanceBuffer),
+ new(OpCodes.Add),
+ new(OpCodes.Dup),
+ new(OpCodes.Mul),
+ new(OpCodes.Stloc_2),
+
+ // pitch = ev.Pitch;
+ new(OpCodes.Ldloc_0),
+ new(OpCodes.Ldloc, ev.LocalIndex),
+ new(OpCodes.Callvirt, PropertyGetter(typeof(SendingGunSoundEventArgs), nameof(SendingGunSoundEventArgs.Pitch))),
+ new(OpCodes.Stfld, Field(displayClassType, "pitch")),
+
+ // ownPos = ev.SendingPosition;
+ new(OpCodes.Ldloc_0),
+ new(OpCodes.Ldloc, ev.LocalIndex),
+ new(OpCodes.Callvirt, PropertyGetter(typeof(SendingGunSoundEventArgs), nameof(SendingGunSoundEventArgs.SendingPosition))),
+ new CodeInstruction(OpCodes.Stfld, Field(displayClassType, "ownPos")),
+ ]);
+
+ newInstructions[newInstructions.Count - 1].WithLabels(ret);
+
+ for (int z = 0; z < newInstructions.Count; z++)
+ yield return newInstructions[z];
+
+ ListPool.Pool.Return(newInstructions);
+ }
+ }
+}