~a1batross/xash3d-fwgs

This adds an ability for mods developers to override hardcoded sounds
with custom ones.

Alibek Omarov (3):
  engine: add basic sounds.lst implementation
  engine: client: use soundlist to acquire random sounds for temp
    entities
  engine: server: use soundlist to acquire random sounds for physics

 Documentation/extensions/sounds.lst.md |  66 ++++++
 engine/client/cl_tent.c                | 124 ++++++-----
 engine/common/common.h                 |  28 +++
 engine/common/host.c                   |   2 +
 engine/common/sounds.c                 | 272 +++++++++++++++++++++++++
 engine/server/sv_move.c                |  36 +---
 engine/server/sv_phys.c                |   8 +-
 7 files changed, 438 insertions(+), 98 deletions(-)
 create mode 100644 Documentation/extensions/sounds.lst.md
 create mode 100644 engine/common/sounds.c

-- 
2.34.1
Export patchset (mbox)
How do I use this?

Copy & paste the following snippet into your terminal to import this patchset into git:

curl -s https://lists.sr.ht/~a1batross/xash3d-fwgs/patches/49038/mbox | git am -3
Learn more about email & git

[PATCH 1/3] engine: add basic sounds.lst implementation Export this patch

---
 Documentation/extensions/sounds.lst.md |  66 ++++++
 engine/common/common.h                 |  28 +++
 engine/common/host.c                   |   2 +
 engine/common/sounds.c                 | 272 +++++++++++++++++++++++++
 4 files changed, 368 insertions(+)
 create mode 100644 Documentation/extensions/sounds.lst.md
 create mode 100644 engine/common/sounds.c

diff --git a/Documentation/extensions/sounds.lst.md b/Documentation/extensions/sounds.lst.md
new file mode 100644
index 00000000..77104d44
--- /dev/null
+++ b/Documentation/extensions/sounds.lst.md
@@ -0,0 +1,66 @@
# sounds.lst.md

Using sounds.lst located in scripts folder, modder can override some of the hardcoded sounds in temp entities and server physics.

File format:
```
<group name>
{
	<path1>
	<path2>
	<path3>
}

<group2 name> <path with %d> <min number> <max number>
```

* Sounds can use any supported sound format (WAV or MP3).
* The path must be relative to the sounds/ folder in the game or base directory root, addon folder, or archive root.
* Groups can be empty or omitted from the file to load no sound.
* Groups can either list a set of files or specify a format string and a range.
* Anything after // will be considered a comment and ignored.
* Behavior is undefined if the group was listed multiple times.

Currently supported groups are:
|Group name|Usage|
|----------|-----|
|`BouncePlayerShell`|Used for BOUNCE_SHELL tempentity hitsound|
|`BounceWeaponShell`|Used for BOUCNE_SHOTSHELL tempentity hitsound|
|`BounceConcrete`|Used for BOUNCE_CONCRETE tempentity hitsound|
|`BounceGlass`|Used for BOUCNE_GLASS|
|`BounceMetal`|Used for BOUNCE_METAL|
|`BounceFlesh`|Used for BOUNCE_FLESH|
|`BounceWood`|Used for BOUNCE_WOOD|
|`Ricochet`|Used for BOUNCE_SHRAP and ricochet tempentities|
|`Explode`|Used for tempentity explosions|
|`EntityWaterEnter`|Used for entity entering water|
|`EntityWaterExit`|Used for entity exiting water|
|`PlayerWaterEnter`|Used for player entering water|
|`PlayerWaterExit`|Used for player exiting water|

## Example

This example is based on defaults sounds used in Half-Life:

```
BouncePlayerShell "player/pl_shell%d.wav" 1 3
BounceWeaponShell "weapons/sshell%d.wav" 1 3
BounceConcrete "debris/concrete%d.wav" 1 3
BounceGlass "debris/glass%d.wav" 1 4
BounceMetal "debris/metal%d.wav" 1 6
BounceFlesh "debris/flesh%d.wav" 1 7
BounceWood "debris/wood%d.wav" 1 4
Ricochet "weapons/ric%d.wav" 1 5
Explode "weapons/explode%d" 3 5
EntityWaterEnter "player/pl_wade%d.wav" 1 4
EntityWaterExit "player/pl_wade%d.wav" 1 4
PlayerWaterEnter
{
	"player/pl_wade1.wav"
}

PlayerWaterExit
{
	"player/pl_wade2.wav"
}
```
diff --git a/engine/common/common.h b/engine/common/common.h
index 26c02c62..31f1bc8c 100644
--- a/engine/common/common.h
+++ b/engine/common/common.h
@@ -815,6 +815,34 @@ void NET_MasterClear( void );
void NET_MasterShutdown( void );
qboolean NET_GetMaster( netadr_t from, uint *challenge, double *last_heartbeat );

//
// sounds.c
//
typedef enum soundlst_group_e
{
	BouncePlayerShell = 0,
	BounceWeaponShell,
	BounceConcrete,
	BounceGlass,
	BounceMetal,
	BounceFlesh,
	BounceWood,
	Ricochet,
	Explode,
	PlayerWaterEnter,
	PlayerWaterExit,
	EntityWaterEnter,
	EntityWaterExit,

	SoundList_Groups // must be last
} soundlst_group_t;

int SoundList_Count( soundlst_group_t group );
const char *SoundList_GetRandom( soundlst_group_t group );
const char *SoundList_Get( soundlst_group_t group, int idx );
void SoundList_Init( void );
void SoundList_Shutdown( void );

#ifdef REF_DLL
#error "common.h in ref_dll"
#endif
diff --git a/engine/common/host.c b/engine/common/host.c
index f990f1cc..65ae4267 100644
--- a/engine/common/host.c
+++ b/engine/common/host.c
@@ -1310,6 +1310,7 @@ int EXPORT Host_Main( int argc, char **argv, const char *progname, int bChangeGa

	HTTP_Init();
	ID_Init();
	SoundList_Init();

	if( Host_IsDedicated() )
	{
@@ -1412,6 +1413,7 @@ void EXPORT Host_Shutdown( void )
	SV_ShutdownFilter();
	CL_Shutdown();

	SoundList_Shutdown();
	Mod_Shutdown();
	NET_Shutdown();
	HTTP_Shutdown();
diff --git a/engine/common/sounds.c b/engine/common/sounds.c
new file mode 100644
index 00000000..b1e3ed27
--- /dev/null
+++ b/engine/common/sounds.c
@@ -0,0 +1,272 @@
/*
sounds.c - sounds.lst parser
Copyright (C) 2024 Alibek Omarov

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.
*/
#include "common.h"

enum soundlst_type_e
{
	SoundList_None,
	SoundList_Range,
	SoundList_List
};

static const char *soundlst_groups[SoundList_Groups] =
{
	"BouncePlayerShell",
	"BounceWeaponShell",
	"BounceConcrete",
	"BounceGlass",
	"BounceMetal",
	"BounceFlesh",
	"BounceWood",
	"Ricochet",
	"Explode",
	"PlayerWaterEnter",
	"PlayerWaterExit",
	"EntityWaterEnter",
	"EntityWaterExit",
};

typedef struct soundlst_s
{
	enum soundlst_type_e type;
	char *snd;
	int min; // the string length if type is group
	int max; // the string count if type is group
} soundlst_t;

soundlst_t soundlst[SoundList_Groups];

static void SoundList_Free( soundlst_t *lst )
{
	if( lst->snd )
	{
		Mem_Free( lst->snd );
		lst->snd = NULL;
	}

	lst->min = lst->max = 0;
	lst->type = SoundList_None;
}

void SoundList_Shutdown( void )
{
	int i;

	for( i = 0; i < SoundList_Groups; i++ )
		SoundList_Free( &soundlst[i] );
}

int SoundList_Count( enum soundlst_group_e group )
{
	soundlst_t *lst = &soundlst[group];

	switch( lst->type )
	{
	case SoundList_Range:
		return lst->max - lst->min + 1;
	case SoundList_List:
		return lst->max;
	}

	return 0;
}

const char *SoundList_Get( enum soundlst_group_e group, int i )
{
	static string temp;
	soundlst_t *lst = &soundlst[group];

	if( i < 0 || i >= SoundList_Count( group ))
		return NULL;

	switch( lst->type )
	{
	case SoundList_Range:
		Q_snprintf( temp, sizeof( temp ), lst->snd, lst->min + i );
		return temp;
	case SoundList_List:
		return &lst->snd[i * lst->min];
	}

	return NULL;
}

const char *SoundList_GetRandom( enum soundlst_group_e group )
{
	int count = SoundList_Count( group );
	int idx = COM_RandomLong( 0, count - 1 );

	return SoundList_Get( group, idx );
}

static qboolean SoundList_ParseGroup( soundlst_t *lst, char **file )
{
	string token;
	int count = 0, slen = 0, i;
	char *p;

	p = *file;

	for( ; p && *p; count++, p = COM_ParseFile( p, token, sizeof( token )))
	{
		int len = Q_strlen( token ) + 1;
		if( slen < len )
			slen = len;

		if( !Q_strcmp( token, "}" ))
			break;

		if( !Q_strcmp( token, "{" ))
		{
			Con_Printf( "%s: expected '}' but got '{' during group list parse\n", __func__ );
			return false;
		}
		else if( !COM_CheckStringEmpty( token ))
		{
			Con_Printf( "%s: expected '}' but got EOF during group list parse\n", __func__ );
			return false;
		}
	}

	lst->type = SoundList_List;
	lst->min = slen;
	lst->max = count;
	lst->snd = Mem_Malloc( host.mempool, count * slen ); // allocate single buffer for the whole group

	for( i = 0; i < count; i++ )
	{
		*file = COM_ParseFile( *file, token, sizeof( token ));

		Q_strncpy( &lst->snd[i * slen], token, slen );
	}

	return true;
}

static qboolean SoundList_ParseRange( soundlst_t *lst, char **file )
{
	string token, snd;
	char *p;
	int i;

	lst->type = SoundList_Range;
	*file = COM_ParseFile( *file, snd, sizeof( snd ));

	// validate format string
	for( i = 0, p = snd; p; i++, p = Q_strchr( p, '%' ));
	if( i != 1 )
	{
		Con_Printf( "%s: invalid range string %s\n", __func__, snd );
		return false;
	}

	*file = COM_ParseFile( *file, token, sizeof( token ));
	if( !Q_isdigit( token ))
	{
		Con_Printf( "%s: %s must be a digit\n", __func__, token );
		return false;
	}
	lst->min = Q_atoi( token );

	*file = COM_ParseFile( *file, token, sizeof( token ));
	if( !Q_isdigit( token ))
	{
		Con_Printf( "%s: %s must be a digit\n", __func__, token );
		return false;
	}
	lst->max = Q_atoi( token );
	lst->snd = copystring( snd );

	return true;
}

static qboolean SoundList_Parse( char *file )
{
	string token;
	string name;
	int i;

	while( file && *file )
	{
		const char *group;
		soundlst_t *lst = NULL;
		file = COM_ParseFile( file, token, sizeof( token ));

		for( i = 0; i < SoundList_Groups; i++ )
		{
			if( !Q_strcmp( token, soundlst_groups[i] ))
				lst = &soundlst[i];
		}

		if( !lst )
		{
			Con_Printf( "%s: unexpected token %s, must be group name\n", __func__, token );
			goto cleanup;
		}

		file = COM_ParseFile( file, token, sizeof( token ));

		// group is a range
		if( Q_strcmp( token, "{" ))
		{
			if( !SoundList_ParseRange( lst, &file ))
				goto cleanup;
		}
		else
		{
			if( !SoundList_ParseGroup( lst, &file ))
				goto cleanup;
		}
	}

	return true;

cleanup:
	SoundList_Shutdown();
	return false;
}

// I wish we had #embed already
static const char default_sounds_lst[] =
"BouncePlayerShell \"player/pl_shell%d.wav\" 1 3\n"
"BounceWeaponShell \"weapons/sshell%d.wav\" 1 3\n"
"BounceConcrete \"debris/concrete%d.wav\" 1 3\n"
"BounceGlass \"debris/glass%d.wav\" 1 4\n"
"BounceMetal \"debris/metal%d.wav\" 1 6\n"
"BounceFlesh \"debris/flesh%d.wav\" 1 7\n"
"BounceWood \"debris/wood%d.wav\" 1 4\n"
"Ricochet \"weapons/ric%d.wav\" 1 5\n"
"Explode \"weapons/explode%d\" 3 5\n"
"EntityWaterEnter \"player/pl_wade%d.wav\" 1 4\n"
"EntityWaterExit \"player/pl_wade%d.wav\" 1 4\n"
"PlayerWaterEnter\n"
"{\n"
"	\"player/pl_wade1.wav\"\n"
"}\n"
"\n"
"PlayerWaterExit\n"
"{\n"
"	\"player/pl_wade2.wav\"\n"
"}\n";

void SoundList_Init( void )
{
	char *pfile;

	pfile = FS_LoadFile( "scripts/sounds.lst", NULL, false );

	if( !pfile || !SoundList_Parse( pfile ))
		SoundList_Parse( (char *)default_sounds_lst );
}
-- 
2.34.1

[PATCH 2/3] engine: client: use soundlist to acquire random sounds for temp entities Export this patch

---
 engine/client/cl_tent.c | 124 +++++++++++++++++++---------------------
 1 file changed, 58 insertions(+), 66 deletions(-)

diff --git a/engine/client/cl_tent.c b/engine/client/cl_tent.c
index 7023a7f9..86235c3d 100644
--- a/engine/client/cl_tent.c
+++ b/engine/client/cl_tent.c
@@ -57,36 +57,6 @@ const char *cl_default_sprites[] =
	"sprites/shellchrome.spr",
};

const char *cl_player_shell_sounds[] =
{
	"player/pl_shell1.wav",
	"player/pl_shell2.wav",
	"player/pl_shell3.wav",
};

const char *cl_weapon_shell_sounds[] =
{
	"weapons/sshell1.wav",
	"weapons/sshell2.wav",
	"weapons/sshell3.wav",
};

const char *cl_ricochet_sounds[] =
{
	"weapons/ric1.wav",
	"weapons/ric2.wav",
	"weapons/ric3.wav",
	"weapons/ric4.wav",
	"weapons/ric5.wav",
};

const char *cl_explode_sounds[] =
{
	"weapons/explode3.wav",
	"weapons/explode4.wav",
	"weapons/explode5.wav",
};

static void CL_PlayerDecal( int playerIndex, int textureIndex, int entityIndex, float *pos );

/*
@@ -148,6 +118,7 @@ client resources not precached by server
*/
void CL_AddClientResources( void )
{
	const char *snd;
	char	filepath[MAX_QPATH];
	int	i;

@@ -163,37 +134,37 @@ void CL_AddClientResources( void )
	}

	// then check sounds
	for( i = 0; i < ARRAYSIZE( cl_player_shell_sounds ); i++ )
	for( i = 0; ( snd = SoundList_Get( BouncePlayerShell, i )); i++ )
	{
		Q_snprintf( filepath, sizeof( filepath ), DEFAULT_SOUNDPATH "%s", cl_player_shell_sounds[i] );
		Q_snprintf( filepath, sizeof( filepath ), DEFAULT_SOUNDPATH "%s", snd );

		if( !FS_FileExists( filepath, false ))
			CL_AddClientResource( cl_player_shell_sounds[i], t_sound );
			CL_AddClientResource( snd, t_sound );
	}

	for( i = 0; i < ARRAYSIZE( cl_weapon_shell_sounds ); i++ )
	for( i = 0; ( snd = SoundList_Get( BounceWeaponShell, i )); i++ )
	{
		Q_snprintf( filepath, sizeof( filepath ), DEFAULT_SOUNDPATH "%s", cl_weapon_shell_sounds[i] );
		Q_snprintf( filepath, sizeof( filepath ), DEFAULT_SOUNDPATH "%s", snd );

		if( !FS_FileExists( filepath, false ))
			CL_AddClientResource( cl_weapon_shell_sounds[i], t_sound );
			CL_AddClientResource( snd, t_sound );
	}

	for( i = 0; i < ARRAYSIZE( cl_explode_sounds ); i++ )
	for( i = 0; ( snd = SoundList_Get( Explode, i )); i++ )
	{
		Q_snprintf( filepath, sizeof( filepath ), DEFAULT_SOUNDPATH "%s", cl_explode_sounds[i] );
		Q_snprintf( filepath, sizeof( filepath ), DEFAULT_SOUNDPATH "%s", snd );

		if( !FS_FileExists( filepath, false ))
			CL_AddClientResource( cl_explode_sounds[i], t_sound );
			CL_AddClientResource( snd, t_sound );
	}

#if 0	// ric sounds was precached by server-side
	for( i = 0; i < ARRAYSIZE( cl_ricochet_sounds ); i++ )
	for( i = 0; ( snd = SoundList_Get( Ricochet, i )); i++ )
	{
		Q_snprintf( filepath, sizeof( filepath ), DEFAULT_SOUNDPATH "%s", cl_ricochet_sounds[i] );
		Q_snprintf( filepath, sizeof( filepath ), DEFAULT_SOUNDPATH "%s", snd );

		if( !FS_FileExists( filepath, false ))
			CL_AddClientResource( cl_ricochet_sounds[i], t_sound );
			CL_AddClientResource( snd, t_sound );
	}
#endif
}
@@ -301,7 +272,7 @@ play collide sound
static void CL_TempEntPlaySound( TEMPENTITY *pTemp, float damp )
{
	float	fvol;
	char	soundname[32];
	const char	*soundname = NULL;
	qboolean	isshellcasing = false;
	int	zvel;

@@ -312,36 +283,39 @@ static void CL_TempEntPlaySound( TEMPENTITY *pTemp, float damp )
	switch( pTemp->hitSound )
	{
	case BOUNCE_GLASS:
		Q_snprintf( soundname, sizeof( soundname ), "debris/glass%i.wav", COM_RandomLong( 1, 4 ));
		soundname = SoundList_GetRandom( BounceGlass );
		break;
	case BOUNCE_METAL:
		Q_snprintf( soundname, sizeof( soundname ), "debris/metal%i.wav", COM_RandomLong( 1, 6 ));
		soundname = SoundList_GetRandom( BounceMetal );
		break;
	case BOUNCE_FLESH:
		Q_snprintf( soundname, sizeof( soundname ), "debris/flesh%i.wav", COM_RandomLong( 1, 7 ));
		soundname = SoundList_GetRandom( BounceFlesh );
		break;
	case BOUNCE_WOOD:
		Q_snprintf( soundname, sizeof( soundname ), "debris/wood%i.wav", COM_RandomLong( 1, 4 ));
		soundname = SoundList_GetRandom( BounceWood );
		break;
	case BOUNCE_SHRAP:
		Q_strncpy( soundname, cl_ricochet_sounds[COM_RandomLong( 0, 4 )], sizeof( soundname ) );
		soundname = SoundList_GetRandom( Ricochet );
		break;
	case BOUNCE_SHOTSHELL:
		Q_strncpy( soundname, cl_weapon_shell_sounds[COM_RandomLong( 0, 2 )], sizeof( soundname ) );
		soundname = SoundList_GetRandom( BounceWeaponShell );
		isshellcasing = true; // shell casings have different playback parameters
		fvol = 0.5f;
		break;
	case BOUNCE_SHELL:
		Q_strncpy( soundname, cl_player_shell_sounds[COM_RandomLong( 0, 2 )], sizeof( soundname ) );
		soundname = SoundList_GetRandom( BouncePlayerShell );
		isshellcasing = true; // shell casings have different playback parameters
		break;
	case BOUNCE_CONCRETE:
		Q_snprintf( soundname, sizeof( soundname ), "debris/concrete%i.wav", COM_RandomLong( 1, 3 ));
		soundname = SoundList_GetRandom( BounceConcrete );
		break;
	default:	// null sound
		return;
	}

	if( !soundname )
		return;

	zvel = abs( pTemp->entity.baseline.origin[2] );

	// only play one out of every n
@@ -1491,7 +1465,7 @@ void GAME_EXPORT R_FunnelSprite( const vec3_t org, int modelIndex, int reverse )
===============
R_SparkEffect

Create a streaks + richochet sprite
Create a streaks + ricochet sprite
===============
*/
void GAME_EXPORT R_SparkEffect( const vec3_t pos, int count, int velocityMin, int velocityMax )
@@ -1507,18 +1481,25 @@ R_RicochetSound
Make a random ricochet sound
==============
*/
static void R_RicochetSound_( const vec3_t pos, int sound )
static void R_RicochetSoundByName( const vec3_t pos, const char *name )
{
	sound_t	handle;

	handle = S_RegisterSound( cl_ricochet_sounds[sound] );

	sound_t handle;
	handle = S_RegisterSound( name );
	S_StartSound( pos, 0, CHAN_AUTO, handle, VOL_NORM, 1.0, 100, 0 );
}

static void R_RicochetSoundByIndex( const vec3_t pos, int idx )
{
	const char *name = SoundList_Get( Ricochet, idx );
	if( name )
		R_RicochetSoundByName( pos, name );
}

void GAME_EXPORT R_RicochetSound( const vec3_t pos )
{
	R_RicochetSound_( pos, COM_RandomLong( 0, 4 ));
	const char *name = SoundList_GetRandom( Ricochet );
	if( name )
		R_RicochetSoundByName( pos, name );
}

/*
@@ -1665,8 +1646,12 @@ void GAME_EXPORT R_Explosion( vec3_t pos, int model, float scale, float framerat

	if( !FBitSet( flags, TE_EXPLFLAG_NOSOUND ))
	{
		hSound = S_RegisterSound( cl_explode_sounds[COM_RandomLong( 0, 2 )] );
		S_StartSound( pos, 0, CHAN_STATIC, hSound, VOL_NORM, 0.3f, PITCH_NORM, 0 );
		const char *name = SoundList_GetRandom( Explode );
		if( name )
		{
			hSound = S_RegisterSound( name );
			S_StartSound( pos, 0, CHAN_STATIC, hSound, VOL_NORM, 0.3f, PITCH_NORM, 0 );
		}
	}
}

@@ -1909,6 +1894,7 @@ void CL_ParseTempEntity( sizebuf_t *msg )
	cl_entity_t	*pEnt;
	dlight_t		*dl;
	sound_t	hSound;
	const char *name;

	if( cls.legacymode )
		iSize = MSG_ReadByte( msg );
@@ -1968,8 +1954,11 @@ void CL_ParseTempEntity( sizebuf_t *msg )
		pos[2] = MSG_ReadCoord( &buf );
		R_BlobExplosion( pos );

		hSound = S_RegisterSound( cl_explode_sounds[0] );
		S_StartSound( pos, -1, CHAN_AUTO, hSound, VOL_NORM, 1.0f, PITCH_NORM, 0 );
		if(( name = SoundList_Get( Explode, 0 )))
		{
			hSound = S_RegisterSound( name );
			S_StartSound( pos, -1, CHAN_AUTO, hSound, VOL_NORM, 1.0f, PITCH_NORM, 0 );
		}
		break;
	case TE_SMOKE:
		pos[0] = MSG_ReadCoord( &buf );
@@ -2022,8 +2011,11 @@ void CL_ParseTempEntity( sizebuf_t *msg )
		dl->die = cl.time + 0.5;
		dl->decay = 300;

		hSound = S_RegisterSound( cl_explode_sounds[0] );
		S_StartSound( pos, -1, CHAN_AUTO, hSound, VOL_NORM, 0.6f, PITCH_NORM, 0 );
		if(( name = SoundList_Get( Explode, 0 )))
		{
			hSound = S_RegisterSound( name );
			S_StartSound( pos, -1, CHAN_AUTO, hSound, VOL_NORM, 1.0f, PITCH_NORM, 0 );
		}
		break;
	case TE_BSPDECAL:
	case TE_DECAL:
@@ -2251,8 +2243,8 @@ void CL_ParseTempEntity( sizebuf_t *msg )
		CL_DecalShoot( CL_DecalIndex( decalIndex ), entityIndex, 0, pos, 0 );
		R_BulletImpactParticles( pos );
		flags = COM_RandomLong( 0, 0x7fff );
		if( flags < 0x3fff )
			R_RicochetSound_( pos, flags % 5 );
		if( flags < 0x3fff && ( count = SoundList_Count( Ricochet )))
			R_RicochetSoundByIndex( pos, flags % count );
		break;
	case TE_SPRAY:
	case TE_SPRITE_SPRAY:
-- 
2.34.1

[PATCH 3/3] engine: server: use soundlist to acquire random sounds for physics Export this patch

---
 engine/server/sv_move.c | 36 ++++++------------------------------
 engine/server/sv_phys.c |  8 ++++++--
 2 files changed, 12 insertions(+), 32 deletions(-)

diff --git a/engine/server/sv_move.c b/engine/server/sv_move.c
index e62bfd46..25cd894a 100644
--- a/engine/server/sv_move.c
+++ b/engine/server/sv_move.c
@@ -150,21 +150,9 @@ void SV_WaterMove( edict_t *ent )
		if( flags & FL_INWATER )
		{
			// leave the water.
			switch( COM_RandomLong( 0, 3 ))
			{
			case 0:
				SV_StartSound( ent, CHAN_BODY, "player/pl_wade1.wav", 1.0f, ATTN_NORM, 0, 100 );
				break;
			case 1:
				SV_StartSound( ent, CHAN_BODY, "player/pl_wade2.wav", 1.0f, ATTN_NORM, 0, 100 );
				break;
			case 2:
				SV_StartSound( ent, CHAN_BODY, "player/pl_wade3.wav", 1.0f, ATTN_NORM, 0, 100 );
				break;
			case 3:
				SV_StartSound( ent, CHAN_BODY, "player/pl_wade4.wav", 1.0f, ATTN_NORM, 0, 100 );
				break;
			}
			const char *snd = SoundList_GetRandom( EntityWaterExit );
			if( snd )
				SV_StartSound( ent, CHAN_BODY, snd, 1.0f, ATTN_NORM, 0, 100 );

			ent->v.flags = flags & ~FL_INWATER;
		}
@@ -197,21 +185,9 @@ void SV_WaterMove( edict_t *ent )
		if( watertype == CONTENTS_WATER )
		{
			// entering the water
			switch( COM_RandomLong( 0, 3 ))
			{
			case 0:
				SV_StartSound( ent, CHAN_BODY, "player/pl_wade1.wav", 1.0f, ATTN_NORM, 0, 100 );
				break;
			case 1:
				SV_StartSound( ent, CHAN_BODY, "player/pl_wade2.wav", 1.0f, ATTN_NORM, 0, 100 );
				break;
			case 2:
				SV_StartSound( ent, CHAN_BODY, "player/pl_wade3.wav", 1.0f, ATTN_NORM, 0, 100 );
				break;
			case 3:
				SV_StartSound( ent, CHAN_BODY, "player/pl_wade4.wav", 1.0f, ATTN_NORM, 0, 100 );
				break;
			}
			const char *snd = SoundList_GetRandom( EntityWaterEnter );
			if( snd )
				SV_StartSound( ent, CHAN_BODY, snd, 1.0f, ATTN_NORM, 0, 100 );
		}

		ent->v.flags = flags | FL_INWATER;
diff --git a/engine/server/sv_phys.c b/engine/server/sv_phys.c
index 29fd6a9f..f65954fd 100644
--- a/engine/server/sv_phys.c
+++ b/engine/server/sv_phys.c
@@ -1384,7 +1384,9 @@ static void SV_CheckWaterTransition( edict_t *ent )
		if( ent->v.watertype == CONTENTS_EMPTY )
		{
			// just crossed into water
			SV_StartSound( ent, CHAN_AUTO, "player/pl_wade1.wav", 1.0f, ATTN_NORM, 0, 100 );
			const char *snd = SoundList_GetRandom( PlayerWaterEnter );
			if( snd )
				SV_StartSound( ent, CHAN_AUTO, snd, 1.0f, ATTN_NORM, 0, 100 );
			ent->v.velocity[2] *= 0.5f;
		}

@@ -1418,7 +1420,9 @@ static void SV_CheckWaterTransition( edict_t *ent )
		if( ent->v.watertype != CONTENTS_EMPTY )
		{
			// just crossed into water
			SV_StartSound( ent, CHAN_AUTO, "player/pl_wade2.wav", 1.0f, ATTN_NORM, 0, 100 );
			const char *snd = SoundList_GetRandom( PlayerWaterExit );
			if( snd )
				SV_StartSound( ent, CHAN_AUTO, snd, 1.0f, ATTN_NORM, 0, 100 );
		}
		ent->v.watertype = CONTENTS_EMPTY;
		ent->v.waterlevel = 0;
-- 
2.34.1