From ba91f27baf4b900751283b3c9bcf078488cbae61 Mon Sep 17 00:00:00 2001
From: Unbistrackted <112902220+Unbistrackted@users.noreply.github.com>
Date: Mon, 23 Feb 2026 02:45:15 -0300
Subject: [PATCH 01/37] feat(warhead): Add ``IsOnCooldown`` property (#757)
---
EXILED/Exiled.API/Features/Warhead.cs | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/EXILED/Exiled.API/Features/Warhead.cs b/EXILED/Exiled.API/Features/Warhead.cs
index f261577770..5b53082a8c 100644
--- a/EXILED/Exiled.API/Features/Warhead.cs
+++ b/EXILED/Exiled.API/Features/Warhead.cs
@@ -129,6 +129,11 @@ public static WarheadStatus Status
///
public static bool IsInProgress => Controller.Info.InProgress;
+ ///
+ /// Gets a value indicating whether the warhead detonation is on cooldown.
+ ///
+ public static bool IsOnCooldown => Controller.CooldownEndTime > NetworkTime.time;
+
///
/// Gets or sets the warhead detonation timer.
///
@@ -164,7 +169,7 @@ public static int Kills
///
/// Gets a value indicating whether the warhead can be started.
///
- public static bool CanBeStarted => !IsInProgress && !IsDetonated && Controller.CooldownEndTime <= NetworkTime.time;
+ public static bool CanBeStarted => !IsInProgress && !IsDetonated && !IsOnCooldown;
///
/// Closes the surface blast doors.
From d2f5dbc11a671cb8a182c1dbfb1e656cdbd36df3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mustafa=20SAVA=C5=9E?=
Date: Sat, 28 Feb 2026 01:52:14 +0300
Subject: [PATCH 02/37] Automatic id / throwing errors changed to log error
---
.../Exiled.API/Features/Audio/WavUtility.cs | 7 +-
EXILED/Exiled.API/Features/Toys/Speaker.cs | 71 +++++++++++++------
2 files changed, 55 insertions(+), 23 deletions(-)
diff --git a/EXILED/Exiled.API/Features/Audio/WavUtility.cs b/EXILED/Exiled.API/Features/Audio/WavUtility.cs
index c1b1bc3f73..f88749c39e 100644
--- a/EXILED/Exiled.API/Features/Audio/WavUtility.cs
+++ b/EXILED/Exiled.API/Features/Audio/WavUtility.cs
@@ -92,7 +92,10 @@ public static void SkipHeader(Stream stream)
short bits = BinaryPrimitives.ReadInt16LittleEndian(fmtData.Slice(14, 2));
if (format != 1 || channels != 1 || rate != VoiceChatSettings.SampleRate || bits != 16)
- throw new InvalidDataException($"Invalid WAV format (format={format}, channels={channels}, rate={rate}, bits={bits}). Expected PCM16, mono and {VoiceChatSettings.SampleRate}Hz.");
+ {
+ Log.Error($"[Speaker] Invalid WAV format (format={format}, channels={channels}, rate={rate}, bits={bits}). Expected PCM16, mono and {VoiceChatSettings.SampleRate}Hz.");
+ throw new InvalidDataException("Unsupported WAV format.");
+ }
if (chunkSize > 16)
stream.Seek(chunkSize - 16, SeekOrigin.Current);
@@ -109,7 +112,7 @@ public static void SkipHeader(Stream stream)
}
if (stream.Position >= stream.Length)
- throw new InvalidDataException("WAV file does not contain a 'data' chunk.");
+ Log.Error("[Speaker] WAV file does not contain a 'data' chunk.");
}
}
}
diff --git a/EXILED/Exiled.API/Features/Toys/Speaker.cs b/EXILED/Exiled.API/Features/Toys/Speaker.cs
index 4478c1143b..dd4b3e4a0e 100644
--- a/EXILED/Exiled.API/Features/Toys/Speaker.cs
+++ b/EXILED/Exiled.API/Features/Toys/Speaker.cs
@@ -16,6 +16,7 @@ namespace Exiled.API.Features.Toys
using Enums;
using Exiled.API.Features.Audio;
+ using Exiled.API.Features.Pools;
using Interfaces;
@@ -29,6 +30,7 @@ namespace Exiled.API.Features.Toys
using VoiceChat.Codec;
using VoiceChat.Codec.Enums;
using VoiceChat.Networking;
+ using VoiceChat.Playbacks;
using Object = UnityEngine.Object;
@@ -288,20 +290,8 @@ public byte ControllerId
/// The scale of the .
/// Whether the should be initially spawned.
/// The new .
- public static Speaker Create(Vector3? position, Vector3? rotation, Vector3? scale, bool spawn)
- {
- Speaker speaker = new(Object.Instantiate(Prefab))
- {
- Position = position ?? Vector3.zero,
- Rotation = Quaternion.Euler(rotation ?? Vector3.zero),
- Scale = scale ?? Vector3.one,
- };
-
- if (spawn)
- speaker.Spawn();
-
- return speaker;
- }
+ [Obsolete("Use the Create(parent, position, scale, controllerId, spawn, worldPositonStays) methods, rotation useless.")]
+ public static Speaker Create(Vector3? position, Vector3? rotation, Vector3? scale, bool spawn) => Create(parent: null, position: position, scale: scale, controllerId: null, spawn: spawn, worldPositionStays: true);
///
/// Creates a new .
@@ -310,21 +300,60 @@ public static Speaker Create(Vector3? position, Vector3? rotation, Vector3? scal
/// Whether the should be initially spawned.
/// Whether the should keep the same world position.
/// The new .
- public static Speaker Create(Transform transform, bool spawn, bool worldPositionStays = true)
+ public static Speaker Create(Transform transform, bool spawn, bool worldPositionStays = true) => Create(parent: transform, position: Vector3.zero, scale: transform.localScale.normalized, controllerId: null, spawn: spawn, worldPositionStays: worldPositionStays);
+
+ ///
+ /// Creates a new .
+ ///
+ /// The parent transform to attach the to.
+ /// The local position of the .
+ /// The scale of the .
+ /// The specific controller ID to assign. If null, the next available ID is used.
+ /// Whether the should be initially spawned.
+ /// Whether the should keep the same world position when parented.
+ /// The new .
+ public static Speaker Create(Transform parent = null, Vector3? position = null, Vector3? scale = null, byte? controllerId = null, bool spawn = true, bool worldPositionStays = true)
{
- Speaker speaker = new(Object.Instantiate(Prefab, transform, worldPositionStays))
+ Speaker speaker = new(Object.Instantiate(Prefab, parent, worldPositionStays))
{
- Position = transform.position,
- Rotation = transform.rotation,
- Scale = transform.localScale.normalized,
+ Scale = scale ?? Vector3.one,
+ ControllerId = controllerId ?? GetNextControllerId(),
};
+ speaker.Transform.localPosition = position ?? Vector3.zero;
+
if (spawn)
speaker.Spawn();
return speaker;
}
+ ///
+ /// Gets the next available controller ID for a .
+ ///
+ /// The next available byte ID, or 0 if all IDs are currently in use.
+ public static byte GetNextControllerId()
+ {
+ byte id = 0;
+ HashSet usedIds = HashSetPool.Pool.Get();
+
+ foreach (SpeakerToyPlaybackBase playbackBase in SpeakerToyPlaybackBase.AllInstances)
+ usedIds.Add(playbackBase.ControllerId);
+
+ while (usedIds.Contains(id))
+ {
+ id++;
+ if (id == 255)
+ {
+ HashSetPool.Pool.Return(usedIds);
+ return 0;
+ }
+ }
+
+ HashSetPool.Pool.Return(usedIds);
+ return id;
+ }
+
///
/// Plays audio through this speaker.
///
@@ -354,10 +383,10 @@ public static void Play(AudioMessage message, IEnumerable targets = null
public void Play(string path, bool stream = false, bool destroyAfter = false, bool loop = false)
{
if (!File.Exists(path))
- throw new FileNotFoundException("The specified file does not exist.", path);
+ Log.Warn($"[Speaker] The specified file does not exist, path: `{path}`.");
if (!path.EndsWith(".wav", StringComparison.OrdinalIgnoreCase))
- throw new NotSupportedException($"The file type '{Path.GetExtension(path)}' is not supported. Please use .wav file.");
+ Log.Error($"[Speaker] The file type '{Path.GetExtension(path)}' is not supported. Please use .wav file.");
TryInitializePlayBack();
Stop();
From bae29947c893e6eb408741772ab4ddbde8e8b50a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mustafa=20SAVA=C5=9E?=
Date: Sat, 28 Feb 2026 01:55:18 +0300
Subject: [PATCH 03/37] f
---
EXILED/Exiled.API/Features/Audio/WavUtility.cs | 3 +++
1 file changed, 3 insertions(+)
diff --git a/EXILED/Exiled.API/Features/Audio/WavUtility.cs b/EXILED/Exiled.API/Features/Audio/WavUtility.cs
index f88749c39e..12ea4d32d0 100644
--- a/EXILED/Exiled.API/Features/Audio/WavUtility.cs
+++ b/EXILED/Exiled.API/Features/Audio/WavUtility.cs
@@ -112,7 +112,10 @@ public static void SkipHeader(Stream stream)
}
if (stream.Position >= stream.Length)
+ {
Log.Error("[Speaker] WAV file does not contain a 'data' chunk.");
+ throw new InvalidDataException("Missing 'data' chunk in WAV file.");
+ }
}
}
}
From be87e9fc818566fc89a4069ad7b04f7eb0597592 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mustafa=20SAVA=C5=9E?=
Date: Sat, 28 Feb 2026 01:57:43 +0300
Subject: [PATCH 04/37] .
---
EXILED/Exiled.API/Features/Toys/Speaker.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/EXILED/Exiled.API/Features/Toys/Speaker.cs b/EXILED/Exiled.API/Features/Toys/Speaker.cs
index dd4b3e4a0e..2de141c0fc 100644
--- a/EXILED/Exiled.API/Features/Toys/Speaker.cs
+++ b/EXILED/Exiled.API/Features/Toys/Speaker.cs
@@ -290,7 +290,7 @@ public byte ControllerId
/// The scale of the .
/// Whether the should be initially spawned.
/// The new .
- [Obsolete("Use the Create(parent, position, scale, controllerId, spawn, worldPositonStays) methods, rotation useless.")]
+ [Obsolete("Use the Create(parent, position, scale, controllerId, spawn, worldPositonStays) method, rotation is useless.")]
public static Speaker Create(Vector3? position, Vector3? rotation, Vector3? scale, bool spawn) => Create(parent: null, position: position, scale: scale, controllerId: null, spawn: spawn, worldPositionStays: true);
///
From ee82708f61d57eb994a8ca697bc2fb18f149737e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mustafa=20SAVA=C5=9E?=
Date: Sat, 28 Feb 2026 02:10:45 +0300
Subject: [PATCH 05/37] s
---
EXILED/Exiled.API/Features/Toys/Speaker.cs | 17 +++++++++--------
1 file changed, 9 insertions(+), 8 deletions(-)
diff --git a/EXILED/Exiled.API/Features/Toys/Speaker.cs b/EXILED/Exiled.API/Features/Toys/Speaker.cs
index 2de141c0fc..008b3f365f 100644
--- a/EXILED/Exiled.API/Features/Toys/Speaker.cs
+++ b/EXILED/Exiled.API/Features/Toys/Speaker.cs
@@ -317,7 +317,7 @@ public static Speaker Create(Transform parent = null, Vector3? position = null,
Speaker speaker = new(Object.Instantiate(Prefab, parent, worldPositionStays))
{
Scale = scale ?? Vector3.one,
- ControllerId = controllerId ?? GetNextControllerId(),
+ ControllerId = controllerId ?? GetNextFreeControllerId(),
};
speaker.Transform.localPosition = position ?? Vector3.zero;
@@ -331,8 +331,8 @@ public static Speaker Create(Transform parent = null, Vector3? position = null,
///
/// Gets the next available controller ID for a .
///
- /// The next available byte ID, or 0 if all IDs are currently in use.
- public static byte GetNextControllerId()
+ /// The next available byte ID. If all IDs are currently in use, returns a default of 0.
+ public static byte GetNextFreeControllerId()
{
byte id = 0;
HashSet usedIds = HashSetPool.Pool.Get();
@@ -340,14 +340,15 @@ public static byte GetNextControllerId()
foreach (SpeakerToyPlaybackBase playbackBase in SpeakerToyPlaybackBase.AllInstances)
usedIds.Add(playbackBase.ControllerId);
+ if (usedIds.Count >= byte.MaxValue + 1)
+ {
+ HashSetPool.Pool.Return(usedIds);
+ return 0;
+ }
+
while (usedIds.Contains(id))
{
id++;
- if (id == 255)
- {
- HashSetPool.Pool.Return(usedIds);
- return 0;
- }
}
HashSetPool.Pool.Return(usedIds);
From 36428d67630ceaac66fca93d96cee8df835529dd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mustafa=20SAVA=C5=9E?=
Date: Sat, 28 Feb 2026 02:14:44 +0300
Subject: [PATCH 06/37] gf
---
EXILED/Exiled.API/Features/Toys/Speaker.cs | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/EXILED/Exiled.API/Features/Toys/Speaker.cs b/EXILED/Exiled.API/Features/Toys/Speaker.cs
index 008b3f365f..0bf29dbc2c 100644
--- a/EXILED/Exiled.API/Features/Toys/Speaker.cs
+++ b/EXILED/Exiled.API/Features/Toys/Speaker.cs
@@ -384,10 +384,16 @@ public static void Play(AudioMessage message, IEnumerable targets = null
public void Play(string path, bool stream = false, bool destroyAfter = false, bool loop = false)
{
if (!File.Exists(path))
+ {
Log.Warn($"[Speaker] The specified file does not exist, path: `{path}`.");
+ return;
+ }
if (!path.EndsWith(".wav", StringComparison.OrdinalIgnoreCase))
+ {
Log.Error($"[Speaker] The file type '{Path.GetExtension(path)}' is not supported. Please use .wav file.");
+ return;
+ }
TryInitializePlayBack();
Stop();
From 841d20d72cec341cdeb37d3facb76b0341f7b22f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mustafa=20SAVA=C5=9E?=
Date: Sat, 28 Feb 2026 02:15:53 +0300
Subject: [PATCH 07/37] =?UTF-8?q?e=C4=9FH?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
EXILED/Exiled.API/Features/Toys/Speaker.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/EXILED/Exiled.API/Features/Toys/Speaker.cs b/EXILED/Exiled.API/Features/Toys/Speaker.cs
index 0bf29dbc2c..eb5106e728 100644
--- a/EXILED/Exiled.API/Features/Toys/Speaker.cs
+++ b/EXILED/Exiled.API/Features/Toys/Speaker.cs
@@ -385,7 +385,7 @@ public void Play(string path, bool stream = false, bool destroyAfter = false, bo
{
if (!File.Exists(path))
{
- Log.Warn($"[Speaker] The specified file does not exist, path: `{path}`.");
+ Log.Error($"[Speaker] The specified file does not exist, path: `{path}`.");
return;
}
From a08e3ffed5e7ec3f61e218349568bfd36ffbaa38 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mustafa=20SAVA=C5=9E?=
Date: Sun, 1 Mar 2026 22:14:10 +0300
Subject: [PATCH 08/37] =?UTF-8?q?added=20new=20=C3=B6zellik?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
EXILED/Exiled.API/Features/Toys/Speaker.cs | 26 ++++++++++++++++++++++
1 file changed, 26 insertions(+)
diff --git a/EXILED/Exiled.API/Features/Toys/Speaker.cs b/EXILED/Exiled.API/Features/Toys/Speaker.cs
index eb5106e728..72f8f14740 100644
--- a/EXILED/Exiled.API/Features/Toys/Speaker.cs
+++ b/EXILED/Exiled.API/Features/Toys/Speaker.cs
@@ -366,6 +366,32 @@ public static void Play(AudioMessage message, IEnumerable targets = null
target.Connection.Send(message);
}
+ ///
+ /// Plays a wav file one time through a newly spawned speaker and destroys it afterwards. (File must be 16 bit, mono and 48khz.)
+ ///
+ /// The path to the wav file.
+ /// The position of the speaker.
+ /// The parent transform, if any.
+ /// The play mode determining how audio is sent to players.
+ /// Whether to stream the audio or preload it.
+ /// The target player if PlayMode is Player.
+ /// The list of target players if PlayMode is PlayerList.
+ /// The condition if PlayMode is Predicate.
+ /// The created Speaker instance.
+ public static Speaker PlayOneShot(string path, Vector3 position, Transform parent = null, SpeakerPlayMode playMode = SpeakerPlayMode.Global, bool stream = false, Player targetPlayer = null, HashSet targetPlayers = null, Func predicate = null)
+ {
+ Speaker speaker = Create(parent: parent, position: position, spawn: true);
+
+ speaker.PlayMode = playMode;
+ speaker.TargetPlayer = targetPlayer;
+ speaker.TargetPlayers = targetPlayers;
+ speaker.Predicate = predicate;
+
+ speaker.Play(path, stream: stream, destroyAfter: true, loop: false);
+
+ return speaker;
+ }
+
///
/// Plays audio through this speaker.
///
From c6fe071a3b434fed4bea0cc503616b61a43e51a0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mustafa=20SAVA=C5=9E?=
Date: Sun, 1 Mar 2026 22:31:54 +0300
Subject: [PATCH 09/37] update
---
EXILED/Exiled.API/Features/Toys/Speaker.cs | 36 ++++++++++++++++------
1 file changed, 27 insertions(+), 9 deletions(-)
diff --git a/EXILED/Exiled.API/Features/Toys/Speaker.cs b/EXILED/Exiled.API/Features/Toys/Speaker.cs
index 72f8f14740..9da2ab49d1 100644
--- a/EXILED/Exiled.API/Features/Toys/Speaker.cs
+++ b/EXILED/Exiled.API/Features/Toys/Speaker.cs
@@ -335,14 +335,16 @@ public static Speaker Create(Transform parent = null, Vector3? position = null,
public static byte GetNextFreeControllerId()
{
byte id = 0;
- HashSet usedIds = HashSetPool.Pool.Get();
+ HashSet usedIds = NorthwoodLib.Pools.HashSetPool.Shared.Rent(256);
foreach (SpeakerToyPlaybackBase playbackBase in SpeakerToyPlaybackBase.AllInstances)
+ {
usedIds.Add(playbackBase.ControllerId);
+ }
if (usedIds.Count >= byte.MaxValue + 1)
{
- HashSetPool.Pool.Return(usedIds);
+ NorthwoodLib.Pools.HashSetPool.Shared.Return(usedIds);
return 0;
}
@@ -351,7 +353,7 @@ public static byte GetNextFreeControllerId()
id++;
}
- HashSetPool.Pool.Return(usedIds);
+ NorthwoodLib.Pools.HashSetPool.Shared.Return(usedIds);
return id;
}
@@ -377,7 +379,7 @@ public static void Play(AudioMessage message, IEnumerable targets = null
/// The target player if PlayMode is Player.
/// The list of target players if PlayMode is PlayerList.
/// The condition if PlayMode is Predicate.
- /// The created Speaker instance.
+ /// The created instance if playback started successfully; otherwise, null.
public static Speaker PlayOneShot(string path, Vector3 position, Transform parent = null, SpeakerPlayMode playMode = SpeakerPlayMode.Global, bool stream = false, Player targetPlayer = null, HashSet targetPlayers = null, Func predicate = null)
{
Speaker speaker = Create(parent: parent, position: position, spawn: true);
@@ -387,7 +389,11 @@ public static Speaker PlayOneShot(string path, Vector3 position, Transform paren
speaker.TargetPlayers = targetPlayers;
speaker.Predicate = predicate;
- speaker.Play(path, stream: stream, destroyAfter: true, loop: false);
+ if (!speaker.Play(path, stream: stream, destroyAfter: true, loop: false))
+ {
+ speaker.Destroy();
+ return null;
+ }
return speaker;
}
@@ -407,18 +413,19 @@ public static Speaker PlayOneShot(string path, Vector3 position, Transform paren
/// Whether to stream the audio or preload it.
/// Whether to destroy the speaker after playback.
/// Whether to loop the audio.
- public void Play(string path, bool stream = false, bool destroyAfter = false, bool loop = false)
+ /// true if the audio file was successfully found, loaded, and playback started; otherwise, false.
+ public bool Play(string path, bool stream = false, bool destroyAfter = false, bool loop = false)
{
if (!File.Exists(path))
{
Log.Error($"[Speaker] The specified file does not exist, path: `{path}`.");
- return;
+ return false;
}
if (!path.EndsWith(".wav", StringComparison.OrdinalIgnoreCase))
{
Log.Error($"[Speaker] The file type '{Path.GetExtension(path)}' is not supported. Please use .wav file.");
- return;
+ return false;
}
TryInitializePlayBack();
@@ -427,8 +434,19 @@ public void Play(string path, bool stream = false, bool destroyAfter = false, bo
Loop = loop;
LastTrack = path;
DestroyAfter = destroyAfter;
- source = stream ? new WavStreamSource(path) : new PreloadedPcmSource(path);
+
+ try
+ {
+ source = stream ? new WavStreamSource(path) : new PreloadedPcmSource(path);
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex);
+ return false;
+ }
+
playBackRoutine = Timing.RunCoroutine(PlayBackCoroutine().CancelWith(GameObject));
+ return true;
}
///
From 2d9feea2014b63dbd5adde889cb80c1268d934d9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mustafa=20SAVA=C5=9E?=
Date: Sun, 1 Mar 2026 22:43:30 +0300
Subject: [PATCH 10/37] update log
---
EXILED/Exiled.API/Features/Toys/Speaker.cs | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/EXILED/Exiled.API/Features/Toys/Speaker.cs b/EXILED/Exiled.API/Features/Toys/Speaker.cs
index 9da2ab49d1..9c5abc8899 100644
--- a/EXILED/Exiled.API/Features/Toys/Speaker.cs
+++ b/EXILED/Exiled.API/Features/Toys/Speaker.cs
@@ -441,7 +441,8 @@ public bool Play(string path, bool stream = false, bool destroyAfter = false, bo
}
catch (Exception ex)
{
- Log.Error(ex);
+ string loadMode = stream ? "Stream" : "Preload";
+ Log.Error($"[Speaker] Failed to initialize audio source ({loadMode}) for file at path: '{path}'.\nException Details: {ex}");
return false;
}
From 8ec3cde5cf5f2cfbaf6df0ebed7eb7730b7daadf Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mustafa=20SAVA=C5=9E?=
Date: Sun, 1 Mar 2026 23:21:42 +0300
Subject: [PATCH 11/37] color on wrong order
---
EXILED/Exiled.API/Features/Toys/Light.cs | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/EXILED/Exiled.API/Features/Toys/Light.cs b/EXILED/Exiled.API/Features/Toys/Light.cs
index 9e167d1bb5..4dad774de2 100644
--- a/EXILED/Exiled.API/Features/Toys/Light.cs
+++ b/EXILED/Exiled.API/Features/Toys/Light.cs
@@ -151,13 +151,12 @@ public static Light Create(Vector3? position /*= null*/, Vector3? rotation /*= n
Position = position ?? Vector3.zero,
Rotation = Quaternion.Euler(rotation ?? Vector3.zero),
Scale = scale ?? Vector3.one,
+ Color = color ?? Color.gray,
};
if (spawn)
light.Spawn();
- light.Color = color ?? Color.gray;
-
return light;
}
From 670dc1a64109260f3be92b46ae72214488dd85b2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mustafa=20SAVA=C5=9E?=
Date: Mon, 2 Mar 2026 01:12:17 +0300
Subject: [PATCH 12/37] wwait i will fix
---
EXILED/Exiled.API/Features/Toys/Speaker.cs | 87 +++++++++++++++++++++-
1 file changed, 86 insertions(+), 1 deletion(-)
diff --git a/EXILED/Exiled.API/Features/Toys/Speaker.cs b/EXILED/Exiled.API/Features/Toys/Speaker.cs
index 9c5abc8899..d39c3e7c34 100644
--- a/EXILED/Exiled.API/Features/Toys/Speaker.cs
+++ b/EXILED/Exiled.API/Features/Toys/Speaker.cs
@@ -24,6 +24,8 @@ namespace Exiled.API.Features.Toys
using Mirror;
+ using NorthwoodLib.Pools;
+
using UnityEngine;
using VoiceChat;
@@ -42,6 +44,8 @@ public class Speaker : AdminToy, IWrapper
private const int FrameSize = VoiceChatSettings.PacketSizePerChannel;
private const float FrameTime = (float)FrameSize / VoiceChatSettings.SampleRate;
+ private static readonly Queue Pool = new();
+
private float[] frame;
private byte[] encoded;
private float[] resampleBuffer;
@@ -282,6 +286,11 @@ public byte ControllerId
set => Base.NetworkControllerId = value;
}
+ ///
+ /// Gets or sets a value indicating whether the speaker should return to the pool after playback finishes.
+ ///
+ public bool ReturnToPoolAfter { get; set; }
+
///
/// Creates a new .
///
@@ -328,6 +337,45 @@ public static Speaker Create(Transform parent = null, Vector3? position = null,
return speaker;
}
+ ///
+ /// Rents an available speaker from the pool or creates a new one if the pool is empty.
+ ///
+ /// The local position of the .
+ /// The parent transform to attach the to.
+ /// A clean instance ready for use.
+ public static Speaker Rent(Vector3 position, Transform parent = null)
+ {
+ Speaker speaker = null;
+
+ while (Pool.Count > 0)
+ {
+ speaker = Pool.Dequeue();
+
+ if (speaker != null && speaker.Base != null)
+ break;
+
+ speaker = null;
+ }
+
+ if (speaker == null)
+ {
+ speaker = Create(parent: parent, position: position, spawn: true);
+ }
+ else
+ {
+ speaker.Transform.SetParent(parent);
+ speaker.Transform.localPosition = position;
+ speaker.ControllerId = GetNextFreeControllerId();
+ }
+
+ speaker.Volume = 1f;
+
+ speaker.ReturnToPoolAfter = true;
+ speaker.DestroyAfter = false;
+
+ return speaker;
+ }
+
///
/// Gets the next available controller ID for a .
///
@@ -465,6 +513,41 @@ public void Stop()
source = null;
}
+ ///
+ /// blabalbla.
+ ///
+ public void ReturnToPool()
+ {
+ Stop();
+
+ Transform.SetParent(null);
+ Transform.localPosition = Vector3.down * 999f;
+
+ Loop = false;
+ PlayMode = default;
+ DestroyAfter = false;
+ ReturnToPoolAfter = false;
+ Channel = Channels.ReliableOrdered2;
+
+ LastTrack = null;
+ Predicate = null;
+ TargetPlayer = null;
+ TargetPlayers = null;
+
+ Pitch = 1f;
+ Volume = 0f;
+ IsSpatial = true;
+
+ MinDistance = 1f;
+ MaxDistance = 15f;
+
+ resampleTime = 0.0;
+ resampleBufferFilled = 0;
+ isPitchDefault = true;
+
+ Pool.Enqueue(this);
+ }
+
private void TryInitializePlayBack()
{
if (isPlayBackInitialized)
@@ -526,7 +609,9 @@ private IEnumerator PlayBackCoroutine()
continue;
}
- if (DestroyAfter)
+ if (ReturnToPoolAfter)
+ ReturnToPool();
+ else if (DestroyAfter)
Destroy();
else
Stop();
From 527613c3a62d36d70f67874f8284f20494eb0620 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mustafa=20SAVA=C5=9E?=
Date: Mon, 2 Mar 2026 01:35:36 +0300
Subject: [PATCH 13/37] finished
---
EXILED/Exiled.API/Features/Toys/Speaker.cs | 25 +++++++++++-----------
1 file changed, 13 insertions(+), 12 deletions(-)
diff --git a/EXILED/Exiled.API/Features/Toys/Speaker.cs b/EXILED/Exiled.API/Features/Toys/Speaker.cs
index d39c3e7c34..38d16bc674 100644
--- a/EXILED/Exiled.API/Features/Toys/Speaker.cs
+++ b/EXILED/Exiled.API/Features/Toys/Speaker.cs
@@ -369,9 +369,8 @@ public static Speaker Rent(Vector3 position, Transform parent = null)
}
speaker.Volume = 1f;
-
- speaker.ReturnToPoolAfter = true;
- speaker.DestroyAfter = false;
+ speaker.MinDistance = 1f;
+ speaker.MaxDistance = 15f;
return speaker;
}
@@ -417,7 +416,7 @@ public static void Play(AudioMessage message, IEnumerable targets = null
}
///
- /// Plays a wav file one time through a newly spawned speaker and destroys it afterwards. (File must be 16 bit, mono and 48khz.)
+ /// Rents a speaker from the pool, plays a wav file one time, and automatically returns it to the pool afterwards. (File must be 16 bit, mono and 48khz.)
///
/// The path to the wav file.
/// The position of the speaker.
@@ -427,19 +426,21 @@ public static void Play(AudioMessage message, IEnumerable targets = null
/// The target player if PlayMode is Player.
/// The list of target players if PlayMode is PlayerList.
/// The condition if PlayMode is Predicate.
- /// The created instance if playback started successfully; otherwise, null.
- public static Speaker PlayOneShot(string path, Vector3 position, Transform parent = null, SpeakerPlayMode playMode = SpeakerPlayMode.Global, bool stream = false, Player targetPlayer = null, HashSet targetPlayers = null, Func predicate = null)
+ /// The rented instance if playback started successfully; otherwise, null.
+ public static Speaker PlayFromPool(string path, Vector3 position, Transform parent = null, SpeakerPlayMode playMode = SpeakerPlayMode.Global, bool stream = false, Player targetPlayer = null, HashSet targetPlayers = null, Func predicate = null)
{
- Speaker speaker = Create(parent: parent, position: position, spawn: true);
+ Speaker speaker = Rent(position, parent);
speaker.PlayMode = playMode;
speaker.TargetPlayer = targetPlayer;
speaker.TargetPlayers = targetPlayers;
speaker.Predicate = predicate;
- if (!speaker.Play(path, stream: stream, destroyAfter: true, loop: false))
+ speaker.ReturnToPoolAfter = true;
+
+ if (!speaker.Play(path, stream: stream))
{
- speaker.Destroy();
+ speaker.ReturnToPool();
return null;
}
@@ -521,7 +522,7 @@ public void ReturnToPool()
Stop();
Transform.SetParent(null);
- Transform.localPosition = Vector3.down * 999f;
+ Transform.localPosition = Vector3.down * 9999;
Loop = false;
PlayMode = default;
@@ -538,8 +539,8 @@ public void ReturnToPool()
Volume = 0f;
IsSpatial = true;
- MinDistance = 1f;
- MaxDistance = 15f;
+ MinDistance = 0;
+ MaxDistance = 0;
resampleTime = 0.0;
resampleBufferFilled = 0;
From 26909f645940e20b14a52b2573cbf6964164bfe2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mustafa=20SAVA=C5=9E?=
Date: Mon, 2 Mar 2026 02:15:35 +0300
Subject: [PATCH 14/37] fix
---
EXILED/Exiled.API/Features/Toys/Speaker.cs | 34 +++++++++++++++-------
1 file changed, 23 insertions(+), 11 deletions(-)
diff --git a/EXILED/Exiled.API/Features/Toys/Speaker.cs b/EXILED/Exiled.API/Features/Toys/Speaker.cs
index 38d16bc674..fff07ffebe 100644
--- a/EXILED/Exiled.API/Features/Toys/Speaker.cs
+++ b/EXILED/Exiled.API/Features/Toys/Speaker.cs
@@ -363,15 +363,14 @@ public static Speaker Rent(Vector3 position, Transform parent = null)
}
else
{
- speaker.Transform.SetParent(parent);
+ if (parent != null)
+ speaker.Transform.SetParent(parent);
+
+ speaker.Volume = 1f;
speaker.Transform.localPosition = position;
speaker.ControllerId = GetNextFreeControllerId();
}
- speaker.Volume = 1f;
- speaker.MinDistance = 1f;
- speaker.MaxDistance = 15f;
-
return speaker;
}
@@ -421,20 +420,32 @@ public static void Play(AudioMessage message, IEnumerable targets = null
/// The path to the wav file.
/// The position of the speaker.
/// The parent transform, if any.
+ /// Whether the audio source is spatialized.
+ /// The minimum distance at which the audio reaches full volume.
+ /// The maximum distance at which the audio can be heard.
/// The play mode determining how audio is sent to players.
/// Whether to stream the audio or preload it.
/// The target player if PlayMode is Player.
/// The list of target players if PlayMode is PlayerList.
/// The condition if PlayMode is Predicate.
/// The rented instance if playback started successfully; otherwise, null.
- public static Speaker PlayFromPool(string path, Vector3 position, Transform parent = null, SpeakerPlayMode playMode = SpeakerPlayMode.Global, bool stream = false, Player targetPlayer = null, HashSet targetPlayers = null, Func predicate = null)
+ public static Speaker PlayFromPool(string path, Vector3 position, Transform parent = null, bool isSpatial = true, float? minDistance = null, float? maxDistance = null, SpeakerPlayMode playMode = SpeakerPlayMode.Global, bool stream = false, Player targetPlayer = null, HashSet targetPlayers = null, Func predicate = null)
{
Speaker speaker = Rent(position, parent);
+ if (!isSpatial)
+ speaker.IsSpatial = isSpatial;
+
+ if (minDistance != null)
+ speaker.MinDistance = (float)minDistance;
+
+ if (maxDistance != null)
+ speaker.MaxDistance = (float)maxDistance;
+
speaker.PlayMode = playMode;
+ speaker.Predicate = predicate;
speaker.TargetPlayer = targetPlayer;
speaker.TargetPlayers = targetPlayers;
- speaker.Predicate = predicate;
speaker.ReturnToPoolAfter = true;
@@ -444,6 +455,7 @@ public static Speaker PlayFromPool(string path, Vector3 position, Transform pare
return null;
}
+ Log.Warn("pool size " + Pool.Count);
return speaker;
}
@@ -522,12 +534,12 @@ public void ReturnToPool()
Stop();
Transform.SetParent(null);
- Transform.localPosition = Vector3.down * 9999;
+ Transform.localPosition = Vector3.zero;
Loop = false;
- PlayMode = default;
DestroyAfter = false;
ReturnToPoolAfter = false;
+ PlayMode = SpeakerPlayMode.Global;
Channel = Channels.ReliableOrdered2;
LastTrack = null;
@@ -539,8 +551,8 @@ public void ReturnToPool()
Volume = 0f;
IsSpatial = true;
- MinDistance = 0;
- MaxDistance = 0;
+ MinDistance = 1;
+ MaxDistance = 15;
resampleTime = 0.0;
resampleBufferFilled = 0;
From 6da8cd226aa4e92076bf42c0cdfbbf0f65598992 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mustafa=20SAVA=C5=9E?=
Date: Mon, 2 Mar 2026 02:32:11 +0300
Subject: [PATCH 15/37] pool clear
---
EXILED/Exiled.API/Features/Toys/Speaker.cs | 4 ++--
EXILED/Exiled.Events/Handlers/Internal/Round.cs | 3 +++
2 files changed, 5 insertions(+), 2 deletions(-)
diff --git a/EXILED/Exiled.API/Features/Toys/Speaker.cs b/EXILED/Exiled.API/Features/Toys/Speaker.cs
index fff07ffebe..f82a01bcd1 100644
--- a/EXILED/Exiled.API/Features/Toys/Speaker.cs
+++ b/EXILED/Exiled.API/Features/Toys/Speaker.cs
@@ -44,8 +44,6 @@ public class Speaker : AdminToy, IWrapper
private const int FrameSize = VoiceChatSettings.PacketSizePerChannel;
private const float FrameTime = (float)FrameSize / VoiceChatSettings.SampleRate;
- private static readonly Queue Pool = new();
-
private float[] frame;
private byte[] encoded;
private float[] resampleBuffer;
@@ -60,6 +58,8 @@ public class Speaker : AdminToy, IWrapper
private bool isPitchDefault = true;
private bool isPlayBackInitialized = false;
+ internal static readonly Queue Pool = new();
+
///
/// Initializes a new instance of the class.
///
diff --git a/EXILED/Exiled.Events/Handlers/Internal/Round.cs b/EXILED/Exiled.Events/Handlers/Internal/Round.cs
index cebcffa749..886f8f05b5 100644
--- a/EXILED/Exiled.Events/Handlers/Internal/Round.cs
+++ b/EXILED/Exiled.Events/Handlers/Internal/Round.cs
@@ -18,6 +18,7 @@ namespace Exiled.Events.Handlers.Internal
using Exiled.API.Features.Items;
using Exiled.API.Features.Pools;
using Exiled.API.Features.Roles;
+ using Exiled.API.Features.Toys;
using Exiled.API.Structs;
using Exiled.Events.EventArgs.Player;
using Exiled.Events.EventArgs.Scp049;
@@ -65,6 +66,8 @@ public static void OnWaitingForPlayers()
///
public static void OnRestartingRound()
{
+ Speaker.Pool.Clear();
+
Scp049Role.TurnedPlayers.Clear();
Scp173Role.TurnedPlayers.Clear();
Scp096Role.TurnedPlayers.Clear();
From 952200f0d18395e215d04ab0d91ed5631dbdb9a5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mustafa=20SAVA=C5=9E?=
Date: Mon, 2 Mar 2026 02:32:47 +0300
Subject: [PATCH 16/37] fAHH!
---
EXILED/Exiled.API/Features/Toys/Speaker.cs | 1 -
1 file changed, 1 deletion(-)
diff --git a/EXILED/Exiled.API/Features/Toys/Speaker.cs b/EXILED/Exiled.API/Features/Toys/Speaker.cs
index f82a01bcd1..984f83f2a9 100644
--- a/EXILED/Exiled.API/Features/Toys/Speaker.cs
+++ b/EXILED/Exiled.API/Features/Toys/Speaker.cs
@@ -455,7 +455,6 @@ public static Speaker PlayFromPool(string path, Vector3 position, Transform pare
return null;
}
- Log.Warn("pool size " + Pool.Count);
return speaker;
}
From 06e8ffdfd0755bfa98ac3936bec77a171ced3e00 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mustafa=20SAVA=C5=9E?=
Date: Mon, 2 Mar 2026 02:34:25 +0300
Subject: [PATCH 17/37] Fahh
---
EXILED/Exiled.API/Features/Toys/Speaker.cs | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/EXILED/Exiled.API/Features/Toys/Speaker.cs b/EXILED/Exiled.API/Features/Toys/Speaker.cs
index 984f83f2a9..c9cc9957a4 100644
--- a/EXILED/Exiled.API/Features/Toys/Speaker.cs
+++ b/EXILED/Exiled.API/Features/Toys/Speaker.cs
@@ -41,6 +41,12 @@ namespace Exiled.API.Features.Toys
///
public class Speaker : AdminToy, IWrapper
{
+ ///
+ /// A queue used for object pooling of instances.
+ /// Reusing idle speakers instead of constantly creating and destroying them significantly improves server performance, especially for frequent audio events.
+ ///
+ internal static readonly Queue Pool = new();
+
private const int FrameSize = VoiceChatSettings.PacketSizePerChannel;
private const float FrameTime = (float)FrameSize / VoiceChatSettings.SampleRate;
@@ -58,8 +64,6 @@ public class Speaker : AdminToy, IWrapper
private bool isPitchDefault = true;
private bool isPlayBackInitialized = false;
- internal static readonly Queue Pool = new();
-
///
/// Initializes a new instance of the class.
///
From 8cab48f2d1bd6f7b066735f3c5d82695878d2138 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mustafa=20SAVA=C5=9E?=
Date: Mon, 2 Mar 2026 03:51:05 +0300
Subject: [PATCH 18/37] better and more functionally
---
EXILED/Exiled.API/Features/Toys/Speaker.cs | 20 +++++++++++++-------
1 file changed, 13 insertions(+), 7 deletions(-)
diff --git a/EXILED/Exiled.API/Features/Toys/Speaker.cs b/EXILED/Exiled.API/Features/Toys/Speaker.cs
index c9cc9957a4..1b6c0d0477 100644
--- a/EXILED/Exiled.API/Features/Toys/Speaker.cs
+++ b/EXILED/Exiled.API/Features/Toys/Speaker.cs
@@ -425,27 +425,33 @@ public static void Play(AudioMessage message, IEnumerable targets = null
/// The position of the speaker.
/// The parent transform, if any.
/// Whether the audio source is spatialized.
+ /// The volume level of the audio source.
/// The minimum distance at which the audio reaches full volume.
/// The maximum distance at which the audio can be heard.
+ /// The playback pitch level of the audio source.
/// The play mode determining how audio is sent to players.
/// Whether to stream the audio or preload it.
/// The target player if PlayMode is Player.
/// The list of target players if PlayMode is PlayerList.
/// The condition if PlayMode is Predicate.
/// The rented instance if playback started successfully; otherwise, null.
- public static Speaker PlayFromPool(string path, Vector3 position, Transform parent = null, bool isSpatial = true, float? minDistance = null, float? maxDistance = null, SpeakerPlayMode playMode = SpeakerPlayMode.Global, bool stream = false, Player targetPlayer = null, HashSet targetPlayers = null, Func predicate = null)
+ public static Speaker PlayFromPool(string path, Vector3 position, Transform parent = null, bool isSpatial = true, float? volume = null, float? minDistance = null, float? maxDistance = null, float pitch = 1f, SpeakerPlayMode playMode = SpeakerPlayMode.Global, bool stream = false, Player targetPlayer = null, HashSet targetPlayers = null, Func predicate = null)
{
Speaker speaker = Rent(position, parent);
if (!isSpatial)
speaker.IsSpatial = isSpatial;
- if (minDistance != null)
- speaker.MinDistance = (float)minDistance;
+ if (volume.HasValue)
+ speaker.Volume = volume.Value;
- if (maxDistance != null)
- speaker.MaxDistance = (float)maxDistance;
+ if (minDistance.HasValue)
+ speaker.MinDistance = minDistance.Value;
+ if (maxDistance.HasValue)
+ speaker.MaxDistance = maxDistance.Value;
+
+ speaker.Pitch = pitch;
speaker.PlayMode = playMode;
speaker.Predicate = predicate;
speaker.TargetPlayer = targetPlayer;
@@ -554,8 +560,8 @@ public void ReturnToPool()
Volume = 0f;
IsSpatial = true;
- MinDistance = 1;
- MaxDistance = 15;
+ MinDistance = 1f;
+ MaxDistance = 15f;
resampleTime = 0.0;
resampleBufferFilled = 0;
From 699eb564ac60ea734551cca898bdd449cdefdff7 Mon Sep 17 00:00:00 2001
From: MS-crew <100300664+MS-crew@users.noreply.github.com>
Date: Mon, 2 Mar 2026 04:39:41 +0300
Subject: [PATCH 19/37] Return to pool doc
---
EXILED/Exiled.API/Features/Toys/Speaker.cs | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/EXILED/Exiled.API/Features/Toys/Speaker.cs b/EXILED/Exiled.API/Features/Toys/Speaker.cs
index 1b6c0d0477..b6accaf781 100644
--- a/EXILED/Exiled.API/Features/Toys/Speaker.cs
+++ b/EXILED/Exiled.API/Features/Toys/Speaker.cs
@@ -536,8 +536,8 @@ public void Stop()
}
///
- /// blabalbla.
- ///
+ /// Stops the current playback, resets all properties of the , and returns the instance to the object pool for future reuse.
+ ///
public void ReturnToPool()
{
Stop();
From ec11415ee9b93ae2d727c4060f844fccb169592c Mon Sep 17 00:00:00 2001
From: MS-crew <100300664+MS-crew@users.noreply.github.com>
Date: Mon, 2 Mar 2026 04:44:57 +0300
Subject: [PATCH 20/37] Cleanup & Breaking Changes for exiled 10
Fck this old useless things
---
EXILED/Exiled.API/Features/Toys/Speaker.cs | 45 +---------------------
1 file changed, 2 insertions(+), 43 deletions(-)
diff --git a/EXILED/Exiled.API/Features/Toys/Speaker.cs b/EXILED/Exiled.API/Features/Toys/Speaker.cs
index b6accaf781..d653240ac4 100644
--- a/EXILED/Exiled.API/Features/Toys/Speaker.cs
+++ b/EXILED/Exiled.API/Features/Toys/Speaker.cs
@@ -295,39 +295,17 @@ public byte ControllerId
///
public bool ReturnToPoolAfter { get; set; }
- ///
- /// Creates a new .
- ///
- /// The position of the .
- /// The rotation of the .
- /// The scale of the .
- /// Whether the should be initially spawned.
- /// The new .
- [Obsolete("Use the Create(parent, position, scale, controllerId, spawn, worldPositonStays) method, rotation is useless.")]
- public static Speaker Create(Vector3? position, Vector3? rotation, Vector3? scale, bool spawn) => Create(parent: null, position: position, scale: scale, controllerId: null, spawn: spawn, worldPositionStays: true);
-
- ///
- /// Creates a new .
- ///
- /// The transform to create this on.
- /// Whether the should be initially spawned.
- /// Whether the should keep the same world position.
- /// The new .
- public static Speaker Create(Transform transform, bool spawn, bool worldPositionStays = true) => Create(parent: transform, position: Vector3.zero, scale: transform.localScale.normalized, controllerId: null, spawn: spawn, worldPositionStays: worldPositionStays);
-
///
/// Creates a new .
///
/// The parent transform to attach the to.
/// The local position of the .
- /// The scale of the .
/// The specific controller ID to assign. If null, the next available ID is used.
/// Whether the should be initially spawned.
- /// Whether the should keep the same world position when parented.
/// The new .
- public static Speaker Create(Transform parent = null, Vector3? position = null, Vector3? scale = null, byte? controllerId = null, bool spawn = true, bool worldPositionStays = true)
+ public static Speaker Create(Transform parent = null, Vector3? position = null, byte? controllerId = null, bool spawn = true)
{
- Speaker speaker = new(Object.Instantiate(Prefab, parent, worldPositionStays))
+ Speaker speaker = new(Object.Instantiate(Prefab, parent))
{
Scale = scale ?? Vector3.one,
ControllerId = controllerId ?? GetNextFreeControllerId(),
@@ -407,17 +385,6 @@ public static byte GetNextFreeControllerId()
return id;
}
- ///
- /// Plays audio through this speaker.
- ///
- /// An instance.
- /// Targets who will hear the audio. If null, audio will be sent to all players.
- public static void Play(AudioMessage message, IEnumerable targets = null)
- {
- foreach (Player target in targets ?? Player.List)
- target.Connection.Send(message);
- }
-
///
/// Rents a speaker from the pool, plays a wav file one time, and automatically returns it to the pool afterwards. (File must be 16 bit, mono and 48khz.)
///
@@ -468,14 +435,6 @@ public static Speaker PlayFromPool(string path, Vector3 position, Transform pare
return speaker;
}
- ///
- /// Plays audio through this speaker.
- ///
- /// Audio samples.
- /// The length of the samples array.
- /// Targets who will hear the audio. If null, audio will be sent to all players.
- public void Play(byte[] samples, int? length = null, IEnumerable targets = null) => Play(new AudioMessage(ControllerId, samples, length ?? samples.Length), targets);
-
///
/// Plays a wav file through this speaker.(File must be 16 bit, mono and 48khz.)
///
From 0c4371fe130881bea890431f6813c0bd87e29a29 Mon Sep 17 00:00:00 2001
From: MS-crew <100300664+MS-crew@users.noreply.github.com>
Date: Mon, 2 Mar 2026 04:52:38 +0300
Subject: [PATCH 21/37] Update Speaker.cs
---
EXILED/Exiled.API/Features/Toys/Speaker.cs | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/EXILED/Exiled.API/Features/Toys/Speaker.cs b/EXILED/Exiled.API/Features/Toys/Speaker.cs
index d653240ac4..7ad9bd5c61 100644
--- a/EXILED/Exiled.API/Features/Toys/Speaker.cs
+++ b/EXILED/Exiled.API/Features/Toys/Speaker.cs
@@ -307,7 +307,6 @@ public static Speaker Create(Transform parent = null, Vector3? position = null,
{
Speaker speaker = new(Object.Instantiate(Prefab, parent))
{
- Scale = scale ?? Vector3.one,
ControllerId = controllerId ?? GetNextFreeControllerId(),
};
@@ -496,7 +495,7 @@ public void Stop()
///
/// Stops the current playback, resets all properties of the , and returns the instance to the object pool for future reuse.
- ///
+ ///
public void ReturnToPool()
{
Stop();
From b5edd7a4bdc465899153f415733cc4e150a7452c Mon Sep 17 00:00:00 2001
From: MS-crew <100300664+MS-crew@users.noreply.github.com>
Date: Mon, 2 Mar 2026 05:00:00 +0300
Subject: [PATCH 22/37] Update Speaker.cs
---
EXILED/Exiled.API/Features/Toys/Speaker.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/EXILED/Exiled.API/Features/Toys/Speaker.cs b/EXILED/Exiled.API/Features/Toys/Speaker.cs
index 7ad9bd5c61..639bd1ada3 100644
--- a/EXILED/Exiled.API/Features/Toys/Speaker.cs
+++ b/EXILED/Exiled.API/Features/Toys/Speaker.cs
@@ -425,7 +425,7 @@ public static Speaker PlayFromPool(string path, Vector3 position, Transform pare
speaker.ReturnToPoolAfter = true;
- if (!speaker.Play(path, stream: stream))
+ if (!speaker.Play(path, stream))
{
speaker.ReturnToPool();
return null;
From 03e9a4ed2e47b5cc38ea8908eb9ed5d3daeee8de Mon Sep 17 00:00:00 2001
From: MS-crew <100300664+MS-crew@users.noreply.github.com>
Date: Mon, 2 Mar 2026 05:04:01 +0300
Subject: [PATCH 23/37] Update Speaker.cs
---
EXILED/Exiled.API/Features/Toys/Speaker.cs | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/EXILED/Exiled.API/Features/Toys/Speaker.cs b/EXILED/Exiled.API/Features/Toys/Speaker.cs
index 639bd1ada3..4b254506f8 100644
--- a/EXILED/Exiled.API/Features/Toys/Speaker.cs
+++ b/EXILED/Exiled.API/Features/Toys/Speaker.cs
@@ -469,8 +469,7 @@ public bool Play(string path, bool stream = false, bool destroyAfter = false, bo
}
catch (Exception ex)
{
- string loadMode = stream ? "Stream" : "Preload";
- Log.Error($"[Speaker] Failed to initialize audio source ({loadMode}) for file at path: '{path}'.\nException Details: {ex}");
+ Log.Error($"[Speaker] Failed to initialize audio source for file at path: '{path}'.\nException Details: {ex}");
return false;
}
From 44e41b85adb1089d95a3f2ae696fb66da29fab37 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mustafa=20SAVA=C5=9E?=
Date: Mon, 2 Mar 2026 15:21:13 +0300
Subject: [PATCH 24/37] little refactor
---
EXILED/Exiled.API/Features/Toys/Speaker.cs | 171 ++++++++++-----------
1 file changed, 85 insertions(+), 86 deletions(-)
diff --git a/EXILED/Exiled.API/Features/Toys/Speaker.cs b/EXILED/Exiled.API/Features/Toys/Speaker.cs
index 4b254506f8..45cc0749cc 100644
--- a/EXILED/Exiled.API/Features/Toys/Speaker.cs
+++ b/EXILED/Exiled.API/Features/Toys/Speaker.cs
@@ -16,7 +16,6 @@ namespace Exiled.API.Features.Toys
using Enums;
using Exiled.API.Features.Audio;
- using Exiled.API.Features.Pools;
using Interfaces;
@@ -127,6 +126,11 @@ internal Speaker(SpeakerToy speakerToy)
///
public bool DestroyAfter { get; set; }
+ ///
+ /// Gets or sets a value indicating whether the speaker should return to the pool after playback finishes.
+ ///
+ public bool ReturnToPoolAfter { get; set; }
+
///
/// Gets or sets the play mode for this speaker, determining how audio is sent to players.
///
@@ -187,12 +191,12 @@ public double CurrentTime
get => source?.CurrentTime ?? 0.0;
set
{
- if (source != null)
- {
- source.CurrentTime = value;
- resampleTime = 0.0;
- resampleBufferFilled = 0;
- }
+ if (source == null)
+ return;
+
+ source.CurrentTime = value;
+ resampleTime = 0.0;
+ resampleBufferFilled = 0;
}
}
@@ -290,11 +294,6 @@ public byte ControllerId
set => Base.NetworkControllerId = value;
}
- ///
- /// Gets or sets a value indicating whether the speaker should return to the pool after playback finishes.
- ///
- public bool ReturnToPoolAfter { get; set; }
-
///
/// Creates a new .
///
@@ -355,35 +354,6 @@ public static Speaker Rent(Vector3 position, Transform parent = null)
return speaker;
}
- ///
- /// Gets the next available controller ID for a .
- ///
- /// The next available byte ID. If all IDs are currently in use, returns a default of 0.
- public static byte GetNextFreeControllerId()
- {
- byte id = 0;
- HashSet usedIds = NorthwoodLib.Pools.HashSetPool.Shared.Rent(256);
-
- foreach (SpeakerToyPlaybackBase playbackBase in SpeakerToyPlaybackBase.AllInstances)
- {
- usedIds.Add(playbackBase.ControllerId);
- }
-
- if (usedIds.Count >= byte.MaxValue + 1)
- {
- NorthwoodLib.Pools.HashSetPool.Shared.Return(usedIds);
- return 0;
- }
-
- while (usedIds.Contains(id))
- {
- id++;
- }
-
- NorthwoodLib.Pools.HashSetPool.Shared.Return(usedIds);
- return id;
- }
-
///
/// Rents a speaker from the pool, plays a wav file one time, and automatically returns it to the pool afterwards. (File must be 16 bit, mono and 48khz.)
///
@@ -434,6 +404,35 @@ public static Speaker PlayFromPool(string path, Vector3 position, Transform pare
return speaker;
}
+ ///
+ /// Gets the next available controller ID for a .
+ ///
+ /// The next available byte ID. If all IDs are currently in use, returns a default of 0.
+ public static byte GetNextFreeControllerId()
+ {
+ byte id = 0;
+ HashSet usedIds = HashSetPool.Shared.Rent(256);
+
+ foreach (SpeakerToyPlaybackBase playbackBase in SpeakerToyPlaybackBase.AllInstances)
+ {
+ usedIds.Add(playbackBase.ControllerId);
+ }
+
+ if (usedIds.Count >= byte.MaxValue + 1)
+ {
+ HashSetPool.Shared.Return(usedIds);
+ return 0;
+ }
+
+ while (usedIds.Contains(id))
+ {
+ id++;
+ }
+
+ HashSetPool.Shared.Return(usedIds);
+ return id;
+ }
+
///
/// Plays a wav file through this speaker.(File must be 16 bit, mono and 48khz.)
///
@@ -602,6 +601,51 @@ private IEnumerator PlayBackCoroutine()
}
}
+ private void SendPacket(int len)
+ {
+ AudioMessage msg = new(ControllerId, encoded, len);
+
+ switch (PlayMode)
+ {
+ case SpeakerPlayMode.Global:
+ NetworkServer.SendToReady(msg, Channel);
+ break;
+
+ case SpeakerPlayMode.Player:
+ TargetPlayer?.Connection.Send(msg, Channel);
+ break;
+
+ case SpeakerPlayMode.PlayerList:
+ using (NetworkWriterPooled writer = NetworkWriterPool.Get())
+ {
+ NetworkMessages.Pack(msg, writer);
+ ArraySegment segment = writer.ToArraySegment();
+
+ foreach (Player ply in TargetPlayers)
+ {
+ ply?.Connection.Send(segment, Channel);
+ }
+ }
+
+ break;
+
+ case SpeakerPlayMode.Predicate:
+ using (NetworkWriterPooled writer = NetworkWriterPool.Get())
+ {
+ NetworkMessages.Pack(msg, writer);
+ ArraySegment segment = writer.ToArraySegment();
+
+ foreach (Player ply in Player.List)
+ {
+ if (Predicate(ply))
+ ply.Connection.Send(segment, Channel);
+ }
+ }
+
+ break;
+ }
+ }
+
private void ResampleFrame()
{
int requiredSize = (int)(FrameSize * Mathf.Abs(Pitch) * 2) + 10;
@@ -672,51 +716,6 @@ private void ResampleFrame()
}
}
- private void SendPacket(int len)
- {
- AudioMessage msg = new(ControllerId, encoded, len);
-
- switch (PlayMode)
- {
- case SpeakerPlayMode.Global:
- NetworkServer.SendToReady(msg, Channel);
- break;
-
- case SpeakerPlayMode.Player:
- TargetPlayer?.Connection.Send(msg, Channel);
- break;
-
- case SpeakerPlayMode.PlayerList:
- using (NetworkWriterPooled writer = NetworkWriterPool.Get())
- {
- NetworkMessages.Pack(msg, writer);
- ArraySegment segment = writer.ToArraySegment();
-
- foreach (Player ply in TargetPlayers)
- {
- ply?.Connection.Send(segment, Channel);
- }
- }
-
- break;
-
- case SpeakerPlayMode.Predicate:
- using (NetworkWriterPooled writer = NetworkWriterPool.Get())
- {
- NetworkMessages.Pack(msg, writer);
- ArraySegment segment = writer.ToArraySegment();
-
- foreach (Player ply in Player.List)
- {
- if (Predicate(ply))
- ply.Connection.Send(segment, Channel);
- }
- }
-
- break;
- }
- }
-
private void OnToyRemoved(AdminToyBase toy)
{
if (toy != Base)
From 6a1bfc3a0a813c3c99a5c23d109372f1fd5b2547 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mustafa=20SAVA=C5=9E?=
Date: Mon, 2 Mar 2026 16:13:51 +0300
Subject: [PATCH 25/37] Performance improvement & fix Architectural problems in
pooling
---
EXILED/Exiled.API/Features/Toys/Speaker.cs | 17 ++++++++++-------
1 file changed, 10 insertions(+), 7 deletions(-)
diff --git a/EXILED/Exiled.API/Features/Toys/Speaker.cs b/EXILED/Exiled.API/Features/Toys/Speaker.cs
index 45cc0749cc..59142b6656 100644
--- a/EXILED/Exiled.API/Features/Toys/Speaker.cs
+++ b/EXILED/Exiled.API/Features/Toys/Speaker.cs
@@ -343,10 +343,11 @@ public static Speaker Rent(Vector3 position, Transform parent = null)
}
else
{
+ speaker.IsStatic = false;
+
if (parent != null)
speaker.Transform.SetParent(parent);
- speaker.Volume = 1f;
speaker.Transform.localPosition = position;
speaker.ControllerId = GetNextFreeControllerId();
}
@@ -370,8 +371,8 @@ public static Speaker Rent(Vector3 position, Transform parent = null)
/// The target player if PlayMode is Player.
/// The list of target players if PlayMode is PlayerList.
/// The condition if PlayMode is Predicate.
- /// The rented instance if playback started successfully; otherwise, null.
- public static Speaker PlayFromPool(string path, Vector3 position, Transform parent = null, bool isSpatial = true, float? volume = null, float? minDistance = null, float? maxDistance = null, float pitch = 1f, SpeakerPlayMode playMode = SpeakerPlayMode.Global, bool stream = false, Player targetPlayer = null, HashSet targetPlayers = null, Func predicate = null)
+ /// true if the audio file was successfully found, loaded, and playback started; otherwise, false.
+ public static bool PlayFromPool(string path, Vector3 position, Transform parent = null, bool isSpatial = true, float? volume = null, float? minDistance = null, float? maxDistance = null, float pitch = 1f, SpeakerPlayMode playMode = SpeakerPlayMode.Global, bool stream = false, Player targetPlayer = null, HashSet targetPlayers = null, Func predicate = null)
{
Speaker speaker = Rent(position, parent);
@@ -398,10 +399,10 @@ public static Speaker PlayFromPool(string path, Vector3 position, Transform pare
if (!speaker.Play(path, stream))
{
speaker.ReturnToPool();
- return null;
+ return false;
}
- return speaker;
+ return true;
}
///
@@ -498,8 +499,10 @@ public void ReturnToPool()
{
Stop();
+ IsStatic = true;
+
Transform.SetParent(null);
- Transform.localPosition = Vector3.zero;
+ Position = Vector3.one * 999;
Loop = false;
DestroyAfter = false;
@@ -513,7 +516,7 @@ public void ReturnToPool()
TargetPlayers = null;
Pitch = 1f;
- Volume = 0f;
+ Volume = 1f;
IsSpatial = true;
MinDistance = 1f;
From d80362bfa4c3f3742787dfcff9dd360b3ad0624a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mustafa=20SAVA=C5=9E?=
Date: Mon, 2 Mar 2026 16:14:02 +0300
Subject: [PATCH 26/37] wth
---
EXILED/Exiled.API/Features/Toys/Speaker.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/EXILED/Exiled.API/Features/Toys/Speaker.cs b/EXILED/Exiled.API/Features/Toys/Speaker.cs
index 59142b6656..e71adf7850 100644
--- a/EXILED/Exiled.API/Features/Toys/Speaker.cs
+++ b/EXILED/Exiled.API/Features/Toys/Speaker.cs
@@ -502,7 +502,7 @@ public void ReturnToPool()
IsStatic = true;
Transform.SetParent(null);
- Position = Vector3.one * 999;
+ Position = Vector3.down * 9999;
Loop = false;
DestroyAfter = false;
From f38df4eecee3ee32f9bebbc592dd792c108991cc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mustafa=20SAVA=C5=9E?=
Date: Tue, 3 Mar 2026 15:15:40 +0300
Subject: [PATCH 27/37] .
---
EXILED/Exiled.API/Features/Toys/Speaker.cs | 12 ++++++++++--
1 file changed, 10 insertions(+), 2 deletions(-)
diff --git a/EXILED/Exiled.API/Features/Toys/Speaker.cs b/EXILED/Exiled.API/Features/Toys/Speaker.cs
index e71adf7850..f3ff7c2be6 100644
--- a/EXILED/Exiled.API/Features/Toys/Speaker.cs
+++ b/EXILED/Exiled.API/Features/Toys/Speaker.cs
@@ -299,14 +299,22 @@ public byte ControllerId
///
/// The parent transform to attach the to.
/// The local position of the .
+ /// The volume level of the audio source.
+ /// Whether the audio source is spatialized (3D sound).
+ /// The minimum distance at which the audio reaches full volume.
+ /// The maximum distance at which the audio can be heard.
/// The specific controller ID to assign. If null, the next available ID is used.
/// Whether the should be initially spawned.
/// The new .
- public static Speaker Create(Transform parent = null, Vector3? position = null, byte? controllerId = null, bool spawn = true)
+ public static Speaker Create(Transform parent = null, Vector3? position = null, float volume = 1f, bool isSpatial = true, float minDistance = 1f, float maxDistance = 15f, byte? controllerId = null, bool spawn = true)
{
Speaker speaker = new(Object.Instantiate(Prefab, parent))
{
- ControllerId = controllerId ?? GetNextFreeControllerId(),
+ Volume = volume,
+ IsSpatial = isSpatial,
+ MinDistance = minDistance,
+ MaxDistance = maxDistance,
+ ControllerId = controllerId ?? GetNextFreeControllerId(),
};
speaker.Transform.localPosition = position ?? Vector3.zero;
From 8ff218091720b59788e138124c93b10aa5660645 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mustafa=20SAVA=C5=9E?=
Date: Thu, 5 Mar 2026 15:11:23 +0300
Subject: [PATCH 28/37] fix return pool
---
EXILED/Exiled.API/Features/Toys/Speaker.cs | 25 ++++++++++++++++------
1 file changed, 18 insertions(+), 7 deletions(-)
diff --git a/EXILED/Exiled.API/Features/Toys/Speaker.cs b/EXILED/Exiled.API/Features/Toys/Speaker.cs
index f3ff7c2be6..6079db2940 100644
--- a/EXILED/Exiled.API/Features/Toys/Speaker.cs
+++ b/EXILED/Exiled.API/Features/Toys/Speaker.cs
@@ -509,9 +509,26 @@ public void ReturnToPool()
IsStatic = true;
- Transform.SetParent(null);
+ if (Transform.parent != null)
+ {
+ Transform.SetParent(null);
+ Base.RpcChangeParent(0);
+ }
+
Position = Vector3.down * 9999;
+ if (Volume != 1f)
+ Volume = 1f;
+
+ if (!IsSpatial)
+ IsSpatial = true;
+
+ if (MinDistance != 1f)
+ MinDistance = 1f;
+
+ if (MaxDistance != 15f)
+ MaxDistance = 15f;
+
Loop = false;
DestroyAfter = false;
ReturnToPoolAfter = false;
@@ -524,12 +541,6 @@ public void ReturnToPool()
TargetPlayers = null;
Pitch = 1f;
- Volume = 1f;
- IsSpatial = true;
-
- MinDistance = 1f;
- MaxDistance = 15f;
-
resampleTime = 0.0;
resampleBufferFilled = 0;
isPitchDefault = true;
From e926b585fc3f2f9f5db0d4e40272e55b96cd0bb2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mustafa=20SAVA=C5=9E?=
Date: Sat, 7 Mar 2026 00:04:31 +0300
Subject: [PATCH 29/37] .
---
EXILED/Exiled.API/Features/Toys/Speaker.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/EXILED/Exiled.API/Features/Toys/Speaker.cs b/EXILED/Exiled.API/Features/Toys/Speaker.cs
index 6079db2940..2e9e7ce6bd 100644
--- a/EXILED/Exiled.API/Features/Toys/Speaker.cs
+++ b/EXILED/Exiled.API/Features/Toys/Speaker.cs
@@ -509,7 +509,7 @@ public void ReturnToPool()
IsStatic = true;
- if (Transform.parent != null)
+ if (Transform.parent != null || AdminToyBase._clientParentId != 0)
{
Transform.SetParent(null);
Base.RpcChangeParent(0);
From 575ec405de3edb45948f5b24dfa98dd9563f18bf Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mustafa=20SAVA=C5=9E?=
Date: Sat, 7 Mar 2026 16:40:49 +0300
Subject: [PATCH 30/37] d
---
EXILED/Exiled.API/Features/Toys/Speaker.cs | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/EXILED/Exiled.API/Features/Toys/Speaker.cs b/EXILED/Exiled.API/Features/Toys/Speaker.cs
index 2e9e7ce6bd..55fc87dd57 100644
--- a/EXILED/Exiled.API/Features/Toys/Speaker.cs
+++ b/EXILED/Exiled.API/Features/Toys/Speaker.cs
@@ -507,8 +507,6 @@ public void ReturnToPool()
{
Stop();
- IsStatic = true;
-
if (Transform.parent != null || AdminToyBase._clientParentId != 0)
{
Transform.SetParent(null);
@@ -529,6 +527,8 @@ public void ReturnToPool()
if (MaxDistance != 15f)
MaxDistance = 15f;
+ IsStatic = true;
+
Loop = false;
DestroyAfter = false;
ReturnToPoolAfter = false;
From b2bdac1068181129aeec8372ae86021b2b8fae4e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mustafa=20SAVA=C5=9E?=
Date: Sun, 8 Mar 2026 02:00:55 +0300
Subject: [PATCH 31/37] removed Hard Coded values
---
EXILED/Exiled.API/Features/Toys/Speaker.cs | 32 ++++++++++++++--------
1 file changed, 20 insertions(+), 12 deletions(-)
diff --git a/EXILED/Exiled.API/Features/Toys/Speaker.cs b/EXILED/Exiled.API/Features/Toys/Speaker.cs
index 55fc87dd57..3cf10a2176 100644
--- a/EXILED/Exiled.API/Features/Toys/Speaker.cs
+++ b/EXILED/Exiled.API/Features/Toys/Speaker.cs
@@ -46,9 +46,17 @@ public class Speaker : AdminToy, IWrapper
///
internal static readonly Queue Pool = new();
+ private const float DefaultVolume = 1f;
+ private const float DefaultMinDistance = 1f;
+ private const float DefaultMaxDistance = 15f;
+
+ private const bool DefaultSpatial = true;
+
private const int FrameSize = VoiceChatSettings.PacketSizePerChannel;
private const float FrameTime = (float)FrameSize / VoiceChatSettings.SampleRate;
+ private static readonly Vector3 SpeakerParkPosition = Vector3.down * 999;
+
private float[] frame;
private byte[] encoded;
private float[] resampleBuffer;
@@ -387,13 +395,13 @@ public static bool PlayFromPool(string path, Vector3 position, Transform parent
if (!isSpatial)
speaker.IsSpatial = isSpatial;
- if (volume.HasValue)
+ if (volume.HasValue && volume.Value != DefaultVolume)
speaker.Volume = volume.Value;
- if (minDistance.HasValue)
+ if (minDistance.HasValue && minDistance.Value != DefaultMinDistance)
speaker.MinDistance = minDistance.Value;
- if (maxDistance.HasValue)
+ if (maxDistance.HasValue && maxDistance.Value != DefaultMaxDistance)
speaker.MaxDistance = maxDistance.Value;
speaker.Pitch = pitch;
@@ -513,19 +521,19 @@ public void ReturnToPool()
Base.RpcChangeParent(0);
}
- Position = Vector3.down * 9999;
+ Position = SpeakerParkPosition;
- if (Volume != 1f)
- Volume = 1f;
+ if (Volume != DefaultVolume)
+ Volume = DefaultVolume;
- if (!IsSpatial)
- IsSpatial = true;
+ if (IsSpatial != DefaultSpatial)
+ IsSpatial = DefaultSpatial;
- if (MinDistance != 1f)
- MinDistance = 1f;
+ if (MinDistance != DefaultMinDistance)
+ MinDistance = DefaultMinDistance;
- if (MaxDistance != 15f)
- MaxDistance = 15f;
+ if (MaxDistance != DefaultMaxDistance)
+ MaxDistance = DefaultMaxDistance;
IsStatic = true;
From d79555bf839d9adaf3827d527072ea950484d556 Mon Sep 17 00:00:00 2001
From: MS-crew <100300664+MS-crew@users.noreply.github.com>
Date: Sun, 8 Mar 2026 06:20:29 +0300
Subject: [PATCH 32/37] Update Speaker.cs
---
EXILED/Exiled.API/Features/Toys/Speaker.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/EXILED/Exiled.API/Features/Toys/Speaker.cs b/EXILED/Exiled.API/Features/Toys/Speaker.cs
index 3cf10a2176..44633f0aa2 100644
--- a/EXILED/Exiled.API/Features/Toys/Speaker.cs
+++ b/EXILED/Exiled.API/Features/Toys/Speaker.cs
@@ -392,7 +392,7 @@ public static bool PlayFromPool(string path, Vector3 position, Transform parent
{
Speaker speaker = Rent(position, parent);
- if (!isSpatial)
+ if (isSpatial != DefaultSpatial)
speaker.IsSpatial = isSpatial;
if (volume.HasValue && volume.Value != DefaultVolume)
From 0ea7bd0a682765d28993f66f48148ee303c47518 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mustafa=20SAVA=C5=9E?=
Date: Sun, 8 Mar 2026 16:09:52 +0300
Subject: [PATCH 33/37] fAHHHHH
---
EXILED/Exiled.API/Features/Toys/Speaker.cs | 23 ++++++++++++++++++++--
1 file changed, 21 insertions(+), 2 deletions(-)
diff --git a/EXILED/Exiled.API/Features/Toys/Speaker.cs b/EXILED/Exiled.API/Features/Toys/Speaker.cs
index 3cf10a2176..db41788158 100644
--- a/EXILED/Exiled.API/Features/Toys/Speaker.cs
+++ b/EXILED/Exiled.API/Features/Toys/Speaker.cs
@@ -163,7 +163,16 @@ internal Speaker(SpeakerToy speakerToy)
///
/// Gets a value indicating whether gets is a sound playing on this speaker or not.
///
- public bool IsPlaying => playBackRoutine.IsRunning && !IsPaused;
+ public bool IsPlaying
+ {
+ get
+ {
+ if (playBackRoutine == null)
+ return false;
+
+ return playBackRoutine.IsRunning && !IsPaused;
+ }
+ }
///
/// Gets or sets a value indicating whether the playback is paused.
@@ -176,6 +185,9 @@ public bool IsPaused
get => playBackRoutine.IsAliveAndPaused;
set
{
+ if (playBackRoutine == null)
+ return;
+
if (!playBackRoutine.IsRunning)
return;
@@ -214,6 +226,12 @@ public double CurrentTime
///
public double TotalDuration => source?.TotalDuration ?? 0.0;
+ ///
+ /// Gets the current playback progress as a value between 0.0 and 1.0.
+ /// Returns 0 if not playing.
+ ///
+ public float PlaybackProgress => TotalDuration > 0.0 ? (float)(CurrentTime / TotalDuration) : 0f;
+
///
/// Gets the path to the last audio file played on this speaker.
///
@@ -428,7 +446,7 @@ public static bool PlayFromPool(string path, Vector3 position, Transform parent
public static byte GetNextFreeControllerId()
{
byte id = 0;
- HashSet usedIds = HashSetPool.Shared.Rent(256);
+ HashSet usedIds = HashSetPool.Shared.Rent(byte.MaxValue + 1);
foreach (SpeakerToyPlaybackBase playbackBase in SpeakerToyPlaybackBase.AllInstances)
{
@@ -438,6 +456,7 @@ public static byte GetNextFreeControllerId()
if (usedIds.Count >= byte.MaxValue + 1)
{
HashSetPool.Shared.Return(usedIds);
+ Log.Warn("[Speaker] All controller IDs are in use. Audio may conflict!");
return 0;
}
From ad14a1240f532547c5fa58472658d7187dde1cd1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mustafa=20SAVA=C5=9E?=
Date: Sun, 8 Mar 2026 17:07:34 +0300
Subject: [PATCH 34/37] fix
---
EXILED/Exiled.API/Features/Toys/Speaker.cs | 25 +++++++++-------------
1 file changed, 10 insertions(+), 15 deletions(-)
diff --git a/EXILED/Exiled.API/Features/Toys/Speaker.cs b/EXILED/Exiled.API/Features/Toys/Speaker.cs
index 2c2c4c5a77..6b672e186b 100644
--- a/EXILED/Exiled.API/Features/Toys/Speaker.cs
+++ b/EXILED/Exiled.API/Features/Toys/Speaker.cs
@@ -161,18 +161,9 @@ internal Speaker(SpeakerToy speakerToy)
public Func Predicate { get; set; }
///
- /// Gets a value indicating whether gets is a sound playing on this speaker or not.
+ /// Gets a value indicating whether a sound is currently playing on this speaker.
///
- public bool IsPlaying
- {
- get
- {
- if (playBackRoutine == null)
- return false;
-
- return playBackRoutine.IsRunning && !IsPaused;
- }
- }
+ public bool IsPlaying => playBackRoutine.IsRunning && !IsPaused;
///
/// Gets or sets a value indicating whether the playback is paused.
@@ -185,9 +176,6 @@ public bool IsPaused
get => playBackRoutine.IsAliveAndPaused;
set
{
- if (playBackRoutine == null)
- return;
-
if (!playBackRoutine.IsRunning)
return;
@@ -519,7 +507,7 @@ public void Stop()
{
if (playBackRoutine.IsRunning)
{
- Timing.KillCoroutines(playBackRoutine);
+ playBackRoutine.IsRunning = false;
OnPlaybackStopped?.Invoke();
}
@@ -665,6 +653,10 @@ private void SendPacket(int len)
break;
case SpeakerPlayMode.PlayerList:
+
+ if (TargetPlayers is null)
+ break;
+
using (NetworkWriterPooled writer = NetworkWriterPool.Get())
{
NetworkMessages.Pack(msg, writer);
@@ -679,6 +671,9 @@ private void SendPacket(int len)
break;
case SpeakerPlayMode.Predicate:
+ if (Predicate is null)
+ break;
+
using (NetworkWriterPooled writer = NetworkWriterPool.Get())
{
NetworkMessages.Pack(msg, writer);
From 817b77807a00faae8da36c72f05b1b91ce4b8536 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mustafa=20SAVA=C5=9E?=
Date: Sun, 8 Mar 2026 17:11:56 +0300
Subject: [PATCH 35/37] f
---
EXILED/Exiled.API/Features/Toys/Speaker.cs | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/EXILED/Exiled.API/Features/Toys/Speaker.cs b/EXILED/Exiled.API/Features/Toys/Speaker.cs
index 6b672e186b..65da247f7b 100644
--- a/EXILED/Exiled.API/Features/Toys/Speaker.cs
+++ b/EXILED/Exiled.API/Features/Toys/Speaker.cs
@@ -50,6 +50,8 @@ public class Speaker : AdminToy, IWrapper
private const float DefaultMinDistance = 1f;
private const float DefaultMaxDistance = 15f;
+ private const byte DefaultControllerId = 0;
+
private const bool DefaultSpatial = true;
private const int FrameSize = VoiceChatSettings.PacketSizePerChannel;
@@ -444,8 +446,8 @@ public static byte GetNextFreeControllerId()
if (usedIds.Count >= byte.MaxValue + 1)
{
HashSetPool.Shared.Return(usedIds);
- Log.Warn("[Speaker] All controller IDs are in use. Audio may conflict!");
- return 0;
+ Log.Warn("[Speaker] All controller IDs are in use. Default Controll Id will be use, Audio may conflict!");
+ return DefaultControllerId;
}
while (usedIds.Contains(id))
From 55a8a838e9f3740fe1fe35ed8c74656eaf8cfd7d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mustafa=20SAVA=C5=9E?=
Date: Sun, 8 Mar 2026 20:19:16 +0300
Subject: [PATCH 36/37] izabel
---
EXILED/Exiled.API/Features/Toys/Speaker.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/EXILED/Exiled.API/Features/Toys/Speaker.cs b/EXILED/Exiled.API/Features/Toys/Speaker.cs
index 65da247f7b..a21d44e09a 100644
--- a/EXILED/Exiled.API/Features/Toys/Speaker.cs
+++ b/EXILED/Exiled.API/Features/Toys/Speaker.cs
@@ -322,7 +322,7 @@ public byte ControllerId
/// The specific controller ID to assign. If null, the next available ID is used.
/// Whether the should be initially spawned.
/// The new .
- public static Speaker Create(Transform parent = null, Vector3? position = null, float volume = 1f, bool isSpatial = true, float minDistance = 1f, float maxDistance = 15f, byte? controllerId = null, bool spawn = true)
+ public static Speaker Create(Transform parent = null, Vector3? position = null, float volume = DefaultVolume, bool isSpatial = DefaultSpatial, float minDistance = DefaultMinDistance, float maxDistance = DefaultMaxDistance, byte? controllerId = null, bool spawn = true)
{
Speaker speaker = new(Object.Instantiate(Prefab, parent))
{
From a476bb898d2a766aa013325655e13c7411094ff8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mustafa=20SAVA=C5=9E?=
Date: Mon, 9 Mar 2026 01:08:40 +0300
Subject: [PATCH 37/37] Release controller ID on pool return to prevent ID
exhaustion
---
EXILED/Exiled.API/Features/Toys/Speaker.cs | 3 +++
1 file changed, 3 insertions(+)
diff --git a/EXILED/Exiled.API/Features/Toys/Speaker.cs b/EXILED/Exiled.API/Features/Toys/Speaker.cs
index a21d44e09a..8980921bcf 100644
--- a/EXILED/Exiled.API/Features/Toys/Speaker.cs
+++ b/EXILED/Exiled.API/Features/Toys/Speaker.cs
@@ -544,6 +544,9 @@ public void ReturnToPool()
if (MaxDistance != DefaultMaxDistance)
MaxDistance = DefaultMaxDistance;
+ if (ControllerId != DefaultControllerId)
+ ControllerId = DefaultControllerId;
+
IsStatic = true;
Loop = false;