Files
itgmania212121/src/Song.cpp
T
Devin J. Pohly b98e5e6dba Bump cache version for XML changes
The XML patchset includes more items in the FGCHANGES line, which is
stored in cache.  Previous caches wouldn't include these entries.

Sorry 'bout that folks.
2014-02-20 15:32:58 -05:00

2093 lines
60 KiB
C++

#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 "NotesWriterJson.h"
#include "NotesWriterSM.h"
#include "NotesWriterSSC.h"
#include "UnlockManager.h"
#include "LyricsLoader.h"
#include <time.h>
#include <set>
#include <float.h>
/**
* @brief The internal version of the cache for StepMania.
*
* Increment this value to invalidate the current cache. */
const int FILE_CACHE_VERSION = 216;
/** @brief How long does a song sample last by default? */
const float DEFAULT_MUSIC_SAMPLE_LENGTH = 12.f;
static Preference<float> g_fLongVerSongSeconds( "LongVerSongSeconds", 60*2.5f );
static Preference<float> g_fMarathonVerSongSeconds( "MarathonVerSongSeconds", 60*5.f );
static Preference<bool> g_BackUpAllSongSaves( "BackUpAllSongSaves", false );
static const char *InstrumentTrackNames[] = {
"Guitar",
"Rhythm",
"Bass",
};
XToString( InstrumentTrack );
StringToX( InstrumentTrack );
Song::Song()
{
FOREACH_BackgroundLayer( i )
m_BackgroundChanges[i] = AutoPtrCopyOnWrite<VBackgroundChange>(new VBackgroundChange);
m_ForegroundChanges = AutoPtrCopyOnWrite<VBackgroundChange>(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(this);
InitSteps( pSteps );
return pSteps;
}
void Song::InitSteps(Steps *pSteps)
{
// TimingData is initially empty (i.e. defaults to song timing)
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<GetBackgroundChanges(iLayer).size()-1; i++ )
if( GetBackgroundChanges(iLayer)[i+1].m_fStartBeat > 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
* <set> into Song.h, which is heavily used. */
static set<RString> 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<RString> 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<RString> 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<RString> 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() );
*/
// Load any .edit files in the song folder.
// Doing this BEFORE setting up AutoGen just in case.
vector<RString> vs;
GetDirListing( sDir + "*.edit", vs, false, false);
// XXX: I'm sure there's a StepMania way of doing this, but familiar with this codebase I am not.
for(unsigned int i = 0; i < vs.size(); ++i) {
// Try SSCLoader
SSCLoader ldSSC;
if( ldSSC.LoadEditFromFile(sDir + vs[i], ProfileSlot_Invalid, true, this) != true )
{
// No dice? Try SMLoader then. If SMLoader fails too, well whatever.
// We don't have to do anything to fail gracefully.
SMLoader ldSM;
ldSM.LoadEditFromFile(sDir + vs[i], ProfileSlot_Invalid, true, this);
}
}
// Note: If vs.empty() then this loop is skipped entirely (vs.size() == 0)
// 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
}
/* This function feels EXTREMELY hacky - copying things on top of pointers so
* they don't break elsewhere. Maybe it could be rewritten to politely ask the
* Song/Steps objects to reload themselves. -- djpohly */
bool Song::ReloadFromSongDir( RString sDir )
{
RemoveAutoGenNotes();
vector<Steps*> vOldSteps = m_vpSteps;
Song copy;
if( !copy.LoadFromSongDir( sDir ) )
return false;
copy.RemoveAutoGenNotes();
*this = copy;
/* Go through the steps, first setting their Song pointer to this song
* (instead of the copy used above), and constructing a map to let us
* easily find the new steps. */
map<StepsID, Steps*> mNewSteps;
for( vector<Steps*>::const_iterator it = m_vpSteps.begin(); it != m_vpSteps.end(); ++it )
{
(*it)->m_pSong = this;
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<Steps*>::const_iterator itOld = vOldSteps.begin(); itOld != vOldSteps.end(); ++itOld )
{
StepsID id;
id.FromSteps( *itOld );
map<StepsID, Steps*>::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<StepsID, Steps*>::const_iterator it = mNewSteps.begin(); it != mNewSteps.end(); ++it )
{
Steps *NewSteps = new Steps(this);
*NewSteps = *(it->second);
AddSteps( NewSteps );
}
AddAutoGenNotes();
return true;
}
static void GetImageDirListing( RString sPath, vector<RString> &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 (this->m_sArtist == "The Dancing Monkeys Project" && this->m_sMainTitle.find_first_of('-') != string::npos)
{
// Dancing Monkeys had a bug/feature where the artist was replaced. Restore it.
vector<RString> titleParts;
split(this->m_sMainTitle, "-", titleParts);
this->m_sArtist = titleParts.front();
Trim(this->m_sArtist);
titleParts.erase(titleParts.begin());
this->m_sMainTitle = join("-", titleParts);
Trim(this->m_sMainTitle);
}
if( !HasMusic() )
{
vector<RString> 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( false );
FOREACH( Steps *, m_vpSteps, s )
{
(*s)->m_Timing.TidyUpData( true );
}
/* 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<RString> 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<RString> 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<RString> 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<RString> 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<RString> 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<RString> 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<RString> 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<RString> arrayImages;
GetImageDirListing( m_sSongDir + "*", arrayImages );
for( unsigned i=0; i<arrayImages.size(); i++ ) // foreach image
{
if( HasBanner() && HasCDTitle() && HasBackground() )
break; // done
// ignore DWI "-char" graphics
RString sLower = arrayImages[i];
sLower.MakeLower();
if( BlacklistedImages.find(sLower) != BlacklistedImages.end() )
continue; // skip
// Skip any image that we've already classified
if( HasBanner() && m_sBannerFile.EqualsNoCase(arrayImages[i]) )
continue; // skip
if( HasBackground() && m_sBackgroundFile.EqualsNoCase(arrayImages[i]) )
continue; // skip
if( HasCDTitle() && m_sCDTitleFile.EqualsNoCase(arrayImages[i]) )
continue; // skip
if( HasJacket() && m_sJacketFile.EqualsNoCase(arrayImages[i]) )
continue; // skip
if( HasDisc() && m_sDiscFile.EqualsNoCase(arrayImages[i]) )
continue; // skip
if( HasCDImage() && m_sCDFile.EqualsNoCase(arrayImages[i]) )
continue; // skip
RString sPath = m_sSongDir + arrayImages[i];
// We only care about the dimensions.
RString error;
RageSurface *img = RageSurfaceUtils::LoadFile( sPath, error, true );
if( !img )
{
LOG->UserLog( "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<RString> 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 );
GetDirListing( m_sSongDir + RString("*.mp4"), arrayPossibleMovies );
GetDirListing( m_sSongDir + RString("*.mkv"), arrayPossibleMovies );
GetDirListing( m_sSongDir + RString("*.flv"), arrayPossibleMovies );
GetDirListing( m_sSongDir + RString("*.f4v"), arrayPossibleMovies );
GetDirListing( m_sSongDir + RString("*.mov"), 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; i<m_vpSteps.size(); i++ )
m_vpSteps[i]->CalculateRadarValues( 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; i<m_vpSteps.size(); i++ )
{
Steps* pSteps = m_vpSteps[i];
pSteps->CalculateRadarValues( m_fMusicLengthSeconds );
// Must initialize before the gotos.
NoteData tempNoteData;
pSteps->GetNoteData( tempNoteData );
// calculate lastSecond
/* 1. If it's autogen, then first/last beat will come from the parent.
* 2. 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->IsAutogen() &&
!( pSteps->IsAnEdit() && m_vpSteps.size() > 1 ) )
{
// 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->GetTimingData()->GetElapsedTimeFromBeat(tempNoteData.GetFirstBeat()));
localLast = max(localLast,
pSteps->GetTimingData()->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<RString> arrayOldFileNames;
GetDirListing( m_sSongDir + "*.bms", arrayOldFileNames );
GetDirListing( m_sSongDir + "*.pms", arrayOldFileNames );
GetDirListing( m_sSongDir + "*.ksf", arrayOldFileNames );
for( unsigned i=0; i<arrayOldFileNames.size(); i++ )
{
const RString sOldPath = m_sSongDir + arrayOldFileNames[i];
const RString sNewPath = sOldPath + ".old";
if( !FileCopy( sOldPath, sNewPath ) )
{
LOG->UserLog( "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<Steps*> 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<Steps*> 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; j<m_vpSteps.size(); j++ )
{
const Steps* pOriginalNotes = m_vpSteps[j];
if( pOriginalNotes->m_StepsType == ntFrom )
{
Steps* pNewNotes = new Steps(this);
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; i<m_vpSteps.size(); i++ )
{
Steps* pSteps = m_vpSteps[i];
if( pSteps->m_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<BackgroundChange> &Song::GetBackgroundChanges( BackgroundLayer bl ) const
{
return *(m_BackgroundChanges[bl]);
}
vector<BackgroundChange> &Song::GetBackgroundChanges( BackgroundLayer bl )
{
return *(m_BackgroundChanges[bl].Get());
}
const vector<BackgroundChange> &Song::GetForegroundChanges() const
{
return *m_ForegroundChanges;
}
vector<BackgroundChange> &Song::GetForegroundChanges()
{
return *m_ForegroundChanges.Get();
}
vector<RString> Song::GetChangesToVectorString(const vector<BackgroundChange> & changes) const
{
vector<RString> ret;
FOREACH_CONST( BackgroundChange, changes, bgc )
{
ret.push_back((*bgc).ToString());
}
return ret;
}
vector<RString> Song::GetBGChanges1ToVectorString() const
{
return this->GetChangesToVectorString(this->GetBackgroundChanges(BACKGROUND_LAYER_1));
}
vector<RString> Song::GetBGChanges2ToVectorString() const
{
return this->GetChangesToVectorString(this->GetBackgroundChanges(BACKGROUND_LAYER_2));
}
vector<RString> Song::GetFGChanges1ToVectorString() const
{
return this->GetChangesToVectorString(this->GetForegroundChanges());
}
vector<RString> Song::GetInstrumentTracksToVectorString() const
{
vector<RString> 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();
RString sRelPath = sSongPath + sPath;
if( DoesFileExist(sRelPath) )
return sRelPath;
/* 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 sRelPath;
// 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 = sRelPath;
}
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<Steps*> &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<RString> 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<Steps*> *setInUse )
{
/* DeleteSteps will remove and recreate autogen notes, which may reorder
* m_vpSteps, so be careful not to skip over entries. */
vector<Steps*> 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<Steps*> &vpStepsOut ) const
{
for( unsigned s=0; s<m_vpSteps.size(); s++ )
{
Steps* pSteps = m_vpSteps[s];
if( pSteps->GetLoadedFromProfileSlot() == slot )
vpStepsOut.push_back( pSteps );
}
}
int Song::GetNumStepsLoadedFromProfile( ProfileSlot slot ) const
{
int iCount = 0;
for( unsigned s=0; s<m_vpSteps.size(); s++ )
{
Steps* pSteps = m_vpSteps[s];
if( pSteps->GetLoadedFromProfileSlot() == 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; i<m_vpSteps.size(); i++ )
{
Steps* pOther = m_vpSteps[i];
if( pOther->GetDifficulty() == Difficulty_Edit &&
pOther->m_StepsType == pSteps->m_StepsType &&
pOther->GetHash() == pSteps->GetHash() )
{
return true;
}
}
return false;
}
bool Song::IsStepsUsingDifferentTiming(Steps *pSteps) const
{
// XXX This no longer depends on Song at all
return !pSteps->m_Timing.empty();
}
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<Song>
{
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<Steps*> &v = p->GetAllSteps();
LuaHelpers::CreateTableFromArray<Steps*>( v, L );
return 1;
}
static int GetStepsByStepsType( T* p, lua_State *L )
{
StepsType st = Enum::Check<StepsType>(L, 1);
const vector<Steps*> &v = p->GetStepsByStepsType( st );
LuaHelpers::CreateTableFromArray<Steps*>( 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<StepsType>(L, 1);
lua_pushboolean( L, p->HasStepsType(st) );
return 1;
}
static int HasStepsTypeAndDifficulty( T* p, lua_State *L )
{
StepsType st = Enum::Check<StepsType>(L, 1);
Difficulty dc = Enum::Check<Difficulty>( 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<Steps>::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<StepsType>(L, 1);
Difficulty dc = Enum::Check<Difficulty>( 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<StepsType>(L, 1);
lua_pushboolean(L, p->HasEdits( st ));
return 1;
}
static int IsEasy( T* p, lua_State *L )
{
StepsType st = Enum::Check<StepsType>(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<float> 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.
*/