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;