#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 = 167; /** @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; m_fFirstBeat = -1; m_fLastBeat = -1; m_fSpecifiedLastBeat = -1; m_SelectionDisplay = SHOW_ALWAYS; m_bEnabled = true; m_DisplayBPMType = DISPLAY_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(); } // 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 ); GetBackgroundChanges(iLayer).push_back( seg ); BackgroundUtil::SortBackgroundChangesArray( GetBackgroundChanges(iLayer) ); } void Song::AddForegroundChange( BackgroundChange seg ) { GetForegroundChanges().push_back( seg ); BackgroundUtil::SortBackgroundChangesArray( GetForegroundChanges() ); } void Song::AddLyricSegment( LyricSegment seg ) { m_LyricSegments.push_back( seg ); } void Song::GetDisplayBpms( DisplayBpms &AddTo ) const { if( m_DisplayBPMType == DISPLAY_SPECIFIED ) { AddTo.Add( m_fSpecifiedBPMMin ); AddTo.Add( m_fSpecifiedBPMMax ); } else { float fMinBPM, fMaxBPM; m_Timing.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_sSongFileName.empty() ); 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( sDir != "" ); // 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() ); bool bLoadedFromSSC = SSCLoader::LoadFromSSCFile( sCacheFilePath, *this, true ); if( bLoadedFromSSC ) SSCLoader::TidyUpData( *this, true ); else { // load from .sm SMLoader::LoadFromSMFile( sCacheFilePath, *this, true ); SMLoader::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 SM, 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; } // Continue on with a blank Song so that people can make adjustments using the editor. } TidyUpData(); // 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 ) { (*s)->SetFilename( sCacheFilePath ); /* 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 + ".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() { // 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_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; } /* Generate these before we autogen notes, so the new notes can inherit * their source's values. */ ReCalculateRadarValuesAndLastBeat(); 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_Timing.m_BPMSegments.empty() ) { LOG->UserLog( "Song file", m_sSongDir + m_sSongFileName, "has no BPM segments, default provided." ); m_Timing.AddBPMSegment( BPMSegment(0, 60) ); } // Make sure the first BPM segment starts at beat 0. if( m_Timing.m_BPMSegments[0].m_iStartRow != 0 ) m_Timing.m_BPMSegments[0].m_iStartRow = 0; if( m_fMusicSampleStartSeconds == -1 || m_fMusicSampleStartSeconds == 0 || m_fMusicSampleStartSeconds+m_fMusicSampleLengthSeconds > this->m_fMusicLengthSeconds ) { m_fMusicSampleStartSeconds = this->GetElapsedTimeFromBeat( 100 ); if( m_fMusicSampleStartSeconds+m_fMusicSampleLengthSeconds > this->m_fMusicLengthSeconds ) { int iBeat = lrintf( m_fLastBeat/2 ); iBeat -= iBeat%4; m_fMusicSampleStartSeconds = this->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..." ); // 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 + "*bg*", arrayPossibleBGs ); GetImageDirListing( m_sSongDir + "*background*", 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. } */ 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() && Sprite::IsDiagonalBanner(width, height) ) { m_sBannerFile = 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; } // todo: add checks for Jacket, Disc, and CDImage -aj } // 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() ) { 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. */ SongUtil::AdjustDuplicateSteps( this ); { /* Generated filename; this doesn't always point to a loadable file, * but instead points to the file we should write changed files to, * and will always be a .SSC. * * This is a little tricky. We can't always use the song title directly, * since it might contain characters we can't store in filenames. Two * easy options: we could manually filter out invalid characters, or we * could use the name of the directory, which is always a valid filename * and should always be the same as the song. The former might not catch * everything--filename restrictions are platform-specific; we might even * be on an 8.3 filesystem, so let's do the latter. * * We can't rely on searching for other data filenames; it works for DWIs, * but not KSFs and BMSs. * * So, let's do this (by priority): * 1. If there's a .SSC file, use that filename. No reason to use anything * else; it's the filename in use. * 2. If there's an .SM, use it with a changed extension. * 3. If there's a .DWI, use it with a changed extension. * 4. Otherwise, use the name of the directory, since it's definitely a valid * filename, and should always be the title of the song (unlike KSFs). */ m_sSongFileName = m_sSongDir; vector asFileNames; do { GetDirListing( m_sSongDir+"*.ssc", asFileNames ); if( !asFileNames.empty() ) { m_sSongFileName += asFileNames[0]; break; } GetDirListing( m_sSongDir+"*.sma", asFileNames ); if (!asFileNames.empty() ) { m_sSongFileName += SetExtension( asFileNames[0], "ssc" ); break; } GetDirListing( m_sSongDir+"*.sm", asFileNames ); if( !asFileNames.empty() ) { m_sSongFileName += SetExtension( asFileNames[0], "ssc" ); break; } GetDirListing( m_sSongDir+"*.dwi", asFileNames ); if( !asFileNames.empty() ) { m_sSongFileName += SetExtension( asFileNames[0], "ssc" ); break; } m_sSongFileName += Basename(m_sSongDir); m_sSongFileName += ".ssc"; } while(0); } // If no time signature specified, assume 4/4 time for the whole song. if( m_Timing.m_vTimeSignatureSegments.empty() ) { TimeSignatureSegment seg; seg.m_iStartRow = 0; seg.m_iNumerator = 4; seg.m_iDenominator = 4; m_Timing.m_vTimeSignatureSegments.push_back( seg ); } /* * Likewise, if no tickcount signature is specified, assume 2 ticks * per beat for the entire song. The default of 2 is chosen more * for compatibility with the Pump Pro series than anything else. */ if( m_Timing.m_TickcountSegments.empty() ) { TickcountSegment seg; seg.m_iStartRow = 0; seg.m_iTicks = 2; m_Timing.m_TickcountSegments.push_back( seg ); } // Have a default combo segment of one just in case. if( m_Timing.m_ComboSegments.empty() ) { ComboSegment seg; seg.m_iStartRow = 0; seg.m_iCombo = 1; m_Timing.m_ComboSegments.push_back( seg ); } } 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::ReCalculateRadarValuesAndLastBeat() { float fFirstBeat = FLT_MAX; // inf float fLastBeat = m_fSpecifiedLastBeat; // Make sure we're at least as long as the specified amount. for( unsigned i=0; iCalculateRadarValues( m_fMusicLengthSeconds ); // calculate lastBeat // If it's autogen, then first/last beat will come from the parent. if( pSteps->IsAutogen() ) continue; /* 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 ) continue; // 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; NoteData tempNoteData; pSteps->GetNoteData( tempNoteData ); /* 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 ) continue; fFirstBeat = min( fFirstBeat, tempNoteData.GetFirstBeat() ); fLastBeat = max( fLastBeat, tempNoteData.GetLastBeat() ); } m_fFirstBeat = fFirstBeat; m_fLastBeat = fLastBeat; } // 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()" ); ReCalculateRadarValuesAndLastBeat(); 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" ); return NotesWriterSM::Write( sPath, *this ); } bool Song::SaveToSSCFile( RString sPath, bool bSavingCache ) { LOG->Trace( "Song::SaveToSSCFile('%s')", sPath.c_str() ); // If the file exists, make a backup. if( !bSavingCache && 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 ); } if( !NotesWriterSSC::Write(sPath, *this, vpStepsToSave, bSavingCache) ) return false; if( !bSavingCache && g_BackUpAllSongSaves.Get() ) { RString sExt = GetExtension( sPath ); RString sBackupFile = SetExtension( sPath, "" ); 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(sPath, sBackupFile) ) LOG->Trace( "Backed up %s to %s", sPath.c_str(), sBackupFile.c_str() ); else LOG->Trace( "Failed to back up %s to %s", sPath.c_str(), sBackupFile.c_str() ); } if( !bSavingCache ) { // 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(); if( !SaveToSSCFile(sPath, true) ) return false; FOREACH( Steps*, m_vpSteps, pSteps ) (*pSteps)->SetFilename( sPath ); return 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; } /* Return false if the song should not be displayed for selection in normal * gameplay (but may still be available in random selection, during extra * stages, or in other special conditions). */ 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(); } // todo: add checks for Jacket, Disc, and CDImage -aj 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(); } 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 ); } // todo: add checks for Jacket, Disc, and CDImage -aj 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( pSteps->GetDifficulty() == Difficulty_Edit ); for( unsigned i=0; iGetDifficulty() == Difficulty_Edit && pOther->m_StepsType == pSteps->m_StepsType && pOther->GetHash() == pSteps->GetHash() ) { return true; } } return false; } bool Song::HasSignificantBpmChangesOrStops() const { if( m_Timing.HasStops() ) return true; // Don't consider BPM changes that only are only for maintaining sync as // a real BpmChange. if( m_DisplayBPMType == DISPLAY_SPECIFIED ) { if( m_fSpecifiedBPMMin != m_fSpecifiedBPMMax ) return true; } else if( m_Timing.HasBpmChanges() ) { return true; } return false; } float Song::GetStepsSeconds() const { return GetElapsedTimeFromBeat( m_fLastBeat ) - GetElapsedTimeFromBeat( m_fFirstBeat ); } 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 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() ) return 0; lua_pushstring(L, s); return 1; } static int GetBannerPath( T* p, lua_State *L ) { RString s = p->GetBannerPath(); if( s.empty() ) return 0; LuaHelpers::Push(L, s); return 1; } static int GetBackgroundPath( T* p, lua_State *L ) { RString s = p->GetBackgroundPath(); if( s.empty() ) return 0; lua_pushstring(L, s); return 1; } static int GetCDTitlePath( T* p, lua_State *L ) { RString s = p->GetCDTitlePath(); if( s.empty() ) return 0; LuaHelpers::Push(L, s); return 1; } static int GetLyricsPath( T* p, lua_State *L ) { RString s = p->GetLyricsPath(); if( s.empty() ) return 0; lua_pushstring(L, s); 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 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; } // 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_Timing.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 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 GetFirstBeat( T* p, lua_State *L ) { lua_pushnumber(L, p->m_fFirstBeat); return 1; } static int GetLastBeat( T* p, lua_State *L ) { lua_pushnumber(L, p->m_fLastBeat); 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 == Song::DISPLAY_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( GetAllSteps ); ADD_METHOD( GetStepsByStepsType ); ADD_METHOD( GetSongDir ); ADD_METHOD( GetMusicPath ); ADD_METHOD( GetBannerPath ); ADD_METHOD( GetBackgroundPath ); ADD_METHOD( GetCDTitlePath ); ADD_METHOD( GetLyricsPath ); ADD_METHOD( GetSongFilePath ); ADD_METHOD( IsTutorial ); ADD_METHOD( IsEnabled ); ADD_METHOD( GetGroupName ); ADD_METHOD( MusicLengthSeconds ); 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( 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( GetLastBeat ); ADD_METHOD( HasAttacks ); ADD_METHOD( GetDisplayBpms ); ADD_METHOD( IsDisplayBpmSecret ); ADD_METHOD( IsDisplayBpmConstant ); ADD_METHOD( IsDisplayBpmRandom ); } }; 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. */