#include "global.h" #include "InputFilter.h" #include "RageLog.h" #include "RageInput.h" #include "RageUtil.h" #include "RageThreads.h" #include "Preference.h" #include "Foreach.h" // for mouse stuff: -aj #include "PrefsManager.h" #include "ScreenDimensions.h" #include struct ButtonState { ButtonState(); bool m_BeingHeld; // actual current state bool m_bLastReportedHeld; // last state reported by Update() RString m_sComment; float m_fSecsHeld; DeviceInput m_DeviceInput; // Timestamp of m_BeingHeld changing. RageTimer m_BeingHeldTime; // The time that we actually reported the last event (used for debouncing). RageTimer m_LastReportTime; // The timestamp of the last reported change. Unlike m_BeingHeldTime, this // value is debounced along with the input state. (This is the same as // m_fSecsHeld, except this isn't affected by Update scaling.) RageTimer m_LastInputTime; }; struct DeviceButtonPair { InputDevice device; DeviceButton button; DeviceButtonPair( InputDevice d, DeviceButton b ): device(d), button(b){ } bool operator<( const DeviceButtonPair &other ) const { if( device != other.device ) return device < other.device; return button < other.button; } }; namespace { /* Maintain a set of all interesting buttons: buttons which are being held * down, or which were held down and need a RELEASE event. We use this to * optimize InputFilter::Update, so we don't have to process every button * we know about when most of them aren't in use. This set is protected * by queuemutex. */ typedef map ButtonStateMap; ButtonStateMap g_ButtonStates; ButtonState &GetButtonState( const DeviceInput &di ) { DeviceButtonPair db(di.device, di.button); ButtonState &bs = g_ButtonStates[db]; bs.m_DeviceInput.button = di.button; bs.m_DeviceInput.device = di.device; return bs; } DeviceInputList g_CurrentState; set g_DisableRepeat; } /* Some input devices require debouncing. Do this on both press and release. * After reporting a change in state, don't report another for the debounce * period. If a button is reported pressed, report it. If the button is * immediately reported released, wait a period before reporting it; if the * button is repressed during that time, the release is never reported. * The detail is important: if a button is pressed for 1ms and released, we must * always report it, even if the debounce period is 10ms, since it might be a * coin counter with a very short signal. The only time we discard events is if * a button is pressed, released and then pressed again quickly. * * This delay in events is ordinarily not noticable, because we report initial * presses and releases immediately. However, if a real press is ever delayed, * this won't cause timing problems, because the event timestamp is preserved. */ static Preference g_fInputDebounceTime( "InputDebounceTime", 0 ); InputFilter* INPUTFILTER = NULL; // global and accessable from anywhere in our program static const float TIME_BEFORE_REPEATS = 0.375f; static const float REPEATS_PER_SEC = 8; static float g_fTimeBeforeRepeats, g_fTimeBetweenRepeats; InputFilter::InputFilter() { queuemutex = new RageMutex("InputFilter"); Reset(); ResetRepeatRate(); m_MouseCoords.fX = 0; m_MouseCoords.fY = 0; m_MouseCoords.fZ = 0; // Register with Lua. { Lua *L = LUA->Get(); lua_pushstring( L, "INPUTFILTER" ); this->PushSelf( L ); lua_settable( L, LUA_GLOBALSINDEX ); LUA->Release( L ); } } InputFilter::~InputFilter() { delete queuemutex; g_ButtonStates.clear(); // Unregister with Lua. LUA->UnsetGlobal( "INPUTFILTER" ); } void InputFilter::Reset() { FOREACH_InputDevice( i ) ResetDevice( InputDevice(i) ); } void InputFilter::SetRepeatRate( float fRepeatRate ) { g_fTimeBetweenRepeats = 1/fRepeatRate; } void InputFilter::SetRepeatDelay( float fDelay ) { g_fTimeBeforeRepeats = fDelay; } void InputFilter::ResetRepeatRate() { SetRepeatRate( REPEATS_PER_SEC ); SetRepeatDelay( TIME_BEFORE_REPEATS ); } ButtonState::ButtonState(): m_BeingHeldTime(RageZeroTimer), m_LastReportTime(RageZeroTimer) { m_BeingHeld = false; m_bLastReportedHeld = false; m_fSecsHeld = 0; } void InputFilter::ButtonPressed( const DeviceInput &di ) { LockMut(*queuemutex); if( di.ts.IsZero() ) LOG->Warn( "InputFilter::ButtonPressed: zero timestamp is invalid" ); ASSERT_M( di.device < NUM_InputDevice, ssprintf("Invalid device %i", di.device) ); ASSERT_M( di.button < NUM_DeviceButton, ssprintf("Invalid button %i", di.button) ); ButtonState &bs = GetButtonState( di ); // Flush any delayed input, like Update() (in case Update() isn't being called). RageTimer now; CheckButtonChange( bs, di, now ); bs.m_DeviceInput = di; bool Down = di.bDown; if( bs.m_BeingHeld != Down ) { bs.m_BeingHeld = Down; bs.m_BeingHeldTime = di.ts; } // Try to report presses immediately. MakeButtonStateList( g_CurrentState ); CheckButtonChange( bs, di, now ); } void InputFilter::SetButtonComment( const DeviceInput &di, const RString &sComment ) { LockMut(*queuemutex); ButtonState &bs = GetButtonState( di ); bs.m_sComment = sComment; } /** @brief Release all buttons on the given device. */ void InputFilter::ResetDevice( InputDevice device ) { LockMut(*queuemutex); RageTimer now; const ButtonStateMap ButtonStates( g_ButtonStates ); for (std::pair const &b : ButtonStates) { const DeviceButtonPair &db = b.first; if( db.device == device ) ButtonPressed( DeviceInput(device, db.button, 0, now) ); } } /** @brief Check for reportable presses. */ void InputFilter::CheckButtonChange( ButtonState &bs, DeviceInput di, const RageTimer &now ) { if( bs.m_BeingHeld == bs.m_bLastReportedHeld ) return; /* If the last IET_FIRST_PRESS or IET_RELEASE event was sent too recently, * wait a while before sending it. */ if( now - bs.m_LastReportTime < g_fInputDebounceTime ) return; bs.m_LastReportTime = now; bs.m_bLastReportedHeld = bs.m_BeingHeld; bs.m_fSecsHeld = 0; bs.m_LastInputTime = bs.m_BeingHeldTime; di.ts = bs.m_BeingHeldTime; if( !bs.m_bLastReportedHeld ) di.level = 0; MakeButtonStateList( g_CurrentState ); ReportButtonChange( di, bs.m_bLastReportedHeld? IET_FIRST_PRESS:IET_RELEASE ); if( !bs.m_bLastReportedHeld ) g_DisableRepeat.erase( di ); } void InputFilter::ReportButtonChange( const DeviceInput &di, InputEventType t ) { queue.push_back( InputEvent() ); InputEvent &ie = queue.back(); ie.type = t; ie.di = di; /* Include a list of all buttons that were pressed at the time of this event. * We can create this efficiently using g_ButtonStates. Use a vector and not * a map, for efficiency; most code will not use this information. Iterating * over g_ButtonStates will be in DeviceInput order, so users can binary * search this list (eg. std::lower_bound). */ ie.m_ButtonState = g_CurrentState; } void InputFilter::MakeButtonStateList( vector &aInputOut ) const { aInputOut.clear(); aInputOut.reserve( g_ButtonStates.size() ); for( ButtonStateMap::const_iterator it = g_ButtonStates.begin(); it != g_ButtonStates.end(); ++it ) { const ButtonState &bs = it->second; aInputOut.push_back( bs.m_DeviceInput ); aInputOut.back().ts = bs.m_LastInputTime; aInputOut.back().bDown = bs.m_bLastReportedHeld; } } void InputFilter::Update( float fDeltaTime ) { RageTimer now; INPUTMAN->Update(); /* Make sure that nothing gets inserted while we do this, to prevent things * like "key pressed, key release, key repeat". */ LockMut(*queuemutex); DeviceInput di( InputDevice_Invalid, DeviceButton_Invalid, 1.0f, now ); MakeButtonStateList( g_CurrentState ); vector ButtonsToErase; FOREACHM( DeviceButtonPair, ButtonState, g_ButtonStates, b ) { di.device = b->first.device; di.button = b->first.button; ButtonState &bs = b->second; // Generate IET_FIRST_PRESS and IET_RELEASE events that were delayed. CheckButtonChange( bs, di, now ); // Generate IET_REPEAT events. if( !bs.m_bLastReportedHeld ) { // If the key isn't pressed, and hasn't been pressed for a while // (so debouncing isn't interested in it), purge the entry. if( now - bs.m_LastReportTime > g_fInputDebounceTime && bs.m_DeviceInput.level == 0.0f ) ButtonsToErase.push_back( b ); continue; } // If repeats are disabled for this button, skip. if( g_DisableRepeat.find(di) != g_DisableRepeat.end() ) continue; const float fOldHoldTime = bs.m_fSecsHeld; bs.m_fSecsHeld += fDeltaTime; const float fNewHoldTime = bs.m_fSecsHeld; if( fNewHoldTime <= g_fTimeBeforeRepeats ) continue; float fRepeatTime; if( fOldHoldTime < g_fTimeBeforeRepeats ) { fRepeatTime = g_fTimeBeforeRepeats; } else { float fAdjustedOldHoldTime = fOldHoldTime - g_fTimeBeforeRepeats; float fAdjustedNewHoldTime = fNewHoldTime - g_fTimeBeforeRepeats; if( int(fAdjustedOldHoldTime/g_fTimeBetweenRepeats) == int(fAdjustedNewHoldTime/g_fTimeBetweenRepeats) ) continue; fRepeatTime = ftruncf( fNewHoldTime, g_fTimeBetweenRepeats ); } /* Set the timestamp to the exact time of the repeat. This way, as long * as tab/` aren't being used, the timestamp will always increase steadily * during repeats. */ di.ts = bs.m_LastInputTime + fRepeatTime; ReportButtonChange( di, IET_REPEAT ); } FOREACH( ButtonStateMap::iterator, ButtonsToErase, it ) g_ButtonStates.erase( *it ); } template const T *FindItemBinarySearch( IT begin, IT end, const T &i ) { IT it = lower_bound( begin, end, i ); if( it == end || *it != i ) return NULL; return &*it; } bool InputFilter::IsBeingPressed( const DeviceInput &di, const DeviceInputList *pButtonState ) const { LockMut(*queuemutex); if( pButtonState == NULL ) pButtonState = &g_CurrentState; const DeviceInput *pDI = FindItemBinarySearch( pButtonState->begin(), pButtonState->end(), di ); return pDI != NULL && pDI->bDown; } float InputFilter::GetSecsHeld( const DeviceInput &di, const DeviceInputList *pButtonState ) const { LockMut(*queuemutex); if( pButtonState == NULL ) pButtonState = &g_CurrentState; const DeviceInput *pDI = FindItemBinarySearch( pButtonState->begin(), pButtonState->end(), di ); if( pDI == NULL ) return 0; return pDI->ts.Ago(); } float InputFilter::GetLevel( const DeviceInput &di, const DeviceInputList *pButtonState ) const { LockMut(*queuemutex); if( pButtonState == NULL ) pButtonState = &g_CurrentState; const DeviceInput *pDI = FindItemBinarySearch( pButtonState->begin(), pButtonState->end(), di ); if( pDI == NULL ) return 0.0f; return pDI->level; } RString InputFilter::GetButtonComment( const DeviceInput &di ) const { LockMut(*queuemutex); return GetButtonState( di ).m_sComment; } void InputFilter::ResetKeyRepeat( const DeviceInput &di ) { LockMut(*queuemutex); GetButtonState( di ).m_fSecsHeld = 0; } /** @brief Stop repeating the specified key until released. */ void InputFilter::RepeatStopKey( const DeviceInput &di ) { LockMut(*queuemutex); // If the button is up, do nothing. ButtonState &bs = GetButtonState( di ); if( !bs.m_bLastReportedHeld ) return; g_DisableRepeat.insert( di ); } void InputFilter::GetInputEvents( vector &array ) { array.clear(); LockMut(*queuemutex); array.swap( queue ); } void InputFilter::GetPressedButtons( vector &array ) const { LockMut(*queuemutex); array = g_CurrentState; } void InputFilter::UpdateCursorLocation(float _fX, float _fY) { m_MouseCoords.fX = _fX; m_MouseCoords.fY = _fY; } void InputFilter::UpdateMouseWheel(float _fZ) { m_MouseCoords.fZ = _fZ; } // lua start #include "LuaBinding.h" /** @brief Allow Lua to have access to InputFilter. */ class LunaInputFilter: public Luna { public: // todo: Should the input be locked to the theme's width/height instead of // the window's width/height? -aj static int GetMouseX( T* p, lua_State *L ){ float fX = p->GetCursorX(); // Scale input to the theme's dimensions fX = SCALE( fX, 0, (PREFSMAN->m_iDisplayHeight * PREFSMAN->m_fDisplayAspectRatio), SCREEN_LEFT, SCREEN_RIGHT ); lua_pushnumber( L, fX ); return 1; } static int GetMouseY( T* p, lua_State *L ){ float fY = p->GetCursorY(); // Scale input to the theme's dimensions fY = SCALE( fY, 0, PREFSMAN->m_iDisplayHeight, SCREEN_TOP, SCREEN_BOTTOM ); lua_pushnumber( L, fY ); return 1; } static int GetMouseWheel( T* p, lua_State *L ){ float fZ = p->GetMouseWheel(); lua_pushnumber( L, fZ ); return 1; } LunaInputFilter() { ADD_METHOD( GetMouseX ); ADD_METHOD( GetMouseY ); ADD_METHOD( GetMouseWheel ); } }; LUA_REGISTER_CLASS( InputFilter ) // lua end /* * (c) 2001-2004 Chris Danford * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, and/or sell copies of the Software, and to permit persons to * whom the Software is furnished to do so, provided that the above * copyright notice(s) and this permission notice appear in all copies of * the Software and that both the above copyright notice(s) and this * permission notice appear in supporting documentation. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF * THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS * INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT * OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS * OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR * PERFORMANCE OF THIS SOFTWARE. */