feat: Add SoundEvent API for emitting sounds with parameters - #1402
feat: Add SoundEvent API for emitting sounds with parameters#1402makadore wants to merge 2 commits into
Conversation
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.
bdbcaea to
bace526
Compare
| return Assert.Single(captured, sound => sound.Guid == (uint)guid); | ||
| } | ||
|
|
||
| private static List<Parameter> Unpack(byte[] packed) |
There was a problem hiding this comment.
I think this test file should have all of the implementation knowledge of how the parameters are packed removed, so it's just testing the API and that running the tests do not crash the server.
What might be useful is to create a test that emits a specific sound audible to all players using all of the available parameters. i.e. play the AK sound pitched down in a loop increasing in volume pitch and distance, and then a player can connect to the test server to verify the results manually (if needed) by running css_itest AudibleSoundTest (example name)
The tests asked whether specific bytes landed in specific places, which pinned them to the wire format rather than to the API. They now exercise the API and check that a server survives it. Adds an audible test that plays a sound to everyone with volume, pitch and then position swept, so the result can be checked by ear with css_itest AudibleSoundTest. Also documents what SourceEntityIndex does, which is what decides whether public.position is used at all.
|
Done, and testing it turned up something worth having in the documentation. The test file no longer knows anything about how parameters are packed. No hashes, no type tags, no unpacking, no message id, no hooks. It exercises the API and checks a server survives it: handles, guids, every value type, setting the same parameter twice, emitting to one player, stopping, disposing twice.
Checking the sweep by ear is what caught the interesting part. Position appeared to do nothing, and I nearly wrote it up as delivered but inert, until I noticed I had been attaching the sound to the listener and then trying to move it away from them.
Zero is how the game emits placed sounds, which is visible in its own messages: they carry an entity index of 0 together with real map coordinates. With that, volume, pitch and position are all audible, including direction and falloff. That is now on The follow-up I mentioned, |
EmitSoundtakes volume and pitch arguments that never reach the client. It goes throughCBaseEntity::EmitSoundFilter, and theCMsgSosStartSoundEventthat comes out of it carries nopacked_paramsat all, so there is nothing for the client to apply. Captured from a live server, this is the whole message the current path sends:The source already says as much:
m_nPitchis annotated "not working, can't fix, i think the game abandon it", andm_flVolumeis avolume_attenrather than a volume. This was raised in #919, and #126 is still open.This adds a
SoundEventthat builds the message directly, so sound operator parameters can be chosen per emit.Emitreturns the guid, whichSoundEvent.Stoptakes to end a sound early. Recipients come from aRecipientFilter, so a sound can go to one player, one team, or everyone.SetParamtakes anypublic.*parameter name, not a fixed set, so anything the sound's operator stack reads can be driven from a plugin without a change here first:Volume, pitch and position are the three this was tested against. Whether any other parameter does something depends on the stack behind the sound, which is noted in the API documentation.
Implementation
Built on the core side using net messages, which is the approach suggested in the discussion.
packed_paramsis a flat sequence of parameters, each one a 32 bit hash of the parameter name, a type tag, a 16 bit length, then the value. The types that appear in game traffic are0x02for integers,0x08for floats and0x0Afor vectors. Parsing 365 parameters captured from ordinary gameplay leaves no remainder.Two seeds are in play, and both go through the SDK's own
MurmurHash2LowerCase: sound event names hash with0x53524332, parameter names withSTRINGTOKEN_MURMURHASH_SEED. Nothing about hashing is reimplemented here.SetParamreplaces an earlier value for the same parameter rather than appending one, so a reused sound event does not grow its blob.EmitSoundis left alone, so no existing behaviour changes.Verification
Checked against the current game build on a live server:
EmitSoundpath plays 1.0 and 0.05 identicallyStopcuts a sound shortSoundEventTests.cscovers the packing, the parameter replacement, the integer type, the empty case and the hash agreement. I could not get the in-game xunit runner to report results in my environment, and the existingRayTraceandUserMessagetests behave identically there, so I ran the same assertions through an equivalent harness instead. Built and run on both platforms: gcc 13 on Linux, and MSVC 14.44 through the samevsdevcmdand CMake steps CI uses onwindows-latest. The new files compile clean on both, with no warnings of their own. The checks listed above pass on a Linux server and on a Windows one running the MSVC build, including the hash agreement with the game.Not covered
CMsgSosSetSoundEventParamswould let a playing sound be changed rather than only started and stopped, which is what a smooth fade needs. It reuses the same parameter blob and would be a small follow-up, left out here to keep this reviewable.A sound name is resolved by the client, so a name that does not exist plays nothing and reports no error. The existing
EmitSoundbehaves the same way. This is called out in the API documentation.The discussion and the research behind it are @samyycX's, including the finding that the parameters are entirely net message based.