Added RadarCategory_Notes. Rewrote NoteDataWithScoring stuff to only traverse the NoteData once, and match the radar code in NoteDataUtil. Fixed hand and lift counting in NoteDataUtil::CalculateRadarValues. Removed cap from RadarValues so that Stream, Voltage, Air, Freeze, and Chaos can be over 1.0. Fixed Stream and Voltage to not count hold heads twice. Changed SetRadarValues in NotesLoaderSSC to not require the stepfile version number.

This commit is contained in:
Kyzentun
2015-04-06 18:45:23 -06:00
parent 016a4053e1
commit 5852067f47
10 changed files with 326 additions and 271 deletions
+233 -185
View File
@@ -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<TapNoteScoreJudgeType> LAST_OR_MINIMUM_TNS ("Gameplay","LastOrMinimumTapNoteScore");
static ThemeMetric<TapNoteScore> 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<in.GetNumTracks(); t++ )
{
FOREACH_NONEMPTY_ROW_IN_TRACK_RANGE( in, t, r, iStartIndex, iEndIndex )
{
const TapNote &tn = in.GetTapNote(t, r);
if( tn.result.tns >= 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; t<in.GetNumTracks(); ++t )
{
NoteData::TrackMap::const_iterator begin, end;
in.GetTapNoteRange( t, 0, MAX_NOTE_ROW, begin, end );
for( ; begin != end; ++begin )
{
const TapNote &tn = begin->second;
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; t<in.GetNumTracks(); t++ )
{
const TapNote &tn = in.GetTapNote(t, i);
if( tn.type == TapNoteType_Empty )
continue;
if( tn.type == TapNoteType_Mine ) // mines don't count
continue;
if (tn.type == TapNoteType_Fake ) // fake arrows don't count
continue;
if( tn.result.tns <= TNS_W5 )
Missed = true;
}
if( Missed )
continue;
// Check hold scores.
for( int t=0; t<in.GetNumTracks(); ++t )
{
int iHeadRow;
if( !in.IsHoldNoteAtRow( t, i, &iHeadRow ) )
continue;
const TapNote &tn = in.GetTapNote( t, iHeadRow );
/* If a hold is released *after* a hand containing it, the hand is
* still good. Ignore the judgement and only examine iLastHeldRow
* to be sure that the hold was still held at the point of this row.
* (Note that if the hold head tap was missed, then iLastHeldRow == i
* and this won't fail--but the tap check above will have already failed.) */
if( tn.HoldResult.iLastHeldRow < i )
Missed = true;
}
if( !Missed )
iNum++;
}
return iNum;
}
int GetSuccessfulLifts( const NoteData &in, TapNoteScore tns, int iStartIndex = 0, int iEndIndex = MAX_NOTE_ROW )
{
int iNumSuccessfulLiftNotes = 0;
NoteData::all_tracks_const_iterator iter = in.GetTapNoteRangeAllTracks( iStartIndex, iEndIndex );
for( ; !iter.IsAtEnd(); ++iter )
{
if( iter->type == 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_t
{
int end_row;
int last_held_row;
hold_status_t(int e, int l)
:end_row(e), last_held_row(l)
{}
};
struct garv_state_t
{
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_status_t> 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_t()
: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_t& 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_t 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_t(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);
}
}
}