diff --git a/src/CourseLoaderCRS.cpp b/src/CourseLoaderCRS.cpp index fd896aece2..2676375bc9 100644 --- a/src/CourseLoaderCRS.cpp +++ b/src/CourseLoaderCRS.cpp @@ -5,7 +5,6 @@ #include "RageUtil.h" #include "SongCacheIndex.h" #include "PrefsManager.h" -#include "MsdFile.h" #include "PlayerOptions.h" #include "SongManager.h" #include "TitleSubstitution.h" @@ -116,207 +115,14 @@ bool CourseLoaderCRS::LoadFromMsd( const RString &sPath, const MsdFile &msd, Cou else if( sValueName.EqualsNoCase("MODS") ) { - Attack attack; - float end = -9999; - for( unsigned j = 1; j < sParams.params.size(); ++j ) - { - std::vector sBits; - split( sParams[j], "=", sBits, false ); - if( sBits.size() < 2 ) - continue; - - Trim( sBits[0] ); - if( !sBits[0].CompareNoCase("TIME") ) - attack.fStartSecond = std::max( StringToFloat(sBits[1]), 0.0f ); - else if( !sBits[0].CompareNoCase("LEN") ) - attack.fSecsRemaining = StringToFloat( sBits[1] ); - else if( !sBits[0].CompareNoCase("END") ) - end = StringToFloat( sBits[1] ); - else if( !sBits[0].CompareNoCase("MODS") ) - { - attack.sModifiers = sBits[1]; - - if( end != -9999 ) - { - attack.fSecsRemaining = end - attack.fStartSecond; - end = -9999; - } - - if( attack.fSecsRemaining <= 0.0f) - { - LOG->UserLog( "Course file", sPath, "has an attack with a nonpositive length: %s", sBits[1].c_str() ); - attack.fSecsRemaining = 0.0f; - } - - // warn on invalid so we catch typos on load - CourseUtil::WarnOnInvalidMods( attack.sModifiers ); - - attacks.push_back( attack ); - } - else - { - LOG->UserLog( "Course file", sPath, "has an unexpected value named '%s'", sBits[0].c_str() ); - } - } - + CourseLoaderCRS::ParseCourseMods(sParams, attacks, sPath); } else if( sValueName.EqualsNoCase("SONG") ) { CourseEntry new_entry; - - // infer entry::Type from the first param - // todo: make sure these aren't generating bogus entries due - // to a lack of songs. -aj - int iNumSongs = SONGMAN->GetNumSongs(); - // most played - if( sParams[1].Left(strlen("BEST")) == "BEST" ) - { - int iChooseIndex = StringToInt( sParams[1].Right(sParams[1].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); - out.m_bIncomplete = true; - continue; // skip this #SONG - } - - new_entry.iChooseIndex = iChooseIndex; - CLAMP( new_entry.iChooseIndex, 0, 500 ); - new_entry.songSort = SongSort_MostPlays; - } - // least played - else if( sParams[1].Left(strlen("WORST")) == "WORST" ) - { - int iChooseIndex = StringToInt( sParams[1].Right(sParams[1].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); - out.m_bIncomplete = true; - continue; // skip this #SONG - } - - new_entry.iChooseIndex = iChooseIndex; - CLAMP( new_entry.iChooseIndex, 0, 500 ); - new_entry.songSort = SongSort_FewestPlays; - } - // best grades - else if( sParams[1].Left(strlen("GRADEBEST")) == "GRADEBEST" ) - { - new_entry.iChooseIndex = StringToInt( sParams[1].Right(sParams[1].size()-strlen("GRADEBEST")) ) - 1; - CLAMP( new_entry.iChooseIndex, 0, 500 ); - new_entry.songSort = SongSort_TopGrades; - } - // worst grades - else if( sParams[1].Left(strlen("GRADEWORST")) == "GRADEWORST" ) - { - new_entry.iChooseIndex = StringToInt( sParams[1].Right(sParams[1].size()-strlen("GRADEWORST")) ) - 1; - CLAMP( new_entry.iChooseIndex, 0, 500 ); - new_entry.songSort = SongSort_LowestGrades; - } - else if( sParams[1] == "*" ) - { - //new_entry.bSecret = true; - } - // group random - else if( sParams[1].Right(1) == "*" ) - { - //new_entry.bSecret = true; - RString sSong = sParams[1]; - sSong.Replace( "\\", "/" ); - std::vector bits; - split( sSong, "/", bits ); - if( bits.size() == 2 ) - { - new_entry.songCriteria.m_sGroupName = bits[0]; - } - else - { - LOG->UserLog( "Course file", sPath, "contains a random_within_group entry \"%s\" that is invalid. " - "Song should be in the format \"/*\".", sSong.c_str() ); - } - - if( !SONGMAN->DoesSongGroupExist(new_entry.songCriteria.m_sGroupName) ) - { - LOG->UserLog( "Course file", sPath, "random_within_group entry \"%s\" specifies a group that doesn't exist. " - "This entry will be ignored.", sSong.c_str() ); - out.m_bIncomplete = true; - continue; // skip this #SONG - } - } - else - { - RString sSong = sParams[1]; - sSong.Replace( "\\", "/" ); - std::vector bits; - split( sSong, "/", bits ); - - Song *pSong = nullptr; - if( bits.size() == 2 ) - { - new_entry.songCriteria.m_sGroupName = bits[0]; - pSong = SONGMAN->FindSong( bits[0], bits[1] ); - } - else if( bits.size() == 1 ) - { - pSong = SONGMAN->FindSong( "", sSong ); - } - new_entry.songID.FromSong( pSong ); - - if( pSong == nullptr ) - { - LOG->UserLog( "Course file", sPath, "contains a fixed song entry \"%s\" that does not exist. " - "This entry will be ignored.", sSong.c_str()); - out.m_bIncomplete = true; - continue; // skip this #SONG - } - } - - new_entry.stepsCriteria.m_difficulty = OldStyleStringToDifficulty( sParams[2] ); - //most CRS files use old-style difficulties, but Difficulty enum values can be used in SM5. Test for those too. - if( new_entry.stepsCriteria.m_difficulty == Difficulty_Invalid ) - new_entry.stepsCriteria.m_difficulty = StringToDifficulty( sParams[2] ); - if( new_entry.stepsCriteria.m_difficulty == Difficulty_Invalid ) - { - int retval = sscanf( sParams[2], "%d..%d", &new_entry.stepsCriteria.m_iLowMeter, &new_entry.stepsCriteria.m_iHighMeter ); - if( retval == 1 ) - new_entry.stepsCriteria.m_iHighMeter = new_entry.stepsCriteria.m_iLowMeter; - else if( retval != 2 ) - { - LOG->UserLog( "Course file", sPath, "contains an invalid difficulty setting: \"%s\", 3..6 used instead", - sParams[2].c_str() ); - new_entry.stepsCriteria.m_iLowMeter = 3; - new_entry.stepsCriteria.m_iHighMeter = 6; - } - new_entry.stepsCriteria.m_iLowMeter = std::max( new_entry.stepsCriteria.m_iLowMeter, 1 ); - new_entry.stepsCriteria.m_iHighMeter = std::max( new_entry.stepsCriteria.m_iHighMeter, new_entry.stepsCriteria.m_iLowMeter ); - } - - { - // If "showcourse" or "noshowcourse" is in the list, force - // new_entry.secret on or off. - std::vector mods; - split( sParams[3], ",", 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 if( sMod.length() > 5 && !sMod.Left(5).CompareNoCase("award") ) - new_entry.iGainLives = StringToInt( sMod.substr(5) ); - else - continue; - mods.erase( mods.begin() + j ); - } - new_entry.sModifiers = join( ",", mods ); + if(CourseLoaderCRS::ParseCourseSong(sParams, new_entry, sPath) == false) { + out.m_bIncomplete = true; + continue; // Skip this #SONG } new_entry.attacks = attacks; @@ -501,6 +307,233 @@ bool CourseLoaderCRS::LoadEditFromBuffer( const RString &sBuffer, const RString return true; } +bool CourseLoaderCRS::ParseCourseMods( const MsdFile::value_t &sParams, AttackArray &attacks, const RString &sPath ) +{ + Attack attack; + float end = -9999; + for( unsigned j = 1; j < sParams.params.size(); ++j ) + { + std::vector sBits; + split( sParams[j], "=", sBits, false ); + if( sBits.size() < 2 ) + continue; + + Trim( sBits[0] ); + if( !sBits[0].CompareNoCase("TIME") ) + attack.fStartSecond = std::max( StringToFloat(sBits[1]), 0.0f ); + else if( !sBits[0].CompareNoCase("LEN") ) + attack.fSecsRemaining = StringToFloat( sBits[1] ); + else if( !sBits[0].CompareNoCase("END") ) + end = StringToFloat( sBits[1] ); + else if( !sBits[0].CompareNoCase("MODS") ) + { + attack.sModifiers = sBits[1]; + + if( end != -9999 ) + { + attack.fSecsRemaining = end - attack.fStartSecond; + end = -9999; + } + + if( attack.fSecsRemaining <= 0.0f) + { + LOG->UserLog( "Course file", sPath, "has an attack with a nonpositive length: %s", sBits[1].c_str() ); + attack.fSecsRemaining = 0.0f; + } + + // warn on invalid so we catch typos on load + CourseUtil::WarnOnInvalidMods( attack.sModifiers ); + + attacks.push_back( attack ); + } + else + { + LOG->UserLog( "Course file", sPath, "has an unexpected value named '%s'", sBits[0].c_str() ); + } + } + return true; +} + +bool CourseLoaderCRS::ParseCourseSong( const MsdFile::value_t &sParams, CourseEntry &new_entry, const RString &sPath ) +{ + LOG->Trace("CourseLoaderCRS::ParseCourseSong parsing song %s", sPath.c_str()); + LOG->Trace("CourseLoaderCRS::ParseCourseSong sParams[1] = %s", sParams[1].c_str()); + // infer entry::Type from the first param + // todo: make sure these aren't generating bogus entries due + // to a lack of songs. -aj + int iNumSongs = SONGMAN->GetNumSongs(); + // most played + if( sParams[1].Left(strlen("BEST")) == "BEST" ) + { + int iChooseIndex = StringToInt( sParams[1].Right(sParams[1].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( sParams[1].Left(strlen("WORST")) == "WORST" ) + { + int iChooseIndex = StringToInt( sParams[1].Right(sParams[1].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( sParams[1].Left(strlen("GRADEBEST")) == "GRADEBEST" ) + { + new_entry.iChooseIndex = StringToInt( sParams[1].Right(sParams[1].size()-strlen("GRADEBEST")) ) - 1; + CLAMP( new_entry.iChooseIndex, 0, 500 ); + new_entry.songSort = SongSort_TopGrades; + } + // worst grades + else if( sParams[1].Left(strlen("GRADEWORST")) == "GRADEWORST" ) + { + new_entry.iChooseIndex = StringToInt( sParams[1].Right(sParams[1].size()-strlen("GRADEWORST")) ) - 1; + 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; + } + // group random + else if( sParams[1].Right(1) == "*" ) + { + //new_entry.bSecret = true; + RString sSong = sParams[1]; + sSong.Replace( "\\", "/" ); + std::vector bits; + split( sSong, "/", bits ); + if( bits.size() == 2 ) + { + new_entry.songCriteria.m_sGroupName = bits[0]; + } + else + { + LOG->UserLog( "Course file", sPath, "contains a random_within_group entry \"%s\" that is invalid. " + "Song should be in the format \"/*\".", sSong.c_str() ); + } + + if( !SONGMAN->DoesSongGroupExist(new_entry.songCriteria.m_sGroupName) ) + { + LOG->UserLog( "Course file", sPath, "random_within_group entry \"%s\" specifies a group that doesn't exist. " + "This entry will be ignored.", sSong.c_str() ); + return false; // skip this #SONG + } + } + else + { + RString sSong = sParams[1]; + sSong.Replace( "\\", "/" ); + std::vector bits; + split( sSong, "/", bits ); + + Song *pSong = nullptr; + if( bits.size() == 2 ) + { + new_entry.songCriteria.m_sGroupName = bits[0]; + pSong = SONGMAN->FindSong( bits[0], bits[1] ); + } + else if( bits.size() == 1 ) + { + pSong = SONGMAN->FindSong( "", sSong ); + } + new_entry.songID.FromSong( pSong ); + + if( pSong == nullptr ) + { + LOG->UserLog( "Course file", sPath, "contains a fixed song entry \"%s\" that does not exist. " + "This entry will be ignored.", sSong.c_str()); + return false; // skip this #SONG + } + } + + new_entry.stepsCriteria.m_difficulty = OldStyleStringToDifficulty( sParams[2] ); + //most CRS files use old-style difficulties, but Difficulty enum values can be used in SM5. Test for those too. + if( new_entry.stepsCriteria.m_difficulty == Difficulty_Invalid ) + { + new_entry.stepsCriteria.m_difficulty = StringToDifficulty( sParams[2] ); + } + + if( new_entry.stepsCriteria.m_difficulty == Difficulty_Invalid ) + { + int retval = sscanf( sParams[2], "%d..%d", &new_entry.stepsCriteria.m_iLowMeter, &new_entry.stepsCriteria.m_iHighMeter ); + if( retval == 1 ) + new_entry.stepsCriteria.m_iHighMeter = new_entry.stepsCriteria.m_iLowMeter; + else if( retval != 2 ) + { + LOG->UserLog( "Course file", sPath, "contains an invalid difficulty setting: \"%s\", 3..6 used instead", + sParams[2].c_str() ); + new_entry.stepsCriteria.m_iLowMeter = 3; + new_entry.stepsCriteria.m_iHighMeter = 6; + } + new_entry.stepsCriteria.m_iLowMeter = std::max( new_entry.stepsCriteria.m_iLowMeter, 1 ); + new_entry.stepsCriteria.m_iHighMeter = std::max( new_entry.stepsCriteria.m_iHighMeter, new_entry.stepsCriteria.m_iLowMeter ); + } + + { + // If "showcourse" or "noshowcourse" is in the list, force + // new_entry.secret on or off. + std::vector mods; + split( sParams[3], ",", 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 if( sMod.length() > 5 && !sMod.Left(5).CompareNoCase("award") ) + new_entry.iGainLives = StringToInt( sMod.substr(5) ); + else + continue; + mods.erase( mods.begin() + j ); + } + new_entry.sModifiers = join( ",", mods ); + } + return true; +} /* * (c) 2001-2004 Chris Danford, Glenn Maynard diff --git a/src/CourseLoaderCRS.h b/src/CourseLoaderCRS.h index 5bf8db4369..386a0d4752 100644 --- a/src/CourseLoaderCRS.h +++ b/src/CourseLoaderCRS.h @@ -4,8 +4,10 @@ #define COURSE_LOADER_CRS_H #include "GameConstantsAndTypes.h" +#include "MsdFile.h" class Course; -class MsdFile; +class CourseEntry; +struct AttackArray; /** @brief The Course Loader handles parsing the .crs files. */ namespace CourseLoaderCRS @@ -49,6 +51,9 @@ namespace CourseLoaderCRS * @return its success or failure. */ bool LoadEditFromBuffer( const RString &sBuffer, const RString &sPath, ProfileSlot slot ); + + 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 ); } #endif diff --git a/src/SongUtil.cpp b/src/SongUtil.cpp index 578aa82f8a..35dbdde09c 100644 --- a/src/SongUtil.cpp +++ b/src/SongUtil.cpp @@ -68,6 +68,16 @@ bool SongCriteria::Matches( const Song *pSong ) const if( m_iMaxStagesForSong != -1 && GAMESTATE->GetNumStagesMultiplierForSong(pSong) > m_iMaxStagesForSong ) return false; + if( m_fMaxBPM != -1 && pSong->m_fSpecifiedBPMMax > m_fMaxBPM ) + { + return false; + } + + if( m_fMinBPM != -1 && pSong->m_fSpecifiedBPMMin < m_fMinBPM ) + { + return false; + } + switch( m_Tutorial ) { DEFAULT_FAIL(m_Tutorial); diff --git a/src/SongUtil.h b/src/SongUtil.h index 7d921c61ec..02c7451db5 100644 --- a/src/SongUtil.h +++ b/src/SongUtil.h @@ -33,8 +33,8 @@ public: std::vector m_vpSongAllowedList; /** @brief How many songs does this take max? Don't use this if it's -1. */ 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_fMinBPM; // don't filter if -1 + float m_fMaxBPM; // don't filter if -1 /** @brief Is this song used for tutorial purposes? */ enum Tutorial { @@ -54,7 +54,7 @@ public: SongCriteria(): m_sGroupName(""), m_bUseSongGenreAllowedList(false), m_vsSongGenreAllowedList(), m_Selectable(Selectable_DontCare), m_bUseSongAllowedList(false), m_vpSongAllowedList(), - m_iMaxStagesForSong(-1), m_Tutorial(Tutorial_DontCare), + m_iMaxStagesForSong(-1), m_fMinBPM(-1), m_fMaxBPM(-1), m_Tutorial(Tutorial_DontCare), m_Locked(Locked_DontCare) { // m_fMinBPM = -1; @@ -84,8 +84,8 @@ public: X(m_bUseSongAllowedList) && X(m_vpSongAllowedList) && X(m_iMaxStagesForSong) && - //X(m_fMinBPM) && - //X(m_fMaxBPM) && + X(m_fMinBPM) && + X(m_fMaxBPM) && X(m_Tutorial) && X(m_Locked); #undef X