Alibek Omarov
8 months ago
4 changed files with 457 additions and 0 deletions
@ -0,0 +1,65 @@
@@ -0,0 +1,65 @@
|
||||
# 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.wav" 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" |
||||
} |
||||
``` |
@ -0,0 +1,362 @@
@@ -0,0 +1,362 @@
|
||||
/*
|
||||
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_Print_f( void ); |
||||
static void SoundList_Free( soundlst_t *lst ) |
||||
{ |
||||
if( lst->snd ) |
||||
Mem_Free( lst->snd ); |
||||
|
||||
memset( lst, 0, sizeof( *lst )); |
||||
} |
||||
|
||||
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 ); |
||||
|
||||
Con_Printf( "%s: %s %d %d\n", __func__, soundlst_groups[group], count, idx ); |
||||
|
||||
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; |
||||
|
||||
while(( p = COM_ParseFile( p, token, sizeof( token )))) |
||||
{ |
||||
int 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; |
||||
} |
||||
|
||||
len = Q_strlen( token ) + 1; |
||||
if( slen < len ) |
||||
slen = len; |
||||
|
||||
count++; |
||||
} |
||||
|
||||
if( count == 0 ) // deactivate group
|
||||
{ |
||||
lst->type = SoundList_None; |
||||
lst->snd = NULL; |
||||
lst->min = lst->max = 0; |
||||
return true; |
||||
} |
||||
|
||||
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 = 0; |
||||
|
||||
lst->type = SoundList_Range; |
||||
*file = COM_ParseFile( *file, snd, sizeof( snd )); |
||||
|
||||
// validate format string, count all % characters
|
||||
p = snd; |
||||
i = 0; |
||||
while(( p = Q_strchr( p, '%' ))) |
||||
{ |
||||
// only decimal
|
||||
if( p[1] != 'd' && p[1] != 'i' && p[1] != 'u' ) |
||||
{ |
||||
Con_Printf( "%s: invalid range string %s, only decimal numbers are allowed", __func__, snd ); |
||||
return false; |
||||
} |
||||
|
||||
i++; |
||||
p++; |
||||
} |
||||
|
||||
// if more than one %, then it's an invalid format string
|
||||
if( i != 1 ) |
||||
{ |
||||
Con_Printf( "%s: invalid range string %s, only single specifier is allowed\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; |
||||
int i; |
||||
|
||||
while(( file = COM_ParseFile( file, token, sizeof( token )))) |
||||
{ |
||||
soundlst_t *lst = NULL; |
||||
char *p; |
||||
|
||||
for( i = 0; i < SoundList_Groups; i++ ) |
||||
{ |
||||
if( !Q_strcmp( token, soundlst_groups[i] )) |
||||
{ |
||||
lst = &soundlst[i]; |
||||
break; |
||||
} |
||||
} |
||||
|
||||
if( !lst ) |
||||
{ |
||||
Con_Printf( "%s: unexpected token %s, must be group name\n", __func__, token ); |
||||
goto cleanup; |
||||
} |
||||
|
||||
p = COM_ParseFile( file, token, sizeof( token )); |
||||
|
||||
// group is a range
|
||||
if( !Q_strcmp( token, "{" )) |
||||
{ |
||||
file = p; // advance pointer
|
||||
|
||||
if( !SoundList_ParseGroup( lst, &file )) |
||||
goto cleanup; |
||||
|
||||
file = COM_ParseFile( file, token, sizeof( token )); |
||||
if( Q_strcmp( token, "}" )) |
||||
{ |
||||
Con_Printf( "%s: unexpected token %s, must be }\n", __func__, token ); |
||||
goto cleanup; |
||||
} |
||||
} |
||||
else |
||||
{ |
||||
// ranges are more simple, but need to rewind pointer for them
|
||||
if( !SoundList_ParseRange( lst, &file )) |
||||
goto cleanup; |
||||
} |
||||
} |
||||
|
||||
if( host_developer.value >= 2 ) |
||||
SoundList_Print_f(); |
||||
|
||||
return true; |
||||
|
||||
cleanup: |
||||
SoundList_Shutdown(); |
||||
return false; |
||||
} |
||||
|
||||
static void SoundList_Print_f( void ) |
||||
{ |
||||
int i; |
||||
|
||||
for( i = 0; i < SoundList_Groups; i++ ) |
||||
{ |
||||
soundlst_t *lst = &soundlst[i]; |
||||
|
||||
switch( lst->type ) |
||||
{ |
||||
case SoundList_Range: |
||||
Con_Reportf( "%-16s\t" S_CYAN "Range" S_DEFAULT " %s [%d; %d]\n", |
||||
soundlst_groups[i], lst->snd, lst->min, lst->max ); |
||||
break; |
||||
case SoundList_List: |
||||
{ |
||||
int j; |
||||
|
||||
Con_Reportf( "%-16s\t" S_MAGENTA "List" S_DEFAULT " [", soundlst_groups[i] ); |
||||
for( j = 0; j < lst->max; j++ ) |
||||
Con_Reportf( "%s%s", &lst->snd[j * lst->min], j + 1 == lst->max ? "" : ", " ); |
||||
Con_Reportf( "]\n" ); |
||||
break; |
||||
} |
||||
default: |
||||
Con_Reportf( "%-16s\t" S_RED "inactive" S_DEFAULT "\n", soundlst_groups[i] ); |
||||
break; |
||||
} |
||||
} |
||||
} |
||||
|
||||
// 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.wav\" 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"; |
||||
|
||||
static void SoundList_Reload_f( void ) |
||||
{ |
||||
qboolean load_internal = false; |
||||
char *pfile = FS_LoadFile( "scripts/sounds.lst", NULL, false ); |
||||
|
||||
if( pfile ) |
||||
{ |
||||
if( !SoundList_Parse( pfile )) |
||||
{ |
||||
Con_Printf( S_ERROR "can't parse sounds.lst file, loading internal...\n" ); |
||||
load_internal = true; |
||||
} |
||||
|
||||
Mem_Free( pfile ); |
||||
} |
||||
else load_internal = true; |
||||
|
||||
if( load_internal ) |
||||
SoundList_Parse( (char *)default_sounds_lst ); |
||||
} |
||||
|
||||
void SoundList_Init( void ) |
||||
{ |
||||
Cmd_AddRestrictedCommand( "host_soundlist_print", SoundList_Print_f, "print current sound list" ); |
||||
Cmd_AddRestrictedCommand( "host_soundlist_reload", SoundList_Reload_f, "reload sound list" ); |
||||
|
||||
SoundList_Reload_f (); |
||||
} |
Loading…
Reference in new issue