2003-02-16 04:01:45 +00:00
|
|
|
#include "global.h"
|
2002-04-16 17:31:00 +00:00
|
|
|
#include "InputFilter.h"
|
2002-05-01 19:14:55 +00:00
|
|
|
#include "RageLog.h"
|
2002-04-16 17:31:00 +00:00
|
|
|
#include "RageInput.h"
|
2005-06-06 18:30:12 +00:00
|
|
|
#include "RageUtil.h"
|
2003-07-26 21:58:48 +00:00
|
|
|
#include "RageThreads.h"
|
2005-09-19 07:09:55 +00:00
|
|
|
#include "Preference.h"
|
2005-10-24 06:32:30 +00:00
|
|
|
#include "Foreach.h"
|
2002-06-14 22:25:22 +00:00
|
|
|
|
2005-10-24 06:32:30 +00:00
|
|
|
#include <set>
|
2005-12-28 20:41:24 +00:00
|
|
|
struct ButtonState
|
|
|
|
|
{
|
|
|
|
|
ButtonState();
|
|
|
|
|
bool m_BeingHeld; // actual current state
|
|
|
|
|
bool m_bLastReportedHeld; // last state reported by Update()
|
2006-01-22 01:00:06 +00:00
|
|
|
RString m_sComment;
|
2005-12-28 20:41:24 +00:00
|
|
|
float m_fSecsHeld;
|
|
|
|
|
float m_Level, m_LastLevel;
|
|
|
|
|
|
|
|
|
|
// Timestamp of m_BeingHeld changing.
|
|
|
|
|
RageTimer m_BeingHeldTime;
|
|
|
|
|
|
|
|
|
|
// The time that we actually reported the last event (used for debouncing).
|
|
|
|
|
RageTimer m_LastReportTime;
|
|
|
|
|
};
|
|
|
|
|
|
2005-10-24 06:32:30 +00:00
|
|
|
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. */
|
2005-12-28 20:41:24 +00:00
|
|
|
typedef map<DeviceInput, ButtonState> ButtonStateMap;
|
|
|
|
|
ButtonStateMap g_ButtonStates;
|
2006-09-02 19:34:54 +00:00
|
|
|
ButtonState &GetButtonState( const DeviceInput &di )
|
2005-10-24 06:32:30 +00:00
|
|
|
{
|
2006-09-02 19:34:54 +00:00
|
|
|
return g_ButtonStates[di];
|
2005-10-24 06:32:30 +00:00
|
|
|
}
|
|
|
|
|
}
|
2005-11-06 15:24:26 +00:00
|
|
|
|
2005-09-19 07:09:55 +00:00
|
|
|
/*
|
|
|
|
|
* 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 the 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<float> g_fInputDebounceTime( "InputDebounceTime", 0 );
|
2002-04-16 17:31:00 +00:00
|
|
|
|
2002-05-19 01:59:48 +00:00
|
|
|
InputFilter* INPUTFILTER = NULL; // global and accessable from anywhere in our program
|
2002-04-16 17:31:00 +00:00
|
|
|
|
2005-09-22 23:59:31 +00:00
|
|
|
static const float TIME_BEFORE_SLOW_REPEATS = 0.375f;
|
2003-11-25 21:46:30 +00:00
|
|
|
static const float TIME_BEFORE_FAST_REPEATS = 1.5f;
|
|
|
|
|
|
2005-09-22 23:22:18 +00:00
|
|
|
static const float REPEATS_PER_SEC = 8;
|
2003-11-25 21:46:30 +00:00
|
|
|
|
2005-09-22 23:22:18 +00:00
|
|
|
static float g_fTimeBeforeSlow, g_fTimeBeforeFast, g_fTimeBetweenRepeats;
|
2003-07-26 21:58:48 +00:00
|
|
|
|
|
|
|
|
|
2002-09-08 06:10:23 +00:00
|
|
|
InputFilter::InputFilter()
|
2003-06-22 04:38:05 +00:00
|
|
|
{
|
2004-03-21 04:10:43 +00:00
|
|
|
queuemutex = new RageMutex("InputFilter");
|
2003-09-03 21:49:11 +00:00
|
|
|
|
2003-07-26 22:29:34 +00:00
|
|
|
Reset();
|
2003-11-25 21:46:30 +00:00
|
|
|
ResetRepeatRate();
|
2003-07-26 21:58:48 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
InputFilter::~InputFilter()
|
|
|
|
|
{
|
|
|
|
|
delete queuemutex;
|
2005-12-28 20:41:24 +00:00
|
|
|
g_ButtonStates.clear();
|
2003-06-22 04:38:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void InputFilter::Reset()
|
2002-09-08 06:10:23 +00:00
|
|
|
{
|
|
|
|
|
for( int i=0; i<NUM_INPUT_DEVICES; i++ )
|
2003-06-25 06:33:18 +00:00
|
|
|
ResetDevice( InputDevice(i) );
|
2002-09-08 06:10:23 +00:00
|
|
|
}
|
|
|
|
|
|
2005-09-22 23:22:18 +00:00
|
|
|
void InputFilter::SetRepeatRate( float fSlowDelay, float fFastDelay, float fRepeatRate )
|
2003-11-25 21:46:30 +00:00
|
|
|
{
|
|
|
|
|
g_fTimeBeforeSlow = fSlowDelay;
|
|
|
|
|
g_fTimeBeforeFast = fFastDelay;
|
2005-09-22 23:22:18 +00:00
|
|
|
g_fTimeBetweenRepeats = 1/fRepeatRate;
|
2003-11-25 21:46:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void InputFilter::ResetRepeatRate()
|
|
|
|
|
{
|
2005-09-22 23:22:18 +00:00
|
|
|
SetRepeatRate( TIME_BEFORE_SLOW_REPEATS, TIME_BEFORE_FAST_REPEATS, REPEATS_PER_SEC );
|
2003-11-25 21:46:30 +00:00
|
|
|
}
|
|
|
|
|
|
2005-12-28 20:41:24 +00:00
|
|
|
ButtonState::ButtonState():
|
2005-09-19 07:09:55 +00:00
|
|
|
m_BeingHeldTime(RageZeroTimer),
|
|
|
|
|
m_LastReportTime(RageZeroTimer)
|
|
|
|
|
{
|
|
|
|
|
m_BeingHeld = false;
|
|
|
|
|
m_bLastReportedHeld = false;
|
|
|
|
|
m_fSecsHeld = m_Level = m_LastLevel = 0;
|
|
|
|
|
}
|
|
|
|
|
|
2005-10-15 16:47:44 +00:00
|
|
|
void InputFilter::ButtonPressed( const DeviceInput &di, bool Down )
|
2002-09-08 06:10:23 +00:00
|
|
|
{
|
2003-09-03 21:49:11 +00:00
|
|
|
LockMut(*queuemutex);
|
|
|
|
|
|
2005-05-17 20:50:39 +00:00
|
|
|
if( di.ts.IsZero() )
|
|
|
|
|
LOG->Warn( "InputFilter::ButtonPressed: zero timestamp is invalid" );
|
|
|
|
|
|
2005-12-28 09:53:03 +00:00
|
|
|
ASSERT_M( di.device < NUM_INPUT_DEVICES, ssprintf("%i", di.device) );
|
|
|
|
|
ASSERT_M( di.button < NUM_DeviceButton, ssprintf("%i", di.button) );
|
2005-06-08 06:29:48 +00:00
|
|
|
|
2006-09-02 19:34:54 +00:00
|
|
|
ButtonState &bs = GetButtonState( di );
|
2004-09-09 22:14:33 +00:00
|
|
|
|
|
|
|
|
bs.m_Level = di.level;
|
|
|
|
|
|
2005-09-19 07:09:55 +00:00
|
|
|
if( bs.m_BeingHeld != Down )
|
|
|
|
|
{
|
2005-11-06 12:14:35 +00:00
|
|
|
/* Flush any delayed input, like Update() (in case Update() isn't being called). */
|
|
|
|
|
RageTimer now;
|
|
|
|
|
CheckButtonChange( bs, di, now );
|
|
|
|
|
|
2005-09-19 07:09:55 +00:00
|
|
|
bs.m_BeingHeld = Down;
|
|
|
|
|
bs.m_BeingHeldTime = di.ts;
|
2005-11-06 12:14:35 +00:00
|
|
|
|
|
|
|
|
/* Try to report presses immediately. */
|
|
|
|
|
CheckButtonChange( bs, di, now );
|
2005-09-19 07:09:55 +00:00
|
|
|
}
|
2003-09-03 21:49:11 +00:00
|
|
|
}
|
|
|
|
|
|
2006-01-22 01:00:06 +00:00
|
|
|
void InputFilter::SetButtonComment( const DeviceInput &di, const RString &sComment )
|
2005-05-08 09:19:35 +00:00
|
|
|
{
|
|
|
|
|
LockMut(*queuemutex);
|
2006-09-02 19:34:54 +00:00
|
|
|
ButtonState &bs = GetButtonState( di );
|
2005-05-08 09:19:35 +00:00
|
|
|
bs.m_sComment = sComment;
|
|
|
|
|
}
|
|
|
|
|
|
2003-06-21 20:58:40 +00:00
|
|
|
/* Release all buttons on the given device. */
|
2005-04-24 02:15:41 +00:00
|
|
|
void InputFilter::ResetDevice( InputDevice device )
|
2003-06-21 20:58:40 +00:00
|
|
|
{
|
2005-12-28 19:21:01 +00:00
|
|
|
LockMut(*queuemutex);
|
2005-05-17 20:50:39 +00:00
|
|
|
RageTimer now;
|
2005-12-28 20:41:24 +00:00
|
|
|
|
|
|
|
|
vector<DeviceInput> DeviceInputs;
|
|
|
|
|
GetPressedButtons( DeviceInputs );
|
|
|
|
|
|
|
|
|
|
FOREACH( DeviceInput, DeviceInputs, di )
|
2005-12-28 19:21:01 +00:00
|
|
|
{
|
2005-12-28 20:41:24 +00:00
|
|
|
if( di->device == device )
|
|
|
|
|
ButtonPressed( DeviceInput(device, di->button, -1, now), false );
|
2005-12-28 19:21:01 +00:00
|
|
|
}
|
2003-06-21 20:58:40 +00:00
|
|
|
}
|
|
|
|
|
|
2005-11-06 12:14:35 +00:00
|
|
|
/* 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,
|
2005-11-06 15:24:26 +00:00
|
|
|
* wait a while before sending it. */
|
2005-11-06 12:14:35 +00:00
|
|
|
if( now - bs.m_LastReportTime < g_fInputDebounceTime )
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
bs.m_LastReportTime = now;
|
|
|
|
|
bs.m_bLastReportedHeld = bs.m_BeingHeld;
|
|
|
|
|
bs.m_fSecsHeld = 0;
|
|
|
|
|
|
|
|
|
|
di.ts = bs.m_BeingHeldTime;
|
|
|
|
|
queue.push_back( InputEvent(di,bs.m_bLastReportedHeld? IET_FIRST_PRESS:IET_RELEASE) );
|
|
|
|
|
}
|
|
|
|
|
|
2005-11-06 13:00:34 +00:00
|
|
|
void InputFilter::Update( float fDeltaTime )
|
2002-04-16 17:31:00 +00:00
|
|
|
{
|
2003-09-03 21:49:11 +00:00
|
|
|
RageTimer now;
|
|
|
|
|
|
2005-12-18 22:53:47 +00:00
|
|
|
INPUTMAN->Update();
|
2002-04-16 17:31:00 +00:00
|
|
|
|
2003-07-14 06:21:12 +00:00
|
|
|
/* Make sure that nothing gets inserted while we do this, to prevent
|
|
|
|
|
* things like "key pressed, key release, key repeat". */
|
2003-07-26 21:58:48 +00:00
|
|
|
LockMut(*queuemutex);
|
2003-07-14 06:21:12 +00:00
|
|
|
|
2005-12-28 08:52:20 +00:00
|
|
|
DeviceInput di( (InputDevice)0,DeviceButton_Invalid,1.0f,now);
|
2003-10-07 05:59:58 +00:00
|
|
|
|
2005-12-28 20:41:24 +00:00
|
|
|
vector<ButtonStateMap::iterator> ButtonsToErase;
|
|
|
|
|
|
|
|
|
|
FOREACHM( DeviceInput, ButtonState, g_ButtonStates, b )
|
2002-04-16 17:31:00 +00:00
|
|
|
{
|
2005-12-28 20:41:24 +00:00
|
|
|
di.device = b->first.device;
|
|
|
|
|
di.button = b->first.button;
|
|
|
|
|
ButtonState &bs = b->second;
|
2005-10-24 06:32:30 +00:00
|
|
|
di.level = bs.m_Level;
|
2003-10-07 05:59:58 +00:00
|
|
|
|
2005-11-06 12:14:35 +00:00
|
|
|
/* Generate IET_FIRST_PRESS and IET_RELEASE events that were delayed. */
|
|
|
|
|
CheckButtonChange( bs, di, now );
|
2005-09-19 07:09:55 +00:00
|
|
|
|
2005-10-24 06:32:30 +00:00
|
|
|
/* Generate IET_LEVEL_CHANGED events. */
|
|
|
|
|
if( bs.m_LastLevel != bs.m_Level && bs.m_Level != -1 )
|
|
|
|
|
{
|
|
|
|
|
queue.push_back( InputEvent(di,IET_LEVEL_CHANGED) );
|
|
|
|
|
bs.m_LastLevel = bs.m_Level;
|
|
|
|
|
}
|
2005-09-19 07:09:55 +00:00
|
|
|
|
2005-10-24 06:32:30 +00:00
|
|
|
/* Generate IET_FAST_REPEAT and IET_SLOW_REPEAT events. */
|
|
|
|
|
if( !bs.m_bLastReportedHeld )
|
|
|
|
|
{
|
2005-12-28 20:41:24 +00:00
|
|
|
// 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 )
|
|
|
|
|
ButtonsToErase.push_back( b );
|
2005-10-24 06:32:30 +00:00
|
|
|
continue;
|
|
|
|
|
}
|
2003-09-03 21:49:11 +00:00
|
|
|
|
2005-10-24 06:32:30 +00:00
|
|
|
const float fOldHoldTime = bs.m_fSecsHeld;
|
|
|
|
|
bs.m_fSecsHeld += fDeltaTime;
|
|
|
|
|
const float fNewHoldTime = bs.m_fSecsHeld;
|
2002-04-16 17:31:00 +00:00
|
|
|
|
2005-10-24 06:32:30 +00:00
|
|
|
float fTimeBeforeRepeats;
|
|
|
|
|
InputEventType iet;
|
|
|
|
|
if( fNewHoldTime > g_fTimeBeforeSlow )
|
|
|
|
|
{
|
|
|
|
|
if( fNewHoldTime > g_fTimeBeforeFast )
|
|
|
|
|
{
|
|
|
|
|
fTimeBeforeRepeats = g_fTimeBeforeFast;
|
|
|
|
|
iet = IET_FAST_REPEAT;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
fTimeBeforeRepeats = g_fTimeBeforeSlow;
|
|
|
|
|
iet = IET_SLOW_REPEAT;
|
|
|
|
|
}
|
2003-02-16 01:35:48 +00:00
|
|
|
|
2005-10-24 06:32:30 +00:00
|
|
|
float fRepeatTime;
|
|
|
|
|
if( fOldHoldTime < fTimeBeforeRepeats )
|
|
|
|
|
{
|
|
|
|
|
fRepeatTime = fTimeBeforeRepeats;
|
|
|
|
|
}
|
|
|
|
|
else
|
2002-04-16 17:31:00 +00:00
|
|
|
{
|
2005-10-24 06:32:30 +00:00
|
|
|
float fAdjustedOldHoldTime = fOldHoldTime - fTimeBeforeRepeats;
|
|
|
|
|
float fAdjustedNewHoldTime = fNewHoldTime - fTimeBeforeRepeats;
|
|
|
|
|
if( int(fAdjustedOldHoldTime/g_fTimeBetweenRepeats) == int(fAdjustedNewHoldTime/g_fTimeBetweenRepeats) )
|
|
|
|
|
continue;
|
|
|
|
|
fRepeatTime = ftruncf( fNewHoldTime, g_fTimeBetweenRepeats );
|
2002-04-16 17:31:00 +00:00
|
|
|
}
|
2005-10-24 06:32:30 +00:00
|
|
|
|
|
|
|
|
/* 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_BeingHeldTime + fRepeatTime;
|
|
|
|
|
|
|
|
|
|
queue.push_back( InputEvent(di,iet) );
|
2002-04-16 17:31:00 +00:00
|
|
|
}
|
|
|
|
|
}
|
2005-12-28 20:41:24 +00:00
|
|
|
|
|
|
|
|
FOREACH( ButtonStateMap::iterator, ButtonsToErase, it )
|
|
|
|
|
g_ButtonStates.erase( *it );
|
2003-02-16 01:35:48 +00:00
|
|
|
}
|
|
|
|
|
|
2005-10-15 16:47:44 +00:00
|
|
|
bool InputFilter::IsBeingPressed( const DeviceInput &di )
|
2003-02-16 01:35:48 +00:00
|
|
|
{
|
2005-12-28 20:41:24 +00:00
|
|
|
LockMut(*queuemutex);
|
2006-09-02 19:34:54 +00:00
|
|
|
return GetButtonState( di ).m_bLastReportedHeld;
|
2003-02-16 01:35:48 +00:00
|
|
|
}
|
|
|
|
|
|
2005-10-15 16:47:44 +00:00
|
|
|
float InputFilter::GetSecsHeld( const DeviceInput &di )
|
2003-02-16 01:35:48 +00:00
|
|
|
{
|
2005-12-28 20:41:24 +00:00
|
|
|
LockMut(*queuemutex);
|
2006-09-02 19:34:54 +00:00
|
|
|
return GetButtonState( di ).m_fSecsHeld;
|
2003-02-16 01:35:48 +00:00
|
|
|
}
|
|
|
|
|
|
2006-01-22 01:00:06 +00:00
|
|
|
RString InputFilter::GetButtonComment( const DeviceInput &di ) const
|
2005-05-08 09:19:35 +00:00
|
|
|
{
|
|
|
|
|
LockMut(*queuemutex);
|
2006-09-02 19:34:54 +00:00
|
|
|
return GetButtonState( di ).m_sComment;
|
2005-05-08 09:19:35 +00:00
|
|
|
}
|
|
|
|
|
|
2005-10-15 16:47:44 +00:00
|
|
|
void InputFilter::ResetKeyRepeat( const DeviceInput &di )
|
2003-11-25 21:46:30 +00:00
|
|
|
{
|
2005-12-28 20:41:24 +00:00
|
|
|
LockMut(*queuemutex);
|
2006-09-02 19:34:54 +00:00
|
|
|
GetButtonState( di ).m_fSecsHeld = 0;
|
2003-11-25 21:46:30 +00:00
|
|
|
}
|
|
|
|
|
|
2003-02-16 01:35:48 +00:00
|
|
|
void InputFilter::GetInputEvents( InputEventArray &array )
|
|
|
|
|
{
|
2003-07-26 21:58:48 +00:00
|
|
|
LockMut(*queuemutex);
|
2003-02-16 01:35:48 +00:00
|
|
|
array = queue;
|
|
|
|
|
queue.clear();
|
2002-07-31 19:40:40 +00:00
|
|
|
}
|
2004-06-08 01:24:17 +00:00
|
|
|
|
2005-12-28 20:41:24 +00:00
|
|
|
void InputFilter::GetPressedButtons( vector<DeviceInput> &array )
|
|
|
|
|
{
|
|
|
|
|
LockMut(*queuemutex);
|
|
|
|
|
FOREACHM( DeviceInput, ButtonState, g_ButtonStates, b )
|
|
|
|
|
{
|
|
|
|
|
if( b->second.m_BeingHeld )
|
|
|
|
|
{
|
|
|
|
|
DeviceInput di = b->first;
|
|
|
|
|
di.level = b->second.m_Level;
|
|
|
|
|
array.push_back( di );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2004-06-08 01:24:17 +00:00
|
|
|
/*
|
|
|
|
|
* (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.
|
|
|
|
|
*/
|