#include "stdafx.h" /* ----------------------------------------------------------------------------- Class: Song Desc: Holds metadata for a song and the song's step data. Copyright (c) 2001-2002 by the person(s) listed below. All rights reserved. Chris Danford ----------------------------------------------------------------------------- */ #include "Notes.h" #include "RageUtil.h" #include // for fmod #include "RageLog.h" #include "IniFile.h" #include "Song.h" #include "NoteData.h" #include "MsdFile.h" #include "RageSoundStream.h" #include "RageSound.h" #include "RageException.h" #include "SongCacheIndex.h" #include "GameManager.h" #include "PrefsManager.h" #include "StyleDef.h" #include "Notes.h" #include "GameState.h" #include "NotesLoaderSM.h" #include "NotesLoaderDWI.h" #include "NotesLoaderBMS.h" #include "NotesLoaderKSF.h" #include "NotesWriterDWI.h" #include "NotesWriterSM.h" #include "SDL.h" #include "SDL_image.h" const int FILE_CACHE_VERSION = 102; // increment this when Song or Notes changes to invalidate cache static int CompareBPMSegments(const BPMSegment &seg1, const BPMSegment &seg2) { return seg1.m_fStartBeat < seg2.m_fStartBeat; } void SortBPMSegmentsArray( CArray &arrayBPMSegments ) { sort( arrayBPMSegments.begin(), arrayBPMSegments.end(), CompareBPMSegments ); } static int CompareStopSegments(const StopSegment &seg1, const StopSegment &seg2) { return seg1.m_fStartBeat < seg2.m_fStartBeat; } void SortStopSegmentsArray( CArray &arrayStopSegments ) { sort( arrayStopSegments.begin(), arrayStopSegments.end(), CompareStopSegments ); } int CompareBackgroundChanges(const BackgroundChange &seg1, const BackgroundChange &seg2) { return seg1.m_fStartBeat < seg2.m_fStartBeat; } void SortBackgroundChangesArray( CArray &arrayBackgroundChanges ) { sort( arrayBackgroundChanges.begin(), arrayBackgroundChanges.end(), CompareBackgroundChanges ); } ////////////////////////////// // Song ////////////////////////////// Song::Song() { m_bChangedSinceSave = false; m_fBeat0OffsetInSeconds = 0; m_fMusicSampleStartSeconds = 0; m_fMusicSampleLengthSeconds = 12.0f; // start fading out at m_fMusicSampleLengthSeconds-1 seconds m_iMusicBytes = 0; m_fMusicLengthSeconds = 0; m_fFirstBeat = -1; m_fLastBeat = -1; m_SelectionDisplay = SHOW_ALWAYS; } Song::~Song() { for( unsigned i=0; iTrace( "GetBeatAndBPSFromElapsedTime( fElapsedTime = %f )", fElapsedTime ); // This function is a nightmare. Don't even try to understand it. :-) fElapsedTime += m_fBeat0OffsetInSeconds; for( unsigned i=0; i fSecondsInThisSegment ) { // this BPMSegement is NOT the current segment fElapsedTime -= fSecondsInThisSegment; continue; } // this BPMSegment IS the current segment float fBeatEstimate = fStartBeatThisSegment + fElapsedTime*fBPS; for( j=0; j m_StopSegments[j].m_fStartBeat || m_StopSegments[j].m_fStartBeat > fStartBeatNextSegment ) continue; // this freeze lies within this BPMSegment if( m_StopSegments[j].m_fStartBeat > fBeatEstimate ) break; fElapsedTime -= m_StopSegments[j].m_fStopSeconds; // re-estimate fBeatEstimate = fStartBeatThisSegment + fElapsedTime*fBPS; if( fBeatEstimate < m_StopSegments[j].m_fStartBeat ) { fBeatOut = m_StopSegments[j].m_fStartBeat; fBPSOut = fBPS; bFreezeOut = true; return; } } fBeatOut = fBeatEstimate; fBPSOut = fBPS; bFreezeOut = false; return; } } // This is a super hack, but it's only called from ScreenEdit, so it's OK. // Writing an inverse function of GetBeatAndBPSFromElapsedTime() uber difficult, // so do a binary search to get close to the correct elapsed time. float Song::GetElapsedTimeFromBeat( float fBeat ) const { float fElapsedTimeBestGuess = this->m_fMusicLengthSeconds/2; // seconds float fSecondsToMove = fElapsedTimeBestGuess; // seconds float fBeatOut, fBPSOut; bool bFreezeOut; while( fSecondsToMove > 0.1f ) { GetBeatAndBPSFromElapsedTime( fElapsedTimeBestGuess, fBeatOut, fBPSOut, bFreezeOut ); if( fBeatOut > fBeat ) fElapsedTimeBestGuess -= fSecondsToMove; else fElapsedTimeBestGuess += fSecondsToMove; fSecondsToMove /= 2; } return fElapsedTimeBestGuess; } CString Song::GetCacheFilePath() const { return ssprintf( "Cache\\%u", GetHashForString(m_sSongDir) ); } /* Get a path to the SM containing data for this song. It might * be a cache file. */ const CString &Song::GetSongFilePath() const { ASSERT ( m_sSongFileName.GetLength() != 0 ); return m_sSongFileName; } NotesLoader *Song::MakeLoader( CString sDir ) const { NotesLoader *ret; /* Actually, none of these have any persistant data, so we * could optimize this, but since they don't have any data, * there's no real point ... */ ret = new SMLoader; if(ret->Loadable( sDir )) return ret; delete ret; ret = new DWILoader; if(ret->Loadable( sDir )) return ret; delete ret; ret = new BMSLoader; if(ret->Loadable( sDir )) return ret; delete ret; ret = new KSFLoader; if(ret->Loadable( sDir )) return ret; delete ret; return NULL; } bool Song::LoadWithoutCache( CString sDir ) { // // There was no entry in the cache for this song. // Let's load it from a file, then write a cache entry. // NotesLoader *ld = MakeLoader( sDir ); if(!ld) { LOG->Warn( "Couldn't find any SM, DWI, BMS, or KSF files in '%s'. This is not a valid song directory.", sDir.GetString() ); return false; } bool success = ld->LoadFromDir( sDir, *this ); delete ld; if(!success) return false; AddAutoGenNotes(); TidyUpData(); // save a cache file so we don't have to parse it all over again next time SaveToCacheFile(); return true; } bool Song::LoadFromSongDir( CString sDir ) { LOG->Trace( "Song::LoadFromSongDir(%s)", sDir.GetString() ); // make sure there is a trailing '\\' at the end of sDir if( sDir.Right(1) != "\\" ) sDir += "\\"; // save song dir m_sSongDir = sDir; // save group name CStringArray sDirectoryParts; split( m_sSongDir, "\\", sDirectoryParts, false ); m_sGroupName = sDirectoryParts[sDirectoryParts.size()-3]; // second from last item // // First look in the cache for this song (without loading NoteData) // unsigned uDirHash = SONGINDEX->GetCacheHash(m_sSongDir); if( GetHashForDirectory(m_sSongDir) == uDirHash && // this cache is up to date DoesFileExist(GetCacheFilePath())) { LOG->Trace( "Loading '%s' from cache file '%s'.", m_sSongDir.GetString(), GetCacheFilePath().GetString() ); SMLoader ld; ld.LoadFromSMFile( GetCacheFilePath(), *this ); } else { if(!LoadWithoutCache(m_sSongDir)) return false; } { /* Generated filename; this doesn't always point to a loadable file, * but instead points to the file we should write changed files to, * and will always be an .SM. * * This is a little tricky. We can't always use the song title directly, * since it might contain characters we can't store in filenames. Two * easy options: we could manually filter out invalid characters, or we * could use the name of the directory, which is always a valid filename * and should always be the same as the song. The former might not catch * everything--filename restrictions are platform-specific; we might even * be on an 8.3 filesystem, so let's do the latter. * * We can't rely on searching for other data filenames; it works for DWIs, * but not KSFs and BMSs. * * So, let's do this (by priority): * 1. If there's an .SM file, use that filename. No reason to use anything * else; it's the filename in use. * 2. If there's a .DWI, use it with a changed extension. * 3. Otherwise, use the name of the directory, since it's definitely a valid * filename, and should always be the title of the song (unlike KSFs). */ m_sSongFileName = m_sSongDir; CStringArray asFileNames; GetDirListing( m_sSongDir+"*.sm", asFileNames ); if( !asFileNames.empty() ) m_sSongFileName += asFileNames[0]; else { GetDirListing( m_sSongDir+"*.dwi", asFileNames ); if( !asFileNames.empty() ) { m_sSongFileName += asFileNames[0]; /* XXX: This would mess up "vote.for.dwight.d.eisenhower.dwi". */ m_sSongFileName.Replace( ".dwi", ".sm" ); } else { m_sSongFileName += sDirectoryParts[sDirectoryParts.size()-2]; // last item m_sSongFileName += ".sm"; } } } return true; } void Song::TidyUpData() { TrimRight(m_sMainTitle); if( m_sMainTitle == "" ) m_sMainTitle = "Untitled song"; TrimRight(m_sSubTitle); if( m_sArtist == "" ) m_sArtist = "Unknown artist"; if( m_BPMSegments.empty() ) throw RageException( "No #BPM specified in '%s%s.'", m_sSongDir.GetString(), m_sSongFileName.GetString() ); if( !HasMusic() ) { CStringArray arrayPossibleMusic; GetDirListing( m_sSongDir + CString("*.mp3"), arrayPossibleMusic ); GetDirListing( m_sSongDir + CString("*.ogg"), arrayPossibleMusic ); GetDirListing( m_sSongDir + CString("*.wav"), arrayPossibleMusic ); if( !arrayPossibleMusic.empty() ) // we found a match m_sMusicFile = arrayPossibleMusic[0]; // Don't throw on missing music. -Chris // else // throw RageException( "The song in '%s' is missing a music file. You must place a music file in the song folder or remove the song", m_sSongDir.GetString() ); } if( HasMusic() ) { #if 1 RageSoundStream sound; sound.Load( GetMusicPath() ); #else RageSound sound; sound.Load( GetMusicPath(), false ); /* don't pre-cache */ #endif m_fMusicLengthSeconds = sound.GetLengthSeconds(); /* XXX: if(m_fMusicLengthSeconds == -1), warn and throw out the song */ } else // ! HasMusic() { m_fMusicLengthSeconds = 100; // guess } // We're going to try and do something intelligent here... // The MusicSampleStart always seems to be about 100-120 beats into // the song regardless of BPM. Let's take a shot-in-the dark guess. if( m_fMusicSampleStartSeconds == 0 ) m_fMusicSampleStartSeconds = this->GetElapsedTimeFromBeat( 100 ); // // Here's the problem: We have a directory full of images. We want to determine which // image is the banner, which is the background, and which is the CDTitle. // // // First, check the file name for hints. // if( !HasBanner() ) { m_sBannerFile = ""; // find an image with "banner" in the file name CStringArray arrayPossibleBanners; GetDirListing( m_sSongDir + CString("*banner*.png"), arrayPossibleBanners ); GetDirListing( m_sSongDir + CString("*banner*.jpg"), arrayPossibleBanners ); GetDirListing( m_sSongDir + CString("*banner*.bmp"), arrayPossibleBanners ); GetDirListing( m_sSongDir + CString("*banner*.gif"), arrayPossibleBanners ); if( !arrayPossibleBanners.empty() ) m_sBannerFile = arrayPossibleBanners[0]; } if( !HasBackground() ) { m_sBackgroundFile = ""; // find an image with "bg" or "background" in the file name CStringArray arrayPossibleBGs; GetDirListing( m_sSongDir + CString("*bg*.png"), arrayPossibleBGs ); GetDirListing( m_sSongDir + CString("*bg*.jpg"), arrayPossibleBGs ); GetDirListing( m_sSongDir + CString("*bg*.bmp"), arrayPossibleBGs ); GetDirListing( m_sSongDir + CString("*bg*.gif"), arrayPossibleBGs ); GetDirListing( m_sSongDir + CString("*background*.png"), arrayPossibleBGs ); GetDirListing( m_sSongDir + CString("*background*.jpg"), arrayPossibleBGs ); GetDirListing( m_sSongDir + CString("*background*.bmp"), arrayPossibleBGs ); GetDirListing( m_sSongDir + CString("*background*.gif"), arrayPossibleBGs ); if( !arrayPossibleBGs.empty() ) m_sBackgroundFile = arrayPossibleBGs[0]; } if( !HasCDTitle() ) { m_sCDTitleFile = ""; // find an image with "cdtitle" in the file name CStringArray arrayPossibleCDTitles; GetDirListing( m_sSongDir + CString("*cdtitle*.png"), arrayPossibleCDTitles ); GetDirListing( m_sSongDir + CString("*cdtitle*.jpg"), arrayPossibleCDTitles ); GetDirListing( m_sSongDir + CString("*cdtitle*.bmp"), arrayPossibleCDTitles ); GetDirListing( m_sSongDir + CString("*cdtitle*.gif"), arrayPossibleCDTitles ); if( !arrayPossibleCDTitles.empty() ) m_sCDTitleFile = arrayPossibleCDTitles[0]; } // // Now, For the images we still haven't found, look at the image dimensions of the remaining unclassified images. // CStringArray arrayImages; GetDirListing( m_sSongDir + CString("*.png"), arrayImages ); GetDirListing( m_sSongDir + CString("*.jpg"), arrayImages ); GetDirListing( m_sSongDir + CString("*.bmp"), arrayImages ); GetDirListing( m_sSongDir + CString("*.gif"), arrayImages ); unsigned i; for( i=0; iw; int height = img->h; SDL_FreeSurface( img ); if( !HasBackground() && width >= 320 && height >= 240 ) { m_sBackgroundFile = arrayImages[i]; continue; } if( !HasBanner() && 100GetBeatAndBPSFromElapsedTime( -this->m_fBeat0OffsetInSeconds, fMusicStartBeat, fBPS, bFreeze ); this->AddBackgroundChange( BackgroundChange(fMusicStartBeat,sBGMovieFile) ); } } ReCalulateRadarValuesAndLastBeat(); // challenge notes are encoded as smaniac. If there is only one Notes for // a NotesType and it's "smaniac", then convert it to "Challenge" for( NotesType nt=(NotesType)0; nt apNotes; GetNotesThatMatch( nt, apNotes ); if( apNotes.size() == 1 ) { if( 0 == apNotes[0]->m_sDescription.CompareNoCase("smaniac") ) { apNotes[0]->m_sDescription = "Challenge"; apNotes[0]->m_Difficulty = DIFFICULTY_HARD; } } } } void Song::ReCalulateRadarValuesAndLastBeat() { // // calculate radar values and first/last beat // for( unsigned int i=0; iGetNoteData( &tempNoteData ); for( int r=0; rm_fRadarValues[r] = NoteDataUtil::GetRadarValue( tempNoteData, (RadarCategory)r, m_fMusicLengthSeconds ); float fFirstBeat = tempNoteData.GetFirstBeat(); float fLastBeat = tempNoteData.GetLastBeat(); if( m_fFirstBeat == -1 ) m_fFirstBeat = fFirstBeat; else m_fFirstBeat = min( m_fFirstBeat, fFirstBeat ); if( m_fLastBeat == -1 ) m_fLastBeat = fLastBeat; else m_fLastBeat = max( m_fLastBeat, fLastBeat ); } } void Song::GetNotesThatMatch( NotesType nt, CArray& arrayAddTo ) const { for( unsigned i=0; im_NotesType == nt ) arrayAddTo.push_back( m_apNotes[i] ); } } /* Return whether the song is playable in the given style. */ bool Song::SongCompleteForStyle( const StyleDef *st ) const { if(!SongHasNotesType(st->m_NotesType)) return false; return true; } bool Song::SongHasNotesType( NotesType nt ) const { for( unsigned i=0; i < m_apNotes.size(); i++ ) // foreach Notes if( m_apNotes[i]->m_NotesType == nt ) return true; return false; } bool Song::SongHasNotesTypeAndDifficulty( NotesType nt, Difficulty dc ) const { for( unsigned i=0; i < m_apNotes.size(); i++ ) // foreach Notes if( m_apNotes[i]->m_NotesType == nt && m_apNotes[i]->m_Difficulty == dc ) return true; return false; } void Song::SaveToCacheFile() { LOG->Trace( "Song::SaveToCacheFile()" ); SONGINDEX->AddCacheIndex(m_sSongDir, GetHashForDirectory(m_sSongDir)); SaveToSMFile( GetCacheFilePath(), true ); } void Song::Save() { LOG->Trace( "Song::SaveToSongFile()" ); /* rename all old files to avoid confusion. * * This also serves as a backup, so rename .sm's, too. If we crash when * saving the .sm, we don't want to lose what we had. But, what we really * should be doing is saving to another file (eg. foo.sm.new), then once we * know we havn't crashed, move the old .sm to .sm.old and the new one to * the real filename. That way, if we crash, we don't leave the song in an * unplayable state where the user has to manually un-rename stuff. XXX -glenn */ CStringArray arrayOldFileNames; GetDirListing( m_sSongDir + "*.bms", arrayOldFileNames ); GetDirListing( m_sSongDir + "*.dwi", arrayOldFileNames ); GetDirListing( m_sSongDir + "*.ksf", arrayOldFileNames ); GetDirListing( m_sSongDir + "*.sm", arrayOldFileNames ); for( unsigned i=0; iTrace( "Song::SaveToSMFile('%s')", sPath.GetString() ); NotesWriterSM wr; wr.Write(sPath, *this, bSavingCache); } void Song::SaveToDWIFile() { LOG->Trace( "Song::SaveToSongFileAndDWI()" ); CString sPath = GetSongFilePath(); sPath.Replace( ".sm", ".dwi" ); NotesWriterDWI wr; wr.Write(sPath, *this); } void Song::AddAutoGenNotes() { for( NotesType ntMissing=(NotesType)0; ntMissingNotesTypeToNumTracks(ntMissing); unsigned j; // look for previously autogen'd data with the same number of tracks for( j=0; jm_NotesType; int iNumOriginalNotesTracks = GAMEMAN->NotesTypeToNumTracks(ntOriginal); if( pOriginalNotes->m_bAutoGen && iNumTracksOfMissing == iNumOriginalNotesTracks ) { AutoGen( ntMissing, ntOriginal ); goto next_notes_type; // done searching } } // look for closest match NotesType ntBestMatch = (NotesType)-1; int iBestTrackDifference = 10000; // inf for( NotesType nt=(NotesType)0; ntNotesTypeToNumTracks(nt); int iTrackDifference = abs(iNumTracks-iNumTracksOfMissing); CArray apNotes; this->GetNotesThatMatch( nt, apNotes ); if( iTrackDifference < iBestTrackDifference && !apNotes.empty() && !apNotes[0]->m_bAutoGen ) { ntBestMatch = nt; iBestTrackDifference = iTrackDifference; } } if( ntBestMatch != -1 ) AutoGen( ntMissing, ntBestMatch ); } } void Song::AutoGen( NotesType ntTo, NotesType ntFrom ) { int iNumTracksOfTo = GAMEMAN->NotesTypeToNumTracks(ntTo); for( unsigned int j=0; jm_NotesType == ntFrom ) { Notes* pNewNotes = new Notes; pNewNotes->m_Difficulty = pOriginalNotes->m_Difficulty; pNewNotes->m_iMeter = pOriginalNotes->m_iMeter; pNewNotes->m_sDescription = pOriginalNotes->m_sDescription; pNewNotes->m_bAutoGen = true; pNewNotes->m_NotesType = ntTo; NoteData originalNoteData; NoteData newNoteData; pOriginalNotes->GetNoteData( &originalNoteData ); newNoteData.LoadTransformedSlidingWindow( &originalNoteData, iNumTracksOfTo ); pNewNotes->SetNoteData( &newNoteData ); this->m_apNotes.push_back( pNewNotes ); } } } Grade Song::GetGradeForDifficulty( const StyleDef *st, int p, Difficulty dc ) const { // return max grade of notes in difficulty class CArray aNotes; this->GetNotesThatMatch( st->m_NotesType, aNotes ); SortNotesArrayByDifficulty( aNotes ); Grade grade = GRADE_NO_DATA; for( unsigned i=0; im_Difficulty == dc ) grade = max( grade, pNotes->m_TopGrade ); } return grade; } bool Song::IsNew() const { return GetNumTimesPlayed()==0; } bool Song::IsEasy( NotesType nt ) const { for( unsigned i=0; im_NotesType != nt ) continue; if( pNotes->m_iMeter <= 2 ) return true; } return false; } ///////////////////////////////////// // Sorting ///////////////////////////////////// int CompareSongPointersByTitle(const Song *pSong1, const Song *pSong2) { //Prefer transliterations to full titles CString sTitle1 = pSong1->GetSortTitle(); CString sTitle2 = pSong2->GetSortTitle(); int ret = sTitle1.CompareNoCase(sTitle2); if(ret < 0) return true; if(ret > 0) return false; /* The titles are the same. Ensure we get a consistent ordering * by comparing the unique SongFilePaths. */ return pSong1->GetSongFilePath().CompareNoCase(pSong2->GetSongFilePath()); } void SortSongPointerArrayByTitle( CArray &arraySongPointers ) { sort( arraySongPointers.begin(), arraySongPointers.end(), CompareSongPointersByTitle ); } int CompareSongPointersByDifficulty(const Song *pSong1, const Song *pSong2) { CArray aNotes1; CArray aNotes2; pSong1->GetNotesThatMatch( GAMESTATE->GetCurrentStyleDef()->m_NotesType, aNotes1 ); pSong2->GetNotesThatMatch( GAMESTATE->GetCurrentStyleDef()->m_NotesType, aNotes2 ); int iEasiestMeter1 = 1000; // infinity int iEasiestMeter2 = 1000; // infinity unsigned i; for( i=0; im_iMeter ); for( i=0; im_iMeter ); if( iEasiestMeter1 < iEasiestMeter2 ) return true; if( iEasiestMeter1 > iEasiestMeter2 ) return false; return CompareSongPointersByTitle( pSong1, pSong2 ); } void SortSongPointerArrayByDifficulty( CArray &arraySongPointers ) { sort( arraySongPointers.begin(), arraySongPointers.end(), CompareSongPointersByDifficulty ); } bool CompareSongPointersByBPM(const Song *pSong1, const Song *pSong2) { float fMinBPM1, fMaxBPM1, fMinBPM2, fMaxBPM2; pSong1->GetMinMaxBPM( fMinBPM1, fMaxBPM1 ); pSong2->GetMinMaxBPM( fMinBPM2, fMaxBPM2 ); if( fMaxBPM1 < fMaxBPM2 ) return true; if( fMaxBPM1 > fMaxBPM2 ) return false; return CompareCStringsAsc( pSong1->GetSongFilePath(), pSong2->GetSongFilePath() ); } void SortSongPointerArrayByBPM( CArray &arraySongPointers ) { sort( arraySongPointers.begin(), arraySongPointers.end(), CompareSongPointersByBPM ); } int CompareSongPointersByArtist(const Song *pSong1, const Song *pSong2) { CString sArtist1 = pSong1->m_sArtist; CString sArtist2 = pSong2->m_sArtist; if( sArtist1 < sArtist2 ) return true; if( sArtist1 > sArtist2 ) return false; return CompareSongPointersByTitle( pSong1, pSong2 ); } void SortSongPointerArrayByArtist( CArray &arraySongPointers ) { sort( arraySongPointers.begin(), arraySongPointers.end(), CompareSongPointersByArtist ); } int CompareSongPointersByGroup(const Song *pSong1, const Song *pSong2) { const CString &sGroup1 = pSong1->m_sGroupName; const CString &sGroup2 = pSong2->m_sGroupName; if( sGroup1 < sGroup2 ) return true; if( sGroup1 > sGroup2 ) return false; /* Same group; compare by difficulty. */ return CompareSongPointersByDifficulty( pSong1, pSong2 ); } void SortSongPointerArrayByGroup( CArray &arraySongPointers ) { sort( arraySongPointers.begin(), arraySongPointers.end(), CompareSongPointersByGroup ); } int CompareSongPointersByMostPlayed(const Song *pSong1, const Song *pSong2) { int iNumTimesPlayed1 = pSong1->GetNumTimesPlayed(); int iNumTimesPlayed2 = pSong2->GetNumTimesPlayed(); if( iNumTimesPlayed1 > iNumTimesPlayed2 ) return true; if( iNumTimesPlayed1 < iNumTimesPlayed2 ) return false; return CompareSongPointersByTitle( pSong1, pSong2 ); } void SortSongPointerArrayByMostPlayed( CArray &arraySongPointers ) { sort( arraySongPointers.begin(), arraySongPointers.end(), CompareSongPointersByMostPlayed ); } bool Song::NormallyDisplayed() const { if(!PREFSMAN->m_bHiddenSongs) return true; return m_SelectionDisplay == SHOW_ALWAYS; } bool Song::RouletteDisplayed() const { if(!PREFSMAN->m_bHiddenSongs) return true; return m_SelectionDisplay != SHOW_NEVER; } bool Song::HasMusic() const {return m_sMusicFile != "" && IsAFile(GetMusicPath()); } bool Song::HasBanner() const {return m_sBannerFile != "" && IsAFile(GetBannerPath()); } bool Song::HasBackground() const {return m_sBackgroundFile != "" && IsAFile(GetBackgroundPath()); } bool Song::HasCDTitle() const {return m_sCDTitleFile != "" && IsAFile(GetCDTitlePath()); } bool Song::HasBGChanges() const {return !m_BackgroundChanges.empty(); } int Song::GetNumTimesPlayed() const { int iTotalNumTimesPlayed = 0; for( unsigned i=0; im_iNumTimesPlayed; } return iTotalNumTimesPlayed; } /* Search semantics for all song files: * * If the path doesn't have any directory separators, it's a filename in the * song directory. * * If it does, it's relative to the top directory of the tree it was loaded * from, eg ".\music\stuff\song.ogg". Most of the time, that's the SM tree, * and that's the PWD, so just return it directly. If the file was originally * loaded from the DWIPath, it's relative to that, so prepend it. * * Some DWI's do have relative paths that don't start with ".\". */ /* We only follow this for song files and cdtitles; it's for compatibility * with DWI. We prefer paths relative to the song directory; only support * that for all other paths. */ /* Note: Prepending the dwipath is ugly. The first impression might be to * add a Song::TopFilePath, but don't do that--we have too much stuff in there * already. We don't really need it; this is the only place it'd be used. What * we *should* be doing is prepending this path when we first load the song. * However, dwipaths are usually like "c:\games\dwi", and we can't store colons * in SM's; they'll get interpreted as delimiters. So: * XXX: Add some kind of escape character to SM's. * * -glenn */ CString Song::GetMusicPath() const { /* If there's no path in the music file, the file is in the same directory * as the song. (This is the preferred configuration.) */ if( m_sMusicFile.Find('\\') == -1) return m_sSongDir+m_sMusicFile; /* The file has a path. If it was loaded from the m_DWIPath, it's relative * to that. */ if( PREFSMAN->m_DWIPath!="" && m_sSongDir.Left(PREFSMAN->m_DWIPath.GetLength()) == PREFSMAN->m_DWIPath ) return PREFSMAN->m_DWIPath+"\\"+m_sMusicFile; /* Otherwise, it's relative to the top of the SM directory (the CWD), so * return it directly. */ return m_sMusicFile; } CString Song::GetBannerPath() const { return m_sSongDir+m_sBannerFile; } CString Song::GetCDTitlePath() const { if( m_sCDTitleFile.Find('\\') == -1) return m_sSongDir+m_sCDTitleFile; if( PREFSMAN->m_DWIPath!="" && m_sSongDir.Left(PREFSMAN->m_DWIPath.GetLength()) == PREFSMAN->m_DWIPath ) return PREFSMAN->m_DWIPath+"\\"+m_sCDTitleFile; return m_sCDTitleFile; } CString Song::GetBackgroundPath() const { return m_sSongDir+m_sBackgroundFile; } /* Get the first/last beat of any currently active note pattern. If two * players are active, they often have the same start beat, but they don't * have to. * * This is currently slow (notedata can't cache the return, and getnotedata * is slow). */ #if 0 /* XXX not finished/tested/used yet -glenn */ float Song::GetFirstBeat() const { float first = MAX_BEATS; for( int pn = 0; pn < NUM_PLAYERS; ++pn) { if(!GAMESTATE->IsPlayerEnabled(pn)) continue; NoteData tempNoteData; GAMESTATE->m_pCurNotes[pn]->GetNoteData( &tempNoteData ); first = min(first, tempNoteData.GetFirstBeat()); } return first; } float Song::GetLastBeat() const { float last = MAX_BEATS; for( int pn = 0; pn < NUM_PLAYERS; ++pn) { if(!GAMESTATE->IsPlayerEnabled(pn)) continue; NoteData tempNoteData; GAMESTATE->m_pCurNotes[pn]->GetNoteData( &tempNoteData ); last = max(last, tempNoteData.GetLastBeat()); } return last; } #endif