Skip to content

Commit bace526

Browse files
committed
feat: Add SoundEvent API for emitting sounds with parameters
EmitSound goes through CBaseEntity::EmitSoundFilter, which sends the sound event message without any packed parameters, so the volume and pitch arguments never reach the client and have no effect. Build the CMsgSosStartSoundEvent message directly instead, so sound operator parameters can be set per emit. Volume and pitch work, and any other public.* parameter the sound stack reads can be set as well. EmitSound is left as it is, so nothing existing changes behaviour.
1 parent afa0c99 commit bace526

8 files changed

Lines changed: 810 additions & 20 deletions

File tree

CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,8 @@ set(SOURCE_FILES
111111
src/core/managers/usermessage_manager.cpp
112112
src/core/managers/usermessage_manager.h
113113
src/scripting/natives/natives_raytrace.cpp
114+
src/scripting/natives/natives_sounds.cpp
115+
src/core/sound_event.cpp
114116
src/scripting/natives/natives_dynamichooks.cpp
115117
src/core/game_system.h
116118
src/core/game_system.cpp

managed/CounterStrikeSharp.API/Generated/Natives/API.cs

Lines changed: 101 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1690,26 +1690,6 @@ public static void GetEntityWorldSpaceAabb(IntPtr entity, IntPtr minsout, IntPtr
16901690
}
16911691
}
16921692

1693-
public static IntPtr GetEconItemSystem(){
1694-
lock (ScriptContext.GlobalScriptContext.Lock) {
1695-
ScriptContext.GlobalScriptContext.Reset();
1696-
ScriptContext.GlobalScriptContext.SetIdentifier(0x981E9B5B);
1697-
ScriptContext.GlobalScriptContext.Invoke();
1698-
ScriptContext.GlobalScriptContext.CheckErrors();
1699-
return ScriptContext.GlobalScriptContext.GetResultPrimitive<IntPtr>();
1700-
}
1701-
}
1702-
1703-
public static bool IsServerPaused(){
1704-
lock (ScriptContext.GlobalScriptContext.Lock) {
1705-
ScriptContext.GlobalScriptContext.Reset();
1706-
ScriptContext.GlobalScriptContext.SetIdentifier(0xB216AAAC);
1707-
ScriptContext.GlobalScriptContext.Invoke();
1708-
ScriptContext.GlobalScriptContext.CheckErrors();
1709-
return ScriptContext.GlobalScriptContext.GetResultPrimitive<bool>();
1710-
}
1711-
}
1712-
17131693
public static short GetSchemaOffset(string classname, string propname){
17141694
lock (ScriptContext.GlobalScriptContext.Lock) {
17151695
ScriptContext.GlobalScriptContext.Reset();
@@ -1799,6 +1779,107 @@ public static void SchemaNetworkStateChanged(IntPtr instance, uint offset, uint
17991779
}
18001780
}
18011781

1782+
public static IntPtr GetEconItemSystem(){
1783+
lock (ScriptContext.GlobalScriptContext.Lock) {
1784+
ScriptContext.GlobalScriptContext.Reset();
1785+
ScriptContext.GlobalScriptContext.SetIdentifier(0x981E9B5B);
1786+
ScriptContext.GlobalScriptContext.Invoke();
1787+
ScriptContext.GlobalScriptContext.CheckErrors();
1788+
return ScriptContext.GlobalScriptContext.GetResultPrimitive<IntPtr>();
1789+
}
1790+
}
1791+
1792+
public static bool IsServerPaused(){
1793+
lock (ScriptContext.GlobalScriptContext.Lock) {
1794+
ScriptContext.GlobalScriptContext.Reset();
1795+
ScriptContext.GlobalScriptContext.SetIdentifier(0xB216AAAC);
1796+
ScriptContext.GlobalScriptContext.Invoke();
1797+
ScriptContext.GlobalScriptContext.CheckErrors();
1798+
return ScriptContext.GlobalScriptContext.GetResultPrimitive<bool>();
1799+
}
1800+
}
1801+
1802+
public static IntPtr SoundEventCreate(string name){
1803+
lock (ScriptContext.GlobalScriptContext.Lock) {
1804+
ScriptContext.GlobalScriptContext.Reset();
1805+
ScriptContext.GlobalScriptContext.PushString(name);
1806+
ScriptContext.GlobalScriptContext.SetIdentifier(0xB724884E);
1807+
ScriptContext.GlobalScriptContext.Invoke();
1808+
ScriptContext.GlobalScriptContext.CheckErrors();
1809+
return ScriptContext.GlobalScriptContext.GetResultPrimitive<IntPtr>();
1810+
}
1811+
}
1812+
1813+
public static void SoundEventRelease(IntPtr soundevent){
1814+
lock (ScriptContext.GlobalScriptContext.Lock) {
1815+
ScriptContext.GlobalScriptContext.Reset();
1816+
ScriptContext.GlobalScriptContext.PushPrimitive(soundevent);
1817+
ScriptContext.GlobalScriptContext.SetIdentifier(0xFF002123);
1818+
ScriptContext.GlobalScriptContext.Invoke();
1819+
ScriptContext.GlobalScriptContext.CheckErrors();
1820+
}
1821+
}
1822+
1823+
public static void SoundEventSetFloat(IntPtr soundevent, string name, float value){
1824+
lock (ScriptContext.GlobalScriptContext.Lock) {
1825+
ScriptContext.GlobalScriptContext.Reset();
1826+
ScriptContext.GlobalScriptContext.PushPrimitive(soundevent);
1827+
ScriptContext.GlobalScriptContext.PushString(name);
1828+
ScriptContext.GlobalScriptContext.PushPrimitive(value);
1829+
ScriptContext.GlobalScriptContext.SetIdentifier(0x311228E7);
1830+
ScriptContext.GlobalScriptContext.Invoke();
1831+
ScriptContext.GlobalScriptContext.CheckErrors();
1832+
}
1833+
}
1834+
1835+
public static void SoundEventSetInt(IntPtr soundevent, string name, int value){
1836+
lock (ScriptContext.GlobalScriptContext.Lock) {
1837+
ScriptContext.GlobalScriptContext.Reset();
1838+
ScriptContext.GlobalScriptContext.PushPrimitive(soundevent);
1839+
ScriptContext.GlobalScriptContext.PushString(name);
1840+
ScriptContext.GlobalScriptContext.PushPrimitive(value);
1841+
ScriptContext.GlobalScriptContext.SetIdentifier(0x470A0744);
1842+
ScriptContext.GlobalScriptContext.Invoke();
1843+
ScriptContext.GlobalScriptContext.CheckErrors();
1844+
}
1845+
}
1846+
1847+
public static void SoundEventSetVector(IntPtr soundevent, string name, IntPtr value){
1848+
lock (ScriptContext.GlobalScriptContext.Lock) {
1849+
ScriptContext.GlobalScriptContext.Reset();
1850+
ScriptContext.GlobalScriptContext.PushPrimitive(soundevent);
1851+
ScriptContext.GlobalScriptContext.PushString(name);
1852+
ScriptContext.GlobalScriptContext.PushPrimitive(value);
1853+
ScriptContext.GlobalScriptContext.SetIdentifier(0x2D238D2E);
1854+
ScriptContext.GlobalScriptContext.Invoke();
1855+
ScriptContext.GlobalScriptContext.CheckErrors();
1856+
}
1857+
}
1858+
1859+
public static int SoundEventEmit(IntPtr soundevent, int sourceentityindex, ulong recipients){
1860+
lock (ScriptContext.GlobalScriptContext.Lock) {
1861+
ScriptContext.GlobalScriptContext.Reset();
1862+
ScriptContext.GlobalScriptContext.PushPrimitive(soundevent);
1863+
ScriptContext.GlobalScriptContext.PushPrimitive(sourceentityindex);
1864+
ScriptContext.GlobalScriptContext.PushPrimitive(recipients);
1865+
ScriptContext.GlobalScriptContext.SetIdentifier(0x30D4F65F);
1866+
ScriptContext.GlobalScriptContext.Invoke();
1867+
ScriptContext.GlobalScriptContext.CheckErrors();
1868+
return ScriptContext.GlobalScriptContext.GetResultPrimitive<int>();
1869+
}
1870+
}
1871+
1872+
public static void SoundEventStop(int guid, ulong recipients){
1873+
lock (ScriptContext.GlobalScriptContext.Lock) {
1874+
ScriptContext.GlobalScriptContext.Reset();
1875+
ScriptContext.GlobalScriptContext.PushPrimitive(guid);
1876+
ScriptContext.GlobalScriptContext.PushPrimitive(recipients);
1877+
ScriptContext.GlobalScriptContext.SetIdentifier(0x30DCA4D2);
1878+
ScriptContext.GlobalScriptContext.Invoke();
1879+
ScriptContext.GlobalScriptContext.CheckErrors();
1880+
}
1881+
}
1882+
18021883
public static IntPtr CreateTimer(float interval, InputArgument callback, int flags){
18031884
lock (ScriptContext.GlobalScriptContext.Lock) {
18041885
ScriptContext.GlobalScriptContext.Reset();
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
/*
2+
* This file is part of CounterStrikeSharp.
3+
* CounterStrikeSharp is free software: you can redistribute it and/or modify
4+
* it under the terms of the GNU General Public License as published by
5+
* the Free Software Foundation, either version 3 of the License, or
6+
* (at your option) any later version.
7+
*
8+
* CounterStrikeSharp is distributed in the hope that it will be useful,
9+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
* GNU General Public License for more details.
12+
*
13+
* You should have received a copy of the GNU General Public License
14+
* along with CounterStrikeSharp. If not, see <https://www.gnu.org/licenses/>. *
15+
*/
16+
17+
using CounterStrikeSharp.API.Core;
18+
using CounterStrikeSharp.API.Modules.Utils;
19+
20+
namespace CounterStrikeSharp.API.Modules.Sounds;
21+
22+
/// <summary>
23+
/// A sound event built and sent as a network message, which lets sound operator parameters such
24+
/// as volume and pitch be chosen per emit.
25+
/// </summary>
26+
/// <example>
27+
/// <code>
28+
/// using var sound = new SoundEvent("Weapon_AK47.Single");
29+
/// sound.SourceEntityIndex = (int)player.Index;
30+
/// sound.SetParam(SoundEvent.Volume, 0.5f);
31+
/// sound.SetParam(SoundEvent.Pitch, 1.5f);
32+
/// sound.EmitToAll();
33+
/// </code>
34+
/// </example>
35+
public class SoundEvent : NativeObject, IDisposable
36+
{
37+
/// <summary>Volume the sound plays at, where 1 leaves it unchanged.</summary>
38+
public const string Volume = "public.volume";
39+
40+
/// <summary>Playback rate, where 1 leaves it unchanged, 2 raises it an octave and 0.5 lowers it one.</summary>
41+
public const string Pitch = "public.pitch";
42+
43+
/// <summary>World position the sound plays from.</summary>
44+
public const string Position = "public.position";
45+
46+
private bool _disposed;
47+
48+
/// <param name="name">Name of the sound event, for example <c>Weapon_AK47.Single</c>.</param>
49+
/// <remarks>
50+
/// The name is resolved by the client, so the server cannot tell a valid one from a typo. A name
51+
/// that does not exist plays nothing and reports no error.
52+
/// </remarks>
53+
public SoundEvent(string name) : base(NativeAPI.SoundEventCreate(name))
54+
{
55+
}
56+
57+
/// <summary>
58+
/// Entity the sound is attached to. Leave it at -1 to emit a sound with no source entity.
59+
/// </summary>
60+
public int SourceEntityIndex { get; set; } = -1;
61+
62+
/// <summary>
63+
/// Sets a sound operator parameter. Setting the same parameter twice keeps the later value.
64+
/// </summary>
65+
/// <param name="name">
66+
/// Any <c>public.*</c> parameter name. <see cref="Volume"/>, <see cref="Pitch"/> and
67+
/// <see cref="Position"/> are the ones this API was tested against; whether any other parameter
68+
/// has an effect depends on the operator stack behind the sound.
69+
/// </param>
70+
/// <param name="value">Value to send.</param>
71+
public void SetParam(string name, float value) => NativeAPI.SoundEventSetFloat(Handle, name, value);
72+
73+
/// <inheritdoc cref="SetParam(string,float)"/>
74+
public void SetParam(string name, int value) => NativeAPI.SoundEventSetInt(Handle, name, value);
75+
76+
/// <inheritdoc cref="SetParam(string,float)"/>
77+
public void SetParam(string name, Vector value) => NativeAPI.SoundEventSetVector(Handle, name, value.Handle);
78+
79+
/// <summary>
80+
/// Emits the sound to the given players.
81+
/// </summary>
82+
/// <returns>The guid the sound was started with, which <see cref="Stop"/> takes.</returns>
83+
public int Emit(RecipientFilter recipients) =>
84+
NativeAPI.SoundEventEmit(Handle, SourceEntityIndex, recipients.GetRecipientMask());
85+
86+
/// <inheritdoc cref="Emit"/>
87+
public int EmitToAll()
88+
{
89+
var recipients = new RecipientFilter();
90+
recipients.AddAllPlayers();
91+
92+
return Emit(recipients);
93+
}
94+
95+
/// <inheritdoc cref="Emit"/>
96+
public int EmitTo(CCSPlayerController player) => Emit(new RecipientFilter(player));
97+
98+
/// <summary>
99+
/// Stops a sound started by <see cref="Emit"/>, using the guid it returned.
100+
/// </summary>
101+
public static void Stop(int guid, RecipientFilter? recipients = null)
102+
{
103+
if (recipients == null)
104+
{
105+
recipients = new RecipientFilter();
106+
recipients.AddAllPlayers();
107+
}
108+
109+
NativeAPI.SoundEventStop(guid, recipients.GetRecipientMask());
110+
}
111+
112+
private void ReleaseUnmanagedResources()
113+
{
114+
if (_disposed) return;
115+
116+
_disposed = true;
117+
118+
// The finalizer runs off the game thread, where natives cannot be called.
119+
var handle = Handle;
120+
Server.NextFrame(() => NativeAPI.SoundEventRelease(handle));
121+
}
122+
123+
public void Dispose()
124+
{
125+
ReleaseUnmanagedResources();
126+
GC.SuppressFinalize(this);
127+
}
128+
129+
~SoundEvent()
130+
{
131+
ReleaseUnmanagedResources();
132+
}
133+
}

0 commit comments

Comments
 (0)