#include "global.h" #include "SongUtil.h" #include "song.h" #include "Steps.h" #include "GameState.h" #include "Style.h" #include "ProfileManager.h" #include "Profile.h" #include "PrefsManager.h" #include "SongManager.h" #include "XmlFile.h" #include "Foreach.h" #include "UnlockManager.h" #include "ThemeMetric.h" #include "LocalizedString.h" #include "RageLog.h" bool SongCriteria::Matches( const Song *pSong ) const { if( !m_sGroupName.empty() && m_sGroupName != pSong->m_sGroupName ) return false; if( m_bUseSongGenreAllowedList ) { if( find(m_vsSongGenreAllowedList.begin(),m_vsSongGenreAllowedList.end(),pSong->m_sGenre) == m_vsSongGenreAllowedList.end() ) return false; } switch( m_Selectable ) { DEFAULT_FAIL(m_Selectable); case Selectable_Yes: if( pSong->m_SelectionDisplay != Song::SHOW_ALWAYS ) return false; break; case Selectable_No: if( pSong->m_SelectionDisplay != Song::SHOW_NEVER ) return false; break; case Selectable_DontCare: break; } if( m_bUseSongAllowedList ) { if( find(m_vpSongAllowedList.begin(),m_vpSongAllowedList.end(),pSong) == m_vpSongAllowedList.end() ) return false; } if( m_iStagesForSong != -1 && SONGMAN->GetNumStagesForSong(pSong) != m_iStagesForSong ) return false; switch( m_Tutorial ) { DEFAULT_FAIL(m_Tutorial); case Tutorial_Yes: if( !pSong->IsTutorial() ) return false; break; case Tutorial_No: if( pSong->IsTutorial() ) return false; break; case Tutorial_DontCare: break; } switch( m_Locked ) { DEFAULT_FAIL(m_Locked); case Locked_Locked: if( UNLOCKMAN && !UNLOCKMAN->SongIsLocked(pSong) ) return false; break; case Locked_Unlocked: if( UNLOCKMAN && UNLOCKMAN->SongIsLocked(pSong) ) return false; break; case Locked_DontCare: break; } return true; } void SongUtil::GetSteps( const Song *pSong, vector& arrayAddTo, StepsType st, Difficulty dc, int iMeterLow, int iMeterHigh, const RString &sDescription, bool bIncludeAutoGen, unsigned uHash, int iMaxToGet ) { if( !iMaxToGet ) return; const vector &vpSteps = st == STEPS_TYPE_INVALID ? pSong->GetAllSteps() : pSong->GetStepsByStepsType(st); for( unsigned i=0; iGetDifficulty() ) continue; if( iMeterLow != -1 && iMeterLow > pSteps->GetMeter() ) continue; if( iMeterHigh != -1 && iMeterHigh < pSteps->GetMeter() ) continue; if( sDescription.size() && sDescription != pSteps->GetDescription() ) continue; if( uHash != 0 && uHash != pSteps->GetHash() ) continue; if( !bIncludeAutoGen && pSteps->IsAutogen() ) continue; arrayAddTo.push_back( pSteps ); if( iMaxToGet != -1 ) { --iMaxToGet; if( !iMaxToGet ) break; } } } Steps* SongUtil::GetOneSteps( const Song *pSong, StepsType st, Difficulty dc, int iMeterLow, int iMeterHigh, const RString &sDescription, unsigned uHash, bool bIncludeAutoGen ) { vector vpSteps; GetSteps( pSong, vpSteps, st, dc, iMeterLow, iMeterHigh, sDescription, bIncludeAutoGen, uHash, 1 ); // get max 1 if( vpSteps.empty() ) return NULL; else return vpSteps[0]; } Steps* SongUtil::GetStepsByDifficulty( const Song *pSong, StepsType st, Difficulty dc, bool bIncludeAutoGen ) { const vector& vpSteps = (st == STEPS_TYPE_INVALID)? pSong->GetAllSteps() : pSong->GetStepsByStepsType(st); for( unsigned i=0; iGetDifficulty() ) continue; if( !bIncludeAutoGen && pSteps->IsAutogen() ) continue; return pSteps; } return NULL; } Steps* SongUtil::GetStepsByMeter( const Song *pSong, StepsType st, int iMeterLow, int iMeterHigh ) { const vector& vpSteps = (st == STEPS_TYPE_INVALID)? pSong->GetAllSteps() : pSong->GetStepsByStepsType(st); for( unsigned i=0; i pSteps->GetMeter() ) continue; if( iMeterHigh < pSteps->GetMeter() ) continue; return pSteps; } return NULL; } Steps* SongUtil::GetStepsByDescription( const Song *pSong, StepsType st, RString sDescription ) { vector vNotes; GetSteps( pSong, vNotes, st, DIFFICULTY_INVALID, -1, -1, sDescription ); if( vNotes.size() == 0 ) return NULL; else return vNotes[0]; } Steps* SongUtil::GetClosestNotes( const Song *pSong, StepsType st, Difficulty dc, bool bIgnoreLocked ) { ASSERT( dc != DIFFICULTY_INVALID ); const vector& vpSteps = (st == STEPS_TYPE_INVALID)? pSong->GetAllSteps() : pSong->GetStepsByStepsType(st); Steps *pClosest = NULL; int iClosestDistance = 999; for( unsigned i=0; iGetDifficulty() == DIFFICULTY_EDIT && dc != DIFFICULTY_EDIT ) continue; if( bIgnoreLocked && UNLOCKMAN->StepsIsLocked(pSong,pSteps) ) continue; int iDistance = abs(dc - pSteps->GetDifficulty()); if( iDistance < iClosestDistance ) { pClosest = pSteps; iClosestDistance = iDistance; } } return pClosest; } /* Make any duplicate difficulties edits. (Note that BMS files do a first pass * on this; see BMSLoader::SlideDuplicateDifficulties.) */ void SongUtil::AdjustDuplicateSteps( Song *pSong ) { FOREACH_StepsType( st ) { FOREACH_Difficulty( dc ) { if( dc == DIFFICULTY_EDIT ) continue; vector vSteps; SongUtil::GetSteps( pSong, vSteps, st, dc ); /* Delete steps that are completely identical. This happened due to a * bug in an earlier version. */ DeleteDuplicateSteps( pSong, vSteps ); CHECKPOINT; StepsUtil::SortNotesArrayByDifficulty( vSteps ); CHECKPOINT; for( unsigned k=1; kSetDifficulty( DIFFICULTY_EDIT ); if( vSteps[k]->GetDescription() == "" ) { /* "Hard Edit" */ RString EditName = Capitalize( DifficultyToString(dc) ) + " Edit"; vSteps[k]->SetDescription( EditName ); } } } /* XXX: Don't allow edits to have descriptions that look like regular difficulties. * These are confusing, and they're ambiguous when passed to GetStepsByID. */ } } static RString RemoveInitialWhitespace( RString s ) { size_t 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. */ void SongUtil::DeleteDuplicateSteps( Song *pSong, vector &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; iGetDescription() != s2->GetDescription() ) continue; if( s1->GetMeter() != s2->GetMeter() ) continue; /* Compare, ignoring whitespace. */ RString sSMNoteData1; s1->GetSMNoteData( sSMNoteData1 ); RString sSMNoteData2; s2->GetSMNoteData( sSMNoteData2 ); if( RemoveInitialWhitespace(sSMNoteData1) != RemoveInitialWhitespace(sSMNoteData2) ) continue; LOG->Trace("Removed %p duplicate steps in song \"%s\" with description \"%s\" and meter \"%i\"", s2, pSong->GetSongDir().c_str(), s1->GetDescription().c_str(), s1->GetMeter() ); pSong->DeleteSteps( s2, false ); vSteps.erase(vSteps.begin()+j); --j; } } } ///////////////////////////////////// // Sorting ///////////////////////////////////// static LocalizedString SORT_NOT_AVAILABLE( "Sort", "NotAvailable" ); static LocalizedString SORT_OTHER ( "Sort", "Other" ); /* Just calculating GetNumTimesPlayed within the sort is pretty slow, so let's precompute * it. (This could be generalized with a template.) */ static map g_mapSongSortVal; static bool CompareSongPointersBySortValueAscending( const Song *pSong1, const Song *pSong2 ) { return g_mapSongSortVal[pSong1] < g_mapSongSortVal[pSong2]; } static bool CompareSongPointersBySortValueDescending( const Song *pSong1, const Song *pSong2 ) { return g_mapSongSortVal[pSong1] > g_mapSongSortVal[pSong2]; } RString SongUtil::MakeSortString( RString s ) { s.MakeUpper(); // Make sure that non-alphanumeric strings 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') && (s[0] < '0' || s[0] > '9') ) s = char(126) + s; } return s; } static bool CompareSongPointersByTitle( const Song *pSong1, const Song *pSong2 ) { // Prefer transliterations to full titles RString s1 = pSong1->GetTranslitMainTitle(); RString s2 = pSong2->GetTranslitMainTitle(); if( s1 == s2 ) { s1 = pSong1->GetTranslitSubTitle(); s2 = pSong2->GetTranslitSubTitle(); } s1 = SongUtil::MakeSortString(s1); s2 = SongUtil::MakeSortString(s2); int ret = strcmp( s1, 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 SongUtil::SortSongPointerArrayByTitle( vector &vpSongsInOut ) { sort( vpSongsInOut.begin(), vpSongsInOut.end(), CompareSongPointersByTitle ); } static bool CompareSongPointersByBPM( const Song *pSong1, const Song *pSong2 ) { DisplayBpms bpms1, bpms2; pSong1->GetDisplayBpms( bpms1 ); pSong2->GetDisplayBpms( bpms2 ); if( bpms1.GetMax() < bpms2.GetMax() ) return true; if( bpms1.GetMax() > bpms2.GetMax() ) return false; return CompareRStringsAsc( pSong1->GetSongFilePath(), pSong2->GetSongFilePath() ); } void SongUtil::SortSongPointerArrayByBPM( vector &vpSongsInOut ) { sort( vpSongsInOut.begin(), vpSongsInOut.end(), CompareSongPointersByBPM ); } void AppendOctal( int n, int digits, RString &out ) { for( int p = digits-1; p >= 0; --p ) { const int shift = p*3; int n2 = (n >> shift) & 0x7; out.insert( out.end(), (char) (n2+'0') ); } } static bool CompDescending( const pair &a, const pair &b ) { return a.second > b.second; } static bool CompAscending( const pair &a, const pair &b ) { return a.second < b.second; } void SongUtil::SortSongPointerArrayByGrades( vector &vpSongsInOut, bool bDescending ) { /* Optimize by pre-writing a string to compare, since doing GetNumNotesWithGrade * inside the sort is too slow. */ typedef pair< Song *, RString > val; vector vals; vals.reserve( vpSongsInOut.size() ); for( unsigned i = 0; i < vpSongsInOut.size(); ++i ) { Song *pSong = vpSongsInOut[i]; int iCounts[NUM_Grade]; const Profile *pProfile = PROFILEMAN->GetMachineProfile(); const Style *pStyle = GAMESTATE->GetCurrentStyle(); StepsType st = pStyle->m_StepsType; pProfile->GetGrades( pSong, st, iCounts ); RString foo; foo.reserve(256); for( int g=Grade_Tier01; g<=Grade_NoData; ++g ) AppendOctal( iCounts[g], 3, foo ); vals.push_back( val(pSong, foo) ); } sort( vals.begin(), vals.end(), bDescending ? CompDescending : CompAscending ); for( unsigned i = 0; i < vpSongsInOut.size(); ++i ) vpSongsInOut[i] = vals[i].first; } void SongUtil::SortSongPointerArrayByArtist( vector &vpSongsInOut ) { for( unsigned i = 0; i < vpSongsInOut.size(); ++i ) g_mapSongSortVal[vpSongsInOut[i]] = MakeSortString( vpSongsInOut[i]->GetTranslitArtist() ); stable_sort( vpSongsInOut.begin(), vpSongsInOut.end(), CompareSongPointersBySortValueAscending ); } /* This is for internal use, not display; sorting by Unicode codepoints isn't very * interesting for display. */ void SongUtil::SortSongPointerArrayByDisplayArtist( vector &vpSongsInOut ) { for( unsigned i = 0; i < vpSongsInOut.size(); ++i ) g_mapSongSortVal[vpSongsInOut[i]] = MakeSortString( vpSongsInOut[i]->GetDisplayArtist() ); stable_sort( vpSongsInOut.begin(), vpSongsInOut.end(), CompareSongPointersBySortValueAscending ); } static int CompareSongPointersByGenre(const Song *pSong1, const Song *pSong2) { return pSong1->m_sGenre < pSong2->m_sGenre; } void SongUtil::SortSongPointerArrayByGenre( vector &vpSongsInOut ) { stable_sort( vpSongsInOut.begin(), vpSongsInOut.end(), CompareSongPointersByGenre ); } int SongUtil::CompareSongPointersByGroup(const Song *pSong1, const Song *pSong2) { return pSong1->m_sGroupName < pSong2->m_sGroupName; } static int CompareSongPointersByGroupAndTitle( const Song *pSong1, const Song *pSong2 ) { const RString &sGroup1 = pSong1->m_sGroupName; const RString &sGroup2 = pSong2->m_sGroupName; if( sGroup1 < sGroup2 ) return true; if( sGroup1 > sGroup2 ) return false; /* Same group; compare by name. */ return CompareSongPointersByTitle( pSong1, pSong2 ); } void SongUtil::SortSongPointerArrayByGroupAndTitle( vector &vpSongsInOut ) { sort( vpSongsInOut.begin(), vpSongsInOut.end(), CompareSongPointersByGroupAndTitle ); } void SongUtil::SortSongPointerArrayByNumPlays( vector &vpSongsInOut, ProfileSlot slot, bool bDescending ) { if( !PROFILEMAN->IsPersistentProfile(slot) ) return; // nothing to do since we don't have data Profile* pProfile = PROFILEMAN->GetProfile(slot); SortSongPointerArrayByNumPlays( vpSongsInOut, pProfile, bDescending ); } void SongUtil::SortSongPointerArrayByNumPlays( vector &vpSongsInOut, const Profile* pProfile, bool bDescending ) { ASSERT( pProfile ); for(unsigned i = 0; i < vpSongsInOut.size(); ++i) g_mapSongSortVal[vpSongsInOut[i]] = ssprintf("%9i", pProfile->GetSongNumTimesPlayed(vpSongsInOut[i])); stable_sort( vpSongsInOut.begin(), vpSongsInOut.end(), bDescending ? CompareSongPointersBySortValueDescending : CompareSongPointersBySortValueAscending ); g_mapSongSortVal.clear(); } RString SongUtil::GetSectionNameFromSongAndSort( const Song* pSong, SortOrder so ) { if( pSong == NULL ) return RString(); switch( so ) { case SORT_PREFERRED: return RString(); case SORT_GROUP: // guaranteed not empty return pSong->m_sGroupName; case SORT_TITLE: case SORT_ARTIST: { RString s; switch( so ) { case SORT_TITLE: s = pSong->GetTranslitMainTitle(); break; case SORT_ARTIST: s = pSong->GetTranslitArtist(); break; default: ASSERT(0); } s = MakeSortString(s); // resulting string will be uppercase if( s.empty() ) return RString(); else if( s[0] >= '0' && s[0] <= '9' ) return "0-9"; else if( s[0] < 'A' || s[0] > 'Z') return SORT_OTHER.GetValue(); else return s.Left(1); } case SORT_GENRE: if( !pSong->m_sGenre.empty() ) return pSong->m_sGenre; return SORT_NOT_AVAILABLE.GetValue(); case SORT_BPM: { const int iBPMGroupSize = 20; DisplayBpms bpms; pSong->GetDisplayBpms( bpms ); int iMaxBPM = (int)bpms.GetMax(); iMaxBPM += iBPMGroupSize - (iMaxBPM%iBPMGroupSize) - 1; return ssprintf("%03d-%03d",iMaxBPM-(iBPMGroupSize-1), iMaxBPM); } case SORT_POPULARITY: return RString(); case SORT_TOP_GRADES: { int iCounts[NUM_Grade]; PROFILEMAN->GetMachineProfile()->GetGrades( pSong, GAMESTATE->GetCurrentStyle()->m_StepsType, iCounts ); for( int i=Grade_Tier01; i 0 ) return ssprintf( "%4s x %d", GradeToLocalizedString(g).c_str(), iCounts[i] ); } return GradeToLocalizedString( Grade_NoData ); } case SORT_EASY_METER: { Steps* pSteps = GetStepsByDifficulty(pSong, GAMESTATE->GetCurrentStyle()->m_StepsType,DIFFICULTY_EASY); if( pSteps && !UNLOCKMAN->StepsIsLocked(pSong,pSteps) ) return ssprintf("%02d", pSteps->GetMeter() ); return SORT_NOT_AVAILABLE.GetValue(); } case SORT_MEDIUM_METER: { Steps* pSteps = GetStepsByDifficulty(pSong, GAMESTATE->GetCurrentStyle()->m_StepsType,DIFFICULTY_MEDIUM); if( pSteps && !UNLOCKMAN->StepsIsLocked(pSong,pSteps) ) return ssprintf("%02d", pSteps->GetMeter() ); return SORT_NOT_AVAILABLE.GetValue(); } case SORT_HARD_METER: { Steps* pSteps = GetStepsByDifficulty(pSong, GAMESTATE->GetCurrentStyle()->m_StepsType,DIFFICULTY_HARD); if( pSteps && !UNLOCKMAN->StepsIsLocked(pSong,pSteps) ) return ssprintf("%02d", pSteps->GetMeter() ); return SORT_NOT_AVAILABLE.GetValue(); } case SORT_CHALLENGE_METER: { Steps* pSteps = GetStepsByDifficulty(pSong, GAMESTATE->GetCurrentStyle()->m_StepsType,DIFFICULTY_CHALLENGE); if( pSteps && !UNLOCKMAN->StepsIsLocked(pSong,pSteps) ) return ssprintf("%02d", pSteps->GetMeter() ); return SORT_NOT_AVAILABLE.GetValue(); } case SORT_MODE_MENU: return RString(); case SORT_ALL_COURSES: case SORT_NONSTOP_COURSES: case SORT_ONI_COURSES: case SORT_ENDLESS_COURSES: default: ASSERT(0); return RString(); } } void SongUtil::SortSongPointerArrayBySectionName( vector &vpSongsInOut, SortOrder so ) { RString sOther = SORT_OTHER.GetValue(); for(unsigned i = 0; i < vpSongsInOut.size(); ++i) { RString val = GetSectionNameFromSongAndSort( vpSongsInOut[i], so ); /* Make sure 0-9 comes first and OTHER comes last. */ if( val == "0-9" ) val = "0"; else if( val == sOther ) val = "2"; else val = "1" + MakeSortString(val); g_mapSongSortVal[vpSongsInOut[i]] = val; } stable_sort( vpSongsInOut.begin(), vpSongsInOut.end(), CompareSongPointersBySortValueAscending ); g_mapSongSortVal.clear(); } void SongUtil::SortSongPointerArrayByMeter( vector &vpSongsInOut, Difficulty dc ) { g_mapSongSortVal.clear(); for(unsigned i = 0; i < vpSongsInOut.size(); ++i) { /* Ignore locked steps. */ const Steps* pSteps = GetClosestNotes( vpSongsInOut[i], GAMESTATE->GetCurrentStyle()->m_StepsType, dc, true ); RString &s = g_mapSongSortVal[vpSongsInOut[i]]; s = ssprintf("%03d", pSteps ? pSteps->GetMeter() : 0); /* Hack: always put tutorial songs first. */ s += ssprintf( "%c", vpSongsInOut[i]->IsTutorial()? '0':'1' ); /* * pSteps may not be exactly the difficulty we want; for example, we may * be sorting by Hard difficulty and a song may have no Hard steps. * * In this case, we can end up with unintuitive ties; for example, pSteps * may be Medium with a meter of 5, which will sort it among the 5-meter * Hard songs. Break the tie, by adding the difficulty to the sort as * well. That way, we'll always put Medium 5s before Hard 5s. If all * songs are using the preferred difficulty (dc), this will be a no-op. */ s += ssprintf( "%c", (pSteps? pSteps->GetDifficulty():0) + '0' ); if( PREFSMAN->m_bSubSortByNumSteps ) s += ssprintf("%06.0f",pSteps ? pSteps->GetRadarValues(PLAYER_1)[RadarCategory_TapsAndHolds] : 0); } stable_sort( vpSongsInOut.begin(), vpSongsInOut.end(), CompareSongPointersBySortValueAscending ); } void SongUtil::SortByMostRecentlyPlayedForMachine( vector &vpSongsInOut ) { Profile *pProfile = PROFILEMAN->GetMachineProfile(); FOREACH_CONST( Song*, vpSongsInOut, s ) { int iNumTimesPlayed = pProfile->GetSongNumTimesPlayed( *s ); RString val = iNumTimesPlayed ? pProfile->GetSongLastPlayedDateTime(*s).GetString() : "0"; g_mapSongSortVal[*s] = val; } stable_sort( vpSongsInOut.begin(), vpSongsInOut.end(), CompareSongPointersBySortValueDescending ); g_mapSongSortVal.clear(); } bool SongUtil::IsEditDescriptionUnique( const Song* pSong, StepsType st, const RString &sPreferredDescription, const Steps *pExclude ) { FOREACH_CONST( Steps*, pSong->GetAllSteps(), s ) { Steps *pSteps = *s; if( pSteps->GetDifficulty() != DIFFICULTY_EDIT ) continue; if( pSteps->m_StepsType != st ) continue; if( pSteps == pExclude ) continue; if( pSteps->GetDescription() == sPreferredDescription ) return false; } return true; } RString SongUtil::MakeUniqueEditDescription( const Song *pSong, StepsType st, const RString &sPreferredDescription ) { if( IsEditDescriptionUnique( pSong, st, sPreferredDescription, NULL ) ) return sPreferredDescription; RString sTemp; for( int i=0; i<1000; i++ ) { // make name "My Edit" -> "My Edit2" RString sNum = ssprintf("%d", i+1); sTemp = sPreferredDescription.Left( MAX_EDIT_STEPS_DESCRIPTION_LENGTH - sNum.size() ) + sNum; if( IsEditDescriptionUnique(pSong, st, sTemp, NULL) ) return sTemp; } // Edit limit guards should keep us from ever having more than 1000 edits per song. ASSERT(0); return RString(); } static LocalizedString YOU_MUST_SUPPLY_NAME ( "SongUtil", "You must supply a name for your new edit." ); static LocalizedString EDIT_NAME_CONFLICTS ( "SongUtil", "The name you chose conflicts with another edit. Please use a different name." ); static LocalizedString EDIT_NAME_CANNOT_CONTAIN ( "SongUtil", "The edit name cannot contain any of the following characters: %s" ); bool SongUtil::ValidateCurrentEditStepsDescription( const RString &sAnswer, RString &sErrorOut ) { Steps *pSteps = GAMESTATE->m_pCurSteps[PLAYER_1]; Song *pSong = SONGMAN->GetSongFromSteps( pSteps ); ASSERT( pSteps->IsAnEdit() ); if( sAnswer.empty() ) { sErrorOut = YOU_MUST_SUPPLY_NAME; return false; } static const RString sInvalidChars = "\\/:*?\"<>|"; if( strpbrk(sAnswer, sInvalidChars) != NULL ) { sErrorOut = ssprintf( EDIT_NAME_CANNOT_CONTAIN.GetValue(), sInvalidChars.c_str() ); return false; } // Steps name must be unique for this song. vector v; GetSteps( pSong, v, STEPS_TYPE_INVALID, DIFFICULTY_EDIT ); FOREACH_CONST( Steps*, v, s ) { if( pSteps == *s ) continue; // don't comepare name against ourself if( (*s)->GetDescription() == sAnswer ) { sErrorOut = EDIT_NAME_CONFLICTS; return false; } } return true; } bool SongUtil::ValidateCurrentStepsDescription( const RString &sAnswer, RString &sErrorOut ) { if( sAnswer.empty() ) return true; /* Don't allow duplicate edit names within the same StepsType; edit names uniquely * identify the edit. */ Steps *pSteps = GAMESTATE->m_pCurSteps[PLAYER_1]; /* If unchanged: */ if( pSteps->GetDescription() == sAnswer ) return true; if( pSteps->IsAnEdit() ) { return SongUtil::ValidateCurrentEditStepsDescription( sAnswer, sErrorOut ); } return true; } void SongUtil::GetAllSongGenres( vector &vsOut ) { set genres; FOREACH_CONST( Song*, SONGMAN->GetAllSongs(), song ) { if( !(*song)->m_sGenre.empty() ) genres.insert( (*song)->m_sGenre ); } FOREACHS_CONST( RString, genres, s ) { vsOut.push_back( *s ); } } void SongUtil::FilterSongs( const SongCriteria &sc, const vector &in, vector &out ) { FOREACH_CONST( Song*, in, s ) { if( sc.Matches( *s ) ) { out.push_back( *s ); } } } ////////////////////////////////// // SongID ////////////////////////////////// void SongID::FromSong( const Song *p ) { if( p ) sDir = p->GetSongDir(); else sDir = ""; // HACK for backwards compatibility: // Strip off leading "/". 2005/05/21 file layer changes added a leading slash. if( sDir.Left(1) == "/" ) sDir.erase( sDir.begin() ); } Song *SongID::ToSong() const { // HACK for backwards compatibility: // Re-add the leading "/". 2005/05/21 file layer changes added a leading slash. RString sDir2 = sDir; if( sDir2.Left(1) != "/" ) sDir2 = "/" + sDir2; return SONGMAN->GetSongFromDir( sDir2 ); } XNode* SongID::CreateNode() const { XNode* pNode = new XNode; pNode->m_sName = "Song"; pNode->AppendAttr( "Dir", sDir ); return pNode; } void SongID::LoadFromNode( const XNode* pNode ) { ASSERT( pNode->m_sName == "Song" ); pNode->GetAttrValue("Dir", sDir); } RString SongID::ToString() const { return sDir; } bool SongID::IsValid() const { return !sDir.empty(); } /* * (c) 2001-2004 Chris Danford, Glenn Maynard * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, and/or sell copies of the Software, and to permit persons to * whom the Software is furnished to do so, provided that the above * copyright notice(s) and this permission notice appear in all copies of * the Software and that both the above copyright notice(s) and this * permission notice appear in supporting documentation. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF * THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS * INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT * OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS * OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR * PERFORMANCE OF THIS SOFTWARE. */