diff --git a/Docs/CourseFormat.txt b/Docs/CourseFormat.txt index dbf024c191..c91941425f 100644 --- a/Docs/CourseFormat.txt +++ b/Docs/CourseFormat.txt @@ -111,15 +111,18 @@ Complete Course Template -- - Unlike the #SONG tag, this will not take into account group names. -- If you want to specify a specific song from a specific group, use -- this along with the GROUP parameter. --- - This does not support names that have commmas. +-- - Commas, and other control characters (`#`,`;`, `=`, and `:`) can be used, +-- but they must be escaped (like `\#`, `\,`, etc). -- Example: #SONGSELECT:TITLE=Goin' Under,Springtime; +#SONGSELECT:TITLE=thank u\, next; -- GROUP= A list of one or more song groups, separated by commas. -- Notes: -- - The "group" refers to the directory name of the song group. --- - This does not support folders that have commas in their name. -- - Group names must be an *exact match*. +-- - Commas, and other control characters (`#`,`;`, `=`, and `:`) can be used, +-- but they must be escaped (like `\#`, `\,`, etc). -- Example: #SONGSELECT:GROUP=Stepmania 5,In The Groove; @@ -127,13 +130,16 @@ Complete Course Template -- Notes: -- - The "artist" refers to either the #ARTIST or #ARTISTTRANSLIT value -- of the simfile. --- - This does not support artist names that contain commas. +-- - Commas, and other control characters (`#`,`;`, `=`, and `:`) can be used, +-- but they must be escaped (like `\#`, `\,`, etc). -- Example: #SONGSELECT:ARTIST=Kommisar,NegaRen; -- GENRE= A list of one or more song genres, separated by commas. -- Notes: -- - The "genre" refers to the #GENRE tag of a song. +-- - Commas, and other control characters (`#`,`;`, `=`, and `:`) can be used, +-- but they must be escaped (like `\#`, `\,`, etc). -- - Most songs don't actually have a #GENRE defined, which unfortunately -- makes this parameter not terribly useful at this time. -- Example: diff --git a/src/CourseLoaderCRS.cpp b/src/CourseLoaderCRS.cpp index 370fe7c15b..89fdab2674 100644 --- a/src/CourseLoaderCRS.cpp +++ b/src/CourseLoaderCRS.cpp @@ -32,6 +32,25 @@ const char *g_CRSDifficultyNames[] = "Edit", }; +// This is a convenience function to handle splitting a string that potentially has +// escaped delimitor characters. +// We need to split the string while still maintaining those escaped characters. +// So first, replace escaped characters with "||escaped-delim||" +// Then, replace any remaining delimitor characters with "||regular-delim||" +// Then, put the escaped characters back, by replacing "||escaped-delim||" with "\\" + sDelimitor +// And finally, split the string by instances of "||regular-delim||" +// So for instance, "Thing 1, Thing\,2" becomes "Thing 1||regular-delim||Thing \, 2" +void split_minding_escaped_delims(const RString &sSource, const RString &sDelimitor, std::vector& asAddit) +{ + RString sourceCopy = sSource; + RString escaped_delim = "||escaped-delim||"; + RString regular_delim = "||regular-delim||"; + sourceCopy.Replace("\\" + sDelimitor, escaped_delim); + sourceCopy.Replace(sDelimitor, regular_delim); + sourceCopy.Replace(escaped_delim, "\\" + sDelimitor); + split(sourceCopy, regular_delim, asAddit); +} + /** * @brief Retrieve the course difficulty based on the string name. * @param s the name of the difficulty. @@ -531,7 +550,8 @@ bool CourseLoaderCRS::ParseCourseSongSelect(const MsdFile::value_t &sParams, Cou for( unsigned i = 1; i < sParams.params.size(); ++i ) { std::vector sParamParts; - split(sParams[i], "=", sParamParts); + split_minding_escaped_delims(sParams[i], "=", sParamParts); + if( sParamParts.size() != 2 ) { LOG->UserLog( "Course file", sPath, "has an invalid SONGSELECT sub-parameter, \"%s\"", sParams[i].c_str()); @@ -688,9 +708,8 @@ bool CourseLoaderCRS::ParseCommaSeparatedList(const RString &sParamValue, std::v // Because we're dealing with a comma-separated list, we have to handle any escaped commas // as a special case. Hopefully nobody writes a song called "||escaped-comma||" RString unescapedParamValue = sParamValue; - unescapedParamValue.Replace("\\,", "||escaped-comma||"); - unescapedParamValue = SmUnescape(unescapedParamValue); - split(unescapedParamValue, ",", items); + + split_minding_escaped_delims(sParamValue, ",", items); if(items.size() == 0) { LOG->UserLog( "Course file", sPath, "has an invalid %s parameter, expected at least one sub-parameter, but found none.", sParamName.c_str() ); @@ -698,7 +717,7 @@ bool CourseLoaderCRS::ParseCommaSeparatedList(const RString &sParamValue, std::v } for (unsigned long i = 0; i < items.size(); i++) { - items[i].Replace("||escaped-comma||", ","); + items[i] = SmUnescape(items[i]); Trim(items[i]); } dest.insert(dest.end(), items.begin(), items.end()); diff --git a/src/CourseWriterCRS.cpp b/src/CourseWriterCRS.cpp index af728657ad..54c6b019dd 100644 --- a/src/CourseWriterCRS.cpp +++ b/src/CourseWriterCRS.cpp @@ -229,23 +229,23 @@ bool CourseWriterCRS::WriteSongSelectCourseEntry( const CourseEntry &entry, Rage //TODO: Re-escape everything if( entry.songCriteria.m_vsSongNames.size() > 0 ) { - RString songNames = join(",", SmEscape( entry.songCriteria.m_vsSongNames, {'\\', ':', ';', '#', ','} )); + RString songNames = join(",", SmEscape( entry.songCriteria.m_vsSongNames, {'\\', ':', ';', '#', ',', '='} )); songSelectParams.push_back(ssprintf("TITLE=%s", songNames.c_str())); } if( entry.songCriteria.m_vsGroupNames.size() > 0) { - RString groupNames = join(",", SmEscape( entry.songCriteria.m_vsGroupNames, {'\\', ':', ';', '#', ','} )); + RString groupNames = join(",", SmEscape( entry.songCriteria.m_vsGroupNames, {'\\', ':', ';', '#', ',', '='} )); songSelectParams.push_back(ssprintf("GROUP=%s", groupNames.c_str())); } if( entry.songCriteria.m_vsArtistNames.size() > 0) { - RString artistNames = join(",", SmEscape( entry.songCriteria.m_vsArtistNames, {'\\', ':', ';', '#', ','} )); + RString artistNames = join(",", SmEscape( entry.songCriteria.m_vsArtistNames, {'\\', ':', ';', '#', ',', '='} )); songSelectParams.push_back(ssprintf("ARTIST=%s", artistNames.c_str())); } if( entry.songCriteria.m_bUseSongAllowedList && entry.songCriteria.m_vsSongGenreAllowedList.size() > 0) { - RString genreNames = join(",", SmEscape( entry.songCriteria.m_vsSongGenreAllowedList, {'\\', ':', ';', '#', ','} )); + RString genreNames = join(",", SmEscape( entry.songCriteria.m_vsSongGenreAllowedList, {'\\', ':', ';', '#', ',', '='} )); songSelectParams.push_back(ssprintf("GENRE=%s", genreNames.c_str())); } if( entry.stepsCriteria.m_vDifficulties.size() > 0 )