Added BPMRANGE song option for Courses.

Started breaking out some of the logic in CourseLoaderCRS into smaller functions for readibility.
This commit is contained in:
Michael Votaw
2024-03-06 01:45:26 -08:00
committed by teejusb
parent 93f27a4ae2
commit f44357c3f5
4 changed files with 252 additions and 204 deletions
+231 -198
View File
@@ -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<RString> 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<RString> 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 \"<group>/*\".", 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<RString> 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<RString> 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<RString> 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<RString> 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<RString> 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 \"<group>/*\".", 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<RString> 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<RString> 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