/*
===========================================================================
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 /////////////////////