#include "global.h" #include "SongManager.h" #include "arch/LoadingWindow/LoadingWindow.h" #include "ActorUtil.h" #include "AnnouncerManager.h" #include "BackgroundUtil.h" #include "ImageCache.h" #include "CommonMetrics.h" #include "Course.h" #include "CourseLoaderCRS.h" #include "CourseUtil.h" #include "GameManager.h" #include "GameState.h" #include "LocalizedString.h" #include "MemoryCardManager.h" #include "MsdFile.h" #include "NoteSkinManager.h" #include "NotesLoaderDWI.h" #include "NotesLoaderSSC.h" #include "NotesLoaderSM.h" #include "PrefsManager.h" #include "Profile.h" #include "ProfileManager.h" #include "RageFile.h" #include "RageFileManager.h" #include "RageLog.h" #include "Song.h" #include "SongCacheIndex.h" #include "SongUtil.h" #include "Sprite.h" #include "StatsManager.h" #include "Steps.h" #include "StepsUtil.h" #include "Style.h" #include "ThemeManager.h" #include "TitleSubstitution.h" #include "TrailUtil.h" #include "UnlockManager.h" #include "SpecialFiles.h" #include #include #include SongManager* SONGMAN = nullptr; // global and accessible from anywhere in our program /** @brief The file that contains various random attacks. */ const RString ATTACK_FILE = "/Data/RandomAttacks.txt"; const RString EDIT_SUBDIR = "Edits/"; static const ThemeMetric EXTRA_COLOR ( "SongManager", "ExtraColor" ); static const ThemeMetric EXTRA_COLOR_METER ( "SongManager", "ExtraColorMeter" ); static const ThemeMetric USE_PREFERRED_SORT_COLOR ( "SongManager", "UsePreferredSortColor" ); static const ThemeMetric USE_UNLOCK_COLOR ( "SongManager", "UseUnlockColor" ); static const ThemeMetric UNLOCK_COLOR ( "SongManager", "UnlockColor" ); static const ThemeMetric MOVE_UNLOCKS_TO_BOTTOM_OF_PREFERRED_SORT ( "SongManager", "MoveUnlocksToBottomOfPreferredSort" ); static const ThemeMetric EXTRA_STAGE2_DIFFICULTY_MAX ( "SongManager", "ExtraStage2DifficultyMax" ); static Preference g_sDisabledSongs( "DisabledSongs", "" ); static Preference g_bHideIncompleteCourses( "HideIncompleteCourses", false ); RString SONG_GROUP_COLOR_NAME( std::size_t i ) { return ssprintf( "SongGroupColor%i", (int) i+1 ); } RString COURSE_GROUP_COLOR_NAME( std::size_t i ) { return ssprintf( "CourseGroupColor%i", (int) i+1 ); } RString profile_song_group_color_name(std::size_t i) { return ssprintf("ProfileSongGroupColor%i", (int)i+1); } static const float next_loading_window_update= 0.02f; SongManager::SongManager() { // Register with Lua. { Lua *L = LUA->Get(); lua_pushstring( L, "SONGMAN" ); this->PushSelf( L ); lua_settable( L, LUA_GLOBALSINDEX ); LUA->Release( L ); } NUM_SONG_GROUP_COLORS .Load( "SongManager", "NumSongGroupColors" ); SONG_GROUP_COLOR .Load( "SongManager", SONG_GROUP_COLOR_NAME, NUM_SONG_GROUP_COLORS ); NUM_COURSE_GROUP_COLORS .Load( "SongManager", "NumCourseGroupColors" ); COURSE_GROUP_COLOR .Load( "SongManager", COURSE_GROUP_COLOR_NAME, NUM_COURSE_GROUP_COLORS ); num_profile_song_group_colors.Load("SongManager", "NumProfileSongGroupColors"); profile_song_group_colors.Load("SongManager", profile_song_group_color_name, num_profile_song_group_colors); } SongManager::~SongManager() { // Unregister with Lua. LUA->UnsetGlobal( "SONGMAN" ); // Courses depend on Songs and Songs don't depend on Courses. // So, delete the Courses first. FreeCourses(); FreeSongs(); } void SongManager::InitAll( LoadingWindow *ld, bool onlyAdditions ) { std::vector never_cache; split(PREFSMAN->m_NeverCacheList, ",", never_cache); for(std::vector::iterator group= never_cache.begin(); group != never_cache.end(); ++group) { m_GroupsToNeverCache.insert(*group); } InitSongsFromDisk( ld, onlyAdditions ); InitCoursesFromDisk( ld, onlyAdditions ); if (onlyAdditions) { DeleteAutogenCourses(); } InitAutogenCourses(); InitRandomAttacks(); } static LocalizedString RELOADING ( "SongManager", "Reloading..." ); static LocalizedString UNLOADING_SONGS ( "SongManager", "Unloading songs..." ); static LocalizedString UNLOADING_COURSES ( "SongManager", "Unloading courses..." ); static LocalizedString SANITY_CHECKING_GROUPS("SongManager", "Sanity checking groups..."); void SongManager::Reload( bool bAllowFastLoad, LoadingWindow *ld ) { FILEMAN->FlushDirCache( SpecialFiles::SONGS_DIR ); FILEMAN->FlushDirCache( SpecialFiles::COURSES_DIR ); FILEMAN->FlushDirCache( EDIT_SUBDIR ); if( ld ) ld->SetText( RELOADING ); // save scores before unloading songs, or the scores will be lost PROFILEMAN->SaveMachineProfile(); GAMESTATE->SavePlayerProfiles(); std::vector> rejoinPlayers; FOREACH_HumanPlayer(pn) { if (GAMESTATE->m_bSideIsJoined[pn]) { if (PROFILEMAN->ProfileWasLoadedFromMemoryCard(pn)) { rejoinPlayers.push_back(std::make_tuple(pn, RString(""), true)); } else if (PROFILEMAN->IsPersistentProfile(pn)) { int numLocalProfiles = PROFILEMAN->GetNumLocalProfiles(); for (int i = 0; i < numLocalProfiles; i++) { Profile *profile = PROFILEMAN->GetLocalProfileFromIndex(i); if (profile->m_sGuid == PROFILEMAN->GetProfile(pn)->m_sGuid) { RString profileID = PROFILEMAN->GetLocalProfileIDFromIndex(i); rejoinPlayers.push_back(std::make_tuple(pn, profileID, false)); break; } } } else { rejoinPlayers.push_back(std::make_tuple(pn, RString(""), false)); } GAMESTATE->UnjoinPlayer(pn); PROFILEMAN->UnloadProfile(pn); } } if( ld ) ld->SetText( UNLOADING_COURSES ); FreeCourses(); if( ld ) ld->SetText( UNLOADING_SONGS ); FreeSongs(); const bool oldVal = PREFSMAN->m_bFastLoad; if( !bAllowFastLoad ) PREFSMAN->m_bFastLoad.Set( false ); InitAll( ld, /*onlyAdditions=*/false ); // reload scores and unlocks afterward PROFILEMAN->LoadMachineProfile(); MEMCARDMAN->WaitForCheckingToComplete(); for (auto& it : rejoinPlayers) { PlayerNumber pn; RString profileID; bool isMemoryCard; std::tie(pn, profileID, isMemoryCard) = it; GAMESTATE->JoinPlayer(pn); if (isMemoryCard) { bool success = MEMCARDMAN->MountCard(pn); if (success) { PROFILEMAN->LoadProfileFromMemoryCard(pn, true); MEMCARDMAN->UnmountCard(pn); } } else { PROFILEMAN->m_sDefaultLocalProfileID[pn].Set(profileID); PROFILEMAN->LoadLocalProfileFromMachine(pn); } GAMESTATE->LoadCurrentSettingsFromProfile(pn); } UNLOCKMAN->Reload(); if( !bAllowFastLoad ) PREFSMAN->m_bFastLoad.Set( oldVal ); UpdatePreferredSort(); } void SongManager::LoadAdditions( LoadingWindow *ld ) { FILEMAN->FlushDirCache( SpecialFiles::SONGS_DIR ); FILEMAN->FlushDirCache( SpecialFiles::COURSES_DIR ); FILEMAN->FlushDirCache( EDIT_SUBDIR ); InitAll( ld, /*onlyAdditions=*/true ); UNLOCKMAN->Reload(); UpdatePreferredSort(); } void SongManager::InitSongsFromDisk( LoadingWindow *ld, bool onlyAdditions ) { RageTimer tm; // Tell SONGINDEX to not write the cache index file every time a song adds // an entry. -Kyz SONGINDEX->delay_save_cache = true; IMAGECACHE->delay_save_cache = true; LoadSongDir( SpecialFiles::SONGS_DIR, ld, onlyAdditions ); LoadEnabledSongsFromPref(); SONGINDEX->SaveCacheIndex(); SONGINDEX->delay_save_cache = false; IMAGECACHE->WriteToDisk(); IMAGECACHE->delay_save_cache = false; LOG->Trace( "Found %d songs in %f seconds.", (int)m_pSongs.size(), tm.GetDeltaTime() ); } static LocalizedString FOLDER_CONTAINS_MUSIC_FILES( "SongManager", "The folder \"%s\" appears to be a song folder. All song folders must reside in a group folder. For example, \"Songs/Originals/My Song\"." ); void SongManager::SanityCheckGroupDir( RString sDir ) const { // Check to see if they put a song directly inside the group folder. std::vector arrayFiles; GetDirListing( sDir + "/*", arrayFiles ); const std::vector& audio_exts= ActorUtil::GetTypeExtensionList(FT_Sound); for (RString &fname : arrayFiles) { const RString ext= GetExtension(fname); for (RString const &aud : audio_exts) { if(ext == aud) { RageException::Throw( FOLDER_CONTAINS_MUSIC_FILES.GetValue(), sDir.c_str()); } } } } void SongManager::AddGroup( RString sDir, RString sGroupDirName ) { unsigned j; for(j = 0; j < m_sSongGroupNames.size(); ++j) if( sGroupDirName == m_sSongGroupNames[j] ) break; if( j != m_sSongGroupNames.size() ) return; // the group is already added // Look for a group banner in this group folder std::vector arrayGroupBanners; GetDirListing( sDir+sGroupDirName+"/*.png", arrayGroupBanners ); GetDirListing( sDir+sGroupDirName+"/*.jpg", arrayGroupBanners ); GetDirListing( sDir+sGroupDirName+"/*.jpeg", arrayGroupBanners ); GetDirListing( sDir+sGroupDirName+"/*.gif", arrayGroupBanners ); GetDirListing( sDir+sGroupDirName+"/*.bmp", arrayGroupBanners ); RString sBannerPath; if( !arrayGroupBanners.empty() ) sBannerPath = sDir+sGroupDirName+"/"+arrayGroupBanners[0] ; else { // Look for a group banner in the parent folder GetDirListing( sDir+sGroupDirName+".png", arrayGroupBanners ); GetDirListing( sDir+sGroupDirName+".jpg", arrayGroupBanners ); GetDirListing( sDir+sGroupDirName+".jpeg", arrayGroupBanners ); GetDirListing( sDir+sGroupDirName+".gif", arrayGroupBanners ); GetDirListing( sDir+sGroupDirName+".bmp", arrayGroupBanners ); if( !arrayGroupBanners.empty() ) sBannerPath = sDir+arrayGroupBanners[0]; } /* Other group graphics are a bit trickier, and usually don't exist. * A themer has a few options, namely checking the aspect ratio and * operating on it. -aj * TODO: Once the files are implemented in Song, bring the extensions * from there into here. -aj */ // Group background //vector arrayGroupBackgrounds; //GetDirListing( sDir+sGroupDirName+"/*-bg.png", arrayGroupBanners ); //GetDirListing( sDir+sGroupDirName+"/*-bg.jpg", arrayGroupBanners ); //GetDirListing( sDir+sGroupDirName+"/*-bg.jpeg", arrayGroupBanners ); //GetDirListing( sDir+sGroupDirName+"/*-bg.gif", arrayGroupBanners ); //GetDirListing( sDir+sGroupDirName+"/*-bg.bmp", arrayGroupBanners ); /* RString sBackgroundPath; if( !arrayGroupBackgrounds.empty() ) sBackgroundPath = sDir+sGroupDirName+"/"+arrayGroupBackgrounds[0]; else { // Look for a group background in the parent folder GetDirListing( sDir+sGroupDirName+"-bg.png", arrayGroupBackgrounds ); GetDirListing( sDir+sGroupDirName+"-bg.jpg", arrayGroupBackgrounds ); GetDirListing( sDir+sGroupDirName+"-bg.jpeg", arrayGroupBackgrounds ); GetDirListing( sDir+sGroupDirName+"-bg.gif", arrayGroupBackgrounds ); GetDirListing( sDir+sGroupDirName+"-bg.bmp", arrayGroupBackgrounds ); if( !arrayGroupBackgrounds.empty() ) sBackgroundPath = sDir+arrayGroupBackgrounds[0]; } */ /* LOG->Trace( "Group banner for '%s' is '%s'.", sGroupDirName.c_str(), sBannerPath != ""? sBannerPath.c_str():"(none)" ); */ m_sSongGroupNames.push_back( sGroupDirName ); m_sSongGroupBannerPaths.push_back( sBannerPath ); //m_sSongGroupBackgroundPaths.push_back( sBackgroundPath ); } static LocalizedString LOADING_SONGS ( "SongManager", "Loading songs..." ); void SongManager::LoadSongDir( RString sDir, LoadingWindow *ld, bool onlyAdditions ) { if( ld ) ld->SetText( LOADING_SONGS ); // Compositors and other stuff can impose some overhead on updating the // loading window, which slows down startup time for some people. // loading_window_last_update_time provides a timer so the loading window // isn't updated after every song and course. -Kyz RageTimer loading_window_last_update_time; loading_window_last_update_time.Touch(); // Make sure sDir has a trailing slash. if( sDir.Right(1) != "/" ) sDir += "/"; // Find all group directories in "Songs" folder std::vector arrayGroupDirs; GetDirListing( sDir+"*", arrayGroupDirs, true ); SortRStringArray( arrayGroupDirs ); StripCvsAndSvn( arrayGroupDirs ); StripMacResourceForks( arrayGroupDirs ); std::vector> arrayGroupSongDirs; int groupIndex, songCount, songIndex; groupIndex = 0; songCount = 0; if(ld) { ld->SetIndeterminate(false); ld->SetTotalWork(arrayGroupDirs.size()); } int sanity_index= 0; for (RString const &sGroupDirName : arrayGroupDirs) // foreach dir in /Songs/ { if(ld && loading_window_last_update_time.Ago() > next_loading_window_update) { loading_window_last_update_time.Touch(); ld->SetProgress(sanity_index); ld->SetText(SANITY_CHECKING_GROUPS.GetValue() + ssprintf("\n%s", Basename(sGroupDirName).c_str())); } // TODO: If this check fails, log a warning instead of crashing. SanityCheckGroupDir(sDir+sGroupDirName); // Find all Song folders in this group directory std::vector arraySongDirs; GetDirListing( sDir+sGroupDirName + "/*", arraySongDirs, true, true ); StripCvsAndSvn( arraySongDirs ); StripMacResourceForks( arraySongDirs ); SortRStringArray( arraySongDirs ); arrayGroupSongDirs.push_back(arraySongDirs); songCount += arraySongDirs.size(); } if( songCount==0 ) return; if( ld ) { ld->SetIndeterminate( false ); ld->SetTotalWork( songCount ); } groupIndex = 0; songIndex = 0; for (RString const &sGroupDirName : arrayGroupDirs) // foreach dir in /Songs/ { std::vector &arraySongDirs = arrayGroupSongDirs[groupIndex++]; LOG->Trace("Attempting to load %i songs from \"%s\"", int(arraySongDirs.size()), (sDir+sGroupDirName).c_str() ); int loaded = 0; SongPointerVector& index_entry = m_mapSongGroupIndex[sGroupDirName]; RString group_base_name= Basename(sGroupDirName); for( unsigned j=0; j< arraySongDirs.size(); ++j ) // for each song dir { RString sSongDirName = arraySongDirs[j]; // Skip already loaded songs if onlyAdditions is set. if (onlyAdditions) { SongID songID; songID.FromString(sSongDirName); if (songID.ToSong() != nullptr) continue; } // this is a song directory. Load a new song. if(ld && loading_window_last_update_time.Ago() > next_loading_window_update) { loading_window_last_update_time.Touch(); ld->SetProgress(songIndex); ld->SetText( LOADING_SONGS.GetValue() + ssprintf("\n%s\n%s", group_base_name.c_str(), Basename(sSongDirName).c_str() ) ); } Song* pNewSong = new Song; if( !pNewSong->LoadFromSongDir( sSongDirName ) ) { // The song failed to load. delete pNewSong; continue; } AddSongToList(pNewSong); index_entry.push_back( pNewSong ); loaded++; songIndex++; } LOG->Trace("Loaded %i songs from \"%s\"", loaded, (sDir+sGroupDirName).c_str() ); // Don't add the group name if we didn't load any songs in this group. if(!loaded) continue; // Add this group to the group array. AddGroup(sDir, sGroupDirName); // Cache and load the group banner. (and background if it has one -aj) IMAGECACHE->CacheImage( "Banner", GetSongGroupBannerPath(sGroupDirName) ); // Load the group sym links (if any) LoadGroupSymLinks(sDir, sGroupDirName); } if( ld ) { ld->SetIndeterminate( true ); } } // Instead of "symlinks", songs should have membership in multiple groups. -Chris void SongManager::LoadGroupSymLinks(RString sDir, RString sGroupFolder) { // Find all symlink files in this folder std::vector arraySymLinks; GetDirListing( sDir+sGroupFolder+"/*.include", arraySymLinks, false ); SortRStringArray( arraySymLinks ); SongPointerVector& index_entry = m_mapSongGroupIndex[sGroupFolder]; for( unsigned s=0; s< arraySymLinks.size(); s++ ) // for each symlink in this dir, add it in as a song. { MsdFile msdF; msdF.ReadFile( sDir+sGroupFolder+"/"+arraySymLinks[s].c_str(), false ); // don't unescape RString sSymDestination = msdF.GetParam(0,1); // Should only be 1 value & param...period. Song* pNewSong = new Song; if( !pNewSong->LoadFromSongDir( sSymDestination ) ) { delete pNewSong; // The song failed to load. } else { const std::vector& vpSteps = pNewSong->GetAllSteps(); while( vpSteps.size() ) pNewSong->DeleteSteps( vpSteps[0] ); FOREACH_BackgroundLayer( i ) pNewSong->GetBackgroundChanges(i).clear(); pNewSong->m_bIsSymLink = true; // Very important so we don't double-parse later pNewSong->m_sGroupName = sGroupFolder; AddSongToList(pNewSong); index_entry.push_back( pNewSong ); } } } void SongManager::PreloadSongImages() { if( PREFSMAN->m_ImageCache != IMGCACHE_FULL ) return; /* Load textures before unloading old ones, so we don't reload textures * that we don't need to. */ RageTexturePreloader preload; const std::vector &songs = GetAllSongs(); for( unsigned i = 0; i < songs.size(); ++i ) { if( !songs[i]->HasBanner() ) continue; const RageTextureID ID = Sprite::SongBannerTexture( songs[i]->GetBannerPath() ); preload.Load( ID ); } std::vector courses; GetAllCourses( courses, false ); for( unsigned i = 0; i < courses.size(); ++i ) { if( !courses[i]->HasBanner() ) continue; const RageTextureID ID = Sprite::SongBannerTexture( courses[i]->GetBannerPath() ); preload.Load( ID ); } preload.Swap( m_TexturePreload ); } void SongManager::FreeSongs() { m_sSongGroupNames.clear(); m_sSongGroupBannerPaths.clear(); //m_sSongGroupBackgroundPaths.clear(); for (Song *song : m_pSongs) { SAFE_DELETE( song ); } m_pSongs.clear(); m_SongsByDir.clear(); // also free the songs that have been deleted from disk for ( unsigned i=0; i* songVectors[3] = { &m_pSongs, &m_pPopularSongs, &m_pShuffledSongs }; for (int songVecIdx=0; songVecIdx<3; ++songVecIdx) { std::vector& v = *songVectors[songVecIdx]; for (std::size_t i=0; i &AddTo ) const { AddTo.insert(AddTo.end(), m_sSongGroupNames.begin(), m_sSongGroupNames.end() ); } bool SongManager::DoesSongGroupExist( RString sSongGroup ) const { return find( m_sSongGroupNames.begin(), m_sSongGroupNames.end(), sSongGroup ) != m_sSongGroupNames.end(); } RageColor SongManager::GetSongGroupColor( const RString &sSongGroup ) const { for( unsigned i=0; iGetProfile(pn); if(prof != nullptr) { if(prof->GetDisplayNameOrHighScoreName() == sSongGroup) { return profile_song_group_colors.GetValue(pn % num_profile_song_group_colors); } } } LuaHelpers::ReportScriptErrorFmt("requested color for song group '%s' that doesn't exist",sSongGroup.c_str()); return RageColor(1,1,1,1); } RageColor SongManager::GetSongColor( const Song* pSong ) const { ASSERT( pSong != nullptr ); // protected by royal freem corporation. any modification/removal of // this code will result in prosecution. if( pSong->m_sMainTitle == "DVNO") return RageColor(1.0f,0.8f,0.0f,1.0f); // end royal freem protection // Use unlock color if applicable const UnlockEntry *pUE = UNLOCKMAN->FindSong( pSong ); if( pUE && USE_UNLOCK_COLOR.GetValue() ) return UNLOCK_COLOR.GetValue(); if( USE_PREFERRED_SORT_COLOR ) { int sortIndex = 0; for (PreferredSortSection const &v : m_vPreferredSongSort) { if (std::any_of(v.vpSongs.begin(), v.vpSongs.end(), [&](Song const *s) { return s == pSong; })) { return SONG_GROUP_COLOR.GetValue( sortIndex % NUM_SONG_GROUP_COLORS ); } sortIndex += 1; } int i = m_vPreferredSongSort.size(); return SONG_GROUP_COLOR.GetValue( i%NUM_SONG_GROUP_COLORS ); } else // TODO: Have a better fallback plan with colors? { /* XXX: Previously, this matched all notes, which set a song to "extra" * if it had any 10-foot steps at all, even edits or doubles. * * For now, only look at notes for the current note type. This means * that if a song has 10-foot steps on Doubles, it'll only show up red * in Doubles. That's not too bad, I think. This will also change it * in the song scroll, which is a little odd but harmless. * * XXX: Ack. This means this function can only be called when we have * a style set up, which is too restrictive. How to handle this? */ //const StepsType st = GAMESTATE->GetCurrentStyle()->m_StepsType; const std::vector& vpSteps = pSong->GetAllSteps(); for( unsigned i=0; iGetDifficulty() ) { case Difficulty_Challenge: case Difficulty_Edit: continue; default: break; } //if(pSteps->m_StepsType != st) // continue; if( pSteps->GetMeter() >= EXTRA_COLOR_METER ) return (RageColor)EXTRA_COLOR; } return GetSongGroupColor( pSong->m_sGroupName ); } } RString SongManager::GetCourseGroupBannerPath( const RString &sCourseGroup ) const { std::map::const_iterator iter = m_mapCourseGroupToInfo.find( sCourseGroup ); if( iter == m_mapCourseGroupToInfo.end() ) { ASSERT_M( 0, ssprintf("requested banner for course group '%s' that doesn't exist",sCourseGroup.c_str()) ); return RString(); } else { return iter->second.m_sBannerPath; } } void SongManager::GetCourseGroupNames( std::vector &AddTo ) const { for (std::pair const &iter : m_mapCourseGroupToInfo) AddTo.push_back( iter.first ); } bool SongManager::DoesCourseGroupExist( const RString &sCourseGroup ) const { return m_mapCourseGroupToInfo.find( sCourseGroup ) != m_mapCourseGroupToInfo.end(); } RageColor SongManager::GetCourseGroupColor( const RString &sCourseGroup ) const { int iIndex = 0; for (std::pair const &iter : m_mapCourseGroupToInfo) { if( iter.first == sCourseGroup ) return SONG_GROUP_COLOR.GetValue( iIndex%NUM_SONG_GROUP_COLORS ); iIndex++; } ASSERT_M( 0, ssprintf("requested color for course group '%s' that doesn't exist",sCourseGroup.c_str()) ); return RageColor(1,1,1,1); } RageColor SongManager::GetCourseColor( const Course* pCourse ) const { // Use unlock color if applicable const UnlockEntry *pUE = UNLOCKMAN->FindCourse( pCourse ); if( pUE && USE_UNLOCK_COLOR.GetValue() ) return UNLOCK_COLOR.GetValue(); if( USE_PREFERRED_SORT_COLOR ) { int courseIndex = 0; for (CoursePointerVector const &v : m_vPreferredCourseSort) { if (std::any_of(v.begin(), v.end(), [&](Course const *s) { return s == pCourse; })) { CHECKPOINT_M( ssprintf( "%i, NUM_COURSE_GROUP_COLORS = %i", courseIndex, NUM_COURSE_GROUP_COLORS.GetValue()) ); return COURSE_GROUP_COLOR.GetValue( courseIndex % NUM_COURSE_GROUP_COLORS ); } courseIndex += 1; } int i = m_vPreferredCourseSort.size(); CHECKPOINT_M( ssprintf( "%i, NUM_COURSE_GROUP_COLORS = %i", i, NUM_COURSE_GROUP_COLORS.GetValue()) ); return COURSE_GROUP_COLOR.GetValue( i % NUM_COURSE_GROUP_COLORS ); } else { return GetCourseGroupColor( pCourse->m_sGroupName ); } } void SongManager::ResetGroupColors() { // Reload song/course group colors to prevent a crash when switching // themes in-game. (apparently not, though.) -aj SONG_GROUP_COLOR.Clear(); COURSE_GROUP_COLOR.Clear(); NUM_SONG_GROUP_COLORS .Load( "SongManager", "NumSongGroupColors" ); SONG_GROUP_COLOR .Load( "SongManager", SONG_GROUP_COLOR_NAME, NUM_SONG_GROUP_COLORS ); NUM_COURSE_GROUP_COLORS .Load( "SongManager", "NumCourseGroupColors" ); COURSE_GROUP_COLOR .Load( "SongManager", COURSE_GROUP_COLOR_NAME, NUM_COURSE_GROUP_COLORS ); } const std::vector &SongManager::GetSongs( const RString &sGroupName ) const { static const std::vector vEmpty; if( sGroupName == GROUP_ALL ) return m_pSongs; std::map::const_iterator iter = m_mapSongGroupIndex.find( sGroupName ); if ( iter != m_mapSongGroupIndex.end() ) return iter->second; FOREACH_EnabledPlayer(pn) { Profile* prof= PROFILEMAN->GetProfile(pn); if(prof != nullptr) { if(prof->GetDisplayNameOrHighScoreName() == sGroupName) { return prof->m_songs; } } } return vEmpty; } void SongManager::GetPreferredSortSongs( std::vector &AddTo ) const { if( m_vPreferredSongSort.empty() ) { AddTo.insert( AddTo.end(), m_pSongs.begin(), m_pSongs.end() ); return; } for (PreferredSortSection const &v : m_vPreferredSongSort) AddTo.insert( AddTo.end(), v.vpSongs.begin(), v.vpSongs.end() ); } RString SongManager::SongToPreferredSortSectionName( const Song *pSong ) const { for (PreferredSortSection const &v : m_vPreferredSongSort) { if (std::any_of(v.vpSongs.begin(), v.vpSongs.end(), [&](Song const *s) { return s == pSong; })) { return v.sName; } } return RString(); } void SongManager::GetPreferredSortSongsBySectionName( const RString &sSectionName, std::vector &AddTo ) const { // Use m_mapPreferredSectionToSongs std::map::const_iterator iter = m_mapPreferredSectionToSongs.find( sSectionName ); if( iter != m_mapPreferredSectionToSongs.end() ) AddTo.insert( AddTo.end(), iter->second.begin(), iter->second.end() ); } std::vector SongManager::GetPreferredSortSongsBySectionName( const RString &sSectionName ) const { std::vector AddTo; GetPreferredSortSongsBySectionName(sSectionName, AddTo); return AddTo; } std::vector SongManager::GetPreferredSortSectionNames() const { std::vector sectionNames; // Use m_mapPreferredSectionToSongs for (std::pair const &iter : m_mapPreferredSectionToSongs) sectionNames.push_back(iter.first); return sectionNames; } void SongManager::GetPreferredSortCourses( CourseType ct, std::vector &AddTo, bool bIncludeAutogen ) const { if( m_vPreferredCourseSort.empty() ) { GetCourses( ct, AddTo, bIncludeAutogen ); return; } for (CoursePointerVector const &v : m_vPreferredCourseSort) { for (Course *pCourse : v) { if( pCourse->GetCourseType() == ct ) AddTo.push_back( pCourse ); } } } std::vector SongManager::GetSongsByMeter(const int iMeter) const { std::vector AddTo; auto iter = m_mapSongsByDifficulty.find(iMeter); if (iter != m_mapSongsByDifficulty.end()) { // Found the level, add the songs to AddTo AddTo.insert(AddTo.end(), iter->second.begin(), iter->second.end()); } return AddTo; } int SongManager::GetNumSongs() const { return m_pSongs.size(); } int SongManager::GetNumLockedSongs() const { return std::count_if(m_pSongs.begin(), m_pSongs.end(), [](Song const *s) { return UNLOCKMAN->SongIsLocked(s); }); } int SongManager::GetNumUnlockedSongs() const { return std::count_if(m_pSongs.begin(), m_pSongs.end(), [](Song const *s) { return UNLOCKMAN->SongIsLocked(s) & ~LOCKED_LOCK; }); } int SongManager::GetNumSelectableAndUnlockedSongs() const { return std::count_if(m_pSongs.begin(), m_pSongs.end(), [](Song const *s) { return UNLOCKMAN->SongIsLocked(s) & ~(LOCKED_LOCK | LOCKED_SELECTABLE); }); } int SongManager::GetNumSongGroups() const { return m_sSongGroupNames.size(); } int SongManager::GetNumCourses() const { return m_pCourses.size(); } int SongManager::GetNumCourseGroups() const { return m_mapCourseGroupToInfo.size(); } RString SongManager::ShortenGroupName( RString sLongGroupName ) { static TitleSubst tsub("Groups"); TitleFields title; title.Title = sLongGroupName; tsub.Subst( title ); return title.Title; } static LocalizedString LOADING_COURSES ( "SongManager", "Loading courses..." ); void SongManager::InitCoursesFromDisk( LoadingWindow *ld, bool onlyAdditions ) { LOG->Trace( "Loading courses." ); if( ld ) ld->SetText( LOADING_COURSES ); RageTimer loading_window_last_update_time; loading_window_last_update_time.Touch(); std::vector vsCourseGroupNames; // Find all group directories in Courses dir GetDirListing( SpecialFiles::COURSES_DIR + "*", vsCourseGroupNames, true, true ); StripCvsAndSvn( vsCourseGroupNames ); StripMacResourceForks( vsCourseGroupNames ); // Search for courses both in COURSES_DIR and in subdirectories vsCourseGroupNames.push_back( SpecialFiles::COURSES_DIR ); SortRStringArray( vsCourseGroupNames ); int courseIndex = 0; for (RString const &sCourseGroup : vsCourseGroupNames) // for each dir in /Courses/ { // Find all CRS files in this group directory std::vector vsCoursePaths; GetDirListing( sCourseGroup + "/*.crs", vsCoursePaths, false, true ); SortRStringArray( vsCoursePaths ); if( ld ) { ld->SetIndeterminate( false ); ld->SetTotalWork( vsCoursePaths.size() ); } RString base_course_group= Basename(sCourseGroup); for (RString const &sCoursePath : vsCoursePaths) { // Skip already loaded courses if onlyAdditions is set. if (onlyAdditions) { CourseID courseID; courseID.FromPath(sCoursePath); if (courseID.ToCourse() != nullptr) continue; } if(ld && loading_window_last_update_time.Ago() > next_loading_window_update) { loading_window_last_update_time.Touch(); ld->SetProgress(courseIndex); ld->SetText( LOADING_COURSES.GetValue()+ssprintf("\n%s\n%s", base_course_group.c_str(), Basename(sCoursePath).c_str())); } Course* pCourse = new Course; CourseLoaderCRS::LoadFromCRSFile( sCoursePath, *pCourse ); if( g_bHideIncompleteCourses.Get() && pCourse->m_bIncomplete ) { delete pCourse; continue; } m_pCourses.push_back( pCourse ); courseIndex++; } } if( ld ) { ld->SetIndeterminate( true ); } RefreshCourseGroupInfo(); } void SongManager::InitAutogenCourses() { // Create group courses for Endless and Nonstop std::vector saGroupNames; this->GetSongGroupNames( saGroupNames ); Course* pCourse; for( unsigned g=0; gm_sScripter = "Autogen"; m_pCourses.push_back( pCourse ); pCourse = new Course; CourseUtil::AutogenNonstopFromGroup( sGroupName, Difficulty_Medium, *pCourse ); pCourse->m_sScripter = "Autogen"; m_pCourses.push_back( pCourse ); } std::vector apCourseSongs = GetAllSongs(); // Generate "All Songs" endless course. pCourse = new Course; CourseUtil::AutogenEndlessFromGroup( "", Difficulty_Medium, *pCourse ); pCourse->m_sScripter = "Autogen"; m_pCourses.push_back( pCourse ); /* Generate Oni courses from artists. Only create courses if we have at least * four songs from an artist; create 3- and 4-song courses. */ { /* We normally sort by translit artist. However, display artist is more * consistent. For example, transliterated Japanese names are alternately * spelled given- and family-name first, but display titles are more consistent. */ std::vector apSongs = this->GetAllSongs(); SongUtil::SortSongPointerArrayByDisplayArtist( apSongs ); RString sCurArtist = ""; RString sCurArtistTranslit = ""; int iCurArtistCount = 0; std::vector aSongs; unsigned i = 0; do { RString sArtist = i >= apSongs.size()? RString(""): apSongs[i]->GetDisplayArtist(); RString sTranslitArtist = i >= apSongs.size()? RString(""): apSongs[i]->GetTranslitArtist(); if( i < apSongs.size() && !sCurArtist.CompareNoCase(sArtist) ) { aSongs.push_back( apSongs[i] ); ++iCurArtistCount; continue; } /* Different artist, or we're at the end. If we have enough entries for * the last artist, add it. Skip blanks and "Unknown artist". */ if( iCurArtistCount >= 3 && sCurArtistTranslit != "" && sCurArtistTranslit.CompareNoCase("Unknown artist") && sCurArtist.CompareNoCase("Unknown artist") ) { pCourse = new Course; CourseUtil::AutogenOniFromArtist( sCurArtist, sCurArtistTranslit, aSongs, Difficulty_Hard, *pCourse ); pCourse->m_sScripter = "Autogen"; m_pCourses.push_back( pCourse ); } aSongs.clear(); if( i < apSongs.size() ) { sCurArtist = sArtist; sCurArtistTranslit = sTranslitArtist; iCurArtistCount = 1; aSongs.push_back( apSongs[i] ); } } while( i++ < apSongs.size() ); } } void SongManager::InitRandomAttacks() { GAMESTATE->m_RandomAttacks.clear(); if( !IsAFile(ATTACK_FILE) ) LOG->Trace( "File Data/RandomAttacks.txt was not found" ); else { MsdFile msd; if( !msd.ReadFile( ATTACK_FILE, true ) ) LuaHelpers::ReportScriptErrorFmt( "Error opening file '%s' for reading: %s.", ATTACK_FILE.c_str(), msd.GetError().c_str() ); else { for( unsigned i=0; i 2 ) { LuaHelpers::ReportScriptErrorFmt( "Got \"%s:%s\" tag with too many parameters", sType.c_str(), sAttack.c_str() ); continue; } if( !sType.EqualsNoCase("ATTACK") ) { LuaHelpers::ReportScriptErrorFmt( "Got \"%s:%s\" tag with wrong declaration", sType.c_str(), sAttack.c_str() ); continue; } GAMESTATE->m_RandomAttacks.push_back( sAttack ); } } } } void SongManager::FreeCourses() { for( unsigned i=0; i vNewCourses; for( std::vector::iterator it = m_pCourses.begin(); it != m_pCourses.end(); ++it ) { if( (*it)->m_bIsAutogen ) { delete *it; } else { vNewCourses.push_back( *it ); } } m_pCourses.swap( vNewCourses ); UpdatePopular(); UpdateShuffled(); RefreshCourseGroupInfo(); } void SongManager::AddCourse( Course *pCourse ) { m_pCourses.push_back( pCourse ); UpdatePopular(); UpdateShuffled(); m_mapCourseGroupToInfo[ pCourse->m_sGroupName ]; // insert } void SongManager::DeleteCourse( Course *pCourse ) { std::vector::iterator iter = find( m_pCourses.begin(), m_pCourses.end(), pCourse ); ASSERT( iter != m_pCourses.end() ); m_pCourses.erase( iter ); UpdatePopular(); UpdateShuffled(); RefreshCourseGroupInfo(); } void SongManager::InvalidateCachedTrails() { for (Course *pCourse : m_pCourses) { if( pCourse->IsAnEdit() ) pCourse->m_TrailCache.clear(); } } /* Called periodically to wipe out cached NoteData. This is called when we * change screens. */ void SongManager::Cleanup() { for (Song *pSong : m_pShuffledSongs) { if (pSong) { const std::vector& vpSteps = pSong->GetAllSteps(); for (Steps *pSteps : vpSteps) { pSteps->Compress(); } } } } /* Flush all Song*, Steps* and Course* caches. This is when a Song or its Steps * are removed or changed. This doesn't touch GAMESTATE and StageStats * pointers. Currently, the only time Steps are altered independently of the * Courses and Songs is in Edit Mode, which updates the other pointers it needs. */ void SongManager::Invalidate( const Song *pStaleSong ) { // TODO: This is unnecessarily expensive. // Can we regenerate only the autogen courses that are affected? DeleteAutogenCourses(); for (Course *c : this->m_pCourses) { c->Invalidate( pStaleSong ); } InitAutogenCourses(); UpdatePopular(); UpdateShuffled(); RefreshCourseGroupInfo(); } void SongManager::RegenerateNonFixedCourses() { for( unsigned i=0; i < m_pCourses.size(); i++ ) m_pCourses[i]->RegenerateNonFixedTrails(); } void SongManager::SetPreferences() { for( unsigned int i=0; im_bAutogenSteps may have changed. m_pSongs[i]->RemoveAutoGenNotes(); m_pSongs[i]->AddAutoGenNotes(); } } void SongManager::SaveEnabledSongsToPref() { std::vector vsDisabledSongs; // Intentionally drop disabled song entries for songs that aren't currently loaded. const std::vector &apSongs = SONGMAN->GetAllSongs(); for (Song *pSong : apSongs) { SongID sid; sid.FromSong( pSong ); if( !pSong->GetEnabled() ) vsDisabledSongs.push_back( sid.ToString() ); } g_sDisabledSongs.Set( join(";", vsDisabledSongs) ); } void SongManager::LoadEnabledSongsFromPref() { std::vector asDisabledSongs; split( g_sDisabledSongs, ";", asDisabledSongs, true ); for (RString const &s : asDisabledSongs) { SongID sid; sid.FromString( s ); Song *pSong = sid.ToSong(); if( pSong ) pSong->SetEnabled( false ); } } void SongManager::GetStepsLoadedFromProfile( std::vector &AddTo, ProfileSlot slot ) const { const std::vector &vSongs = GetAllSongs(); for (Song *song : vSongs) { song->GetStepsLoadedFromProfile( slot, AddTo ); } } void SongManager::DeleteSteps( Steps *pSteps ) { pSteps->m_pSong->DeleteSteps( pSteps ); } void SongManager::GetAllCourses( std::vector &AddTo, bool bIncludeAutogen ) const { for( unsigned i=0; im_bIsAutogen ) AddTo.push_back( m_pCourses[i] ); } void SongManager::GetCourses( CourseType ct, std::vector &AddTo, bool bIncludeAutogen ) const { for( unsigned i=0; iGetCourseType() == ct ) if( bIncludeAutogen || !m_pCourses[i]->m_bIsAutogen ) AddTo.push_back( m_pCourses[i] ); } void SongManager::GetCoursesInGroup( std::vector &AddTo, const RString &sCourseGroup, bool bIncludeAutogen ) const { for( unsigned i=0; im_sGroupName == sCourseGroup ) if( bIncludeAutogen || !m_pCourses[i]->m_bIsAutogen ) AddTo.push_back( m_pCourses[i] ); } bool SongManager::GetExtraStageInfoFromCourse( bool bExtra2, RString sPreferredGroup, Song*& pSongOut, Steps*& pStepsOut, StepsType stype ) { const RString sCourseSuffix = sPreferredGroup + (bExtra2 ? "/extra2.crs" : "/extra1.crs"); RString sCoursePath = SpecialFiles::SONGS_DIR + sCourseSuffix; Course course; CourseLoaderCRS::LoadFromCRSFile( sCoursePath, course ); if( course.GetEstimatedNumStages() <= 0 ) return false; Trail *pTrail = course.GetTrail(stype); if( pTrail->m_vEntries.empty() ) return false; pSongOut = pTrail->m_vEntries[0].pSong; pStepsOut = pTrail->m_vEntries[0].pSteps; return true; } // Return true if n1 < n2. bool CompareNotesPointersForExtra(const Steps *n1, const Steps *n2) { // Equate CHALLENGE to HARD. Difficulty d1 = std::min(n1->GetDifficulty(), Difficulty_Hard); Difficulty d2 = std::min(n2->GetDifficulty(), Difficulty_Hard); if(d1 < d2) return true; if(d1 > d2) return false; // n1 difficulty == n2 difficulty if(StepsUtil::CompareNotesPointersByMeter(n1,n2)) return true; if(StepsUtil::CompareNotesPointersByMeter(n2,n1)) return false; // n1 meter == n2 meter return StepsUtil::CompareNotesPointersByRadarValues(n1,n2); } void SongManager::GetExtraStageInfo( bool bExtra2, const Style *sd, Song*& pSongOut, Steps*& pStepsOut ) { RString sGroup = GAMESTATE->m_sPreferredSongGroup; if( sGroup == GROUP_ALL ) { if( GAMESTATE->m_pCurSong == nullptr ) { // This normally shouldn't happen, but it's helpful to permit it for testing. LuaHelpers::ReportScriptErrorFmt( "GetExtraStageInfo() called in GROUP_ALL, but GAMESTATE->m_pCurSong == nullptr" ); GAMESTATE->m_pCurSong.Set( GetRandomSong() ); } sGroup = GAMESTATE->m_pCurSong->m_sGroupName; } ASSERT_M( sGroup != "", ssprintf("%p '%s' '%s'", static_cast(GAMESTATE->m_pCurSong.Get()), GAMESTATE->m_pCurSong? GAMESTATE->m_pCurSong->GetSongDir().c_str():"", GAMESTATE->m_pCurSong? GAMESTATE->m_pCurSong->m_sGroupName.c_str():"") ); // Check preferred group if( GetExtraStageInfoFromCourse(bExtra2, sGroup, pSongOut, pStepsOut, sd->m_StepsType) ) return; // Optionally, check the Songs folder for extra1/2.crs files. if( GetExtraStageInfoFromCourse(bExtra2, "", pSongOut, pStepsOut, sd->m_StepsType) ) return; // Choose a hard song for the extra stage Song* pExtra1Song = nullptr; // the absolute hardest Song and Steps. Use this for extra stage 1. Steps* pExtra1Notes = nullptr; Song* pExtra2Song = nullptr; // a medium-hard Song and Steps. Use this for extra stage 2. Steps* pExtra2Notes = nullptr; const std::vector &apSongs = GetSongs( sGroup ); for( unsigned s=0; s apSteps; SongUtil::GetSteps( pSong, apSteps, sd->m_StepsType ); for( unsigned n=0; n 8 (assuming dance) if( bExtra2 && pSteps->GetMeter() > EXTRA_STAGE2_DIFFICULTY_MAX ) continue; // skip if( pExtra2Notes == nullptr || CompareNotesPointersForExtra(pExtra2Notes,pSteps) ) // pSteps is harder than pHardestNotes { pExtra2Song = pSong; pExtra2Notes = pSteps; } } } if( pExtra2Song == nullptr && pExtra1Song != nullptr ) { pExtra2Song = pExtra1Song; pExtra2Notes = pExtra1Notes; } // If there are any notes at all that match this StepsType, everything should be filled out. // Also, it's guaranteed that there is at least one Steps that matches the StepsType because the player // had to play something before reaching the extra stage! ASSERT( pExtra2Song && pExtra1Song && pExtra2Notes && pExtra1Notes ); pSongOut = (bExtra2 ? pExtra2Song : pExtra1Song); pStepsOut = (bExtra2 ? pExtra2Notes : pExtra1Notes); } Song* SongManager::GetRandomSong() { if( m_pShuffledSongs.empty() ) return nullptr; static int i = 0; for( int iThrowAway=0; iThrowAway<100; iThrowAway++ ) { i++; wrap( i, m_pShuffledSongs.size() ); Song *pSong = m_pShuffledSongs[ i ]; if( pSong->IsTutorial() ) continue; if( !pSong->NormallyDisplayed() ) continue; return pSong; } return nullptr; } Course* SongManager::GetRandomCourse() { if( m_pShuffledCourses.empty() ) return nullptr; static int i = 0; for( int iThrowAway=0; iThrowAway<100; iThrowAway++ ) { i++; wrap( i, m_pShuffledCourses.size() ); Course *pCourse = m_pShuffledCourses[ i ]; if( pCourse->m_bIsAutogen && !PREFSMAN->m_bAutogenGroupCourses ) continue; if( pCourse->GetCourseType() == COURSE_TYPE_ENDLESS ) continue; if( UNLOCKMAN->CourseIsLocked(pCourse) ) continue; return pCourse; } return nullptr; } Song* SongManager::GetSongFromDir(RString dir) const { if(dir.Right(1) != "/") { dir += "/"; } dir.Replace('\\', '/'); dir.MakeLower(); std::map::const_iterator entry= m_SongsByDir.find(dir); if(entry != m_SongsByDir.end()) { return entry->second; } return nullptr; } Course* SongManager::GetCourseFromPath( RString sPath ) const { if( sPath == "" ) return nullptr; for (Course *c : m_pCourses) { if( sPath.CompareNoCase(c->m_sPath) == 0 ) return c; } return nullptr; } Course* SongManager::GetCourseFromName( RString sName ) const { if( sName == "" ) return nullptr; for (Course *c : m_pCourses) if( sName.CompareNoCase(c->GetDisplayFullTitle()) == 0 ) return c; return nullptr; } /* GetSongDir() contains a path to the song, possibly a full path, eg: * Songs\Group\SongName or * My Other Song Folder\Group\SongName or * c:\Corny J-pop\Group\SongName * * Most course group names are "Group\SongName", so we want to match against the * last two elements. Let's also support "SongName" alone, since the group is * only important when it's potentially ambiguous. * * Let's *not* support "Songs\Group\SongName" in course files. That's probably * a common error, but that would result in course files floating around that * only work for people who put songs in "Songs"; we don't want that. */ Song *SongManager::FindSong( RString sPath ) const { sPath.Replace( '\\', '/' ); std::vector bits; split( sPath, "/", bits ); if( bits.size() == 1 ) return FindSong( "", bits[0] ); else if( bits.size() == 2 ) return FindSong( bits[0], bits[1] ); return nullptr; } Song *SongManager::FindSong( RString sGroup, RString sSong ) const { // foreach song const std::vector &vSongs = GetSongs( sGroup.empty()? GROUP_ALL:sGroup ); for (Song *s : vSongs) { if( s->Matches(sGroup, sSong) ) return s; } return nullptr; } Course *SongManager::FindCourse( RString sPath ) const { sPath.Replace( '\\', '/' ); std::vector bits; split( sPath, "/", bits ); if( bits.size() == 1 ) return FindCourse( "", bits[0] ); else if( bits.size() == 2 ) return FindCourse( bits[0], bits[1] ); return nullptr; } Course *SongManager::FindCourse( RString sGroup, RString sName ) const { for (Course *c : m_pCourses) { if( c->Matches(sGroup, sName) ) return c; } return nullptr; } void SongManager::UpdatePopular() { // update players best std::vector apBestSongs = m_pSongs; for ( unsigned j=0; j < apBestSongs.size() ; ++j ) { bool bFiltered = false; // Filter out locked songs. if( !apBestSongs[j]->NormallyDisplayed() ) bFiltered = true; if( !bFiltered ) continue; // Remove it. std::swap( apBestSongs[j], apBestSongs.back() ); apBestSongs.erase( apBestSongs.end()-1 ); --j; } SongUtil::SortSongPointerArrayByTitle( apBestSongs ); std::vector apBestCourses[NUM_CourseType]; FOREACH_ENUM( CourseType, ct ) { GetCourses( ct, apBestCourses[ct], PREFSMAN->m_bAutogenGroupCourses ); CourseUtil::SortCoursePointerArrayByTitle( apBestCourses[ct] ); } m_pPopularSongs = apBestSongs; SongUtil::SortSongPointerArrayByNumPlays( m_pPopularSongs, ProfileSlot_Machine, true ); FOREACH_CourseType( ct ) { std::vector &vpCourses = m_pPopularCourses[ct]; vpCourses = apBestCourses[ct]; CourseUtil::SortCoursePointerArrayByNumPlays( vpCourses, ProfileSlot_Machine, true ); } } std::map> SongManager::UpdateMeterSort( std::vector songs) { // Empty the map m_mapSongsByDifficulty.clear(); std::vector apDifficultSongs = songs; // For each song, for each step for( unsigned i = 0; i < apDifficultSongs.size(); ++i ) { std::vector vpSteps; SongUtil::GetPlayableSteps( apDifficultSongs[i], vpSteps ); for( unsigned j = 0; j < vpSteps.size(); ++j ) { Steps *pSteps = vpSteps[j]; // Check if the meter is already in m_mapSongsByDifficulty if (std::find(m_mapSongsByDifficulty[pSteps->GetMeter()].begin(), m_mapSongsByDifficulty[pSteps->GetMeter()].end(), apDifficultSongs[i]) != m_mapSongsByDifficulty[pSteps->GetMeter()].end()) continue; else { m_mapSongsByDifficulty[pSteps->GetMeter()].push_back(apDifficultSongs[i]); } } } return m_mapSongsByDifficulty; } void SongManager::UpdateShuffled() { // update shuffled m_pShuffledSongs = m_pSongs; std::shuffle( m_pShuffledSongs.begin(), m_pShuffledSongs.end(), g_RandomNumberGenerator ); m_pShuffledCourses = m_pCourses; std::shuffle( m_pShuffledCourses.begin(), m_pShuffledCourses.end(), g_RandomNumberGenerator ); } void SongManager::SetPreferredSongs(RString sPreferredSongs, bool bIsAbsolute) { ASSERT( UNLOCKMAN != nullptr ); m_vPreferredSongSort.clear(); m_mapPreferredSectionToSongs.clear(); std::vector asLines; RString sFile = sPreferredSongs; if (!bIsAbsolute) sFile = THEME->GetPathO( "SongManager", sPreferredSongs ); GetFileContents( sFile, asLines ); if( asLines.empty() ) return; PreferredSortSection section; std::map mapSongToPri; for (RString sLine : asLines) { bool bSectionDivider = BeginsWith(sLine, "---"); if( bSectionDivider ) { if( !section.vpSongs.empty() ) { m_vPreferredSongSort.push_back( section ); m_mapPreferredSectionToSongs[section.sName] = section.vpSongs; section = PreferredSortSection(); } section.sName = sLine.Right( sLine.length() - RString("---").length() ); TrimLeft( section.sName ); TrimRight( section.sName ); } else { /* if the line ends in slash-star, check if the section exists, * and if it does, add all the songs in that group to the list. */ if( EndsWith(sLine,"/*") ) { RString group = sLine.Left( sLine.length() - RString("/*").length() ); if( DoesSongGroupExist(group) ) { // add all songs in group const std::vector &vSongs = GetSongs( group ); for (Song *song : vSongs) { if( UNLOCKMAN->SongIsLocked(song) & LOCKED_SELECTABLE ) continue; section.vpSongs.push_back( song ); } } } Song *pSong = FindSong( sLine ); if( pSong == nullptr ) continue; if( UNLOCKMAN->SongIsLocked(pSong) & LOCKED_SELECTABLE ) continue; section.vpSongs.push_back( pSong ); } } if( !section.vpSongs.empty() ) { m_vPreferredSongSort.push_back( section ); m_mapPreferredSectionToSongs[section.sName] = section.vpSongs; section = PreferredSortSection(); } if( MOVE_UNLOCKS_TO_BOTTOM_OF_PREFERRED_SORT.GetValue() ) { // move all unlock songs to a group at the bottom PreferredSortSection PFSection; PFSection.sName = "Unlocks"; for (UnlockEntry const &ue : UNLOCKMAN->m_UnlockEntries) { if( ue.m_Type == UnlockRewardType_Song ) { Song *pSong = ue.m_Song.ToSong(); if( pSong ) PFSection.vpSongs.push_back( pSong ); } } // NOTE(crashcringle): This code removed the unlocks from other sections they might have been in. // This was needed due to the previous 1:1 relationship between songs and section in order for the song to appear in the Unlocks section correctly. // Commented out for now. // for (std::vector::iterator v = m_vPreferredSongSort.begin(); v != m_vPreferredSongSort.end(); ++v) // { // for( int i=v->vpSongs.size()-1; i>=0; i-- ) // { // Song *pSong = v->vpSongs[i]; // if( find(PFSection.vpSongs.begin(),PFSection.vpSongs.end(),pSong) != PFSection.vpSongs.end() ) // { // v->vpSongs.erase( v->vpSongs.begin()+i ); // } // } // } m_vPreferredSongSort.push_back( PFSection ); m_mapPreferredSectionToSongs[PFSection.sName] = PFSection.vpSongs; } // prune empty groups for( int i=m_vPreferredSongSort.size()-1; i>=0; i-- ) if( m_vPreferredSongSort[i].vpSongs.empty() ) { m_vPreferredSongSort.erase( m_vPreferredSongSort.begin()+i ); m_mapPreferredSectionToSongs.erase( m_vPreferredSongSort[i].sName ); } for (PreferredSortSection const &i : m_vPreferredSongSort) { for (Song const *j : i.vpSongs) { ASSERT( j != nullptr ); } } } void SongManager::SetPreferredCourses(RString sPreferredCourses, bool bIsAbsolute) { ASSERT( UNLOCKMAN != nullptr ); m_vPreferredCourseSort.clear(); std::vector asLines; RString sFile = sPreferredCourses; if (!bIsAbsolute) sFile = THEME->GetPathO( "SongManager", sPreferredCourses ); if( !GetFileContents(sFile, asLines) ) return; std::vector vpCourses; for (RString sLine : asLines) { bool bSectionDivider = BeginsWith( sLine, "---" ); if( bSectionDivider ) { if( !vpCourses.empty() ) { m_vPreferredCourseSort.push_back( vpCourses ); vpCourses.clear(); } continue; } Course *pCourse = FindCourse( sLine ); if( pCourse == nullptr ) continue; if( UNLOCKMAN->CourseIsLocked(pCourse) & LOCKED_SELECTABLE ) continue; vpCourses.push_back( pCourse ); } if( !vpCourses.empty() ) { m_vPreferredCourseSort.push_back( vpCourses ); vpCourses.clear(); } if( MOVE_UNLOCKS_TO_BOTTOM_OF_PREFERRED_SORT.GetValue() ) { // move all unlock Courses to a group at the bottom std::vector vpUnlockCourses; for (UnlockEntry const &ue : UNLOCKMAN->m_UnlockEntries) { if( ue.m_Type == UnlockRewardType_Course ) if( ue.m_Course.IsValid() ) vpUnlockCourses.push_back( ue.m_Course.ToCourse() ); } for (auto v = m_vPreferredCourseSort.begin(); v != m_vPreferredCourseSort.end(); ++v) { for( int i=v->size()-1; i>=0; i-- ) { Course *pCourse = (*v)[i]; if( find(vpUnlockCourses.begin(),vpUnlockCourses.end(),pCourse) != vpUnlockCourses.end() ) { v->erase( v->begin()+i ); } } } m_vPreferredCourseSort.push_back( vpUnlockCourses ); } // prune empty groups for( int i=m_vPreferredCourseSort.size()-1; i>=0; i-- ) if( m_vPreferredCourseSort[i].empty() ) m_vPreferredCourseSort.erase( m_vPreferredCourseSort.begin()+i ); for (CoursePointerVector const &i : m_vPreferredCourseSort) { for (Course *j : i) { ASSERT( j != nullptr ); } } } void SongManager::UpdatePreferredSort(RString sPreferredSongs, RString sPreferredCourses) { SetPreferredSongs(sPreferredSongs); SetPreferredCourses(sPreferredCourses); } void SongManager::SortSongs() { SongUtil::SortSongPointerArrayByTitle( m_pSongs ); } void SongManager::UpdateRankingCourses() { /* Updating the ranking courses data is fairly expensive since it involves * comparing strings. Do so sparingly. */ std::vector RankingCourses; split( THEME->GetMetric("ScreenRanking","CoursesToShow"),",", RankingCourses); for (Course *c : m_pCourses) { bool bLotsOfStages = c->GetEstimatedNumStages() > 7; c->m_SortOrder_Ranking = bLotsOfStages? 3 : 2; for( unsigned j = 0; j < RankingCourses.size(); j++ ) if( !RankingCourses[j].CompareNoCase(c->m_sPath) ) c->m_SortOrder_Ranking = 1; } } void SongManager::RefreshCourseGroupInfo() { m_mapCourseGroupToInfo.clear(); for (Course const * c : m_pCourses) { m_mapCourseGroupToInfo[c->m_sGroupName]; // insert } } void SongManager::LoadStepEditsFromProfileDir( const RString &sProfileDir, ProfileSlot slot ) { // Load all edit steps RString sDir = sProfileDir + EDIT_STEPS_SUBDIR; SSCLoader loaderSSC; SMLoader loaderSM; int iNumEditsLoaded = GetNumEditsLoadedFromProfile( slot ); // Pass 1: Flat folder (old style) std::vector vsFiles; int size = std::min( (int) vsFiles.size(), MAX_EDIT_STEPS_PER_PROFILE - iNumEditsLoaded ); GetDirListing( sDir+"*.edit", vsFiles, false, true ); // XXX: If some edits are invalid and they're close to the edit limit, this may erroneously skip some edits, and won't warn. for( int i=0; i MAX_EDIT_STEPS_PER_PROFILE - iNumEditsLoaded ) { LuaHelpers::ReportScriptErrorFmt("Profile %s has too many edits; some have been skipped.", ProfileSlotToString( slot ).c_str() ); return; } // Some .edit files may have been invalid, so re-query instead of just += size. iNumEditsLoaded = GetNumEditsLoadedFromProfile( slot ); // Pass 2: Group and song folders with #SONG inferred from folder (optional new style) std::vector vsGroups; GetDirListing( sDir+"*", vsGroups, true, false ); // XXX: Same as above, edits may be skipped in error in some cases for( unsigned i=0; i vsSongs; GetDirListing(sDir+sGroupDir+"*", vsSongs, true, false ); for( unsigned j=0; j vsEdits; RString sSongDir = sGroupDir+vsSongs[j]+"/"; // XXX There doesn't appear to be a songdir const? Song *given = GetSongFromDir( "/Songs/"+sSongDir ); // NOTE: We don't have to check the return value of GetSongFromDir here, // because if it fails, it returns nullptr, which is then passed to NotesLoader*.LoadEditFromFile(), // which will interpret that as "we couldn't infer the song from the path", // which is what we want in that case anyway. GetDirListing(sDir+sSongDir+"/*.edit", vsEdits, false, true ); size = std::min( (int) vsEdits.size(), MAX_EDIT_STEPS_PER_PROFILE - iNumEditsLoaded ); for( int k=0; k MAX_EDIT_STEPS_PER_PROFILE - iNumEditsLoaded ) { LuaHelpers::ReportScriptErrorFmt("Profile %s has too many edits; some have been skipped.", ProfileSlotToString( slot ).c_str() ); return; } // Some .edit files may have been invalid, so re-query instead of just += size. iNumEditsLoaded = GetNumEditsLoadedFromProfile( slot ); } } } void SongManager::LoadCourseEditsFromProfileDir( const RString &sProfileDir, ProfileSlot slot ) { // Load all edit courses RString sDir = sProfileDir + EDIT_COURSES_SUBDIR; std::vector vsFiles; GetDirListing( sDir+"*.crs", vsFiles, false, true ); int iNumEditsLoaded = GetNumEditsLoadedFromProfile( slot ); int size = std::min( (int) vsFiles.size(), MAX_EDIT_COURSES_PER_PROFILE - iNumEditsLoaded ); for( int i=0; i apSteps; SongUtil::GetSteps( pSong, apSteps ); for( unsigned i = 0; i < apSteps.size(); ++i ) { const Steps *pSteps = apSteps[i]; if( pSteps->WasLoadedFromProfile() && pSteps->GetLoadedFromProfileSlot() == slot ) ++iCount; } } return iCount; } void SongManager::AddSongToList(Song* new_song) { new_song->SetEnabled(true); m_pSongs.push_back(new_song); RString dir= new_song->GetSongDir(); dir.MakeLower(); m_SongsByDir.insert(make_pair(dir, new_song)); } void SongManager::FreeAllLoadedFromProfile( ProfileSlot slot ) { // Profile courses may refer to profile steps, so free profile courses first. std::vector apToDelete; for (Course *pCourse : m_pCourses) { if( pCourse->GetLoadedFromProfileSlot() == ProfileSlot_Invalid ) continue; if( slot == ProfileSlot_Invalid || pCourse->GetLoadedFromProfileSlot() == slot ) apToDelete.push_back( pCourse ); } /* We don't use DeleteCourse here, so we don't UpdatePopular and * UpdateShuffled repeatedly. */ for( unsigned i = 0; i < apToDelete.size(); ++i ) { std::vector::iterator iter = find( m_pCourses.begin(), m_pCourses.end(), apToDelete[i] ); ASSERT( iter != m_pCourses.end() ); m_pCourses.erase( iter ); delete apToDelete[i]; } // Popular and Shuffled may refer to courses that we just freed. UpdatePopular(); UpdateShuffled(); RefreshCourseGroupInfo(); // Free profile steps. std::set setInUse; if( STATSMAN ) STATSMAN->GetStepsInUse( setInUse ); for (Song *s : m_pSongs) s->FreeAllLoadedFromProfile( slot, &setInUse ); } int SongManager::GetNumStepsLoadedFromProfile() { int iCount = 0; for (Song const *s : m_pSongs) { std::vector vpAllSteps = s->GetAllSteps(); iCount += std::count_if(vpAllSteps.begin(), vpAllSteps.end(), [](Steps const *step) { return step->GetLoadedFromProfileSlot() != ProfileSlot_Invalid; }); } return iCount; } template int FindCourseIndexOfSameMode( T begin, T end, const Course *p ) { const PlayMode pm = p->GetPlayMode(); int n = 0; for( T it = begin; it != end; ++it ) { if( *it == p ) return n; /* If it's not playable in this mode, don't increment. It might result in * different output in different modes, but that's better than having holes. */ if( !(*it)->IsPlayableIn( GAMESTATE->GetCurrentStyle(GAMESTATE->GetMasterPlayerNumber())->m_StepsType ) ) continue; if( (*it)->GetPlayMode() != pm ) continue; ++n; } return -1; } int SongManager::GetSongRank(Song* pSong) { const int index = FindIndex( m_pPopularSongs.begin(), m_pPopularSongs.end(), pSong ); return index; // -1 means we didn't find it } // lua start #include "LuaBinding.h" /** @brief Allow Lua to have access to the SongManager. */ class LunaSongManager: public Luna { public: static int SetPreferredSongs( T* p, lua_State *L ) { RString sPreferredSongs = SArg(1); if ( lua_gettop(L) >= 2 && !lua_isnil(L, 2) ) { p->SetPreferredSongs( sPreferredSongs, BArg(2) ); } else { p->SetPreferredSongs( sPreferredSongs ); } COMMON_RETURN_SELF; } static int SetPreferredCourses( T* p, lua_State *L ) { RString sPreferredCourses = SArg(1); if ( lua_gettop(L) >= 2 && !lua_isnil(L, 2) ) { p->SetPreferredSongs( sPreferredCourses, BArg(2) ); } else { p->SetPreferredSongs( sPreferredCourses ); } COMMON_RETURN_SELF; } static int GetAllSongs( T* p, lua_State *L ) { const std::vector &v = p->GetAllSongs(); LuaHelpers::CreateTableFromArray( v, L ); return 1; } static int GetAllCourses( T* p, lua_State *L ) { std::vector v; p->GetAllCourses( v, BArg(1) ); LuaHelpers::CreateTableFromArray( v, L ); return 1; } static int GetPreferredSortSongs( T* p, lua_State *L ) { std::vector v; p->GetPreferredSortSongs(v); LuaHelpers::CreateTableFromArray( v, L ); return 1; } static int GetPreferredSortCourses( T* p, lua_State *L ) { std::vector v; CourseType ct = Enum::Check(L,1); p->GetPreferredSortCourses( ct, v, BArg(2) ); LuaHelpers::CreateTableFromArray( v, L ); return 1; } static int FindSong( T* p, lua_State *L ) { Song *pS = p->FindSong(SArg(1)); if(pS) pS->PushSelf(L); else lua_pushnil(L); return 1; } static int FindCourse( T* p, lua_State *L ) { Course *pC = p->FindCourse(SArg(1)); if(pC) pC->PushSelf(L); else lua_pushnil(L); return 1; } static int GetRandomSong( T* p, lua_State *L ) { Song *pS = p->GetRandomSong(); if(pS) pS->PushSelf(L); else lua_pushnil(L); return 1; } static int GetRandomCourse( T* p, lua_State *L ) { Course *pC = p->GetRandomCourse(); if(pC) pC->PushSelf(L); else lua_pushnil(L); return 1; } static int GetNumSongs( T* p, lua_State *L ) { lua_pushnumber( L, p->GetNumSongs() ); return 1; } static int GetNumLockedSongs( T* p, lua_State *L ) { lua_pushnumber( L, p->GetNumLockedSongs() ); return 1; } static int GetNumUnlockedSongs( T* p, lua_State *L ) { lua_pushnumber( L, p->GetNumUnlockedSongs() ); return 1; } static int GetNumSelectableAndUnlockedSongs( T* p, lua_State *L ) { lua_pushnumber( L, p->GetNumSelectableAndUnlockedSongs() ); return 1; } static int GetNumAdditionalSongs( T* p, lua_State *L ) { lua_pushnumber( L, 0 ); return 1; } // deprecated static int GetNumSongGroups( T* p, lua_State *L ) { lua_pushnumber( L, p->GetNumSongGroups() ); return 1; } static int GetNumCourses( T* p, lua_State *L ) { lua_pushnumber( L, p->GetNumCourses() ); return 1; } static int GetNumAdditionalCourses( T* p, lua_State *L ){ lua_pushnumber( L, 0 ); return 1; } // deprecated static int GetNumCourseGroups( T* p, lua_State *L ) { lua_pushnumber( L, p->GetNumCourseGroups() ); return 1; } /* Note: this could now be implemented as Luna::GetSong */ static int GetSongFromSteps( T* p, lua_State *L ) { Song *pSong = nullptr; if( lua_isnil(L,1) ) { pSong = nullptr; } else { Steps *pSteps = Luna::check(L,1); pSong = pSteps->m_pSong; } if(pSong) pSong->PushSelf(L); else lua_pushnil(L); return 1; } static int GetExtraStageInfo( T* p, lua_State *L ) { bool bExtra2 = BArg( 1 ); const Style *pStyle = Luna