From a2798e16f414dcd5a4ad537bf8579b3d55dbe552 Mon Sep 17 00:00:00 2001 From: Kyzentun Date: Mon, 23 Mar 2015 11:35:30 -0600 Subject: [PATCH] More aggressive caching. In general, this makes Stepmania avoid looking in the song directory at all if the song is in the cache, in order to load songs faster. All the RadarValue calculations were also rolled into one to reduce the time needed to build the cache. --- src/GameConstantsAndTypes.h | 2 + src/NoteDataUtil.cpp | 354 +++++++++++++------------ src/NoteDataUtil.h | 21 -- src/RageTimer.cpp | 5 + src/RageTimer.h | 6 +- src/RageUtil.cpp | 41 +++ src/RageUtil.h | 5 + src/ScreenEdit.cpp | 13 +- src/Song.cpp | 511 ++++++++++++++++++------------------ src/Song.h | 1 + 10 files changed, 502 insertions(+), 457 deletions(-) diff --git a/src/GameConstantsAndTypes.h b/src/GameConstantsAndTypes.h index 900359148a..614b6f6c9e 100644 --- a/src/GameConstantsAndTypes.h +++ b/src/GameConstantsAndTypes.h @@ -41,6 +41,8 @@ enum RadarCategory RadarCategory_Rolls, /**< How many rolls are in the song? */ RadarCategory_Lifts, /**< How many lifts are in the song? */ RadarCategory_Fakes, /**< How many fakes are in the song? */ + // If you add another radar category, make sure you update + // NoteDataUtil::CalculateRadarValues to calculate it. -Kyz NUM_RadarCategory, /**< The number of radar categories. */ RadarCategory_Invalid }; diff --git a/src/NoteDataUtil.cpp b/src/NoteDataUtil.cpp index 3d7a54ea0f..862ef6cc79 100644 --- a/src/NoteDataUtil.cpp +++ b/src/NoteDataUtil.cpp @@ -951,194 +951,192 @@ void NoteDataUtil::AutogenKickbox(const NoteData& in, NoteData& out, const Timin out.RevalidateATIs(vector(), false); } -RadarStats CalculateRadarStatsFast( const NoteData &in, RadarStats &out ) +struct recent_note_t { - out.taps = 0; - out.jumps = 0; - out.hands = 0; - out.quads = 0; - map simultaneousMap; - map simultaneousMapNoHold; - map simultaneousMapTapHoldHead; - map::iterator itr; - for( int t=0; tGetProcessedTimingData()->IsJudgableAtRow(r)) - continue; - - const TapNote &tn = in.GetTapNote(t, r); - switch( tn.type ) - { - case TapNoteType_Mine: - case TapNoteType_Empty: - case TapNoteType_Fake: - case TapNoteType_AutoKeysound: - continue; // skip these types - they don't count - default: break; - } - - if( (itr = simultaneousMap.find(r)) == simultaneousMap.end() ) - simultaneousMap[r] = 1; - else - itr->second++; - - if( (itr = simultaneousMapNoHold.find(r)) == simultaneousMapNoHold.end() ) - simultaneousMapNoHold[r] = 1; - else - itr->second++; - - if( tn.type == TapNoteType_Tap || tn.type == TapNoteType_Lift || tn.type == TapNoteType_HoldHead ) - { - simultaneousMapTapHoldHead[r] = 1; - } - - if( tn.type == TapNoteType_HoldHead ) - { - int searchStartRow = r + 1; - int searchEndRow = r + tn.iDuration; - FOREACH_NONEMPTY_ROW_ALL_TRACKS_RANGE( in, rr, searchStartRow, searchEndRow ) - { - switch( in.GetTapNote(t, rr).type ) - { - case TapNoteType_Mine: - case TapNoteType_Empty: - case TapNoteType_Fake: - continue; // skip these types - they don't count - default: break; - } - if( (itr = simultaneousMap.find(rr)) == simultaneousMap.end() ) - simultaneousMap[rr] = 1; - else - itr->second++; - } - } - } - } - for( itr = simultaneousMap.begin(); itr != simultaneousMap.end(); itr ++ ) - { - if( itr->second >= 3 ) - { - out.hands ++; - if( itr->second >= 4 ) - { - out.quads ++; - } - } - } - for( itr = simultaneousMapNoHold.begin(); itr != simultaneousMapNoHold.end(); itr ++ ) - { - if( itr->second >= 2 ) - { - out.jumps ++; - } - } - out.taps = simultaneousMapTapHoldHead.size(); - return out; -} + int row; + int track; + recent_note_t() + :row(0), track(0) {} + recent_note_t(int r, int t) + :row(r), track(t) {} +}; void NoteDataUtil::CalculateRadarValues( const NoteData &in, float fSongSeconds, RadarValues& out ) { - RadarStats stats; - CalculateRadarStatsFast( in, stats ); - - // The for loop and the assert are used to ensure that all fields of - // RadarValue get set in here. - FOREACH_ENUM( RadarCategory, rc ) + int curr_row= -1; + bool judgable= false; + // recent_notes is used to calculate the voltage. Each element is the row + // and track number of a tap note. When the pair at the beginning is too + // old, it's deleted. This provides a way to have a rolling window + // that scans for the peak step density. -Kyz + vector recent_notes; + NoteData::all_tracks_const_iterator curr_note= + in.GetTapNoteRangeAllTracks(0, MAX_NOTE_ROW); + int num_notes_on_curr_row= 0; + TimingData* timing= GAMESTATE->GetProcessedTimingData(); + // total_taps exists because the stream calculation needs GetNumTapNotes, + // but TapsAndHolds + Jumps + Hands would be inaccurate. -Kyz + float total_taps= 0; + // hold_ends tracks where currently active holds will end, which is used + // to count the number of hands. -Kyz + vector hold_ends; + // num_holds_on_curr_row saves us the work of tracking where holds started + // just to keep a jump of two holds from counting as a hand. + int num_holds_on_curr_row= 0; + const float voltage_window_beats= 8.0f; + const int voltage_window= BeatToNoteRow(voltage_window_beats); + size_t max_notes_in_voltage_window= 0; + int num_chaos_rows= 0; + + // Steps zeros the RadarValues before calling CalculateRadarValues. -Kyz + + while(!curr_note.IsAtEnd()) { - switch( rc ) + if(curr_note.Row() != curr_row) { - case RadarCategory_Stream: out[rc] = GetStreamRadarValue( in, fSongSeconds ); break; - case RadarCategory_Voltage: out[rc] = GetVoltageRadarValue( in, fSongSeconds ); break; - case RadarCategory_Air: out[rc] = GetAirRadarValue( in, fSongSeconds ); break; - case RadarCategory_Freeze: out[rc] = GetFreezeRadarValue( in, fSongSeconds ); break; - case RadarCategory_Chaos: out[rc] = GetChaosRadarValue( in, fSongSeconds ); break; - case RadarCategory_TapsAndHolds: out[rc] = (float) stats.taps; break; - case RadarCategory_Jumps: out[rc] = (float) stats.jumps; break; - case RadarCategory_Holds: out[rc] = (float) in.GetNumHoldNotes(); break; - case RadarCategory_Mines: out[rc] = (float) in.GetNumMines(); break; - case RadarCategory_Hands: out[rc] = (float) in.GetNumHands(); break; - case RadarCategory_Rolls: out[rc] = (float) in.GetNumRolls(); break; - case RadarCategory_Lifts: out[rc] = (float) in.GetNumLifts(); break; - case RadarCategory_Fakes: out[rc] = (float) in.GetNumFakes(); break; - default: FAIL_M("Non-existant radar category attempted to be set!"); + curr_row= curr_note.Row(); + num_notes_on_curr_row= 0; + num_holds_on_curr_row= 0; + judgable= timing->IsJudgableAtRow(curr_row); + for(size_t n= 0; n < hold_ends.size(); ++n) + { + if(hold_ends[n] < curr_row) + { + hold_ends.erase(hold_ends.begin() + n); + --n; + } + } + for(size_t n= 0; n < recent_notes.size(); ++n) + { + if(recent_notes[n].row < curr_row - voltage_window) + { + recent_notes.erase(recent_notes.begin() + n); + --n; + } + else + { + // recent_notes is kept sorted, so reaching the first note that + // isn't old enough to remove means we're finished. -Kyz + break; + } + } + // GetChaosRadarValue did not care about whether a row is judgable. + // So chaos is checked here. -Kyz + if(GetNoteType(curr_row) >= NOTE_TYPE_12TH) + { + ++num_chaos_rows; + } } + if(judgable) + { + switch(curr_note->type) + { + case TapNoteType_Tap: + case TapNoteType_HoldHead: + // HoldTails and Attacks are counted by IsTap. -Kyz + case TapNoteType_HoldTail: + case TapNoteType_Attack: + ++num_notes_on_curr_row; + // Rolls are not counted by GetNumHoldNotes, which is what + // GetStreamRadarValue and GetVoltageRadarValue used to use. -Kyz + switch(curr_note->type) + { + case TapNoteType_HoldHead: + case TapNoteType_HoldTail: + if(curr_note->subType != TapNoteSubType_Hold) + { + break; + } + // This is really dumb: GetNumTapNotes counts hold heads, and + // GetNumHoldNotes also counts hold heads. So stream and + // voltage count hold heads twice. -Kyz + ++total_taps; + recent_notes.push_back( + recent_note_t(curr_row, curr_note.Track())); + case TapNoteType_Tap: + case TapNoteType_Attack: + ++total_taps; + recent_notes.push_back( + recent_note_t(curr_row, curr_note.Track())); + max_notes_in_voltage_window= max(recent_notes.size(), + max_notes_in_voltage_window); + break; + } + // If there is one hold active, and one tap on this row, it does + // not count as a jump. Hands do need to count the number of + // holds active though. -Kyz + switch(num_notes_on_curr_row) + { + case 1: + ++out[RadarCategory_TapsAndHolds]; + break; + case 2: + ++out[RadarCategory_Jumps]; + break; + default: + break; + } + if(num_notes_on_curr_row + (hold_ends.size() - + num_holds_on_curr_row) >= 3) + { + ++out[RadarCategory_Hands]; + } + if(curr_note->type == TapNoteType_HoldHead) + { + hold_ends.push_back(curr_row + curr_note->iDuration); + ++num_holds_on_curr_row; + switch(curr_note->subType) + { + case TapNoteSubType_Hold: + ++out[RadarCategory_Holds]; + break; + case TapNoteSubType_Roll: + ++out[RadarCategory_Rolls]; + break; + default: + break; + } + } + break; + case TapNoteType_Mine: + ++out[RadarCategory_Mines]; + break; + case TapNoteType_Lift: + ++out[RadarCategory_Lifts]; + break; + case TapNoteType_Fake: + ++out[RadarCategory_Fakes]; + break; + } + } + else + { + ++out[RadarCategory_Fakes]; + } + ++curr_note; } -} - -float NoteDataUtil::GetStreamRadarValue( const NoteData &in, float fSongSeconds ) -{ - if( !fSongSeconds ) - return 0.0f; - // density of steps - int iNumNotes = in.GetNumTapNotes() + in.GetNumHoldNotes(); - float fNotesPerSecond = iNumNotes/fSongSeconds; - float fReturn = fNotesPerSecond / 7; - return min( fReturn, 1.0f ); -} - -float NoteDataUtil::GetVoltageRadarValue( const NoteData &in, float fSongSeconds ) -{ - if( !fSongSeconds ) - return 0.0f; - - const float fLastBeat = in.GetLastBeat(); - const float fAvgBPS = fLastBeat / fSongSeconds; - - // peak density of steps - float fMaxDensitySoFar = 0; - - const float BEAT_WINDOW = 8; - const int BEAT_WINDOW_ROWS = BeatToNoteRow( BEAT_WINDOW ); - - for( int i=0; i<=BeatToNoteRow(fLastBeat); i+=BEAT_WINDOW_ROWS ) + // Walking the notes complete, now assign any values that remain. -Kyz +#define CAP_RADAR(n) min(1.0f, (n)) + if(fSongSeconds > 0.0f) { - int iNumNotesThisWindow = in.GetNumTapNotes( i, i+BEAT_WINDOW_ROWS ) + in.GetNumHoldNotes( i, i+BEAT_WINDOW_ROWS ); - float fDensityThisWindow = iNumNotesThisWindow / BEAT_WINDOW; - fMaxDensitySoFar = max( fMaxDensitySoFar, fDensityThisWindow ); + out[RadarCategory_Stream]= CAP_RADAR((total_taps / fSongSeconds) / 7.0f); + // As seen in GetVoltageRadarValue: Don't use the timing data, just + // pretend the beats are evenly spaced. -Kyz + int avg_bps= in.GetLastBeat() / fSongSeconds; + out[RadarCategory_Voltage]= CAP_RADAR( + ((max_notes_in_voltage_window / voltage_window_beats) * avg_bps) / + 10.0f); + out[RadarCategory_Air]= CAP_RADAR( + out[RadarCategory_Jumps] / fSongSeconds); + out[RadarCategory_Freeze]= CAP_RADAR( + out[RadarCategory_Holds] / fSongSeconds); + out[RadarCategory_Chaos]= CAP_RADAR( + num_chaos_rows / fSongSeconds * .5f); } - - float fReturn = fMaxDensitySoFar*fAvgBPS/10; - return min( fReturn, 1.0f ); -} - -float NoteDataUtil::GetAirRadarValue( const NoteData &in, float fSongSeconds ) -{ - if( !fSongSeconds ) - return 0.0f; - // number of doubles - int iNumDoubles = in.GetNumJumps(); - float fReturn = iNumDoubles / fSongSeconds; - return min( fReturn, 1.0f ); -} - -float NoteDataUtil::GetFreezeRadarValue( const NoteData &in, float fSongSeconds ) -{ - if( !fSongSeconds ) - return 0.0f; - // number of hold steps - float fReturn = in.GetNumHoldNotes() / fSongSeconds; - return min( fReturn, 1.0f ); -} - -float NoteDataUtil::GetChaosRadarValue( const NoteData &in, float fSongSeconds ) -{ - if( !fSongSeconds ) - return 0.0f; - // count number of notes smaller than 8ths - int iNumChaosNotes = 0; - - FOREACH_NONEMPTY_ROW_ALL_TRACKS( in, r ) - { - if( GetNoteType(r) >= NOTE_TYPE_12TH ) - iNumChaosNotes++; - } - - float fReturn = iNumChaosNotes / fSongSeconds * 0.5f; - return min( fReturn, 1.0f ); +#undef CAP_RADAR + // Sorry, there's not an assert here anymore for making sure all fields + // are set. There's a comment in the RadarCategory enum to direct + // attention here when adding new categories. -Kyz } void NoteDataUtil::RemoveHoldNotes( NoteData &in, int iStartIndex, int iEndIndex ) diff --git a/src/NoteDataUtil.h b/src/NoteDataUtil.h index 15f4f5c2e2..7e081855dd 100644 --- a/src/NoteDataUtil.h +++ b/src/NoteDataUtil.h @@ -11,23 +11,9 @@ class Song; struct AttackArray; class TimingData; -/** @brief A limited selection of the RadarValues. */ -struct RadarStats -{ - /** @brief The number of tap notes in the song. */ - int taps; - /** @brief The number of jumps in the song. */ - int jumps; - /** @brief The number of 3 panel hits in the song. */ - int hands; - /** @brief The number of 4 panel hits in the song. */ - int quads; -}; - void PlaceAutoKeysound( NoteData &out, int row, TapNote akTap ); int FindLongestOverlappingHoldNoteForAnyTrack( const NoteData &in, int iRow ); void LightTransformHelper( const NoteData &in, NoteData &out, const vector &aiTracks ); -RadarStats CalculateRadarStatsFast( const NoteData &in, RadarStats &out ); /** * @brief Utility functions that deal with NoteData. @@ -68,13 +54,6 @@ namespace NoteDataUtil // later. -Kyz void AutogenKickbox(const NoteData& in, NoteData& out, const TimingData& timing, StepsType out_type, int nonrandom_seed); - // radar values - return between 0.0 and 1.2 - float GetStreamRadarValue( const NoteData &in, float fSongSeconds ); - float GetVoltageRadarValue( const NoteData &in, float fSongSeconds ); - float GetAirRadarValue( const NoteData &in, float fSongSeconds ); - float GetFreezeRadarValue( const NoteData &in, float fSongSeconds ); - float GetChaosRadarValue( const NoteData &in, float fSongSeconds ); - void CalculateRadarValues( const NoteData &in, float fSongSeconds, RadarValues& out ); /** diff --git a/src/RageTimer.cpp b/src/RageTimer.cpp index c2c2cfa69e..98bee8dfda 100644 --- a/src/RageTimer.cpp +++ b/src/RageTimer.cpp @@ -58,6 +58,11 @@ float RageTimer::GetTimeSinceStart( bool bAccurate ) return uint32_t(usecs>>32) * 4294.967296f + uint32_t(usecs)/1000000.f; } +uint64_t RageTimer::GetUsecsSinceStart() +{ + return GetTime(true) - g_iStartTime; +} + void RageTimer::Touch() { uint64_t usecs = GetTime( true ); diff --git a/src/RageTimer.h b/src/RageTimer.h index fda5ab3a9a..6c55870771 100644 --- a/src/RageTimer.h +++ b/src/RageTimer.h @@ -23,6 +23,7 @@ public: /* deprecated: */ static float GetTimeSinceStart( bool bAccurate = true ); // seconds since the program was started static float GetTimeSinceStartFast() { return GetTimeSinceStart(false); } + static uint64_t GetUsecsSinceStart(); /* Get a timer representing half of the time ago as this one. */ RageTimer Half() const; @@ -52,8 +53,9 @@ private: extern const RageTimer RageZeroTimer; // For profiling how long some chunk of code takes. -Kyz -#define START_TIME(name) float name##_start_time= RageTimer::GetTimeSinceStartFast(); -#define END_TIME(name) float name##_end_time= RageTimer::GetTimeSinceStartFast(); LOG->Warn(#name " time: %f to %f = %f", name##_start_time, name##_end_time, name##_end_time - name##_start_time); +#define START_TIME(name) uint64_t name##_start_time= RageTimer::GetUsecsSinceStart(); +#define END_TIME(name) uint64_t name##_end_time= RageTimer::GetUsecsSinceStart(); LOG->Warn(#name " time: %zu to %zu = %zu", name##_start_time, name##_end_time, name##_end_time - name##_start_time); +#define END_TIME_ADD_TO(name) uint64_t name##_end_time= RageTimer::GetUsecsSinceStart(); name##_total += name##_end_time - name##_start_time; #endif diff --git a/src/RageUtil.cpp b/src/RageUtil.cpp index b61b3f12b3..32cfd09cb3 100644 --- a/src/RageUtil.cpp +++ b/src/RageUtil.cpp @@ -970,6 +970,47 @@ void MakeValidFilename( RString &sName ) sName = WStringToRString( wsName ); } +bool FindFirstFilenameContaining(const vector& filenames, + RString& out, const vector& starts_with, + const vector& contains, const vector& ends_with) +{ + for(size_t i= 0; i < filenames.size(); ++i) + { + RString lower= GetFileNameWithoutExtension(filenames[i]); + lower.MakeLower(); + for(size_t s= 0; s < starts_with.size(); ++s) + { + if(!lower.compare(0, starts_with[s].size(), starts_with[s])) + { + out= filenames[i]; + return true; + } + } + size_t lower_size= lower.size(); + for(size_t s= 0; s < ends_with.size(); ++s) + { + if(lower_size >= ends_with[s].size()) + { + size_t end_pos= lower_size - ends_with[s].size(); + if(!lower.compare(end_pos, string::npos, ends_with[s])) + { + out= filenames[i]; + return true; + } + } + } + for(size_t s= 0; s < contains.size(); ++s) + { + if(lower.find(contains[s]) != string::npos) + { + out= filenames[i]; + return true; + } + } + } + return false; +} + int g_argc = 0; char **g_argv = NULL; diff --git a/src/RageUtil.h b/src/RageUtil.h index 6e809cb727..5e4368f470 100644 --- a/src/RageUtil.h +++ b/src/RageUtil.h @@ -382,6 +382,11 @@ RString GetExtension( const RString &sPath ); RString GetFileNameWithoutExtension( const RString &sPath ); void MakeValidFilename( RString &sName ); +bool FindFirstFilenameContaining( + const vector& filenames, RString& out, + const vector& starts_with, + const vector& contains, const vector& ends_with); + extern const wchar_t INVALID_CHAR; int utf8_get_char_len( char p ); diff --git a/src/ScreenEdit.cpp b/src/ScreenEdit.cpp index bc8c590d9e..0b65fe747e 100644 --- a/src/ScreenEdit.cpp +++ b/src/ScreenEdit.cpp @@ -4775,11 +4775,14 @@ void ScreenEdit::HandleMainMenuChoice( MainMenuChoice c, const vector &iAns g_StepsData.rows[lifts].SetOneUnthemedChoice( ssprintf("%d", m_NoteDataEdit.GetNumLifts()) ); g_StepsData.rows[fakes].SetOneUnthemedChoice( ssprintf("%d", m_NoteDataEdit.GetNumFakes()) ); } - g_StepsData.rows[stream].SetOneUnthemedChoice( ssprintf("%.2f", NoteDataUtil::GetStreamRadarValue(m_NoteDataEdit,fMusicSeconds)) ); - g_StepsData.rows[voltage].SetOneUnthemedChoice( ssprintf("%.2f", NoteDataUtil::GetVoltageRadarValue(m_NoteDataEdit,fMusicSeconds)) ); - g_StepsData.rows[air].SetOneUnthemedChoice( ssprintf("%.2f", NoteDataUtil::GetAirRadarValue(m_NoteDataEdit,fMusicSeconds)) ); - g_StepsData.rows[freeze].SetOneUnthemedChoice( ssprintf("%.2f", NoteDataUtil::GetFreezeRadarValue(m_NoteDataEdit,fMusicSeconds)) ); - g_StepsData.rows[chaos].SetOneUnthemedChoice( ssprintf("%.2f", NoteDataUtil::GetChaosRadarValue(m_NoteDataEdit,fMusicSeconds)) ); + RadarValues radar; + radar.Zero(); + NoteDataUtil::CalculateRadarValues(m_NoteDataEdit, fMusicSeconds, radar); + g_StepsData.rows[stream].SetOneUnthemedChoice(ssprintf("%.2f", radar[RadarCategory_Stream])); + g_StepsData.rows[voltage].SetOneUnthemedChoice(ssprintf("%.2f", radar[RadarCategory_Voltage])); + g_StepsData.rows[air].SetOneUnthemedChoice(ssprintf("%.2f", radar[RadarCategory_Air])); + g_StepsData.rows[freeze].SetOneUnthemedChoice(ssprintf("%.2f", radar[RadarCategory_Freeze])); + g_StepsData.rows[chaos].SetOneUnthemedChoice(ssprintf("%.2f", radar[RadarCategory_Chaos])); EditMiniMenu( &g_StepsData, SM_BackFromStepsData, SM_None ); break; } diff --git a/src/Song.cpp b/src/Song.cpp index e9726ffd5b..42149f0bac 100644 --- a/src/Song.cpp +++ b/src/Song.cpp @@ -43,7 +43,7 @@ * @brief The internal version of the cache for StepMania. * * Increment this value to invalidate the current cache. */ -const int FILE_CACHE_VERSION = 223; +const int FILE_CACHE_VERSION = 224; /** @brief How long does a song sample last by default? */ const float DEFAULT_MUSIC_SAMPLE_LENGTH = 12.f; @@ -292,9 +292,9 @@ bool Song::LoadFromSongDir( RString sDir, bool load_autosave ) RString sCacheFilePath = GetCacheFilePath(); if( !DoesFileExist(sCacheFilePath) ) - bUseCache = false; - if( !PREFSMAN->m_bFastLoad && GetHashForDirectory(m_sSongDir) != uCacheHash ) - bUseCache = false; // this cache is out of date + { bUseCache = false; } + if(!PREFSMAN->m_bFastLoad && GetHashForDirectory(m_sSongDir) != uCacheHash) + { bUseCache = false; } // this cache is out of date if(load_autosave) { bUseCache= false; } @@ -344,6 +344,10 @@ bool Song::LoadFromSongDir( RString sDir, bool load_autosave ) } TidyUpData(false, true); + // If edits are not cached, looking for them causes a substantial hit to + // loading time. -Kyz + LoadEditsFromSongDir(sDir); + // Don't save a cache file if the autosave is being loaded, because the // cache file would contain the autosave filename. -Kyz if(!load_autosave) @@ -369,24 +373,6 @@ bool Song::LoadFromSongDir( RString sDir, bool load_autosave ) 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 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(); @@ -396,7 +382,6 @@ bool Song::LoadFromSongDir( RString sDir, bool load_autosave ) LOG->UserLog( "Song", sDir, "has no music; ignored." ); return false; // don't load this song } - return true; // do load this song } @@ -469,6 +454,28 @@ bool Song::ReloadFromSongDir( RString sDir ) return true; } +void Song::LoadEditsFromSongDir(RString dir) +{ + // Load any .edit files in the song folder. + // Doing this BEFORE setting up AutoGen just in case. + vector vs; + GetDirListing(dir + "*.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(dir + 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(dir + vs[i], ProfileSlot_Invalid, true, this); + } + } + // Note: If vs.empty() then this loop is skipped entirely (vs.size() == 0) +} + bool Song::HasAutosaveFile() { if(m_sSongFileName.empty()) @@ -530,7 +537,7 @@ void FixupPath( RString &path, const RString &sSongPath ) } // Songs in BlacklistImages will never be autodetected as song images. -void Song::TidyUpData( bool fromCache, bool /* duringCache */ ) +void Song::TidyUpData( bool from_cache, bool /* duringCache */ ) { // We need to do this before calling any of HasMusic, HasHasCDTitle, etc. ASSERT_M( m_sSongDir.Left(3) != "../", m_sSongDir ); // meaningless @@ -559,262 +566,265 @@ void Song::TidyUpData( bool fromCache, bool /* duringCache */ ) Trim(this->m_sMainTitle); } - if( !HasMusic() ) - { - vector arrayPossibleMusic; - FILEMAN->GetDirListingWithMultipleExtensions(m_sSongDir, - ActorUtil::GetTypeExtensionList(FT_Sound), 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(m_fMusicLengthSeconds > 0.0f || lastSecond > -1.0f) - { - if(m_fMusicLengthSeconds == 0.0f) - { - m_fMusicLengthSeconds= lastSecond; - } - } - else 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_sMainTitle == "") + NotesLoader::GetMainAndSubTitlesFromFullTitle( + Basename(this->GetSongDir()), m_sMainTitle, m_sSubTitle); - if( m_sArtist == "" ) + 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..." ); + CHECKPOINT_M("Looking for images..."); - if( !fromCache ) + m_SongTiming.TidyUpData(false); + + FOREACH(Steps *, m_vpSteps, s) { + (*s)->m_Timing.TidyUpData(true); + } + + if(!from_cache) + { + // Set the has flags before tidying so that tidying can check them instead + // of using the has functions that hit the disk. -Kyz + // These will be written to cache, for Song::LoadFromSongDir to use later. + m_bHasMusic = HasMusic(); + m_bHasBanner = HasBanner(); + m_bHasBackground = HasBackground(); + + if(m_bHasBanner) + { BANNERCACHE->CacheBanner(GetBannerPath()); } + /* + if(m_bHasBackground) + { BANNERCACHE->CacheBackground(GetBackgroundPath()); } + */ + + if(!m_bHasMusic) + { + vector arrayPossibleMusic; + FILEMAN->GetDirListingWithMultipleExtensions(m_sSongDir, + ActorUtil::GetTypeExtensionList(FT_Sound), 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]; + m_bHasMusic= true; + } + } + // This must be done before radar calculation. + if(m_bHasMusic) + { + 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; + } + 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; } + + // Fetch the list of images in the song dir ONCE, not TWELVE TIMES! -Kyz + vector image_dir_list; + GetImageDirListing(m_sSongDir + "*", image_dir_list); + // For blank args to FindFirstFilenameContaining. -Kyz + vector empty_list; + + bool has_jacket= HasJacket(); + bool has_cdimage= HasCDImage(); + bool has_disc= HasDisc(); + bool has_cdtitle= HasCDTitle(); // First, check the file name for hints. - if( !HasBanner() ) + if(!m_bHasBanner) { /* 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 ); - + vector contains(1, "banner"); /* 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]; + vector ends_with(1, " bn"); + m_bHasBanner= FindFirstFilenameContaining(image_dir_list, + m_sBannerFile, empty_list, contains, ends_with); } - if( !HasBackground() ) + if(!m_bHasBackground) { //m_sBackgroundFile = ""; // find an image with "bg" or "background" in the file name - vector arrayPossibleBGs; - GetImageDirListing( m_sSongDir + "*background*", arrayPossibleBGs ); - // don't match e.g. "subgroup", "hobgoblin", etc. - GetImageDirListing( m_sSongDir + "*bg", arrayPossibleBGs ); - if( !arrayPossibleBGs.empty() ) - m_sBackgroundFile = arrayPossibleBGs[0]; + vector contains(1, "background"); + vector ends_with(1, "bg"); + m_bHasBackground= FindFirstFilenameContaining(image_dir_list, + m_sBackgroundFile, empty_list, contains, ends_with); } - if( !HasJacket() ) + if(!has_jacket) { // 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]; + vector starts_with(1, "jk_"); + vector contains; + contains.reserve(2); + contains.push_back("jacket"); + contains.push_back("albumart"); + has_jacket= FindFirstFilenameContaining(image_dir_list, + m_sJacketFile, starts_with, contains, empty_list); } - if( !HasCDImage() ) + if(!has_cdimage) { // 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]; + vector ends_with(1, "-cd"); + has_cdimage= FindFirstFilenameContaining(image_dir_list, + m_sCDFile, empty_list, empty_list, ends_with); } - if( !HasDisc() ) + if(!has_disc) { // a rectangular graphic, not to be confused with CDImage above. - vector arrayPossibleDiscImages; - GetImageDirListing( m_sSongDir + "* disc", arrayPossibleDiscImages ); - GetImageDirListing( m_sSongDir + "* title", arrayPossibleDiscImages ); - if( !arrayPossibleDiscImages.empty() ) - m_sDiscFile = arrayPossibleDiscImages[0]; + vector ends_with; + ends_with.reserve(2); + ends_with.push_back(" disc"); + ends_with.push_back(" title"); + has_disc= FindFirstFilenameContaining(image_dir_list, + m_sDiscFile, empty_list, empty_list, ends_with); } - if( !HasCDTitle() ) + if(!has_cdtitle) { // find an image with "cdtitle" in the file name - vector arrayPossibleCDTitles; - GetImageDirListing( m_sSongDir + "*cdtitle*", arrayPossibleCDTitles ); - if( !arrayPossibleCDTitles.empty() ) - m_sCDTitleFile = arrayPossibleCDTitles[0]; + vector contains(1, "cdtitle"); + has_cdtitle= FindFirstFilenameContaining(image_dir_list, + m_sCDTitleFile, empty_list, contains, empty_list); } - if( !HasLyrics() ) + 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]; + vector lyric_files; + GetDirListing(m_sSongDir + RString("*.lrc"), lyric_files); + if(!lyric_files.empty()) + { m_sLyricsFile = lyric_files[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() ); + LOG->UserLog("Graphic file", sPath, "couldn't be loaded: %s", error.c_str()); continue; } @@ -822,23 +832,27 @@ void Song::TidyUpData( bool fromCache, bool /* duringCache */ ) const int height = img->h; delete img; - if( !HasBackground() && width >= 320 && height >= 240 ) + if(!m_bHasBackground && width >= 320 && height >= 240) { - m_sBackgroundFile = arrayImages[i]; + m_sBackgroundFile = image_dir_list[i]; + m_bHasBackground= true; continue; } - if( !HasBanner() && 100<=width && width<=320 && 50<=height && height<=240 ) + if(!m_bHasBanner && 100 <= width && width <= 320 && + 50 <= height && height <= 240) { - m_sBannerFile = arrayImages[i]; + m_sBannerFile = image_dir_list[i]; + m_bHasBanner= true; 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 ) + if(!m_bHasBanner && width > 200 && float(width) / height > 2.0f) { - m_sBannerFile = arrayImages[i]; + m_sBannerFile = image_dir_list[i]; + m_bHasBanner= true; continue; } @@ -857,75 +871,70 @@ void Song::TidyUpData( bool fromCache, bool /* duringCache */ ) * 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 ) + if(!has_cdtitle && width <= 100 && height <= 48) { - m_sCDTitleFile = arrayImages[i]; + m_sCDTitleFile = image_dir_list[i]; + has_cdtitle= true; continue; } // Jacket files typically have the same width and height. - if( !HasJacket() && width == height ) + if(!has_jacket && width == height) { - m_sJacketFile = arrayImages[i]; + m_sJacketFile = image_dir_list[i]; + has_jacket= true; continue; } // Disc images are typically rectangular; make sure we have a banner already. - if( !HasDisc() && (width > height) && HasBanner() ) + if(!has_disc && (width > height) && m_bHasBanner) { - if( arrayImages[i] != m_sBannerFile ) - m_sDiscFile = arrayImages[i]; + if(image_dir_list[i] != m_sBannerFile) + { + m_sDiscFile = image_dir_list[i]; + has_disc= true; + } continue; } // CD images are the same as Jackets, typically the same width and height - if( !HasCDImage() && width == height ) + if(!has_cdimage && width == height) { - m_sCDFile = arrayImages[i]; + m_sCDFile = image_dir_list[i]; + has_cdimage= true; continue; } } + // 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; + FILEMAN->GetDirListingWithMultipleExtensions(m_sSongDir, + ActorUtil::GetTypeExtensionList(FT_Movie), arrayPossibleMovies); - } - - // These will be written to cache, for Song::LoadFromSongDir to use later. - m_bHasMusic = HasMusic(); - m_bHasBanner = HasBanner(); - m_bHasBackground = HasBackground(); - - if( HasBanner() ) - BANNERCACHE->CacheBanner( GetBannerPath() ); - /* - if( HasBackground() ) - BANNERCACHE->CacheBackground( GetBackgroundPath() ); - */ - - // If no BGChanges are specified and there are movies in the song directory, then assume - // they are DWI style where the movie begins at beat 0. - if( (!HasBGChanges() && !fromCache) ) - { - vector arrayPossibleMovies; - FILEMAN->GetDirListingWithMultipleExtensions(m_sSongDir, - ActorUtil::GetTypeExtensionList(FT_Movie), arrayPossibleMovies); - - /* Use this->GetBeatFromElapsedTime(0) instead of 0 to start when the - * music starts. */ - if( arrayPossibleMovies.size() == 1 ) + /* 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)); + 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); } - - /* 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 ) + /* Generate these before we autogen notes, so the new notes can inherit + * their source's values. */ + ReCalculateRadarValuesAndLastSecond(from_cache, true); + // If the music length is suspiciously shorter than the last second, adjust + // the length. This prevents the ogg patch from setting a false length. -Kyz + if(m_fMusicLengthSeconds < lastSecond - 10.0f) { - SongUtil::AdjustDuplicateSteps( this ); + m_fMusicLengthSeconds= lastSecond; } } diff --git a/src/Song.h b/src/Song.h index c40149a636..9271abee89 100644 --- a/src/Song.h +++ b/src/Song.h @@ -90,6 +90,7 @@ public: bool LoadFromSongDir( RString sDir, bool load_autosave= false ); // This one takes the effort to reuse Steps pointers as best as it can bool ReloadFromSongDir( RString sDir ); + void LoadEditsFromSongDir(RString dir); bool HasAutosaveFile(); bool LoadAutosaveFile();