#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" #include "GameManager.h" #include "CommonMetrics.h" #include "LuaBinding.h" #include "EnumHelper.h" bool SongCriteria::Matches( const Song *pSong ) const { if( !m_sGroupName.empty() && m_sGroupName != pSong->m_sGroupName ) return false; if( UNLOCKMAN->SongIsLocked(pSong) & LOCKED_DISABLED ) 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( UNLOCKMAN && UNLOCKMAN->SongIsLocked(pSong) & LOCKED_SELECTABLE ) return false; break; case Selectable_No: if( UNLOCKMAN && !(UNLOCKMAN->SongIsLocked(pSong) & LOCKED_SELECTABLE) ) 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_iMaxStagesForSong != -1 && GAMESTATE->GetNumStagesMultiplierForSong(pSong) > m_iMaxStagesForSong ) 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) & LOCKED_LOCK) ) return false; break; case Locked_Unlocked: if( UNLOCKMAN && UNLOCKMAN->SongIsLocked(pSong) & LOCKED_LOCK ) 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 == StepsType_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 == StepsType_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 == StepsType_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 == StepsType_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_ENUM( StepsType, st ) { FOREACH_ENUM( 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] == '.' ) // like the song ".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 ); } static bool CompareSongPointersByLength( const Song *pSong1, const Song *pSong2 ) { float length1, length2; length1 = pSong1->m_fMusicLengthSeconds; length2 = pSong2->m_fMusicLengthSeconds; if( length1 < length2 ) return true; if( length1 > length2 ) return false; return CompareRStringsAsc( pSong1->GetSongFilePath(), pSong2->GetSongFilePath() ); } void SongUtil::SortSongPointerArrayByLength( vector &vpSongsInOut ) { sort( vpSongsInOut.begin(), vpSongsInOut.end(), CompareSongPointersByLength ); } 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(); ASSERT( pProfile ); // XXX: Debugging. const Style *pStyle = GAMESTATE->GetCurrentStyle(); ASSERT( pStyle ); // XXX: Debugging. 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 SONGMAN->SongToPreferredSortSectionName( pSong ); 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: { // todo: make this a theme metric? -aj 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_LENGTH: { // todo: make this a theme metric? -aj const int iSortLengthSize = 5; int iMaxLength = (int)pSong->m_fMusicLengthSeconds; iMaxLength += (iSortLengthSize - (iMaxLength%iSortLengthSize) - 1); int iMinLength = iMaxLength - (iSortLengthSize-1); return ssprintf( "%s-%s", SecondsToMMSS(iMinLength).c_str(), SecondsToMMSS(iMaxLength).c_str() ); } case SORT_POPULARITY: case SORT_RECENT: 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_BEGINNER_METER: case SORT_EASY_METER: case SORT_MEDIUM_METER: case SORT_HARD_METER: case SORT_CHALLENGE_METER: case SORT_DOUBLE_EASY_METER: case SORT_DOUBLE_MEDIUM_METER: case SORT_DOUBLE_HARD_METER: case SORT_DOUBLE_CHALLENGE_METER: { StepsType st; Difficulty dc; SongUtil::GetStepsTypeAndDifficultyFromSortOrder( so, st, dc ); Steps* pSteps = GetStepsByDifficulty(pSong,st,dc); 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::SortSongPointerArrayByStepsTypeAndMeter( vector &vpSongsInOut, StepsType st, Difficulty dc ) { g_mapSongSortVal.clear(); for(unsigned i = 0; i < vpSongsInOut.size(); ++i) { /* Ignore locked steps. */ const Steps* pSteps = GetClosestNotes( vpSongsInOut[i], st, dc, true ); RString &s = g_mapSongSortVal[vpSongsInOut[i]]; s = ssprintf("%03d", pSteps ? pSteps->GetMeter() : 0); /* * 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' ); // ???: will this cause a crash with player 2 only? -aj // no, but I'm not sure if it causes problems either... 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, StepsType_Invalid, Difficulty_Edit ); FOREACH_CONST( Steps*, v, s ) { if( pSteps == *s ) continue; // don't compare 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 ) { out.reserve( in.size() ); FOREACH_CONST( Song*, in, s ) { if( sc.Matches( *s ) ) { out.push_back( *s ); } } } void SongUtil::GetPlayableStepsTypes( const Song *pSong, set &vOut ) { vector vpPossibleStyles; if( CommonMetrics::AUTO_SET_STYLE ) GAMEMAN->GetCompatibleStyles( GAMESTATE->m_pCurGame, GAMESTATE->GetNumPlayersEnabled(), vpPossibleStyles ); else vpPossibleStyles.push_back( GAMESTATE->m_pCurStyle ); // Only allow OneSide Styles in Workout if( GAMESTATE->m_bMultiplayer ) { for( int i=vpPossibleStyles.size()-1; i>=0; i-- ) { const Style *pStyle = vpPossibleStyles[i]; switch( pStyle->m_StyleType ) { DEFAULT_FAIL( pStyle->m_StyleType ); case StyleType_OnePlayerOneSide: continue; case StyleType_TwoPlayersTwoSides: case StyleType_OnePlayerTwoSides: case StyleType_TwoPlayersSharedSides: vpPossibleStyles.erase( vpPossibleStyles.begin() + i ); break; } } } set vStepsTypes; FOREACH( const Style*, vpPossibleStyles, s ) vStepsTypes.insert( (*s)->m_StepsType ); /* filter out hidden StepsTypes, and remove steps that we don't * have enough stages left to play. */ // this being const may have caused some problems... -aj const vector &vstToShow = CommonMetrics::STEPS_TYPES_TO_SHOW.GetValue(); FOREACHS( StepsType, vStepsTypes, st ) { bool bShowThisStepsType = find( vstToShow.begin(), vstToShow.end(), *st ) != vstToShow.end(); int iNumPlayers = GAMESTATE->GetNumPlayersEnabled(); iNumPlayers = max( iNumPlayers, 1 ); const Style *pStyle = GAMEMAN->GetFirstCompatibleStyle( GAMESTATE->m_pCurGame, iNumPlayers, *st ); bool bEnoughStages = GAMESTATE->IsAnExtraStage() || GAMESTATE->GetSmallestNumStagesLeftForAnyHumanPlayer() >= GAMESTATE->GetNumStagesForSongAndStyleType(pSong, pStyle->m_StyleType); if( bShowThisStepsType && bEnoughStages ) vOut.insert( *st ); } } void SongUtil::GetPlayableSteps( const Song *pSong, vector &vOut ) { set vStepsType; GetPlayableStepsTypes( pSong, vStepsType ); FOREACHS( StepsType, vStepsType, st ) SongUtil::GetSteps( pSong, vOut, *st ); StepsUtil::RemoveLockedSteps( pSong, vOut ); StepsUtil::SortNotesArrayByDifficulty( vOut ); StepsUtil::SortStepsByTypeAndDifficulty( vOut ); } bool SongUtil::IsStepsTypePlayable( Song *pSong, StepsType st ) { set vStepsType; GetPlayableStepsTypes( pSong, vStepsType ); return vStepsType.find( st ) != vStepsType.end(); } bool SongUtil::IsStepsPlayable( Song *pSong, Steps *pSteps ) { vector vpSteps; GetPlayableSteps( pSong, vpSteps ); return find( vpSteps.begin(), vpSteps.end(), pSteps ) != vpSteps.end(); } bool SongUtil::GetStepsTypeAndDifficultyFromSortOrder( SortOrder so, StepsType &stOut, Difficulty &dcOut ) { switch( so ) { case SORT_BEGINNER_METER: dcOut = Difficulty_Beginner; break; case SORT_EASY_METER: dcOut = Difficulty_Easy; break; case SORT_MEDIUM_METER: dcOut = Difficulty_Medium; break; case SORT_HARD_METER: dcOut = Difficulty_Hard; break; case SORT_CHALLENGE_METER: dcOut = Difficulty_Challenge; break; case SORT_DOUBLE_EASY_METER: dcOut = Difficulty_Easy; break; case SORT_DOUBLE_MEDIUM_METER: dcOut = Difficulty_Medium; break; case SORT_DOUBLE_HARD_METER: dcOut = Difficulty_Hard; break; case SORT_DOUBLE_CHALLENGE_METER: dcOut = Difficulty_Challenge; break; default: return false; } switch( so ) { DEFAULT_FAIL( so ); case SORT_BEGINNER_METER: case SORT_EASY_METER: case SORT_MEDIUM_METER: case SORT_HARD_METER: case SORT_CHALLENGE_METER: stOut = GAMESTATE->GetCurrentStyle()->m_StepsType; break; case SORT_DOUBLE_EASY_METER: case SORT_DOUBLE_MEDIUM_METER: case SORT_DOUBLE_HARD_METER: case SORT_DOUBLE_CHALLENGE_METER: stOut = GAMESTATE->GetCurrentStyle()->m_StepsType; // in case we don't find any matches below vector vpStyles; GAMEMAN->GetStylesForGame(GAMESTATE->m_pCurGame,vpStyles); FOREACH_CONST( const Style*, vpStyles, i ) { if( (*i)->m_StyleType == StyleType_OnePlayerTwoSides ) { // Ugly hack to ignore pump's half-double. bool bContainsHalf = ((RString)(*i)->m_szName).find("half") != RString::npos; if( bContainsHalf ) continue; stOut = (*i)->m_StepsType; break; } } break; } return true; } ////////////////////////////////// // 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() ); m_Cache.Unset(); } Song *SongID::ToSong() const { Song *pRet = NULL; if( !m_Cache.Get(&pRet) ) { // 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; if( !sDir2.empty() ) pRet = SONGMAN->GetSongFromDir( sDir2 ); m_Cache.Set( pRet ); } return pRet; } XNode* SongID::CreateNode() const { XNode* pNode = new XNode( "Song" ); pNode->AppendAttr( "Dir", sDir ); return pNode; } void SongID::LoadFromNode( const XNode* pNode ) { ASSERT( pNode->GetName() == "Song" ); pNode->GetAttrValue("Dir", sDir); m_Cache.Unset(); } RString SongID::ToString() const { return sDir; } bool SongID::IsValid() const { return ToSong() != NULL; } // lua start #include "LuaBinding.h" namespace { int IsStepsTypePlayable( lua_State *L ) { Song *pSong = Luna::check( L, 1, true ); StepsType st = Enum::Check(L, 2); bool b = SongUtil::IsStepsTypePlayable( pSong, st ); LuaHelpers::Push( L, b ); return 1; } int IsStepsPlayable( lua_State *L ) { Song *pSong = Luna::check( L, 1, true ); Steps *pSteps = Luna::check( L, 2, true ); bool b = SongUtil::IsStepsPlayable( pSong, pSteps ); LuaHelpers::Push( L, b ); return 1; } const luaL_Reg SongUtilTable[] = { LIST_METHOD( IsStepsTypePlayable ), LIST_METHOD( IsStepsPlayable ), { NULL, NULL } }; } LUA_REGISTER_NAMESPACE( SongUtil ) /* * (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. */