Added additional criteria to StepsCriteria and SongCriteria

Added new m_sSongName property to Song class
Slight refactor in Song::LoadSongFromDir() to set m_sSongName
Implemented the basics of #SONGSELECT course parameter
This commit is contained in:
Michael Votaw
2024-03-06 01:45:26 -08:00
committed by teejusb
parent 343f2de657
commit be87de401a
9 changed files with 257 additions and 37 deletions
+1 -1
View File
@@ -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) );
+200 -22
View File
@@ -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<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;
@@ -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<RString> 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<RString> 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<RString> 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<RString> 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<RString> 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<RString> difficultyStrs;
std::vector<Difficulty> 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<RString> 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<RString> 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<RString> 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<RString> 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.
+2
View File
@@ -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
+5 -10
View File
@@ -295,11 +295,13 @@ bool Song::LoadFromSongDir(RString sDir, bool load_autosave, ProfileSlot from_pr
bool use_cache = true;
std::vector<RString> 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<RString> 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<RString> 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;
+3
View File
@@ -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.
+25 -2
View File
@@ -32,7 +32,20 @@ ThemeMetric<bool> 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);
+13 -2
View File
@@ -26,6 +26,9 @@ public:
*
* If an empty string, don't bother using this for searching. */
std::vector<RString> m_vsGroupNames;
std::vector<RString> m_vsSongNames;
std::vector<RString> m_vsArtistNames;
bool m_bUseSongGenreAllowedList;
std::vector<RString> 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
+6
View File
@@ -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 )
+2
View File
@@ -22,6 +22,7 @@ public:
*
* Don't filter here if the Difficulty is Difficulty_Invalid. */
Difficulty m_difficulty;
std::vector<Difficulty> 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)
{