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();