2022-09-18 15:37:21 +00:00
/*
= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
Copyright ( C ) 2000 - 2013 , Raven Software , Inc .
Copyright ( C ) 2001 - 2013 , Activision , Inc .
Copyright ( C ) 2013 - 2015 , OpenJK contributors
This file is part of the OpenJK source code .
OpenJK is free software ; you can redistribute it and / or modify it
under the terms of the GNU General Public License version 2 as
published by the Free Software Foundation .
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 .
You should have received a copy of the GNU General Public License
along with this program ; if not , see < http : //www.gnu.org/licenses/>.
= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
*/
// Filename:- snd_music.cpp
//
// Stuff to parse in special x-fade music format and handle blending etc
//Anything above this #include will be ignored by the compiler
# include "../server/exe_headers.h"
# include "../qcommon/sstring.h"
# include <algorithm>
# include <string>
# include <sstream>
# include "snd_local.h"
# include "cl_mp3.h"
//
# include "snd_music.h"
# include "../game/genericparser2.h"
extern qboolean S_FileExists ( const char * psFilename ) ;
# define sKEY_MUSICFILES CSTRING_VIEW( "musicfiles" )
# define sKEY_ENTRY CSTRING_VIEW( "entry" )
# define sKEY_EXIT CSTRING_VIEW( "exit" )
# define sKEY_MARKER CSTRING_VIEW( "marker" )
# define sKEY_TIME CSTRING_VIEW( "time" )
# define sKEY_NEXTFILE CSTRING_VIEW( "nextfile" )
# define sKEY_NEXTMARK CSTRING_VIEW( "nextmark" )
# define sKEY_LEVELMUSIC CSTRING_VIEW( "levelmusic" )
# define sKEY_EXPLORE CSTRING_VIEW( "explore" )
# define sKEY_ACTION CSTRING_VIEW( "action" )
# define sKEY_BOSS CSTRING_VIEW( "boss" )
# define sKEY_DEATH CSTRING_VIEW( "death" )
# define sKEY_USES CSTRING_VIEW( "uses" )
# define sKEY_USEBOSS CSTRING_VIEW( "useboss" )
# define sKEY_PLACEHOLDER "placeholder" // ignore these
# define sFILENAME_DMS "ext_data / dms.dat"
typedef struct
{
sstring_t sNextFile ;
sstring_t sNextMark ; // blank if used for an explore piece, name of marker point to enter new file at
} MusicExitPoint_t ;
struct MusicExitTime_t // need to declare this way for operator < below
{
float fTime ;
int iExitPoint ;
// I'm defining this '<' operator so STL's sort algorithm will work
//
bool operator < ( const MusicExitTime_t & X ) const { return ( fTime < X . fTime ) ; }
} ;
// it's possible for all 3 of these to be empty if it's boss or death music
//
typedef std : : vector < MusicExitPoint_t > MusicExitPoints_t ;
typedef std : : vector < MusicExitTime_t > MusicExitTimes_t ;
typedef std : : map < sstring_t , float > MusicEntryTimes_t ; // key eg "marker1"
typedef struct
{
sstring_t sFileNameBase ;
MusicEntryTimes_t MusicEntryTimes ;
MusicExitPoints_t MusicExitPoints ;
MusicExitTimes_t MusicExitTimes ;
} MusicFile_t ;
typedef std : : map < sstring_t , MusicFile_t > MusicData_t ; // string is "explore", "action", "boss" etc
MusicData_t * MusicData = NULL ;
// there are now 2 of these, because of the new "uses" keyword...
//
sstring_t gsLevelNameForLoad ; // eg "kejim_base", formed from literal BSP name, but also used as dir name for music paths
sstring_t gsLevelNameForCompare ; // eg "kejim_base", formed from literal BSP name, but also used as dir name for music paths
sstring_t gsLevelNameForBossLoad ; // eg "kejim_base', special case for enabling boss music to come from a different dir - sigh....
void Music_Free ( void )
{
if ( MusicData )
{
MusicData - > clear ( ) ;
}
MusicData = NULL ;
}
namespace detail
{
static void build_string ( std : : ostream & stream )
{
}
template < typename T , typename . . . Tail >
static void build_string ( std : : ostream & stream , const T & head , Tail . . . tail )
{
stream < < head ;
build_string ( stream , tail . . . ) ;
}
}
template < typename . . . Tail >
static std : : string build_string ( Tail . . . tail )
{
std : : ostringstream os ;
detail : : build_string ( os , tail . . . ) ;
return os . str ( ) ;
}
// some sort of error in the music data...
// only use during parse, not run-time use, and bear in mid that data is zapped after error message, so exit any loops immediately
//
static void Music_Parse_Error ( gsl : : czstring filename , const std : : string & error )
{
std : : string message = build_string (
S_COLOR_RED " Error parsing music data (in \" " , filename , " \" ): \n " ,
error , " \n "
) ;
Com_Printf ( " %s " , message . c_str ( ) ) ;
MusicData - > clear ( ) ;
}
// something to just mention if interested...
//
static void Music_Parse_Warning ( const std : : string & error )
{
extern cvar_t * s_debugdynamic ;
if ( s_debugdynamic & & s_debugdynamic - > integer )
{
Com_Printf ( S_COLOR_YELLOW " %s " , error . c_str ( ) ) ;
}
}
// the 2nd param here is pretty kludgy (sigh), and only used for testing for the "boss" type.
// Unfortunately two of the places that calls this doesn't have much other access to the state other than
// a string, not an enum, so for those cases they only pass in BOSS or EXPLORE, so don't rely on it totally.
//
static const char * Music_BuildFileName ( const char * psFileNameBase , MusicState_e eMusicState )
{
static sstring_t sFileName ;
//HACK!
if ( eMusicState = = eBGRNDTRACK_DEATH )
{
return " music/death_music.mp3 " ;
}
const char * psDirName = ( eMusicState = = eBGRNDTRACK_BOSS ) ? gsLevelNameForBossLoad . c_str ( ) : gsLevelNameForLoad . c_str ( ) ;
sFileName = va ( " music/%s/%s.mp3 " , psDirName , psFileNameBase ) ;
return sFileName . c_str ( ) ;
}
// this MUST return NULL for non-base states unless doing debug-query
const char * Music_BaseStateToString ( MusicState_e eMusicState , qboolean bDebugPrintQuery /* = qfalse */ )
{
switch ( eMusicState )
{
case eBGRNDTRACK_EXPLORE : return " explore " ;
case eBGRNDTRACK_ACTION : return " action " ;
case eBGRNDTRACK_BOSS : return " boss " ;
case eBGRNDTRACK_SILENCE : return " silence " ; // not used in this module, but snd_dma uses it now it's de-static'd
case eBGRNDTRACK_DEATH : return " death " ;
// info only, not map<> lookup keys (unlike above)...
//
case eBGRNDTRACK_ACTIONTRANS0 : if ( bDebugPrintQuery ) return " action_tr0 " ;
case eBGRNDTRACK_ACTIONTRANS1 : if ( bDebugPrintQuery ) return " action_tr1 " ;
case eBGRNDTRACK_ACTIONTRANS2 : if ( bDebugPrintQuery ) return " action_tr2 " ;
case eBGRNDTRACK_ACTIONTRANS3 : if ( bDebugPrintQuery ) return " action_tr3 " ;
case eBGRNDTRACK_EXPLORETRANS0 : if ( bDebugPrintQuery ) return " explore_tr0 " ;
case eBGRNDTRACK_EXPLORETRANS1 : if ( bDebugPrintQuery ) return " explore_tr1 " ;
case eBGRNDTRACK_EXPLORETRANS2 : if ( bDebugPrintQuery ) return " explore_tr2 " ;
case eBGRNDTRACK_EXPLORETRANS3 : if ( bDebugPrintQuery ) return " explore_tr3 " ;
case eBGRNDTRACK_FADE : if ( bDebugPrintQuery ) return " fade " ;
default : break ;
}
return NULL ;
}
static qboolean Music_ParseMusic ( gsl : : czstring filename , const CGenericParser2 & Parser , MusicData_t * MusicData , const CGPGroup & pgMusicFiles , const gsl : : cstring_view & psMusicName , const gsl : : cstring_view & psMusicNameKey , MusicState_e eMusicState )
{
bool bReturn = false ;
MusicFile_t MusicFile ;
const CGPGroup * const pgMusicFile = pgMusicFiles . FindSubGroup ( psMusicName ) ;
if ( pgMusicFile )
{
// read subgroups...
//
bool bEntryFound = false ;
bool bExitFound = false ;
//
// (read entry points first, so I can check exit points aren't too close in time)
//
const CGPGroup * pEntryGroup = pgMusicFile - > FindSubGroup ( sKEY_ENTRY ) ;
if ( pEntryGroup )
{
// read entry points...
//
for ( auto & prop : pEntryGroup - > GetProperties ( ) )
{
//if( Q::substr( prop.GetName(), 0, sKEY_MARKER.size() ) == sKEY_MARKER ) // for now, assume anything is a marker
{
MusicFile . MusicEntryTimes [ prop . GetName ( ) ] = Q : : svtoi ( prop . GetTopValue ( ) ) ;
bEntryFound = true ;
}
}
}
for ( auto & group : pgMusicFile - > GetSubGroups ( ) )
{
auto & groupName = group . GetName ( ) ;
if ( groupName = = sKEY_ENTRY )
{
// skip entry points, I've already read them in above
//
}
else if ( groupName = = sKEY_EXIT )
{
int iThisExitPointIndex = MusicFile . MusicExitPoints . size ( ) ; // must eval this first, so unaffected by push_back etc
//
// read this set of exit points...
//
MusicExitPoint_t MusicExitPoint ;
for ( auto & prop : group . GetProperties ( ) )
{
auto & key = prop . GetName ( ) ;
auto & value = prop . GetTopValue ( ) ;
if ( key = = sKEY_NEXTFILE )
{
MusicExitPoint . sNextFile = value ;
bExitFound = true ; // harmless to keep setting
}
else if ( key = = sKEY_NEXTMARK )
{
MusicExitPoint . sNextMark = value ;
}
else if ( Q : : substr ( key , 0 , sKEY_TIME . size ( ) ) = = sKEY_TIME )
{
MusicExitTime_t MusicExitTime ;
MusicExitTime . fTime = Q : : svtof ( value ) ;
MusicExitTime . iExitPoint = iThisExitPointIndex ;
// new check, don't keep this this exit point if it's within 1.5 seconds either way of an entry point...
//
bool bTooCloseToEntryPoint = false ;
for ( auto & item : MusicFile . MusicEntryTimes )
{
float fThisEntryTime = item . second ;
if ( Q_fabs ( fThisEntryTime - MusicExitTime . fTime ) < 1.5f )
{
// bTooCloseToEntryPoint = true; // not sure about this, ignore for now
break ;
}
}
if ( ! bTooCloseToEntryPoint )
{
MusicFile . MusicExitTimes . push_back ( MusicExitTime ) ;
}
}
}
MusicFile . MusicExitPoints . push_back ( MusicExitPoint ) ;
int iNumExitPoints = MusicFile . MusicExitPoints . size ( ) ;
// error checking...
//
switch ( eMusicState )
{
case eBGRNDTRACK_EXPLORE :
if ( iNumExitPoints > iMAX_EXPLORE_TRANSITIONS )
{
Music_Parse_Error ( filename , build_string ( " \" " , psMusicName , " \" has > " , iMAX_EXPLORE_TRANSITIONS , " " , psMusicNameKey , " transitions defined! \n " ) ) ;
return qfalse ;
}
break ;
case eBGRNDTRACK_ACTION :
if ( iNumExitPoints > iMAX_ACTION_TRANSITIONS )
{
Music_Parse_Error ( filename , build_string ( " \" " , psMusicName , " \" has > " , iMAX_ACTION_TRANSITIONS , " " , psMusicNameKey , " transitions defined! \n " ) ) ;
return qfalse ;
}
break ;
case eBGRNDTRACK_BOSS :
case eBGRNDTRACK_DEATH :
Music_Parse_Error ( filename , build_string ( " \" " , psMusicName , " \" has " , psMusicNameKey , " transitions defined, this is not allowed! \n " ) ) ;
return qfalse ;
default :
break ;
}
}
}
// for now, assume everything was ok unless some obvious things are missing...
//
bReturn = true ;
// boss & death pieces can omit entry/exit stuff
if ( eMusicState ! = eBGRNDTRACK_BOSS & & eMusicState ! = eBGRNDTRACK_DEATH )
{
if ( ! bEntryFound )
{
Music_Parse_Error ( filename , build_string ( " Unable to find subgroup \" " , sKEY_ENTRY , " \" in group \" " , psMusicName , " \" \n " ) ) ;
bReturn = false ;
}
if ( ! bExitFound )
{
Music_Parse_Error ( filename , build_string ( " Unable to find subgroup \" " , sKEY_EXIT , " \" in group \" " , psMusicName , " \" \n " ) ) ;
bReturn = false ;
}
}
}
else
{
Music_Parse_Error ( filename , build_string ( " Unable to find musicfiles entry \" " , psMusicName , " \" \n " ) ) ;
}
if ( bReturn )
{
MusicFile . sFileNameBase = psMusicName ;
( * MusicData ) [ psMusicNameKey ] = MusicFile ;
}
return ( qboolean ) bReturn ;
}
// called from SV_SpawnServer, but before map load and music start etc.
//
// This just initialises the Lucas music structs so the background music player can interrogate them...
//
sstring_t gsLevelNameFromServer ;
void Music_SetLevelName ( const char * psLevelName )
{
gsLevelNameFromServer = psLevelName ;
}
static qboolean Music_ParseLeveldata ( gsl : : czstring psLevelName )
{
qboolean bReturn = qfalse ;
if ( MusicData = = NULL )
{
// sorry vv, false leaks make it hard to find true leaks
static MusicData_t singleton ;
MusicData = & singleton ;
}
// already got this data?
//
if ( MusicData - > size ( ) & & ! Q_stricmp ( psLevelName , gsLevelNameForCompare . c_str ( ) ) )
{
return qtrue ;
}
MusicData - > clear ( ) ;
// shorten level name to MAX_QPATH so sstring's assignment assertion is satisfied.
char sLevelName [ MAX_QPATH ] ;
Q_strncpyz ( sLevelName , psLevelName , sizeof ( sLevelName ) ) ;
gsLevelNameForLoad = sLevelName ; // harmless to init here even if we fail to parse dms.dat file
gsLevelNameForCompare = sLevelName ; // harmless to init here even if we fail to parse dms.dat file
gsLevelNameForBossLoad = sLevelName ; // harmless to init here even if we fail to parse dms.dat file
gsl : : czstring filename = sFILENAME_DMS ;
CGenericParser2 Parser ;
if ( ! Parser . Parse ( filename ) )
{
Music_Parse_Error ( filename , " Error using GP to parse file \n " ) ;
}
else
{
const CGPGroup & pFileGroup = Parser . GetBaseParseGroup ( ) ;
const CGPGroup * pgMusicFiles = pFileGroup . FindSubGroup ( sKEY_MUSICFILES ) ;
if ( ! pgMusicFiles )
{
Music_Parse_Error ( filename , build_string ( " Unable to find subgroup \" " , sKEY_MUSICFILES , " \" \n " ) ) ;
}
else
{
const CGPGroup * pgLevelMusic = pFileGroup . FindSubGroup ( sKEY_LEVELMUSIC ) ;
if ( ! pgLevelMusic )
{
Music_Parse_Error ( filename , build_string ( " Unable to find subgroup \" " , sKEY_MUSICFILES , " \" \n " ) ) ;
}
else
{
const CGPGroup * pgThisLevelMusic = nullptr ;
//
// check for new USE keyword...
//
int steps = 0 ;
gsl : : cstring_view searchName { & sLevelName [ 0 ] , & sLevelName [ strlen ( & sLevelName [ 0 ] ) ] } ;
const int sanityLimit = 10 ;
while ( ! searchName . empty ( ) & & steps < sanityLimit )
{
gsLevelNameForLoad = searchName ;
gsLevelNameForBossLoad = gsLevelNameForLoad ;
pgThisLevelMusic = pgLevelMusic - > FindSubGroup ( searchName ) ;
if ( pgThisLevelMusic )
{
const CGPProperty * pValue = pgThisLevelMusic - > FindProperty ( sKEY_USES ) ;
if ( pValue )
{
// re-search using the USE param...
//
searchName = pValue - > GetTopValue ( ) ;
steps + + ;
// Com_DPrintf("Using \"%s\"\n",sSearchName.c_str());
}
else
{
// no new USE keyword found...
//
searchName = { } ;
}
}
else
{
// level entry not found...
//
break ;
}
}
// now go ahead and use the final music set we've decided on...
//
if ( ! pgThisLevelMusic | | steps > = sanityLimit )
{
Music_Parse_Warning ( build_string ( " Unable to find entry for \" " , sLevelName , " \" in \" " , filename , " \" \n " ) ) ;
}
else
{
// these are optional fields, so see which ones we find...
//
gsl : : cstring_view psName_Explore ;
gsl : : cstring_view psName_Action ;
gsl : : cstring_view psName_Boss ;
gsl : : cstring_view psName_UseBoss ;
for ( auto & prop : pgThisLevelMusic - > GetProperties ( ) )
{
auto & key = prop . GetName ( ) ;
auto & value = prop . GetTopValue ( ) ;
if ( Q : : stricmp ( value , sKEY_PLACEHOLDER ) = = Q : : Ordering : : EQ )
{
// ignore "placeholder" items
continue ;
}
if ( Q : : stricmp ( key , sKEY_EXPLORE ) = = Q : : Ordering : : EQ )
{
psName_Explore = value ;
}
else if ( Q : : stricmp ( key , sKEY_ACTION ) = = Q : : Ordering : : EQ )
{
psName_Action = value ;
}
else if ( Q : : stricmp ( key , sKEY_USEBOSS ) = = Q : : Ordering : : EQ )
{
psName_UseBoss = value ;
}
else if ( Q : : stricmp ( key , sKEY_BOSS ) = = Q : : Ordering : : EQ )
{
psName_Boss = value ;
}
}
bReturn = qtrue ; // defualt to ON now, so I can turn it off if "useboss" fails
if ( ! psName_UseBoss . empty ( ) )
{
const CGPGroup * pgLevelMusicOfBoss = pgLevelMusic - > FindSubGroup ( psName_UseBoss ) ;
if ( ! pgLevelMusicOfBoss )
{
Music_Parse_Error ( filename , build_string ( " Unable to find 'useboss' entry \" " , psName_UseBoss , " \" \n " , psName_UseBoss ) ) ;
bReturn = qfalse ;
}
else
{
const CGPProperty * pValueBoss = pgLevelMusicOfBoss - > FindProperty ( sKEY_BOSS ) ;
if ( ! pValueBoss )
{
Music_Parse_Error ( filename , build_string ( " 'useboss' \" " , psName_UseBoss , " \" has no \" boss \" entry! \n " ) ) ;
bReturn = qfalse ;
}
else
{
psName_Boss = pValueBoss - > GetTopValue ( ) ;
gsLevelNameForBossLoad = psName_UseBoss ;
}
}
}
// done this way in case I want to conditionally pass any bools depending on music type...
//
if ( bReturn & & psName_Explore )
{
bReturn = Music_ParseMusic ( filename , Parser , MusicData , * pgMusicFiles , psName_Explore , sKEY_EXPLORE , eBGRNDTRACK_EXPLORE ) ;
}
if ( bReturn & & psName_Action )
{
bReturn = Music_ParseMusic ( filename , Parser , MusicData , * pgMusicFiles , psName_Action , sKEY_ACTION , eBGRNDTRACK_ACTION ) ;
}
if ( bReturn & & psName_Boss )
{
bReturn = Music_ParseMusic ( filename , Parser , MusicData , * pgMusicFiles , psName_Boss , sKEY_BOSS , eBGRNDTRACK_BOSS ) ;
}
if ( bReturn /*&& psName_Death*/ ) // LAST MINUTE HACK!!, always force in some death music!!!!
{
//bReturn = Music_ParseMusic(Parser, MusicData, pgMusicFiles, psName_Death, sKEY_DEATH, eBGRNDTRACK_DEATH);
MusicFile_t m ;
m . sFileNameBase = " death_music " ;
( * MusicData ) [ sKEY_DEATH ] = m ;
}
}
}
}
}
if ( bReturn )
{
// sort exit points, and do some error checking...
//
for ( MusicData_t : : iterator itMusicData = MusicData - > begin ( ) ; itMusicData ! = MusicData - > end ( ) ; + + itMusicData )
{
const char * psMusicStateType = ( * itMusicData ) . first . c_str ( ) ;
MusicFile_t & MusicFile = ( * itMusicData ) . second ;
// kludge up an enum, only interested in boss or not at the moment, so...
//
MusicState_e eMusicState = ! Q_stricmp ( psMusicStateType , " boss " ) ? eBGRNDTRACK_BOSS : ! Q_stricmp ( psMusicStateType , " death " ) ? eBGRNDTRACK_DEATH : eBGRNDTRACK_EXPLORE ;
if ( ! MusicFile . MusicExitTimes . empty ( ) )
{
sort ( MusicFile . MusicExitTimes . begin ( ) , MusicFile . MusicExitTimes . end ( ) ) ;
}
// check music exists...
//
const char * psMusicFileName = Music_BuildFileName ( MusicFile . sFileNameBase . c_str ( ) , eMusicState ) ;
if ( ! S_FileExists ( psMusicFileName ) )
{
Music_Parse_Error ( filename , build_string ( " Music file \" " , psMusicFileName , " \" not found! \n " ) ) ;
return qfalse ; // have to return, because music data destroyed now
}
// check all transition music pieces exist, and that entry points into new pieces after transitions also exist...
//
for ( size_t iExitPoint = 0 ; iExitPoint < MusicFile . MusicExitPoints . size ( ) ; iExitPoint + + )
{
MusicExitPoint_t & MusicExitPoint = MusicFile . MusicExitPoints [ iExitPoint ] ;
const char * psTransitionFileName = Music_BuildFileName ( MusicExitPoint . sNextFile . c_str ( ) , eMusicState ) ;
if ( ! S_FileExists ( psTransitionFileName ) )
{
Music_Parse_Error ( filename , build_string ( " Transition file \" " , psTransitionFileName , " \" (entry \" " , MusicExitPoint . sNextFile . c_str ( ) , " \" ) not found! \n " ) ) ;
return qfalse ; // have to return, because music data destroyed now
}
const char * psNextMark = MusicExitPoint . sNextMark . c_str ( ) ;
if ( strlen ( psNextMark ) ) // always NZ ptr
{
// then this must be "action" music under current rules...
//
2023-05-04 18:53:52 +00:00
//assert( !strcmp(psMusicStateType, Music_BaseStateToString(eBGRNDTRACK_ACTION) ? Music_BaseStateToString(eBGRNDTRACK_ACTION):"") );
2022-09-18 15:37:21 +00:00
//
// does this marker exist in the explore piece?
//
MusicData_t : : iterator itExploreMusicData = MusicData - > find ( Music_BaseStateToString ( eBGRNDTRACK_EXPLORE ) ) ;
if ( itExploreMusicData ! = MusicData - > end ( ) )
{
MusicFile_t & MusicFile_Explore = ( * itExploreMusicData ) . second ;
if ( ! MusicFile_Explore . MusicEntryTimes . count ( psNextMark ) )
{
Music_Parse_Error ( filename , build_string ( " Unable to find entry point \" " , psNextMark , " \" in description for \" " , MusicFile_Explore . sFileNameBase . c_str ( ) , " \" \n " ) ) ;
return qfalse ; // have to return, because music data destroyed now
}
}
else
{
Music_Parse_Error ( filename , build_string ( " Unable to find " , Music_BaseStateToString ( eBGRNDTRACK_EXPLORE ) , " piece to match \" " , MusicFile . sFileNameBase . c_str ( ) , " \" \n " ) ) ;
return qfalse ; // have to return, because music data destroyed now
}
}
}
}
}
return bReturn ;
}
// returns ptr to music file, or NULL for error/missing...
//
static MusicFile_t * Music_GetBaseMusicFile ( const char * psMusicState ) // where psMusicState is (eg) "explore", "action" or "boss"
{
MusicData_t : : iterator it = MusicData - > find ( psMusicState ) ;
if ( it ! = MusicData - > end ( ) )
{
MusicFile_t * pMusicFile = & ( * it ) . second ;
return pMusicFile ;
}
return NULL ;
}
static MusicFile_t * Music_GetBaseMusicFile ( MusicState_e eMusicState )
{
const char * psMusicStateString = Music_BaseStateToString ( eMusicState ) ;
if ( psMusicStateString )
{
return Music_GetBaseMusicFile ( psMusicStateString ) ;
}
return NULL ;
}
// where label is (eg) "kejim_base"...
//
qboolean Music_DynamicDataAvailable ( const char * psDynamicMusicLabel )
{
char sLevelName [ MAX_QPATH ] ;
Q_strncpyz ( sLevelName , COM_SkipPath ( const_cast < char * > ( ( psDynamicMusicLabel & & psDynamicMusicLabel [ 0 ] ) ? psDynamicMusicLabel : gsLevelNameFromServer . c_str ( ) ) ) , sizeof ( sLevelName ) ) ;
Q_strlwr ( sLevelName ) ;
if ( strlen ( sLevelName ) ) // avoid error messages when there's no music waiting to be played and we try and restart it...
{
if ( Music_ParseLeveldata ( sLevelName ) )
{
return ( qboolean ) ( Music_GetBaseMusicFile ( eBGRNDTRACK_EXPLORE ) & &
Music_GetBaseMusicFile ( eBGRNDTRACK_ACTION ) ) ;
}
}
return qfalse ;
}
const char * Music_GetFileNameForState ( MusicState_e eMusicState )
{
MusicFile_t * pMusicFile = NULL ;
switch ( eMusicState )
{
case eBGRNDTRACK_EXPLORE :
case eBGRNDTRACK_ACTION :
case eBGRNDTRACK_BOSS :
case eBGRNDTRACK_DEATH :
pMusicFile = Music_GetBaseMusicFile ( eMusicState ) ;
if ( pMusicFile )
{
return Music_BuildFileName ( pMusicFile - > sFileNameBase . c_str ( ) , eMusicState ) ;
}
break ;
case eBGRNDTRACK_ACTIONTRANS0 :
case eBGRNDTRACK_ACTIONTRANS1 :
case eBGRNDTRACK_ACTIONTRANS2 :
case eBGRNDTRACK_ACTIONTRANS3 :
pMusicFile = Music_GetBaseMusicFile ( eBGRNDTRACK_ACTION ) ;
if ( pMusicFile )
{
size_t iTransNum = eMusicState - eBGRNDTRACK_ACTIONTRANS0 ;
if ( iTransNum < pMusicFile - > MusicExitPoints . size ( ) )
{
return Music_BuildFileName ( pMusicFile - > MusicExitPoints [ iTransNum ] . sNextFile . c_str ( ) , eMusicState ) ;
}
}
break ;
case eBGRNDTRACK_EXPLORETRANS0 :
case eBGRNDTRACK_EXPLORETRANS1 :
case eBGRNDTRACK_EXPLORETRANS2 :
case eBGRNDTRACK_EXPLORETRANS3 :
pMusicFile = Music_GetBaseMusicFile ( eBGRNDTRACK_EXPLORE ) ;
if ( pMusicFile )
{
size_t iTransNum = eMusicState - eBGRNDTRACK_EXPLORETRANS0 ;
if ( iTransNum < pMusicFile - > MusicExitPoints . size ( ) )
{
return Music_BuildFileName ( pMusicFile - > MusicExitPoints [ iTransNum ] . sNextFile . c_str ( ) , eMusicState ) ;
}
}
break ;
default :
# ifndef FINAL_BUILD
assert ( 0 ) ; // duh....what state are they asking for?
Com_Printf ( S_COLOR_RED " Music_GetFileNameForState( %d ) unhandled case! \n " , eMusicState ) ;
# endif
break ;
}
return NULL ;
}
qboolean Music_StateIsTransition ( MusicState_e eMusicState )
{
return ( qboolean ) ( eMusicState > = eBGRNDTRACK_FIRSTTRANSITION & &
eMusicState < = eBGRNDTRACK_LASTTRANSITION ) ;
}
qboolean Music_StateCanBeInterrupted ( MusicState_e eMusicState , MusicState_e eProposedMusicState )
{
// death music can interrupt anything...
//
if ( eProposedMusicState = = eBGRNDTRACK_DEATH )
return qtrue ;
//
// ... and can't be interrupted once started...(though it will internally-switch to silence at the end, rather than loop)
//
if ( eMusicState = = eBGRNDTRACK_DEATH )
{
return qfalse ;
}
// boss music can interrupt anything (other than death, but that's already handled above)...
//
if ( eProposedMusicState = = eBGRNDTRACK_BOSS )
return qtrue ;
//
// ... and can't be interrupted once started...
//
if ( eMusicState = = eBGRNDTRACK_BOSS )
{
// ...except by silence (or death, but again, that's already handled above)
//
if ( eProposedMusicState = = eBGRNDTRACK_SILENCE )
return qtrue ;
return qfalse ;
}
// action music can interrupt anything (after boss & death filters above)...
//
if ( eProposedMusicState = = eBGRNDTRACK_ACTION )
return qtrue ;
// nothing can interrupt a transition (after above filters)...
//
if ( Music_StateIsTransition ( eMusicState ) )
return qfalse ;
// current state is therefore interruptable...
//
return qtrue ;
}
// returns qtrue if music is allowed to transition out of current state, based on current play position...
// (doesn't bother returning final state after transition (eg action->transition->explore) becuase it's fairly obvious)
//
// supply:
//
// playing point in float seconds
// enum of track being queried
//
// get:
//
// enum of transition track to switch to
// float time of entry point of new track *after* transition
//
qboolean Music_AllowedToTransition ( float fPlayingTimeElapsed ,
MusicState_e eMusicState ,
//
MusicState_e * peTransition /* = NULL */ ,
float * pfNewTrackEntryTime /* = NULL */
)
{
const float fTimeEpsilon = 0.3f ; // arb., how close we have to be to an exit point to take it.
// if set too high then music change is sloppy
// if set too low[/precise] then we might miss an exit if client fps is poor
MusicFile_t * pMusicFile = Music_GetBaseMusicFile ( eMusicState ) ;
if ( pMusicFile & & ! pMusicFile - > MusicExitTimes . empty ( ) )
{
MusicExitTime_t T ;
T . fTime = fPlayingTimeElapsed ;
// since a MusicExitTimes_t item is a sorted array, we can use the equal_range algorithm...
//
std : : pair < MusicExitTimes_t : : iterator , MusicExitTimes_t : : iterator > itp = equal_range ( pMusicFile - > MusicExitTimes . begin ( ) , pMusicFile - > MusicExitTimes . end ( ) , T ) ;
if ( itp . first ! = pMusicFile - > MusicExitTimes . begin ( ) )
itp . first - - ; // encompass the one before, in case we've just missed an exit point by < fTimeEpsilon
if ( itp . second ! = pMusicFile - > MusicExitTimes . end ( ) )
itp . second + + ; // increase range to one beyond, so we can do normal STL being/end looping below
for ( MusicExitTimes_t : : iterator it = itp . first ; it ! = itp . second ; + + it )
{
MusicExitTimes_t : : iterator pExitTime = it ;
if ( Q_fabs ( pExitTime - > fTime - fPlayingTimeElapsed ) < = fTimeEpsilon )
{
// got an exit point!, work out feedback params...
//
size_t iExitPoint = pExitTime - > iExitPoint ;
//
// the two params to give back...
//
MusicState_e eFeedBackTransition = eBGRNDTRACK_EXPLORETRANS0 ; // any old default
float fFeedBackNewTrackEntryTime = 0.0f ;
//
// check legality in case of crap data...
//
if ( iExitPoint < pMusicFile - > MusicExitPoints . size ( ) )
{
MusicExitPoint_t & ExitPoint = pMusicFile - > MusicExitPoints [ iExitPoint ] ;
switch ( eMusicState )
{
case eBGRNDTRACK_EXPLORE :
{
assert ( iExitPoint < iMAX_EXPLORE_TRANSITIONS ) ; // already been checked, but sanity
assert ( ! ExitPoint . sNextMark . c_str ( ) [ 0 ] ) ; // simple error checking, but harmless if tripped. explore transitions go to silence, hence no entry time for [silence] state after transition
eFeedBackTransition = ( MusicState_e ) ( eBGRNDTRACK_EXPLORETRANS0 + iExitPoint ) ;
}
break ;
case eBGRNDTRACK_ACTION :
{
assert ( iExitPoint < iMAX_ACTION_TRANSITIONS ) ; // already been checked, but sanity
// if there's an entry marker point defined...
//
if ( ExitPoint . sNextMark . c_str ( ) [ 0 ] )
{
MusicData_t : : iterator itExploreMusicData = MusicData - > find ( Music_BaseStateToString ( eBGRNDTRACK_EXPLORE ) ) ;
//
// find "explore" music...
//
if ( itExploreMusicData ! = MusicData - > end ( ) )
{
MusicFile_t & MusicFile_Explore = ( * itExploreMusicData ) . second ;
//
// find the entry marker within the music and read the time there...
//
MusicEntryTimes_t : : iterator itEntryTime = MusicFile_Explore . MusicEntryTimes . find ( ExitPoint . sNextMark . c_str ( ) ) ;
if ( itEntryTime ! = MusicFile_Explore . MusicEntryTimes . end ( ) )
{
fFeedBackNewTrackEntryTime = ( * itEntryTime ) . second ;
eFeedBackTransition = ( MusicState_e ) ( eBGRNDTRACK_ACTIONTRANS0 + iExitPoint ) ;
}
else
{
# ifndef FINAL_BUILD
assert ( 0 ) ; // sanity, should have been caught elsewhere, but harmless to do this
Com_Printf ( S_COLOR_RED " Music_AllowedToTransition() unable to find entry marker \" %s \" in \" %s \" " , ExitPoint . sNextMark . c_str ( ) , MusicFile_Explore . sFileNameBase . c_str ( ) ) ;
# endif
return qfalse ;
}
}
else
{
# ifndef FINAL_BUILD
assert ( 0 ) ; // sanity, should have been caught elsewhere, but harmless to do this
Com_Printf ( S_COLOR_RED " Music_AllowedToTransition() unable to find %s version of \" %s \" \n " , Music_BaseStateToString ( eBGRNDTRACK_EXPLORE ) , pMusicFile - > sFileNameBase . c_str ( ) ) ;
# endif
return qfalse ;
}
}
else
{
eFeedBackTransition = eBGRNDTRACK_ACTIONTRANS0 ;
fFeedBackNewTrackEntryTime = 0.0f ; // already set to this, but FYI
}
}
break ;
default :
{
# ifndef FINAL_BUILD
assert ( 0 ) ;
Com_Printf ( S_COLOR_RED " Music_AllowedToTransition(): No code to transition from music type %d \n " , eMusicState ) ;
# endif
return qfalse ;
}
break ;
}
}
else
{
# ifndef FINAL_BUILD
assert ( 0 ) ;
Com_Printf ( S_COLOR_RED " Music_AllowedToTransition(): Illegal exit point %d, max = %d (music: \" %s \" ) \n " , iExitPoint , pMusicFile - > MusicExitPoints . size ( ) - 1 , pMusicFile - > sFileNameBase . c_str ( ) ) ;
# endif
return qfalse ;
}
// feed back answers...
//
if ( peTransition )
{
* peTransition = eFeedBackTransition ;
}
if ( pfNewTrackEntryTime )
{
* pfNewTrackEntryTime = fFeedBackNewTrackEntryTime ;
}
return qtrue ;
}
}
}
return qfalse ;
}
// typically used to get a (predefined) random entry point for the action music, but will work on any defined type with entry points,
// defaults safely to 0.0f if no info available...
//
float Music_GetRandomEntryTime ( MusicState_e eMusicState )
{
MusicData_t : : iterator itMusicData = MusicData - > find ( Music_BaseStateToString ( eMusicState ) ) ;
if ( itMusicData ! = MusicData - > end ( ) )
{
MusicFile_t & MusicFile = ( * itMusicData ) . second ;
if ( MusicFile . MusicEntryTimes . size ( ) ) // make sure at least one defined, else default to start
{
// Quake's random number generator isn't very good, so instead of this:
//
// int iRandomEntryNum = Q_irand(0, (MusicFile.MusicEntryTimes.size()-1) );
//
// ... I'll do this (ensuring we don't get the same result on two consecutive calls, but without while-loop)...
//
static int iPrevRandomNumber = - 1 ;
static int iCallCount = 0 ;
iCallCount + + ;
int iRandomEntryNum = ( rand ( ) + iCallCount ) % ( MusicFile . MusicEntryTimes . size ( ) ) ; // legal range
if ( iRandomEntryNum = = iPrevRandomNumber & & MusicFile . MusicEntryTimes . size ( ) > 1 )
{
iRandomEntryNum + = 1 ;
iRandomEntryNum % = ( MusicFile . MusicEntryTimes . size ( ) ) ;
}
iPrevRandomNumber = iRandomEntryNum ;
// OutputDebugString(va("Music_GetRandomEntryTime(): Entry %d\n",iRandomEntryNum));
for ( MusicEntryTimes_t : : iterator itEntryTime = MusicFile . MusicEntryTimes . begin ( ) ; itEntryTime ! = MusicFile . MusicEntryTimes . end ( ) ; + + itEntryTime )
{
if ( ! iRandomEntryNum - - )
{
return ( * itEntryTime ) . second ;
}
}
}
}
return 0.0f ;
}
// info only, used in "soundinfo" command...
//
const char * Music_GetLevelSetName ( void )
{
if ( Q_stricmp ( gsLevelNameForCompare . c_str ( ) , gsLevelNameForLoad . c_str ( ) ) )
{
// music remap via USES command...
//
return va ( " %s -> %s " , gsLevelNameForCompare . c_str ( ) , gsLevelNameForLoad . c_str ( ) ) ;
}
return gsLevelNameForLoad . c_str ( ) ;
}
///////////////// eof /////////////////////