#include "global.h" #include "CourseUtil.h" #include "Course.h" #include "RageTimer.h" #include "ProfileManager.h" #include "Profile.h" #include "SongManager.h" #include "XmlFile.h" #include "GameState.h" #include "Style.h" #include "Foreach.h" #include "GameState.h" #include "LocalizedString.h" #include "RageLog.h" #include "arch/Dialog/Dialog.h" #include "RageFileManager.h" #include "CourseWriterCRS.h" // Sorting stuff static bool CompareCoursePointersByName( const Course* pCourse1, const Course* pCourse2 ) { RString sName1 = pCourse1->GetDisplayFullTitle(); RString sName2 = pCourse2->GetDisplayFullTitle(); return sName1.CompareNoCase( sName2 ) < 0; } static bool CompareCoursePointersByAutogen( const Course* pCourse1, const Course* pCourse2 ) { int b1 = pCourse1->m_bIsAutogen; int b2 = pCourse2->m_bIsAutogen; if( b1 < b2 ) return true; else if( b1 > b2 ) return false; else return CompareCoursePointersByName(pCourse1,pCourse2); } static bool CompareCoursePointersByDifficulty( const Course* pCourse1, const Course* pCourse2 ) { int iNum1 = pCourse1->GetEstimatedNumStages(); int iNum2 = pCourse2->GetEstimatedNumStages(); if( iNum1 < iNum2 ) return true; else if( iNum1 > iNum2 ) return false; else // iNum1 == iNum2 return CompareCoursePointersByAutogen( pCourse1, pCourse2 ); } static bool CompareCoursePointersByTotalDifficulty( const Course* pCourse1, const Course* pCourse2 ) { int iNum1 = pCourse1->m_SortOrder_TotalDifficulty; int iNum2 = pCourse2->m_SortOrder_TotalDifficulty; if( iNum1 == iNum2 ) return CompareCoursePointersByAutogen( pCourse1, pCourse2 ); return iNum1 < iNum2; } static bool MovePlayersBestToEnd( const Course* pCourse1, const Course* pCourse2 ) { bool C1HasBest = pCourse1->CourseHasBestOrWorst(); bool C2HasBest = pCourse2->CourseHasBestOrWorst(); if( !C1HasBest && !C2HasBest ) return false; if( C1HasBest && !C2HasBest ) return false; if( !C1HasBest && C2HasBest ) return true; return CompareCoursePointersByName( pCourse1, pCourse2 ); } static bool CompareRandom( const Course* pCourse1, const Course* pCourse2 ) { return ( pCourse1->AllSongsAreFixed() && !pCourse2->AllSongsAreFixed() ); } static bool CompareCoursePointersByRanking( const Course* pCourse1, const Course* pCourse2 ) { int iNum1 = pCourse1->m_SortOrder_Ranking; int iNum2 = pCourse2->m_SortOrder_Ranking; if( iNum1 == iNum2 ) return CompareCoursePointersByAutogen( pCourse1, pCourse2 ); return iNum1 < iNum2; } void CourseUtil::SortCoursePointerArrayByDifficulty( vector &vpCoursesInOut ) { sort( vpCoursesInOut.begin(), vpCoursesInOut.end(), CompareCoursePointersByDifficulty ); } void CourseUtil::SortCoursePointerArrayByRanking( vector &vpCoursesInOut ) { for( unsigned i=0; iUpdateCourseStats( GAMESTATE->GetCurrentStyle()->m_StepsType ); sort( vpCoursesInOut.begin(), vpCoursesInOut.end(), CompareCoursePointersByRanking ); } void CourseUtil::SortCoursePointerArrayByTotalDifficulty( vector &vpCoursesInOut ) { for( unsigned i=0; iUpdateCourseStats( GAMESTATE->GetCurrentStyle()->m_StepsType ); sort( vpCoursesInOut.begin(), vpCoursesInOut.end(), CompareCoursePointersByTotalDifficulty ); } // this code isn't ready yet!! #if 0 RString GetSectionNameFromCourseAndSort( const Course *pCourse, SortOrder so ) { if( pCourse == NULL ) return RString(); // more code here } void SortCoursePointerArrayBySectionName( vector &vpCoursesInOut, SortOrder so ) { RString sOther = SORT_OTHER.GetValue(); for(unsigned i = 0; i < vpCoursesInOut.size(); ++i) { RString val = GetSectionNameFromCourseAndSort( vpCoursesInOut[i], so ); /* Make sure 0-9 comes first and OTHER comes last. */ if( val == "0-9" ) val = "0"; else if( val == sOther ) val = "2"; else val = "1" + MakeSortString(val); //g_mapSongSortVal[vpSongsInOut[i]] = val; } } #endif // ok real code begins again static bool CompareCoursePointersByType( const Course* pCourse1, const Course* pCourse2 ) { return pCourse1->GetPlayMode() < pCourse2->GetPlayMode(); } void CourseUtil::SortCoursePointerArrayByType( vector &vpCoursesInOut ) { stable_sort( vpCoursesInOut.begin(), vpCoursesInOut.end(), CompareCoursePointersByType ); } void CourseUtil::MoveRandomToEnd( vector &vpCoursesInOut ) { stable_sort( vpCoursesInOut.begin(), vpCoursesInOut.end(), CompareRandom ); } static map course_sort_val; bool CompareCoursePointersBySortValueAscending( const Course *pSong1, const Course *pSong2 ) { return course_sort_val[pSong1] < course_sort_val[pSong2]; } bool CompareCoursePointersBySortValueDescending( const Course *pSong1, const Course *pSong2 ) { return course_sort_val[pSong1] > course_sort_val[pSong2]; } bool CompareCoursePointersByTitle( const Course *pCourse1, const Course *pCourse2 ) { return CompareCoursePointersByName( pCourse1, pCourse2 ); } void CourseUtil::SortCoursePointerArrayByTitle( vector &vpCoursesInOut ) { sort( vpCoursesInOut.begin(), vpCoursesInOut.end(), CompareCoursePointersByTitle ); } void CourseUtil::SortCoursePointerArrayByAvgDifficulty( vector &vpCoursesInOut ) { RageTimer foo; course_sort_val.clear(); for( unsigned i = 0; i < vpCoursesInOut.size(); ++i ) { int iMeter = vpCoursesInOut[i]->GetMeter( GAMESTATE->GetCurrentStyle()->m_StepsType, Difficulty_Medium ); course_sort_val[vpCoursesInOut[i]] = ssprintf( "%06i", iMeter ); } sort( vpCoursesInOut.begin(), vpCoursesInOut.end(), CompareCoursePointersByTitle ); stable_sort( vpCoursesInOut.begin(), vpCoursesInOut.end(), CompareCoursePointersBySortValueAscending ); stable_sort( vpCoursesInOut.begin(), vpCoursesInOut.end(), MovePlayersBestToEnd ); } void CourseUtil::SortCoursePointerArrayByNumPlays( vector &vpCoursesInOut, ProfileSlot slot, bool bDescending ) { if( !PROFILEMAN->IsPersistentProfile(slot) ) return; // nothing to do since we don't have data Profile* pProfile = PROFILEMAN->GetProfile(slot); SortCoursePointerArrayByNumPlays( vpCoursesInOut, pProfile, bDescending ); } void CourseUtil::SortCoursePointerArrayByNumPlays( vector &vpCoursesInOut, const Profile* pProfile, bool bDescending ) { ASSERT( pProfile ); for(unsigned i = 0; i < vpCoursesInOut.size(); ++i) course_sort_val[vpCoursesInOut[i]] = ssprintf( "%09i", pProfile->GetCourseNumTimesPlayed(vpCoursesInOut[i]) ); stable_sort( vpCoursesInOut.begin(), vpCoursesInOut.end(), bDescending ? CompareCoursePointersBySortValueDescending : CompareCoursePointersBySortValueAscending ); course_sort_val.clear(); } void CourseUtil::SortByMostRecentlyPlayedForMachine( vector &vpCoursesInOut ) { Profile *pProfile = PROFILEMAN->GetMachineProfile(); FOREACH_CONST( Course*, vpCoursesInOut, c ) { int iNumTimesPlayed = pProfile->GetCourseNumTimesPlayed( *c ); RString val = iNumTimesPlayed ? pProfile->GetCourseLastPlayedDateTime(*c).GetString() : "9999999999999"; course_sort_val[*c] = val; } stable_sort( vpCoursesInOut.begin(), vpCoursesInOut.end(), CompareCoursePointersBySortValueAscending ); course_sort_val.clear(); } void CourseUtil::MakeDefaultEditCourseEntry( CourseEntry& out ) { out.songID.FromSong( GAMESTATE->GetDefaultSong() ); out.stepsCriteria.m_difficulty = Difficulty_Medium; } ////////////////////////////////// // Autogen ////////////////////////////////// void CourseUtil::AutogenEndlessFromGroup( const RString &sGroupName, Difficulty diff, Course &out ) { out.m_bIsAutogen = true; out.m_bRepeat = true; out.m_bShuffle = true; out.m_iLives = -1; FOREACH_ENUM( Difficulty,dc) out.m_iCustomMeter[dc] = -1; if( sGroupName == "" ) { out.m_sMainTitle = "All Songs"; // this sounds reasonable... -aj out.m_sBannerPath = THEME->GetPathG("Banner","all music"); } else { out.m_sMainTitle = SONGMAN->ShortenGroupName( sGroupName ); out.m_sBannerPath = SONGMAN->GetSongGroupBannerPath( sGroupName ); } // We want multiple songs, so we can try to prevent repeats during // gameplay. (We might still get a repeat at the repeat boundary, // but that'd be rare.) -glenn CourseEntry e; e.songCriteria.m_sGroupName = sGroupName; e.stepsCriteria.m_difficulty = diff; e.bSecret = true; // Insert a copy of e for each song in the group. out.m_vEntries.insert( out.m_vEntries.end(), SONGMAN->GetSongs(sGroupName).size(), e ); } void CourseUtil::AutogenNonstopFromGroup( const RString &sGroupName, Difficulty diff, Course &out ) { AutogenEndlessFromGroup( sGroupName, diff, out ); out.m_bRepeat = false; out.m_sMainTitle += " Random"; // resize to 4 while( out.m_vEntries.size() < 4 ) out.m_vEntries.push_back( out.m_vEntries[0] ); while( out.m_vEntries.size() > 4 ) out.m_vEntries.pop_back(); } void CourseUtil::AutogenOniFromArtist( const RString &sArtistName, RString sArtistNameTranslit, vector aSongs, Difficulty dc, Course &out ) { out.m_bIsAutogen = true; out.m_bRepeat = false; out.m_bShuffle = true; out.m_bSortByMeter = true; out.m_iLives = 4; FOREACH_ENUM( Difficulty,cd) out.m_iCustomMeter[cd] = -1; ASSERT( sArtistName != "" ); ASSERT( aSongs.size() > 0 ); /* "Artist Oni" is a little repetitive; "by Artist" stands out less, and * lowercasing "by" puts more emphasis on the artist's name. It also sorts * them together. */ out.m_sMainTitle = "by " + sArtistName; if( sArtistNameTranslit != sArtistName ) out.m_sMainTitleTranslit = "by " + sArtistNameTranslit; // How would we handle Artist Oni course banners, anyways? -aj // m_sBannerPath = ""; // XXX /* Shuffle the list to determine which songs we'll use. Shuffle it * deterministically, so we always get the same set of songs unless the * song set changes. */ { RandomGen rng( GetHashForString( sArtistName ) + aSongs.size() ); random_shuffle( aSongs.begin(), aSongs.end(), rng ); } // Only use up to four songs. if( aSongs.size() > 4 ) aSongs.erase( aSongs.begin()+4, aSongs.end() ); CourseEntry e; e.stepsCriteria.m_difficulty = dc; for( unsigned i = 0; i < aSongs.size(); ++i ) { e.songID.FromSong( aSongs[i] ); out.m_vEntries.push_back( e ); } } void CourseUtil::WarnOnInvalidMods( RString sMods ) { PlayerOptions po; SongOptions so; vector vs; split( sMods, ",", vs, true ); FOREACH_CONST( RString, vs, s ) { bool bValid = false; RString sErrorDetail; bValid |= po.FromOneModString( *s, sErrorDetail ); bValid |= so.FromOneModString( *s, sErrorDetail ); /* ==Invalid options that used to be valid== * all noteskins (solo, note, foon, &c.) * protiming (done in Lua now) * ==Things I've seen in real course files== * 900% BRINK (damnit japan) * TISPY * 9000% OVERCOMING (what?) * 9200% TORNADE (it's like a grenade but a tornado) * 50% PROTIMING (HOW THE HELL DOES 50% PROTIMING EVEN WORK) * BREAK */ if( !bValid ) { RString sFullError = ssprintf("Error processing '%s' in '%s'", (*s).c_str(), sMods.c_str() ); if( !sErrorDetail.empty() ) sFullError += ": " + sErrorDetail; LOG->UserLog( "", "", "%s", sFullError.c_str() ); Dialog::OK( sFullError, "INVALID_PLAYER_OPTION_WARNING" ); } } } int EditCourseUtil::MAX_NAME_LENGTH = 16; int EditCourseUtil::MAX_PER_PROFILE = 32; int EditCourseUtil::MIN_WORKOUT_MINUTES = 4; int EditCourseUtil::MAX_WORKOUT_MINUTES = 90; bool EditCourseUtil::s_bNewCourseNeedsName = false; bool EditCourseUtil::Save( Course *pCourse ) { return EditCourseUtil::RenameAndSave( pCourse, pCourse->GetDisplayFullTitle() ); } bool EditCourseUtil::RenameAndSave( Course *pCourse, RString sNewName ) { ASSERT( !sNewName.empty() ); EditCourseUtil::s_bNewCourseNeedsName = false; RString sNewFilePath; if( pCourse->IsAnEdit() ) { sNewFilePath = PROFILEMAN->GetProfileDir(ProfileSlot_Machine) + EDIT_COURSES_SUBDIR + sNewName + ".crs"; } else { RString sDir, sName, sExt; splitpath( pCourse->m_sPath, sDir, sName, sExt ); sNewFilePath = sDir + sNewName + sExt; } // remove the old file if the name is changing if( !pCourse->m_sPath.empty() && sNewFilePath != pCourse->m_sPath ) FILEMAN->Remove( pCourse->m_sPath ); // not fatal if this fails pCourse->m_sMainTitle = sNewName; pCourse->m_sPath = sNewFilePath; return CourseWriterCRS::Write( *pCourse, pCourse->m_sPath, false ); } bool EditCourseUtil::RemoveAndDeleteFile( Course *pCourse ) { if( !FILEMAN->Remove( pCourse->m_sPath ) ) return false; FILEMAN->Remove( pCourse->GetCacheFilePath() ); if( pCourse->IsAnEdit() ) { PROFILEMAN->LoadMachineProfile(); } else { SONGMAN->DeleteCourse( pCourse ); delete pCourse; } return true; } static LocalizedString YOU_MUST_SUPPLY_NAME ( "CourseUtil", "You must supply a name for your course." ); static LocalizedString EDIT_NAME_CONFLICTS ( "CourseUtil", "The name you chose conflicts with another course. Please use a different name." ); static LocalizedString EDIT_NAME_CANNOT_CONTAIN ( "CourseUtil", "The course name cannot contain any of the following characters: %s" ); bool EditCourseUtil::ValidateEditCourseName( const RString &sAnswer, RString &sErrorOut ) { if( sAnswer.empty() ) { sErrorOut = YOU_MUST_SUPPLY_NAME; return false; } static const RString sInvalidChars = "\\/:*?\"<>|"; if( strpbrk(sAnswer, sInvalidChars) != NULL ) { sErrorOut = ssprintf( EDIT_NAME_CANNOT_CONTAIN.GetValue(), sInvalidChars.c_str() ); return false; } // Check for name conflicts vector vpCourses; EditCourseUtil::GetAllEditCourses( vpCourses ); FOREACH_CONST( Course*, vpCourses, p ) { if( GAMESTATE->m_pCurCourse == *p ) continue; // don't comepare name against ourself if( (*p)->GetDisplayFullTitle() == sAnswer ) { sErrorOut = EDIT_NAME_CONFLICTS; return false; } } return true; } void EditCourseUtil::UpdateAndSetTrail() { ASSERT( GAMESTATE->m_pCurStyle ); StepsType st = GAMESTATE->m_pCurStyle->m_StepsType; Trail *pTrail = NULL; if( GAMESTATE->m_pCurCourse ) pTrail = GAMESTATE->m_pCurCourse->GetTrailForceRegenCache( st ); GAMESTATE->m_pCurTrail[PLAYER_1].Set( pTrail ); } void EditCourseUtil::PrepareForPlay() { GAMESTATE->m_pCurSong.Set( NULL ); // CurSong will be set if we back out. Set it back to NULL so that ScreenStage won't show the last song. GAMESTATE->m_PlayMode.Set( PLAY_MODE_ENDLESS ); GAMESTATE->m_bSideIsJoined[0] = true; PROFILEMAN->GetProfile(ProfileSlot_Player1)->m_GoalType = GoalType_Time; Course *pCourse = GAMESTATE->m_pCurCourse; PROFILEMAN->GetProfile(ProfileSlot_Player1)->m_iGoalSeconds = static_cast(pCourse->m_fGoalSeconds); } void EditCourseUtil::GetAllEditCourses( vector &vpCoursesOut ) { vector vpCoursesTemp; SONGMAN->GetAllCourses( vpCoursesTemp, false ); FOREACH_CONST( Course*, vpCoursesTemp, c ) { if( (*c)->GetLoadedFromProfileSlot() != ProfileSlot_Invalid ) vpCoursesOut.push_back( *c ); } } void EditCourseUtil::LoadDefaults( Course &out ) { out = Course(); out.m_fGoalSeconds = 0; // pick a default name // XXX: Make this localizable for( int i=0; i<10000; i++ ) { out.m_sMainTitle = ssprintf("Workout %d", i+1); bool bNameInUse = false; vector vpCourses; EditCourseUtil::GetAllEditCourses( vpCourses ); FOREACH_CONST( Course*, vpCourses, p ) { if( out.m_sMainTitle == (*p)->m_sMainTitle ) { bNameInUse = true; break; } } if( !bNameInUse ) break; } vector vpSongs; SONGMAN->GetPreferredSortSongs( vpSongs ); for( int i=0; i<(int)vpSongs.size() && i<6; i++ ) { CourseEntry ce; ce.songID.FromSong( vpSongs[i] ); ce.stepsCriteria.m_difficulty = Difficulty_Easy; out.m_vEntries.push_back( ce ); } } ////////////////////////////////// // CourseID ////////////////////////////////// void CourseID::FromCourse( const Course *p ) { if( p ) { if( p->m_bIsAutogen ) { sPath = ""; sFullTitle = p->GetTranslitFullTitle(); } else { sPath = p->m_sPath; sFullTitle = ""; } } else { sPath = ""; sFullTitle = ""; } // HACK for backwards compatibility: // Strip off leading "/". 2005/05/21 file layer changes added a leading slash. if( sPath.Left(1) == "/" ) sPath.erase( sPath.begin() ); m_Cache.Unset(); } Course *CourseID::ToCourse() const { // HACK for backwards compatibility: // Re-add the leading "/". 2005/05/21 file layer changes added a leading slash. RString sPath2 = sPath; if( sPath2.Left(1) != "/" ) sPath2 = "/" + sPath2; Course *pCourse = NULL; if( m_Cache.Get(&pCourse) ) return pCourse; if( pCourse == NULL && !sPath2.empty() ) pCourse = SONGMAN->GetCourseFromPath( sPath2 ); if( pCourse == NULL && !sFullTitle.empty() ) pCourse = SONGMAN->GetCourseFromName( sFullTitle ); m_Cache.Set( pCourse ); return pCourse; } XNode* CourseID::CreateNode() const { XNode* pNode = new XNode( "Course" );; if( !sPath.empty() ) pNode->AppendAttr( "Path", sPath ); if( !sFullTitle.empty() ) pNode->AppendAttr( "FullTitle", sFullTitle ); return pNode; } void CourseID::LoadFromNode( const XNode* pNode ) { ASSERT( pNode->GetName() == "Course" ); sFullTitle = RString(); sPath = RString(); if( !pNode->GetAttrValue("Path", sPath) ) pNode->GetAttrValue( "FullTitle", sFullTitle ); m_Cache.Unset(); } RString CourseID::ToString() const { if( !sPath.empty() ) return sPath; if( !sFullTitle.empty() ) return sFullTitle; return RString(); } bool CourseID::IsValid() const { return !sPath.empty() || !sFullTitle.empty(); } /* * (c) 2001-2004 Chris Danford * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, and/or sell copies of the Software, and to permit persons to * whom the Software is furnished to do so, provided that the above * copyright notice(s) and this permission notice appear in all copies of * the Software and that both the above copyright notice(s) and this * permission notice appear in supporting documentation. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF * THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS * INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT * OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS * OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR * PERFORMANCE OF THIS SOFTWARE. */