Merge pull request #552 from kyzentun/radar_note_count
RadarCategory_Notes, uncapping and fixes to radar calculation.
This commit is contained in:
@@ -30,6 +30,7 @@ static const char *RadarCategoryNames[] = {
|
||||
"Air",
|
||||
"Freeze",
|
||||
"Chaos",
|
||||
"Notes",
|
||||
"TapsAndHolds",
|
||||
"Jumps",
|
||||
"Holds",
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+83
-77
@@ -951,59 +951,88 @@ void NoteDataUtil::AutogenKickbox(const NoteData& in, NoteData& out, const Timin
|
||||
out.RevalidateATIs(vector<int>(), 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_note_t> 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<int> 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_note> 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
|
||||
|
||||
+233
-185
@@ -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
|
||||
{
|
||||
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_status> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
+11
-15
@@ -352,25 +352,21 @@ void SetRadarValues(StepsTagInfo& info)
|
||||
{
|
||||
if(info.from_cache || info.for_load_edit)
|
||||
{
|
||||
vector<RString> 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<RString> 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<RadarCategory>(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
|
||||
{
|
||||
|
||||
@@ -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.
|
||||
|
||||
+1
-1
@@ -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;
|
||||
|
||||
+1
-1
@@ -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;
|
||||
|
||||
+18
-1
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user