/* =========================================================================== 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 . =========================================================================== */ // 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 #include #include #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 MusicExitPoints_t; typedef std::vector MusicExitTimes_t; typedef std::map MusicEntryTimes_t; // key eg "marker1" typedef struct { sstring_t sFileNameBase; MusicEntryTimes_t MusicEntryTimes; MusicExitPoints_t MusicExitPoints; MusicExitTimes_t MusicExitTimes; } MusicFile_t; typedef std::map 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... // //assert( !strcmp(psMusicStateType, Music_BaseStateToString(eBGRNDTRACK_ACTION) ? Music_BaseStateToString(eBGRNDTRACK_ACTION):"") ); // // 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( (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 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 /////////////////////