diff --git a/src/GameConstantsAndTypes.cpp b/src/GameConstantsAndTypes.cpp
index 12417ca98e..2c6e4d1363 100644
--- a/src/GameConstantsAndTypes.cpp
+++ b/src/GameConstantsAndTypes.cpp
@@ -283,6 +283,7 @@ static const char *TimingWindowNames[] = {
"Checkpoint"
};
XToString( TimingWindow );
+LuaXType( TimingWindow );
static const char *ScoreEventNames[] = {
"CheckpointHit",
diff --git a/src/GameConstantsAndTypes.h b/src/GameConstantsAndTypes.h
index 1099eb82a5..5caea2055e 100644
--- a/src/GameConstantsAndTypes.h
+++ b/src/GameConstantsAndTypes.h
@@ -294,9 +294,11 @@ enum TimingWindow
TW_Hold,
TW_Roll,
TW_Checkpoint,
- NUM_TimingWindow
+ NUM_TimingWindow,
+ TimingWindow_Invalid,
};
const RString& TimingWindowToString( TimingWindow tw );
+LuaDeclareType( TimingWindow );
/** @brief The list of score events that can take place while playing. */
enum ScoreEvent
diff --git a/src/Player.cpp b/src/Player.cpp
index 145d50d3ad..e2ed301697 100644
--- a/src/Player.cpp
+++ b/src/Player.cpp
@@ -1002,11 +1002,17 @@ void Player::Update( float fDeltaTime )
// 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));
+ const auto &disabledWindows = m_pPlayerState->m_PlayerOptions.GetCurrent().m_twDisabledWindows;
+ if (disabledWindows.find(TW_W1) == disabledWindows.end())
+ largestWindow = max(largestWindow, GetWindowSeconds(TW_W1));
+ if (disabledWindows.find(TW_W2) == disabledWindows.end())
+ largestWindow = max(largestWindow, GetWindowSeconds(TW_W2));
+ if (disabledWindows.find(TW_W3) == disabledWindows.end())
+ largestWindow = max(largestWindow, GetWindowSeconds(TW_W3));
+ if (disabledWindows.find(TW_W4) == disabledWindows.end())
+ largestWindow = max(largestWindow, GetWindowSeconds(TW_W4));
+ if (disabledWindows.find(TW_W5) == disabledWindows.end())
+ 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)
@@ -2264,18 +2270,31 @@ void Player::Step( int col, int row, const RageTimer &tm, bool bHeld, bool bRele
// bug. (It was fNoteOffset > 0.f before) -DaisuMaster
if( !REQUIRE_STEP_ON_HOLD_HEADS && ( fNoteOffset <= GetWindowSeconds( TW_W5 ) && GetWindowSeconds( TW_W5 ) != 0 ) )
{
- score = TNS_W1;
+ // Set it to the first non-disabled window.
+ const auto &disabledWindows = m_pPlayerState->m_PlayerOptions.GetCurrent().m_twDisabledWindows;
+ if (disabledWindows.find(TW_W1) == disabledWindows.end())
+ score = TNS_W1;
+ else if (disabledWindows.find(TW_W2) == disabledWindows.end())
+ score = TNS_W2;
+ else if (disabledWindows.find(TW_W3) == disabledWindows.end())
+ score = TNS_W3;
+ else if (disabledWindows.find(TW_W4) == disabledWindows.end())
+ score = TNS_W4;
+ else if (disabledWindows.find(TW_W5) == disabledWindows.end())
+ score = TNS_W5;
+
break;
}
// Fall through to default.
default:
if( (pTN->type == TapNoteType_Lift) == bRelease )
{
- if( fSecondsFromExact <= GetWindowSeconds(TW_W1) ) score = TNS_W1;
- else if( fSecondsFromExact <= GetWindowSeconds(TW_W2) ) score = TNS_W2;
- else if( fSecondsFromExact <= GetWindowSeconds(TW_W3) ) score = TNS_W3;
- else if( fSecondsFromExact <= GetWindowSeconds(TW_W4) ) score = TNS_W4;
- else if( fSecondsFromExact <= GetWindowSeconds(TW_W5) ) score = TNS_W5;
+ const auto &disabledWindows = m_pPlayerState->m_PlayerOptions.GetCurrent().m_twDisabledWindows;
+ if( fSecondsFromExact <= GetWindowSeconds(TW_W1) && disabledWindows.find(TW_W1) == disabledWindows.end()) score = TNS_W1;
+ else if( fSecondsFromExact <= GetWindowSeconds(TW_W2) && disabledWindows.find(TW_W2) == disabledWindows.end()) score = TNS_W2;
+ else if( fSecondsFromExact <= GetWindowSeconds(TW_W3) && disabledWindows.find(TW_W3) == disabledWindows.end()) score = TNS_W3;
+ else if( fSecondsFromExact <= GetWindowSeconds(TW_W4) && disabledWindows.find(TW_W4) == disabledWindows.end()) score = TNS_W4;
+ else if( fSecondsFromExact <= GetWindowSeconds(TW_W5) && disabledWindows.find(TW_W5) == disabledWindows.end()) score = TNS_W5;
}
break;
}
@@ -2283,15 +2302,32 @@ void Player::Step( int col, int row, const RageTimer &tm, bool bHeld, bool bRele
case PC_CPU:
case PC_AUTOPLAY:
- score = PlayerAI::GetTapNoteScore( m_pPlayerState );
+ {
+ score = PlayerAI::GetTapNoteScore(m_pPlayerState);
+ const auto& disabledWindows = m_pPlayerState->m_PlayerOptions.GetCurrent().m_twDisabledWindows;
+ for (int i = score; i >= TNS_W5; i--)
+ {
+ TapNoteScore cur_score = (TapNoteScore)i;
+ // Downgrade the TapNoteScore if that specific window is disabled.
+ if (cur_score == TNS_W1 && disabledWindows.find(TW_W1) == disabledWindows.end())
+ score = TNS_W2;
+ else if (cur_score == TNS_W2 && disabledWindows.find(TW_W2) == disabledWindows.end())
+ score = TNS_W3;
+ else if (cur_score == TNS_W3 && disabledWindows.find(TW_W3) == disabledWindows.end())
+ score = TNS_W4;
+ else if (cur_score == TNS_W4 && disabledWindows.find(TW_W4) == disabledWindows.end())
+ score = TNS_W5;
+ else if (cur_score == TNS_W5 && disabledWindows.find(TW_W5) == disabledWindows.end())
+ score = TNS_None;
+ }
/* XXX: This doesn't make sense.
* Step should only be called in autoplay for hit notes. */
#if 0
- // GetTapNoteScore always returns TNS_W1 in autoplay.
- // If the step is far away, don't judge it.
- if( m_pPlayerState->m_PlayerController == PC_AUTOPLAY &&
- fSecondsFromExact > GetWindowSeconds(TW_W5) )
+ // GetTapNoteScore always returns TNS_W1 in autoplay.
+ // If the step is far away, don't judge it.
+ if (m_pPlayerState->m_PlayerController == PC_AUTOPLAY &&
+ fSecondsFromExact > GetWindowSeconds(TW_W5))
{
score = TNS_None;
break;
@@ -2300,36 +2336,36 @@ void Player::Step( int col, int row, const RageTimer &tm, bool bHeld, bool bRele
// TRICKY: We're asking the AI to judge mines. Consider TNS_W4 and
// below as "mine was hit" and everything else as "mine was avoided"
- if( pTN->type == TapNoteType_Mine )
+ if (pTN->type == TapNoteType_Mine)
{
// The CPU hits a lot of mines. Only consider hitting the
// first mine for a row. We know we're the first mine if
// there are are no mines to the left of us.
- for( int t=0; t
= get_to_avoid )
+ if (score >= get_to_avoid)
return; // avoided
else
score = TNS_HitMine;
}
- if( pTN->type == TapNoteType_Attack && score > TNS_W4 )
+ if (pTN->type == TapNoteType_Attack && score > TNS_W4)
score = TNS_W2; // sentinel
/* AI will generate misses here. Don't handle a miss like a regular
* note because we want the judgment animation to appear delayed.
* Instead, return early if AI generated a miss, and let
* UpdateTapNotesMissedOlderThan() detect and handle the misses. */
- if( score == TNS_Miss )
+ if (score == TNS_Miss)
return;
// Put some small, random amount in fNoteOffset so that demonstration
@@ -2347,7 +2383,7 @@ void Player::Step( int col, int row, const RageTimer &tm, bool bHeld, bool bRele
float fWindowW5 = GetWindowSeconds(TW_W5);
// W1 is the top judgment, there is no overlap.
- if( score == TNS_W1 )
+ if (score == TNS_W1)
fNoteOffset = randomf(-fWindowW1, fWindowW1);
else
{
@@ -2355,25 +2391,25 @@ void Player::Step( int col, int row, const RageTimer &tm, bool bHeld, bool bRele
float fLowerBound = 0.0f; // negative upper limit
float fUpperBound = 0.0f; // positive lower limit
float fCompareWindow = 0.0f; // filled in here:
- if( score == TNS_W2 )
+ if (score == TNS_W2)
{
fLowerBound = -fWindowW1;
fUpperBound = fWindowW1;
fCompareWindow = fWindowW2;
}
- else if( score == TNS_W3 )
+ else if (score == TNS_W3)
{
fLowerBound = -fWindowW2;
fUpperBound = fWindowW2;
fCompareWindow = fWindowW3;
}
- else if( score == TNS_W4 )
+ else if (score == TNS_W4)
{
fLowerBound = -fWindowW3;
fUpperBound = fWindowW3;
fCompareWindow = fWindowW4;
}
- else if( score == TNS_W5 )
+ else if (score == TNS_W5)
{
fLowerBound = -fWindowW4;
fUpperBound = fWindowW4;
@@ -2382,7 +2418,7 @@ void Player::Step( int col, int row, const RageTimer &tm, bool bHeld, bool bRele
float f1 = randomf(-fCompareWindow, fLowerBound);
float f2 = randomf(fUpperBound, fCompareWindow);
- if(randomf() * 100 >= 50)
+ if (randomf() * 100 >= 50)
fNoteOffset = f1;
else
fNoteOffset = f2;
@@ -2399,6 +2435,7 @@ void Player::Step( int col, int row, const RageTimer &tm, bool bHeld, bool bRele
fNoteOffset = TapNoteOffset attribute
break;
*/
+ }
default:
FAIL_M(ssprintf("Invalid player controller type: %i", m_pPlayerState->m_PlayerController));
}
diff --git a/src/PlayerOptions.cpp b/src/PlayerOptions.cpp
index 081aefb367..f319ff65e8 100644
--- a/src/PlayerOptions.cpp
+++ b/src/PlayerOptions.cpp
@@ -10,6 +10,7 @@
#include "Style.h"
#include "CommonMetrics.h"
#include
+#include
static const char *LifeTypeNames[] = {
"Bar",
@@ -96,6 +97,7 @@ void PlayerOptions::Init()
m_bCosecant = false;
m_sNoteSkin = "";
m_fVisualDelay = 0.0f;
+ m_twDisabledWindows.clear();
ZERO( m_fMovesX ); ONE( m_SpeedfMovesX );
ZERO( m_fMovesY ); ONE( m_SpeedfMovesY );
ZERO( m_fMovesZ ); ONE( m_SpeedfMovesZ );
@@ -187,6 +189,7 @@ void PlayerOptions::Approach( const PlayerOptions& other, float fDeltaSeconds )
DO_COPY( m_MinTNSToHideNotes );
DO_COPY( m_sNoteSkin );
DO_COPY( m_fVisualDelay );
+ DO_COPY( m_twDisabledWindows );
#undef APPROACH
#undef DO_COPY
}
@@ -564,6 +567,20 @@ void PlayerOptions::GetMods( vector &AddTo, bool bForceNoteSkin ) const
// Note that we don't process sub-millisecond visual delay.
AddTo.push_back( ssprintf("%.0fms VisualDelay", m_fVisualDelay * 1000.0f) );
}
+
+ if (!m_twDisabledWindows.empty()) {
+ std::stringstream ss;
+ ss << "No ";
+ for (auto it=m_twDisabledWindows.begin(); it != m_twDisabledWindows.end(); ++it) {
+ if (it != m_twDisabledWindows.begin()) {
+ ss << "/";
+ }
+ ss << TimingWindowToString(*it).c_str();
+ }
+
+ // Final string will be something like "No W4/W5"
+ AddTo.push_back(ss.str());
+ }
}
/* Options are added to the current settings; call Init() beforehand if
@@ -644,6 +661,7 @@ bool PlayerOptions::FromOneModString( const RString &sOneMod, RString &sErrorOut
const bool on = (level > 0.5f);
static Regex mult("^([0-9]+(\\.[0-9]+)?)x$");
+ static Regex disabledWindows("(w[1-5])/{0,1}");
vector matches;
if( mult.Compare(sBit, matches) )
{
@@ -1142,6 +1160,20 @@ bool PlayerOptions::FromOneModString( const RString &sOneMod, RString &sErrorOut
else if( sBit == "zbuffer" ) m_bZBuffer = on;
else if( sBit == "cosecant" ) m_bCosecant = on;
else if( sBit == "visualdelay" ) m_fVisualDelay = level;
+ else if( disabledWindows.Compare(sBit, matches)) {
+ for (auto& match : matches) {
+ static std::map name_to_window = {
+ {"w1", TW_W1},
+ {"w2", TW_W2},
+ {"w3", TW_W3},
+ {"w4", TW_W4},
+ {"w5", TW_W5},
+ };
+ if (name_to_window.find(match) != name_to_window.end()) {
+ m_twDisabledWindows.insert(name_to_window[match]);
+ }
+ }
+ }
// deprecated mods/left in for compatibility
else if( sBit == "converge" ) SET_FLOAT( fScrolls[SCROLL_CENTERED] )
// end of the list
@@ -1395,6 +1427,7 @@ bool PlayerOptions::operator==( const PlayerOptions &other ) const
return false;
}
COMPARE(m_fVisualDelay);
+ COMPARE(m_twDisabledWindows); // != is defined correctly for ordered sets.
for( int i = 0; i < PlayerOptions::NUM_ACCELS; ++i )
COMPARE(m_fAccels[i]);
for( int i = 0; i < PlayerOptions::NUM_EFFECTS; ++i )
@@ -1461,6 +1494,7 @@ PlayerOptions& PlayerOptions::operator=(PlayerOptions const& other)
CPY(m_bZBuffer);
CPY(m_bCosecant);
CPY(m_fVisualDelay);
+ CPY(m_twDisabledWindows);
CPY_SPEED(fDark);
CPY_SPEED(fBlind);
CPY_SPEED(fCover);
@@ -1693,6 +1727,7 @@ RString PlayerOptions::GetSavedPrefsString() const
SAVE( m_bMuteOnError );
SAVE( m_sNoteSkin );
SAVE( m_fVisualDelay );
+ SAVE( m_twDisabledWindows );
#undef SAVE
return po_prefs.GetString();
}
@@ -1741,6 +1776,7 @@ void PlayerOptions::ResetPrefs( ResetPrefsType type )
// Don't clear this.
// CPY( m_sNoteSkin );
CPY(m_fVisualDelay);
+ CPY(m_twDisabledWindows);
#undef CPY
}
@@ -1991,6 +2027,44 @@ public:
FLOAT_NO_SPEED_INTERFACE(VisualDelay, VisualDelay, true);
+ static int DisableTimingWindow(T* p, lua_State* L)
+ {
+ int original_top= lua_gettop(L);
+ if (original_top >= 1 && !lua_isnil(L, 1))
+ {
+ // Insert the specified TNS into the disabled windows set.
+ p->m_twDisabledWindows.insert(Enum::Check(L, 1));
+ }
+ // Construct a new table indicating all of the disabled windows.
+ lua_newtable( L );
+ for (TimingWindow window : p->m_twDisabledWindows)
+ {
+ Enum::Push(L, window);
+ }
+ OPTIONAL_RETURN_SELF(original_top);
+ return 1;
+ }
+
+ static int ResetDisabledTimingWindows(T* p, lua_State* L)
+ {
+ int original_top= lua_gettop(L);
+ p->m_twDisabledWindows.clear();
+ OPTIONAL_RETURN_SELF(original_top);
+ return 1;
+ }
+
+ static int GetDisabledTimingWindows(T* p, lua_State* L)
+ {
+ int original_top= lua_gettop(L);
+ lua_newtable( L );
+ for (TimingWindow window : p->m_twDisabledWindows)
+ {
+ Enum::Push(L, window);
+ }
+ OPTIONAL_RETURN_SELF(original_top);
+ return 1;
+ }
+
// NoteSkins
static int NoteSkin(T* p, lua_State* L)
{
@@ -2509,12 +2583,15 @@ public:
ADD_MULTICOL_METHOD(Bumpy);
ADD_MULTICOL_METHOD(Reverse);
-
ADD_METHOD(NoteSkin);
ADD_METHOD(FailSetting);
ADD_METHOD(MinTNSToHideNotes);
ADD_METHOD(VisualDelay);
+ ADD_METHOD(DisableTimingWindow);
+ ADD_METHOD(ResetDisabledTimingWindows);
+ ADD_METHOD(GetDisabledTimingWindows);
+
// Speed
ADD_METHOD( CMod );
ADD_METHOD( XMod );
diff --git a/src/PlayerOptions.h b/src/PlayerOptions.h
index b2ea2931bf..3ff8d3ec52 100644
--- a/src/PlayerOptions.h
+++ b/src/PlayerOptions.h
@@ -9,6 +9,8 @@ struct lua_State;
#define ONE( arr ) { for( unsigned Z = 0; Z < ARRAYLEN(arr); ++Z ) arr[Z]=1.0f; }
+#include
+
#include "GameConstantsAndTypes.h"
#include "PlayerNumber.h"
#include "PrefsManager.h"
@@ -398,6 +400,12 @@ public:
/** @brief The Visual Delay additionally applied on a per-player basis in ms. */
float m_fVisualDelay;
+ /** @brief The TimingWindow that can be disabled. Valid values are only W1-W5.
+ * Other values are ignore.
+ * We use a set instead of unordered_set because we want the ordering so that
+ * the generated player options string is consistent. */
+ std::set m_twDisabledWindows;
+
void NextAccel();
void NextEffect();
void NextAppearance();