From 64e7b85a9eeab9f8395aaf6e1ba02eb30d3551cd Mon Sep 17 00:00:00 2001 From: AJ Kelly Date: Sun, 1 Jul 2012 12:24:39 -0500 Subject: [PATCH] add SaveToJsonFile() function from SM4 betas. --- src/Song.cpp | 4082 +++++++++++++++++++++++++------------------------- src/Song.h | 954 ++++++------ 2 files changed, 2523 insertions(+), 2513 deletions(-) diff --git a/src/Song.cpp b/src/Song.cpp index 719509a18a..75558a5337 100644 --- a/src/Song.cpp +++ b/src/Song.cpp @@ -1,2038 +1,2044 @@ -#include "global.h" -#include "Song.h" -#include "Steps.h" -#include "RageUtil.h" -#include "RageLog.h" -#include "NoteData.h" -#include "RageSoundReader_FileReader.h" -#include "RageSurface_Load.h" -#include "SongCacheIndex.h" -#include "GameManager.h" -#include "PrefsManager.h" -#include "Style.h" -#include "FontCharAliases.h" -#include "TitleSubstitution.h" -#include "BannerCache.h" -//#include "BackgroundCache.h" -#include "Sprite.h" -#include "RageFileManager.h" -#include "RageSurface.h" -#include "NoteDataUtil.h" -#include "SongUtil.h" -#include "StepsUtil.h" -#include "Foreach.h" -#include "BackgroundUtil.h" -#include "SpecialFiles.h" -#include "NotesLoader.h" -#include "NotesLoaderSM.h" -#include "NotesLoaderSSC.h" -#include "NotesWriterDWI.h" -#include "NotesWriterSM.h" -#include "NotesWriterSSC.h" -#include "UnlockManager.h" -#include "LyricsLoader.h" - -#include -#include -#include - - -/** - * @brief The internal version of the cache for StepMania. - * - * Increment this value to invalidate the current cache. */ -const int FILE_CACHE_VERSION = 208; - -/** @brief How long does a song sample last by default? */ -const float DEFAULT_MUSIC_SAMPLE_LENGTH = 12.f; - -static Preference g_fLongVerSongSeconds( "LongVerSongSeconds", 60*2.5f ); -static Preference g_fMarathonVerSongSeconds( "MarathonVerSongSeconds", 60*5.f ); -static Preference g_BackUpAllSongSaves( "BackUpAllSongSaves", false ); - -static const char *InstrumentTrackNames[] = { - "Guitar", - "Rhythm", - "Bass", -}; -XToString( InstrumentTrack ); -StringToX( InstrumentTrack ); - -Song::Song() -{ - FOREACH_BackgroundLayer( i ) - m_BackgroundChanges[i] = AutoPtrCopyOnWrite(new VBackgroundChange); - m_ForegroundChanges = AutoPtrCopyOnWrite(new VBackgroundChange); - - m_LoadedFromProfile = ProfileSlot_Invalid; - m_fVersion = STEPFILE_VERSION_NUMBER; - m_fMusicSampleStartSeconds = -1; - m_fMusicSampleLengthSeconds = DEFAULT_MUSIC_SAMPLE_LENGTH; - m_fMusicLengthSeconds = 0; - firstSecond = -1; - lastSecond = -1; - specifiedLastSecond = -1; - m_SelectionDisplay = SHOW_ALWAYS; - m_bEnabled = true; - m_DisplayBPMType = DISPLAY_BPM_ACTUAL; - m_fSpecifiedBPMMin = 0; - m_fSpecifiedBPMMax = 0; - m_bIsSymLink = false; - m_bHasMusic = false; - m_bHasBanner = false; - m_bHasBackground = false; -} - -Song::~Song() -{ - FOREACH( Steps*, m_vpSteps, s ) - SAFE_DELETE( *s ); - m_vpSteps.clear(); - - // It's the responsibility of the owner of this Song to make sure - // that all pointers to this Song and its Steps are invalidated. -} - -void Song::DetachSteps() -{ - m_vpSteps.clear(); - FOREACH_ENUM( StepsType, st ) - m_vpStepsByType[st].clear(); -} - -float Song::GetFirstSecond() const -{ - return this->firstSecond; -} - -float Song::GetFirstBeat() const -{ - return this->m_SongTiming.GetBeatFromElapsedTime(this->firstSecond); -} - -float Song::GetLastSecond() const -{ - return this->lastSecond; -} - -float Song::GetLastBeat() const -{ - return this->m_SongTiming.GetBeatFromElapsedTime(this->lastSecond); -} - -float Song::GetSpecifiedLastSecond() const -{ - return this->specifiedLastSecond; -} - -float Song::GetSpecifiedLastBeat() const -{ - return this->m_SongTiming.GetBeatFromElapsedTime(this->specifiedLastSecond); -} - -void Song::SetFirstSecond(const float f) -{ - this->firstSecond = f; -} - -void Song::SetLastSecond(const float f) -{ - this->lastSecond = f; -} - -void Song::SetSpecifiedLastSecond(const float f) -{ - this->specifiedLastSecond = f; -} - -// Reset to an empty song. -void Song::Reset() -{ - FOREACH( Steps*, m_vpSteps, s ) - SAFE_DELETE( *s ); - m_vpSteps.clear(); - FOREACH_ENUM( StepsType, st ) - m_vpStepsByType[st].clear(); - - Song empty; - *this = empty; - - // It's the responsibility of the owner of this Song to make sure - // that all pointers to this Song and its Steps are invalidated. -} - - -void Song::AddBackgroundChange( BackgroundLayer iLayer, BackgroundChange seg ) -{ - // Delete old background change at this start beat, if any. - FOREACH( BackgroundChange, GetBackgroundChanges(iLayer), bgc ) - { - if( bgc->m_fStartBeat == seg.m_fStartBeat ) - { - GetBackgroundChanges(iLayer).erase( bgc ); - break; - } - } - - ASSERT( iLayer >= 0 && iLayer < NUM_BackgroundLayer ); - BackgroundUtil::AddBackgroundChange( GetBackgroundChanges(iLayer), seg ); -} - -void Song::AddForegroundChange( BackgroundChange seg ) -{ - BackgroundUtil::AddBackgroundChange( GetForegroundChanges(), seg ); -} - -void Song::AddLyricSegment( LyricSegment seg ) -{ - m_LyricSegments.push_back( seg ); -} - -Steps *Song::CreateSteps() -{ - Steps *pSteps = new Steps; - InitSteps( pSteps ); - return pSteps; -} - -void Song::InitSteps(Steps *pSteps) -{ - pSteps->m_Timing = this->m_SongTiming; - pSteps->m_sAttackString = this->m_sAttackString; - pSteps->m_Attacks = this->m_Attacks; - pSteps->SetDisplayBPM(this->m_DisplayBPMType); - pSteps->SetMinBPM(this->m_fSpecifiedBPMMin); - pSteps->SetMaxBPM(this->m_fSpecifiedBPMMax); -} - -void Song::GetDisplayBpms( DisplayBpms &AddTo ) const -{ - if( m_DisplayBPMType == DISPLAY_BPM_SPECIFIED ) - { - AddTo.Add( m_fSpecifiedBPMMin ); - AddTo.Add( m_fSpecifiedBPMMax ); - } - else - { - float fMinBPM, fMaxBPM; - m_SongTiming.GetActualBPM( fMinBPM, fMaxBPM ); - AddTo.Add( fMinBPM ); - AddTo.Add( fMaxBPM ); - } -} - -const BackgroundChange &Song::GetBackgroundAtBeat( BackgroundLayer iLayer, float fBeat ) const -{ - for( unsigned i=0; i fBeat ) - return GetBackgroundChanges(iLayer)[i]; - return GetBackgroundChanges(iLayer)[0]; -} - - -RString Song::GetCacheFilePath() const -{ - return SongCacheIndex::GetCacheFilePath( "Songs", m_sSongDir ); -} - -// Get a path to the SM containing data for this song. It might be a cache file. -const RString &Song::GetSongFilePath() const -{ - ASSERT_M( !m_sSongFileName.empty(), - ssprintf("The song %s has no filename associated with it!", - this->m_sMainTitle.c_str())); - return m_sSongFileName; -} - -/* Hack: This should be a parameter to TidyUpData, but I don't want to pull in - * into Song.h, which is heavily used. */ -static set BlacklistedImages; - -/* If PREFSMAN->m_bFastLoad is true, always load from cache if possible. - * Don't read the contents of sDir if we can avoid it. That means we can't call - * HasMusic(), HasBanner() or GetHashForDirectory(). - * If true, check the directory hash and reload the song from scratch if it's changed. - */ -bool Song::LoadFromSongDir( RString sDir ) -{ -// LOG->Trace( "Song::LoadFromSongDir(%s)", sDir.c_str() ); - ASSERT_M( sDir != "", "Songs can't be loaded from an empty directory!" ); - - // make sure there is a trailing slash at the end of sDir - if( sDir.Right(1) != "/" ) - sDir += "/"; - - // save song dir - m_sSongDir = sDir; - - // save group name - vector sDirectoryParts; - split( m_sSongDir, "/", sDirectoryParts, false ); - ASSERT( sDirectoryParts.size() >= 4 ); /* e.g. "/Songs/Slow/Taps/" */ - m_sGroupName = sDirectoryParts[sDirectoryParts.size()-3]; // second from last item - ASSERT( m_sGroupName != "" ); - - // First, look in the cache for this song (without loading NoteData) - unsigned uCacheHash = SONGINDEX->GetCacheHash(m_sSongDir); - bool bUseCache = true; - RString sCacheFilePath = GetCacheFilePath(); - - if( !DoesFileExist(sCacheFilePath) ) - bUseCache = false; - if( !PREFSMAN->m_bFastLoad && GetHashForDirectory(m_sSongDir) != uCacheHash ) - bUseCache = false; // this cache is out of date - - if( bUseCache ) - { - /* - LOG->Trace("Loading '%s' from cache file '%s'.", - m_sSongDir.c_str(), - GetCacheFilePath().c_str()); - */ - SSCLoader loaderSSC; - bool bLoadedFromSSC = loaderSSC.LoadFromSimfile( sCacheFilePath, *this, true ); - if( !bLoadedFromSSC ) - { - // load from .sm - SMLoader loaderSM; - loaderSM.LoadFromSimfile( sCacheFilePath, *this, true ); - loaderSM.TidyUpData( *this, true ); - } - } - else - { - // There was no entry in the cache for this song, or it was out of date. - // Let's load it from a file, then write a cache entry. - - if( !NotesLoader::LoadFromDir(sDir, *this, BlacklistedImages) ) - { - LOG->UserLog( "Song", sDir, "has no SSC, SM, SMA, DWI, BMS, or KSF files." ); - - vector vs; - GetDirListing( sDir + "*.mp3", vs, false, false ); - GetDirListing( sDir + "*.oga", vs, false, false ); - GetDirListing( sDir + "*.ogg", vs, false, false ); - bool bHasMusic = !vs.empty(); - - if( !bHasMusic ) - { - LOG->UserLog( "Song", sDir, "has no music file either. Ignoring this song directory." ); - return false; - } - // Make sure we have a future filename figured out. - vector folders; - split(sDir, "/", folders); - RString songName = folders[2] + ".ssc"; - this->m_sSongFileName = sDir + songName; - // Continue on with a blank Song so that people can make adjustments using the editor. - } - TidyUpData(false, true); - - // save a cache file so we don't have to parse it all over again next time - if( !SaveToCacheFile() ) - sCacheFilePath = RString(); - } - - FOREACH( Steps*, m_vpSteps, s ) - { - /* Compress all Steps. During initial caching, this will remove cached - * NoteData; during cached loads, this will just remove cached SMData. */ - (*s)->Compress(); - } - - // Load the cached banners, if it's not loaded already. - if( PREFSMAN->m_BannerCache == BNCACHE_LOW_RES_PRELOAD && m_bHasBanner ) - BANNERCACHE->LoadBanner( GetBannerPath() ); - // Load the cached background, if it's not loaded already. - /* - if( PREFSMAN->m_BackgroundCache == BGCACHE_LOW_RES_PRELOAD && m_bHasBackground ) - BACKGROUNDCACHE->LoadBackground( GetBackgroundPath() ); - */ - - // Add AutoGen pointers. (These aren't cached.) - AddAutoGenNotes(); - - if( !m_bHasMusic ) - { - LOG->UserLog( "Song", sDir, "has no music; ignored." ); - return false; // don't load this song - } - - return true; // do load this song -} - -bool Song::ReloadFromSongDir( RString sDir ) -{ - RemoveAutoGenNotes(); - vector vOldSteps = m_vpSteps; - - Song copy; - if( !copy.LoadFromSongDir( sDir ) ) - return false; - copy.RemoveAutoGenNotes(); - *this = copy; - - // First we assemble a map to let us easily find the new steps - map mNewSteps; - for( vector::const_iterator it = m_vpSteps.begin(); it != m_vpSteps.end(); ++it ) - { - StepsID id; - id.FromSteps( *it ); - mNewSteps[id] = *it; - } - - // Now we wipe out the new pointers, which were shallow copied and not deep copied... - m_vpSteps.clear(); - FOREACH_ENUM( StepsType, i ) - m_vpStepsByType[i].clear(); - - /* Then we copy as many Steps as possible on top of the old pointers. - * The only pointers that change are pointers to Steps that are not in the - * reverted file, which we delete, and pointers to Steps that are in the - * reverted file but not the original *this, which we create new copies of. - * We have to go through these hoops because many places assume the Steps - * pointers don't change - even though there are other ways they can change, - * such as deleting a Steps via the editor. */ - for( vector::const_iterator itOld = vOldSteps.begin(); itOld != vOldSteps.end(); ++itOld ) - { - StepsID id; - id.FromSteps( *itOld ); - map::iterator itNew = mNewSteps.find( id ); - if( itNew == mNewSteps.end() ) - { - // This stepchart didn't exist in the file we reverted from - delete *itOld; - } - else - { - Steps *OldSteps = *itOld; - *OldSteps = *(itNew->second); - AddSteps( OldSteps ); - mNewSteps.erase( itNew ); - } - } - // The leftovers in the map are steps that didn't exist before we reverted - for( map::const_iterator it = mNewSteps.begin(); it != mNewSteps.end(); ++it ) - { - Steps *NewSteps = new Steps(); - *NewSteps = *(it->second); - AddSteps( NewSteps ); - } - - AddAutoGenNotes(); - return true; -} - -static void GetImageDirListing( RString sPath, vector &AddTo ) -{ - GetDirListing( sPath + ".png", AddTo, false, false ); - GetDirListing( sPath + ".jpg", AddTo, false, false ); - GetDirListing( sPath + ".jpeg", AddTo, false, false ); - GetDirListing( sPath + ".bmp", AddTo, false, false ); - GetDirListing( sPath + ".gif", AddTo, false, false ); -} - -/* Fix up song paths. If there's a leading "./", be sure to keep it: it's - * a signal that the path is from the root directory, not the song directory. - * Other than a leading "./", song paths must never contain "." or "..". */ -void FixupPath( RString &path, const RString &sSongPath ) -{ - // Replace backslashes with slashes in all paths. - FixSlashesInPlace( path ); - - /* Many imported files contain erroneous whitespace before or after - * filenames. Paths usually don't actually start or end with spaces, - * so let's just remove it. */ - Trim( path ); -} - -// Songs in BlacklistImages will never be autodetected as song images. -void Song::TidyUpData( bool fromCache, bool duringCache ) -{ - // We need to do this before calling any of HasMusic, HasHasCDTitle, etc. - ASSERT_M( m_sSongDir.Left(3) != "../", m_sSongDir ); // meaningless - FixupPath( m_sSongDir, "" ); - FixupPath( m_sMusicFile, m_sSongDir ); - FOREACH_ENUM( InstrumentTrack, i ) - if( !m_sInstrumentTrackFile[i].empty() ) - FixupPath( m_sInstrumentTrackFile[i], m_sSongDir ); - FixupPath( m_sBannerFile, m_sSongDir ); - FixupPath( m_sJacketFile, m_sSongDir ); - FixupPath( m_sCDFile, m_sSongDir ); - FixupPath( m_sDiscFile, m_sSongDir ); - FixupPath( m_sLyricsFile, m_sSongDir ); - FixupPath( m_sBackgroundFile, m_sSongDir ); - FixupPath( m_sCDTitleFile, m_sSongDir ); - - if( !HasMusic() ) - { - vector arrayPossibleMusic; - GetDirListing( m_sSongDir + RString("*.mp3"), arrayPossibleMusic ); - GetDirListing( m_sSongDir + RString("*.oga"), arrayPossibleMusic ); - GetDirListing( m_sSongDir + RString("*.ogg"), arrayPossibleMusic ); - GetDirListing( m_sSongDir + RString("*.wav"), arrayPossibleMusic ); - - if( !arrayPossibleMusic.empty() ) - { - int idx = 0; - /* If the first song is "intro", and we have more than one available, - * don't use it--it's probably a KSF intro music file, which we don't - * (yet) support. */ - if( arrayPossibleMusic.size() > 1 && - !arrayPossibleMusic[0].Left(5).CompareNoCase("intro") ) - ++idx; - - // we found a match - m_sMusicFile = arrayPossibleMusic[idx]; - } - } - - // This must be done before radar calculation. - if( HasMusic() ) - { - RString error; - RageSoundReader *Sample = RageSoundReader_FileReader::OpenFile( GetMusicPath(), error ); - /* XXX: Checking if the music file exists eliminates a warning - * originating from BMS files (which have no music file, per se) - * but it's something of a hack. */ - if( Sample == NULL && m_sMusicFile != "" ) - { - LOG->UserLog( "Sound file", GetMusicPath(), "couldn't be opened: %s", error.c_str() ); - - // Don't use this file. - m_sMusicFile = ""; - } - else if ( Sample != NULL ) - { - m_fMusicLengthSeconds = Sample->GetLength() / 1000.0f; - delete Sample; - - if( m_fMusicLengthSeconds < 0 ) - { - // It failed; bad file or something. It's already logged a warning. - m_fMusicLengthSeconds = 100; // guess - } - else if( m_fMusicLengthSeconds == 0 ) - { - LOG->UserLog( "Sound file", GetMusicPath(), "is empty." ); - } - } - } - else // ! HasMusic() - { - m_fMusicLengthSeconds = 100; // guess - LOG->UserLog("Song", - GetSongDir(), - "has no music file; guessing at %f seconds", - m_fMusicLengthSeconds); - } - - if( m_fMusicLengthSeconds < 0 ) - { - LOG->UserLog("Sound file", - GetMusicPath(), - "has a negative length %f.", - m_fMusicLengthSeconds); - m_fMusicLengthSeconds = 0; - } - - m_SongTiming.TidyUpData(); - - FOREACH( Steps *, m_vpSteps, s ) - { - (*s)->m_Timing.TidyUpData(); - } - - /* Generate these before we autogen notes, so the new notes can inherit - * their source's values. */ - ReCalculateRadarValuesAndLastSecond( fromCache, true ); - - Trim( m_sMainTitle ); - Trim( m_sSubTitle ); - Trim( m_sArtist ); - - // Fall back on the song directory name. - if( m_sMainTitle == "" ) - NotesLoader::GetMainAndSubTitlesFromFullTitle(Basename(this->GetSongDir()), - m_sMainTitle, m_sSubTitle ); - - if( m_sArtist == "" ) - m_sArtist = "Unknown artist"; - TranslateTitles(); - - if( m_fMusicSampleStartSeconds == -1 || - m_fMusicSampleLengthSeconds == 0 || - m_fMusicSampleStartSeconds+m_fMusicSampleLengthSeconds > this->m_fMusicLengthSeconds ) - { - const TimingData &timing = this->m_SongTiming; - m_fMusicSampleStartSeconds = timing.GetElapsedTimeFromBeat( 100 ); - - if( m_fMusicSampleStartSeconds+m_fMusicSampleLengthSeconds > this->m_fMusicLengthSeconds ) - { - // Attempt to get a reasonable default. - int iBeat = lrintf(this->m_SongTiming.GetBeatFromElapsedTime(this->GetLastSecond())/2); - iBeat -= iBeat%4; - m_fMusicSampleStartSeconds = timing.GetElapsedTimeFromBeat( (float)iBeat ); - } - } - - // The old logic meant that you couldn't have sample lengths that go forever, - // e.g. those in Donkey Konga. I never liked that. -freem - if( m_fMusicSampleLengthSeconds <= 0.00f ) - m_fMusicSampleLengthSeconds = DEFAULT_MUSIC_SAMPLE_LENGTH; - - // 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. - - CHECKPOINT_M( "Looking for images..." ); - - if( !fromCache ) - { - - // First, check the file name for hints. - if( !HasBanner() ) - { - /* If a nonexistant banner file is specified, and we can't find a - * replacement, don't wipe out the old value. */ - //m_sBannerFile = ""; - - // find an image with "banner" in the file name - vector arrayPossibleBanners; - GetImageDirListing( m_sSongDir + "*banner*", arrayPossibleBanners ); - - /* Some people do things differently for the sake of being different. - * Don't match eg. abnormal, numbness. */ - GetImageDirListing( m_sSongDir + "* BN", arrayPossibleBanners ); - - if( !arrayPossibleBanners.empty() ) - m_sBannerFile = arrayPossibleBanners[0]; - } - - if( !HasBackground() ) - { - //m_sBackgroundFile = ""; - - // find an image with "bg" or "background" in the file name - vector arrayPossibleBGs; - GetImageDirListing( m_sSongDir + "*background*", arrayPossibleBGs ); - // don't match e.g. "subgroup", "hobgoblin", etc. - GetImageDirListing( m_sSongDir + "*bg", arrayPossibleBGs ); - if( !arrayPossibleBGs.empty() ) - m_sBackgroundFile = arrayPossibleBGs[0]; - } - - if( !HasJacket() ) - { - // find an image with "jacket" or "albumart" in the filename. - vector arrayPossibleJackets; - GetImageDirListing( m_sSongDir + "jk_*", arrayPossibleJackets ); - GetImageDirListing( m_sSongDir + "*jacket*", arrayPossibleJackets ); - GetImageDirListing( m_sSongDir + "*albumart*", arrayPossibleJackets ); - if( !arrayPossibleJackets.empty() ) - m_sJacketFile = arrayPossibleJackets[0]; - } - - if( !HasCDImage() ) - { - // CD image, a la ddr 1st-3rd (not to be confused with CDTitles) - // find an image with "-cd" at the end of the filename. - vector arrayPossibleCDImages; - GetImageDirListing( m_sSongDir + "*-cd", arrayPossibleCDImages ); - if( !arrayPossibleCDImages.empty() ) - m_sCDFile = arrayPossibleCDImages[0]; - } - - if( !HasDisc() ) - { - // a rectangular graphic, not to be confused with CDImage above. - vector arrayPossibleDiscImages; - GetImageDirListing( m_sSongDir + "* disc", arrayPossibleDiscImages ); - GetImageDirListing( m_sSongDir + "* title", arrayPossibleDiscImages ); - if( !arrayPossibleDiscImages.empty() ) - m_sDiscFile = arrayPossibleDiscImages[0]; - } - - if( !HasCDTitle() ) - { - // find an image with "cdtitle" in the file name - vector arrayPossibleCDTitles; - GetImageDirListing( m_sSongDir + "*cdtitle*", arrayPossibleCDTitles ); - if( !arrayPossibleCDTitles.empty() ) - m_sCDTitleFile = arrayPossibleCDTitles[0]; - } - - if( !HasLyrics() ) - { - // Check if there is a lyric file in here - vector arrayLyricFiles; - GetDirListing(m_sSongDir + RString("*.lrc"), arrayLyricFiles ); - if( !arrayLyricFiles.empty() ) - m_sLyricsFile = arrayLyricFiles[0]; - } - - /* Now, For the images we still haven't found, - * look at the image dimensions of the remaining unclassified images. */ - vector arrayImages; - GetImageDirListing( m_sSongDir + "*", arrayImages ); - - for( unsigned i=0; iUserLog( "Graphic file", sPath, "couldn't be loaded: %s", error.c_str() ); - continue; - } - - const int width = img->w; - const int height = img->h; - delete img; - - if( !HasBackground() && width >= 320 && height >= 240 ) - { - m_sBackgroundFile = arrayImages[i]; - continue; - } - - if( !HasBanner() && 100<=width && width<=320 && 50<=height && height<=240 ) - { - m_sBannerFile = arrayImages[i]; - continue; - } - - /* Some songs have overlarge banners. Check if the ratio is reasonable - * (over 2:1; usually over 3:1), and large (not a cdtitle). */ - if( !HasBanner() && width > 200 && float(width) / height > 2.0f ) - { - m_sBannerFile = arrayImages[i]; - continue; - } - - /* Agh. DWI's inline title images are triggering this, resulting in - * kanji, etc., being used as a CDTitle for songs with none. Some - * sample data from random incarnations: - * 42x50 35x50 50x50 144x49 - * It looks like ~50 height is what people use to align to DWI's font. - * - * My tallest CDTitle is 44. Let's cut off in the middle and hope for - * the best. -(who? -aj) */ - /* The proper size of a CDTitle is 64x48 or sometihng. Simfile artists - * typically don't give a shit about this (see Cetaka's fucking banner - * -sized CDTitle). This is also subverted in certain designs (beta - * Mungyodance 3 simfiles, for instance, used the CDTitle to hold - * various information about the song in question). As it stands, - * I'm keeping this code until I figure out wtf to do -aj - */ - if( !HasCDTitle() && width<=100 && height<=48 ) - { - m_sCDTitleFile = arrayImages[i]; - continue; - } - - // Jacket files typically have the same width and height. - if( !HasJacket() && width == height ) - { - m_sJacketFile = arrayImages[i]; - continue; - } - - // Disc images are typically rectangular; make sure we have a banner already. - if( !HasDisc() && (width > height) && HasBanner() ) - { - if( arrayImages[i] != m_sBannerFile ) - m_sDiscFile = arrayImages[i]; - continue; - } - - // CD images are the same as Jackets, typically the same width and height - if( !HasCDImage() && width == height ) - { - m_sCDFile = arrayImages[i]; - continue; - } - } - - } - - // These will be written to cache, for Song::LoadFromSongDir to use later. - m_bHasMusic = HasMusic(); - m_bHasBanner = HasBanner(); - m_bHasBackground = HasBackground(); - - if( HasBanner() ) - BANNERCACHE->CacheBanner( GetBannerPath() ); - /* - if( HasBackground() ) - BANNERCACHE->CacheBackground( GetBackgroundPath() ); - */ - - // If no BGChanges are specified and there are movies in the song directory, then assume - // they are DWI style where the movie begins at beat 0. - if( (!HasBGChanges() && !fromCache) ) - { - vector arrayPossibleMovies; - GetDirListing( m_sSongDir + RString("*.ogv"), arrayPossibleMovies ); - GetDirListing( m_sSongDir + RString("*.avi"), arrayPossibleMovies ); - GetDirListing( m_sSongDir + RString("*.mpg"), arrayPossibleMovies ); - GetDirListing( m_sSongDir + RString("*.mpeg"), arrayPossibleMovies ); - /* Use this->GetBeatFromElapsedTime(0) instead of 0 to start when the - * music starts. */ - if( arrayPossibleMovies.size() == 1 ) - this->AddBackgroundChange(BACKGROUND_LAYER_1, - BackgroundChange(0, - arrayPossibleMovies[0], - "", - 1.f, - SBE_StretchNoLoop)); - } - - - /* Don't allow multiple Steps of the same StepsType and Difficulty (except - * for edits). We should be able to use difficulty names as unique - * identifiers for steps. */ - if( !fromCache ) - { - SongUtil::AdjustDuplicateSteps( this ); - } -} - -void Song::TranslateTitles() -{ - static TitleSubst tsub("Songs"); - - TitleFields title; - title.LoadFromStrings(m_sMainTitle, m_sSubTitle, m_sArtist, - m_sMainTitleTranslit, m_sSubTitleTranslit, m_sArtistTranslit ); - tsub.Subst( title ); - title.SaveToStrings(m_sMainTitle, m_sSubTitle, m_sArtist, - m_sMainTitleTranslit, m_sSubTitleTranslit, m_sArtistTranslit ); -} - -void Song::ReCalculateRadarValuesAndLastSecond(bool fromCache, bool duringCache) -{ - if( fromCache && this->GetFirstSecond() >= 0 && this->GetLastSecond() > 0 ) - { - // this is loaded from cache, then we just have to calculate the radar values. - for( unsigned i=0; iCalculateRadarValues( m_fMusicLengthSeconds ); - return; - } - - float localFirst = FLT_MAX; // inf - // Make sure we're at least as long as the specified amount below. - float localLast = this->specifiedLastSecond; - - for( unsigned i=0; iCalculateRadarValues( m_fMusicLengthSeconds ); - - // Must initialize before the gotos. - NoteData tempNoteData; - pSteps->GetNoteData( tempNoteData ); - - // calculate lastSecond - - // If it's autogen, then first/last beat will come from the parent. - if( pSteps->IsAutogen() ) - goto wipe_notedata; - - /* Don't calculate with edits unless the song only contains an edit - * chart, like those in Mungyodance 3. Otherwise, edits installed on - * the machine could extend the length of the song. */ - if( pSteps->IsAnEdit() && m_vpSteps.size() > 1 ) - goto wipe_notedata; - - // Don't set first/last beat based on lights. They often start very - // early and end very late. - if( pSteps->m_StepsType == StepsType_lights_cabinet ) - continue; // no need to wipe this. - - /* Many songs have stray, empty song patterns. Ignore them, so they - * don't force the first beat of the whole song to 0. */ - if( tempNoteData.GetLastRow() != 0 ) - { - localFirst = min(localFirst, - pSteps->m_Timing.GetElapsedTimeFromBeat(tempNoteData.GetFirstBeat())); - localLast = max(localLast, - pSteps->m_Timing.GetElapsedTimeFromBeat(tempNoteData.GetLastBeat())); - } - wipe_notedata: - if (duringCache) - { - NoteData dummy; - dummy.SetNumTracks(tempNoteData.GetNumTracks()); - pSteps->SetNoteData(dummy); - } - } - - // Yes, for some reason we can have freaky stuff take place here. - this->firstSecond = (localFirst < localLast) ? localFirst : 0; - this->lastSecond = localLast; -} - -// Return whether the song is playable in the given style. -bool Song::SongCompleteForStyle( const Style *st ) const -{ - return HasStepsType( st->m_StepsType ); -} - -bool Song::HasStepsType( StepsType st ) const -{ - return SongUtil::GetOneSteps( this, st ) != NULL; -} - -bool Song::HasStepsTypeAndDifficulty( StepsType st, Difficulty dc ) const -{ - return SongUtil::GetOneSteps( this, st, dc ) != NULL; -} - -void Song::Save() -{ - LOG->Trace( "Song::SaveToSongFile()" ); - - ReCalculateRadarValuesAndLastSecond(); - TranslateTitles(); - - // Save the new files. These calls make backups on their own. - if( !SaveToSSCFile(GetSongFilePath(), false) ) - return; - SaveToSMFile(); - //SaveToDWIFile(); - SaveToCacheFile(); - - /* We've safely written our files and created backups. Rename non-SM and - * non-DWI files to avoid confusion. */ - vector arrayOldFileNames; - GetDirListing( m_sSongDir + "*.bms", arrayOldFileNames ); - GetDirListing( m_sSongDir + "*.pms", arrayOldFileNames ); - GetDirListing( m_sSongDir + "*.ksf", arrayOldFileNames ); - - for( unsigned i=0; iUserLog( "Song file", sOldPath, "couldn't be backed up." ); - // Don't remove. - } - else - FILEMAN->Remove( sOldPath ); - } -} - -bool Song::SaveToSMFile() -{ - const RString sPath = SetExtension( GetSongFilePath(), "sm" ); - LOG->Trace( "Song::SaveToSMFile(%s)", sPath.c_str() ); - - // If the file exists, make a backup. - if( IsAFile(sPath) ) - FileCopy( sPath, sPath + ".old" ); - - vector vpStepsToSave; - FOREACH_CONST( Steps*, m_vpSteps, s ) - { - Steps *pSteps = *s; - if( pSteps->IsAutogen() ) - continue; // don't write autogen notes - - // Only save steps that weren't loaded from a profile. - if( pSteps->WasLoadedFromProfile() ) - continue; - - vpStepsToSave.push_back( pSteps ); - } - - return NotesWriterSM::Write( sPath, *this, vpStepsToSave ); - -} - -bool Song::SaveToSSCFile( RString sPath, bool bSavingCache ) -{ - RString path = sPath; - if (!bSavingCache) - path = SetExtension(sPath, "ssc"); - - LOG->Trace( "Song::SaveToSSCFile('%s')", path.c_str() ); - - // If the file exists, make a backup. - if( !bSavingCache && IsAFile(path) ) - FileCopy( path, path + ".old" ); - - vector vpStepsToSave; - FOREACH_CONST( Steps*, m_vpSteps, s ) - { - Steps *pSteps = *s; - if( pSteps->IsAutogen() ) - continue; // don't write autogen notes - - // Only save steps that weren't loaded from a profile. - if( pSteps->WasLoadedFromProfile() ) - continue; - - if (!bSavingCache) - pSteps->SetFilename(path); - vpStepsToSave.push_back( pSteps ); - } - - if (bSavingCache) - { - return NotesWriterSSC::Write(path, *this, vpStepsToSave, bSavingCache); - } - - if( !NotesWriterSSC::Write(path, *this, vpStepsToSave, bSavingCache) ) - return false; - - if( g_BackUpAllSongSaves.Get() ) - { - RString sExt = GetExtension( path ); - RString sBackupFile = SetExtension( path, "" ); - - time_t cur_time; - time( &cur_time ); - struct tm now; - localtime_r( &cur_time, &now ); - - sBackupFile += ssprintf( "-%04i-%02i-%02i--%02i-%02i-%02i", - 1900+now.tm_year, now.tm_mon+1, now.tm_mday, now.tm_hour, now.tm_min, now.tm_sec ); - sBackupFile = SetExtension( sBackupFile, sExt ); - sBackupFile += ssprintf( ".old" ); - - if( FileCopy(path, sBackupFile) ) - LOG->Trace( "Backed up %s to %s", path.c_str(), sBackupFile.c_str() ); - else - LOG->Trace( "Failed to back up %s to %s", path.c_str(), sBackupFile.c_str() ); - } - - // Mark these steps saved to disk. - FOREACH( Steps*, vpStepsToSave, s ) - (*s)->SetSavedToDisk( true ); - - return true; -} - -bool Song::SaveToCacheFile() -{ - SONGINDEX->AddCacheIndex(m_sSongDir, GetHashForDirectory(m_sSongDir)); - const RString sPath = GetCacheFilePath(); - return SaveToSSCFile(sPath, true); -} - -bool Song::SaveToDWIFile() -{ - const RString sPath = SetExtension( GetSongFilePath(), "dwi" ); - LOG->Trace( "Song::SaveToDWIFile(%s)", sPath.c_str() ); - - // If the file exists, make a backup. - if( IsAFile(sPath) ) - FileCopy( sPath, sPath + ".old" ); - - return NotesWriterDWI::Write( sPath, *this ); -} - - -void Song::AddAutoGenNotes() -{ - bool HasNotes[NUM_StepsType]; - memset( HasNotes, 0, sizeof(HasNotes) ); - for( unsigned i=0; i < m_vpSteps.size(); i++ ) // foreach Steps - { - if( m_vpSteps[i]->IsAutogen() ) - continue; - - StepsType st = m_vpSteps[i]->m_StepsType; - HasNotes[st] = true; - } - - FOREACH_ENUM( StepsType, stMissing ) - { - if( HasNotes[stMissing] ) - continue; - - // If m_bAutogenSteps is disabled, only autogen lights. - if( !PREFSMAN->m_bAutogenSteps && stMissing != StepsType_lights_cabinet ) - continue; - if( !GAMEMAN->GetStepsTypeInfo(stMissing).bAllowAutogen ) - continue; - - // missing Steps of this type - int iNumTracksOfMissing = GAMEMAN->GetStepsTypeInfo(stMissing).iNumTracks; - - // look for closest match - StepsType stBestMatch = StepsType_Invalid; - int iBestTrackDifference = INT_MAX; - - FOREACH_ENUM( StepsType, st ) - { - if( !HasNotes[st] ) - continue; - - // has (non-autogen) Steps of this type - const int iNumTracks = GAMEMAN->GetStepsTypeInfo(st).iNumTracks; - const int iTrackDifference = abs(iNumTracks-iNumTracksOfMissing); - if( iTrackDifference < iBestTrackDifference ) - { - stBestMatch = st; - iBestTrackDifference = iTrackDifference; - } - } - - if( stBestMatch != StepsType_Invalid ) - AutoGen( stMissing, stBestMatch ); - } -} - -void Song::AutoGen( StepsType ntTo, StepsType ntFrom ) -{ - // int iNumTracksOfTo = GAMEMAN->StepsTypeToNumTracks(ntTo); - - for( unsigned int j=0; jm_StepsType == ntFrom ) - { - Steps* pNewNotes = new Steps; - pNewNotes->AutogenFrom( pOriginalNotes, ntTo ); - this->AddSteps( pNewNotes ); - } - } -} - -void Song::RemoveAutoGenNotes() -{ - FOREACH_ENUM( StepsType, st ) - { - for( int j=m_vpStepsByType[st].size()-1; j>=0; j-- ) - { - if( m_vpStepsByType[st][j]->IsAutogen() ) - { - // delete m_vpSteps[j]; // delete below - m_vpStepsByType[st].erase( m_vpStepsByType[st].begin()+j ); - } - } - } - - for( int j=m_vpSteps.size()-1; j>=0; j-- ) - { - if( m_vpSteps[j]->IsAutogen() ) - { - delete m_vpSteps[j]; - m_vpSteps.erase( m_vpSteps.begin()+j ); - } - } -} - -bool Song::IsEasy( StepsType st ) const -{ - /* Very fast songs and songs with wide tempo changes are hard for new - * players, even if they have beginner steps. */ - DisplayBpms bpms; - this->GetDisplayBpms(bpms); - if( bpms.GetMax() >= 250 || bpms.GetMax() - bpms.GetMin() >= 75 ) - return false; - - /* The easy marker indicates which songs a beginner, having selected - * "beginner", can play and actually get a very easy song: if there are - * actual beginner steps, or if the light steps are 1- or 2-foot. */ - const Steps* pBeginnerNotes = SongUtil::GetStepsByDifficulty( this, st, Difficulty_Beginner ); - if( pBeginnerNotes ) - return true; - - const Steps* pEasyNotes = SongUtil::GetStepsByDifficulty( this, st, Difficulty_Easy ); - if( pEasyNotes && pEasyNotes->GetMeter() == 1 ) - return true; - - return false; -} - -bool Song::IsTutorial() const -{ - // A Song is considered a Tutorial if it has only Beginner steps. - FOREACH_CONST( Steps*, m_vpSteps, s ) - { - if( (*s)->m_StepsType == StepsType_lights_cabinet ) - continue; // ignore - if( (*s)->GetDifficulty() != Difficulty_Beginner ) - return false; - } - - return true; -} - -bool Song::HasEdits( StepsType st ) const -{ - for( unsigned i=0; im_StepsType == st && - pSteps->GetDifficulty() == Difficulty_Edit ) - { - return true; - } - } - - return false; -} - -bool Song::NormallyDisplayed() const -{ - return UNLOCKMAN == NULL || !UNLOCKMAN->SongIsLocked(this); -} - -bool Song::ShowInDemonstrationAndRanking() const { return !IsTutorial() && NormallyDisplayed(); } - - -// Hack: see Song::TidyUpData comments. -bool Song::HasMusic() const -{ - // If we have keys, we always have music. - if( m_vsKeysoundFile.size() != 0 ) - return true; - - return m_sMusicFile != "" && IsAFile(GetMusicPath()); -} -bool Song::HasBanner() const -{ - return m_sBannerFile != "" && IsAFile(GetBannerPath()); -} -bool Song::HasInstrumentTrack( InstrumentTrack it ) const -{ - return m_sInstrumentTrackFile[it] != "" && IsAFile(GetInstrumentTrackPath(it)); -} -bool Song::HasLyrics() const -{ - return m_sLyricsFile != "" && IsAFile(GetLyricsPath()); -} -bool Song::HasBackground() const -{ - return m_sBackgroundFile != "" && IsAFile(GetBackgroundPath()); -} -bool Song::HasCDTitle() const -{ - return m_sCDTitleFile != "" && IsAFile(GetCDTitlePath()); -} -bool Song::HasBGChanges() const -{ - FOREACH_BackgroundLayer( i ) - { - if( !GetBackgroundChanges(i).empty() ) - return true; - } - return false; -} -bool Song::HasAttacks() const {return !m_Attacks.empty(); } -bool Song::HasJacket() const -{ - return m_sJacketFile != "" && IsAFile(GetJacketPath()); -} -bool Song::HasDisc() const -{ - return m_sDiscFile != "" && IsAFile(GetDiscPath()); -} -bool Song::HasCDImage() const -{ - return m_sCDFile != "" && IsAFile(GetCDImagePath()); -} -bool Song::HasPreviewVid() const -{ - return m_sPreviewVidFile != "" && IsAFile(GetPreviewVidPath()); -} - -const vector &Song::GetBackgroundChanges( BackgroundLayer bl ) const -{ - return *(m_BackgroundChanges[bl]); -} -vector &Song::GetBackgroundChanges( BackgroundLayer bl ) -{ - return *(m_BackgroundChanges[bl].Get()); -} - -const vector &Song::GetForegroundChanges() const -{ - return *m_ForegroundChanges; -} -vector &Song::GetForegroundChanges() -{ - return *m_ForegroundChanges.Get(); -} - -vector Song::GetChangesToVectorString(const vector & changes) const -{ - vector ret; - FOREACH_CONST( BackgroundChange, changes, bgc ) - { - ret.push_back((*bgc).ToString()); - } - return ret; -} - -vector Song::GetBGChanges1ToVectorString() const -{ - return this->GetChangesToVectorString(this->GetBackgroundChanges(BACKGROUND_LAYER_1)); -} - -vector Song::GetBGChanges2ToVectorString() const -{ - return this->GetChangesToVectorString(this->GetBackgroundChanges(BACKGROUND_LAYER_2)); -} - -vector Song::GetFGChanges1ToVectorString() const -{ - return this->GetChangesToVectorString(this->GetForegroundChanges()); -} - -vector Song::GetInstrumentTracksToVectorString() const -{ - vector ret; - FOREACH_ENUM(InstrumentTrack, it) - { - if (this->HasInstrumentTrack(it)) - { - ret.push_back(InstrumentTrackToString(it) - + "=" - + this->m_sInstrumentTrackFile[it]); - } - } - return ret; -} - -RString GetSongAssetPath( RString sPath, const RString &sSongPath ) -{ - if( sPath == "" ) - return RString(); - - /* If there's no path in the file, the file is in the same directory as the - * song. (This is the preferred configuration.) */ - if( sPath.find('/') == string::npos ) - return sSongPath+sPath; - - // The song contains a path; treat it as relative to the top SM directory. - if( sPath.Left(3) == "../" ) - { - // The path begins with "../". Resolve it wrt. the song directory. - sPath = sSongPath + sPath; - } - - CollapsePath( sPath ); - - /* If the path still begins with "../", then there were an unreasonable number - * of them at the beginning of the path. Ignore the path entirely. */ - if( sPath.Left(3) == "../" ) - return RString(); - - return sPath; -} - -/* Note that supplying a path relative to the top-level directory is only for - * compatibility with DWI. We prefer paths relative to the song directory. */ -RString Song::GetMusicPath() const -{ - return GetSongAssetPath( m_sMusicFile, m_sSongDir ); -} - -RString Song::GetInstrumentTrackPath( InstrumentTrack it ) const -{ - return GetSongAssetPath( m_sInstrumentTrackFile[it], m_sSongDir ); -} - -RString Song::GetBannerPath() const -{ - return GetSongAssetPath( m_sBannerFile, m_sSongDir ); -} - -RString Song::GetLyricsPath() const -{ - return GetSongAssetPath( m_sLyricsFile, m_sSongDir ); -} - -RString Song::GetCDTitlePath() const -{ - return GetSongAssetPath( m_sCDTitleFile, m_sSongDir ); -} - -RString Song::GetBackgroundPath() const -{ - return GetSongAssetPath( m_sBackgroundFile, m_sSongDir ); -} - -RString Song::GetJacketPath() const -{ - return GetSongAssetPath( m_sJacketFile, m_sSongDir ); -} - -RString Song::GetDiscPath() const -{ - return GetSongAssetPath( m_sDiscFile, m_sSongDir ); -} - -RString Song::GetCDImagePath() const -{ - return GetSongAssetPath( m_sCDFile, m_sSongDir ); -} - -RString Song::GetPreviewVidPath() const -{ - return GetSongAssetPath( m_sPreviewVidFile, m_sSongDir ); -} - -RString Song::GetDisplayMainTitle() const -{ - if(!PREFSMAN->m_bShowNativeLanguage) return GetTranslitMainTitle(); - return m_sMainTitle; -} - -RString Song::GetDisplaySubTitle() const -{ - if(!PREFSMAN->m_bShowNativeLanguage) return GetTranslitSubTitle(); - return m_sSubTitle; -} - -RString Song::GetDisplayArtist() const -{ - if(!PREFSMAN->m_bShowNativeLanguage) return GetTranslitArtist(); - return m_sArtist; -} - -RString Song::GetDisplayFullTitle() const -{ - RString Title = GetDisplayMainTitle(); - RString SubTitle = GetDisplaySubTitle(); - - if(!SubTitle.empty()) Title += " " + SubTitle; - return Title; -} - -RString Song::GetTranslitFullTitle() const -{ - RString Title = GetTranslitMainTitle(); - RString SubTitle = GetTranslitSubTitle(); - - if(!SubTitle.empty()) Title += " " + SubTitle; - return Title; -} - -void Song::AddSteps( Steps* pSteps ) -{ - m_vpSteps.push_back( pSteps ); - ASSERT_M( pSteps->m_StepsType < NUM_StepsType, ssprintf("%i", pSteps->m_StepsType) ); - m_vpStepsByType[pSteps->m_StepsType].push_back( pSteps ); -} - -void Song::DeleteSteps( const Steps* pSteps, bool bReAutoGen ) -{ - ASSERT( !pSteps->IsAutogen() ); - - // Avoid any stale Note::parent pointers by removing all AutoGen'd Steps, - // then adding them again. - - if( bReAutoGen ) - RemoveAutoGenNotes(); - - vector &vpSteps = m_vpStepsByType[pSteps->m_StepsType]; - for( int j=vpSteps.size()-1; j>=0; j-- ) - { - if( vpSteps[j] == pSteps ) - { - //delete vpSteps[j]; // delete below - vpSteps.erase( vpSteps.begin()+j ); - break; - } - } - - for( int j=m_vpSteps.size()-1; j>=0; j-- ) - { - if( m_vpSteps[j] == pSteps ) - { - delete m_vpSteps[j]; - m_vpSteps.erase( m_vpSteps.begin()+j ); - break; - } - } - - if( bReAutoGen ) - AddAutoGenNotes(); -} - -bool Song::Matches(RString sGroup, RString sSong) const -{ - if( sGroup.size() && sGroup.CompareNoCase(this->m_sGroupName) != 0) - return false; - - RString sDir = this->GetSongDir(); - sDir.Replace("\\","/"); - vector bits; - split( sDir, "/", bits ); - ASSERT(bits.size() >= 2); // should always have at least two parts - const RString &sLastBit = bits[bits.size()-1]; - - // match on song dir or title (ala DWI) - if( !sSong.CompareNoCase(sLastBit) ) - return true; - if( !sSong.CompareNoCase(this->GetTranslitFullTitle()) ) - return true; - - return false; -} - -/* If apInUse is set, it contains a list of steps which are in use elsewhere, - * and should not be deleted. */ -void Song::FreeAllLoadedFromProfile( ProfileSlot slot, const set *setInUse ) -{ - /* DeleteSteps will remove and recreate autogen notes, which may reorder - * m_vpSteps, so be careful not to skip over entries. */ - vector apToRemove; - for( int s=m_vpSteps.size()-1; s>=0; s-- ) - { - Steps* pSteps = m_vpSteps[s]; - if( !pSteps->WasLoadedFromProfile() ) - continue; - if( slot != ProfileSlot_Invalid && pSteps->GetLoadedFromProfileSlot() != slot ) - continue; - if( setInUse != NULL && setInUse->find(pSteps) != setInUse->end() ) - continue; - apToRemove.push_back( pSteps ); - } - - for( unsigned i = 0; i < apToRemove.size(); ++i ) - this->DeleteSteps( apToRemove[i] ); -} - -void Song::GetStepsLoadedFromProfile( ProfileSlot slot, vector &vpStepsOut ) const -{ - for( unsigned s=0; sGetLoadedFromProfileSlot() == slot ) - vpStepsOut.push_back( pSteps ); - } -} - -int Song::GetNumStepsLoadedFromProfile( ProfileSlot slot ) const -{ - int iCount = 0; - for( unsigned s=0; sGetLoadedFromProfileSlot() == slot ) - iCount++; - } - return iCount; -} - -bool Song::IsEditAlreadyLoaded( Steps* pSteps ) const -{ - ASSERT_M( pSteps->GetDifficulty() == Difficulty_Edit, - ssprintf("The %s chart for %s is no edit, thus it can't be checked for loading.", - DifficultyToString(pSteps->GetDifficulty()).c_str(), - this->m_sMainTitle.c_str())); - - for( unsigned i=0; iGetDifficulty() == Difficulty_Edit && - pOther->m_StepsType == pSteps->m_StepsType && - pOther->GetHash() == pSteps->GetHash() ) - { - return true; - } - } - - return false; -} - -bool Song::IsStepsUsingDifferentTiming(Steps *pSteps) const -{ - return pSteps->m_Timing != this->m_SongTiming; -} - -bool Song::HasSignificantBpmChangesOrStops() const -{ - if( m_SongTiming.HasStops() || m_SongTiming.HasDelays() ) - return true; - - // Don't consider BPM changes that only are only for maintaining sync as - // a real BpmChange. - if( m_DisplayBPMType == DISPLAY_BPM_SPECIFIED ) - { - if( m_fSpecifiedBPMMin != m_fSpecifiedBPMMax ) - return true; - } - else if( m_SongTiming.HasBpmChanges() ) - { - return true; - } - - return false; -} - -float Song::GetStepsSeconds() const -{ - return this->GetLastSecond() - this->GetFirstSecond(); -} - -bool Song::IsLong() const -{ - return !IsMarathon() && m_fMusicLengthSeconds >= g_fLongVerSongSeconds; -} - -bool Song::IsMarathon() const -{ - return m_fMusicLengthSeconds >= g_fMarathonVerSongSeconds; -} - -// lua start -#include "LuaBinding.h" - -/** @brief Allow Lua to have access to the Song. */ -class LunaSong: public Luna -{ -public: - static int GetDisplayFullTitle( T* p, lua_State *L ) - { - lua_pushstring(L, p->GetDisplayFullTitle() ); return 1; - } - static int GetTranslitFullTitle( T* p, lua_State *L ) - { - lua_pushstring(L, p->GetTranslitFullTitle() ); return 1; - } - static int GetDisplayMainTitle( T* p, lua_State *L ) - { - lua_pushstring(L, p->GetDisplayMainTitle() ); return 1; - } - static int GetTranslitMainTitle( T* p, lua_State *L ) - { - lua_pushstring(L, p->GetTranslitMainTitle() ); return 1; - } - static int GetDisplaySubTitle( T* p, lua_State *L ) - { - lua_pushstring(L, p->GetDisplaySubTitle() ); return 1; - } - static int GetTranslitSubTitle( T* p, lua_State *L ) - { - lua_pushstring(L, p->GetTranslitSubTitle() ); return 1; - } - static int GetDisplayArtist( T* p, lua_State *L ) - { - lua_pushstring(L, p->GetDisplayArtist() ); return 1; - } - static int GetTranslitArtist( T* p, lua_State *L ) - { - lua_pushstring(L, p->GetTranslitArtist() ); return 1; - } - static int GetGenre( T* p, lua_State *L ) - { - lua_pushstring(L, p->m_sGenre ); return 1; - } - static int GetOrigin( T* p, lua_State *L ) - { - lua_pushstring(L, p->m_sOrigin ); return 1; - } - static int GetAllSteps( T* p, lua_State *L ) - { - const vector &v = p->GetAllSteps(); - LuaHelpers::CreateTableFromArray( v, L ); - return 1; - } - static int GetStepsByStepsType( T* p, lua_State *L ) - { - StepsType st = Enum::Check(L, 1); - const vector &v = p->GetStepsByStepsType( st ); - LuaHelpers::CreateTableFromArray( v, L ); - return 1; - } - static int GetSongDir( T* p, lua_State *L ) - { - lua_pushstring(L, p->GetSongDir() ); - return 1; - } - static int GetMusicPath( T* p, lua_State *L ) - { - RString s = p->GetMusicPath(); - if( !s.empty() ) - lua_pushstring(L, s); - else - lua_pushnil(L); - return 1; - } - static int GetBannerPath( T* p, lua_State *L ) - { - RString s = p->GetBannerPath(); - if( !s.empty() ) - lua_pushstring(L, s); - else - lua_pushnil(L); - return 1; - } - static int GetBackgroundPath( T* p, lua_State *L ) - { - RString s = p->GetBackgroundPath(); - if( !s.empty() ) - lua_pushstring(L, s); - else - lua_pushnil(L); - return 1; - } - static int GetPreviewVidPath( T* p, lua_State *L ) - { - RString s = p->GetPreviewVidPath(); - if( !s.empty() ) - lua_pushstring(L, s); - else - lua_pushnil(L); - return 1; - } - static int GetJacketPath( T* p, lua_State *L ) - { - RString s = p->GetJacketPath(); - if( !s.empty() ) - lua_pushstring(L, s); - else - lua_pushnil(L); - return 1; - } - static int GetCDImagePath( T* p, lua_State *L ) - { - RString s = p->GetCDImagePath(); - if( !s.empty() ) - lua_pushstring(L, s); - else - lua_pushnil(L); - return 1; - } - static int GetDiscPath( T* p, lua_State *L ) - { - RString s = p->GetDiscPath(); - if( !s.empty() ) - lua_pushstring(L, s); - else - lua_pushnil(L); - return 1; - } - static int GetCDTitlePath( T* p, lua_State *L ) - { - RString s = p->GetCDTitlePath(); - if( !s.empty() ) - lua_pushstring(L, s); - else - lua_pushnil(L); - return 1; - } - static int GetLyricsPath( T* p, lua_State *L ) - { - RString s = p->GetLyricsPath(); - if( !s.empty() ) - lua_pushstring(L, s); - else - lua_pushnil(L); - return 1; - } - static int GetSongFilePath( T* p, lua_State *L ) - { - lua_pushstring(L, p->GetSongFilePath() ); - return 1; - } - static int IsTutorial( T* p, lua_State *L ) - { - lua_pushboolean(L, p->IsTutorial()); - return 1; - } - static int IsEnabled( T* p, lua_State *L ) - { - lua_pushboolean(L, p->GetEnabled()); - return 1; - } - static int GetGroupName( T* p, lua_State *L ) - { - lua_pushstring(L, p->m_sGroupName); - return 1; - } - static int MusicLengthSeconds( T* p, lua_State *L ) - { - lua_pushnumber(L, p->m_fMusicLengthSeconds); - return 1; - } - static int GetSampleStart( T* p, lua_State *L ) - { - lua_pushnumber(L, p->m_fMusicSampleStartSeconds); - return 1; - } - static int GetSampleLength( T* p, lua_State *L ) - { - lua_pushnumber(L, p->m_fMusicSampleLengthSeconds); - return 1; - } - static int IsLong( T* p, lua_State *L ) - { - lua_pushboolean(L, p->IsLong()); - return 1; - } - static int IsMarathon( T* p, lua_State *L ) - { - lua_pushboolean(L, p->IsMarathon()); - return 1; - } - static int HasStepsType( T* p, lua_State *L ) - { - StepsType st = Enum::Check(L, 1); - lua_pushboolean( L, p->HasStepsType(st) ); - return 1; - } - static int HasStepsTypeAndDifficulty( T* p, lua_State *L ) - { - StepsType st = Enum::Check(L, 1); - Difficulty dc = Enum::Check( L, 2 ); - lua_pushboolean( L, p->HasStepsTypeAndDifficulty(st, dc) ); - return 1; - } - static int IsStepsUsingDifferentTiming(T* p, lua_State *L) - { - lua_pushboolean(L, p->IsStepsUsingDifferentTiming(Luna::check( L, 1, true ))); - return 1; - } - /* TODO: HasStepsTypeAndDifficulty and GetOneSteps should be in - * a SongUtil Lua table and a method of Steps. */ - static int GetOneSteps( T* p, lua_State *L ) - { - StepsType st = Enum::Check(L, 1); - Difficulty dc = Enum::Check( L, 2 ); - Steps *pSteps = SongUtil::GetOneSteps( p, st, dc ); - if( pSteps ) - pSteps->PushSelf(L); - else - lua_pushnil(L); - return 1; - } - static int GetTimingData( T* p, lua_State *L ) { p->m_SongTiming.PushSelf(L); return 1; } - // has functions - static int HasMusic( T* p, lua_State *L ) { lua_pushboolean(L, p->HasMusic()); return 1; } - static int HasBanner( T* p, lua_State *L ) { lua_pushboolean(L, p->HasBanner()); return 1; } - static int HasBackground( T* p, lua_State *L ) { lua_pushboolean(L, p->HasBackground()); return 1; } - static int HasPreviewVid( T* p, lua_State *L ) { lua_pushboolean(L, p->HasPreviewVid()); return 1; } - static int HasJacket( T* p, lua_State *L ) { lua_pushboolean(L, p->HasJacket()); return 1; } - static int HasDisc( T* p, lua_State *L ) { lua_pushboolean(L, p->HasDisc()); return 1; } - static int HasCDImage( T* p, lua_State *L ) { lua_pushboolean(L, p->HasCDImage()); return 1; } - static int HasCDTitle( T* p, lua_State *L ) { lua_pushboolean(L, p->HasCDTitle()); return 1; } - static int HasBGChanges( T* p, lua_State *L ) { lua_pushboolean(L, p->HasBGChanges()); return 1; } - static int HasLyrics( T* p, lua_State *L ) { lua_pushboolean(L, p->HasLyrics()); return 1; } - // functions that AJ loves - static int HasSignificantBPMChangesOrStops( T* p, lua_State *L ) - { - lua_pushboolean(L, p->HasSignificantBpmChangesOrStops()); - return 1; - } - static int HasEdits( T* p, lua_State *L ) - { - StepsType st = Enum::Check(L, 1); - lua_pushboolean(L, p->HasEdits( st )); - return 1; - } - static int IsEasy( T* p, lua_State *L ) - { - StepsType st = Enum::Check(L, 1); - lua_pushboolean(L, p->IsEasy( st )); - return 1; - } - static int GetStepsSeconds( T* p, lua_State *L ) - { - lua_pushnumber(L, p->GetStepsSeconds()); - return 1; - } - static int NormallyDisplayed( T* p, lua_State *L ) - { - lua_pushboolean(L, p->NormallyDisplayed()); - return 1; - } - static int ShowInDemonstrationAndRanking( T* p, lua_State *L ) - { - lua_pushboolean(L, p->ShowInDemonstrationAndRanking()); - return 1; - } - static int GetFirstSecond(T* p, lua_State *L) - { - lua_pushnumber(L, p->GetFirstSecond()); - return 1; - } - static int GetLastSecond(T* p, lua_State *L) - { - lua_pushnumber(L, p->GetLastSecond()); - return 1; - } - static int GetFirstBeat( T* p, lua_State *L ) - { - lua_pushnumber(L, p->GetFirstBeat()); - return 1; - } - static int GetLastBeat( T* p, lua_State *L ) - { - lua_pushnumber(L, p->GetLastBeat()); - return 1; - } - static int HasAttacks( T* p, lua_State *L ) - { - lua_pushboolean(L, p->HasAttacks()); - return 1; - } - static int GetDisplayBpms( T* p, lua_State *L ) - { - DisplayBpms temp; - p->GetDisplayBpms(temp); - float fMin = temp.GetMin(); - float fMax = temp.GetMax(); - vector fBPMs; - fBPMs.push_back( fMin ); - fBPMs.push_back( fMax ); - LuaHelpers::CreateTableFromArray(fBPMs, L); - return 1; - } - static int IsDisplayBpmSecret( T* p, lua_State *L ) - { - DisplayBpms temp; - p->GetDisplayBpms(temp); - lua_pushboolean( L, temp.IsSecret() ); - return 1; - } - static int IsDisplayBpmConstant( T* p, lua_State *L ) - { - DisplayBpms temp; - p->GetDisplayBpms(temp); - lua_pushboolean( L, temp.BpmIsConstant() ); - return 1; - } - static int IsDisplayBpmRandom( T* p, lua_State *L ) - { - lua_pushboolean( L, p->m_DisplayBPMType == DISPLAY_BPM_RANDOM ); - return 1; - } - - LunaSong() - { - ADD_METHOD( GetDisplayFullTitle ); - ADD_METHOD( GetTranslitFullTitle ); - ADD_METHOD( GetDisplayMainTitle ); - ADD_METHOD( GetTranslitMainTitle ); - ADD_METHOD( GetDisplaySubTitle ); - ADD_METHOD( GetTranslitSubTitle ); - ADD_METHOD( GetDisplayArtist ); - ADD_METHOD( GetTranslitArtist ); - ADD_METHOD( GetGenre ); - ADD_METHOD( GetOrigin ); - ADD_METHOD( GetAllSteps ); - ADD_METHOD( GetStepsByStepsType ); - ADD_METHOD( GetSongDir ); - ADD_METHOD( GetMusicPath ); - ADD_METHOD( GetBannerPath ); - ADD_METHOD( GetBackgroundPath ); - ADD_METHOD( GetJacketPath ); - ADD_METHOD( GetCDImagePath ); - ADD_METHOD( GetDiscPath ); - ADD_METHOD( GetCDTitlePath ); - ADD_METHOD( GetLyricsPath ); - ADD_METHOD( GetSongFilePath ); - ADD_METHOD( IsTutorial ); - ADD_METHOD( IsEnabled ); - ADD_METHOD( GetGroupName ); - ADD_METHOD( MusicLengthSeconds ); - ADD_METHOD( GetSampleStart ); - ADD_METHOD( GetSampleLength ); - ADD_METHOD( IsLong ); - ADD_METHOD( IsMarathon ); - ADD_METHOD( HasStepsType ); - ADD_METHOD( HasStepsTypeAndDifficulty ); - ADD_METHOD( GetOneSteps ); - ADD_METHOD( GetTimingData ); - ADD_METHOD( HasMusic ); - ADD_METHOD( HasBanner ); - ADD_METHOD( HasBackground ); - ADD_METHOD( HasJacket ); - ADD_METHOD( HasCDImage ); - ADD_METHOD( HasDisc ); - ADD_METHOD( HasCDTitle ); - ADD_METHOD( HasBGChanges ); - ADD_METHOD( HasLyrics ); - ADD_METHOD( HasSignificantBPMChangesOrStops ); - ADD_METHOD( HasEdits ); - ADD_METHOD( IsEasy ); - ADD_METHOD( GetStepsSeconds ); - ADD_METHOD( NormallyDisplayed ); - ADD_METHOD( GetFirstBeat ); - ADD_METHOD( GetFirstSecond ); - ADD_METHOD( GetLastBeat ); - ADD_METHOD( GetLastSecond ); - ADD_METHOD( HasAttacks ); - ADD_METHOD( GetDisplayBpms ); - ADD_METHOD( IsDisplayBpmSecret ); - ADD_METHOD( IsDisplayBpmConstant ); - ADD_METHOD( IsDisplayBpmRandom ); - ADD_METHOD( IsStepsUsingDifferentTiming ); - ADD_METHOD( ShowInDemonstrationAndRanking ); - ADD_METHOD( HasPreviewVid ); - ADD_METHOD( GetPreviewVidPath ); - } -}; - -LUA_REGISTER_CLASS( Song ) -// lua end - - -/* - * (c) 2001-2004 Chris Danford, Glenn Maynard - * 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. - */ +#include "global.h" +#include "Song.h" +#include "Steps.h" +#include "RageUtil.h" +#include "RageLog.h" +#include "NoteData.h" +#include "RageSoundReader_FileReader.h" +#include "RageSurface_Load.h" +#include "SongCacheIndex.h" +#include "GameManager.h" +#include "PrefsManager.h" +#include "Style.h" +#include "FontCharAliases.h" +#include "TitleSubstitution.h" +#include "BannerCache.h" +//#include "BackgroundCache.h" +#include "Sprite.h" +#include "RageFileManager.h" +#include "RageSurface.h" +#include "NoteDataUtil.h" +#include "SongUtil.h" +#include "StepsUtil.h" +#include "Foreach.h" +#include "BackgroundUtil.h" +#include "SpecialFiles.h" +#include "NotesLoader.h" +#include "NotesLoaderSM.h" +#include "NotesLoaderSSC.h" +#include "NotesWriterDWI.h" +#include "NotesWriterSM.h" +#include "NotesWriterSSC.h" +#include "UnlockManager.h" +#include "LyricsLoader.h" + +#include +#include +#include + + +/** + * @brief The internal version of the cache for StepMania. + * + * Increment this value to invalidate the current cache. */ +const int FILE_CACHE_VERSION = 208; + +/** @brief How long does a song sample last by default? */ +const float DEFAULT_MUSIC_SAMPLE_LENGTH = 12.f; + +static Preference g_fLongVerSongSeconds( "LongVerSongSeconds", 60*2.5f ); +static Preference g_fMarathonVerSongSeconds( "MarathonVerSongSeconds", 60*5.f ); +static Preference g_BackUpAllSongSaves( "BackUpAllSongSaves", false ); + +static const char *InstrumentTrackNames[] = { + "Guitar", + "Rhythm", + "Bass", +}; +XToString( InstrumentTrack ); +StringToX( InstrumentTrack ); + +Song::Song() +{ + FOREACH_BackgroundLayer( i ) + m_BackgroundChanges[i] = AutoPtrCopyOnWrite(new VBackgroundChange); + m_ForegroundChanges = AutoPtrCopyOnWrite(new VBackgroundChange); + + m_LoadedFromProfile = ProfileSlot_Invalid; + m_fVersion = STEPFILE_VERSION_NUMBER; + m_fMusicSampleStartSeconds = -1; + m_fMusicSampleLengthSeconds = DEFAULT_MUSIC_SAMPLE_LENGTH; + m_fMusicLengthSeconds = 0; + firstSecond = -1; + lastSecond = -1; + specifiedLastSecond = -1; + m_SelectionDisplay = SHOW_ALWAYS; + m_bEnabled = true; + m_DisplayBPMType = DISPLAY_BPM_ACTUAL; + m_fSpecifiedBPMMin = 0; + m_fSpecifiedBPMMax = 0; + m_bIsSymLink = false; + m_bHasMusic = false; + m_bHasBanner = false; + m_bHasBackground = false; +} + +Song::~Song() +{ + FOREACH( Steps*, m_vpSteps, s ) + SAFE_DELETE( *s ); + m_vpSteps.clear(); + + // It's the responsibility of the owner of this Song to make sure + // that all pointers to this Song and its Steps are invalidated. +} + +void Song::DetachSteps() +{ + m_vpSteps.clear(); + FOREACH_ENUM( StepsType, st ) + m_vpStepsByType[st].clear(); +} + +float Song::GetFirstSecond() const +{ + return this->firstSecond; +} + +float Song::GetFirstBeat() const +{ + return this->m_SongTiming.GetBeatFromElapsedTime(this->firstSecond); +} + +float Song::GetLastSecond() const +{ + return this->lastSecond; +} + +float Song::GetLastBeat() const +{ + return this->m_SongTiming.GetBeatFromElapsedTime(this->lastSecond); +} + +float Song::GetSpecifiedLastSecond() const +{ + return this->specifiedLastSecond; +} + +float Song::GetSpecifiedLastBeat() const +{ + return this->m_SongTiming.GetBeatFromElapsedTime(this->specifiedLastSecond); +} + +void Song::SetFirstSecond(const float f) +{ + this->firstSecond = f; +} + +void Song::SetLastSecond(const float f) +{ + this->lastSecond = f; +} + +void Song::SetSpecifiedLastSecond(const float f) +{ + this->specifiedLastSecond = f; +} + +// Reset to an empty song. +void Song::Reset() +{ + FOREACH( Steps*, m_vpSteps, s ) + SAFE_DELETE( *s ); + m_vpSteps.clear(); + FOREACH_ENUM( StepsType, st ) + m_vpStepsByType[st].clear(); + + Song empty; + *this = empty; + + // It's the responsibility of the owner of this Song to make sure + // that all pointers to this Song and its Steps are invalidated. +} + + +void Song::AddBackgroundChange( BackgroundLayer iLayer, BackgroundChange seg ) +{ + // Delete old background change at this start beat, if any. + FOREACH( BackgroundChange, GetBackgroundChanges(iLayer), bgc ) + { + if( bgc->m_fStartBeat == seg.m_fStartBeat ) + { + GetBackgroundChanges(iLayer).erase( bgc ); + break; + } + } + + ASSERT( iLayer >= 0 && iLayer < NUM_BackgroundLayer ); + BackgroundUtil::AddBackgroundChange( GetBackgroundChanges(iLayer), seg ); +} + +void Song::AddForegroundChange( BackgroundChange seg ) +{ + BackgroundUtil::AddBackgroundChange( GetForegroundChanges(), seg ); +} + +void Song::AddLyricSegment( LyricSegment seg ) +{ + m_LyricSegments.push_back( seg ); +} + +Steps *Song::CreateSteps() +{ + Steps *pSteps = new Steps; + InitSteps( pSteps ); + return pSteps; +} + +void Song::InitSteps(Steps *pSteps) +{ + pSteps->m_Timing = this->m_SongTiming; + pSteps->m_sAttackString = this->m_sAttackString; + pSteps->m_Attacks = this->m_Attacks; + pSteps->SetDisplayBPM(this->m_DisplayBPMType); + pSteps->SetMinBPM(this->m_fSpecifiedBPMMin); + pSteps->SetMaxBPM(this->m_fSpecifiedBPMMax); +} + +void Song::GetDisplayBpms( DisplayBpms &AddTo ) const +{ + if( m_DisplayBPMType == DISPLAY_BPM_SPECIFIED ) + { + AddTo.Add( m_fSpecifiedBPMMin ); + AddTo.Add( m_fSpecifiedBPMMax ); + } + else + { + float fMinBPM, fMaxBPM; + m_SongTiming.GetActualBPM( fMinBPM, fMaxBPM ); + AddTo.Add( fMinBPM ); + AddTo.Add( fMaxBPM ); + } +} + +const BackgroundChange &Song::GetBackgroundAtBeat( BackgroundLayer iLayer, float fBeat ) const +{ + for( unsigned i=0; i fBeat ) + return GetBackgroundChanges(iLayer)[i]; + return GetBackgroundChanges(iLayer)[0]; +} + + +RString Song::GetCacheFilePath() const +{ + return SongCacheIndex::GetCacheFilePath( "Songs", m_sSongDir ); +} + +// Get a path to the SM containing data for this song. It might be a cache file. +const RString &Song::GetSongFilePath() const +{ + ASSERT_M( !m_sSongFileName.empty(), + ssprintf("The song %s has no filename associated with it!", + this->m_sMainTitle.c_str())); + return m_sSongFileName; +} + +/* Hack: This should be a parameter to TidyUpData, but I don't want to pull in + * into Song.h, which is heavily used. */ +static set BlacklistedImages; + +/* If PREFSMAN->m_bFastLoad is true, always load from cache if possible. + * Don't read the contents of sDir if we can avoid it. That means we can't call + * HasMusic(), HasBanner() or GetHashForDirectory(). + * If true, check the directory hash and reload the song from scratch if it's changed. + */ +bool Song::LoadFromSongDir( RString sDir ) +{ +// LOG->Trace( "Song::LoadFromSongDir(%s)", sDir.c_str() ); + ASSERT_M( sDir != "", "Songs can't be loaded from an empty directory!" ); + + // make sure there is a trailing slash at the end of sDir + if( sDir.Right(1) != "/" ) + sDir += "/"; + + // save song dir + m_sSongDir = sDir; + + // save group name + vector sDirectoryParts; + split( m_sSongDir, "/", sDirectoryParts, false ); + ASSERT( sDirectoryParts.size() >= 4 ); /* e.g. "/Songs/Slow/Taps/" */ + m_sGroupName = sDirectoryParts[sDirectoryParts.size()-3]; // second from last item + ASSERT( m_sGroupName != "" ); + + // First, look in the cache for this song (without loading NoteData) + unsigned uCacheHash = SONGINDEX->GetCacheHash(m_sSongDir); + bool bUseCache = true; + RString sCacheFilePath = GetCacheFilePath(); + + if( !DoesFileExist(sCacheFilePath) ) + bUseCache = false; + if( !PREFSMAN->m_bFastLoad && GetHashForDirectory(m_sSongDir) != uCacheHash ) + bUseCache = false; // this cache is out of date + + if( bUseCache ) + { + /* + LOG->Trace("Loading '%s' from cache file '%s'.", + m_sSongDir.c_str(), + GetCacheFilePath().c_str()); + */ + SSCLoader loaderSSC; + bool bLoadedFromSSC = loaderSSC.LoadFromSimfile( sCacheFilePath, *this, true ); + if( !bLoadedFromSSC ) + { + // load from .sm + SMLoader loaderSM; + loaderSM.LoadFromSimfile( sCacheFilePath, *this, true ); + loaderSM.TidyUpData( *this, true ); + } + } + else + { + // There was no entry in the cache for this song, or it was out of date. + // Let's load it from a file, then write a cache entry. + + if( !NotesLoader::LoadFromDir(sDir, *this, BlacklistedImages) ) + { + LOG->UserLog( "Song", sDir, "has no SSC, SM, SMA, DWI, BMS, or KSF files." ); + + vector vs; + GetDirListing( sDir + "*.mp3", vs, false, false ); + GetDirListing( sDir + "*.oga", vs, false, false ); + GetDirListing( sDir + "*.ogg", vs, false, false ); + bool bHasMusic = !vs.empty(); + + if( !bHasMusic ) + { + LOG->UserLog( "Song", sDir, "has no music file either. Ignoring this song directory." ); + return false; + } + // Make sure we have a future filename figured out. + vector folders; + split(sDir, "/", folders); + RString songName = folders[2] + ".ssc"; + this->m_sSongFileName = sDir + songName; + // Continue on with a blank Song so that people can make adjustments using the editor. + } + TidyUpData(false, true); + + // save a cache file so we don't have to parse it all over again next time + if( !SaveToCacheFile() ) + sCacheFilePath = RString(); + } + + FOREACH( Steps*, m_vpSteps, s ) + { + /* Compress all Steps. During initial caching, this will remove cached + * NoteData; during cached loads, this will just remove cached SMData. */ + (*s)->Compress(); + } + + // Load the cached banners, if it's not loaded already. + if( PREFSMAN->m_BannerCache == BNCACHE_LOW_RES_PRELOAD && m_bHasBanner ) + BANNERCACHE->LoadBanner( GetBannerPath() ); + // Load the cached background, if it's not loaded already. + /* + if( PREFSMAN->m_BackgroundCache == BGCACHE_LOW_RES_PRELOAD && m_bHasBackground ) + BACKGROUNDCACHE->LoadBackground( GetBackgroundPath() ); + */ + + // Add AutoGen pointers. (These aren't cached.) + AddAutoGenNotes(); + + if( !m_bHasMusic ) + { + LOG->UserLog( "Song", sDir, "has no music; ignored." ); + return false; // don't load this song + } + + return true; // do load this song +} + +bool Song::ReloadFromSongDir( RString sDir ) +{ + RemoveAutoGenNotes(); + vector vOldSteps = m_vpSteps; + + Song copy; + if( !copy.LoadFromSongDir( sDir ) ) + return false; + copy.RemoveAutoGenNotes(); + *this = copy; + + // First we assemble a map to let us easily find the new steps + map mNewSteps; + for( vector::const_iterator it = m_vpSteps.begin(); it != m_vpSteps.end(); ++it ) + { + StepsID id; + id.FromSteps( *it ); + mNewSteps[id] = *it; + } + + // Now we wipe out the new pointers, which were shallow copied and not deep copied... + m_vpSteps.clear(); + FOREACH_ENUM( StepsType, i ) + m_vpStepsByType[i].clear(); + + /* Then we copy as many Steps as possible on top of the old pointers. + * The only pointers that change are pointers to Steps that are not in the + * reverted file, which we delete, and pointers to Steps that are in the + * reverted file but not the original *this, which we create new copies of. + * We have to go through these hoops because many places assume the Steps + * pointers don't change - even though there are other ways they can change, + * such as deleting a Steps via the editor. */ + for( vector::const_iterator itOld = vOldSteps.begin(); itOld != vOldSteps.end(); ++itOld ) + { + StepsID id; + id.FromSteps( *itOld ); + map::iterator itNew = mNewSteps.find( id ); + if( itNew == mNewSteps.end() ) + { + // This stepchart didn't exist in the file we reverted from + delete *itOld; + } + else + { + Steps *OldSteps = *itOld; + *OldSteps = *(itNew->second); + AddSteps( OldSteps ); + mNewSteps.erase( itNew ); + } + } + // The leftovers in the map are steps that didn't exist before we reverted + for( map::const_iterator it = mNewSteps.begin(); it != mNewSteps.end(); ++it ) + { + Steps *NewSteps = new Steps(); + *NewSteps = *(it->second); + AddSteps( NewSteps ); + } + + AddAutoGenNotes(); + return true; +} + +static void GetImageDirListing( RString sPath, vector &AddTo ) +{ + GetDirListing( sPath + ".png", AddTo, false, false ); + GetDirListing( sPath + ".jpg", AddTo, false, false ); + GetDirListing( sPath + ".jpeg", AddTo, false, false ); + GetDirListing( sPath + ".bmp", AddTo, false, false ); + GetDirListing( sPath + ".gif", AddTo, false, false ); +} + +/* Fix up song paths. If there's a leading "./", be sure to keep it: it's + * a signal that the path is from the root directory, not the song directory. + * Other than a leading "./", song paths must never contain "." or "..". */ +void FixupPath( RString &path, const RString &sSongPath ) +{ + // Replace backslashes with slashes in all paths. + FixSlashesInPlace( path ); + + /* Many imported files contain erroneous whitespace before or after + * filenames. Paths usually don't actually start or end with spaces, + * so let's just remove it. */ + Trim( path ); +} + +// Songs in BlacklistImages will never be autodetected as song images. +void Song::TidyUpData( bool fromCache, bool duringCache ) +{ + // We need to do this before calling any of HasMusic, HasHasCDTitle, etc. + ASSERT_M( m_sSongDir.Left(3) != "../", m_sSongDir ); // meaningless + FixupPath( m_sSongDir, "" ); + FixupPath( m_sMusicFile, m_sSongDir ); + FOREACH_ENUM( InstrumentTrack, i ) + if( !m_sInstrumentTrackFile[i].empty() ) + FixupPath( m_sInstrumentTrackFile[i], m_sSongDir ); + FixupPath( m_sBannerFile, m_sSongDir ); + FixupPath( m_sJacketFile, m_sSongDir ); + FixupPath( m_sCDFile, m_sSongDir ); + FixupPath( m_sDiscFile, m_sSongDir ); + FixupPath( m_sLyricsFile, m_sSongDir ); + FixupPath( m_sBackgroundFile, m_sSongDir ); + FixupPath( m_sCDTitleFile, m_sSongDir ); + + if( !HasMusic() ) + { + vector arrayPossibleMusic; + GetDirListing( m_sSongDir + RString("*.mp3"), arrayPossibleMusic ); + GetDirListing( m_sSongDir + RString("*.oga"), arrayPossibleMusic ); + GetDirListing( m_sSongDir + RString("*.ogg"), arrayPossibleMusic ); + GetDirListing( m_sSongDir + RString("*.wav"), arrayPossibleMusic ); + + if( !arrayPossibleMusic.empty() ) + { + int idx = 0; + /* If the first song is "intro", and we have more than one available, + * don't use it--it's probably a KSF intro music file, which we don't + * (yet) support. */ + if( arrayPossibleMusic.size() > 1 && + !arrayPossibleMusic[0].Left(5).CompareNoCase("intro") ) + ++idx; + + // we found a match + m_sMusicFile = arrayPossibleMusic[idx]; + } + } + + // This must be done before radar calculation. + if( HasMusic() ) + { + RString error; + RageSoundReader *Sample = RageSoundReader_FileReader::OpenFile( GetMusicPath(), error ); + /* XXX: Checking if the music file exists eliminates a warning + * originating from BMS files (which have no music file, per se) + * but it's something of a hack. */ + if( Sample == NULL && m_sMusicFile != "" ) + { + LOG->UserLog( "Sound file", GetMusicPath(), "couldn't be opened: %s", error.c_str() ); + + // Don't use this file. + m_sMusicFile = ""; + } + else if ( Sample != NULL ) + { + m_fMusicLengthSeconds = Sample->GetLength() / 1000.0f; + delete Sample; + + if( m_fMusicLengthSeconds < 0 ) + { + // It failed; bad file or something. It's already logged a warning. + m_fMusicLengthSeconds = 100; // guess + } + else if( m_fMusicLengthSeconds == 0 ) + { + LOG->UserLog( "Sound file", GetMusicPath(), "is empty." ); + } + } + } + else // ! HasMusic() + { + m_fMusicLengthSeconds = 100; // guess + LOG->UserLog("Song", + GetSongDir(), + "has no music file; guessing at %f seconds", + m_fMusicLengthSeconds); + } + + if( m_fMusicLengthSeconds < 0 ) + { + LOG->UserLog("Sound file", + GetMusicPath(), + "has a negative length %f.", + m_fMusicLengthSeconds); + m_fMusicLengthSeconds = 0; + } + + m_SongTiming.TidyUpData(); + + FOREACH( Steps *, m_vpSteps, s ) + { + (*s)->m_Timing.TidyUpData(); + } + + /* Generate these before we autogen notes, so the new notes can inherit + * their source's values. */ + ReCalculateRadarValuesAndLastSecond( fromCache, true ); + + Trim( m_sMainTitle ); + Trim( m_sSubTitle ); + Trim( m_sArtist ); + + // Fall back on the song directory name. + if( m_sMainTitle == "" ) + NotesLoader::GetMainAndSubTitlesFromFullTitle(Basename(this->GetSongDir()), + m_sMainTitle, m_sSubTitle ); + + if( m_sArtist == "" ) + m_sArtist = "Unknown artist"; + TranslateTitles(); + + if( m_fMusicSampleStartSeconds == -1 || + m_fMusicSampleLengthSeconds == 0 || + m_fMusicSampleStartSeconds+m_fMusicSampleLengthSeconds > this->m_fMusicLengthSeconds ) + { + const TimingData &timing = this->m_SongTiming; + m_fMusicSampleStartSeconds = timing.GetElapsedTimeFromBeat( 100 ); + + if( m_fMusicSampleStartSeconds+m_fMusicSampleLengthSeconds > this->m_fMusicLengthSeconds ) + { + // Attempt to get a reasonable default. + int iBeat = lrintf(this->m_SongTiming.GetBeatFromElapsedTime(this->GetLastSecond())/2); + iBeat -= iBeat%4; + m_fMusicSampleStartSeconds = timing.GetElapsedTimeFromBeat( (float)iBeat ); + } + } + + // The old logic meant that you couldn't have sample lengths that go forever, + // e.g. those in Donkey Konga. I never liked that. -freem + if( m_fMusicSampleLengthSeconds <= 0.00f ) + m_fMusicSampleLengthSeconds = DEFAULT_MUSIC_SAMPLE_LENGTH; + + // 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. + + CHECKPOINT_M( "Looking for images..." ); + + if( !fromCache ) + { + + // First, check the file name for hints. + if( !HasBanner() ) + { + /* If a nonexistant banner file is specified, and we can't find a + * replacement, don't wipe out the old value. */ + //m_sBannerFile = ""; + + // find an image with "banner" in the file name + vector arrayPossibleBanners; + GetImageDirListing( m_sSongDir + "*banner*", arrayPossibleBanners ); + + /* Some people do things differently for the sake of being different. + * Don't match eg. abnormal, numbness. */ + GetImageDirListing( m_sSongDir + "* BN", arrayPossibleBanners ); + + if( !arrayPossibleBanners.empty() ) + m_sBannerFile = arrayPossibleBanners[0]; + } + + if( !HasBackground() ) + { + //m_sBackgroundFile = ""; + + // find an image with "bg" or "background" in the file name + vector arrayPossibleBGs; + GetImageDirListing( m_sSongDir + "*background*", arrayPossibleBGs ); + // don't match e.g. "subgroup", "hobgoblin", etc. + GetImageDirListing( m_sSongDir + "*bg", arrayPossibleBGs ); + if( !arrayPossibleBGs.empty() ) + m_sBackgroundFile = arrayPossibleBGs[0]; + } + + if( !HasJacket() ) + { + // find an image with "jacket" or "albumart" in the filename. + vector arrayPossibleJackets; + GetImageDirListing( m_sSongDir + "jk_*", arrayPossibleJackets ); + GetImageDirListing( m_sSongDir + "*jacket*", arrayPossibleJackets ); + GetImageDirListing( m_sSongDir + "*albumart*", arrayPossibleJackets ); + if( !arrayPossibleJackets.empty() ) + m_sJacketFile = arrayPossibleJackets[0]; + } + + if( !HasCDImage() ) + { + // CD image, a la ddr 1st-3rd (not to be confused with CDTitles) + // find an image with "-cd" at the end of the filename. + vector arrayPossibleCDImages; + GetImageDirListing( m_sSongDir + "*-cd", arrayPossibleCDImages ); + if( !arrayPossibleCDImages.empty() ) + m_sCDFile = arrayPossibleCDImages[0]; + } + + if( !HasDisc() ) + { + // a rectangular graphic, not to be confused with CDImage above. + vector arrayPossibleDiscImages; + GetImageDirListing( m_sSongDir + "* disc", arrayPossibleDiscImages ); + GetImageDirListing( m_sSongDir + "* title", arrayPossibleDiscImages ); + if( !arrayPossibleDiscImages.empty() ) + m_sDiscFile = arrayPossibleDiscImages[0]; + } + + if( !HasCDTitle() ) + { + // find an image with "cdtitle" in the file name + vector arrayPossibleCDTitles; + GetImageDirListing( m_sSongDir + "*cdtitle*", arrayPossibleCDTitles ); + if( !arrayPossibleCDTitles.empty() ) + m_sCDTitleFile = arrayPossibleCDTitles[0]; + } + + if( !HasLyrics() ) + { + // Check if there is a lyric file in here + vector arrayLyricFiles; + GetDirListing(m_sSongDir + RString("*.lrc"), arrayLyricFiles ); + if( !arrayLyricFiles.empty() ) + m_sLyricsFile = arrayLyricFiles[0]; + } + + /* Now, For the images we still haven't found, + * look at the image dimensions of the remaining unclassified images. */ + vector arrayImages; + GetImageDirListing( m_sSongDir + "*", arrayImages ); + + for( unsigned i=0; iUserLog( "Graphic file", sPath, "couldn't be loaded: %s", error.c_str() ); + continue; + } + + const int width = img->w; + const int height = img->h; + delete img; + + if( !HasBackground() && width >= 320 && height >= 240 ) + { + m_sBackgroundFile = arrayImages[i]; + continue; + } + + if( !HasBanner() && 100<=width && width<=320 && 50<=height && height<=240 ) + { + m_sBannerFile = arrayImages[i]; + continue; + } + + /* Some songs have overlarge banners. Check if the ratio is reasonable + * (over 2:1; usually over 3:1), and large (not a cdtitle). */ + if( !HasBanner() && width > 200 && float(width) / height > 2.0f ) + { + m_sBannerFile = arrayImages[i]; + continue; + } + + /* Agh. DWI's inline title images are triggering this, resulting in + * kanji, etc., being used as a CDTitle for songs with none. Some + * sample data from random incarnations: + * 42x50 35x50 50x50 144x49 + * It looks like ~50 height is what people use to align to DWI's font. + * + * My tallest CDTitle is 44. Let's cut off in the middle and hope for + * the best. -(who? -aj) */ + /* The proper size of a CDTitle is 64x48 or sometihng. Simfile artists + * typically don't give a shit about this (see Cetaka's fucking banner + * -sized CDTitle). This is also subverted in certain designs (beta + * Mungyodance 3 simfiles, for instance, used the CDTitle to hold + * various information about the song in question). As it stands, + * I'm keeping this code until I figure out wtf to do -aj + */ + if( !HasCDTitle() && width<=100 && height<=48 ) + { + m_sCDTitleFile = arrayImages[i]; + continue; + } + + // Jacket files typically have the same width and height. + if( !HasJacket() && width == height ) + { + m_sJacketFile = arrayImages[i]; + continue; + } + + // Disc images are typically rectangular; make sure we have a banner already. + if( !HasDisc() && (width > height) && HasBanner() ) + { + if( arrayImages[i] != m_sBannerFile ) + m_sDiscFile = arrayImages[i]; + continue; + } + + // CD images are the same as Jackets, typically the same width and height + if( !HasCDImage() && width == height ) + { + m_sCDFile = arrayImages[i]; + continue; + } + } + + } + + // These will be written to cache, for Song::LoadFromSongDir to use later. + m_bHasMusic = HasMusic(); + m_bHasBanner = HasBanner(); + m_bHasBackground = HasBackground(); + + if( HasBanner() ) + BANNERCACHE->CacheBanner( GetBannerPath() ); + /* + if( HasBackground() ) + BANNERCACHE->CacheBackground( GetBackgroundPath() ); + */ + + // If no BGChanges are specified and there are movies in the song directory, then assume + // they are DWI style where the movie begins at beat 0. + if( (!HasBGChanges() && !fromCache) ) + { + vector arrayPossibleMovies; + GetDirListing( m_sSongDir + RString("*.ogv"), arrayPossibleMovies ); + GetDirListing( m_sSongDir + RString("*.avi"), arrayPossibleMovies ); + GetDirListing( m_sSongDir + RString("*.mpg"), arrayPossibleMovies ); + GetDirListing( m_sSongDir + RString("*.mpeg"), arrayPossibleMovies ); + /* Use this->GetBeatFromElapsedTime(0) instead of 0 to start when the + * music starts. */ + if( arrayPossibleMovies.size() == 1 ) + this->AddBackgroundChange(BACKGROUND_LAYER_1, + BackgroundChange(0, + arrayPossibleMovies[0], + "", + 1.f, + SBE_StretchNoLoop)); + } + + + /* Don't allow multiple Steps of the same StepsType and Difficulty (except + * for edits). We should be able to use difficulty names as unique + * identifiers for steps. */ + if( !fromCache ) + { + SongUtil::AdjustDuplicateSteps( this ); + } +} + +void Song::TranslateTitles() +{ + static TitleSubst tsub("Songs"); + + TitleFields title; + title.LoadFromStrings(m_sMainTitle, m_sSubTitle, m_sArtist, + m_sMainTitleTranslit, m_sSubTitleTranslit, m_sArtistTranslit ); + tsub.Subst( title ); + title.SaveToStrings(m_sMainTitle, m_sSubTitle, m_sArtist, + m_sMainTitleTranslit, m_sSubTitleTranslit, m_sArtistTranslit ); +} + +void Song::ReCalculateRadarValuesAndLastSecond(bool fromCache, bool duringCache) +{ + if( fromCache && this->GetFirstSecond() >= 0 && this->GetLastSecond() > 0 ) + { + // this is loaded from cache, then we just have to calculate the radar values. + for( unsigned i=0; iCalculateRadarValues( m_fMusicLengthSeconds ); + return; + } + + float localFirst = FLT_MAX; // inf + // Make sure we're at least as long as the specified amount below. + float localLast = this->specifiedLastSecond; + + for( unsigned i=0; iCalculateRadarValues( m_fMusicLengthSeconds ); + + // Must initialize before the gotos. + NoteData tempNoteData; + pSteps->GetNoteData( tempNoteData ); + + // calculate lastSecond + + // If it's autogen, then first/last beat will come from the parent. + if( pSteps->IsAutogen() ) + goto wipe_notedata; + + /* Don't calculate with edits unless the song only contains an edit + * chart, like those in Mungyodance 3. Otherwise, edits installed on + * the machine could extend the length of the song. */ + if( pSteps->IsAnEdit() && m_vpSteps.size() > 1 ) + goto wipe_notedata; + + // Don't set first/last beat based on lights. They often start very + // early and end very late. + if( pSteps->m_StepsType == StepsType_lights_cabinet ) + continue; // no need to wipe this. + + /* Many songs have stray, empty song patterns. Ignore them, so they + * don't force the first beat of the whole song to 0. */ + if( tempNoteData.GetLastRow() != 0 ) + { + localFirst = min(localFirst, + pSteps->m_Timing.GetElapsedTimeFromBeat(tempNoteData.GetFirstBeat())); + localLast = max(localLast, + pSteps->m_Timing.GetElapsedTimeFromBeat(tempNoteData.GetLastBeat())); + } + wipe_notedata: + if (duringCache) + { + NoteData dummy; + dummy.SetNumTracks(tempNoteData.GetNumTracks()); + pSteps->SetNoteData(dummy); + } + } + + // Yes, for some reason we can have freaky stuff take place here. + this->firstSecond = (localFirst < localLast) ? localFirst : 0; + this->lastSecond = localLast; +} + +// Return whether the song is playable in the given style. +bool Song::SongCompleteForStyle( const Style *st ) const +{ + return HasStepsType( st->m_StepsType ); +} + +bool Song::HasStepsType( StepsType st ) const +{ + return SongUtil::GetOneSteps( this, st ) != NULL; +} + +bool Song::HasStepsTypeAndDifficulty( StepsType st, Difficulty dc ) const +{ + return SongUtil::GetOneSteps( this, st, dc ) != NULL; +} + +void Song::Save() +{ + LOG->Trace( "Song::SaveToSongFile()" ); + + ReCalculateRadarValuesAndLastSecond(); + TranslateTitles(); + + // Save the new files. These calls make backups on their own. + if( !SaveToSSCFile(GetSongFilePath(), false) ) + return; + SaveToSMFile(); + //SaveToDWIFile(); + SaveToCacheFile(); + + /* We've safely written our files and created backups. Rename non-SM and + * non-DWI files to avoid confusion. */ + vector arrayOldFileNames; + GetDirListing( m_sSongDir + "*.bms", arrayOldFileNames ); + GetDirListing( m_sSongDir + "*.pms", arrayOldFileNames ); + GetDirListing( m_sSongDir + "*.ksf", arrayOldFileNames ); + + for( unsigned i=0; iUserLog( "Song file", sOldPath, "couldn't be backed up." ); + // Don't remove. + } + else + FILEMAN->Remove( sOldPath ); + } +} + +bool Song::SaveToSMFile() +{ + const RString sPath = SetExtension( GetSongFilePath(), "sm" ); + LOG->Trace( "Song::SaveToSMFile(%s)", sPath.c_str() ); + + // If the file exists, make a backup. + if( IsAFile(sPath) ) + FileCopy( sPath, sPath + ".old" ); + + vector vpStepsToSave; + FOREACH_CONST( Steps*, m_vpSteps, s ) + { + Steps *pSteps = *s; + if( pSteps->IsAutogen() ) + continue; // don't write autogen notes + + // Only save steps that weren't loaded from a profile. + if( pSteps->WasLoadedFromProfile() ) + continue; + + vpStepsToSave.push_back( pSteps ); + } + + return NotesWriterSM::Write( sPath, *this, vpStepsToSave ); + +} + +bool Song::SaveToSSCFile( RString sPath, bool bSavingCache ) +{ + RString path = sPath; + if (!bSavingCache) + path = SetExtension(sPath, "ssc"); + + LOG->Trace( "Song::SaveToSSCFile('%s')", path.c_str() ); + + // If the file exists, make a backup. + if( !bSavingCache && IsAFile(path) ) + FileCopy( path, path + ".old" ); + + vector vpStepsToSave; + FOREACH_CONST( Steps*, m_vpSteps, s ) + { + Steps *pSteps = *s; + if( pSteps->IsAutogen() ) + continue; // don't write autogen notes + + // Only save steps that weren't loaded from a profile. + if( pSteps->WasLoadedFromProfile() ) + continue; + + if (!bSavingCache) + pSteps->SetFilename(path); + vpStepsToSave.push_back( pSteps ); + } + + if (bSavingCache) + { + return NotesWriterSSC::Write(path, *this, vpStepsToSave, bSavingCache); + } + + if( !NotesWriterSSC::Write(path, *this, vpStepsToSave, bSavingCache) ) + return false; + + if( g_BackUpAllSongSaves.Get() ) + { + RString sExt = GetExtension( path ); + RString sBackupFile = SetExtension( path, "" ); + + time_t cur_time; + time( &cur_time ); + struct tm now; + localtime_r( &cur_time, &now ); + + sBackupFile += ssprintf( "-%04i-%02i-%02i--%02i-%02i-%02i", + 1900+now.tm_year, now.tm_mon+1, now.tm_mday, now.tm_hour, now.tm_min, now.tm_sec ); + sBackupFile = SetExtension( sBackupFile, sExt ); + sBackupFile += ssprintf( ".old" ); + + if( FileCopy(path, sBackupFile) ) + LOG->Trace( "Backed up %s to %s", path.c_str(), sBackupFile.c_str() ); + else + LOG->Trace( "Failed to back up %s to %s", path.c_str(), sBackupFile.c_str() ); + } + + // Mark these steps saved to disk. + FOREACH( Steps*, vpStepsToSave, s ) + (*s)->SetSavedToDisk( true ); + + return true; +} + +bool Song::SaveToJsonFile( RString sPath) +{ + LOG->Trace( "Song::SaveToJsonFile('%s')", sPath.c_str() ); + return NotesWriterJson::WriteSong(sPath, *this, true); +} + +bool Song::SaveToCacheFile() +{ + SONGINDEX->AddCacheIndex(m_sSongDir, GetHashForDirectory(m_sSongDir)); + const RString sPath = GetCacheFilePath(); + return SaveToSSCFile(sPath, true); +} + +bool Song::SaveToDWIFile() +{ + const RString sPath = SetExtension( GetSongFilePath(), "dwi" ); + LOG->Trace( "Song::SaveToDWIFile(%s)", sPath.c_str() ); + + // If the file exists, make a backup. + if( IsAFile(sPath) ) + FileCopy( sPath, sPath + ".old" ); + + return NotesWriterDWI::Write( sPath, *this ); +} + + +void Song::AddAutoGenNotes() +{ + bool HasNotes[NUM_StepsType]; + memset( HasNotes, 0, sizeof(HasNotes) ); + for( unsigned i=0; i < m_vpSteps.size(); i++ ) // foreach Steps + { + if( m_vpSteps[i]->IsAutogen() ) + continue; + + StepsType st = m_vpSteps[i]->m_StepsType; + HasNotes[st] = true; + } + + FOREACH_ENUM( StepsType, stMissing ) + { + if( HasNotes[stMissing] ) + continue; + + // If m_bAutogenSteps is disabled, only autogen lights. + if( !PREFSMAN->m_bAutogenSteps && stMissing != StepsType_lights_cabinet ) + continue; + if( !GAMEMAN->GetStepsTypeInfo(stMissing).bAllowAutogen ) + continue; + + // missing Steps of this type + int iNumTracksOfMissing = GAMEMAN->GetStepsTypeInfo(stMissing).iNumTracks; + + // look for closest match + StepsType stBestMatch = StepsType_Invalid; + int iBestTrackDifference = INT_MAX; + + FOREACH_ENUM( StepsType, st ) + { + if( !HasNotes[st] ) + continue; + + // has (non-autogen) Steps of this type + const int iNumTracks = GAMEMAN->GetStepsTypeInfo(st).iNumTracks; + const int iTrackDifference = abs(iNumTracks-iNumTracksOfMissing); + if( iTrackDifference < iBestTrackDifference ) + { + stBestMatch = st; + iBestTrackDifference = iTrackDifference; + } + } + + if( stBestMatch != StepsType_Invalid ) + AutoGen( stMissing, stBestMatch ); + } +} + +void Song::AutoGen( StepsType ntTo, StepsType ntFrom ) +{ + // int iNumTracksOfTo = GAMEMAN->StepsTypeToNumTracks(ntTo); + + for( unsigned int j=0; jm_StepsType == ntFrom ) + { + Steps* pNewNotes = new Steps; + pNewNotes->AutogenFrom( pOriginalNotes, ntTo ); + this->AddSteps( pNewNotes ); + } + } +} + +void Song::RemoveAutoGenNotes() +{ + FOREACH_ENUM( StepsType, st ) + { + for( int j=m_vpStepsByType[st].size()-1; j>=0; j-- ) + { + if( m_vpStepsByType[st][j]->IsAutogen() ) + { + // delete m_vpSteps[j]; // delete below + m_vpStepsByType[st].erase( m_vpStepsByType[st].begin()+j ); + } + } + } + + for( int j=m_vpSteps.size()-1; j>=0; j-- ) + { + if( m_vpSteps[j]->IsAutogen() ) + { + delete m_vpSteps[j]; + m_vpSteps.erase( m_vpSteps.begin()+j ); + } + } +} + +bool Song::IsEasy( StepsType st ) const +{ + /* Very fast songs and songs with wide tempo changes are hard for new + * players, even if they have beginner steps. */ + DisplayBpms bpms; + this->GetDisplayBpms(bpms); + if( bpms.GetMax() >= 250 || bpms.GetMax() - bpms.GetMin() >= 75 ) + return false; + + /* The easy marker indicates which songs a beginner, having selected + * "beginner", can play and actually get a very easy song: if there are + * actual beginner steps, or if the light steps are 1- or 2-foot. */ + const Steps* pBeginnerNotes = SongUtil::GetStepsByDifficulty( this, st, Difficulty_Beginner ); + if( pBeginnerNotes ) + return true; + + const Steps* pEasyNotes = SongUtil::GetStepsByDifficulty( this, st, Difficulty_Easy ); + if( pEasyNotes && pEasyNotes->GetMeter() == 1 ) + return true; + + return false; +} + +bool Song::IsTutorial() const +{ + // A Song is considered a Tutorial if it has only Beginner steps. + FOREACH_CONST( Steps*, m_vpSteps, s ) + { + if( (*s)->m_StepsType == StepsType_lights_cabinet ) + continue; // ignore + if( (*s)->GetDifficulty() != Difficulty_Beginner ) + return false; + } + + return true; +} + +bool Song::HasEdits( StepsType st ) const +{ + for( unsigned i=0; im_StepsType == st && + pSteps->GetDifficulty() == Difficulty_Edit ) + { + return true; + } + } + + return false; +} + +bool Song::NormallyDisplayed() const +{ + return UNLOCKMAN == NULL || !UNLOCKMAN->SongIsLocked(this); +} + +bool Song::ShowInDemonstrationAndRanking() const { return !IsTutorial() && NormallyDisplayed(); } + + +// Hack: see Song::TidyUpData comments. +bool Song::HasMusic() const +{ + // If we have keys, we always have music. + if( m_vsKeysoundFile.size() != 0 ) + return true; + + return m_sMusicFile != "" && IsAFile(GetMusicPath()); +} +bool Song::HasBanner() const +{ + return m_sBannerFile != "" && IsAFile(GetBannerPath()); +} +bool Song::HasInstrumentTrack( InstrumentTrack it ) const +{ + return m_sInstrumentTrackFile[it] != "" && IsAFile(GetInstrumentTrackPath(it)); +} +bool Song::HasLyrics() const +{ + return m_sLyricsFile != "" && IsAFile(GetLyricsPath()); +} +bool Song::HasBackground() const +{ + return m_sBackgroundFile != "" && IsAFile(GetBackgroundPath()); +} +bool Song::HasCDTitle() const +{ + return m_sCDTitleFile != "" && IsAFile(GetCDTitlePath()); +} +bool Song::HasBGChanges() const +{ + FOREACH_BackgroundLayer( i ) + { + if( !GetBackgroundChanges(i).empty() ) + return true; + } + return false; +} +bool Song::HasAttacks() const {return !m_Attacks.empty(); } +bool Song::HasJacket() const +{ + return m_sJacketFile != "" && IsAFile(GetJacketPath()); +} +bool Song::HasDisc() const +{ + return m_sDiscFile != "" && IsAFile(GetDiscPath()); +} +bool Song::HasCDImage() const +{ + return m_sCDFile != "" && IsAFile(GetCDImagePath()); +} +bool Song::HasPreviewVid() const +{ + return m_sPreviewVidFile != "" && IsAFile(GetPreviewVidPath()); +} + +const vector &Song::GetBackgroundChanges( BackgroundLayer bl ) const +{ + return *(m_BackgroundChanges[bl]); +} +vector &Song::GetBackgroundChanges( BackgroundLayer bl ) +{ + return *(m_BackgroundChanges[bl].Get()); +} + +const vector &Song::GetForegroundChanges() const +{ + return *m_ForegroundChanges; +} +vector &Song::GetForegroundChanges() +{ + return *m_ForegroundChanges.Get(); +} + +vector Song::GetChangesToVectorString(const vector & changes) const +{ + vector ret; + FOREACH_CONST( BackgroundChange, changes, bgc ) + { + ret.push_back((*bgc).ToString()); + } + return ret; +} + +vector Song::GetBGChanges1ToVectorString() const +{ + return this->GetChangesToVectorString(this->GetBackgroundChanges(BACKGROUND_LAYER_1)); +} + +vector Song::GetBGChanges2ToVectorString() const +{ + return this->GetChangesToVectorString(this->GetBackgroundChanges(BACKGROUND_LAYER_2)); +} + +vector Song::GetFGChanges1ToVectorString() const +{ + return this->GetChangesToVectorString(this->GetForegroundChanges()); +} + +vector Song::GetInstrumentTracksToVectorString() const +{ + vector ret; + FOREACH_ENUM(InstrumentTrack, it) + { + if (this->HasInstrumentTrack(it)) + { + ret.push_back(InstrumentTrackToString(it) + + "=" + + this->m_sInstrumentTrackFile[it]); + } + } + return ret; +} + +RString GetSongAssetPath( RString sPath, const RString &sSongPath ) +{ + if( sPath == "" ) + return RString(); + + /* If there's no path in the file, the file is in the same directory as the + * song. (This is the preferred configuration.) */ + if( sPath.find('/') == string::npos ) + return sSongPath+sPath; + + // The song contains a path; treat it as relative to the top SM directory. + if( sPath.Left(3) == "../" ) + { + // The path begins with "../". Resolve it wrt. the song directory. + sPath = sSongPath + sPath; + } + + CollapsePath( sPath ); + + /* If the path still begins with "../", then there were an unreasonable number + * of them at the beginning of the path. Ignore the path entirely. */ + if( sPath.Left(3) == "../" ) + return RString(); + + return sPath; +} + +/* Note that supplying a path relative to the top-level directory is only for + * compatibility with DWI. We prefer paths relative to the song directory. */ +RString Song::GetMusicPath() const +{ + return GetSongAssetPath( m_sMusicFile, m_sSongDir ); +} + +RString Song::GetInstrumentTrackPath( InstrumentTrack it ) const +{ + return GetSongAssetPath( m_sInstrumentTrackFile[it], m_sSongDir ); +} + +RString Song::GetBannerPath() const +{ + return GetSongAssetPath( m_sBannerFile, m_sSongDir ); +} + +RString Song::GetLyricsPath() const +{ + return GetSongAssetPath( m_sLyricsFile, m_sSongDir ); +} + +RString Song::GetCDTitlePath() const +{ + return GetSongAssetPath( m_sCDTitleFile, m_sSongDir ); +} + +RString Song::GetBackgroundPath() const +{ + return GetSongAssetPath( m_sBackgroundFile, m_sSongDir ); +} + +RString Song::GetJacketPath() const +{ + return GetSongAssetPath( m_sJacketFile, m_sSongDir ); +} + +RString Song::GetDiscPath() const +{ + return GetSongAssetPath( m_sDiscFile, m_sSongDir ); +} + +RString Song::GetCDImagePath() const +{ + return GetSongAssetPath( m_sCDFile, m_sSongDir ); +} + +RString Song::GetPreviewVidPath() const +{ + return GetSongAssetPath( m_sPreviewVidFile, m_sSongDir ); +} + +RString Song::GetDisplayMainTitle() const +{ + if(!PREFSMAN->m_bShowNativeLanguage) return GetTranslitMainTitle(); + return m_sMainTitle; +} + +RString Song::GetDisplaySubTitle() const +{ + if(!PREFSMAN->m_bShowNativeLanguage) return GetTranslitSubTitle(); + return m_sSubTitle; +} + +RString Song::GetDisplayArtist() const +{ + if(!PREFSMAN->m_bShowNativeLanguage) return GetTranslitArtist(); + return m_sArtist; +} + +RString Song::GetDisplayFullTitle() const +{ + RString Title = GetDisplayMainTitle(); + RString SubTitle = GetDisplaySubTitle(); + + if(!SubTitle.empty()) Title += " " + SubTitle; + return Title; +} + +RString Song::GetTranslitFullTitle() const +{ + RString Title = GetTranslitMainTitle(); + RString SubTitle = GetTranslitSubTitle(); + + if(!SubTitle.empty()) Title += " " + SubTitle; + return Title; +} + +void Song::AddSteps( Steps* pSteps ) +{ + m_vpSteps.push_back( pSteps ); + ASSERT_M( pSteps->m_StepsType < NUM_StepsType, ssprintf("%i", pSteps->m_StepsType) ); + m_vpStepsByType[pSteps->m_StepsType].push_back( pSteps ); +} + +void Song::DeleteSteps( const Steps* pSteps, bool bReAutoGen ) +{ + ASSERT( !pSteps->IsAutogen() ); + + // Avoid any stale Note::parent pointers by removing all AutoGen'd Steps, + // then adding them again. + + if( bReAutoGen ) + RemoveAutoGenNotes(); + + vector &vpSteps = m_vpStepsByType[pSteps->m_StepsType]; + for( int j=vpSteps.size()-1; j>=0; j-- ) + { + if( vpSteps[j] == pSteps ) + { + //delete vpSteps[j]; // delete below + vpSteps.erase( vpSteps.begin()+j ); + break; + } + } + + for( int j=m_vpSteps.size()-1; j>=0; j-- ) + { + if( m_vpSteps[j] == pSteps ) + { + delete m_vpSteps[j]; + m_vpSteps.erase( m_vpSteps.begin()+j ); + break; + } + } + + if( bReAutoGen ) + AddAutoGenNotes(); +} + +bool Song::Matches(RString sGroup, RString sSong) const +{ + if( sGroup.size() && sGroup.CompareNoCase(this->m_sGroupName) != 0) + return false; + + RString sDir = this->GetSongDir(); + sDir.Replace("\\","/"); + vector bits; + split( sDir, "/", bits ); + ASSERT(bits.size() >= 2); // should always have at least two parts + const RString &sLastBit = bits[bits.size()-1]; + + // match on song dir or title (ala DWI) + if( !sSong.CompareNoCase(sLastBit) ) + return true; + if( !sSong.CompareNoCase(this->GetTranslitFullTitle()) ) + return true; + + return false; +} + +/* If apInUse is set, it contains a list of steps which are in use elsewhere, + * and should not be deleted. */ +void Song::FreeAllLoadedFromProfile( ProfileSlot slot, const set *setInUse ) +{ + /* DeleteSteps will remove and recreate autogen notes, which may reorder + * m_vpSteps, so be careful not to skip over entries. */ + vector apToRemove; + for( int s=m_vpSteps.size()-1; s>=0; s-- ) + { + Steps* pSteps = m_vpSteps[s]; + if( !pSteps->WasLoadedFromProfile() ) + continue; + if( slot != ProfileSlot_Invalid && pSteps->GetLoadedFromProfileSlot() != slot ) + continue; + if( setInUse != NULL && setInUse->find(pSteps) != setInUse->end() ) + continue; + apToRemove.push_back( pSteps ); + } + + for( unsigned i = 0; i < apToRemove.size(); ++i ) + this->DeleteSteps( apToRemove[i] ); +} + +void Song::GetStepsLoadedFromProfile( ProfileSlot slot, vector &vpStepsOut ) const +{ + for( unsigned s=0; sGetLoadedFromProfileSlot() == slot ) + vpStepsOut.push_back( pSteps ); + } +} + +int Song::GetNumStepsLoadedFromProfile( ProfileSlot slot ) const +{ + int iCount = 0; + for( unsigned s=0; sGetLoadedFromProfileSlot() == slot ) + iCount++; + } + return iCount; +} + +bool Song::IsEditAlreadyLoaded( Steps* pSteps ) const +{ + ASSERT_M( pSteps->GetDifficulty() == Difficulty_Edit, + ssprintf("The %s chart for %s is no edit, thus it can't be checked for loading.", + DifficultyToString(pSteps->GetDifficulty()).c_str(), + this->m_sMainTitle.c_str())); + + for( unsigned i=0; iGetDifficulty() == Difficulty_Edit && + pOther->m_StepsType == pSteps->m_StepsType && + pOther->GetHash() == pSteps->GetHash() ) + { + return true; + } + } + + return false; +} + +bool Song::IsStepsUsingDifferentTiming(Steps *pSteps) const +{ + return pSteps->m_Timing != this->m_SongTiming; +} + +bool Song::HasSignificantBpmChangesOrStops() const +{ + if( m_SongTiming.HasStops() || m_SongTiming.HasDelays() ) + return true; + + // Don't consider BPM changes that only are only for maintaining sync as + // a real BpmChange. + if( m_DisplayBPMType == DISPLAY_BPM_SPECIFIED ) + { + if( m_fSpecifiedBPMMin != m_fSpecifiedBPMMax ) + return true; + } + else if( m_SongTiming.HasBpmChanges() ) + { + return true; + } + + return false; +} + +float Song::GetStepsSeconds() const +{ + return this->GetLastSecond() - this->GetFirstSecond(); +} + +bool Song::IsLong() const +{ + return !IsMarathon() && m_fMusicLengthSeconds >= g_fLongVerSongSeconds; +} + +bool Song::IsMarathon() const +{ + return m_fMusicLengthSeconds >= g_fMarathonVerSongSeconds; +} + +// lua start +#include "LuaBinding.h" + +/** @brief Allow Lua to have access to the Song. */ +class LunaSong: public Luna +{ +public: + static int GetDisplayFullTitle( T* p, lua_State *L ) + { + lua_pushstring(L, p->GetDisplayFullTitle() ); return 1; + } + static int GetTranslitFullTitle( T* p, lua_State *L ) + { + lua_pushstring(L, p->GetTranslitFullTitle() ); return 1; + } + static int GetDisplayMainTitle( T* p, lua_State *L ) + { + lua_pushstring(L, p->GetDisplayMainTitle() ); return 1; + } + static int GetTranslitMainTitle( T* p, lua_State *L ) + { + lua_pushstring(L, p->GetTranslitMainTitle() ); return 1; + } + static int GetDisplaySubTitle( T* p, lua_State *L ) + { + lua_pushstring(L, p->GetDisplaySubTitle() ); return 1; + } + static int GetTranslitSubTitle( T* p, lua_State *L ) + { + lua_pushstring(L, p->GetTranslitSubTitle() ); return 1; + } + static int GetDisplayArtist( T* p, lua_State *L ) + { + lua_pushstring(L, p->GetDisplayArtist() ); return 1; + } + static int GetTranslitArtist( T* p, lua_State *L ) + { + lua_pushstring(L, p->GetTranslitArtist() ); return 1; + } + static int GetGenre( T* p, lua_State *L ) + { + lua_pushstring(L, p->m_sGenre ); return 1; + } + static int GetOrigin( T* p, lua_State *L ) + { + lua_pushstring(L, p->m_sOrigin ); return 1; + } + static int GetAllSteps( T* p, lua_State *L ) + { + const vector &v = p->GetAllSteps(); + LuaHelpers::CreateTableFromArray( v, L ); + return 1; + } + static int GetStepsByStepsType( T* p, lua_State *L ) + { + StepsType st = Enum::Check(L, 1); + const vector &v = p->GetStepsByStepsType( st ); + LuaHelpers::CreateTableFromArray( v, L ); + return 1; + } + static int GetSongDir( T* p, lua_State *L ) + { + lua_pushstring(L, p->GetSongDir() ); + return 1; + } + static int GetMusicPath( T* p, lua_State *L ) + { + RString s = p->GetMusicPath(); + if( !s.empty() ) + lua_pushstring(L, s); + else + lua_pushnil(L); + return 1; + } + static int GetBannerPath( T* p, lua_State *L ) + { + RString s = p->GetBannerPath(); + if( !s.empty() ) + lua_pushstring(L, s); + else + lua_pushnil(L); + return 1; + } + static int GetBackgroundPath( T* p, lua_State *L ) + { + RString s = p->GetBackgroundPath(); + if( !s.empty() ) + lua_pushstring(L, s); + else + lua_pushnil(L); + return 1; + } + static int GetPreviewVidPath( T* p, lua_State *L ) + { + RString s = p->GetPreviewVidPath(); + if( !s.empty() ) + lua_pushstring(L, s); + else + lua_pushnil(L); + return 1; + } + static int GetJacketPath( T* p, lua_State *L ) + { + RString s = p->GetJacketPath(); + if( !s.empty() ) + lua_pushstring(L, s); + else + lua_pushnil(L); + return 1; + } + static int GetCDImagePath( T* p, lua_State *L ) + { + RString s = p->GetCDImagePath(); + if( !s.empty() ) + lua_pushstring(L, s); + else + lua_pushnil(L); + return 1; + } + static int GetDiscPath( T* p, lua_State *L ) + { + RString s = p->GetDiscPath(); + if( !s.empty() ) + lua_pushstring(L, s); + else + lua_pushnil(L); + return 1; + } + static int GetCDTitlePath( T* p, lua_State *L ) + { + RString s = p->GetCDTitlePath(); + if( !s.empty() ) + lua_pushstring(L, s); + else + lua_pushnil(L); + return 1; + } + static int GetLyricsPath( T* p, lua_State *L ) + { + RString s = p->GetLyricsPath(); + if( !s.empty() ) + lua_pushstring(L, s); + else + lua_pushnil(L); + return 1; + } + static int GetSongFilePath( T* p, lua_State *L ) + { + lua_pushstring(L, p->GetSongFilePath() ); + return 1; + } + static int IsTutorial( T* p, lua_State *L ) + { + lua_pushboolean(L, p->IsTutorial()); + return 1; + } + static int IsEnabled( T* p, lua_State *L ) + { + lua_pushboolean(L, p->GetEnabled()); + return 1; + } + static int GetGroupName( T* p, lua_State *L ) + { + lua_pushstring(L, p->m_sGroupName); + return 1; + } + static int MusicLengthSeconds( T* p, lua_State *L ) + { + lua_pushnumber(L, p->m_fMusicLengthSeconds); + return 1; + } + static int GetSampleStart( T* p, lua_State *L ) + { + lua_pushnumber(L, p->m_fMusicSampleStartSeconds); + return 1; + } + static int GetSampleLength( T* p, lua_State *L ) + { + lua_pushnumber(L, p->m_fMusicSampleLengthSeconds); + return 1; + } + static int IsLong( T* p, lua_State *L ) + { + lua_pushboolean(L, p->IsLong()); + return 1; + } + static int IsMarathon( T* p, lua_State *L ) + { + lua_pushboolean(L, p->IsMarathon()); + return 1; + } + static int HasStepsType( T* p, lua_State *L ) + { + StepsType st = Enum::Check(L, 1); + lua_pushboolean( L, p->HasStepsType(st) ); + return 1; + } + static int HasStepsTypeAndDifficulty( T* p, lua_State *L ) + { + StepsType st = Enum::Check(L, 1); + Difficulty dc = Enum::Check( L, 2 ); + lua_pushboolean( L, p->HasStepsTypeAndDifficulty(st, dc) ); + return 1; + } + static int IsStepsUsingDifferentTiming(T* p, lua_State *L) + { + lua_pushboolean(L, p->IsStepsUsingDifferentTiming(Luna::check( L, 1, true ))); + return 1; + } + /* TODO: HasStepsTypeAndDifficulty and GetOneSteps should be in + * a SongUtil Lua table and a method of Steps. */ + static int GetOneSteps( T* p, lua_State *L ) + { + StepsType st = Enum::Check(L, 1); + Difficulty dc = Enum::Check( L, 2 ); + Steps *pSteps = SongUtil::GetOneSteps( p, st, dc ); + if( pSteps ) + pSteps->PushSelf(L); + else + lua_pushnil(L); + return 1; + } + static int GetTimingData( T* p, lua_State *L ) { p->m_SongTiming.PushSelf(L); return 1; } + // has functions + static int HasMusic( T* p, lua_State *L ) { lua_pushboolean(L, p->HasMusic()); return 1; } + static int HasBanner( T* p, lua_State *L ) { lua_pushboolean(L, p->HasBanner()); return 1; } + static int HasBackground( T* p, lua_State *L ) { lua_pushboolean(L, p->HasBackground()); return 1; } + static int HasPreviewVid( T* p, lua_State *L ) { lua_pushboolean(L, p->HasPreviewVid()); return 1; } + static int HasJacket( T* p, lua_State *L ) { lua_pushboolean(L, p->HasJacket()); return 1; } + static int HasDisc( T* p, lua_State *L ) { lua_pushboolean(L, p->HasDisc()); return 1; } + static int HasCDImage( T* p, lua_State *L ) { lua_pushboolean(L, p->HasCDImage()); return 1; } + static int HasCDTitle( T* p, lua_State *L ) { lua_pushboolean(L, p->HasCDTitle()); return 1; } + static int HasBGChanges( T* p, lua_State *L ) { lua_pushboolean(L, p->HasBGChanges()); return 1; } + static int HasLyrics( T* p, lua_State *L ) { lua_pushboolean(L, p->HasLyrics()); return 1; } + // functions that AJ loves + static int HasSignificantBPMChangesOrStops( T* p, lua_State *L ) + { + lua_pushboolean(L, p->HasSignificantBpmChangesOrStops()); + return 1; + } + static int HasEdits( T* p, lua_State *L ) + { + StepsType st = Enum::Check(L, 1); + lua_pushboolean(L, p->HasEdits( st )); + return 1; + } + static int IsEasy( T* p, lua_State *L ) + { + StepsType st = Enum::Check(L, 1); + lua_pushboolean(L, p->IsEasy( st )); + return 1; + } + static int GetStepsSeconds( T* p, lua_State *L ) + { + lua_pushnumber(L, p->GetStepsSeconds()); + return 1; + } + static int NormallyDisplayed( T* p, lua_State *L ) + { + lua_pushboolean(L, p->NormallyDisplayed()); + return 1; + } + static int ShowInDemonstrationAndRanking( T* p, lua_State *L ) + { + lua_pushboolean(L, p->ShowInDemonstrationAndRanking()); + return 1; + } + static int GetFirstSecond(T* p, lua_State *L) + { + lua_pushnumber(L, p->GetFirstSecond()); + return 1; + } + static int GetLastSecond(T* p, lua_State *L) + { + lua_pushnumber(L, p->GetLastSecond()); + return 1; + } + static int GetFirstBeat( T* p, lua_State *L ) + { + lua_pushnumber(L, p->GetFirstBeat()); + return 1; + } + static int GetLastBeat( T* p, lua_State *L ) + { + lua_pushnumber(L, p->GetLastBeat()); + return 1; + } + static int HasAttacks( T* p, lua_State *L ) + { + lua_pushboolean(L, p->HasAttacks()); + return 1; + } + static int GetDisplayBpms( T* p, lua_State *L ) + { + DisplayBpms temp; + p->GetDisplayBpms(temp); + float fMin = temp.GetMin(); + float fMax = temp.GetMax(); + vector fBPMs; + fBPMs.push_back( fMin ); + fBPMs.push_back( fMax ); + LuaHelpers::CreateTableFromArray(fBPMs, L); + return 1; + } + static int IsDisplayBpmSecret( T* p, lua_State *L ) + { + DisplayBpms temp; + p->GetDisplayBpms(temp); + lua_pushboolean( L, temp.IsSecret() ); + return 1; + } + static int IsDisplayBpmConstant( T* p, lua_State *L ) + { + DisplayBpms temp; + p->GetDisplayBpms(temp); + lua_pushboolean( L, temp.BpmIsConstant() ); + return 1; + } + static int IsDisplayBpmRandom( T* p, lua_State *L ) + { + lua_pushboolean( L, p->m_DisplayBPMType == DISPLAY_BPM_RANDOM ); + return 1; + } + + LunaSong() + { + ADD_METHOD( GetDisplayFullTitle ); + ADD_METHOD( GetTranslitFullTitle ); + ADD_METHOD( GetDisplayMainTitle ); + ADD_METHOD( GetTranslitMainTitle ); + ADD_METHOD( GetDisplaySubTitle ); + ADD_METHOD( GetTranslitSubTitle ); + ADD_METHOD( GetDisplayArtist ); + ADD_METHOD( GetTranslitArtist ); + ADD_METHOD( GetGenre ); + ADD_METHOD( GetOrigin ); + ADD_METHOD( GetAllSteps ); + ADD_METHOD( GetStepsByStepsType ); + ADD_METHOD( GetSongDir ); + ADD_METHOD( GetMusicPath ); + ADD_METHOD( GetBannerPath ); + ADD_METHOD( GetBackgroundPath ); + ADD_METHOD( GetJacketPath ); + ADD_METHOD( GetCDImagePath ); + ADD_METHOD( GetDiscPath ); + ADD_METHOD( GetCDTitlePath ); + ADD_METHOD( GetLyricsPath ); + ADD_METHOD( GetSongFilePath ); + ADD_METHOD( IsTutorial ); + ADD_METHOD( IsEnabled ); + ADD_METHOD( GetGroupName ); + ADD_METHOD( MusicLengthSeconds ); + ADD_METHOD( GetSampleStart ); + ADD_METHOD( GetSampleLength ); + ADD_METHOD( IsLong ); + ADD_METHOD( IsMarathon ); + ADD_METHOD( HasStepsType ); + ADD_METHOD( HasStepsTypeAndDifficulty ); + ADD_METHOD( GetOneSteps ); + ADD_METHOD( GetTimingData ); + ADD_METHOD( HasMusic ); + ADD_METHOD( HasBanner ); + ADD_METHOD( HasBackground ); + ADD_METHOD( HasJacket ); + ADD_METHOD( HasCDImage ); + ADD_METHOD( HasDisc ); + ADD_METHOD( HasCDTitle ); + ADD_METHOD( HasBGChanges ); + ADD_METHOD( HasLyrics ); + ADD_METHOD( HasSignificantBPMChangesOrStops ); + ADD_METHOD( HasEdits ); + ADD_METHOD( IsEasy ); + ADD_METHOD( GetStepsSeconds ); + ADD_METHOD( NormallyDisplayed ); + ADD_METHOD( GetFirstBeat ); + ADD_METHOD( GetFirstSecond ); + ADD_METHOD( GetLastBeat ); + ADD_METHOD( GetLastSecond ); + ADD_METHOD( HasAttacks ); + ADD_METHOD( GetDisplayBpms ); + ADD_METHOD( IsDisplayBpmSecret ); + ADD_METHOD( IsDisplayBpmConstant ); + ADD_METHOD( IsDisplayBpmRandom ); + ADD_METHOD( IsStepsUsingDifferentTiming ); + ADD_METHOD( ShowInDemonstrationAndRanking ); + ADD_METHOD( HasPreviewVid ); + ADD_METHOD( GetPreviewVidPath ); + } +}; + +LUA_REGISTER_CLASS( Song ) +// lua end + + +/* + * (c) 2001-2004 Chris Danford, Glenn Maynard + * 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. + */ diff --git a/src/Song.h b/src/Song.h index f06102adbe..1ed35cb13d 100644 --- a/src/Song.h +++ b/src/Song.h @@ -1,475 +1,479 @@ -#ifndef SONG_H -#define SONG_H - -#include "Attack.h" -#include "TimingData.h" -#include "Difficulty.h" -#include "EnumHelper.h" -#include "RageUtil_AutoPtr.h" -#include "RageUtil_CachedObject.h" -#include "RageTypes.h" -#include "Steps.h" -#include - -class Style; -class StepsID; -struct lua_State; -struct BackgroundChange; - -void FixupPath( RString &path, const RString &sSongPath ); -RString GetSongAssetPath( RString sPath, const RString &sSongPath ); - -/** @brief The version of the .ssc file format. */ -const static float STEPFILE_VERSION_NUMBER = 0.81f; - -/** @brief How many edits for this song can each profile have? */ -const int MAX_EDITS_PER_SONG_PER_PROFILE = 15; -/** @brief How many edits for this song can be available? */ -const int MAX_EDITS_PER_SONG = MAX_EDITS_PER_SONG_PER_PROFILE * NUM_ProfileSlot; - -extern const int FILE_CACHE_VERSION; - -/** @brief The different background layers available. */ -enum BackgroundLayer -{ - BACKGROUND_LAYER_1, - BACKGROUND_LAYER_2, - //BACKGROUND_LAYER_3, // StepCollection get - NUM_BackgroundLayer, - BACKGROUND_LAYER_Invalid -}; - -/** @brief A custom foreach loop for the different background layers. */ -#define FOREACH_BackgroundLayer( bl ) FOREACH_ENUM( BackgroundLayer, bl ) - -/** @brief Different instrument tracks for band type games. */ -enum InstrumentTrack -{ - InstrumentTrack_Guitar, - InstrumentTrack_Rhythm, - InstrumentTrack_Bass, - NUM_InstrumentTrack, - InstrumentTrack_Invalid -}; -const RString& InstrumentTrackToString( InstrumentTrack it ); -InstrumentTrack StringToInstrumentTrack( const RString& s ); - -/** @brief The collection of lyrics for the Song. */ -struct LyricSegment -{ - float m_fStartTime; /** @brief When does the lyric show up? */ - RString m_sLyric; /** @brief The lyrics themselves. */ - RageColor m_Color; /** @brief The color of the lyrics. */ -}; - -/** @brief Holds all music metadata and steps for one song. */ -class Song -{ - RString m_sSongDir; -public: - void SetSongDir( const RString sDir ) { m_sSongDir = sDir; } - RString GetSongDir() { return m_sSongDir; } - - /** @brief When should this song be displayed in the music wheel? */ - enum SelectionDisplay - { - SHOW_ALWAYS, /**< always show on the wheel. */ - SHOW_NEVER /**< never show on the wheel (unless song hiding is turned off). */ - } m_SelectionDisplay; - - Song(); - ~Song(); - void Reset(); - void DetachSteps(); - - /** - * @brief Load a song from the chosen directory. - * - * This assumes that there is no song present right now. - * @param sDir the song directory from which to load. */ - bool LoadFromSongDir( RString sDir ); - // This one takes the effort to reuse Steps pointers as best as it can - bool ReloadFromSongDir( RString sDir ); - - /** - * @brief Call this after loading a song to clean up invalid data. - * @param fromCache was this data loaded from the cache file? - * @param duringCache was this data loaded during the cache process? */ - void TidyUpData( bool fromCache = false, bool duringCache = false ); - - /** - * @brief Get the new radar values, and determine the last second at the same time. - * This is called by TidyUpData, after saving the Song. - * @param fromCache was this data loaded from the cache file? - * @param duringCache was this data loaded during the cache process? */ - void ReCalculateRadarValuesAndLastSecond(bool fromCache = false, bool duringCache = false); - /** - * @brief Translate any titles that aren't in english. - * This is called by TidyUpData. */ - void TranslateTitles(); - - /** - * @brief Save to the new SSC file format. - * @param sPath the path where we're saving the file. - * @param bSavingCache a flag to determine if we're saving cache data. - */ - bool SaveToSSCFile( RString sPath, bool bSavingCache ); - /** @brief Save to the SSC and SM files no matter what. */ - void Save(); - /** - * @brief Save the current Song to a cache file using the preferred format. - * @return its success or failure. */ - bool SaveToCacheFile(); - /** - * @brief Save the current Song to a SM file. - * @return its success or failure. */ - bool SaveToSMFile(); - /** - * @brief Save the current Song to a DWI file if possible. - * @return its success or failure. */ - bool SaveToDWIFile(); - - const RString &GetSongFilePath() const; - RString GetCacheFilePath() const; - - void AddAutoGenNotes(); - /** - * @brief Automatically generate steps from one type to another. - * @param ntTo the StepsType we're making. - * @param ntFrom the StepsType we're generating from. - */ - void AutoGen( StepsType ntTo, StepsType ntFrom ); - void RemoveAutoGenNotes(); - - // Directory this song data came from: - const RString &GetSongDir() const { return m_sSongDir; } - - /** - * @brief Filename associated with this file. - * This will always have a .SSC extension. If we loaded a .SSC, this will - * point to it, but if we loaded any other type, this will point to - * a generated .SSC filename. */ - RString m_sSongFileName; - - /** @brief The group this Song is in. */ - RString m_sGroupName; - - /** - * @brief the Profile this came from. - * This is ProfileSlot_Invalid if it wasn't loaded from a profile. */ - ProfileSlot m_LoadedFromProfile; - /** @brief Is the song file itself a symlink to another file? */ - bool m_bIsSymLink; - bool m_bEnabled; - - /** @brief The title of the Song. */ - RString m_sMainTitle; - /** @brief The subtitle of the Song, if it exists. */ - RString m_sSubTitle; - /** @brief The artist of the Song, if it exists. */ - RString m_sArtist; - /** @brief The transliterated title of the Song, if it exists. */ - RString m_sMainTitleTranslit; - /** @brief The transliterated subtitle of the Song, if it exists. */ - RString m_sSubTitleTranslit; - /** @brief The transliterated artist of the Song, if it exists. */ - RString m_sArtistTranslit; - - /* If PREFSMAN->m_bShowNative is off, these are the same as GetTranslit* - * below. Otherwise, they return the main titles. */ - RString GetDisplayMainTitle() const; - RString GetDisplaySubTitle() const; - RString GetDisplayArtist() const; - - /** - * @brief Retrieve the transliterated title, or the main title if there is no translit. - * @return the proper title. */ - RString GetTranslitMainTitle() const - { - return m_sMainTitleTranslit.size()? m_sMainTitleTranslit: m_sMainTitle; - } - /** - * @brief Retrieve the transliterated subtitle, or the main subtitle if there is no translit. - * @return the proper subtitle. */ - RString GetTranslitSubTitle() const - { - return m_sSubTitleTranslit.size()? m_sSubTitleTranslit: m_sSubTitle; - } - /** - * @brief Retrieve the transliterated artist, or the main artist if there is no translit. - * @return the proper artist. */ - RString GetTranslitArtist() const - { - return m_sArtistTranslit.size()? m_sArtistTranslit:m_sArtist; - } - - // "title subtitle" - RString GetDisplayFullTitle() const; - RString GetTranslitFullTitle() const; - - /** @brief The version of the song/file. */ - float m_fVersion; - /** @brief The genre of the song/file. */ - RString m_sGenre; - - /** - * @brief The person who worked with the song file who should be credited. - * This is read and saved, but never actually used. */ - RString m_sCredit; - - RString m_sOrigin; // song origin (for .ssc format) - - RString m_sMusicFile; - RString m_sInstrumentTrackFile[NUM_InstrumentTrack]; - - /** @brief The length of the music file. */ - float m_fMusicLengthSeconds; - float m_fMusicSampleStartSeconds; - float m_fMusicSampleLengthSeconds; - DisplayBPM m_DisplayBPMType; - float m_fSpecifiedBPMMin; - float m_fSpecifiedBPMMax; // if a range, then Min != Max - - RString m_sBannerFile; // typically a 16:5 ratio graphic (e.g. 256x80) - RString m_sJacketFile; // typically square (e.g. 192x192, 256x256) - RString m_sCDFile; // square (e.g. 128x128 [DDR 1st-3rd]) - RString m_sDiscFile; // rectangular (e.g. 256x192 [Pump], 200x150 [MGD3]) - /** @brief The location of the lyrics file, if it exists. */ - RString m_sLyricsFile; - RString m_sBackgroundFile; - RString m_sCDTitleFile; - RString m_sPreviewVidFile; - - AttackArray m_Attacks; - vector m_sAttackString; - - RString GetMusicPath() const; - RString GetInstrumentTrackPath( InstrumentTrack it ) const; - RString GetBannerPath() const; - RString GetJacketPath() const; - RString GetCDImagePath() const; - RString GetDiscPath() const; - RString GetLyricsPath() const; - RString GetBackgroundPath() const; - RString GetCDTitlePath() const; - RString GetPreviewVidPath() const; - - // For loading only: - bool m_bHasMusic, m_bHasBanner, m_bHasBackground; - - bool HasMusic() const; - bool HasInstrumentTrack( InstrumentTrack it ) const; - /** - * @brief Does this song have a banner? - * @return true if it does, false otherwise. */ - bool HasBanner() const; - /** - * @brief Does this song have a background image? - * @return true if it does, false otherwise. */ - bool HasBackground() const; - bool HasJacket() const; - bool HasCDImage() const; - bool HasDisc() const; - bool HasCDTitle() const; - //bool HasMovieBackground() const; - bool HasBGChanges() const; - bool HasLyrics() const; - bool HasAttacks() const; - bool HasPreviewVid() const; - - bool Matches(RString sGroup, RString sSong) const; - - /** @brief The Song's TimingData. */ - TimingData m_SongTiming; - - float GetFirstBeat() const; - float GetFirstSecond() const; - float GetLastBeat() const; - float GetLastSecond() const; - float GetSpecifiedLastBeat() const; - float GetSpecifiedLastSecond() const; - - void SetFirstSecond(const float f); - void SetLastSecond(const float f); - void SetSpecifiedLastSecond(const float f); - - typedef vector VBackgroundChange; -private: - /** @brief The first second that a note is hit. */ - float firstSecond; - /** @brief The last second that a note is hit. */ - float lastSecond; - /** @brief The last second of the song for playing purposes. */ - float specifiedLastSecond; - /** - * @brief The background changes (sorted by layer) that are for this Song. - * This uses an AutoPtr instead of a raw pointer so that the - * auto gen'd copy constructor works correctly. - * This must be sorted before gameplay. */ - AutoPtrCopyOnWrite m_BackgroundChanges[NUM_BackgroundLayer]; - /** - * @brief The foreground changes that are for this Song. - * This uses an AutoPtr instead of a raw pointer so that the - * auto gen'd copy constructor works correctly. - * This must be sorted before gameplay. */ - AutoPtrCopyOnWrite m_ForegroundChanges; - - vector GetChangesToVectorString(const vector & changes) const; -public: - const vector &GetBackgroundChanges( BackgroundLayer bl ) const; - vector &GetBackgroundChanges( BackgroundLayer bl ); - const vector &GetForegroundChanges() const; - vector &GetForegroundChanges(); - - vector GetBGChanges1ToVectorString() const; - vector GetBGChanges2ToVectorString() const; - vector GetFGChanges1ToVectorString() const; - - vector GetInstrumentTracksToVectorString() const; - - /** - * @brief The list of LyricSegments. - * This must be sorted before gameplay. */ - vector m_LyricSegments; - -/* [splittiming] - void AddBPMSegment( const BPMSegment &seg ) { m_Timing.AddBPMSegment( seg ); } - void AddStopSegment( const StopSegment &seg ) { m_Timing.AddStopSegment( seg ); } - void AddWarpSegment( const WarpSegment &seg ) { m_Timing.AddWarpSegment( seg ); } -*/ - void AddBackgroundChange( BackgroundLayer blLayer, BackgroundChange seg ); - void AddForegroundChange( BackgroundChange seg ); - void AddLyricSegment( LyricSegment seg ); - - void GetDisplayBpms( DisplayBpms &AddTo ) const; - const BackgroundChange &GetBackgroundAtBeat( BackgroundLayer iLayer, float fBeat ) const; - -/* [splittiming] - float GetBPMAtBeat( float fBeat ) const { return m_Timing.GetBPMAtBeat( fBeat ); } - void SetBPMAtBeat( float fBeat, float fBPM ) { m_Timing.SetBPMAtBeat( fBeat, fBPM ); } - BPMSegment& GetBPMSegmentAtBeat( float fBeat ) { return m_Timing.GetBPMSegmentAtBeat( fBeat ); } -*/ - - Steps *CreateSteps(); - void InitSteps(Steps *pSteps); - - /* [splittiming] - float SongGetBeatFromElapsedTime( float fElapsedTime ) const - { - return m_SongTiming.GetBeatFromElapsedTime( fElapsedTime ); - } - float StepsGetBeatFromElapsedTime( float fElapsedTime, const Steps &steps ) const - { - return steps.m_Timing.GetBeatFromElapsedTime( fElapsedTime ); - } - - float SongGetElapsedTimeFromBeat( float fBeat ) const - { - return m_SongTiming.GetElapsedTimeFromBeat( fBeat ); - } - float StepsGetElapsedTimeFromBeat( float fBeat, const Steps &steps ) const - { - return steps.m_Timing.GetElapsedTimeFromBeat( fBeat ); - } - */ - - /* [splittiming] - float GetBeatFromElapsedTime( float fElapsedTime ) const - { - return m_Timing.GetBeatFromElapsedTime( fElapsedTime ); - } - float GetElapsedTimeFromBeat( float fBeat ) const { return m_Timing.GetElapsedTimeFromBeat( fBeat ); } - */ - - bool HasSignificantBpmChangesOrStops() const; - float GetStepsSeconds() const; - bool IsLong() const; - bool IsMarathon() const; - - bool SongCompleteForStyle( const Style *st ) const; - bool HasStepsType( StepsType st ) const; - bool HasStepsTypeAndDifficulty( StepsType st, Difficulty dc ) const; - const vector& GetAllSteps() const { return m_vpSteps; } - const vector& GetStepsByStepsType( StepsType st ) const { return m_vpStepsByType[st]; } - bool IsEasy( StepsType st ) const; - bool IsTutorial() const; - bool HasEdits( StepsType st ) const; - - void SetEnabled( bool b ) { m_bEnabled = b; } - bool GetEnabled() const { return m_bEnabled; } - /** - * @brief Determine if the song should be shown on the MusicWheel normally. - * Songs that are not displayed normally may still be available during - * random selection, extra stages, or other special conditions. - * @return true if displayed normally, false otherwise. */ - bool NormallyDisplayed() const; - bool ShowInDemonstrationAndRanking() const; - - /** - * @brief Add the chosen Steps to the Song. - * We are responsible for deleting the memory pointed to by pSteps! - * @param pSteps the new steps. */ - void AddSteps( Steps* pSteps ); - void DeleteSteps( const Steps* pSteps, bool bReAutoGen = true ); - - void FreeAllLoadedFromProfile( ProfileSlot slot = ProfileSlot_Invalid, const set *setInUse = NULL ); - bool WasLoadedFromProfile() const { return m_LoadedFromProfile != ProfileSlot_Invalid; } - void GetStepsLoadedFromProfile( ProfileSlot slot, vector &vpStepsOut ) const; - int GetNumStepsLoadedFromProfile( ProfileSlot slot ) const; - bool IsEditAlreadyLoaded( Steps* pSteps ) const; - - bool IsStepsUsingDifferentTiming(Steps *pSteps ) const; - - /** - * @brief An array of keysound file names (e.g. "beep.wav"). - * The index in this array corresponds to the index in TapNote. - * If you change the index in here, you must change all NoteData too. - * Any note that doesn't have a value in the range of this array - * means "this note doesn't have a keysound". */ - vector m_vsKeysoundFile; - - CachedObject m_CachedObject; - - RString GetAttackString() const - { - return join(":", this->m_sAttackString); - } - - // Lua - void PushSelf( lua_State *L ); - -private: - /** @brief the Steps that belong to this Song. */ - vector m_vpSteps; - /** @brief the Steps of a particular StepsType that belong to this Song. */ - vector m_vpStepsByType[NUM_StepsType]; -}; - -#endif - -/** - * @file - * @author Chris Danford, Glenn Maynard (c) 2001-2004 - * @section LICENSE - * 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. - */ +#ifndef SONG_H +#define SONG_H + +#include "Attack.h" +#include "TimingData.h" +#include "Difficulty.h" +#include "EnumHelper.h" +#include "RageUtil_AutoPtr.h" +#include "RageUtil_CachedObject.h" +#include "RageTypes.h" +#include "Steps.h" +#include + +class Style; +class StepsID; +struct lua_State; +struct BackgroundChange; + +void FixupPath( RString &path, const RString &sSongPath ); +RString GetSongAssetPath( RString sPath, const RString &sSongPath ); + +/** @brief The version of the .ssc file format. */ +const static float STEPFILE_VERSION_NUMBER = 0.81f; + +/** @brief How many edits for this song can each profile have? */ +const int MAX_EDITS_PER_SONG_PER_PROFILE = 15; +/** @brief How many edits for this song can be available? */ +const int MAX_EDITS_PER_SONG = MAX_EDITS_PER_SONG_PER_PROFILE * NUM_ProfileSlot; + +extern const int FILE_CACHE_VERSION; + +/** @brief The different background layers available. */ +enum BackgroundLayer +{ + BACKGROUND_LAYER_1, + BACKGROUND_LAYER_2, + //BACKGROUND_LAYER_3, // StepCollection get + NUM_BackgroundLayer, + BACKGROUND_LAYER_Invalid +}; + +/** @brief A custom foreach loop for the different background layers. */ +#define FOREACH_BackgroundLayer( bl ) FOREACH_ENUM( BackgroundLayer, bl ) + +/** @brief Different instrument tracks for band type games. */ +enum InstrumentTrack +{ + InstrumentTrack_Guitar, + InstrumentTrack_Rhythm, + InstrumentTrack_Bass, + NUM_InstrumentTrack, + InstrumentTrack_Invalid +}; +const RString& InstrumentTrackToString( InstrumentTrack it ); +InstrumentTrack StringToInstrumentTrack( const RString& s ); + +/** @brief The collection of lyrics for the Song. */ +struct LyricSegment +{ + float m_fStartTime; /** @brief When does the lyric show up? */ + RString m_sLyric; /** @brief The lyrics themselves. */ + RageColor m_Color; /** @brief The color of the lyrics. */ +}; + +/** @brief Holds all music metadata and steps for one song. */ +class Song +{ + RString m_sSongDir; +public: + void SetSongDir( const RString sDir ) { m_sSongDir = sDir; } + RString GetSongDir() { return m_sSongDir; } + + /** @brief When should this song be displayed in the music wheel? */ + enum SelectionDisplay + { + SHOW_ALWAYS, /**< always show on the wheel. */ + SHOW_NEVER /**< never show on the wheel (unless song hiding is turned off). */ + } m_SelectionDisplay; + + Song(); + ~Song(); + void Reset(); + void DetachSteps(); + + /** + * @brief Load a song from the chosen directory. + * + * This assumes that there is no song present right now. + * @param sDir the song directory from which to load. */ + bool LoadFromSongDir( RString sDir ); + // This one takes the effort to reuse Steps pointers as best as it can + bool ReloadFromSongDir( RString sDir ); + + /** + * @brief Call this after loading a song to clean up invalid data. + * @param fromCache was this data loaded from the cache file? + * @param duringCache was this data loaded during the cache process? */ + void TidyUpData( bool fromCache = false, bool duringCache = false ); + + /** + * @brief Get the new radar values, and determine the last second at the same time. + * This is called by TidyUpData, after saving the Song. + * @param fromCache was this data loaded from the cache file? + * @param duringCache was this data loaded during the cache process? */ + void ReCalculateRadarValuesAndLastSecond(bool fromCache = false, bool duringCache = false); + /** + * @brief Translate any titles that aren't in english. + * This is called by TidyUpData. */ + void TranslateTitles(); + + /** + * @brief Save to the new SSC file format. + * @param sPath the path where we're saving the file. + * @param bSavingCache a flag to determine if we're saving cache data. + */ + bool SaveToSSCFile( RString sPath, bool bSavingCache ); + /** @brief Save to the SSC and SM files no matter what. */ + void Save(); + /** + * @brief Save the current Song to a JSON file. + * @return its success or failure. */ + bool SaveToJsonFile(); + /** + * @brief Save the current Song to a cache file using the preferred format. + * @return its success or failure. */ + bool SaveToCacheFile(); + /** + * @brief Save the current Song to a SM file. + * @return its success or failure. */ + bool SaveToSMFile(); + /** + * @brief Save the current Song to a DWI file if possible. + * @return its success or failure. */ + bool SaveToDWIFile(); + + const RString &GetSongFilePath() const; + RString GetCacheFilePath() const; + + void AddAutoGenNotes(); + /** + * @brief Automatically generate steps from one type to another. + * @param ntTo the StepsType we're making. + * @param ntFrom the StepsType we're generating from. + */ + void AutoGen( StepsType ntTo, StepsType ntFrom ); + void RemoveAutoGenNotes(); + + // Directory this song data came from: + const RString &GetSongDir() const { return m_sSongDir; } + + /** + * @brief Filename associated with this file. + * This will always have a .SSC extension. If we loaded a .SSC, this will + * point to it, but if we loaded any other type, this will point to + * a generated .SSC filename. */ + RString m_sSongFileName; + + /** @brief The group this Song is in. */ + RString m_sGroupName; + + /** + * @brief the Profile this came from. + * This is ProfileSlot_Invalid if it wasn't loaded from a profile. */ + ProfileSlot m_LoadedFromProfile; + /** @brief Is the song file itself a symlink to another file? */ + bool m_bIsSymLink; + bool m_bEnabled; + + /** @brief The title of the Song. */ + RString m_sMainTitle; + /** @brief The subtitle of the Song, if it exists. */ + RString m_sSubTitle; + /** @brief The artist of the Song, if it exists. */ + RString m_sArtist; + /** @brief The transliterated title of the Song, if it exists. */ + RString m_sMainTitleTranslit; + /** @brief The transliterated subtitle of the Song, if it exists. */ + RString m_sSubTitleTranslit; + /** @brief The transliterated artist of the Song, if it exists. */ + RString m_sArtistTranslit; + + /* If PREFSMAN->m_bShowNative is off, these are the same as GetTranslit* + * below. Otherwise, they return the main titles. */ + RString GetDisplayMainTitle() const; + RString GetDisplaySubTitle() const; + RString GetDisplayArtist() const; + + /** + * @brief Retrieve the transliterated title, or the main title if there is no translit. + * @return the proper title. */ + RString GetTranslitMainTitle() const + { + return m_sMainTitleTranslit.size()? m_sMainTitleTranslit: m_sMainTitle; + } + /** + * @brief Retrieve the transliterated subtitle, or the main subtitle if there is no translit. + * @return the proper subtitle. */ + RString GetTranslitSubTitle() const + { + return m_sSubTitleTranslit.size()? m_sSubTitleTranslit: m_sSubTitle; + } + /** + * @brief Retrieve the transliterated artist, or the main artist if there is no translit. + * @return the proper artist. */ + RString GetTranslitArtist() const + { + return m_sArtistTranslit.size()? m_sArtistTranslit:m_sArtist; + } + + // "title subtitle" + RString GetDisplayFullTitle() const; + RString GetTranslitFullTitle() const; + + /** @brief The version of the song/file. */ + float m_fVersion; + /** @brief The genre of the song/file. */ + RString m_sGenre; + + /** + * @brief The person who worked with the song file who should be credited. + * This is read and saved, but never actually used. */ + RString m_sCredit; + + RString m_sOrigin; // song origin (for .ssc format) + + RString m_sMusicFile; + RString m_sInstrumentTrackFile[NUM_InstrumentTrack]; + + /** @brief The length of the music file. */ + float m_fMusicLengthSeconds; + float m_fMusicSampleStartSeconds; + float m_fMusicSampleLengthSeconds; + DisplayBPM m_DisplayBPMType; + float m_fSpecifiedBPMMin; + float m_fSpecifiedBPMMax; // if a range, then Min != Max + + RString m_sBannerFile; // typically a 16:5 ratio graphic (e.g. 256x80) + RString m_sJacketFile; // typically square (e.g. 192x192, 256x256) + RString m_sCDFile; // square (e.g. 128x128 [DDR 1st-3rd]) + RString m_sDiscFile; // rectangular (e.g. 256x192 [Pump], 200x150 [MGD3]) + /** @brief The location of the lyrics file, if it exists. */ + RString m_sLyricsFile; + RString m_sBackgroundFile; + RString m_sCDTitleFile; + RString m_sPreviewVidFile; + + AttackArray m_Attacks; + vector m_sAttackString; + + RString GetMusicPath() const; + RString GetInstrumentTrackPath( InstrumentTrack it ) const; + RString GetBannerPath() const; + RString GetJacketPath() const; + RString GetCDImagePath() const; + RString GetDiscPath() const; + RString GetLyricsPath() const; + RString GetBackgroundPath() const; + RString GetCDTitlePath() const; + RString GetPreviewVidPath() const; + + // For loading only: + bool m_bHasMusic, m_bHasBanner, m_bHasBackground; + + bool HasMusic() const; + bool HasInstrumentTrack( InstrumentTrack it ) const; + /** + * @brief Does this song have a banner? + * @return true if it does, false otherwise. */ + bool HasBanner() const; + /** + * @brief Does this song have a background image? + * @return true if it does, false otherwise. */ + bool HasBackground() const; + bool HasJacket() const; + bool HasCDImage() const; + bool HasDisc() const; + bool HasCDTitle() const; + //bool HasMovieBackground() const; + bool HasBGChanges() const; + bool HasLyrics() const; + bool HasAttacks() const; + bool HasPreviewVid() const; + + bool Matches(RString sGroup, RString sSong) const; + + /** @brief The Song's TimingData. */ + TimingData m_SongTiming; + + float GetFirstBeat() const; + float GetFirstSecond() const; + float GetLastBeat() const; + float GetLastSecond() const; + float GetSpecifiedLastBeat() const; + float GetSpecifiedLastSecond() const; + + void SetFirstSecond(const float f); + void SetLastSecond(const float f); + void SetSpecifiedLastSecond(const float f); + + typedef vector VBackgroundChange; +private: + /** @brief The first second that a note is hit. */ + float firstSecond; + /** @brief The last second that a note is hit. */ + float lastSecond; + /** @brief The last second of the song for playing purposes. */ + float specifiedLastSecond; + /** + * @brief The background changes (sorted by layer) that are for this Song. + * This uses an AutoPtr instead of a raw pointer so that the + * auto gen'd copy constructor works correctly. + * This must be sorted before gameplay. */ + AutoPtrCopyOnWrite m_BackgroundChanges[NUM_BackgroundLayer]; + /** + * @brief The foreground changes that are for this Song. + * This uses an AutoPtr instead of a raw pointer so that the + * auto gen'd copy constructor works correctly. + * This must be sorted before gameplay. */ + AutoPtrCopyOnWrite m_ForegroundChanges; + + vector GetChangesToVectorString(const vector & changes) const; +public: + const vector &GetBackgroundChanges( BackgroundLayer bl ) const; + vector &GetBackgroundChanges( BackgroundLayer bl ); + const vector &GetForegroundChanges() const; + vector &GetForegroundChanges(); + + vector GetBGChanges1ToVectorString() const; + vector GetBGChanges2ToVectorString() const; + vector GetFGChanges1ToVectorString() const; + + vector GetInstrumentTracksToVectorString() const; + + /** + * @brief The list of LyricSegments. + * This must be sorted before gameplay. */ + vector m_LyricSegments; + +/* [splittiming] + void AddBPMSegment( const BPMSegment &seg ) { m_Timing.AddBPMSegment( seg ); } + void AddStopSegment( const StopSegment &seg ) { m_Timing.AddStopSegment( seg ); } + void AddWarpSegment( const WarpSegment &seg ) { m_Timing.AddWarpSegment( seg ); } +*/ + void AddBackgroundChange( BackgroundLayer blLayer, BackgroundChange seg ); + void AddForegroundChange( BackgroundChange seg ); + void AddLyricSegment( LyricSegment seg ); + + void GetDisplayBpms( DisplayBpms &AddTo ) const; + const BackgroundChange &GetBackgroundAtBeat( BackgroundLayer iLayer, float fBeat ) const; + +/* [splittiming] + float GetBPMAtBeat( float fBeat ) const { return m_Timing.GetBPMAtBeat( fBeat ); } + void SetBPMAtBeat( float fBeat, float fBPM ) { m_Timing.SetBPMAtBeat( fBeat, fBPM ); } + BPMSegment& GetBPMSegmentAtBeat( float fBeat ) { return m_Timing.GetBPMSegmentAtBeat( fBeat ); } +*/ + + Steps *CreateSteps(); + void InitSteps(Steps *pSteps); + + /* [splittiming] + float SongGetBeatFromElapsedTime( float fElapsedTime ) const + { + return m_SongTiming.GetBeatFromElapsedTime( fElapsedTime ); + } + float StepsGetBeatFromElapsedTime( float fElapsedTime, const Steps &steps ) const + { + return steps.m_Timing.GetBeatFromElapsedTime( fElapsedTime ); + } + + float SongGetElapsedTimeFromBeat( float fBeat ) const + { + return m_SongTiming.GetElapsedTimeFromBeat( fBeat ); + } + float StepsGetElapsedTimeFromBeat( float fBeat, const Steps &steps ) const + { + return steps.m_Timing.GetElapsedTimeFromBeat( fBeat ); + } + */ + + /* [splittiming] + float GetBeatFromElapsedTime( float fElapsedTime ) const + { + return m_Timing.GetBeatFromElapsedTime( fElapsedTime ); + } + float GetElapsedTimeFromBeat( float fBeat ) const { return m_Timing.GetElapsedTimeFromBeat( fBeat ); } + */ + + bool HasSignificantBpmChangesOrStops() const; + float GetStepsSeconds() const; + bool IsLong() const; + bool IsMarathon() const; + + bool SongCompleteForStyle( const Style *st ) const; + bool HasStepsType( StepsType st ) const; + bool HasStepsTypeAndDifficulty( StepsType st, Difficulty dc ) const; + const vector& GetAllSteps() const { return m_vpSteps; } + const vector& GetStepsByStepsType( StepsType st ) const { return m_vpStepsByType[st]; } + bool IsEasy( StepsType st ) const; + bool IsTutorial() const; + bool HasEdits( StepsType st ) const; + + void SetEnabled( bool b ) { m_bEnabled = b; } + bool GetEnabled() const { return m_bEnabled; } + /** + * @brief Determine if the song should be shown on the MusicWheel normally. + * Songs that are not displayed normally may still be available during + * random selection, extra stages, or other special conditions. + * @return true if displayed normally, false otherwise. */ + bool NormallyDisplayed() const; + bool ShowInDemonstrationAndRanking() const; + + /** + * @brief Add the chosen Steps to the Song. + * We are responsible for deleting the memory pointed to by pSteps! + * @param pSteps the new steps. */ + void AddSteps( Steps* pSteps ); + void DeleteSteps( const Steps* pSteps, bool bReAutoGen = true ); + + void FreeAllLoadedFromProfile( ProfileSlot slot = ProfileSlot_Invalid, const set *setInUse = NULL ); + bool WasLoadedFromProfile() const { return m_LoadedFromProfile != ProfileSlot_Invalid; } + void GetStepsLoadedFromProfile( ProfileSlot slot, vector &vpStepsOut ) const; + int GetNumStepsLoadedFromProfile( ProfileSlot slot ) const; + bool IsEditAlreadyLoaded( Steps* pSteps ) const; + + bool IsStepsUsingDifferentTiming(Steps *pSteps ) const; + + /** + * @brief An array of keysound file names (e.g. "beep.wav"). + * The index in this array corresponds to the index in TapNote. + * If you change the index in here, you must change all NoteData too. + * Any note that doesn't have a value in the range of this array + * means "this note doesn't have a keysound". */ + vector m_vsKeysoundFile; + + CachedObject m_CachedObject; + + RString GetAttackString() const + { + return join(":", this->m_sAttackString); + } + + // Lua + void PushSelf( lua_State *L ); + +private: + /** @brief the Steps that belong to this Song. */ + vector m_vpSteps; + /** @brief the Steps of a particular StepsType that belong to this Song. */ + vector m_vpStepsByType[NUM_StepsType]; +}; + +#endif + +/** + * @file + * @author Chris Danford, Glenn Maynard (c) 2001-2004 + * @section LICENSE + * 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. + */