c4f0c0f5c0
ignore all whitespace, since newlines are significant in SM data.)
1718 lines
49 KiB
C++
1718 lines
49 KiB
C++
#include "global.h"
|
|
/*
|
|
-----------------------------------------------------------------------------
|
|
Class: Song
|
|
|
|
Desc: Holds metadata for a song and the song's step data.
|
|
|
|
Copyright (c) 2001-2002 by the person(s) listed below. All rights reserved.
|
|
Chris Danford
|
|
Glenn Maynard
|
|
-----------------------------------------------------------------------------
|
|
*/
|
|
|
|
#include "song.h"
|
|
#include "Steps.h"
|
|
#include "RageUtil.h"
|
|
#include "RageLog.h"
|
|
#include "IniFile.h"
|
|
#include "NoteData.h"
|
|
#include "RageSound.h"
|
|
#include "RageException.h"
|
|
#include "SongCacheIndex.h"
|
|
#include "GameManager.h"
|
|
#include "PrefsManager.h"
|
|
#include "StyleDef.h"
|
|
#include "GameState.h"
|
|
#include "FontCharAliases.h"
|
|
#include "TitleSubstitution.h"
|
|
#include "BannerCache.h"
|
|
#include "Sprite.h"
|
|
#include "PrefsManager.h"
|
|
#include "arch/arch.h"
|
|
#include "RageFile.h"
|
|
#include "NoteDataUtil.h"
|
|
|
|
#include "NotesLoaderSM.h"
|
|
#include "NotesLoaderDWI.h"
|
|
#include "NotesLoaderBMS.h"
|
|
#include "NotesLoaderKSF.h"
|
|
#include "NotesWriterDWI.h"
|
|
#include "NotesWriterSM.h"
|
|
|
|
#include "LyricsLoader.h"
|
|
|
|
#include "SDL.h"
|
|
#include "SDL_image.h"
|
|
|
|
#include <set>
|
|
|
|
#define CACHE_DIR BASE_PATH "Cache" SLASH
|
|
|
|
const int FILE_CACHE_VERSION = 128; // increment this when Song or Steps changes to invalidate cache
|
|
|
|
const float DEFAULT_MUSIC_SAMPLE_LENGTH = 12.f;
|
|
|
|
|
|
static int CompareBPMSegments(const BPMSegment &seg1, const BPMSegment &seg2)
|
|
{
|
|
return seg1.m_fStartBeat < seg2.m_fStartBeat;
|
|
}
|
|
|
|
void SortBPMSegmentsArray( vector<BPMSegment> &arrayBPMSegments )
|
|
{
|
|
sort( arrayBPMSegments.begin(), arrayBPMSegments.end(), CompareBPMSegments );
|
|
}
|
|
|
|
static int CompareStopSegments(const StopSegment &seg1, const StopSegment &seg2)
|
|
{
|
|
return seg1.m_fStartBeat < seg2.m_fStartBeat;
|
|
}
|
|
|
|
void SortStopSegmentsArray( vector<StopSegment> &arrayStopSegments )
|
|
{
|
|
sort( arrayStopSegments.begin(), arrayStopSegments.end(), CompareStopSegments );
|
|
}
|
|
|
|
int CompareBackgroundChanges(const BackgroundChange &seg1, const BackgroundChange &seg2)
|
|
{
|
|
return seg1.m_fStartBeat < seg2.m_fStartBeat;
|
|
}
|
|
|
|
void SortBackgroundChangesArray( vector<BackgroundChange> &arrayBackgroundChanges )
|
|
{
|
|
sort( arrayBackgroundChanges.begin(), arrayBackgroundChanges.end(), CompareBackgroundChanges );
|
|
}
|
|
|
|
|
|
//////////////////////////////
|
|
// Song
|
|
//////////////////////////////
|
|
Song::Song()
|
|
{
|
|
m_bChangedSinceSave = false;
|
|
m_fBeat0OffsetInSeconds = 0;
|
|
m_fMusicSampleStartSeconds = -1;
|
|
m_fMusicSampleLengthSeconds = DEFAULT_MUSIC_SAMPLE_LENGTH;
|
|
m_fMusicLengthSeconds = 0;
|
|
m_fFirstBeat = -1;
|
|
m_fLastBeat = -1;
|
|
m_SelectionDisplay = SHOW_ALWAYS;
|
|
m_DisplayBPMType = DISPLAY_ACTUAL;
|
|
m_fSpecifiedBPMMin = 0;
|
|
m_fSpecifiedBPMMax = 0;
|
|
m_bIsSymLink = false;
|
|
}
|
|
|
|
Song::~Song()
|
|
{
|
|
for( unsigned i=0; i<m_apNotes.size(); i++ )
|
|
SAFE_DELETE( m_apNotes[i] );
|
|
|
|
m_apNotes.clear();
|
|
}
|
|
|
|
/* Reset to an empty song. */
|
|
void Song::Reset()
|
|
{
|
|
for( unsigned i=0; i<m_apNotes.size(); i++ )
|
|
SAFE_DELETE( m_apNotes[i] );
|
|
m_apNotes.clear();
|
|
|
|
Song empty;
|
|
*this = empty;
|
|
}
|
|
|
|
|
|
void Song::AddBPMSegment( BPMSegment seg )
|
|
{
|
|
m_BPMSegments.push_back( seg );
|
|
SortBPMSegmentsArray( m_BPMSegments );
|
|
}
|
|
|
|
void Song::SetBPMAtBeat( float fBeat, float fBPM )
|
|
{
|
|
unsigned i;
|
|
for( i=0; i<m_BPMSegments.size(); i++ )
|
|
if( m_BPMSegments[i].m_fStartBeat == fBeat )
|
|
break;
|
|
|
|
if( i == m_BPMSegments.size() ) // there is no BPMSegment at the current beat
|
|
{
|
|
// create a new BPMSegment
|
|
AddBPMSegment( BPMSegment(fBeat, fBPM) );
|
|
}
|
|
else // BPMSegment being modified is m_BPMSegments[i]
|
|
{
|
|
if( i > 0 && fabsf(m_BPMSegments[i-1].m_fBPM - fBPM) < 0.009f )
|
|
m_BPMSegments.erase( m_BPMSegments.begin()+i,
|
|
m_BPMSegments.begin()+i+1);
|
|
else
|
|
m_BPMSegments[i].m_fBPM = fBPM;
|
|
}
|
|
}
|
|
|
|
void Song::AddStopSegment( StopSegment seg )
|
|
{
|
|
m_StopSegments.push_back( seg );
|
|
SortStopSegmentsArray( m_StopSegments );
|
|
}
|
|
|
|
|
|
void Song::AddBackgroundChange( BackgroundChange seg )
|
|
{
|
|
m_BackgroundChanges.push_back( seg );
|
|
SortBackgroundChangesArray( m_BackgroundChanges );
|
|
}
|
|
|
|
|
|
void Song::AddLyricSegment( LyricSegment seg )
|
|
{
|
|
m_LyricSegments.push_back( seg );
|
|
}
|
|
|
|
|
|
float Song::GetMusicStartBeat() const
|
|
{
|
|
float fBPS = m_BPMSegments[0].m_fBPM / 60.0f;
|
|
return -(PREFSMAN->m_fGlobalOffsetSeconds+m_fBeat0OffsetInSeconds)*fBPS;
|
|
};
|
|
|
|
void Song::GetBeatAndBPSFromElapsedTime( float fElapsedTime, float &fBeatOut, float &fBPSOut, bool &bFreezeOut ) const
|
|
{
|
|
// LOG->Trace( "GetBeatAndBPSFromElapsedTime( fElapsedTime = %f )", fElapsedTime );
|
|
// This function is a nightmare. Don't even try to understand it. :-)
|
|
|
|
fElapsedTime += PREFSMAN->m_fGlobalOffsetSeconds;
|
|
|
|
fElapsedTime += m_fBeat0OffsetInSeconds;
|
|
|
|
|
|
for( unsigned i=0; i<m_BPMSegments.size(); i++ ) // foreach BPMSegment
|
|
{
|
|
float fStartBeatThisSegment = m_BPMSegments[i].m_fStartBeat;
|
|
bool bIsLastBPMSegment = i==m_BPMSegments.size()-1;
|
|
float fStartBeatNextSegment = bIsLastBPMSegment ? 40000/*inf*/ : m_BPMSegments[i+1].m_fStartBeat;
|
|
float fBeatsInThisSegment = fStartBeatNextSegment - fStartBeatThisSegment;
|
|
float fBPM = m_BPMSegments[i].m_fBPM;
|
|
float fBPS = fBPM / 60.0f;
|
|
|
|
// calculate the number of seconds in this segment
|
|
float fSecondsInThisSegment = fBeatsInThisSegment / fBPS;
|
|
unsigned j;
|
|
for( j=0; j<m_StopSegments.size(); j++ ) // foreach freeze
|
|
{
|
|
if( fStartBeatThisSegment <= m_StopSegments[j].m_fStartBeat && m_StopSegments[j].m_fStartBeat < fStartBeatNextSegment )
|
|
{
|
|
// this freeze lies within this BPMSegment
|
|
fSecondsInThisSegment += m_StopSegments[j].m_fStopSeconds;
|
|
}
|
|
}
|
|
|
|
|
|
if( fElapsedTime > fSecondsInThisSegment )
|
|
{
|
|
// this BPMSegement is NOT the current segment
|
|
fElapsedTime -= fSecondsInThisSegment;
|
|
continue;
|
|
}
|
|
|
|
// this BPMSegment IS the current segment
|
|
|
|
float fBeatEstimate = fStartBeatThisSegment + fElapsedTime*fBPS;
|
|
|
|
for( j=0; j<m_StopSegments.size(); j++ ) // foreach freeze
|
|
{
|
|
if( fStartBeatThisSegment > m_StopSegments[j].m_fStartBeat ||
|
|
m_StopSegments[j].m_fStartBeat > fStartBeatNextSegment )
|
|
continue;
|
|
|
|
// this freeze lies within this BPMSegment
|
|
|
|
if( m_StopSegments[j].m_fStartBeat > fBeatEstimate )
|
|
break;
|
|
|
|
fElapsedTime -= m_StopSegments[j].m_fStopSeconds;
|
|
// re-estimate
|
|
fBeatEstimate = fStartBeatThisSegment + fElapsedTime*fBPS;
|
|
if( fBeatEstimate < m_StopSegments[j].m_fStartBeat )
|
|
{
|
|
fBeatOut = m_StopSegments[j].m_fStartBeat;
|
|
fBPSOut = fBPS;
|
|
bFreezeOut = true;
|
|
return;
|
|
}
|
|
}
|
|
|
|
fBeatOut = fBeatEstimate;
|
|
fBPSOut = fBPS;
|
|
bFreezeOut = false;
|
|
return;
|
|
}
|
|
}
|
|
|
|
|
|
float Song::GetElapsedTimeFromBeat( float fBeat ) const
|
|
{
|
|
float fElapsedTime = 0;
|
|
fElapsedTime -= PREFSMAN->m_fGlobalOffsetSeconds;
|
|
fElapsedTime -= m_fBeat0OffsetInSeconds;
|
|
|
|
for( unsigned j=0; j<m_StopSegments.size(); j++ ) // foreach freeze
|
|
{
|
|
if( m_StopSegments[j].m_fStartBeat >= fBeat )
|
|
break;
|
|
fElapsedTime += m_StopSegments[j].m_fStopSeconds;
|
|
}
|
|
|
|
for( unsigned i=0; i<m_BPMSegments.size(); i++ ) // foreach BPMSegment
|
|
{
|
|
const float fStartBeatThisSegment = m_BPMSegments[i].m_fStartBeat;
|
|
const bool bIsLastBPMSegment = i==m_BPMSegments.size()-1;
|
|
const float fStartBeatNextSegment = bIsLastBPMSegment ? 40000/*inf*/ : m_BPMSegments[i+1].m_fStartBeat;
|
|
const float fBPS = m_BPMSegments[i].m_fBPM / 60.0f;
|
|
const float fBeatsInThisSegment = fStartBeatNextSegment - fStartBeatThisSegment;
|
|
|
|
fElapsedTime += min(fBeat, fBeatsInThisSegment) / fBPS;
|
|
fBeat -= fBeatsInThisSegment;
|
|
|
|
if( fBeat <= 0 )
|
|
return fElapsedTime;
|
|
}
|
|
|
|
ASSERT(0);
|
|
return fElapsedTime;
|
|
#if 0
|
|
// This is a super hack, but it's only called from ScreenEdit, so it's OK.
|
|
// Writing an inverse function of GetBeatAndBPSFromElapsedTime() uber difficult,
|
|
// so do a binary search to get close to the correct elapsed time.
|
|
|
|
float fElapsedTimeBestGuess = this->m_fMusicLengthSeconds/2; // seconds
|
|
float fSecondsToMove = fElapsedTimeBestGuess; // seconds
|
|
float fBeatOut, fBPSOut;
|
|
bool bFreezeOut;
|
|
|
|
/* 0.001 gives higher precision and takes about 7 more iterations than
|
|
* 0.100. A 90-second song took about 9 iterations; now it takes about
|
|
* 16. -glenn */
|
|
while( fSecondsToMove > 0.001f )
|
|
{
|
|
GetBeatAndBPSFromElapsedTime( fElapsedTimeBestGuess, fBeatOut, fBPSOut, bFreezeOut );
|
|
/* If this is an exact match, we're done. However, if we're on a
|
|
* freeze segment, keep moving backwards until we're off it, so we
|
|
* return the time associated with the beginning of the freeze segment
|
|
* and not some random place in its middle. */
|
|
if( fBeatOut == fBeat && !bFreezeOut)
|
|
break;
|
|
|
|
if( fBeatOut >= fBeat )
|
|
fElapsedTimeBestGuess -= fSecondsToMove;
|
|
else
|
|
fElapsedTimeBestGuess += fSecondsToMove;
|
|
|
|
fSecondsToMove /= 2;
|
|
}
|
|
|
|
return fElapsedTimeBestGuess;
|
|
#endif
|
|
}
|
|
|
|
CString Song::GetCacheFilePath() const
|
|
{
|
|
return ssprintf( CACHE_DIR "Songs" SLASH "%u", GetHashForString(m_sSongDir) );
|
|
}
|
|
|
|
/* Get a path to the SM containing data for this song. It might
|
|
* be a cache file. */
|
|
const CString &Song::GetSongFilePath() const
|
|
{
|
|
ASSERT ( m_sSongFileName.GetLength() != 0 );
|
|
return m_sSongFileName;
|
|
}
|
|
|
|
NotesLoader *Song::MakeLoader( CString sDir ) const
|
|
{
|
|
NotesLoader *ret;
|
|
|
|
/* Actually, none of these have any persistant data, so we
|
|
* could optimize this, but since they don't have any data,
|
|
* there's no real point ... */
|
|
ret = new SMLoader;
|
|
if(ret->Loadable( sDir )) return ret;
|
|
delete ret;
|
|
|
|
ret = new DWILoader;
|
|
if(ret->Loadable( sDir )) return ret;
|
|
delete ret;
|
|
|
|
ret = new BMSLoader;
|
|
if(ret->Loadable( sDir )) return ret;
|
|
delete ret;
|
|
|
|
ret = new KSFLoader;
|
|
if(ret->Loadable( sDir )) return ret;
|
|
delete ret;
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/* Hack: This should be a parameter to TidyUpData, but I don't want to
|
|
* pull in <set> into Song.h, which is heavily used. */
|
|
static set<istring> BlacklistedImages;
|
|
|
|
bool Song::LoadWithoutCache( CString sDir )
|
|
{
|
|
//
|
|
// There was no entry in the cache for this song, or it was out of date.
|
|
// Let's load it from a file, then write a cache entry.
|
|
//
|
|
|
|
NotesLoader *ld = MakeLoader( sDir );
|
|
if(!ld)
|
|
{
|
|
LOG->Warn( "Couldn't find any SM, DWI, BMS, or KSF files in '%s'. This is not a valid song directory.", sDir.c_str() );
|
|
return false;
|
|
}
|
|
|
|
bool success = ld->LoadFromDir( sDir, *this );
|
|
BlacklistedImages = ld->GetBlacklistedImages();
|
|
delete ld;
|
|
|
|
if(!success)
|
|
return false;
|
|
|
|
TidyUpData();
|
|
|
|
// save a cache file so we don't have to parse it all over again next time
|
|
SaveToCacheFile();
|
|
return true;
|
|
}
|
|
|
|
bool Song::LoadFromSongDir( CString sDir )
|
|
{
|
|
// LOG->Trace( "Song::LoadFromSongDir(%s)", sDir.c_str() );
|
|
ASSERT( sDir != "" );
|
|
|
|
// make sure there is a trailing slash at the end of sDir
|
|
if( sDir.Right(1) != SLASH )
|
|
sDir += SLASH;
|
|
|
|
// save song dir
|
|
m_sSongDir = sDir;
|
|
|
|
// save group name
|
|
CStringArray sDirectoryParts;
|
|
split( m_sSongDir, SLASH, sDirectoryParts, false );
|
|
ASSERT( sDirectoryParts.size() >= 4 ); /* Songs/Slow/Taps/ */
|
|
m_sGroupName = sDirectoryParts[sDirectoryParts.size()-3]; // second from last item
|
|
ASSERT( m_sGroupName != "" );
|
|
|
|
//
|
|
// First look in the cache for this song (without loading NoteData)
|
|
//
|
|
unsigned uDirHash = SONGINDEX->GetCacheHash(m_sSongDir);
|
|
if( GetHashForDirectory(m_sSongDir) == uDirHash && // this cache is up to date
|
|
DoesFileExist(GetCacheFilePath()))
|
|
{
|
|
// LOG->Trace( "Loading '%s' from cache file '%s'.", m_sSongDir.c_str(), GetCacheFilePath().c_str() );
|
|
SMLoader ld;
|
|
ld.LoadFromSMFile( GetCacheFilePath(), *this, true );
|
|
}
|
|
else
|
|
{
|
|
if(!LoadWithoutCache(m_sSongDir))
|
|
return false;
|
|
}
|
|
|
|
/* Load the cached banners, if it's not loaded already. */
|
|
if( HasBanner() )
|
|
BANNERCACHE->LoadBanner( GetBannerPath() );
|
|
|
|
{
|
|
/* Generated filename; this doesn't always point to a loadable file,
|
|
* but instead points to the file we should write changed files to,
|
|
* and will always be an .SM.
|
|
*
|
|
* This is a little tricky. We can't always use the song title directly,
|
|
* since it might contain characters we can't store in filenames. Two
|
|
* easy options: we could manually filter out invalid characters, or we
|
|
* could use the name of the directory, which is always a valid filename
|
|
* and should always be the same as the song. The former might not catch
|
|
* everything--filename restrictions are platform-specific; we might even
|
|
* be on an 8.3 filesystem, so let's do the latter.
|
|
*
|
|
* We can't rely on searching for other data filenames; it works for DWIs,
|
|
* but not KSFs and BMSs.
|
|
*
|
|
* So, let's do this (by priority):
|
|
* 1. If there's an .SM file, use that filename. No reason to use anything
|
|
* else; it's the filename in use.
|
|
* 2. If there's a .DWI, use it with a changed extension.
|
|
* 3. Otherwise, use the name of the directory, since it's definitely a valid
|
|
* filename, and should always be the title of the song (unlike KSFs).
|
|
*/
|
|
m_sSongFileName = m_sSongDir;
|
|
CStringArray asFileNames;
|
|
GetDirListing( m_sSongDir+"*.sm", asFileNames );
|
|
if( !asFileNames.empty() )
|
|
m_sSongFileName += asFileNames[0];
|
|
else {
|
|
GetDirListing( m_sSongDir+"*.dwi", asFileNames );
|
|
if( !asFileNames.empty() ) {
|
|
m_sSongFileName += SetExtension( asFileNames[0], "sm" );
|
|
} else {
|
|
m_sSongFileName += sDirectoryParts[sDirectoryParts.size()-2]; // last item
|
|
m_sSongFileName += ".sm";
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Add AutoGen pointers. (These aren't cached.) */
|
|
AddAutoGenNotes();
|
|
|
|
if( !HasMusic() )
|
|
return false; // don't load this song
|
|
else
|
|
return true; // do load this song
|
|
}
|
|
|
|
void Song::RevertFromDisk()
|
|
{
|
|
unsigned CurNoteIndex[NUM_PLAYERS];
|
|
memset(CurNoteIndex, 0, sizeof(CurNoteIndex));
|
|
for( int pn = 0; pn < NUM_PLAYERS; ++pn )
|
|
for( unsigned n = 0; n < m_apNotes.size(); ++n )
|
|
if( m_apNotes[n] == GAMESTATE->m_pCurNotes[pn] )
|
|
CurNoteIndex[pn] = n;
|
|
|
|
const CString dir = GetSongDir();
|
|
|
|
/* Erase all existing data. */
|
|
Reset();
|
|
LoadFromSongDir( dir );
|
|
|
|
if( GAMESTATE->m_pCurSong == this )
|
|
{
|
|
/* m_pCurNotes points at notes that we just reloaded; fix it. */
|
|
for( int pn = 0; pn < NUM_PLAYERS; ++pn )
|
|
{
|
|
int ind = min(CurNoteIndex[pn], m_apNotes.size()-1);
|
|
GAMESTATE->m_pCurNotes[pn] = m_apNotes[ind];
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
static void GetImageDirListing( CString sPath, CStringArray &AddTo, bool bReturnPathToo=false )
|
|
{
|
|
GetDirListing( sPath + ".png", AddTo, false, bReturnPathToo );
|
|
GetDirListing( sPath + ".jpg", AddTo, false, bReturnPathToo );
|
|
GetDirListing( sPath + ".bmp", AddTo, false, bReturnPathToo );
|
|
GetDirListing( sPath + ".gif", AddTo, false, bReturnPathToo );
|
|
}
|
|
|
|
static CString RemoveInitialWhitespace( CString s )
|
|
{
|
|
unsigned i = s.find_first_not_of(" \t\r\n");
|
|
if( i != s.npos )
|
|
s.erase( 0, i );
|
|
return s;
|
|
}
|
|
|
|
/* This is called within TidyUpData, before autogen notes are added. */
|
|
static void DeleteDuplicateSteps( Song *song, vector<Steps*> &vSteps )
|
|
{
|
|
/* vSteps have the same StepsType and Difficulty. Delete them if they have the
|
|
* same m_sDescription, m_iMeter and SMNoteData. */
|
|
CHECKPOINT;
|
|
for( unsigned i=0; i<vSteps.size(); i++ )
|
|
{
|
|
CHECKPOINT;
|
|
const Steps *s1 = vSteps[i];
|
|
LOG->Trace("comparing %i (%p) ...", i, s1);
|
|
for( unsigned j=i+1; j<vSteps.size(); j++ )
|
|
{
|
|
CHECKPOINT;
|
|
const Steps *s2 = vSteps[j];
|
|
LOG->Trace(" with %i (%p) ...", j, s2);
|
|
if( s1->GetDescription() != s2->GetDescription() )
|
|
continue;
|
|
if( s1->GetMeter() != s2->GetMeter() )
|
|
continue;
|
|
/* Compare, ignoring whitespace. */
|
|
if( RemoveInitialWhitespace(s1->GetSMNoteData()) != RemoveInitialWhitespace(s2->GetSMNoteData()) )
|
|
continue;
|
|
|
|
LOG->Trace("Removed %p duplicate steps in song \"%s\" with description \"%s\" and meter \"%i\"",
|
|
s2, song->GetSongDir().c_str(), s1->GetDescription().c_str(), s1->GetMeter() );
|
|
|
|
/* Don't use RemoveNotes; autogen notes havn't yet been created and it'll
|
|
* create them. */
|
|
for( int k=song->m_apNotes.size()-1; k>=0; k-- )
|
|
{
|
|
if( song->m_apNotes[k] == s2 )
|
|
{
|
|
delete song->m_apNotes[k];
|
|
song->m_apNotes.erase( song->m_apNotes.begin()+k );
|
|
break;
|
|
}
|
|
}
|
|
|
|
vSteps.erase(vSteps.begin()+j);
|
|
--j;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/* Songs in BlacklistImages will never be autodetected as song images. */
|
|
void Song::TidyUpData()
|
|
{
|
|
if( !HasMusic() )
|
|
{
|
|
CStringArray arrayPossibleMusic;
|
|
GetDirListing( m_sSongDir + CString("*.mp3"), arrayPossibleMusic );
|
|
GetDirListing( m_sSongDir + CString("*.ogg"), arrayPossibleMusic );
|
|
GetDirListing( m_sSongDir + CString("*.wav"), arrayPossibleMusic );
|
|
|
|
if( !arrayPossibleMusic.empty() ) // we found a match
|
|
m_sMusicFile = arrayPossibleMusic[0];
|
|
// Don't throw on missing music. -Chris
|
|
// else
|
|
// RageException::Throw( "The song in '%s' is missing a music file. You must place a music file in the song folder or remove the song", m_sSongDir.c_str() );
|
|
}
|
|
|
|
/* This must be done before radar calculation. */
|
|
if( HasMusic() )
|
|
{
|
|
RageSound sound;
|
|
sound.Load( GetMusicPath(), false ); /* don't pre-cache */
|
|
|
|
m_fMusicLengthSeconds = sound.GetLengthSeconds();
|
|
|
|
if(m_fMusicLengthSeconds == -1)
|
|
{
|
|
/* It failed; bad file or something. It's already logged a warning,
|
|
* so just set the file to 0 seconds. */
|
|
m_fMusicLengthSeconds = 0;
|
|
} else if(m_fMusicLengthSeconds == 0) {
|
|
LOG->Warn("File %s is empty?", GetMusicPath().c_str());
|
|
}
|
|
}
|
|
else // ! HasMusic()
|
|
{
|
|
m_fMusicLengthSeconds = 100; // guess
|
|
LOG->Warn("This song has no music file; guessing at %f seconds", m_fMusicLengthSeconds);
|
|
}
|
|
|
|
if(m_fMusicLengthSeconds < 0)
|
|
{
|
|
LOG->Warn("File %i has negative length? (%i)", GetMusicPath().c_str(), m_fMusicLengthSeconds);
|
|
m_fMusicLengthSeconds = 0;
|
|
}
|
|
|
|
/* Generate these before we autogen notes, so the new notes can inherit
|
|
* their source's values. */
|
|
ReCalculateRadarValuesAndLastBeat();
|
|
|
|
TrimRight(m_sMainTitle);
|
|
if( m_sMainTitle == "" )
|
|
{
|
|
/* Use the song directory name. */
|
|
CString SongDir = this->GetSongDir();
|
|
vector<CString> parts;
|
|
split(SongDir, SLASH, parts);
|
|
ASSERT(parts.size() > 0);
|
|
|
|
NotesLoader::GetMainAndSubTitlesFromFullTitle( parts[parts.size()-1], m_sMainTitle, m_sSubTitle );
|
|
}
|
|
TrimRight(m_sSubTitle);
|
|
if( m_sArtist == "" ) m_sArtist = "Unknown artist";
|
|
TranslateTitles();
|
|
|
|
if( m_BPMSegments.empty() )
|
|
{
|
|
/* XXX: Once we have a way to display warnings that the user actually
|
|
* cares about (unlike most warnings), this should be one of them. */
|
|
LOG->Warn( "No BPM segments specified in '%s%s', default provided.",
|
|
m_sSongDir.c_str(), m_sSongFileName.c_str() );
|
|
|
|
AddBPMSegment( BPMSegment(0, 60) );
|
|
}
|
|
|
|
/* Only automatically set the sample time if there was no sample length
|
|
* (m_fMusicSampleStartSeconds == -1). */
|
|
/* We don't want to test if m_fMusicSampleStartSeconds == 0, since some
|
|
* people really do want the sample to start at the very beginning of the song. */
|
|
|
|
/* Having a sample start of 0 sounds terrible for most songs because because
|
|
* of the silence at the beginning. Many of my files have a 0 for the
|
|
* sample start that was not manually set, and I assume other people have the same.
|
|
* If there are complaints atou manually-set sample start at 0 is being ignored,
|
|
* then change this back. -Chris */
|
|
if( m_fMusicSampleStartSeconds == -1 ||
|
|
m_fMusicSampleStartSeconds == 0 ||
|
|
m_fMusicSampleStartSeconds+m_fMusicSampleLengthSeconds > this->m_fMusicLengthSeconds )
|
|
{
|
|
m_fMusicSampleStartSeconds = this->GetElapsedTimeFromBeat( 100 );
|
|
|
|
if( m_fMusicSampleStartSeconds+m_fMusicSampleLengthSeconds > this->m_fMusicLengthSeconds )
|
|
{
|
|
// fix for BAG and other slow songs
|
|
int iBeat = (int)(m_fLastBeat/2);
|
|
/* Er. I see that this truncates the beat down to a multiple
|
|
* of 10, but what's the logic behind doing that? (It'd make
|
|
* sense to use a multiple of 4, so we try to line up to a
|
|
* measure ...) -glenn */
|
|
iBeat = iBeat - iBeat%10;
|
|
m_fMusicSampleStartSeconds = this->GetElapsedTimeFromBeat( (float)iBeat );
|
|
}
|
|
}
|
|
|
|
|
|
/* Some DWIs have lengths in ms when they meant seconds, eg. #SAMPLELENGTH:10;.
|
|
* If the sample length is way too short, change it. */
|
|
if( m_fMusicSampleLengthSeconds < 3 || m_fMusicSampleLengthSeconds > 30 )
|
|
m_fMusicSampleLengthSeconds = DEFAULT_MUSIC_SAMPLE_LENGTH;
|
|
|
|
//
|
|
// Here's the problem: We have a directory full of images. We want to determine which
|
|
// image is the banner, which is the background, and which is the CDTitle.
|
|
//
|
|
|
|
LOG->Trace("Looking for images...");
|
|
|
|
/* Replace backslashes with slashes in all paths. */
|
|
FixSlashesInPlace( m_sMusicFile );
|
|
FixSlashesInPlace( m_sBannerFile );
|
|
FixSlashesInPlace( m_sBackgroundFile );
|
|
FixSlashesInPlace( m_sCDTitleFile );
|
|
FixSlashesInPlace( m_sLyricsFile );
|
|
|
|
/* Many imported files contain erroneous whitespace before or after
|
|
* filenames. Paths usually don't actually start or end with spaces,
|
|
* so let's just remove it. */
|
|
TrimLeft(m_sBannerFile);
|
|
TrimRight(m_sBannerFile);
|
|
TrimLeft(m_sBackgroundFile);
|
|
TrimRight(m_sBackgroundFile);
|
|
TrimLeft(m_sCDTitleFile);
|
|
TrimRight(m_sCDTitleFile);
|
|
TrimLeft(m_sLyricsFile);
|
|
TrimRight(m_sLyricsFile);
|
|
|
|
//
|
|
// First, check the file name for hints.
|
|
//
|
|
if( !HasBanner() )
|
|
{
|
|
// m_sBannerFile = "";
|
|
|
|
// find an image with "banner" in the file name
|
|
CStringArray arrayPossibleBanners;
|
|
GetImageDirListing( m_sSongDir + "*banner*", arrayPossibleBanners );
|
|
|
|
/* Some people do things differently for the sake of being different. Don't
|
|
* match eg. abnormal, numbness. */
|
|
GetImageDirListing( m_sSongDir + "* BN", arrayPossibleBanners );
|
|
|
|
if( !arrayPossibleBanners.empty() )
|
|
m_sBannerFile = arrayPossibleBanners[0];
|
|
}
|
|
|
|
if( !HasBackground() )
|
|
{
|
|
// m_sBackgroundFile = "";
|
|
|
|
// find an image with "bg" or "background" in the file name
|
|
CStringArray arrayPossibleBGs;
|
|
GetImageDirListing( m_sSongDir + "*bg*", arrayPossibleBGs );
|
|
GetImageDirListing( m_sSongDir + "*background*", arrayPossibleBGs );
|
|
if( !arrayPossibleBGs.empty() )
|
|
m_sBackgroundFile = arrayPossibleBGs[0];
|
|
}
|
|
|
|
if( !HasCDTitle() )
|
|
{
|
|
// find an image with "cdtitle" in the file name
|
|
CStringArray arrayPossibleCDTitles;
|
|
GetImageDirListing( m_sSongDir + "*cdtitle*", arrayPossibleCDTitles );
|
|
if( !arrayPossibleCDTitles.empty() )
|
|
m_sCDTitleFile = arrayPossibleCDTitles[0];
|
|
}
|
|
|
|
if( !HasLyrics() )
|
|
{
|
|
// Check if there is a lyric file in here
|
|
CStringArray arrayLyricFiles;
|
|
GetDirListing(m_sSongDir + CString("*.lrc"), arrayLyricFiles );
|
|
if( !arrayLyricFiles.empty() )
|
|
m_sLyricsFile = arrayLyricFiles[0];
|
|
}
|
|
|
|
|
|
|
|
//
|
|
// Now, For the images we still haven't found, look at the image dimensions of the remaining unclassified images.
|
|
//
|
|
CStringArray arrayImages;
|
|
GetImageDirListing( m_sSongDir + "*", arrayImages );
|
|
|
|
unsigned i;
|
|
for( i=0; i<arrayImages.size(); i++ ) // foreach image
|
|
{
|
|
// ignore DWI "-char" graphics
|
|
if( BlacklistedImages.find( arrayImages[i].c_str() ) != BlacklistedImages.end() )
|
|
continue; // skip
|
|
|
|
// Skip any image that we've already classified
|
|
|
|
if( HasBanner() && stricmp(m_sBannerFile, arrayImages[i])==0 )
|
|
continue; // skip
|
|
|
|
if( HasBackground() && stricmp(m_sBackgroundFile, arrayImages[i])==0 )
|
|
continue; // skip
|
|
|
|
if( HasCDTitle() && stricmp(m_sCDTitleFile, arrayImages[i])==0 )
|
|
continue; // skip
|
|
|
|
CString sPath = m_sSongDir + arrayImages[i];
|
|
SDL_Surface *img = IMG_Load( sPath );
|
|
if( !img )
|
|
{
|
|
LOG->Trace("Couldn't load '%s': %s", sPath.c_str(), SDL_GetError());
|
|
continue;
|
|
}
|
|
|
|
const int width = img->w;
|
|
const int height = img->h;
|
|
SDL_FreeSurface( img );
|
|
|
|
if( !HasBackground() && width >= 320 && height >= 240 )
|
|
{
|
|
m_sBackgroundFile = arrayImages[i];
|
|
continue;
|
|
}
|
|
|
|
if( !HasBanner() && Sprite::IsDiagonalBanner(width, height) )
|
|
{
|
|
m_sBannerFile = arrayImages[i];
|
|
continue;
|
|
}
|
|
|
|
if( !HasBanner() && 100<=width && width<=320 && 50<=height && height<=240 )
|
|
{
|
|
m_sBannerFile = arrayImages[i];
|
|
continue;
|
|
}
|
|
|
|
/* Some songs have overlarge banners. Check if the ratio is reasonable (over
|
|
* 2:1; usually over 3:1), and large (not a cdtitle). */
|
|
if( !HasBanner() && width > 200 && float(width) / height > 2.0f )
|
|
{
|
|
m_sBannerFile = arrayImages[i];
|
|
continue;
|
|
}
|
|
|
|
/* Agh. DWI's inline title images are triggering this, resulting in kanji,
|
|
* etc., being used as a CDTitle for songs with none. Some sample data
|
|
* from random incarnations:
|
|
* 42x50 35x50 50x50 144x49
|
|
* It looks like ~50 height is what people use to align to DWI's font.
|
|
*
|
|
* My tallest CDTitle is 44. Let's cut off in the middle and hope for
|
|
* the best. */
|
|
if( !HasCDTitle() && width<=100 && height<=48 )
|
|
{
|
|
m_sCDTitleFile = arrayImages[i];
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if( HasBanner() )
|
|
BANNERCACHE->CacheBanner( GetBannerPath() );
|
|
|
|
|
|
// If no BGChanges are specified and there are movies in the song directory, then assume
|
|
// they are DWI style where the movie begins at beat 0.
|
|
if( !HasBGChanges() )
|
|
{
|
|
CStringArray arrayPossibleMovies;
|
|
GetDirListing( m_sSongDir + CString("*movie*.avi"), arrayPossibleMovies );
|
|
GetDirListing( m_sSongDir + CString("*movie*.mpg"), arrayPossibleMovies );
|
|
GetDirListing( m_sSongDir + CString("*movie*.mpeg"), arrayPossibleMovies );
|
|
GetDirListing( m_sSongDir + CString("*.avi"), arrayPossibleMovies );
|
|
GetDirListing( m_sSongDir + CString("*.mpg"), arrayPossibleMovies );
|
|
GetDirListing( m_sSongDir + CString("*.mpeg"), arrayPossibleMovies );
|
|
/* Use this->GetBeatFromElapsedTime(0) instead of 0 to start when the
|
|
* music starts. */
|
|
if( arrayPossibleMovies.size() == 1 )
|
|
this->AddBackgroundChange( BackgroundChange(0,arrayPossibleMovies[0],1.f,true,true,false) );
|
|
}
|
|
|
|
|
|
/* Don't allow multiple Steps of the same StepsType and Diffiuclty.
|
|
* This happens a lot reading BMS files because they we have to guess
|
|
* the Difficulty from the meter. */
|
|
for( i=0; i<NUM_STEPS_TYPES; i++ )
|
|
{
|
|
StepsType st = (StepsType)i;
|
|
|
|
for( unsigned j=0; j<NUM_DIFFICULTIES; j++ )
|
|
{
|
|
Difficulty dc = (Difficulty)j;
|
|
|
|
vector<Steps*> vSteps;
|
|
this->GetSteps( vSteps, st, dc );
|
|
|
|
/* Delete steps that are completely identical. This happened due to a
|
|
* bug in an earlier version. */
|
|
DeleteDuplicateSteps( this, vSteps );
|
|
|
|
CHECKPOINT;
|
|
SortNotesArrayByDifficulty( vSteps );
|
|
CHECKPOINT;
|
|
for( unsigned k=1; k<vSteps.size(); k++ )
|
|
{
|
|
Steps* pSteps = vSteps[k];
|
|
|
|
Difficulty dc2 = min( (Difficulty)(dc+1), DIFFICULTY_CHALLENGE );
|
|
pSteps->SetDifficulty( dc2 );
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// Compress all Steps
|
|
for( i=0; i<m_apNotes.size(); i++ )
|
|
{
|
|
m_apNotes[i]->Compress();
|
|
}
|
|
}
|
|
|
|
void Song::TranslateTitles()
|
|
{
|
|
static TitleSubst tsub("songs");
|
|
|
|
tsub.Subst(m_sMainTitle, m_sSubTitle, m_sArtist,
|
|
m_sMainTitleTranslit, m_sSubTitleTranslit, m_sArtistTranslit);
|
|
}
|
|
|
|
void Song::ReCalculateRadarValuesAndLastBeat()
|
|
{
|
|
//
|
|
// calculate radar values and first/last beat
|
|
//
|
|
for( unsigned i=0; i<m_apNotes.size(); i++ )
|
|
{
|
|
Steps* pNotes = m_apNotes[i];
|
|
NoteData tempNoteData;
|
|
pNotes->GetNoteData( &tempNoteData );
|
|
|
|
for( int r=0; r<NUM_RADAR_CATEGORIES; r++ )
|
|
{
|
|
/* If it's autogen, radar vals come from the parent. */
|
|
if(pNotes->IsAutogen())
|
|
continue;
|
|
|
|
pNotes->SetRadarValue(r, NoteDataUtil::GetRadarValue( tempNoteData, (RadarCategory)r, m_fMusicLengthSeconds ));
|
|
}
|
|
|
|
/* Many songs have stray, empty song patterns. Ignore them, so
|
|
* they don't force the first beat of the whole song to 0. XXX Should
|
|
* we just delete them, now that new patterns can be created quickly
|
|
* in the editor? */
|
|
|
|
if(tempNoteData.GetLastRow() == 0)
|
|
continue;
|
|
|
|
float fFirstBeat = tempNoteData.GetFirstBeat();
|
|
float fLastBeat = tempNoteData.GetLastBeat();
|
|
if( m_fFirstBeat == -1 )
|
|
m_fFirstBeat = fFirstBeat;
|
|
else
|
|
m_fFirstBeat = min( m_fFirstBeat, fFirstBeat );
|
|
if( m_fLastBeat == -1 )
|
|
m_fLastBeat = fLastBeat;
|
|
else
|
|
m_fLastBeat = max( m_fLastBeat, fLastBeat );
|
|
}
|
|
}
|
|
|
|
void Song::GetSteps( vector<Steps*>& arrayAddTo, StepsType nt, Difficulty dc, int iMeterLow, int iMeterHigh, CString sDescription, bool bIncludeAutoGen ) const
|
|
{
|
|
for( unsigned i=0; i<m_apNotes.size(); i++ ) // for each of the Song's Steps
|
|
{
|
|
if( m_apNotes[i]->m_StepsType != nt ) continue;
|
|
if( dc != DIFFICULTY_INVALID && dc != m_apNotes[i]->GetDifficulty() )
|
|
continue;
|
|
if( iMeterLow != -1 && iMeterLow > m_apNotes[i]->GetMeter() )
|
|
continue;
|
|
if( iMeterHigh != -1 && iMeterHigh < m_apNotes[i]->GetMeter() )
|
|
continue;
|
|
if( sDescription != "" && sDescription != m_apNotes[i]->GetDescription() )
|
|
continue;
|
|
if( !bIncludeAutoGen && m_apNotes[i]->IsAutogen() )
|
|
continue;
|
|
arrayAddTo.push_back( m_apNotes[i] );
|
|
}
|
|
}
|
|
|
|
Steps* Song::GetStepsByDifficulty( StepsType nt, Difficulty dc, bool bIncludeAutoGen ) const
|
|
{
|
|
vector<Steps*> vNotes;
|
|
GetSteps( vNotes, nt, dc, -1, -1, "", bIncludeAutoGen );
|
|
if( vNotes.size() == 0 )
|
|
return NULL;
|
|
else
|
|
return vNotes[0];
|
|
}
|
|
|
|
Steps* Song::GetStepsByMeter( StepsType nt, int iMeterLow, int iMeterHigh, bool bIncludeAutoGen ) const
|
|
{
|
|
vector<Steps*> vNotes;
|
|
GetSteps( vNotes, nt, DIFFICULTY_INVALID, iMeterLow, iMeterHigh, "", bIncludeAutoGen );
|
|
if( vNotes.size() == 0 )
|
|
return NULL;
|
|
else
|
|
return vNotes[0];
|
|
}
|
|
|
|
Steps* Song::GetStepsByDescription( StepsType nt, CString sDescription, bool bIncludeAutoGen ) const
|
|
{
|
|
vector<Steps*> vNotes;
|
|
GetSteps( vNotes, nt, DIFFICULTY_INVALID, -1, -1, sDescription, bIncludeAutoGen );
|
|
if( vNotes.size() == 0 )
|
|
return NULL;
|
|
else
|
|
return vNotes[0];
|
|
}
|
|
|
|
|
|
Steps* Song::GetClosestNotes( StepsType nt, Difficulty dc, bool bIncludeAutoGen ) const
|
|
{
|
|
Difficulty newDC = dc;
|
|
Steps* pNotes;
|
|
pNotes = GetStepsByDifficulty( nt, newDC, bIncludeAutoGen );
|
|
if( pNotes )
|
|
return pNotes;
|
|
newDC = (Difficulty)(dc-1);
|
|
CLAMP( (int&)newDC, 0, NUM_DIFFICULTIES-1 );
|
|
pNotes = GetStepsByDifficulty( nt, newDC, bIncludeAutoGen );
|
|
if( pNotes )
|
|
return pNotes;
|
|
newDC = (Difficulty)(dc+1);
|
|
CLAMP( (int&)newDC, 0, NUM_DIFFICULTIES-1 );
|
|
pNotes = GetStepsByDifficulty( nt, newDC, bIncludeAutoGen );
|
|
if( pNotes )
|
|
return pNotes;
|
|
newDC = (Difficulty)(dc-2);
|
|
CLAMP( (int&)newDC, 0, NUM_DIFFICULTIES-1 );
|
|
pNotes = GetStepsByDifficulty( nt, newDC, bIncludeAutoGen );
|
|
if( pNotes )
|
|
return pNotes;
|
|
newDC = (Difficulty)(dc+2);
|
|
CLAMP( (int&)newDC, 0, NUM_DIFFICULTIES-1 );
|
|
pNotes = GetStepsByDifficulty( nt, newDC, bIncludeAutoGen );
|
|
return pNotes;
|
|
}
|
|
|
|
void Song::GetEdits( vector<Steps*>& arrayAddTo, StepsType nt, bool bIncludeAutoGen ) const
|
|
{
|
|
}
|
|
|
|
|
|
/* Return whether the song is playable in the given style. */
|
|
bool Song::SongCompleteForStyle( const StyleDef *st ) const
|
|
{
|
|
if(!SongHasNotesType(st->m_StepsType))
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
bool Song::SongHasNotesType( StepsType nt ) const
|
|
{
|
|
for( unsigned i=0; i < m_apNotes.size(); i++ ) // foreach Steps
|
|
if( m_apNotes[i]->m_StepsType == nt )
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
bool Song::SongHasNotesTypeAndDifficulty( StepsType nt, Difficulty dc ) const
|
|
{
|
|
for( unsigned i=0; i < m_apNotes.size(); i++ ) // foreach Steps
|
|
if( m_apNotes[i]->m_StepsType == nt && m_apNotes[i]->GetDifficulty() == dc )
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
void Song::SaveToCacheFile()
|
|
{
|
|
LOG->Trace( "Song::SaveToCacheFile()" );
|
|
|
|
SONGINDEX->AddCacheIndex(m_sSongDir, GetHashForDirectory(m_sSongDir));
|
|
SaveToSMFile( GetCacheFilePath(), true );
|
|
}
|
|
|
|
void Song::Save()
|
|
{
|
|
LOG->Trace( "Song::SaveToSongFile()" );
|
|
|
|
/* rename all old files to avoid confusion.
|
|
*
|
|
* This also serves as a backup, so rename .sm's, too. If we crash when
|
|
* saving the .sm, we don't want to lose what we had. But, what we really
|
|
* should be doing is saving to another file (eg. foo.sm.new), then once we
|
|
* know we havn't crashed, move the old .sm to .sm.old and the new one to
|
|
* the real filename. That way, if we crash, we don't leave the song in an
|
|
* unplayable state where the user has to manually un-rename stuff. XXX -glenn
|
|
*/
|
|
CStringArray arrayOldFileNames;
|
|
GetDirListing( m_sSongDir + "*.bms", arrayOldFileNames );
|
|
GetDirListing( m_sSongDir + "*.dwi", arrayOldFileNames );
|
|
GetDirListing( m_sSongDir + "*.ksf", arrayOldFileNames );
|
|
GetDirListing( m_sSongDir + "*.sm", arrayOldFileNames );
|
|
|
|
for( unsigned i=0; i<arrayOldFileNames.size(); i++ )
|
|
{
|
|
CString sOldPath = m_sSongDir + arrayOldFileNames[i];
|
|
CString sNewPath = sOldPath + ".old";
|
|
rename( sOldPath, sNewPath );
|
|
}
|
|
|
|
ReCalculateRadarValuesAndLastBeat();
|
|
TranslateTitles();
|
|
|
|
SaveToSMFile( GetSongFilePath(), false );
|
|
SaveToDWIFile();
|
|
}
|
|
|
|
|
|
void Song::SaveToSMFile( CString sPath, bool bSavingCache )
|
|
{
|
|
LOG->Trace( "Song::SaveToSMFile('%s')", sPath.c_str() );
|
|
|
|
NotesWriterSM wr;
|
|
wr.Write(sPath, *this, bSavingCache);
|
|
}
|
|
|
|
void Song::SaveToDWIFile()
|
|
{
|
|
LOG->Trace( "Song::SaveToSongFileAndDWI()" );
|
|
|
|
CString sPath = SetExtension( GetSongFilePath(), "dwi" );
|
|
|
|
NotesWriterDWI wr;
|
|
wr.Write(sPath, *this);
|
|
}
|
|
|
|
|
|
void Song::AddAutoGenNotes()
|
|
{
|
|
for( StepsType ntMissing=(StepsType)0; ntMissing<NUM_STEPS_TYPES; ntMissing=(StepsType)(ntMissing+1) )
|
|
{
|
|
if( SongHasNotesType(ntMissing) )
|
|
continue;
|
|
|
|
|
|
// missing Steps of this type
|
|
int iNumTracksOfMissing = GAMEMAN->NotesTypeToNumTracks(ntMissing);
|
|
|
|
// look for closest match
|
|
StepsType ntBestMatch = (StepsType)-1;
|
|
int iBestTrackDifference = 10000; // inf
|
|
|
|
for( StepsType nt=(StepsType)0; nt<NUM_STEPS_TYPES; nt=(StepsType)(nt+1) )
|
|
{
|
|
vector<Steps*> apNotes;
|
|
this->GetSteps( apNotes, nt );
|
|
|
|
if(apNotes.empty() || apNotes[0]->IsAutogen())
|
|
continue; /* can't autogen from other autogen */
|
|
|
|
int iNumTracks = GAMEMAN->NotesTypeToNumTracks(nt);
|
|
int iTrackDifference = abs(iNumTracks-iNumTracksOfMissing);
|
|
if( iTrackDifference < iBestTrackDifference )
|
|
{
|
|
ntBestMatch = nt;
|
|
iBestTrackDifference = iTrackDifference;
|
|
}
|
|
}
|
|
|
|
if( ntBestMatch != -1 )
|
|
AutoGen( ntMissing, ntBestMatch );
|
|
}
|
|
}
|
|
|
|
void Song::AutoGen( StepsType ntTo, StepsType ntFrom )
|
|
{
|
|
// int iNumTracksOfTo = GAMEMAN->NotesTypeToNumTracks(ntTo);
|
|
|
|
for( unsigned int j=0; j<m_apNotes.size(); j++ )
|
|
{
|
|
Steps* pOriginalNotes = m_apNotes[j];
|
|
if( pOriginalNotes->m_StepsType == ntFrom )
|
|
{
|
|
Steps* pNewNotes = new Steps;
|
|
pNewNotes->AutogenFrom(pOriginalNotes, ntTo);
|
|
this->m_apNotes.push_back( pNewNotes );
|
|
}
|
|
}
|
|
}
|
|
|
|
void Song::RemoveAutoGenNotes()
|
|
{
|
|
for( int j=m_apNotes.size()-1; j>=0; j-- )
|
|
{
|
|
if( m_apNotes[j]->IsAutogen() )
|
|
{
|
|
delete m_apNotes[j];
|
|
m_apNotes.erase( m_apNotes.begin()+j );
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
Grade Song::GetGradeForDifficulty( const StyleDef *st, MemoryCard card, Difficulty dc ) const
|
|
{
|
|
// return max grade of notes in difficulty class
|
|
vector<Steps*> aNotes;
|
|
this->GetSteps( aNotes, st->m_StepsType );
|
|
SortNotesArrayByDifficulty( aNotes );
|
|
|
|
Grade grade = GRADE_NO_DATA;
|
|
|
|
for( unsigned i=0; i<aNotes.size(); i++ )
|
|
{
|
|
const Steps* pNotes = aNotes[i];
|
|
if( pNotes->GetDifficulty() == dc )
|
|
grade = max( grade, pNotes->m_MemCardScores[card].grade );
|
|
}
|
|
return grade;
|
|
}
|
|
|
|
|
|
bool Song::IsNew() const
|
|
{
|
|
return GetNumTimesPlayed()==0;
|
|
}
|
|
|
|
bool Song::IsEasy( StepsType nt ) const
|
|
{
|
|
Steps* pBeginnerNotes = GetStepsByDifficulty( nt, DIFFICULTY_BEGINNER );
|
|
Steps* pEasyNotes = GetStepsByDifficulty( nt, DIFFICULTY_EASY );
|
|
Steps* pHardNotes = GetStepsByDifficulty( nt, DIFFICULTY_HARD );
|
|
|
|
// HACK: Looks bizarre to see the easy mark by Legend of MAX.
|
|
if( pHardNotes && pHardNotes->GetMeter() > 9 )
|
|
return false;
|
|
|
|
/* Originally, there was only Easy. Beginners found easy steps by
|
|
* going to the top of the default sort, since the default sort was
|
|
* by Easy meter.
|
|
*
|
|
* Now, there's Beginner. We can't very well sort by it; the meter is
|
|
* almost always 1 or 2. So, let's mark "easy" songs as those with any
|
|
* beginner steps, so beginners (playing on beginner) can be told to look
|
|
* for the easy icon, and other beginners (playing on light) can go by the
|
|
* sort like they have been. Don't make IsEasy some kind of mixture of
|
|
* beginner and easy, since that's too confusing and makes it useless to
|
|
* beginners.
|
|
*
|
|
* Hmm. I guess it does make sense to show as easy if it has 1-foot light
|
|
* steps, too: if the user selected Beginner, then 1-foot light is probably
|
|
* fine, and if the song has no beginner steps that's what he'll get. */
|
|
// return pBeginnerNotes != NULL;
|
|
|
|
if( pBeginnerNotes )
|
|
return true;
|
|
else if( pEasyNotes && pEasyNotes->GetMeter()==1 )
|
|
return true;
|
|
else
|
|
return false;
|
|
}
|
|
|
|
bool Song::HasEdits( StepsType nt ) const
|
|
{
|
|
vector<Steps*> vpNotes;
|
|
this->GetEdits( vpNotes, nt );
|
|
return vpNotes.size() > 0;
|
|
}
|
|
|
|
/////////////////////////////////////
|
|
// Sorting
|
|
/////////////////////////////////////
|
|
|
|
CString MakeSortString( CString s )
|
|
{
|
|
s.MakeUpper();
|
|
|
|
// Make sure that non-alphanumeric characters are placed at the very end
|
|
if( s.size()>0 )
|
|
{
|
|
if( s[0] == '.' ) // ".59"
|
|
s.erase(s.begin());
|
|
if( s[0] < 'A' || s[0] > 'Z' )
|
|
if( s[0] < '0' && s[0] > '9' )
|
|
s = char(126) + s; // prepend with a high ASCII character
|
|
}
|
|
|
|
return s;
|
|
}
|
|
|
|
bool CompareSongPointersByTitle(const Song *pSong1, const Song *pSong2)
|
|
{
|
|
// Prefer transliterations to full titles
|
|
CString s1 = pSong1->GetTranslitMainTitle();
|
|
CString s2 = pSong2->GetTranslitMainTitle();
|
|
if( s1 == s2 )
|
|
{
|
|
s1 = pSong1->GetTranslitSubTitle();
|
|
s2 = pSong2->GetTranslitSubTitle();
|
|
}
|
|
|
|
s1 = MakeSortString(s1);
|
|
s2 = MakeSortString(s2);
|
|
|
|
int ret = s1.CompareNoCase( s2 );
|
|
if(ret < 0) return true;
|
|
if(ret > 0) return false;
|
|
|
|
/* The titles are the same. Ensure we get a consistent ordering
|
|
* by comparing the unique SongFilePaths. */
|
|
return pSong1->GetSongFilePath().CompareNoCase(pSong2->GetSongFilePath()) < 0;
|
|
}
|
|
|
|
void SortSongPointerArrayByTitle( vector<Song*> &arraySongPointers )
|
|
{
|
|
sort( arraySongPointers.begin(), arraySongPointers.end(), CompareSongPointersByTitle );
|
|
}
|
|
|
|
static int GetSongSortDifficulty(const Song *pSong)
|
|
{
|
|
vector<Steps*> aNotes;
|
|
pSong->GetSteps( aNotes, GAMESTATE->GetCurrentStyleDef()->m_StepsType );
|
|
|
|
// sort by anything but beginner
|
|
/* This makes the sort a little odd; songs can end up in unintuitive places because,
|
|
* for example, of a HARD difficulty with an incorrectly-set meter of 1, or because
|
|
* of challenge steps. */
|
|
/*
|
|
int iEasiestMeter = 1000; // infinity
|
|
for( unsigned i=0; i<aNotes.size(); i++ )
|
|
{
|
|
if(aNotes[i]->GetDifficulty() == DIFFICULTY_BEGINNER)
|
|
continue;
|
|
|
|
iEasiestMeter = min( iEasiestMeter, aNotes[i]->GetMeter() );
|
|
}
|
|
// odd case where there are only beginner steps... what to do? should probably
|
|
// act just like it was a 1-footer, even if the beginner steps aren't
|
|
if( iEasiestMeter1 == 1000 )
|
|
iEasiestMeter1 = 1;
|
|
if( iEasiestMeter2 == 1000 )
|
|
iEasiestMeter2 = 1;
|
|
*/
|
|
|
|
/* Instead, always sort by the first difficulty found in the following order: */
|
|
const Difficulty d[] = { DIFFICULTY_EASY, DIFFICULTY_MEDIUM, DIFFICULTY_HARD,
|
|
DIFFICULTY_CHALLENGE, DIFFICULTY_INVALID };
|
|
|
|
for(int i = 0; d[i] != DIFFICULTY_INVALID; ++i)
|
|
{
|
|
for( unsigned j = 0; j < aNotes.size(); ++j)
|
|
{
|
|
if(aNotes[j]->GetDifficulty() != d[i])
|
|
continue;
|
|
|
|
return aNotes[j]->GetMeter();
|
|
}
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
int CompareSongPointersByDifficulty(const Song *pSong1, const Song *pSong2)
|
|
{
|
|
int iEasiestMeter1 = GetSongSortDifficulty(pSong1);
|
|
int iEasiestMeter2 = GetSongSortDifficulty(pSong2);
|
|
|
|
if( iEasiestMeter1 < iEasiestMeter2 )
|
|
return true;
|
|
if( iEasiestMeter1 > iEasiestMeter2 )
|
|
return false;
|
|
return CompareSongPointersByTitle( pSong1, pSong2 );
|
|
}
|
|
|
|
void SortSongPointerArrayByDifficulty( vector<Song*> &arraySongPointers )
|
|
{
|
|
sort( arraySongPointers.begin(), arraySongPointers.end(), CompareSongPointersByDifficulty );
|
|
}
|
|
|
|
bool CompareSongPointersByBPM(const Song *pSong1, const Song *pSong2)
|
|
{
|
|
float fMinBPM1, fMaxBPM1, fMinBPM2, fMaxBPM2;
|
|
pSong1->GetDisplayBPM( fMinBPM1, fMaxBPM1 );
|
|
pSong2->GetDisplayBPM( fMinBPM2, fMaxBPM2 );
|
|
|
|
if( fMaxBPM1 < fMaxBPM2 )
|
|
return true;
|
|
if( fMaxBPM1 > fMaxBPM2 )
|
|
return false;
|
|
|
|
return CompareCStringsAsc( pSong1->GetSongFilePath(), pSong2->GetSongFilePath() );
|
|
}
|
|
|
|
void SortSongPointerArrayByBPM( vector<Song*> &arraySongPointers )
|
|
{
|
|
sort( arraySongPointers.begin(), arraySongPointers.end(), CompareSongPointersByBPM );
|
|
}
|
|
|
|
|
|
bool CompareSongPointersByGrade(const Song *pSong1, const Song *pSong2)
|
|
{
|
|
for( int i=NUM_GRADES; i>GRADE_NO_DATA; i-- )
|
|
{
|
|
Grade g = (Grade)i;
|
|
int iCount1 = pSong1->GetNumNotesWithGrade( g );
|
|
int iCount2 = pSong2->GetNumNotesWithGrade( g );
|
|
|
|
if( iCount1 > iCount2 )
|
|
return true;
|
|
if( iCount1 < iCount2 )
|
|
return false;
|
|
}
|
|
|
|
return CompareSongPointersByTitle( pSong1, pSong2 );
|
|
}
|
|
|
|
void SortSongPointerArrayByGrade( vector<Song*> &arraySongPointers )
|
|
{
|
|
sort( arraySongPointers.begin(), arraySongPointers.end(), CompareSongPointersByGrade );
|
|
}
|
|
|
|
|
|
int CompareSongPointersByArtist(const Song *pSong1, const Song *pSong2)
|
|
{
|
|
CString s1 = pSong1->GetTranslitArtist();
|
|
CString s2 = pSong2->GetTranslitArtist();
|
|
|
|
s1 = MakeSortString(s1);
|
|
s2 = MakeSortString(s2);
|
|
|
|
if( s1 < s2 )
|
|
return true;
|
|
if( s1 > s2 )
|
|
return false;
|
|
return CompareSongPointersByTitle( pSong1, pSong2 );
|
|
}
|
|
|
|
void SortSongPointerArrayByArtist( vector<Song*> &arraySongPointers )
|
|
{
|
|
sort( arraySongPointers.begin(), arraySongPointers.end(), CompareSongPointersByArtist );
|
|
}
|
|
|
|
int CompareSongPointersByGroupAndDifficulty(const Song *pSong1, const Song *pSong2)
|
|
{
|
|
const CString &sGroup1 = pSong1->m_sGroupName;
|
|
const CString &sGroup2 = pSong2->m_sGroupName;
|
|
|
|
if( sGroup1 < sGroup2 )
|
|
return true;
|
|
if( sGroup1 > sGroup2 )
|
|
return false;
|
|
|
|
/* Same group; compare by difficulty. */
|
|
return CompareSongPointersByDifficulty( pSong1, pSong2 );
|
|
}
|
|
|
|
void SortSongPointerArrayByGroupAndDifficulty( vector<Song*> &arraySongPointers )
|
|
{
|
|
sort( arraySongPointers.begin(), arraySongPointers.end(), CompareSongPointersByGroupAndDifficulty );
|
|
}
|
|
|
|
int CompareSongPointersByGroupAndTitle(const Song *pSong1, const Song *pSong2)
|
|
{
|
|
const CString &sGroup1 = pSong1->m_sGroupName;
|
|
const CString &sGroup2 = pSong2->m_sGroupName;
|
|
|
|
if( sGroup1 < sGroup2 )
|
|
return true;
|
|
if( sGroup1 > sGroup2 )
|
|
return false;
|
|
|
|
/* Same group; compare by name. */
|
|
return CompareSongPointersByTitle( pSong1, pSong2 );
|
|
}
|
|
|
|
void SortSongPointerArrayByGroupAndTitle( vector<Song*> &arraySongPointers )
|
|
{
|
|
sort( arraySongPointers.begin(), arraySongPointers.end(), CompareSongPointersByGroupAndTitle );
|
|
}
|
|
|
|
bool CompareSongPointersByMostPlayed(const Song *pSong1, const Song *pSong2)
|
|
{
|
|
return pSong1->GetNumTimesPlayed() < pSong2->GetNumTimesPlayed();
|
|
/*
|
|
Comparing titles is slow, and this makes course selection choppy. Turning this
|
|
off means we don't get consistent orderings of songs that have been played
|
|
the same amount, but that shouldn't be a big problem; the songs should stay
|
|
in the same original order during a single load of the game anyway (and will
|
|
probably stay in the same general order even across loads). The number of
|
|
plays is more likely to change.
|
|
|
|
This is mostly a problem with new games, where there are a large number of songs
|
|
that have all been played 0 times.
|
|
|
|
int iNumTimesPlayed1 = pSong1->GetNumTimesPlayed();
|
|
int iNumTimesPlayed2 = pSong2->GetNumTimesPlayed();
|
|
|
|
if( iNumTimesPlayed1 > iNumTimesPlayed2 )
|
|
return true;
|
|
if( iNumTimesPlayed1 < iNumTimesPlayed2 )
|
|
return false;
|
|
return CompareSongPointersByTitle( pSong1, pSong2 );
|
|
*/
|
|
}
|
|
/* Actually, just calculating GetNumTimesPlayed within the sort is pretty
|
|
* slow, so let's precompute it. (This could be generalized with a template.) */
|
|
map<const Song*, CString> song_sort_val;
|
|
|
|
bool CompareSongPointersBySortVal(const Song *pSong1, const Song *pSong2)
|
|
{
|
|
return song_sort_val[pSong1] < song_sort_val[pSong2];
|
|
}
|
|
|
|
void SortSongPointerArrayByMostPlayed( vector<Song*> &arraySongPointers )
|
|
{
|
|
for(unsigned i = 0; i < arraySongPointers.size(); ++i)
|
|
song_sort_val[arraySongPointers[i]] = ssprintf("%9i", arraySongPointers[i]->GetNumTimesPlayed());
|
|
stable_sort( arraySongPointers.begin(), arraySongPointers.end(), CompareSongPointersBySortVal );
|
|
reverse( arraySongPointers.begin(), arraySongPointers.end() );
|
|
song_sort_val.clear();
|
|
}
|
|
|
|
struct CompareSongByMeter
|
|
{
|
|
Difficulty dc;
|
|
|
|
CompareSongByMeter(Difficulty d): dc(d) { }
|
|
bool operator() (const Song* pSong1, const Song* pSong2)
|
|
{
|
|
Steps* pNotes1 = pSong1->GetStepsByDifficulty( GAMESTATE->GetCurrentStyleDef()->m_StepsType, dc );
|
|
Steps* pNotes2 = pSong2->GetStepsByDifficulty( GAMESTATE->GetCurrentStyleDef()->m_StepsType, dc );
|
|
|
|
const int iMeter1 = pNotes1 ? pNotes1->GetMeter() : 0;
|
|
const int iMeter2 = pNotes2 ? pNotes2->GetMeter() : 0;
|
|
|
|
if( iMeter1 < iMeter2 )
|
|
return true;
|
|
if( iMeter1 > iMeter2 )
|
|
return false;
|
|
return CompareSongPointersByTitle( pSong1, pSong2 );
|
|
}
|
|
};
|
|
|
|
void SortSongPointerArrayByMeter( vector<Song*> &arraySongPointers, Difficulty dc )
|
|
{
|
|
sort( arraySongPointers.begin(), arraySongPointers.end(), CompareSongByMeter(dc) );
|
|
}
|
|
|
|
bool Song::NormallyDisplayed() const
|
|
{
|
|
if(!PREFSMAN->m_bHiddenSongs) return true;
|
|
return m_SelectionDisplay == SHOW_ALWAYS;
|
|
}
|
|
|
|
bool Song::RouletteDisplayed() const
|
|
{
|
|
if(!PREFSMAN->m_bHiddenSongs) return true;
|
|
return m_SelectionDisplay != SHOW_NEVER;
|
|
}
|
|
|
|
bool Song::HasMusic() const {return m_sMusicFile != "" && IsAFile(GetMusicPath()); }
|
|
bool Song::HasBanner() const {return m_sBannerFile != "" && IsAFile(GetBannerPath()); }
|
|
bool Song::HasLyrics() const {return m_sLyricsFile != "" && IsAFile(GetLyricsPath()); }
|
|
bool Song::HasBackground() const {return m_sBackgroundFile != "" && IsAFile(GetBackgroundPath()); }
|
|
bool Song::HasCDTitle() const {return m_sCDTitleFile != "" && IsAFile(GetCDTitlePath()); }
|
|
bool Song::HasBGChanges() const {return !m_BackgroundChanges.empty(); }
|
|
|
|
int Song::GetNumTimesPlayed() const
|
|
{
|
|
int iTotalNumTimesPlayed = 0;
|
|
for( unsigned i=0; i<m_apNotes.size(); i++ )
|
|
{
|
|
iTotalNumTimesPlayed += m_apNotes[i]->m_MemCardScores[MEMORY_CARD_MACHINE].iNumTimesPlayed;
|
|
}
|
|
return iTotalNumTimesPlayed;
|
|
}
|
|
|
|
/* Search semantics for all song files:
|
|
*
|
|
* If the path doesn't have any directory separators, it's a filename in the
|
|
* song directory.
|
|
*
|
|
* If it does, it's relative to the top directory of the tree it was loaded
|
|
* from, eg ".\music\stuff\song.ogg". Most of the time, that's the SM tree,
|
|
* and that's the PWD, so just return it directly. If the file was originally
|
|
* loaded from the DWIPath, it's relative to that, so prepend it.
|
|
*
|
|
* Some DWI's do have relative paths that don't start with ".\".
|
|
*/
|
|
|
|
/* We only follow this for song files and cdtitles; it's for compatibility
|
|
* with DWI. We prefer paths relative to the song directory; only support
|
|
* that for all other paths. */
|
|
|
|
/* Note: Prepending the dwipath is ugly. The first impression might be to
|
|
* add a Song::TopFilePath, but don't do that--we have too much stuff in there
|
|
* already. We don't really need it; this is the only place it'd be used. What
|
|
* we *should* be doing is prepending this path when we first load the song.
|
|
* However, dwipaths are usually like "c:\games\dwi", and we can't store colons
|
|
* in SM's; they'll get interpreted as delimiters. So:
|
|
* XXX: Add some kind of escape character to SM's.
|
|
*
|
|
* -glenn */
|
|
|
|
CString Song::GetMusicPath() const
|
|
{
|
|
/* If there's no path in the music file, the file is in the same directory
|
|
* as the song. (This is the preferred configuration.) */
|
|
if( m_sMusicFile.Find('/') == -1)
|
|
return m_sSongDir+m_sMusicFile;
|
|
|
|
/* The file has a path. If it was loaded from the m_DWIPath, it's relative
|
|
* to that. */
|
|
if( PREFSMAN->m_DWIPath!="" && m_sSongDir.Left(PREFSMAN->m_DWIPath.GetLength()) == PREFSMAN->m_DWIPath )
|
|
return PREFSMAN->m_DWIPath+SLASH+m_sMusicFile;
|
|
|
|
/* Otherwise, it's relative to the top of the SM directory (the CWD), so
|
|
* return it directly. */
|
|
return m_sMusicFile;
|
|
}
|
|
|
|
CString Song::GetBannerPath() const
|
|
{
|
|
if( m_sBannerFile == "" )
|
|
return "";
|
|
return m_sSongDir+m_sBannerFile;
|
|
}
|
|
|
|
CString Song::GetLyricsPath() const
|
|
{
|
|
if( m_sLyricsFile == "" )
|
|
return "";
|
|
return m_sSongDir+m_sLyricsFile;
|
|
}
|
|
|
|
CString Song::GetCDTitlePath() const
|
|
{
|
|
if( m_sCDTitleFile.Find('/') == -1)
|
|
return m_sSongDir+m_sCDTitleFile;
|
|
|
|
if( PREFSMAN->m_DWIPath!="" && m_sSongDir.Left(PREFSMAN->m_DWIPath.GetLength()) == PREFSMAN->m_DWIPath )
|
|
return PREFSMAN->m_DWIPath+SLASH+m_sCDTitleFile;
|
|
|
|
return m_sCDTitleFile;
|
|
}
|
|
|
|
CString Song::GetBackgroundPath() const
|
|
{
|
|
return m_sSongDir+m_sBackgroundFile;
|
|
}
|
|
|
|
CString Song::GetDisplayMainTitle() const
|
|
{
|
|
if(!PREFSMAN->m_bShowNative) return GetTranslitMainTitle();
|
|
return m_sMainTitle;
|
|
}
|
|
|
|
CString Song::GetDisplaySubTitle() const
|
|
{
|
|
if(!PREFSMAN->m_bShowNative) return GetTranslitSubTitle();
|
|
return m_sSubTitle;
|
|
}
|
|
|
|
CString Song::GetDisplayArtist() const
|
|
{
|
|
if(!PREFSMAN->m_bShowNative) return GetTranslitArtist();
|
|
return m_sArtist;
|
|
}
|
|
|
|
|
|
CString Song::GetFullDisplayTitle() const
|
|
{
|
|
CString Title = GetDisplayMainTitle();
|
|
CString SubTitle = GetDisplaySubTitle();
|
|
|
|
if(!SubTitle.empty()) Title += " " + SubTitle;
|
|
return Title;
|
|
}
|
|
|
|
CString Song::GetFullTranslitTitle() const
|
|
{
|
|
CString Title = GetTranslitMainTitle();
|
|
CString SubTitle = GetTranslitSubTitle();
|
|
|
|
if(!SubTitle.empty()) Title += " " + SubTitle;
|
|
return Title;
|
|
}
|
|
|
|
void Song::AddNotes( Steps* pNotes )
|
|
{
|
|
m_apNotes.push_back( pNotes );
|
|
}
|
|
|
|
void Song::RemoveNotes( const Steps* pNotes )
|
|
{
|
|
// Avoid any stale Note::parent pointers by removing all AutoGen'd Steps,
|
|
// then adding them again.
|
|
|
|
RemoveAutoGenNotes();
|
|
|
|
for( int j=m_apNotes.size()-1; j>=0; j-- )
|
|
{
|
|
if( m_apNotes[j] == pNotes )
|
|
{
|
|
delete m_apNotes[j];
|
|
m_apNotes.erase( m_apNotes.begin()+j );
|
|
break;
|
|
}
|
|
}
|
|
|
|
AddAutoGenNotes();
|
|
}
|
|
|
|
int Song::GetNumNotesWithGrade( Grade g ) const
|
|
{
|
|
int iCount = 0;
|
|
vector<Steps*> vNotes;
|
|
this->GetSteps( vNotes, GAMESTATE->GetCurrentStyleDef()->m_StepsType );
|
|
for( unsigned j=0; j<vNotes.size(); j++ )
|
|
if( vNotes[j]->m_MemCardScores[MEMORY_CARD_MACHINE].grade == g )
|
|
iCount++;
|
|
return iCount;
|
|
}
|
|
|
|
bool Song::Matches(CString sGroup, CString sSong) const
|
|
{
|
|
if( sGroup.size() && sGroup.CompareNoCase(this->m_sGroupName) != 0)
|
|
return false;
|
|
|
|
CString sDir = this->GetSongDir();
|
|
sDir.Replace("\\","/");
|
|
CStringArray bits;
|
|
split( sDir, "/", bits );
|
|
ASSERT(bits.size() >= 2); /* should always have at least two parts */
|
|
const CString &sLastBit = bits[bits.size()-1];
|
|
|
|
// match on song dir or title (ala DWI)
|
|
if( !sSong.CompareNoCase(sLastBit) )
|
|
return true;
|
|
if( !sSong.CompareNoCase(this->GetFullTranslitTitle()) )
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|