diff --git a/src/GameConstantsAndTypes.cpp b/src/GameConstantsAndTypes.cpp index 8bfec7c0b9..9871c01f2d 100644 --- a/src/GameConstantsAndTypes.cpp +++ b/src/GameConstantsAndTypes.cpp @@ -30,6 +30,7 @@ static const char *RadarCategoryNames[] = { "Air", "Freeze", "Chaos", + "Notes", "TapsAndHolds", "Jumps", "Holds", diff --git a/src/GameConstantsAndTypes.h b/src/GameConstantsAndTypes.h index 614b6f6c9e..5ffc987e47 100644 --- a/src/GameConstantsAndTypes.h +++ b/src/GameConstantsAndTypes.h @@ -33,6 +33,7 @@ enum RadarCategory RadarCategory_Air, /**< How much air is in the song? */ RadarCategory_Freeze, /**< How much freeze (holds) is in the song? */ RadarCategory_Chaos, /**< How much chaos is in the song? */ + RadarCategory_Notes, /**< How many notes are in the song? */ RadarCategory_TapsAndHolds, /**< How many taps and holds are in the song? */ RadarCategory_Jumps, /**< How many jumps are in the song? */ RadarCategory_Holds, /**< How many holds are in the song? */ @@ -42,7 +43,8 @@ enum RadarCategory 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 + // NoteDataUtil::CalculateRadarValues to calculate it. + // Also update NoteDataWithScoring::GetActualRadarValues. -Kyz NUM_RadarCategory, /**< The number of radar categories. */ RadarCategory_Invalid }; diff --git a/src/GrooveRadar.cpp b/src/GrooveRadar.cpp index 5aafb9873c..93deafef8a 100644 --- a/src/GrooveRadar.cpp +++ b/src/GrooveRadar.cpp @@ -107,7 +107,7 @@ void GrooveRadar::GrooveRadarValueMap::SetFromSteps( const RadarValues &rv ) { const float fValueCurrent = m_fValuesOld[c] * (1-m_PercentTowardNew) + m_fValuesNew[c] * m_PercentTowardNew; m_fValuesOld[c] = fValueCurrent; - m_fValuesNew[c] = rv[c]; + m_fValuesNew[c] = clamp(rv[c], 0.0, 1.0); } if( !m_bValuesVisible ) // the values WERE invisible diff --git a/src/NoteDataUtil.cpp b/src/NoteDataUtil.cpp index a3cc22c405..879e38d70a 100644 --- a/src/NoteDataUtil.cpp +++ b/src/NoteDataUtil.cpp @@ -951,59 +951,88 @@ void NoteDataUtil::AutogenKickbox(const NoteData& in, NoteData& out, const Timin out.RevalidateATIs(vector(), false); } -struct recent_note_t +struct recent_note { int row; int track; - recent_note_t() + recent_note() :row(0), track(0) {} - recent_note_t(int r, int t) + recent_note(int r, int t) :row(r), track(t) {} }; -void NoteDataUtil::CalculateRadarValues( const NoteData &in, float fSongSeconds, RadarValues& out ) +// CalculateRadarValues has to delay some stuff until a row ends, but can +// only detect a row ending when it hits the next note. There isn't a note +// after the last row, so it also has to do the delayed stuff after exiting +// its loop. So this state structure exists to be passed to a function that +// can be called from both places to do the work. If this were Lua, +// DoRowEndRadarCalc would be a nested function. -Kyz +struct crv_state { - out.Zero(); - 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; + bool judgable; // 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; + int num_holds_on_curr_row; + int num_notes_on_curr_row; + + crv_state() + :judgable(false), num_holds_on_curr_row(0), num_notes_on_curr_row(0) + {} +}; + +static void DoRowEndRadarCalc(crv_state& state, RadarValues& out) +{ + if(state.judgable) + { + if(state.num_notes_on_curr_row + (state.hold_ends.size() - + state.num_holds_on_curr_row) >= 3) + { + ++out[RadarCategory_Hands]; + } + } +} + +void NoteDataUtil::CalculateRadarValues( const NoteData &in, float fSongSeconds, RadarValues& out ) +{ + // Anybody editing this function should also examine + // NoteDataWithScoring::GetActualRadarValues to make sure it handles things + // the same way. + out.Zero(); + int curr_row= -1; + // 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); + 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; 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 + crv_state state; while(!curr_note.IsAtEnd()) { if(curr_note.Row() != curr_row) { + DoRowEndRadarCalc(state, out); 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) + state.num_notes_on_curr_row= 0; + state.num_holds_on_curr_row= 0; + state.judgable= timing->IsJudgableAtRow(curr_row); + for(size_t n= 0; n < state.hold_ends.size(); ++n) { - if(hold_ends[n] < curr_row) + if(state.hold_ends[n] < curr_row) { - hold_ends.erase(hold_ends.begin() + n); + state.hold_ends.erase(state.hold_ends.begin() + n); --n; } } @@ -1028,45 +1057,29 @@ void NoteDataUtil::CalculateRadarValues( const NoteData &in, float fSongSeconds, ++num_chaos_rows; } } - if(judgable) + if(state.judgable) { switch(curr_note->type) { case TapNoteType_Tap: case TapNoteType_HoldHead: + // Lifts have to be counted with taps for them to be added to max dp + // correctly. -Kyz + case TapNoteType_Lift: // 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; - } + ++out[RadarCategory_Notes]; + ++state.num_notes_on_curr_row; + ++total_taps; + recent_notes.push_back( + recent_note(curr_row, curr_note.Track())); + max_notes_in_voltage_window= max(recent_notes.size(), + max_notes_in_voltage_window); // 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) + switch(state.num_notes_on_curr_row) { case 1: ++out[RadarCategory_TapsAndHolds]; @@ -1077,15 +1090,10 @@ void NoteDataUtil::CalculateRadarValues( const NoteData &in, float fSongSeconds, 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; + state.hold_ends.push_back(curr_row + curr_note->iDuration); + ++state.num_holds_on_curr_row; switch(curr_note->subType) { case TapNoteSubType_Hold: @@ -1098,13 +1106,14 @@ void NoteDataUtil::CalculateRadarValues( const NoteData &in, float fSongSeconds, break; } } + else if(curr_note->type == TapNoteType_Lift) + { + ++out[RadarCategory_Lifts]; + } break; case TapNoteType_Mine: ++out[RadarCategory_Mines]; break; - case TapNoteType_Lift: - ++out[RadarCategory_Lifts]; - break; case TapNoteType_Fake: ++out[RadarCategory_Fakes]; break; @@ -1116,25 +1125,22 @@ void NoteDataUtil::CalculateRadarValues( const NoteData &in, float fSongSeconds, } ++curr_note; } + DoRowEndRadarCalc(state, out); + // Walking the notes complete, now assign any values that remain. -Kyz -#define CAP_RADAR(n) min(1.0f, (n)) if(fSongSeconds > 0.0f) { - out[RadarCategory_Stream]= CAP_RADAR((total_taps / fSongSeconds) / 7.0f); + out[RadarCategory_Stream]= (total_taps / fSongSeconds) / 7.0f; // As seen in GetVoltageRadarValue: Don't use the timing data, just // pretend the beats are evenly spaced. -Kyz float avg_bps= in.GetLastBeat() / fSongSeconds; - out[RadarCategory_Voltage]= CAP_RADAR( + out[RadarCategory_Voltage]= ((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); + 10.0f; + out[RadarCategory_Air]= out[RadarCategory_Jumps] / fSongSeconds; + out[RadarCategory_Freeze]= out[RadarCategory_Holds] / fSongSeconds; + out[RadarCategory_Chaos]= num_chaos_rows / fSongSeconds * .5f; } -#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 diff --git a/src/NoteDataWithScoring.cpp b/src/NoteDataWithScoring.cpp index c5ebb71684..b0ac95ee3b 100644 --- a/src/NoteDataWithScoring.cpp +++ b/src/NoteDataWithScoring.cpp @@ -3,8 +3,10 @@ #include "NoteData.h" #include "PlayerStageStats.h" #include "GameConstantsAndTypes.h" +#include "GameState.h" #include "ThemeMetric.h" #include "RageLog.h" +#include "TimingData.h" namespace { @@ -12,136 +14,6 @@ namespace //ThemeMetric LAST_OR_MINIMUM_TNS ("Gameplay","LastOrMinimumTapNoteScore"); static ThemeMetric MIN_SCORE_TO_MAINTAIN_COMBO( "Gameplay", "MinScoreToMaintainCombo" ); -int GetNumTapNotesWithScore( const NoteData &in, TapNoteScore tns, int iStartIndex = 0, int iEndIndex = MAX_NOTE_ROW ) -{ - int iNumSuccessfulTapNotes = 0; - for( int t=0; t= tns ) - iNumSuccessfulTapNotes++; - } - } - - return iNumSuccessfulTapNotes; -} - -int GetNumNWithScore( const NoteData &in, TapNoteScore tns, int MinTaps, int iStartRow = 0, int iEndRow = MAX_NOTE_ROW ) -{ - int iNumSuccessfulDoubles = 0; - FOREACH_NONEMPTY_ROW_ALL_TRACKS_RANGE( in, r, iStartRow, iEndRow ) - { - int iNumNotesInRow = in.GetNumTracksWithTapOrHoldHead( r ); - TapNoteScore tnsRow = NoteDataWithScoring::LastTapNoteWithResult( in, r ).result.tns; - - if( iNumNotesInRow >= MinTaps && tnsRow >= tns ) - iNumSuccessfulDoubles++; - } - - return iNumSuccessfulDoubles; -} - -int GetNumHoldNotesWithScore( const NoteData &in, TapNoteSubType subType, HoldNoteScore hns ) -{ - ASSERT( subType != TapNoteSubType_Invalid ); - - int iNumSuccessfulHolds = 0; - for( int t=0; tsecond; - if( tn.type != TapNoteType_HoldHead ) - continue; - if( tn.subType != subType ) - continue; - if( tn.HoldResult.hns == hns ) - ++iNumSuccessfulHolds; - } - } - - return iNumSuccessfulHolds; -} - -int GetSuccessfulMines( const NoteData &in, int iStartIndex = 0, int iEndIndex = MAX_NOTE_ROW ) -{ - int iNumSuccessfulMinesNotes = 0; - NoteData::all_tracks_const_iterator iter = in.GetTapNoteRangeAllTracks( iStartIndex, iEndIndex ); - for( ; !iter.IsAtEnd(); ++iter ) - { - if( iter->type == TapNoteType_Mine && iter->result.tns == TNS_AvoidMine ) - ++iNumSuccessfulMinesNotes; - } - return iNumSuccessfulMinesNotes; -} - -// See NoteData::GetNumHands(). -int GetSuccessfulHands( const NoteData &in, int iStartIndex = 0, int iEndIndex = MAX_NOTE_ROW ) -{ - int iNum = 0; - FOREACH_NONEMPTY_ROW_ALL_TRACKS_RANGE( in, i, iStartIndex, iEndIndex ) - { - if( !in.RowNeedsHands(i) ) - continue; - - bool Missed = false; - for( int t=0; ttype == TapNoteType_Lift && iter->result.tns >= tns ) - ++iNumSuccessfulLiftNotes; - } - return iNumSuccessfulLiftNotes; -} - /* Return the last tap score of a row: the grade of the tap that completed * the row. If the row has no tap notes, return -1. If any tap notes aren't * graded (any tap is TNS_None) or are missed (TNS_Miss), return it. */ @@ -276,17 +148,6 @@ bool NoteDataWithScoring::IsRowCompletelyJudged( const NoteData &in, unsigned ro namespace { -// Return the ratio of actual to possible Bs. -float GetActualStreamRadarValue( const NoteData &in, float fSongSeconds ) -{ - int iTotalSteps = in.GetNumTapNotes(); - if( iTotalSteps == 0 ) - return 1.0f; - - const int iW2s = GetNumTapNotesWithScore( in, TNS_W2 ); - return clamp( float(iW2s)/iTotalSteps, 0.0f, 1.0f ); -} - // Return the ratio of actual combo to max combo. float GetActualVoltageRadarValue( const NoteData &in, float fSongSeconds, const PlayerStageStats &pss ) { @@ -295,23 +156,16 @@ float GetActualVoltageRadarValue( const NoteData &in, float fSongSeconds, const * varies depending on the mode and score keeper. Instead, let's use the * length of the longest recorded combo. This is only subtly different: * it's the percent of the song the longest combo took to get. */ + // FIXME: + // If MaxCombo.m_fSizeSeconds is used, it is wrong (too short) for any song + // where the last second is after the last step. However, calculating the + // max combo possible would require consulting the timing data on every step + // for combo segments and generally be a painful waste of effort. -Kyz const PlayerStageStats::Combo_t MaxCombo = pss.GetMaxCombo(); float fComboPercent = SCALE( MaxCombo.m_fSizeSeconds, 0, pss.m_fLastSecond-pss.m_fFirstSecond, 0.0f, 1.0f ); return clamp( fComboPercent, 0.0f, 1.0f ); } -// Return the ratio of actual to possible W2s on jumps. -float GetActualAirRadarValue( const NoteData &in, float fSongSeconds ) -{ - const int iTotalDoubles = in.GetNumJumps(); - if( iTotalDoubles == 0 ) - return 1.0f; // no jumps in song - - // number of doubles - const int iNumDoubles = GetNumNWithScore( in, TNS_W2, 2 ); - return clamp( (float)iNumDoubles / iTotalDoubles, 0.0f, 1.0f ); -} - // Return the ratio of actual to possible dance points. float GetActualChaosRadarValue( const NoteData &in, float fSongSeconds, const PlayerStageStats &pss ) { @@ -322,47 +176,241 @@ float GetActualChaosRadarValue( const NoteData &in, float fSongSeconds, const Pl const int ActualDP = pss.m_iActualDancePoints; return clamp( float(ActualDP)/iPossibleDP, 0.0f, 1.0f ); } - -// Return the ratio of actual to possible successful holds. -float GetActualFreezeRadarValue( const NoteData &in, float fSongSeconds ) -{ - // number of hold steps - const int iTotalHolds = in.GetNumHoldNotes(); - if( iTotalHolds == 0 ) - return 1.0f; - - const int ActualHolds = - GetNumHoldNotesWithScore( in, TapNoteSubType_Hold, HNS_Held ) + - GetNumHoldNotesWithScore( in, TapNoteSubType_Roll, HNS_Held ); - return clamp( float(ActualHolds) / iTotalHolds, 0.0f, 1.0f ); } +struct hold_status +{ + int end_row; + int last_held_row; + hold_status(int e, int l) + :end_row(e), last_held_row(l) + {} +}; + +struct garv_state +{ + int curr_row; + int notes_hit_for_stream; + int jumps_hit_for_air; + int holds_held; + int rolls_held; + int notes_hit; + int taps_hit; + int jumps_hit; + int hands_hit; + int mines_avoided; + int lifts_hit; + // hold_ends tracks where currently active holds will end, which is used + // to count the number of hands. -Kyz + vector hold_ends; + int num_notes_on_curr_row; + // 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; + int num_notes_hit_on_curr_row; + // last_tns_on_row and last_time_on_row are used for deciding whether a jump + // or hand was successfully hit. + TapNoteScore last_tns_on_row; + float last_time_on_row; + // A hand is considered missed if any of the notes is missed. + TapNoteScore worst_tns_on_row; + // TODO? Make these configurable in some way? + TapNoteScore stream_tns; + TapNoteScore air_tns; + TapNoteScore taps_tns; + TapNoteScore jumps_tns; + TapNoteScore hands_tns; + TapNoteScore lifts_tns; + bool judgable; + garv_state() + :curr_row(0), notes_hit_for_stream(0), jumps_hit_for_air(0), + holds_held(0), rolls_held(0), notes_hit(0), taps_hit(0), jumps_hit(0), + hands_hit(0), mines_avoided(0), lifts_hit(0), num_notes_on_curr_row(0), + num_holds_on_curr_row(0), num_notes_hit_on_curr_row(0), + last_tns_on_row(TapNoteScore_Invalid), last_time_on_row(-9999), + worst_tns_on_row(TapNoteScore_Invalid), stream_tns(TNS_W2), + air_tns(TNS_W2), taps_tns(TNS_W4), jumps_tns(TNS_W4), hands_tns(TNS_W4), + lifts_tns(MIN_SCORE_TO_MAINTAIN_COMBO), + judgable(false) + {} +}; + +static void DoRowEndRadarActualCalc(garv_state& state, RadarValues& out) +{ + if(state.judgable && state.last_tns_on_row != TapNoteScore_Invalid) + { + if(state.num_notes_on_curr_row >= 1) + { + state.taps_hit+= (state.last_tns_on_row >= state.taps_tns); + } + if(state.num_notes_on_curr_row >= 2) + { + state.jumps_hit_for_air+= (state.last_tns_on_row >= state.air_tns); + state.jumps_hit+= (state.last_tns_on_row >= state.jumps_tns); + } + if(state.num_notes_on_curr_row + (state.hold_ends.size() - + state.num_holds_on_curr_row) >= 3) + { + if(state.worst_tns_on_row >= state.hands_tns) + { + size_t holds_down= 0; + for(size_t n= 0; n < state.hold_ends.size(); ++n) + { + holds_down+= (state.curr_row <= state.hold_ends[n].last_held_row); + } + state.hands_hit+= (holds_down == state.hold_ends.size()); + } + } + } } - -void NoteDataWithScoring::GetActualRadarValues( const NoteData &in, const PlayerStageStats &pss, float fSongSeconds, RadarValues& out ) +void NoteDataWithScoring::GetActualRadarValues(const NoteData &in, + const PlayerStageStats &pss, float song_seconds, RadarValues& out) { + // Anybody editing this function should also examine + // NoteDataUtil::CalculateRadarValues to make sure it handles things the + // same way. + // Some of this logic is similar or identical to + // NoteDataUtil::CalculateRadarValues because I couldn't figure out a good + // way to combine them into one. -Kyz + PlayerNumber pn= pss.m_player_number; + garv_state state; + + NoteData::all_tracks_const_iterator curr_note= + in.GetTapNoteRangeAllTracks(0, MAX_NOTE_ROW); + TimingData* timing= GAMESTATE->GetProcessedTimingData(); + + while(!curr_note.IsAtEnd()) + { + if(curr_note.Row() != state.curr_row) + { + DoRowEndRadarActualCalc(state, out); + state.curr_row= curr_note.Row(); + state.num_notes_on_curr_row= 0; + state.num_holds_on_curr_row= 0; + state.judgable= timing->IsJudgableAtRow(state.curr_row); + for(size_t n= 0; n < state.hold_ends.size(); ++n) + { + if(state.hold_ends[n].end_row < state.curr_row) + { + state.hold_ends.erase(state.hold_ends.begin() + n); + --n; + } + } + state.last_tns_on_row= TapNoteScore_Invalid; + state.last_time_on_row= -9999; + state.worst_tns_on_row= TapNoteScore_Invalid; + } + bool for_this_player= curr_note->pn == pn || pn == PLAYER_INVALID || + curr_note->pn == PLAYER_INVALID; + if(state.judgable && for_this_player) + { + switch(curr_note->type) + { + case TapNoteType_Tap: + case TapNoteType_HoldHead: + // HoldTails and Attacks are counted by IsTap. -Kyz + case TapNoteType_HoldTail: + case TapNoteType_Attack: + case TapNoteType_Lift: + ++state.num_notes_on_curr_row; + state.notes_hit_for_stream+= (curr_note->result.tns >= state.stream_tns); + state.notes_hit+= (curr_note->result.tns >= state.taps_tns); + if(curr_note->result.tns < state.worst_tns_on_row) + { + state.worst_tns_on_row= curr_note->result.tns; + } + if(curr_note->result.fTapNoteOffset > state.last_time_on_row) + { + state.last_time_on_row= curr_note->result.fTapNoteOffset; + state.last_tns_on_row= curr_note->result.tns; + } + if(curr_note->type == TapNoteType_HoldHead) + { + if(curr_note->subType == TapNoteSubType_Hold) + { + state.holds_held+= (curr_note->HoldResult.hns >= HNS_Held); + } + else if(curr_note->subType == TapNoteSubType_Roll) + { + state.rolls_held+= (curr_note->HoldResult.hns >= HNS_Held); + } + state.hold_ends.push_back( + hold_status(state.curr_row + curr_note->iDuration, + curr_note->HoldResult.iLastHeldRow)); + ++state.num_holds_on_curr_row; + } + else if(curr_note->type == TapNoteType_Lift) + { + state.lifts_hit+= (curr_note->result.tns >= state.lifts_tns); + } + break; + case TapNoteType_Mine: + state.mines_avoided+= (curr_note->result.tns == TNS_AvoidMine); + break; + case TapNoteType_Fake: + break; + } + } + ++curr_note; + } + DoRowEndRadarActualCalc(state, out); + + // ScreenGameplay passes in the RadarValues that were calculated by + // NoteDataUtil::CalculateRadarValues, so those are reused here. -Kyz + int note_count= out[RadarCategory_Notes]; + int jump_count= out[RadarCategory_Jumps]; + int hold_count= out[RadarCategory_Holds]; + int tap_count= out[RadarCategory_TapsAndHolds]; // The for loop and the assert are used to ensure that all fields of // RadarValue get set in here. - FOREACH_ENUM( RadarCategory, rc ) + FOREACH_ENUM(RadarCategory, rc) { - switch( rc ) + switch(rc) { - case RadarCategory_Stream: out[rc] = GetActualStreamRadarValue( in, fSongSeconds ); break; - case RadarCategory_Voltage: out[rc] = GetActualVoltageRadarValue( in, fSongSeconds, pss ); break; - case RadarCategory_Air: out[rc] = GetActualAirRadarValue( in, fSongSeconds ); break; - case RadarCategory_Freeze: out[rc] = GetActualFreezeRadarValue( in, fSongSeconds ); break; - case RadarCategory_Chaos: out[rc] = GetActualChaosRadarValue( in, fSongSeconds, pss ); break; - case RadarCategory_TapsAndHolds: out[rc] = (float) GetNumNWithScore( in, TNS_W4, 1 ); break; - case RadarCategory_Jumps: out[rc] = (float) GetNumNWithScore( in, TNS_W4, 2 ); break; - case RadarCategory_Holds: out[rc] = (float) GetNumHoldNotesWithScore( in, TapNoteSubType_Hold, HNS_Held ); break; - case RadarCategory_Mines: out[rc] = (float) GetSuccessfulMines( in ); break; - case RadarCategory_Hands: out[rc] = (float) GetSuccessfulHands( in ); break; - case RadarCategory_Rolls: out[rc] = (float) GetNumHoldNotesWithScore( in, TapNoteSubType_Roll, HNS_Held ); break; - case RadarCategory_Lifts: out[rc] = (float) GetSuccessfulLifts( in, MIN_SCORE_TO_MAINTAIN_COMBO ); break; - case RadarCategory_Fakes: out[rc] = (float) in.GetNumFakes(); break; - //case RadarCategory_Minefields: out[rc] = (float) GetNumMinefieldsWithScore( in, TapNoteSubType_Mine, HNS_Held ); break; - DEFAULT_FAIL( rc ); + case RadarCategory_Stream: + out[rc]= clamp(float(state.notes_hit_for_stream) / note_count, 0.0f, 1.0f); + break; + case RadarCategory_Voltage: + out[rc]= GetActualVoltageRadarValue(in, song_seconds, pss); + break; + case RadarCategory_Air: + out[rc]= clamp(float(state.jumps_hit_for_air) / jump_count, 0.0f, 1.0f); + break; + case RadarCategory_Freeze: + out[rc]= clamp(float(state.holds_held) / hold_count, 0.0f, 1.0f); + break; + case RadarCategory_Chaos: + out[rc]= GetActualChaosRadarValue(in, song_seconds, pss); + break; + case RadarCategory_TapsAndHolds: + out[rc]= state.taps_hit; + break; + case RadarCategory_Jumps: + out[rc]= state.jumps_hit; + break; + case RadarCategory_Holds: + out[rc]= state.holds_held; + break; + case RadarCategory_Mines: + out[rc]= state.mines_avoided; + break; + case RadarCategory_Hands: + out[rc]= state.hands_hit; + break; + case RadarCategory_Rolls: + out[rc]= state.rolls_held; + break; + case RadarCategory_Lifts: + out[rc]= state.lifts_hit; + break; + case RadarCategory_Fakes: + out[rc]= out[rc]; + break; + case RadarCategory_Notes: + out[rc]= state.notes_hit; + break; + DEFAULT_FAIL(rc); } } } diff --git a/src/NoteDataWithScoring.h b/src/NoteDataWithScoring.h index 56f655dbea..38739a50bd 100644 --- a/src/NoteDataWithScoring.h +++ b/src/NoteDataWithScoring.h @@ -22,8 +22,8 @@ namespace NoteDataWithScoring TapNoteScore MinTapNoteScore( const NoteData &in, unsigned iRow, PlayerNumber plnum = PlayerNumber_Invalid ); const TapNote &LastTapNoteWithResult( const NoteData &in, unsigned iRow, PlayerNumber plnum = PlayerNumber_Invalid ); - void GetActualRadarValues( const NoteData &in, const PlayerStageStats &pss, - float fSongSeconds, RadarValues& out ); + void GetActualRadarValues(const NoteData &in, const PlayerStageStats &pss, + float song_seconds, RadarValues& out); }; #endif diff --git a/src/NotesLoaderSSC.cpp b/src/NotesLoaderSSC.cpp index ad10469b67..d44fbccb0e 100644 --- a/src/NotesLoaderSSC.cpp +++ b/src/NotesLoaderSSC.cpp @@ -352,25 +352,21 @@ void SetRadarValues(StepsTagInfo& info) { if(info.from_cache || info.for_load_edit) { - vector saValues; - split((*info.params)[1], ",", saValues, true); - int categories = NUM_RadarCategory; - if(info.song->m_fVersion < VERSION_RADAR_FAKE && !info.for_load_edit) - { categories -= 1; } - if(saValues.size() == (unsigned int)categories * NUM_PLAYERS) + vector values; + split((*info.params)[1], ",", values, true); + // Instead of trying to use the version to figure out how many + // categories to expect, look at the number of values and split them + // evenly. -Kyz + size_t cats_per_player= values.size() / NUM_PlayerNumber; + RadarValues v[NUM_PLAYERS]; + FOREACH_PlayerNumber(pn) { - RadarValues v[NUM_PLAYERS]; - FOREACH_PlayerNumber(pn) + for(size_t i= 0; i < cats_per_player; ++i) { - // Can't use the foreach anymore due to flexible radar lines. - for(RadarCategory rc = (RadarCategory)0; rc < categories; - enum_add(rc, +1)) - { - v[pn][rc] = StringToFloat(saValues[pn*categories + rc]); - } + v[pn][i]= StringToFloat(values[pn * cats_per_player + i]); } - info.steps->SetCachedRadarValues(v); } + info.steps->SetCachedRadarValues(v); } else { diff --git a/src/NotesLoaderSSC.h b/src/NotesLoaderSSC.h index 9de3383061..7a63914dac 100644 --- a/src/NotesLoaderSSC.h +++ b/src/NotesLoaderSSC.h @@ -32,6 +32,8 @@ const float VERSION_OFFSET_BEFORE_ATTACK = 0.72f; const float VERSION_CHART_NAME_TAG = 0.74f; /** @brief The version that introduced the cache switch tag. */ const float VERSION_CACHE_SWITCH_TAG = 0.77f; +/** @brief The version where note count was added as a radar category. */ +const float VERSION_RADAR_NOTECOUNT = 0.83f; /** * @brief The SSCLoader handles all of the parsing needed for .ssc files. diff --git a/src/Song.cpp b/src/Song.cpp index 2c33a4a71a..88db011735 100644 --- a/src/Song.cpp +++ b/src/Song.cpp @@ -45,7 +45,7 @@ * @brief The internal version of the cache for StepMania. * * Increment this value to invalidate the current cache. */ -const int FILE_CACHE_VERSION = 224; +const int FILE_CACHE_VERSION = 225; /** @brief How long does a song sample last by default? */ const float DEFAULT_MUSIC_SAMPLE_LENGTH = 12.f; diff --git a/src/Song.h b/src/Song.h index db9c901c20..7ab14acf88 100644 --- a/src/Song.h +++ b/src/Song.h @@ -20,7 +20,7 @@ void FixupPath( RString &path, const RString &sSongPath ); RString GetSongAssetPath( RString sPath, const RString &sSongPath ); /** @brief The version of the .ssc file format. */ -const static float STEPFILE_VERSION_NUMBER = 0.82f; +const static float STEPFILE_VERSION_NUMBER = 0.83f; /** @brief How many edits for this song can each profile have? */ const int MAX_EDITS_PER_SONG_PER_PROFILE = 15; diff --git a/src/TimingData.cpp b/src/TimingData.cpp index c684de6ff6..b0becfecec 100644 --- a/src/TimingData.cpp +++ b/src/TimingData.cpp @@ -780,9 +780,21 @@ float TimingData::GetElapsedTimeInternal(GetBeatStarts& start, float beat, float bps= GetBPMAtRow(start.last_row) / 60.0f; #define INC_INDEX(index) ++curr_segment; ++index; bool find_marker= beat < FLT_MAX; + // Stops require this special kluge to handle the case where a stop and a + // warp are on the same row and the lookup table entry would be between + // the stop and the warp. If that happens, then the last_time in the entry + // is pushed past the marker by the stop, so the marker can't be found and + // the step that is on that row comes up as a miss. + // So this kluge backs up the lookup table entry by one step if the last + // thing found was a stop. -Kyz + bool last_found_was_a_stop= false; + GetBeatStarts pre_stop_state= start; + unsigned int start_segment= curr_segment; - while(curr_segment < max_segment) + while(curr_segment < max_segment || curr_segment - start_segment <= 1) { + last_found_was_a_stop= false; + pre_stop_state= start; int event_row= INT_MAX; int event_type= NOT_FOUND; FindEvent(event_row, event_type, start, beat, find_marker, bpms, warps, stops, @@ -802,6 +814,7 @@ float TimingData::GetElapsedTimeInternal(GetBeatStarts& start, float beat, break; case FOUND_STOP: case FOUND_STOP_DELAY: + last_found_was_a_stop= true; time_to_next_event= ToStop(stops[start.stop])->GetPause(); next_event_time= start.last_time + time_to_next_event; start.last_time= next_event_time; @@ -831,6 +844,10 @@ float TimingData::GetElapsedTimeInternal(GetBeatStarts& start, float beat, start.last_row= event_row; } #undef INC_INDEX + if(last_found_was_a_stop) + { + start= pre_stop_state; + } return start.last_time; }