1155 lines
35 KiB
C++
1155 lines
35 KiB
C++
// 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>
|
|
|
|
#ifdef _XBOX
|
|
#include "snd_local_console.h"
|
|
#include <xtl.h>
|
|
#else
|
|
#include "snd_local.h"
|
|
#include "cl_mp3.h"
|
|
#endif
|
|
|
|
//
|
|
#include "snd_music.h"
|
|
|
|
extern qboolean S_FileExists( const char *psFilename );
|
|
|
|
#ifdef _XBOX
|
|
extern void Z_SetNewDeleteTemporary(bool bTemp);
|
|
#endif
|
|
|
|
#define sKEY_MUSICFILES "musicfiles"
|
|
#define sKEY_ENTRY "entry"
|
|
#define sKEY_EXIT "exit"
|
|
#define sKEY_MARKER "marker"
|
|
#define sKEY_TIME "time"
|
|
#define sKEY_NEXTFILE "nextfile"
|
|
#define sKEY_NEXTMARK "nextmark"
|
|
#define sKEY_LEVELMUSIC "levelmusic"
|
|
#define sKEY_EXPLORE "explore"
|
|
#define sKEY_ACTION "action"
|
|
#define sKEY_BOSS "boss"
|
|
#define sKEY_DEATH "death"
|
|
#define sKEY_USES "uses"
|
|
#define sKEY_USEBOSS "useboss"
|
|
|
|
#define sKEY_PLACEHOLDER "placeholder" // ignore these
|
|
|
|
#define sFILENAME_DMS "ext_data/dms.dat"
|
|
|
|
|
|
#define MUSIC_PARSE_ERROR(_string) Music_Parse_Error(_string) // only use during parse, not run-time use, and bear in mid that data is zapped after error message, so exit any loops immediately
|
|
#define MUSIC_PARSE_WARNING(_string) Music_Parse_Warning(_string)
|
|
|
|
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 vector <MusicExitPoint_t> MusicExitPoints_t;
|
|
typedef vector <MusicExitTime_t> MusicExitTimes_t;
|
|
typedef 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 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)
|
|
{
|
|
#ifdef _XBOX
|
|
// Prevents pending state changes from crashing the game after
|
|
// level loads, but before new music data has been parsed.
|
|
extern void S_AvertMusicDisaster(void);
|
|
S_AvertMusicDisaster();
|
|
#endif
|
|
|
|
if (MusicData)
|
|
{
|
|
#ifdef _XBOX
|
|
delete MusicData;
|
|
#else
|
|
MusicData->clear();
|
|
#endif
|
|
}
|
|
MusicData = NULL;
|
|
}
|
|
|
|
// some sort of error in the music data...
|
|
//
|
|
static void Music_Parse_Error(LPCSTR psError)
|
|
{
|
|
Com_Printf(S_COLOR_RED "Error parsing music data ( in \"%s\" ):\n%s\n",sFILENAME_DMS,psError);
|
|
MusicData->clear();
|
|
}
|
|
|
|
// something to just mention if interested...
|
|
//
|
|
static void Music_Parse_Warning(LPCSTR psError)
|
|
{
|
|
extern cvar_t *s_debugdynamic;
|
|
if (s_debugdynamic && s_debugdynamic->integer)
|
|
{
|
|
Com_Printf(S_COLOR_YELLOW "%s", psError);
|
|
}
|
|
}
|
|
|
|
// 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 LPCSTR Music_BuildFileName(LPCSTR psFileNameBase, MusicState_e eMusicState )
|
|
{
|
|
static sstring_t sFileName;
|
|
|
|
//HACK!
|
|
if (eMusicState == eBGRNDTRACK_DEATH)
|
|
{
|
|
return "music/death_music.mp3";
|
|
}
|
|
|
|
LPCSTR 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";
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
static qboolean Music_ParseMusic(CGenericParser2 &Parser, MusicData_t *MusicData, CGPGroup *pgMusicFiles, LPCSTR psMusicName, LPCSTR psMusicNameKey, MusicState_e eMusicState)
|
|
{
|
|
qboolean bReturn = qfalse;
|
|
|
|
#ifdef _XBOX
|
|
Z_SetNewDeleteTemporary(true);
|
|
#endif
|
|
MusicFile_t MusicFile;
|
|
#ifdef _XBOX
|
|
Z_SetNewDeleteTemporary(false);
|
|
#endif
|
|
|
|
CGPGroup *pgMusicFile = pgMusicFiles->FindSubGroup(psMusicName);
|
|
if (pgMusicFile)
|
|
{
|
|
// read subgroups...
|
|
//
|
|
qboolean bEntryFound = qfalse;
|
|
qboolean bExitFound = qfalse;
|
|
//
|
|
// (read entry points first, so I can check exit points aren't too close in time)
|
|
//
|
|
CGPGroup *pEntryGroup = pgMusicFile->FindSubGroup(sKEY_ENTRY);
|
|
if (pEntryGroup)
|
|
{
|
|
// read entry points...
|
|
//
|
|
for (CGPValue *pValue = pEntryGroup->GetPairs(); pValue; pValue = pValue->GetNext())
|
|
{
|
|
LPCSTR psKey = pValue->GetName();
|
|
LPCSTR psValue = pValue->GetTopValue();
|
|
|
|
//if (!strncmp(psKey,sKEY_MARKER,strlen(sKEY_MARKER))) // for now, assume anything is a marker
|
|
{
|
|
MusicFile.MusicEntryTimes[psKey] = atof(psValue);
|
|
bEntryFound = qtrue; // harmless to keep setting
|
|
}
|
|
}
|
|
}
|
|
|
|
for (CGPGroup *pGroup = pgMusicFile->GetSubGroups(); pGroup; pGroup = pGroup->GetNext())
|
|
{
|
|
LPCSTR psGroupName = pGroup->GetName();
|
|
|
|
if (!strcmp(psGroupName,sKEY_ENTRY))
|
|
{
|
|
// skip entry points, I've already read them in above
|
|
//
|
|
}
|
|
else
|
|
if (!strcmp(psGroupName,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 (CGPValue *pValue = pGroup->GetPairs(); pValue; pValue = pValue->GetNext())
|
|
{
|
|
LPCSTR psKey = pValue->GetName();
|
|
LPCSTR psValue = pValue->GetTopValue();
|
|
|
|
if (!strcmp(psKey,sKEY_NEXTFILE))
|
|
{
|
|
MusicExitPoint.sNextFile = psValue;
|
|
bExitFound = qtrue; // harmless to keep setting
|
|
}
|
|
else
|
|
if (!strcmp(psKey,sKEY_NEXTMARK))
|
|
{
|
|
MusicExitPoint.sNextMark = psValue;
|
|
}
|
|
else
|
|
if (!strncmp(psKey,sKEY_TIME,strlen(sKEY_TIME)))
|
|
{
|
|
MusicExitTime_t MusicExitTime;
|
|
MusicExitTime.fTime = atof(psValue);
|
|
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...
|
|
//
|
|
qboolean bTooCloseToEntryPoint = qfalse;
|
|
for (MusicEntryTimes_t::iterator itEntryTimes = MusicFile.MusicEntryTimes.begin(); itEntryTimes != MusicFile.MusicEntryTimes.end(); ++itEntryTimes)
|
|
{
|
|
float fThisEntryTime = (*itEntryTimes).second;
|
|
|
|
if (Q_fabs(fThisEntryTime - MusicExitTime.fTime) < 1.5f)
|
|
{
|
|
// bTooCloseToEntryPoint = qtrue; // not sure about this, ignore for now
|
|
break;
|
|
}
|
|
}
|
|
if (!bTooCloseToEntryPoint)
|
|
{
|
|
#ifdef _XBOX
|
|
Z_SetNewDeleteTemporary(true);
|
|
#endif
|
|
MusicFile.MusicExitTimes.push_back(MusicExitTime);
|
|
#ifdef _XBOX
|
|
Z_SetNewDeleteTemporary(false);
|
|
#endif
|
|
}
|
|
}
|
|
}
|
|
|
|
#ifdef _XBOX
|
|
Z_SetNewDeleteTemporary(true);
|
|
#endif
|
|
MusicFile.MusicExitPoints.push_back(MusicExitPoint);
|
|
#ifdef _XBOX
|
|
Z_SetNewDeleteTemporary(false);
|
|
#endif
|
|
int iNumExitPoints = MusicFile.MusicExitPoints.size();
|
|
|
|
// error checking...
|
|
//
|
|
switch (eMusicState)
|
|
{
|
|
case eBGRNDTRACK_EXPLORE:
|
|
if (iNumExitPoints > iMAX_EXPLORE_TRANSITIONS)
|
|
{
|
|
MUSIC_PARSE_ERROR( va("\"%s\" has > %d %s transitions defined!\n",psMusicName,iMAX_EXPLORE_TRANSITIONS,psMusicNameKey) );
|
|
return qfalse;
|
|
}
|
|
break;
|
|
|
|
case eBGRNDTRACK_ACTION:
|
|
if (iNumExitPoints > iMAX_ACTION_TRANSITIONS)
|
|
{
|
|
MUSIC_PARSE_ERROR( va("\"%s\" has > %d %s transitions defined!\n",psMusicName,iMAX_ACTION_TRANSITIONS,psMusicNameKey) );
|
|
return qfalse;
|
|
}
|
|
break;
|
|
|
|
case eBGRNDTRACK_BOSS:
|
|
case eBGRNDTRACK_DEATH:
|
|
|
|
MUSIC_PARSE_ERROR( va("\"%s\" has %s transitions defined, this is not allowed!\n",psMusicName,psMusicNameKey) );
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// for now, assume everything was ok unless some obvious things are missing...
|
|
//
|
|
bReturn = qtrue;
|
|
|
|
if (eMusicState != eBGRNDTRACK_BOSS && eMusicState != eBGRNDTRACK_DEATH) // boss & death pieces can omit entry/exit stuff
|
|
{
|
|
if (!bEntryFound)
|
|
{
|
|
MUSIC_PARSE_ERROR(va("Unable to find subgroup \"%s\" in group \"%s\"\n",sKEY_ENTRY,psMusicName));
|
|
bReturn = qfalse;
|
|
}
|
|
if (!bExitFound)
|
|
{
|
|
MUSIC_PARSE_ERROR(va("Unable to find subgroup \"%s\" in group \"%s\"\n",sKEY_EXIT,psMusicName));
|
|
bReturn = qfalse;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
MUSIC_PARSE_ERROR(va("Unable to find musicfiles entry \"%s\"\n",psMusicName));
|
|
}
|
|
|
|
if (bReturn)
|
|
{
|
|
MusicFile.sFileNameBase = psMusicName;
|
|
(*MusicData)[ psMusicNameKey ] = MusicFile;
|
|
}
|
|
|
|
return bReturn;
|
|
}
|
|
|
|
|
|
|
|
|
|
// I only need this because GP2 can't cope with trailing whitespace (for !@#$%^'s sake!!!!)...
|
|
//
|
|
// (output buffer will always be just '\n' seperated, regardless of possible "\r\n" pairs)
|
|
//
|
|
// (remember to Z_Free() the returned char * when done with it!!!)
|
|
//
|
|
static char *StripTrailingWhiteSpaceOnEveryLine(char *pText)
|
|
{
|
|
#ifdef _XBOX
|
|
Z_SetNewDeleteTemporary(true);
|
|
#endif
|
|
string strNewText;
|
|
|
|
while (*pText)
|
|
{
|
|
char sOneLine[1024]; // BTO: was 16k
|
|
|
|
// find end of line...
|
|
//
|
|
char *pThisLineEnd = pText;
|
|
while (*pThisLineEnd && *pThisLineEnd != '\r' && ((pThisLineEnd-pText) < sizeof(sOneLine)-1))
|
|
{
|
|
pThisLineEnd++;
|
|
}
|
|
|
|
int iCharsToCopy = pThisLineEnd - pText;
|
|
strncpy(sOneLine, pText, iCharsToCopy);
|
|
sOneLine[iCharsToCopy]='\0';
|
|
pText += iCharsToCopy;
|
|
while (*pText == '\n' || *pText == '\r') pText++;
|
|
|
|
// trim trailing...
|
|
//
|
|
qboolean bTrimmed = qfalse;
|
|
do
|
|
{
|
|
bTrimmed = qfalse;
|
|
int iStrLen = strlen(sOneLine);
|
|
|
|
if (iStrLen)
|
|
{
|
|
if (sOneLine[iStrLen-1] == '\t' || sOneLine[iStrLen-1] == ' ')
|
|
{
|
|
sOneLine[iStrLen-1] = '\0';
|
|
bTrimmed = qtrue;
|
|
}
|
|
}
|
|
}
|
|
while (bTrimmed);
|
|
|
|
strNewText += sOneLine;
|
|
strNewText += "\n";
|
|
}
|
|
|
|
char *pNewText = (char *) Z_Malloc( strlen(strNewText.c_str())+1,
|
|
TAG_TEMP_WORKSPACE,
|
|
qfalse);
|
|
strcpy(pNewText, strNewText.c_str());
|
|
#ifdef _XBOX
|
|
Z_SetNewDeleteTemporary(false);
|
|
#endif
|
|
return pNewText;
|
|
}
|
|
|
|
|
|
// 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(const char *psLevelName)
|
|
{
|
|
qboolean bReturn = qfalse;
|
|
|
|
if (MusicData == NULL)
|
|
{
|
|
#ifdef _XBOX
|
|
MusicData = new MusicData_t;
|
|
#else
|
|
// sorry vv, false leaks make it hard to find true leaks
|
|
static MusicData_t singleton;
|
|
MusicData = &singleton;
|
|
#endif
|
|
}
|
|
|
|
// already got this data?
|
|
//
|
|
if (MusicData->size() && !Q_stricmp(psLevelName,gsLevelNameForCompare.c_str()))
|
|
{
|
|
return qtrue;
|
|
}
|
|
|
|
MusicData->clear();
|
|
|
|
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
|
|
|
|
char *pText = NULL;
|
|
/*int iTotalBytesLoaded = */FS_ReadFile(sFILENAME_DMS, (void **)&pText );
|
|
if (pText)
|
|
{
|
|
char *psStrippedText = StripTrailingWhiteSpaceOnEveryLine(pText);
|
|
CGenericParser2 Parser;
|
|
char *psDataPtr = psStrippedText; // because ptr gets advanced, so we supply a clone that GP can alter
|
|
if (Parser.Parse(&psDataPtr, true))
|
|
{
|
|
CGPGroup *pFileGroup = Parser.GetBaseParseGroup();
|
|
if (pFileGroup)
|
|
{
|
|
CGPGroup *pgMusicFiles = pFileGroup->FindSubGroup(sKEY_MUSICFILES);
|
|
if (pgMusicFiles)
|
|
{
|
|
CGPGroup *pgLevelMusic = pFileGroup->FindSubGroup(sKEY_LEVELMUSIC);
|
|
|
|
if (pgLevelMusic)
|
|
{
|
|
CGPGroup *pgThisLevelMusic = NULL;
|
|
//
|
|
// check for new USE keyword...
|
|
//
|
|
int iSanityLimit = 0;
|
|
sstring_t sSearchName(sLevelName);
|
|
|
|
while (sSearchName.c_str()[0] && iSanityLimit < 10)
|
|
{
|
|
gsLevelNameForLoad = sSearchName;
|
|
gsLevelNameForBossLoad = sSearchName;
|
|
pgThisLevelMusic = pgLevelMusic->FindSubGroup(sSearchName.c_str());
|
|
|
|
if (pgThisLevelMusic)
|
|
{
|
|
CGPValue *pValue = pgThisLevelMusic->FindPair(sKEY_USES);
|
|
if (pValue)
|
|
{
|
|
// re-search using the USE param...
|
|
//
|
|
sSearchName = pValue->GetTopValue();
|
|
iSanityLimit++;
|
|
// Com_DPrintf("Using \"%s\"\n",sSearchName.c_str());
|
|
}
|
|
else
|
|
{
|
|
// no new USE keyword found...
|
|
//
|
|
sSearchName = "";
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// level entry not found...
|
|
//
|
|
break;
|
|
}
|
|
}
|
|
|
|
// now go ahead and use the final music set we've decided on...
|
|
//
|
|
if (pgThisLevelMusic && iSanityLimit < 10)
|
|
{
|
|
// these are optional fields, so see which ones we find...
|
|
//
|
|
LPCSTR psName_Explore = NULL;
|
|
LPCSTR psName_Action = NULL;
|
|
LPCSTR psName_Boss = NULL;
|
|
LPCSTR psName_Death = NULL;
|
|
//
|
|
LPCSTR psName_UseBoss = NULL;
|
|
|
|
for (CGPValue *pValue = pgThisLevelMusic->GetPairs(); pValue; pValue = pValue->GetNext())
|
|
{
|
|
LPCSTR psKey = pValue->GetName();
|
|
LPCSTR psValue = pValue->GetTopValue();
|
|
|
|
if (Q_stricmp(psValue,sKEY_PLACEHOLDER)) // ignore "placeholder" items
|
|
{
|
|
if (!Q_stricmp(psKey,sKEY_EXPLORE))
|
|
{
|
|
psName_Explore = psValue;
|
|
}
|
|
else
|
|
if (!Q_stricmp(psKey,sKEY_ACTION))
|
|
{
|
|
psName_Action = psValue;
|
|
}
|
|
else
|
|
if (!Q_stricmp(psKey,sKEY_USEBOSS))
|
|
{
|
|
psName_UseBoss = psValue;
|
|
}
|
|
else
|
|
if (!Q_stricmp(psKey,sKEY_BOSS))
|
|
{
|
|
psName_Boss = psValue;
|
|
}
|
|
else
|
|
if (!Q_stricmp(psKey,sKEY_DEATH))
|
|
{
|
|
psName_Death = psValue;
|
|
}
|
|
}
|
|
}
|
|
|
|
bReturn = qtrue; // defualt to ON now, so I can turn it off if "useboss" fails
|
|
|
|
if (psName_UseBoss)
|
|
{
|
|
CGPGroup *pgLevelMusicOfBoss = pgLevelMusic->FindSubGroup(psName_UseBoss);
|
|
if (pgLevelMusicOfBoss)
|
|
{
|
|
CGPValue *pValueBoss = pgLevelMusicOfBoss->FindPair(sKEY_BOSS);
|
|
if (pValueBoss)
|
|
{
|
|
psName_Boss = pValueBoss->GetTopValue();
|
|
gsLevelNameForBossLoad = psName_UseBoss;
|
|
}
|
|
else
|
|
{
|
|
MUSIC_PARSE_ERROR(va("'useboss' \"%s\" has no \"boss\" entry!\n",psName_UseBoss));
|
|
bReturn = qfalse;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
MUSIC_PARSE_ERROR(va("Unable to find 'useboss' entry \"%s\"\n",psName_UseBoss));
|
|
bReturn = qfalse;
|
|
}
|
|
}
|
|
|
|
|
|
// done this way in case I want to conditionally pass any bools depending on music type...
|
|
//
|
|
if (bReturn && psName_Explore)
|
|
{
|
|
bReturn = Music_ParseMusic(Parser, MusicData, pgMusicFiles, psName_Explore, sKEY_EXPLORE, eBGRNDTRACK_EXPLORE);
|
|
}
|
|
if (bReturn && psName_Action)
|
|
{
|
|
bReturn = Music_ParseMusic(Parser, MusicData, pgMusicFiles, psName_Action, sKEY_ACTION, eBGRNDTRACK_ACTION);
|
|
}
|
|
if (bReturn && psName_Boss)
|
|
{
|
|
bReturn = Music_ParseMusic(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;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
MUSIC_PARSE_WARNING(va("Unable to find entry for \"%s\" in \"%s\"\n",sLevelName,sFILENAME_DMS));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
MUSIC_PARSE_ERROR(va("Unable to find subgroup \"%s\"\n",sKEY_LEVELMUSIC));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
MUSIC_PARSE_ERROR(va("Unable to find subgroup \"%s\"\n",sKEY_MUSICFILES));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
MUSIC_PARSE_ERROR( "Error calling GP2.GetBaseParseGroup()\n" );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
MUSIC_PARSE_ERROR( "Error using GP to parse file\n" );
|
|
}
|
|
|
|
Z_Free(psStrippedText);
|
|
FS_FreeFile( pText );
|
|
}
|
|
else
|
|
{
|
|
MUSIC_PARSE_ERROR( "Unable to even read main file\n" ); // file name specified in error message
|
|
}
|
|
|
|
if (bReturn)
|
|
{
|
|
// sort exit points, and do some error checking...
|
|
//
|
|
for (MusicData_t::iterator itMusicData = MusicData->begin(); itMusicData != MusicData->end(); ++itMusicData)
|
|
{
|
|
LPCSTR 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 = !stricmp(psMusicStateType,"boss") ? eBGRNDTRACK_BOSS : !stricmp(psMusicStateType,"death") ? eBGRNDTRACK_DEATH : eBGRNDTRACK_EXPLORE;
|
|
|
|
if (!MusicFile.MusicExitTimes.empty())
|
|
{
|
|
sort(MusicFile.MusicExitTimes.begin(),MusicFile.MusicExitTimes.end());
|
|
}
|
|
|
|
// check music exists...
|
|
//
|
|
LPCSTR psMusicFileName = Music_BuildFileName( MusicFile.sFileNameBase.c_str(), eMusicState );
|
|
if (!S_FileExists( psMusicFileName ))
|
|
{
|
|
MUSIC_PARSE_ERROR(va("Music file \"%s\" not found!\n",psMusicFileName));
|
|
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 (int iExitPoint=0; iExitPoint < MusicFile.MusicExitPoints.size(); iExitPoint++)
|
|
{
|
|
MusicExitPoint_t &MusicExitPoint = MusicFile.MusicExitPoints[ iExitPoint ];
|
|
|
|
LPCSTR psTransitionFileName = Music_BuildFileName( MusicExitPoint.sNextFile.c_str(), eMusicState );
|
|
if (!S_FileExists( psTransitionFileName ))
|
|
{
|
|
MUSIC_PARSE_ERROR(va("Transition file \"%s\" (entry \"%s\" ) not found!\n",psTransitionFileName, MusicExitPoint.sNextFile.c_str()));
|
|
return qfalse; // have to return, because music data destroyed now
|
|
}
|
|
|
|
LPCSTR 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( va("Unable to find entry point \"%s\" in description for \"%s\"\n",psNextMark,MusicFile_Explore.sFileNameBase.c_str()) );
|
|
return qfalse; // have to return, because music data destroyed now
|
|
}
|
|
}
|
|
else
|
|
{
|
|
MUSIC_PARSE_ERROR( va("Unable to find %s piece to match \"%s\"\n", Music_BaseStateToString(eBGRNDTRACK_EXPLORE), MusicFile.sFileNameBase.c_str() ) );
|
|
return qfalse; // have to return, because music data destroyed now
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#ifdef _DEBUG
|
|
/*
|
|
// dump the whole thing out to prove it was read in ok...
|
|
//
|
|
if (bReturn)
|
|
{
|
|
for (MusicData_t::iterator itMusicData = MusicData->begin(); itMusicData != MusicData->end(); ++itMusicData)
|
|
{
|
|
LPCSTR psMusicState = (*itMusicData).first.c_str();
|
|
MusicFile_t &MusicFile = (*itMusicData).second;
|
|
|
|
OutputDebugString(va("Music State: \"%s\", File: \"%s\"\n",psMusicState, MusicFile.sFileNameBase.c_str()));
|
|
|
|
// entry times...
|
|
//
|
|
for (MusicEntryTimes_t::iterator itEntryTimes = MusicFile.MusicEntryTimes.begin(); itEntryTimes != MusicFile.MusicEntryTimes.end(); ++itEntryTimes)
|
|
{
|
|
LPCSTR psMarkerName = (*itEntryTimes).first.c_str();
|
|
float fEntryTime = (*itEntryTimes).second;
|
|
|
|
OutputDebugString(va("Entry time for \"%s\": %f\n", psMarkerName, fEntryTime));
|
|
}
|
|
|
|
// exit points...
|
|
//
|
|
for (int i=0; i<MusicFile.MusicExitPoints.size(); i++)
|
|
{
|
|
MusicExitPoint_t &MusicExitPoint = MusicFile.MusicExitPoints[i];
|
|
|
|
OutputDebugString(va("Exit point %d: sNextFile: \"%s\", sNextMark: \"%s\"\n",i,MusicExitPoint.sNextFile.c_str(),MusicExitPoint.sNextMark.c_str()));
|
|
}
|
|
|
|
// exit times...
|
|
//
|
|
for (i=0; i<MusicFile.MusicExitTimes.size(); i++)
|
|
{
|
|
MusicExitTime_t &MusicExitTime = MusicFile.MusicExitTimes[i];
|
|
|
|
OutputDebugString(va("Exit time %d: fTime: %f, iExitPoint: %d\n",i,MusicExitTime.fTime,MusicExitTime.iExitPoint));
|
|
}
|
|
}
|
|
}
|
|
*/
|
|
#endif
|
|
|
|
return bReturn;
|
|
}
|
|
|
|
|
|
// returns ptr to music file, or NULL for error/missing...
|
|
//
|
|
static MusicFile_t *Music_GetBaseMusicFile( LPCSTR 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 )
|
|
{
|
|
LPCSTR 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));
|
|
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 !!( Music_GetBaseMusicFile( eBGRNDTRACK_EXPLORE ) &&
|
|
Music_GetBaseMusicFile( eBGRNDTRACK_ACTION )
|
|
);
|
|
}
|
|
}
|
|
|
|
return qfalse;
|
|
}
|
|
|
|
LPCSTR 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)
|
|
{
|
|
int 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)
|
|
{
|
|
int 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 (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...
|
|
//
|
|
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...
|
|
//
|
|
int 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 /////////////////////
|
|
|