/* 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 (); }