diff --git a/Docs/Luadoc/Lua.xml b/Docs/Luadoc/Lua.xml
index 6b0cde4ffb..9f92d0d66d 100644
--- a/Docs/Luadoc/Lua.xml
+++ b/Docs/Luadoc/Lua.xml
@@ -2027,6 +2027,7 @@
+
diff --git a/Docs/Luadoc/LuaDocumentation.xml b/Docs/Luadoc/LuaDocumentation.xml
index acd8167659..92c61f80ae 100644
--- a/Docs/Luadoc/LuaDocumentation.xml
+++ b/Docs/Luadoc/LuaDocumentation.xml
@@ -6098,6 +6098,11 @@ local spr = Def.Sprite{
Returns true if the Tap Note was judged with a result that would stop it from drawing.
+
+ Returns whether the input for the Tap Note was ever held during
+ the judgment interval. Useful to distinguish a normal miss from
+ a held miss.
+
diff --git a/src/NoteTypes.cpp b/src/NoteTypes.cpp
index 851a981dfe..89ff24842e 100644
--- a/src/NoteTypes.cpp
+++ b/src/NoteTypes.cpp
@@ -210,12 +210,14 @@ public:
DEFINE_METHOD(GetTapNoteScore, tns);
DEFINE_METHOD(GetTapNoteOffset, fTapNoteOffset);
DEFINE_METHOD(GetHidden, bHidden);
+ DEFINE_METHOD(GetHeld, bHeld);
LunaTapNoteResult()
{
ADD_METHOD( GetTapNoteScore );
ADD_METHOD( GetTapNoteOffset );
ADD_METHOD( GetHidden );
+ ADD_METHOD( GetHeld );
}
};
LUA_REGISTER_CLASS( TapNoteResult )
diff --git a/src/NoteTypes.h b/src/NoteTypes.h
index f966291d41..0ad3bd9dfa 100644
--- a/src/NoteTypes.h
+++ b/src/NoteTypes.h
@@ -13,7 +13,7 @@ class XNode;
struct TapNoteResult
{
/** @brief Set up the TapNoteResult with default values. */
- TapNoteResult() : tns(TNS_None), fTapNoteOffset(0.f), bHidden(false) { }
+ TapNoteResult() : tns(TNS_None), fTapNoteOffset(0.f), bHidden(false), bHeld(false) { }
/** @brief The TapNoteScore that was achieved by the player. */
TapNoteScore tns;
@@ -28,6 +28,9 @@ struct TapNoteResult
/** @brief If the whole row has been judged, all taps on the row will be set to hidden. */
bool bHidden;
+ /** @brief Track if the note was held. Used to track held misses. */
+ bool bHeld;
+
// XML
XNode* CreateNode() const;
void LoadFromNode( const XNode* pNode );
diff --git a/src/Player.cpp b/src/Player.cpp
index 0fe98278aa..145d50d3ad 100644
--- a/src/Player.cpp
+++ b/src/Player.cpp
@@ -993,6 +993,72 @@ void Player::Update( float fDeltaTime )
}
}
+ // Track held misses
+ //
+ // In order to track held misses we have to check whether a note was
+ // held any time during the judgment window before it is judged a miss.
+ // Note: at this point we don't actually know yet whether a note will
+ // be a miss or a hit, so we have to track for all notes whether they
+ // were held at some point before getting judged.
+ {
+ float largestWindow = 0.0f;
+ largestWindow = max(largestWindow, GetWindowSeconds(TW_W1));
+ largestWindow = max(largestWindow, GetWindowSeconds(TW_W2));
+ largestWindow = max(largestWindow, GetWindowSeconds(TW_W3));
+ largestWindow = max(largestWindow, GetWindowSeconds(TW_W4));
+ largestWindow = max(largestWindow, GetWindowSeconds(TW_W5));
+
+ // We have to check the unjudged notes that are within the
+ // timing window. Let's find the cutoff point! (lastCheckRow)
+ const float rate = GAMESTATE->m_SongOptions.GetCurrent().m_fMusicRate;
+ const SongPosition songPosition = m_pPlayerState->m_Position;
+ const float musicPosition = songPosition.m_fMusicSeconds + (songPosition.m_LastBeatUpdate.Ago() * rate);
+ // We have to add 1 here, because GetBeatFromElapsedTime() can round down.
+ const int lastCheckRow = BeatToNoteRow(m_Timing->GetBeatFromElapsedTime(musicPosition + (largestWindow * rate)) + 1);
+
+ // The button being held only counts for the first unjudged
+ // note on a track (== column/arrow direction), so we have to
+ // keep track for which tracks we have already seen an unjudged
+ // note.
+ vector seenTracks(m_NoteData.GetNumTracks(), false);
+
+ for(auto iter = *m_pIterNeedsTapJudging; !iter.IsAtEnd() && iter.Row() <= lastCheckRow; ++iter)
+ {
+ TapNote &tn = *iter;
+ const int row = iter.Row();
+ const int track = iter.Track();
+
+ // Skip over warp and fake segments
+ if (!m_Timing->IsJudgableAtRow(row))
+ continue;
+
+ // Held misses only apply to tap notes
+ if (tn.type != TapNoteType_Tap && tn.type != TapNoteType_HoldHead)
+ continue;
+
+ const float notePosition = m_Timing->GetElapsedTimeFromBeat(NoteRowToBeat(row));
+ const float offset = fabsf((notePosition - musicPosition) / rate);
+
+ // Skip if we are outside of the largest timing window
+ if (offset > largestWindow)
+ continue;
+
+ // Skip the note if there is an earlier note on the same track that still awaits judgement
+ if (seenTracks[track])
+ continue;
+
+ seenTracks[track] = true;
+
+ if (!tn.result.bHeld)
+ {
+ PlayerNumber pn = m_pPlayerState->m_PlayerNumber;
+ vector input;
+ GAMESTATE->GetCurrentStyle(pn)->StyleInputToGameInput(track, pn, input);
+
+ tn.result.bHeld = INPUTMAPPER->IsBeingPressed(input, m_pPlayerState->m_mp);
+ }
+ }
+ }
// update HoldNotes logic
{
@@ -3120,6 +3186,9 @@ void Player::SetJudgment( int iRow, int iTrack, const TapNote &tn, TapNoteScore
msg.SetParam( "Early", fTapNoteOffset < 0.0f );
msg.SetParam( "TapNoteOffset", tn.result.fTapNoteOffset );
+ if ( tns == TNS_Miss )
+ msg.SetParam( "HeldMiss", tn.result.bHeld );
+
Lua* L= LUA->Get();
lua_createtable( L, 0, m_NoteData.GetNumTracks() ); // TapNotes this row
lua_createtable( L, 0, m_NoteData.GetNumTracks() ); // HoldHeads of tracks held at this row.