diff --git a/src/MeasureInfo.cpp b/src/MeasureInfo.cpp new file mode 100644 index 0000000000..62b4e94cf4 --- /dev/null +++ b/src/MeasureInfo.cpp @@ -0,0 +1,133 @@ +#include "global.h" +#include "MeasureInfo.h" +#include "NoteData.h" +#include "RageLog.h" +#include "LocalizedString.h" +#include "LuaBinding.h" +#include "TimingData.h" +#include "GameState.h" + + +RString MeasureInfo::ToString() const +{ + std::vector asMeasureInfo; + for (unsigned i = 0; i < npsPerMeasure.size(); i++) + { + asMeasureInfo.push_back(ssprintf("%.6f", npsPerMeasure[i])); + } + + for (unsigned i = 0; i < notesPerMeasure.size(); i++) + { + asMeasureInfo.push_back(ssprintf("%d", notesPerMeasure[i])); + } + + return join(",", asMeasureInfo); +} + +void MeasureInfo::FromString(RString sValues) +{ + std::vector asValues; + split( sValues, ",", asValues, true ); + int half_size = static_cast(asValues.size()) / 2; + + float peak_nps = 0; + for (int i = 0; i < half_size; i++) + { + float nps = StringToFloat(asValues[i]); + this->npsPerMeasure.push_back(nps); + if(nps > peak_nps) + { + peak_nps = nps; + } + } + for (unsigned i = half_size; i < asValues.size(); i++) + { + this->notesPerMeasure.push_back(StringToInt(asValues[i])); + } + this->measureCount = half_size; + this->peakNps = peak_nps; +} + +void MeasureInfo::CalculateMeasureInfo(const NoteData &in, MeasureInfo &out) +{ + int lastRow = in.GetLastRow(); + int lastRowMeasureIndex = 0; + int lastRowBeatIndex = 0; + int lastRowRemainder = 0; + TimingData *timing = GAMESTATE->GetProcessedTimingData(); + timing->NoteRowToMeasureAndBeat(lastRow, lastRowMeasureIndex, lastRowBeatIndex, lastRowRemainder); + + int totalMeasureCount = lastRowMeasureIndex + 1; + + out.notesPerMeasure.clear(); + out.npsPerMeasure.clear(); + + out.notesPerMeasure.resize(totalMeasureCount, 0); + out.npsPerMeasure.resize(totalMeasureCount, 0); + + NoteData::all_tracks_const_iterator curr_note = in.GetTapNoteRangeAllTracks(0, MAX_NOTE_ROW); + + int iMeasureIndexOut = 0; + int iBeatIndexOut = 0; + int iRowsRemainder = 0; + + float peak_nps = 0; + int curr_row = -1; + int notes_this_row = 0; + + while (!curr_note.IsAtEnd()) + { + if(curr_note.Row() != curr_row) + { + // Before moving on to a new row, update the row count for the "current" measure + // Note that we're only add + out.notesPerMeasure[iMeasureIndexOut] += notes_this_row; + // Update iMeasureIndex for the current row + timing->NoteRowToMeasureAndBeat(curr_note.Row(), iMeasureIndexOut, iBeatIndexOut, iRowsRemainder); + curr_row = curr_note.Row(); + notes_this_row = 0; + } + // Update tap and mine count for the current measure + // Regardless of how many notes are on this row, it's only considered 1 "note" when we want to + // calculate nps. So jumps/brackets don't inflate the nps value. Maybe we should call it + // "steps per second" or something like that? + if (curr_note->type == TapNoteType_Tap || curr_note->type == TapNoteType_HoldHead) + { + notes_this_row = 1; + } + ++curr_note; + } + + // And handle the final note... + out.notesPerMeasure[iMeasureIndexOut] += notes_this_row; + + // Now that all of the notes have been parsed, calculate nps for each measure + for (int m = 0; m < totalMeasureCount; m++) + { + if(out.notesPerMeasure[m] == 0) + { + out.npsPerMeasure[m] = 0; + continue; + } + + float measureDuration = timing->GetElapsedTimeFromBeat(4 * (m+1)) - timing->GetElapsedTimeFromBeat(4 * m); + float nps = out.notesPerMeasure[m] / measureDuration; + + if(measureDuration < 0.12) + { + out.npsPerMeasure[m] = 0; + } + else + { + out.npsPerMeasure[m] = nps; + } + + if(nps > peak_nps) + { + peak_nps = nps; + } + } + + out.peakNps = peak_nps; + out.measureCount = totalMeasureCount; +} diff --git a/src/MeasureInfo.h b/src/MeasureInfo.h new file mode 100644 index 0000000000..853b8a6b7d --- /dev/null +++ b/src/MeasureInfo.h @@ -0,0 +1,37 @@ +#ifndef MEASURE_INFO_H +#define MEASURE_INFO_H + +#include "GameConstantsAndTypes.h" +class NoteData; + +/** This is a container for per-measure stats of a stepchart. + This data is calculated and saved to the song cache files as the #MEASUREINFO tag. + This data is provided to the theme via lua functions in Steps.cpp and Trail.cpp, + */ + +struct MeasureInfo +{ + int measureCount; + float peakNps; + std::vector npsPerMeasure; + std::vector notesPerMeasure; + + MeasureInfo() + { + Zero(); + } + + void Zero() + { + measureCount = 0; + peakNps = 0; + npsPerMeasure.clear(); + notesPerMeasure.clear(); + } + + RString ToString() const; + void FromString( RString sValues ); + static void CalculateMeasureInfo(const NoteData &in, MeasureInfo &out); +}; + +#endif diff --git a/src/NotesLoaderSSC.cpp b/src/NotesLoaderSSC.cpp index f5527bd6ab..2a9c87adb9 100644 --- a/src/NotesLoaderSSC.cpp +++ b/src/NotesLoaderSSC.cpp @@ -383,6 +383,30 @@ void SetRadarValues(StepsTagInfo& info) } info.ssc_format= true; } + +void SetMeasureInfo(StepsTagInfo& info) +{ + if (info.from_cache || info.for_load_edit) + { + std::vector values; + split((*info.params)[1], "|", values, true); + + MeasureInfo v[NUM_PLAYERS]; + FOREACH_PlayerNumber(pn) + { + v[pn].FromString(values[pn]); + + } + info.steps->SetCachedMeasureInfo(v); + } + else + { + // just recalc at time. + } + info.ssc_format= true; +} + + void SetCredit(StepsTagInfo& info) { info.steps->SetCredit((*info.params)[1]); @@ -623,6 +647,8 @@ struct ssc_parser_helper_t steps_tag_handlers["SCROLLS"]= &SetStepsScrolls; steps_tag_handlers["FAKES"]= &SetStepsFakes; steps_tag_handlers["LABELS"]= &SetStepsLabels; + steps_tag_handlers["MEASUREINFO"] = &SetMeasureInfo; + /* If this is called, the chart does not use the same attacks * as the Song's timing. No other changes are required. */ steps_tag_handlers["ATTACKS"]= &SetStepsAttacks; diff --git a/src/NotesWriterSSC.cpp b/src/NotesWriterSSC.cpp index 17f747025f..5b0fd3a4aa 100644 --- a/src/NotesWriterSSC.cpp +++ b/src/NotesWriterSSC.cpp @@ -428,6 +428,14 @@ static RString GetSSCNoteData( const Song &song, const Steps &in, bool bSavingCa } if (bSavingCache) { + std::vector asMeasureInfo; + FOREACH_PlayerNumber( pn ) + { + const MeasureInfo &ms = in.GetMeasureInfo(pn); + asMeasureInfo.push_back(ms.ToString()); + } + lines.push_back(ssprintf("#MEASUREINFO:%s;", join("|", asMeasureInfo).c_str())); + lines.push_back(ssprintf("#STEPFILENAME:%s;", in.GetFilename().c_str())); } else diff --git a/src/Steps.cpp b/src/Steps.cpp index b5c08603ff..c1777010f2 100644 --- a/src/Steps.cpp +++ b/src/Steps.cpp @@ -55,6 +55,7 @@ Steps::Steps(Song *song): m_StepsType(StepsType_Invalid), m_pSong(song), m_sDescription(""), m_sChartStyle(""), m_Difficulty(Difficulty_Invalid), m_iMeter(0), m_bAreCachedRadarValuesJustLoaded(false), + m_bAreCachedMeasureInfoJustLoaded(false), m_sCredit(""), displayBPMType(DISPLAY_BPM_ACTUAL), specifiedBPMMin(0), specifiedBPMMax(0) {} @@ -354,6 +355,58 @@ void Steps::CalculateRadarValues( float fMusicLengthSeconds ) GAMESTATE->SetProcessedTimingData(nullptr); } +void Steps::CalculateMeasureInfo() +{ + if(parent != nullptr) + { + return; + } + + if( m_bAreCachedMeasureInfoJustLoaded ) + { + m_bAreCachedMeasureInfoJustLoaded = false; + return; + } + + NoteData tempNoteData; + this->GetNoteData( tempNoteData ); + + FOREACH_PlayerNumber(pn) + m_CachedMeasureInfo[pn] + .Zero(); + + GAMESTATE->SetProcessedTimingData(this->GetTimingData()); + + if( tempNoteData.IsComposite() ) + { + std::vector vParts; + NoteDataUtil::SplitCompositeNoteData( tempNoteData, vParts ); + for( std::size_t pn = 0; pn < std::min(vParts.size(), std::size_t(NUM_PLAYERS)); ++pn ) + { + MeasureInfo::CalculateMeasureInfo(vParts[pn], m_CachedMeasureInfo[pn]); + } + } + else if (GAMEMAN->GetStepsTypeInfo(this->m_StepsType).m_StepsTypeCategory == StepsTypeCategory_Couple) + { + NoteData p1 = tempNoteData; + // XXX: Assumption that couple will always have an even number of notes. + const int tracks = tempNoteData.GetNumTracks() / 2; + p1.SetNumTracks(tracks); + MeasureInfo::CalculateMeasureInfo(tempNoteData, m_CachedMeasureInfo[PLAYER_1]); + NoteDataUtil::ShiftTracks(tempNoteData, tracks); + tempNoteData.SetNumTracks(tracks); + MeasureInfo::CalculateMeasureInfo(tempNoteData, m_CachedMeasureInfo[PLAYER_2]); + } + else + { + MeasureInfo::CalculateMeasureInfo(tempNoteData, m_CachedMeasureInfo[0]); + std::fill_n( m_CachedMeasureInfo + 1, NUM_PLAYERS-1, m_CachedMeasureInfo[0] ); + } + GAMESTATE->SetProcessedTimingData(nullptr); +} + + + void Steps::ChangeFilenamesForCustomSong() { m_sFilename= custom_songify_path(m_sFilename); @@ -503,6 +556,7 @@ void Steps::DeAutogen( bool bCopyNoteData ) m_Difficulty = Real()->m_Difficulty; m_iMeter = Real()->m_iMeter; std::copy( Real()->m_CachedRadarValues, Real()->m_CachedRadarValues + NUM_PLAYERS, m_CachedRadarValues ); + std::copy( Real()->m_CachedMeasureInfo, Real()->m_CachedMeasureInfo + NUM_PLAYERS, m_CachedMeasureInfo ); m_sCredit = Real()->m_sCredit; parent = nullptr; @@ -631,6 +685,13 @@ void Steps::SetCachedRadarValues( const RadarValues v[NUM_PLAYERS] ) m_bAreCachedRadarValuesJustLoaded = true; } +void Steps::SetCachedMeasureInfo(const MeasureInfo ms[NUM_PLAYERS]) +{ + DeAutogen(); + std::copy(ms, ms + NUM_PLAYERS, m_CachedMeasureInfo); + m_bAreCachedMeasureInfoJustLoaded = true; +} + RString Steps::GenerateChartKey() { ChartKey = this->GenerateChartKey(*m_pNoteData, this->GetTimingData()); @@ -813,6 +874,40 @@ public: return 1; } + static int GetNPSPerMeasure(T *p, lua_State *L) + { + PlayerNumber pn = PLAYER_1; + if (!lua_isnil(L, 1)) { + pn = Enum::Check(L, 1); + } + MeasureInfo &ts = const_cast(p->GetMeasureInfo(pn)); + LuaHelpers::CreateTableFromArray(ts.npsPerMeasure, L); + return 1; + } + + static int GetNotesPerMeasure(T *p, lua_State * L) + { + PlayerNumber pn = PLAYER_1; + if (!lua_isnil(L, 1)) { + pn = Enum::Check(L, 1); + } + MeasureInfo &ts = const_cast(p->GetMeasureInfo(pn)); + LuaHelpers::CreateTableFromArray(ts.notesPerMeasure, L); + + return 1; + } + + static int GetPeakNPS(T *p, lua_State *L) + { + PlayerNumber pn = PLAYER_1; + if (!lua_isnil(L, 1)) { + pn = Enum::Check(L, 1); + } + MeasureInfo &ts = const_cast(p->GetMeasureInfo(pn)); + lua_pushnumber(L, ts.peakNps); + return 1; + } + static int GetColumnCues(T *p, lua_State*L) { float minDuration = 1.5; @@ -856,7 +951,7 @@ public: } return 1; } - + LunaSteps() { ADD_METHOD( GetAuthorCredit ); @@ -883,6 +978,9 @@ public: ADD_METHOD( PredictMeter ); ADD_METHOD( GetDisplayBPMType ); ADD_METHOD( GetColumnCues ); + ADD_METHOD( GetNPSPerMeasure ); + ADD_METHOD( GetNotesPerMeasure ); + ADD_METHOD( GetPeakNPS ); } }; diff --git a/src/Steps.h b/src/Steps.h index f4d9072aa5..282b6ad118 100644 --- a/src/Steps.h +++ b/src/Steps.h @@ -10,6 +10,7 @@ #include "RageUtil_AutoPtr.h" #include "TimingData.h" #include "ColumnCues.h" +#include "MeasureInfo.h" #include @@ -140,6 +141,7 @@ public: void SetLoadedFromProfile( ProfileSlot slot ) { m_LoadedFromProfile = slot; } void SetMeter( int meter ); void SetCachedRadarValues( const RadarValues v[NUM_PLAYERS] ); + void SetCachedMeasureInfo(const MeasureInfo ms[NUM_PLAYERS]); float PredictMeter() const; unsigned GetHash() const; @@ -164,6 +166,9 @@ public: void TidyUpData(); void CalculateRadarValues( float fMusicLengthSeconds ); + void CalculateMeasureInfo(); + const MeasureInfo &GetMeasureInfo(PlayerNumber pn) const { return Real()->m_CachedMeasureInfo[pn]; } + /** * @brief The TimingData used by the Steps. * @@ -211,7 +216,7 @@ public: { return join(":", this->m_sAttackString); } - + std::vector GetColumnCues(float minDuration); private: @@ -256,6 +261,10 @@ private: /** @brief The radar values used for each player. */ RadarValues m_CachedRadarValues[NUM_PLAYERS]; bool m_bAreCachedRadarValuesJustLoaded; + + mutable MeasureInfo m_CachedMeasureInfo[NUM_PLAYERS]; + bool m_bAreCachedMeasureInfoJustLoaded; + /** @brief The name of the person who created the Steps. */ RString m_sCredit; /** @brief The name of the chart. */