diff --git a/src/Course.cpp b/src/Course.cpp index fc4bd97454..f208fc222d 100644 --- a/src/Course.cpp +++ b/src/Course.cpp @@ -56,7 +56,7 @@ RString CourseEntry::GetTextDescription() const vsEntryDescription.push_back( pSong->GetTranslitFullTitle() ); else vsEntryDescription.push_back( "Random" ); - if( !songCriteria.m_vsGroupNames.size() > 0 ) + if( songCriteria.m_vsGroupNames.size() > 0 ) vsEntryDescription.push_back( join(",", songCriteria.m_vsGroupNames) ); if( songCriteria.m_bUseSongGenreAllowedList ) vsEntryDescription.push_back( join(",",songCriteria.m_vsSongGenreAllowedList) ); diff --git a/src/CourseLoaderCRS.cpp b/src/CourseLoaderCRS.cpp index d6221a7b3f..67db202ea0 100644 --- a/src/CourseLoaderCRS.cpp +++ b/src/CourseLoaderCRS.cpp @@ -128,7 +128,19 @@ bool CourseLoaderCRS::LoadFromMsd( const RString &sPath, const MsdFile &msd, Cou new_entry.attacks = attacks; new_entry.fGainSeconds = fGainSeconds; attacks.clear(); - + out.m_vEntries.push_back( new_entry ); + } + else if( sValueName.EqualsNoCase("SONGSELECT") ) + { + CourseEntry new_entry; + if( CourseLoaderCRS::ParseCourseSongSelect(sParams, new_entry, sPath) == false ) + { + out.m_bIncomplete = true; + continue; // Skip this #SONGSELECT + } + new_entry.attacks = attacks; + // new_entry.fGainSeconds = fGainSeconds; // SONGSELECT already has a GAINSECONDS option, so we probably don't need this? + attacks.clear(); out.m_vEntries.push_back( new_entry ); } else if( !sValueName.EqualsNoCase("DISPLAYCOURSE") || !sValueName.EqualsNoCase("COMBO") || @@ -406,27 +418,6 @@ bool CourseLoaderCRS::ParseCourseSong( const MsdFile::value_t &sParams, CourseEn CLAMP( new_entry.iChooseIndex, 0, 500 ); new_entry.songSort = SongSort_LowestGrades; } - // random song within a bpm range - else if( sParams[1].Left(strlen("BPMRANGE")) == "BPMRANGE" ) - { - RString sBpmStr = sParams[1].Right(sParams[1].size() - strlen("BPMRANGE")); - std::vector sBpms; - split(sBpmStr, "..", sBpms); - - if( sBpms.size() == 2 ) - { - new_entry.songCriteria.m_fMinBPM = strtof(sBpms[0].c_str(), NULL); - new_entry.songCriteria.m_fMaxBPM = strtof(sBpms[1].c_str(), NULL); - LOG->Trace("BPMRANGE min: %f max %f", new_entry.songCriteria.m_fMinBPM, new_entry.songCriteria.m_fMaxBPM); - } - else - { - LOG->UserLog( "Course file", sPath, "contains an invalid bpm range setting: \"%s\", ignoring", - sBpmStr.c_str() ); - new_entry.songCriteria.m_fMinBPM = -1; - new_entry.songCriteria.m_fMaxBPM = -1; - } - } else if( sParams[1] == "*" ) { //new_entry.bSecret = true; @@ -533,6 +524,193 @@ bool CourseLoaderCRS::ParseCourseSong( const MsdFile::value_t &sParams, CourseEn return true; } +bool CourseLoaderCRS::ParseCourseSongSelect(const MsdFile::value_t &sParams, CourseEntry &new_entry, const RString &sPath) +{ + // I want to be able to make courses that are really weirdly specific, so I'm going to try to put together a different + // format for defining the song selection criteria. + // The basic idea is to free up the order in which the song criteria need to be specified, and to add a bunch more options. + // TITLE, GROUP, ARTIST, DIFFICULTY, BPMRANGE, DURATION, METER, GENRE, SORT, MODS + // #SONGSELECT:TITLE=sometitle,some other title; + // #SONGSELECT:GROUP=DDR A,DDR A3; + // #SONGSELECT:ARTIST=TaQ,Someone else; + // #SONGSELECT:DURATION=69..420; + // #SONGSELECT:DIFFICULTY=Easy,Hard; + // #SONGSELECT:BPMRANGE=150..160; + // #SONGSELECT:METER=5..9; + // #SONGSELECT:GENRE=Pop,Techno,Opera; + // #SONGSELECT:SORT=Best,1; + // #SONGSELECT:SORT=Worst,10; + + for( unsigned i = 1; i < sParams.params.size(); ++i ) + { + std::vector sParamParts; + split(sParams[i], "=", sParamParts); + if( sParamParts.size() != 2 ) + { + LOG->UserLog( "Course file", sPath, "has an invalid SONGSELECT sub-parameter, \"%s\"", sParams[i].c_str()); + return false; + } + RString sParamName = sParamParts[0]; + RString sParamValue = sParamParts[1]; + + // For params that accept multiple items, if someone were to define it twice in one #SONGSELECT, should we overwrite the first, or append? + if( sParamName.EqualsNoCase("TITLE") ) + { + std::vector songTitles; + split(sParamValue, ",", songTitles); + new_entry.songCriteria.m_vsSongNames.insert(new_entry.songCriteria.m_vsSongNames.end(), songTitles.begin(), songTitles.end()); + } + else if( sParamName.EqualsNoCase("GROUP") ) + { + std::vector groups; + split(sParamValue, ",", groups); + new_entry.songCriteria.m_vsGroupNames.insert(new_entry.songCriteria.m_vsGroupNames.end(), groups.begin(), groups.end()); + } + else if( sParamName.EqualsNoCase("ARTIST") ) + { + std::vector artists; + split(sParamValue, ",", artists); + new_entry.songCriteria.m_vsArtistNames.insert(new_entry.songCriteria.m_vsArtistNames.end(), artists.begin(), artists.end()); + } + else if( sParamName.EqualsNoCase("GENRE") ) + { + new_entry.songCriteria.m_bUseSongGenreAllowedList = true; + std::vector genres; + split(sParamValue, ",", genres); + new_entry.songCriteria.m_vsSongGenreAllowedList.insert(new_entry.songCriteria.m_vsSongGenreAllowedList.end(), genres.begin(), genres.end()); + } + else if( sParamName.EqualsNoCase("DIFFICULTY") ) + { + std::vector difficultyStrs; + std::vector difficulties; + split(sParamValue, ",", difficultyStrs); + for (unsigned d = 0; d < difficultyStrs.size(); d++) + { + Difficulty diff = OldStyleStringToDifficulty(difficultyStrs[d]); + // most CRS files use old-style difficulties, but Difficulty enum values can be used in SM5. Test for those too. + if (diff == Difficulty_Invalid) + { + diff = StringToDifficulty(difficultyStrs[d]); + } + if( diff != Difficulty_Invalid ) + { + difficulties.push_back(diff); + } + } + new_entry.stepsCriteria.m_vDifficulties.insert(new_entry.stepsCriteria.m_vDifficulties.end(), difficulties.begin(), difficulties.end()); + } + else if( sParamName.EqualsNoCase("SORT") ) + { + if( CourseLoaderCRS::ParseCourseSongSort(sParamValue, new_entry, sPath) == false ) + { + return false; + } + } + else if( sParamName.EqualsNoCase("DURATION") ) + { + std::vector durations; + split(sParamValue, "..", durations); + new_entry.songCriteria.m_fMinDurationSeconds = StringToFloat(durations[0]); + new_entry.songCriteria.m_fMaxDurationSeconds = StringToFloat(durations[1]); + } + else if( sParamName.EqualsNoCase("BPMRANGE") ) + { + std::vector bpms; + split(sParamValue, "..", bpms); + new_entry.songCriteria.m_fMinBPM = StringToFloat(bpms[0]); + new_entry.songCriteria.m_fMaxBPM = StringToFloat(bpms[1]); + } + else if( sParamName.EqualsNoCase("METER") ) + { + std::vector meters; + split(sParamValue, "..", meters); + new_entry.stepsCriteria.m_iLowMeter = StringToInt(meters[0]); + new_entry.stepsCriteria.m_iHighMeter = StringToInt(meters[1]); + } + else if( sParamName.EqualsNoCase("LIVES") ) + { + new_entry.iGainLives = StringToInt(sParamValue); + } + else if( sParamName.EqualsNoCase("GAINSECONDS") ) + { + new_entry.fGainSeconds = StringToInt(sParamValue); + } + else if( sParamName.EqualsNoCase("MODS") ) + { + std::vector mods; + split( sParamValue, ",", mods, true ); + for( int j = (int) mods.size()-1; j >= 0 ; --j ) + { + RString &sMod = mods[j]; + TrimLeft( sMod ); + TrimRight( sMod ); + if( !sMod.CompareNoCase("showcourse") ) + new_entry.bSecret = false; + else if( !sMod.CompareNoCase("noshowcourse") ) + new_entry.bSecret = true; + else if( !sMod.CompareNoCase("nodifficult") ) + new_entry.bNoDifficult = true; + else + continue; + mods.erase( mods.begin() + j ); + } + new_entry.sModifiers = join( ",", mods ); + } + } + return true; +} + + +bool CourseLoaderCRS::ParseCourseSongSort(RString sParam, CourseEntry &new_entry, const RString &sPath) +{ + int iNumSongs = SONGMAN->GetNumSongs(); + if( sParam.Left(strlen("BEST")) == "BEST" ) + { + int iChooseIndex = StringToInt( sParam.Right(sParam.size()-strlen("BEST")) ) - 1; + if( iChooseIndex > iNumSongs ) + { + // looking up a song that doesn't exist. + LOG->UserLog( "Course file", sPath, "is trying to load BEST%i with only %i songs installed. " + "This entry will be ignored.", iChooseIndex, iNumSongs); + return false; // skip this #SONG + } + + new_entry.iChooseIndex = iChooseIndex; + CLAMP( new_entry.iChooseIndex, 0, 500 ); + new_entry.songSort = SongSort_MostPlays; + } + // least played + else if( sParam.Left(strlen("WORST")) == "WORST" ) + { + int iChooseIndex = StringToInt( sParam.Right(sParam.size()-strlen("WORST")) ) - 1; + if( iChooseIndex > iNumSongs ) + { + // looking up a song that doesn't exist. + LOG->UserLog( "Course file", sPath, "is trying to load WORST%i with only %i songs installed. " + "This entry will be ignored.", iChooseIndex, iNumSongs); + return false; // skip this #SONG + } + + new_entry.iChooseIndex = iChooseIndex; + CLAMP( new_entry.iChooseIndex, 0, 500 ); + new_entry.songSort = SongSort_FewestPlays; + } + // best grades + else if( sParam.Left(strlen("GRADEBEST")) == "GRADEBEST" ) + { + new_entry.iChooseIndex = StringToInt( sParam.Right(sParam.size()-strlen("GRADEBEST")) ) - 1; + CLAMP( new_entry.iChooseIndex, 0, 500 ); + new_entry.songSort = SongSort_TopGrades; + } + // worst grades + else if( sParam.Left(strlen("GRADEWORST")) == "GRADEWORST" ) + { + new_entry.iChooseIndex = StringToInt( sParam.Right(sParam.size()-strlen("GRADEWORST")) ) - 1; + CLAMP( new_entry.iChooseIndex, 0, 500 ); + new_entry.songSort = SongSort_LowestGrades; + } + return true; +} /* * (c) 2001-2004 Chris Danford, Glenn Maynard * All rights reserved. diff --git a/src/CourseLoaderCRS.h b/src/CourseLoaderCRS.h index 386a0d4752..8d2a738cbe 100644 --- a/src/CourseLoaderCRS.h +++ b/src/CourseLoaderCRS.h @@ -54,6 +54,8 @@ namespace CourseLoaderCRS bool ParseCourseMods( const MsdFile::value_t &sParams, AttackArray &attacks, const RString &sPath ); bool ParseCourseSong( const MsdFile::value_t &sParams, CourseEntry &new_entry, const RString &sPath ); + bool ParseCourseSongSelect(const MsdFile::value_t &sParams, CourseEntry &new_entry, const RString &sPath); + bool ParseCourseSongSort(RString sParam, CourseEntry &new_entry, const RString &sPath); } #endif diff --git a/src/Song.cpp b/src/Song.cpp index 410d123053..ad3454bc37 100644 --- a/src/Song.cpp +++ b/src/Song.cpp @@ -295,11 +295,13 @@ bool Song::LoadFromSongDir(RString sDir, bool load_autosave, ProfileSlot from_pr bool use_cache = true; + std::vector sDirectoryParts; + split( m_sSongDir, "/", sDirectoryParts, false ); + m_sSongName = sDirectoryParts[sDirectoryParts.size() - 2]; + ASSERT(m_sSongName != ""); // save group name if(from_profile == ProfileSlot_Invalid) { - std::vector sDirectoryParts; - split( m_sSongDir, "/", sDirectoryParts, false ); ASSERT( sDirectoryParts.size() >= 4 ); /* e.g. "/Songs/Slow/Taps/" */ m_sGroupName = sDirectoryParts[sDirectoryParts.size()-3]; // second from last item ASSERT( m_sGroupName != "" ); @@ -1956,15 +1958,8 @@ bool Song::Matches(RString sGroup, RString sSong) const if( sGroup.size() && sGroup.CompareNoCase(this->m_sGroupName) != 0) return false; - RString sDir = this->GetSongDir(); - sDir.Replace("\\","/"); - std::vector bits; - split( sDir, "/", bits ); - ASSERT(bits.size() >= 2); // should always have at least two parts - const RString &sLastBit = bits[bits.size()-1]; - // match on song dir or title (ala DWI) - if( !sSong.CompareNoCase(sLastBit) ) + if( !sSong.CompareNoCase(m_sSongName) ) return true; if( !sSong.CompareNoCase(this->GetTranslitFullTitle()) ) return true; diff --git a/src/Song.h b/src/Song.h index 7fbe35a543..dd143f7b34 100644 --- a/src/Song.h +++ b/src/Song.h @@ -171,6 +171,9 @@ public: /** @brief The group this Song is in. */ RString m_sGroupName; + + /** @brief The base directory name that this Song is in. */ + RString m_sSongName; /** * @brief the Profile this came from. diff --git a/src/SongUtil.cpp b/src/SongUtil.cpp index 77bbe9e3c4..f9c8f51e40 100644 --- a/src/SongUtil.cpp +++ b/src/SongUtil.cpp @@ -32,7 +32,20 @@ ThemeMetric SHOW_SECTIONS_IN_LENGTH_SORT ( "MusicWheel", "ShowSectionsInLe bool SongCriteria::Matches( const Song *pSong ) const { - if( !m_vsGroupNames.size() > 0 && std::find(m_vsGroupNames.begin(), m_vsGroupNames.end(), pSong->m_sGroupName) == m_vsGroupNames.end() ) + if( m_vsGroupNames.size() > 0 && std::find(m_vsGroupNames.begin(), m_vsGroupNames.end(), pSong->m_sGroupName) == m_vsGroupNames.end() ) + { + return false; + } + + if( m_vsSongNames.size() > 0 && std::find(m_vsSongNames.begin(), m_vsSongNames.end(), pSong->m_sSongName) == m_vsSongNames.end() + && std::find(m_vsSongNames.begin(), m_vsSongNames.end(), pSong->m_sMainTitle) == m_vsSongNames.end() + && std::find(m_vsSongNames.begin(), m_vsSongNames.end(), pSong->m_sMainTitleTranslit) == m_vsSongNames.end() ) + { + return false; + } + + if(m_vsArtistNames.size() > 0 && std::find(m_vsArtistNames.begin(), m_vsArtistNames.end(), pSong->m_sArtist) == m_vsArtistNames.end() + && std::find(m_vsArtistNames.begin(), m_vsArtistNames.end(), pSong->m_sArtistTranslit) == m_vsArtistNames.end() ) { return false; } @@ -79,7 +92,17 @@ bool SongCriteria::Matches( const Song *pSong ) const { return false; } - + + if( m_fMinDurationSeconds != -1 && pSong->m_fMusicLengthSeconds < m_fMinDurationSeconds ) + { + return false; + } + + if( m_fMaxDurationSeconds != -1 && pSong->m_fMusicLengthSeconds > m_fMaxDurationSeconds ) + { + return false; + } + switch( m_Tutorial ) { DEFAULT_FAIL(m_Tutorial); diff --git a/src/SongUtil.h b/src/SongUtil.h index b86651a4a0..48168daa56 100644 --- a/src/SongUtil.h +++ b/src/SongUtil.h @@ -26,6 +26,9 @@ public: * * If an empty string, don't bother using this for searching. */ std::vector m_vsGroupNames; + std::vector m_vsSongNames; + std::vector m_vsArtistNames; + bool m_bUseSongGenreAllowedList; std::vector m_vsSongGenreAllowedList; enum Selectable { Selectable_Yes, Selectable_No, Selectable_DontCare } m_Selectable; @@ -35,6 +38,9 @@ public: int m_iMaxStagesForSong; // don't filter if -1 float m_fMinBPM; // don't filter if -1 float m_fMaxBPM; // don't filter if -1 + float m_fMinDurationSeconds; // don't filter if -1 + float m_fMaxDurationSeconds; // don't filter if -1 + /** @brief Is this song used for tutorial purposes? */ enum Tutorial { @@ -51,10 +57,11 @@ public: } m_Locked; /** @brief Set up some initial song criteria. */ - SongCriteria(): m_vsGroupNames(), m_bUseSongGenreAllowedList(false), + SongCriteria(): m_vsGroupNames(), m_vsSongNames(), m_vsArtistNames(), m_bUseSongGenreAllowedList(false), m_vsSongGenreAllowedList(), m_Selectable(Selectable_DontCare), m_bUseSongAllowedList(false), m_vpSongAllowedList(), - m_iMaxStagesForSong(-1), m_fMinBPM(-1), m_fMaxBPM(-1), m_Tutorial(Tutorial_DontCare), + m_iMaxStagesForSong(-1), m_fMinBPM(-1), m_fMaxBPM(-1), m_fMinDurationSeconds(-1), m_fMaxDurationSeconds(-1), + m_Tutorial(Tutorial_DontCare), m_Locked(Locked_DontCare) { // m_fMinBPM = -1; @@ -78,6 +85,8 @@ public: #define X(x) (x == other.x) return X(m_vsGroupNames) && + X(m_vsSongNames) && + X(m_vsArtistNames) && X(m_bUseSongGenreAllowedList) && X(m_vsSongGenreAllowedList) && X(m_Selectable) && @@ -86,6 +95,8 @@ public: X(m_iMaxStagesForSong) && X(m_fMinBPM) && X(m_fMaxBPM) && + X(m_fMinDurationSeconds) && + X(m_fMaxDurationSeconds) && X(m_Tutorial) && X(m_Locked); #undef X diff --git a/src/StepsUtil.cpp b/src/StepsUtil.cpp index 9ffeb6b6f4..1d12a1f501 100644 --- a/src/StepsUtil.cpp +++ b/src/StepsUtil.cpp @@ -18,6 +18,12 @@ bool StepsCriteria::Matches( const Song *pSong, const Steps *pSteps ) const { if( m_difficulty != Difficulty_Invalid && pSteps->GetDifficulty() != m_difficulty ) return false; + + if(m_vDifficulties.size() > 0 && std::find(m_vDifficulties.begin(), m_vDifficulties.end(), pSteps->GetDifficulty()) == m_vDifficulties.end() ) + { + return false; + } + if( m_iLowMeter != -1 && pSteps->GetMeter() < m_iLowMeter ) return false; if( m_iHighMeter != -1 && pSteps->GetMeter() > m_iHighMeter ) diff --git a/src/StepsUtil.h b/src/StepsUtil.h index 0bd3d6f08f..f5146a7074 100644 --- a/src/StepsUtil.h +++ b/src/StepsUtil.h @@ -22,6 +22,7 @@ public: * * Don't filter here if the Difficulty is Difficulty_Invalid. */ Difficulty m_difficulty; + std::vector m_vDifficulties; /** * @brief The lowest meter to search for. * @@ -51,6 +52,7 @@ public: /** @brief Set up the initial criteria. */ StepsCriteria(): m_difficulty(Difficulty_Invalid), + m_vDifficulties(), m_iLowMeter(-1), m_iHighMeter(-1), m_st(StepsType_Invalid), m_Locked(Locked_DontCare) {