diff --git a/src/Actor.cpp b/src/Actor.cpp index 8645c02d65..62c34f3f46 100644 --- a/src/Actor.cpp +++ b/src/Actor.cpp @@ -1249,7 +1249,7 @@ void Actor::AddCommand( const RString &sCmdName, apActorCommands apac ) bool Actor::HasCommand( const RString &sCmdName ) const { - return GetCommand(sCmdName) != NULL; + return GetCommand(sCmdName) != nullptr; } const apActorCommands *Actor::GetCommand( const RString &sCommandName ) const @@ -1268,7 +1268,7 @@ void Actor::HandleMessage( const Message &msg ) void Actor::PlayCommandNoRecurse( const Message &msg ) { const apActorCommands *pCmd = GetCommand( msg.GetName() ); - if( pCmd != NULL ) + if( pCmd != nullptr ) RunCommands( *pCmd, &msg.GetParamTable() ); } diff --git a/src/ActorFrame.cpp b/src/ActorFrame.cpp index 8d7db5a418..ca7bdceb96 100644 --- a/src/ActorFrame.cpp +++ b/src/ActorFrame.cpp @@ -145,7 +145,7 @@ void ActorFrame::AddChild( Actor *pActor ) Dialog::OK( ssprintf("Actor \"%s\" adds child \"%s\" more than once", GetLineage().c_str(), pActor->GetName().c_str()) ); #endif - ASSERT( pActor != NULL ); + ASSERT( pActor != nullptr ); ASSERT( (void*)pActor != (void*)0xC0000005 ); m_SubActors.push_back( pActor ); @@ -285,14 +285,14 @@ void ActorFrame::PushChildrenTable( lua_State *L ) void ActorFrame::PlayCommandOnChildren( const RString &sCommandName, const LuaReference *pParamTable ) { const apActorCommands *pCmd = GetCommand( sCommandName ); - if( pCmd != NULL ) + if( pCmd != nullptr ) RunCommandsOnChildren( *pCmd, pParamTable ); } void ActorFrame::PlayCommandOnLeaves( const RString &sCommandName, const LuaReference *pParamTable ) { const apActorCommands *pCmd = GetCommand( sCommandName ); - if( pCmd != NULL ) + if( pCmd != nullptr ) RunCommandsOnLeaves( **pCmd, pParamTable ); } diff --git a/src/ActorMultiTexture.cpp b/src/ActorMultiTexture.cpp index 08d0570325..a54df273f2 100644 --- a/src/ActorMultiTexture.cpp +++ b/src/ActorMultiTexture.cpp @@ -65,7 +65,7 @@ void ActorMultiTexture::ClearTextures() int ActorMultiTexture::AddTexture( RageTexture *pTexture ) { - ASSERT( pTexture != NULL ); + ASSERT( pTexture != nullptr ); LOG->Trace( "ActorMultiTexture::AddTexture( %s )", pTexture->GetID().filename.c_str() ); m_aTextureUnits.push_back( TextureUnitState() ); diff --git a/src/ActorProxy.cpp b/src/ActorProxy.cpp index 92f721f9b9..9e2ccf764b 100644 --- a/src/ActorProxy.cpp +++ b/src/ActorProxy.cpp @@ -1,90 +1,90 @@ -#include "global.h" -#include "ActorProxy.h" -#include "ActorUtil.h" - -REGISTER_ACTOR_CLASS( ActorProxy ); - -ActorProxy::ActorProxy() -{ - m_pActorTarget = NULL; -} - -bool ActorProxy::EarlyAbortDraw() const -{ - return m_pActorTarget == NULL || Actor::EarlyAbortDraw(); -} - -void ActorProxy::DrawPrimitives() -{ - if( m_pActorTarget != NULL ) - { - bool bVisible = m_pActorTarget->GetVisible(); - m_pActorTarget->SetVisible( true ); - m_pActorTarget->Draw(); - m_pActorTarget->SetVisible( bVisible ); - } -} - -void ActorProxy::LoadFromNode( const XNode* pNode ) -{ - Actor::LoadFromNode( pNode ); -} - -// lua start -#include "LuaBinding.h" - -/** @brief Allow Lua to have access to the ActorProxy. */ -class LunaActorProxy: public Luna -{ -public: - static int SetTarget( T* p, lua_State *L ) - { - Actor *pTarget = Luna::check( L, 1 ); - p->SetTarget( pTarget ); - return 0; - } - - static int GetTarget( T* p, lua_State *L ) - { - Actor *pTarget = p->GetTarget(); - if( pTarget != NULL ) - pTarget->PushSelf( L ); - else - lua_pushnil( L ); - return 1; - } - - LunaActorProxy() - { - ADD_METHOD( SetTarget ); - ADD_METHOD( GetTarget ); - } -}; - -LUA_REGISTER_DERIVED_CLASS( ActorProxy, Actor ) -// lua end - -/* - * (c) 2006 Glenn Maynard - * 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. - */ +#include "global.h" +#include "ActorProxy.h" +#include "ActorUtil.h" + +REGISTER_ACTOR_CLASS( ActorProxy ); + +ActorProxy::ActorProxy() +{ + m_pActorTarget = NULL; +} + +bool ActorProxy::EarlyAbortDraw() const +{ + return m_pActorTarget == NULL || Actor::EarlyAbortDraw(); +} + +void ActorProxy::DrawPrimitives() +{ + if( m_pActorTarget != nullptr ) + { + bool bVisible = m_pActorTarget->GetVisible(); + m_pActorTarget->SetVisible( true ); + m_pActorTarget->Draw(); + m_pActorTarget->SetVisible( bVisible ); + } +} + +void ActorProxy::LoadFromNode( const XNode* pNode ) +{ + Actor::LoadFromNode( pNode ); +} + +// lua start +#include "LuaBinding.h" + +/** @brief Allow Lua to have access to the ActorProxy. */ +class LunaActorProxy: public Luna +{ +public: + static int SetTarget( T* p, lua_State *L ) + { + Actor *pTarget = Luna::check( L, 1 ); + p->SetTarget( pTarget ); + return 0; + } + + static int GetTarget( T* p, lua_State *L ) + { + Actor *pTarget = p->GetTarget(); + if( pTarget != nullptr ) + pTarget->PushSelf( L ); + else + lua_pushnil( L ); + return 1; + } + + LunaActorProxy() + { + ADD_METHOD( SetTarget ); + ADD_METHOD( GetTarget ); + } +}; + +LUA_REGISTER_DERIVED_CLASS( ActorProxy, Actor ) +// lua end + +/* + * (c) 2006 Glenn Maynard + * 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. + */ diff --git a/src/ActorUtil.cpp b/src/ActorUtil.cpp index 6a38fca7c7..aa9cc70276 100644 --- a/src/ActorUtil.cpp +++ b/src/ActorUtil.cpp @@ -103,7 +103,7 @@ bool ActorUtil::ResolvePath( RString &sPath, const RString &sName ) Actor* ActorUtil::LoadFromNode( const XNode* pNode, Actor *pParentActor ) { - ASSERT( pNode != NULL ); + ASSERT( pNode != nullptr ); // Remove this in favor of using conditionals in Lua. -Chris // There are a number of themes out there that depend on this (including diff --git a/src/AnnouncerManager.cpp b/src/AnnouncerManager.cpp index 88ef40dc27..748f1a8c96 100644 --- a/src/AnnouncerManager.cpp +++ b/src/AnnouncerManager.cpp @@ -120,7 +120,7 @@ RString AnnouncerManager::GetPathTo( RString sAnnouncerName, RString sFolderName /* Search for the announcer folder in the list of aliases. */ int i; - for(i = 0; aliases[i][0] != NULL; ++i) + for(i = 0; aliases[i][0] != nullptr; ++i) { if(!sFolderName.EqualsNoCase(aliases[i][0])) continue; /* no match */ diff --git a/src/Attack.cpp b/src/Attack.cpp index ec24461dc8..3c25cdc8e0 100644 --- a/src/Attack.cpp +++ b/src/Attack.cpp @@ -9,7 +9,7 @@ void Attack::GetAttackBeats( const Song *pSong, float &fStartBeat, float &fEndBeat ) const { - ASSERT( pSong != NULL ); + ASSERT( pSong != nullptr ); ASSERT_M( fStartSecond >= 0, ssprintf("StartSecond: %f",fStartSecond) ); const TimingData &timing = pSong->m_SongTiming; @@ -28,8 +28,8 @@ void Attack::GetRealtimeAttackBeats( const Song *pSong, const PlayerState* pPlay return; } - ASSERT( pPlayerState != NULL ); - ASSERT( pSong != NULL ); + ASSERT( pPlayerState != nullptr ); + ASSERT( pSong != nullptr ); /* If reasonable, push the attack forward 8 beats so that notes on screen don't change suddenly. */ fStartBeat = min( GAMESTATE->m_Position.m_fSongBeat+8, pPlayerState->m_fLastDrawnBeat ); diff --git a/src/AttackDisplay.cpp b/src/AttackDisplay.cpp index 81ea6d37a0..db286c9f2a 100644 --- a/src/AttackDisplay.cpp +++ b/src/AttackDisplay.cpp @@ -1,138 +1,138 @@ -#include "global.h" -#include "AttackDisplay.h" -#include "ThemeManager.h" -#include "GameState.h" -#include "ActorUtil.h" -#include "Character.h" -#include "RageLog.h" -#include -#include "PlayerState.h" - - -RString GetAttackPieceName( const RString &sAttack ) -{ - RString ret = ssprintf( "attack %s", sAttack.c_str() ); - - /* 1.5x -> 1_5x. If we pass a period to THEME->GetPathTo, it'll think - * we're looking for a specific file and not search. */ - ret.Replace( ".", "_" ); - - return ret; -} - -AttackDisplay::AttackDisplay() -{ - if( GAMESTATE->m_PlayMode != PLAY_MODE_BATTLE && - GAMESTATE->m_PlayMode != PLAY_MODE_RAVE ) - return; - - m_sprAttack.SetDiffuseAlpha( 0 ); // invisible - this->AddChild( &m_sprAttack ); -} - -void AttackDisplay::Init( const PlayerState* pPlayerState ) -{ - m_pPlayerState = pPlayerState; - - // TODO: Remove use of PlayerNumber. - PlayerNumber pn = m_pPlayerState->m_PlayerNumber; - m_sprAttack.SetName( ssprintf("TextP%d",pn+1) ); - - if( GAMESTATE->m_PlayMode != PLAY_MODE_BATTLE && - GAMESTATE->m_PlayMode != PLAY_MODE_RAVE ) - return; - - set attacks; - for( int al=0; alm_pCurCharacters[pn]; - ASSERT( ch != NULL ); - const RString* asAttacks = ch->m_sAttacks[al]; - for( int att = 0; att < NUM_ATTACKS_PER_LEVEL; ++att ) - attacks.insert( asAttacks[att] ); - } - - for( set::const_iterator it = attacks.begin(); it != attacks.end(); ++it ) - { - const RString path = THEME->GetPathG( "AttackDisplay", GetAttackPieceName( *it ), true ); - if( path == "" ) - { - LOG->Trace( "Couldn't find \"%s\"", GetAttackPieceName( *it ).c_str() ); - continue; - } - - m_TexturePreload.Load( path ); - } -} - - -void AttackDisplay::Update( float fDelta ) -{ - ActorFrame::Update( fDelta ); - - if( GAMESTATE->m_PlayMode != PLAY_MODE_BATTLE && - GAMESTATE->m_PlayMode != PLAY_MODE_RAVE ) - return; - - if( !m_pPlayerState->m_bAttackBeganThisUpdate ) - return; - // don't handle this again - - for( unsigned s=0; sm_ActiveAttacks.size(); s++ ) - { - const Attack& attack = m_pPlayerState->m_ActiveAttacks[s]; - - if( attack.fStartSecond >= 0 ) - continue; /* hasn't started yet */ - - if( attack.fSecsRemaining <= 0 ) - continue; /* ended already */ - - if( attack.IsBlank() ) - continue; - - SetAttack( attack.sModifiers ); - break; - } -} - -void AttackDisplay::SetAttack( const RString &sText ) -{ - const RString path = THEME->GetPathG( "AttackDisplay", GetAttackPieceName(sText), true ); - if( path == "" ) - return; - - m_sprAttack.SetDiffuseAlpha( 1 ); - m_sprAttack.Load( path ); - - // TODO: Remove use of PlayerNumber. - PlayerNumber pn = m_pPlayerState->m_PlayerNumber; - - const RString sName = ssprintf( "%sP%i", sText.c_str(), pn+1 ); - m_sprAttack.RunCommands( THEME->GetMetricA("AttackDisplay", sName + "OnCommand") ); -} - -/* - * (c) 2003 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. - */ +#include "global.h" +#include "AttackDisplay.h" +#include "ThemeManager.h" +#include "GameState.h" +#include "ActorUtil.h" +#include "Character.h" +#include "RageLog.h" +#include +#include "PlayerState.h" + + +RString GetAttackPieceName( const RString &sAttack ) +{ + RString ret = ssprintf( "attack %s", sAttack.c_str() ); + + /* 1.5x -> 1_5x. If we pass a period to THEME->GetPathTo, it'll think + * we're looking for a specific file and not search. */ + ret.Replace( ".", "_" ); + + return ret; +} + +AttackDisplay::AttackDisplay() +{ + if( GAMESTATE->m_PlayMode != PLAY_MODE_BATTLE && + GAMESTATE->m_PlayMode != PLAY_MODE_RAVE ) + return; + + m_sprAttack.SetDiffuseAlpha( 0 ); // invisible + this->AddChild( &m_sprAttack ); +} + +void AttackDisplay::Init( const PlayerState* pPlayerState ) +{ + m_pPlayerState = pPlayerState; + + // TODO: Remove use of PlayerNumber. + PlayerNumber pn = m_pPlayerState->m_PlayerNumber; + m_sprAttack.SetName( ssprintf("TextP%d",pn+1) ); + + if( GAMESTATE->m_PlayMode != PLAY_MODE_BATTLE && + GAMESTATE->m_PlayMode != PLAY_MODE_RAVE ) + return; + + set attacks; + for( int al=0; alm_pCurCharacters[pn]; + ASSERT( ch != nullptr ); + const RString* asAttacks = ch->m_sAttacks[al]; + for( int att = 0; att < NUM_ATTACKS_PER_LEVEL; ++att ) + attacks.insert( asAttacks[att] ); + } + + for( set::const_iterator it = attacks.begin(); it != attacks.end(); ++it ) + { + const RString path = THEME->GetPathG( "AttackDisplay", GetAttackPieceName( *it ), true ); + if( path == "" ) + { + LOG->Trace( "Couldn't find \"%s\"", GetAttackPieceName( *it ).c_str() ); + continue; + } + + m_TexturePreload.Load( path ); + } +} + + +void AttackDisplay::Update( float fDelta ) +{ + ActorFrame::Update( fDelta ); + + if( GAMESTATE->m_PlayMode != PLAY_MODE_BATTLE && + GAMESTATE->m_PlayMode != PLAY_MODE_RAVE ) + return; + + if( !m_pPlayerState->m_bAttackBeganThisUpdate ) + return; + // don't handle this again + + for( unsigned s=0; sm_ActiveAttacks.size(); s++ ) + { + const Attack& attack = m_pPlayerState->m_ActiveAttacks[s]; + + if( attack.fStartSecond >= 0 ) + continue; /* hasn't started yet */ + + if( attack.fSecsRemaining <= 0 ) + continue; /* ended already */ + + if( attack.IsBlank() ) + continue; + + SetAttack( attack.sModifiers ); + break; + } +} + +void AttackDisplay::SetAttack( const RString &sText ) +{ + const RString path = THEME->GetPathG( "AttackDisplay", GetAttackPieceName(sText), true ); + if( path == "" ) + return; + + m_sprAttack.SetDiffuseAlpha( 1 ); + m_sprAttack.Load( path ); + + // TODO: Remove use of PlayerNumber. + PlayerNumber pn = m_pPlayerState->m_PlayerNumber; + + const RString sName = ssprintf( "%sP%i", sText.c_str(), pn+1 ); + m_sprAttack.RunCommands( THEME->GetMetricA("AttackDisplay", sName + "OnCommand") ); +} + +/* + * (c) 2003 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. + */ diff --git a/src/AutoActor.h b/src/AutoActor.h index 3ee95bec09..d214ba9511 100644 --- a/src/AutoActor.h +++ b/src/AutoActor.h @@ -24,7 +24,7 @@ public: /** * @brief Determine if this actor is presently loaded. * @return true if it is loaded, or false otherwise. */ - bool IsLoaded() const { return m_pActor != NULL; } + bool IsLoaded() const { return m_pActor != nullptr; } void Load( Actor *pActor ); // transfer pointer void Load( const RString &sPath ); void LoadB( const RString &sMetricsGroup, const RString &sElement ); // load a background and set up LuaThreadVariables for recursive loading diff --git a/src/AutoKeysounds.cpp b/src/AutoKeysounds.cpp index 3a46ef82a0..98153a5743 100644 --- a/src/AutoKeysounds.cpp +++ b/src/AutoKeysounds.cpp @@ -278,14 +278,14 @@ void AutoKeysounds::FinishLoading() delete pChain; } } - ASSERT_M( m_pSharedSound != NULL, ssprintf("No keysounds were loaded for the song %s!", pSong->m_sMainTitle.c_str() )); + ASSERT_M( m_pSharedSound != nullptr, ssprintf("No keysounds were loaded for the song %s!", pSong->m_sMainTitle.c_str() )); m_pSharedSound = new RageSoundReader_PitchChange( m_pSharedSound ); m_pSharedSound = new RageSoundReader_PostBuffering( m_pSharedSound ); m_pSharedSound = new RageSoundReader_Pan( m_pSharedSound ); apSounds.push_back( m_pSharedSound ); - if( m_pPlayerSounds[0] != NULL ) + if( m_pPlayerSounds[0] != nullptr ) { m_pPlayerSounds[0] = new RageSoundReader_PitchChange( m_pPlayerSounds[0] ); m_pPlayerSounds[0] = new RageSoundReader_PostBuffering( m_pPlayerSounds[0] ); @@ -293,7 +293,7 @@ void AutoKeysounds::FinishLoading() apSounds.push_back( m_pPlayerSounds[0] ); } - if( m_pPlayerSounds[1] != NULL ) + if( m_pPlayerSounds[1] != nullptr ) { m_pPlayerSounds[1] = new RageSoundReader_PitchChange( m_pPlayerSounds[1] ); m_pPlayerSounds[1] = new RageSoundReader_PostBuffering( m_pPlayerSounds[1] ); diff --git a/src/BGAnimation.cpp b/src/BGAnimation.cpp index 157f5129d8..0dc9ae93ad 100644 --- a/src/BGAnimation.cpp +++ b/src/BGAnimation.cpp @@ -49,7 +49,7 @@ void BGAnimation::AddLayersFromAniDir( const RString &_sAniDir, const XNode *pNo for (RString const &sLayer : vsLayerNames) { const XNode* pKey = pNode->GetChild( sLayer ); - ASSERT( pKey != NULL ); + ASSERT( pKey != nullptr ); RString sImportDir; if( pKey->GetAttrValue("Import", sImportDir) ) diff --git a/src/BPMDisplay.cpp b/src/BPMDisplay.cpp index c2e29c99d8..e49b5ced4e 100644 --- a/src/BPMDisplay.cpp +++ b/src/BPMDisplay.cpp @@ -1,378 +1,378 @@ -#include "global.h" -#include "BPMDisplay.h" -#include "RageUtil.h" -#include "GameConstantsAndTypes.h" -#include "GameState.h" -#include "Course.h" -#include "Style.h" -#include "ActorUtil.h" -#include "CommonMetrics.h" -#include "LocalizedString.h" -#include "Song.h" -#include "Steps.h" - -#include - -REGISTER_ACTOR_CLASS( BPMDisplay ); - -BPMDisplay::BPMDisplay() -{ - m_fBPMFrom = m_fBPMTo = 0; - m_iCurrentBPM = 0; - m_BPMS.push_back(0); - m_fPercentInState = 0; - m_fCycleTime = 1.0f; -} - -void BPMDisplay::Load() -{ - SET_NO_BPM_COMMAND.Load( m_sName, "SetNoBpmCommand"); - SET_NORMAL_COMMAND.Load( m_sName, "SetNormalCommand"); - SET_CHANGING_COMMAND.Load( m_sName, "SetChangeCommand" ); - SET_RANDOM_COMMAND.Load( m_sName, "SetRandomCommand" ); - SET_EXTRA_COMMAND.Load( m_sName, "SetExtraCommand" ); - CYCLE.Load( m_sName, "Cycle" ); - RANDOM_CYCLE_SPEED.Load( m_sName, "RandomCycleSpeed" ); - COURSE_CYCLE_SPEED.Load( m_sName, "CourseCycleSpeed" ); - SEPARATOR.Load( m_sName, "Separator" ); - SHOW_QMARKS.Load( m_sName, "ShowQMarksInRandomCycle" ); - NO_BPM_TEXT.Load( m_sName, "NoBpmText" ); - QUESTIONMARKS_TEXT.Load( m_sName, "QuestionMarksText" ); - RANDOM_TEXT.Load( m_sName, "RandomText" ); - VARIOUS_TEXT.Load( m_sName, "VariousText" ); - BPM_FORMAT_STRING.Load( m_sName, "FormatString" ); - - RunCommands( SET_NORMAL_COMMAND ); -} - -void BPMDisplay::LoadFromNode( const XNode *pNode ) -{ - BitmapText::LoadFromNode( pNode ); - Load(); -} - -float BPMDisplay::GetActiveBPM() const -{ - return m_fBPMTo + (m_fBPMFrom-m_fBPMTo)*m_fPercentInState; -} - -void BPMDisplay::Update( float fDeltaTime ) -{ - BitmapText::Update( fDeltaTime ); - - if( !(bool)CYCLE ) - return; - if( m_BPMS.size() == 0 ) - return; // no bpm - - m_fPercentInState -= fDeltaTime / m_fCycleTime; - if( m_fPercentInState < 0 ) - { - // go to next state - m_fPercentInState = 1; // reset timer - - m_iCurrentBPM = (m_iCurrentBPM + 1) % m_BPMS.size(); - m_fBPMFrom = m_fBPMTo; - m_fBPMTo = m_BPMS[m_iCurrentBPM]; - - if(m_fBPMTo == -1) - { - m_fBPMFrom = -1; - if( (bool)SHOW_QMARKS ) - SetText( (RandomFloat(0,1)>0.90f) ? (RString)QUESTIONMARKS_TEXT : ssprintf((RString)BPM_FORMAT_STRING,RandomFloat(0,999)) ); - else - SetText( ssprintf((RString)BPM_FORMAT_STRING, RandomFloat(0,999)) ); - } - else if(m_fBPMFrom == -1) - { - m_fBPMFrom = m_fBPMTo; - } - } - - if( m_fBPMTo != -1) - { - const float fActualBPM = GetActiveBPM(); - SetText( ssprintf((RString)BPM_FORMAT_STRING, fActualBPM) ); - } -} - -void BPMDisplay::SetBPMRange( const DisplayBpms &bpms ) -{ - ASSERT( !bpms.vfBpms.empty() ); - - m_BPMS.clear(); - - const vector &BPMS = bpms.vfBpms; - - bool AllIdentical = true; - for( unsigned i = 0; i < BPMS.size(); ++i ) - { - if( i > 0 && BPMS[i] != BPMS[i-1] ) - AllIdentical = false; - } - - if( !(bool)CYCLE ) - { - int MinBPM = INT_MAX; - int MaxBPM = INT_MIN; - for( unsigned i = 0; i < BPMS.size(); ++i ) - { - MinBPM = min( MinBPM, (int)lrintf(BPMS[i]) ); - MaxBPM = max( MaxBPM, (int)lrintf(BPMS[i]) ); - } - if( MinBPM == MaxBPM ) - { - if( MinBPM == -1 ) - SetText( RANDOM_TEXT ); // random (was "...") -aj - else - SetText( ssprintf("%i", MinBPM) ); - } - else - { - SetText( ssprintf("%i%s%i", MinBPM, SEPARATOR.GetValue().c_str(), MaxBPM) ); - } - } - else - { - for( unsigned i = 0; i < BPMS.size(); ++i ) - { - m_BPMS.push_back(BPMS[i]); - if( BPMS[i] != -1 ) - m_BPMS.push_back(BPMS[i]); // hold - } - - m_iCurrentBPM = min(1u, m_BPMS.size()); // start on the first hold - m_fBPMFrom = BPMS[0]; - m_fBPMTo = BPMS[0]; - m_fPercentInState = 1; - } - - if( GAMESTATE->IsAnExtraStageAndSelectionLocked() ) - RunCommands( SET_EXTRA_COMMAND ); - else if( !AllIdentical ) - RunCommands( SET_CHANGING_COMMAND ); - else - RunCommands( SET_NORMAL_COMMAND ); -} - -void BPMDisplay::CycleRandomly() -{ - DisplayBpms bpms; - bpms.Add(-1); - SetBPMRange( bpms ); - - RunCommands( SET_RANDOM_COMMAND ); - - m_fCycleTime = (float)RANDOM_CYCLE_SPEED; - - // Go to default value in event of a negative value in the metrics - if( m_fCycleTime < 0 ) - m_fCycleTime = 0.2f; -} - -void BPMDisplay::NoBPM() -{ - m_BPMS.clear(); - SetText( NO_BPM_TEXT ); - RunCommands( SET_NO_BPM_COMMAND ); -} - -void BPMDisplay::SetBpmFromSong( const Song* pSong ) -{ - ASSERT( pSong != NULL ); - switch( pSong->m_DisplayBPMType ) - { - case DISPLAY_BPM_ACTUAL: - case DISPLAY_BPM_SPECIFIED: - { - DisplayBpms bpms; - pSong->GetDisplayBpms( bpms ); - SetBPMRange( bpms ); - m_fCycleTime = 1.0f; - } - break; - case DISPLAY_BPM_RANDOM: - CycleRandomly(); - break; - default: - FAIL_M(ssprintf("Invalid display BPM type: %i", pSong->m_DisplayBPMType)); - } -} - -void BPMDisplay::SetBpmFromSteps( const Steps* pSteps ) -{ - ASSERT( pSteps != NULL ); - DisplayBpms bpms; - float fMinBPM, fMaxBPM; - pSteps->GetTimingData()->GetActualBPM( fMinBPM, fMaxBPM ); - bpms.Add( fMinBPM ); - bpms.Add( fMaxBPM ); - m_fCycleTime = 1.0f; -} - -void BPMDisplay::SetBpmFromCourse( const Course* pCourse ) -{ - ASSERT( pCourse != NULL ); - ASSERT( GAMESTATE->GetCurrentStyle() != NULL ); - - StepsType st = GAMESTATE->GetCurrentStyle()->m_StepsType; - Trail *pTrail = pCourse->GetTrail( st ); - // GetTranslitFullTitle because "Crashinfo.txt is garbled because of the ANSI output as usual." -f - ASSERT_M( pTrail != NULL, ssprintf("Course '%s' has no trail for StepsType '%s'", pCourse->GetTranslitFullTitle().c_str(), StringConversion::ToString(st).c_str() ) ); - - m_fCycleTime = (float)COURSE_CYCLE_SPEED; - - if( (int)pTrail->m_vEntries.size() > CommonMetrics::MAX_COURSE_ENTRIES_BEFORE_VARIOUS ) - { - SetVarious(); - return; - } - - DisplayBpms bpms; - pTrail->GetDisplayBpms( bpms ); - SetBPMRange( bpms ); -} - -void BPMDisplay::SetConstantBpm( float fBPM ) -{ - DisplayBpms bpms; - bpms.Add( fBPM ); - SetBPMRange( bpms ); -} - -void BPMDisplay::SetVarious() -{ - m_BPMS.clear(); - m_BPMS.push_back( -1 ); - SetText( VARIOUS_TEXT ); -} - -void BPMDisplay::SetFromGameState() -{ - if( GAMESTATE->m_pCurSong.Get() ) - { - if( GAMESTATE->IsAnExtraStageAndSelectionLocked() ) - CycleRandomly(); - else - SetBpmFromSong( GAMESTATE->m_pCurSong ); - return; - } - if( GAMESTATE->m_pCurCourse.Get() ) - { - if( GAMESTATE->GetCurrentStyle() == NULL ) - ; // This is true when backing out from ScreenSelectCourse to ScreenTitleMenu. So, don't call SetBpmFromCourse where an assert will fire. - else - SetBpmFromCourse( GAMESTATE->m_pCurCourse ); - - return; - } - - NoBPM(); -} - -// SongBPMDisplay (in-game BPM display) -class SongBPMDisplay: public BPMDisplay -{ -public: - SongBPMDisplay(); - virtual SongBPMDisplay *Copy() const; - virtual void Update( float fDeltaTime ); - -private: - float m_fLastGameStateBPM; - -}; - -SongBPMDisplay::SongBPMDisplay() -{ - m_fLastGameStateBPM = 0; -} - -void SongBPMDisplay::Update( float fDeltaTime ) -{ - float fGameStateBPM = GAMESTATE->m_Position.m_fCurBPS * 60.0f; - if( m_fLastGameStateBPM != fGameStateBPM ) - { - m_fLastGameStateBPM = fGameStateBPM; - SetConstantBpm( fGameStateBPM ); - } - - BPMDisplay::Update( fDeltaTime ); -} - -REGISTER_ACTOR_CLASS( SongBPMDisplay ); - -#include "LuaBinding.h" -/** @brief Allow Lua to have access to the BPMDisplay. */ -class LunaBPMDisplay: public Luna -{ -public: - static int SetFromGameState( T* p, lua_State *L ) { p->SetFromGameState(); return 0; } - static int SetFromSong( T* p, lua_State *L ) - { - if( lua_isnil(L,1) ) { p->NoBPM(); } - else - { - const Song* pSong = Luna::check( L, 1, true ); - p->SetBpmFromSong(pSong); - } - return 0; - } - static int SetFromSteps( T* p, lua_State *L ) - { - if( lua_isnil(L,1) ) { p->NoBPM(); } - else - { - const Steps* pSteps = Luna::check( L, 1, true ); - p->SetBpmFromSteps(pSteps); - } - return 0; - } - static int SetFromCourse( T* p, lua_State *L ) - { - if( lua_isnil(L,1) ) { p->NoBPM(); } - else - { - const Course* pCourse = Luna::check( L, 1, true ); - p->SetBpmFromCourse(pCourse); - } - return 0; - } - static int GetText( T* p, lua_State *L ) { lua_pushstring( L, p->GetText() ); return 1; } - - LunaBPMDisplay() - { - ADD_METHOD( SetFromGameState ); - ADD_METHOD( SetFromSong ); - ADD_METHOD( SetFromSteps ); - ADD_METHOD( SetFromCourse ); - ADD_METHOD( GetText ); - } -}; - -LUA_REGISTER_DERIVED_CLASS( BPMDisplay, BitmapText ) - -/* - * (c) 2001-2002 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. - */ +#include "global.h" +#include "BPMDisplay.h" +#include "RageUtil.h" +#include "GameConstantsAndTypes.h" +#include "GameState.h" +#include "Course.h" +#include "Style.h" +#include "ActorUtil.h" +#include "CommonMetrics.h" +#include "LocalizedString.h" +#include "Song.h" +#include "Steps.h" + +#include + +REGISTER_ACTOR_CLASS( BPMDisplay ); + +BPMDisplay::BPMDisplay() +{ + m_fBPMFrom = m_fBPMTo = 0; + m_iCurrentBPM = 0; + m_BPMS.push_back(0); + m_fPercentInState = 0; + m_fCycleTime = 1.0f; +} + +void BPMDisplay::Load() +{ + SET_NO_BPM_COMMAND.Load( m_sName, "SetNoBpmCommand"); + SET_NORMAL_COMMAND.Load( m_sName, "SetNormalCommand"); + SET_CHANGING_COMMAND.Load( m_sName, "SetChangeCommand" ); + SET_RANDOM_COMMAND.Load( m_sName, "SetRandomCommand" ); + SET_EXTRA_COMMAND.Load( m_sName, "SetExtraCommand" ); + CYCLE.Load( m_sName, "Cycle" ); + RANDOM_CYCLE_SPEED.Load( m_sName, "RandomCycleSpeed" ); + COURSE_CYCLE_SPEED.Load( m_sName, "CourseCycleSpeed" ); + SEPARATOR.Load( m_sName, "Separator" ); + SHOW_QMARKS.Load( m_sName, "ShowQMarksInRandomCycle" ); + NO_BPM_TEXT.Load( m_sName, "NoBpmText" ); + QUESTIONMARKS_TEXT.Load( m_sName, "QuestionMarksText" ); + RANDOM_TEXT.Load( m_sName, "RandomText" ); + VARIOUS_TEXT.Load( m_sName, "VariousText" ); + BPM_FORMAT_STRING.Load( m_sName, "FormatString" ); + + RunCommands( SET_NORMAL_COMMAND ); +} + +void BPMDisplay::LoadFromNode( const XNode *pNode ) +{ + BitmapText::LoadFromNode( pNode ); + Load(); +} + +float BPMDisplay::GetActiveBPM() const +{ + return m_fBPMTo + (m_fBPMFrom-m_fBPMTo)*m_fPercentInState; +} + +void BPMDisplay::Update( float fDeltaTime ) +{ + BitmapText::Update( fDeltaTime ); + + if( !(bool)CYCLE ) + return; + if( m_BPMS.size() == 0 ) + return; // no bpm + + m_fPercentInState -= fDeltaTime / m_fCycleTime; + if( m_fPercentInState < 0 ) + { + // go to next state + m_fPercentInState = 1; // reset timer + + m_iCurrentBPM = (m_iCurrentBPM + 1) % m_BPMS.size(); + m_fBPMFrom = m_fBPMTo; + m_fBPMTo = m_BPMS[m_iCurrentBPM]; + + if(m_fBPMTo == -1) + { + m_fBPMFrom = -1; + if( (bool)SHOW_QMARKS ) + SetText( (RandomFloat(0,1)>0.90f) ? (RString)QUESTIONMARKS_TEXT : ssprintf((RString)BPM_FORMAT_STRING,RandomFloat(0,999)) ); + else + SetText( ssprintf((RString)BPM_FORMAT_STRING, RandomFloat(0,999)) ); + } + else if(m_fBPMFrom == -1) + { + m_fBPMFrom = m_fBPMTo; + } + } + + if( m_fBPMTo != -1) + { + const float fActualBPM = GetActiveBPM(); + SetText( ssprintf((RString)BPM_FORMAT_STRING, fActualBPM) ); + } +} + +void BPMDisplay::SetBPMRange( const DisplayBpms &bpms ) +{ + ASSERT( !bpms.vfBpms.empty() ); + + m_BPMS.clear(); + + const vector &BPMS = bpms.vfBpms; + + bool AllIdentical = true; + for( unsigned i = 0; i < BPMS.size(); ++i ) + { + if( i > 0 && BPMS[i] != BPMS[i-1] ) + AllIdentical = false; + } + + if( !(bool)CYCLE ) + { + int MinBPM = INT_MAX; + int MaxBPM = INT_MIN; + for( unsigned i = 0; i < BPMS.size(); ++i ) + { + MinBPM = min( MinBPM, (int)lrintf(BPMS[i]) ); + MaxBPM = max( MaxBPM, (int)lrintf(BPMS[i]) ); + } + if( MinBPM == MaxBPM ) + { + if( MinBPM == -1 ) + SetText( RANDOM_TEXT ); // random (was "...") -aj + else + SetText( ssprintf("%i", MinBPM) ); + } + else + { + SetText( ssprintf("%i%s%i", MinBPM, SEPARATOR.GetValue().c_str(), MaxBPM) ); + } + } + else + { + for( unsigned i = 0; i < BPMS.size(); ++i ) + { + m_BPMS.push_back(BPMS[i]); + if( BPMS[i] != -1 ) + m_BPMS.push_back(BPMS[i]); // hold + } + + m_iCurrentBPM = min(1u, m_BPMS.size()); // start on the first hold + m_fBPMFrom = BPMS[0]; + m_fBPMTo = BPMS[0]; + m_fPercentInState = 1; + } + + if( GAMESTATE->IsAnExtraStageAndSelectionLocked() ) + RunCommands( SET_EXTRA_COMMAND ); + else if( !AllIdentical ) + RunCommands( SET_CHANGING_COMMAND ); + else + RunCommands( SET_NORMAL_COMMAND ); +} + +void BPMDisplay::CycleRandomly() +{ + DisplayBpms bpms; + bpms.Add(-1); + SetBPMRange( bpms ); + + RunCommands( SET_RANDOM_COMMAND ); + + m_fCycleTime = (float)RANDOM_CYCLE_SPEED; + + // Go to default value in event of a negative value in the metrics + if( m_fCycleTime < 0 ) + m_fCycleTime = 0.2f; +} + +void BPMDisplay::NoBPM() +{ + m_BPMS.clear(); + SetText( NO_BPM_TEXT ); + RunCommands( SET_NO_BPM_COMMAND ); +} + +void BPMDisplay::SetBpmFromSong( const Song* pSong ) +{ + ASSERT( pSong != nullptr ); + switch( pSong->m_DisplayBPMType ) + { + case DISPLAY_BPM_ACTUAL: + case DISPLAY_BPM_SPECIFIED: + { + DisplayBpms bpms; + pSong->GetDisplayBpms( bpms ); + SetBPMRange( bpms ); + m_fCycleTime = 1.0f; + } + break; + case DISPLAY_BPM_RANDOM: + CycleRandomly(); + break; + default: + FAIL_M(ssprintf("Invalid display BPM type: %i", pSong->m_DisplayBPMType)); + } +} + +void BPMDisplay::SetBpmFromSteps( const Steps* pSteps ) +{ + ASSERT( pSteps != nullptr ); + DisplayBpms bpms; + float fMinBPM, fMaxBPM; + pSteps->GetTimingData()->GetActualBPM( fMinBPM, fMaxBPM ); + bpms.Add( fMinBPM ); + bpms.Add( fMaxBPM ); + m_fCycleTime = 1.0f; +} + +void BPMDisplay::SetBpmFromCourse( const Course* pCourse ) +{ + ASSERT( pCourse != nullptr ); + ASSERT( GAMESTATE->GetCurrentStyle() != nullptr ); + + StepsType st = GAMESTATE->GetCurrentStyle()->m_StepsType; + Trail *pTrail = pCourse->GetTrail( st ); + // GetTranslitFullTitle because "Crashinfo.txt is garbled because of the ANSI output as usual." -f + ASSERT_M( pTrail != nullptr, ssprintf("Course '%s' has no trail for StepsType '%s'", pCourse->GetTranslitFullTitle().c_str(), StringConversion::ToString(st).c_str() ) ); + + m_fCycleTime = (float)COURSE_CYCLE_SPEED; + + if( (int)pTrail->m_vEntries.size() > CommonMetrics::MAX_COURSE_ENTRIES_BEFORE_VARIOUS ) + { + SetVarious(); + return; + } + + DisplayBpms bpms; + pTrail->GetDisplayBpms( bpms ); + SetBPMRange( bpms ); +} + +void BPMDisplay::SetConstantBpm( float fBPM ) +{ + DisplayBpms bpms; + bpms.Add( fBPM ); + SetBPMRange( bpms ); +} + +void BPMDisplay::SetVarious() +{ + m_BPMS.clear(); + m_BPMS.push_back( -1 ); + SetText( VARIOUS_TEXT ); +} + +void BPMDisplay::SetFromGameState() +{ + if( GAMESTATE->m_pCurSong.Get() ) + { + if( GAMESTATE->IsAnExtraStageAndSelectionLocked() ) + CycleRandomly(); + else + SetBpmFromSong( GAMESTATE->m_pCurSong ); + return; + } + if( GAMESTATE->m_pCurCourse.Get() ) + { + if( GAMESTATE->GetCurrentStyle() == NULL ) + ; // This is true when backing out from ScreenSelectCourse to ScreenTitleMenu. So, don't call SetBpmFromCourse where an assert will fire. + else + SetBpmFromCourse( GAMESTATE->m_pCurCourse ); + + return; + } + + NoBPM(); +} + +// SongBPMDisplay (in-game BPM display) +class SongBPMDisplay: public BPMDisplay +{ +public: + SongBPMDisplay(); + virtual SongBPMDisplay *Copy() const; + virtual void Update( float fDeltaTime ); + +private: + float m_fLastGameStateBPM; + +}; + +SongBPMDisplay::SongBPMDisplay() +{ + m_fLastGameStateBPM = 0; +} + +void SongBPMDisplay::Update( float fDeltaTime ) +{ + float fGameStateBPM = GAMESTATE->m_Position.m_fCurBPS * 60.0f; + if( m_fLastGameStateBPM != fGameStateBPM ) + { + m_fLastGameStateBPM = fGameStateBPM; + SetConstantBpm( fGameStateBPM ); + } + + BPMDisplay::Update( fDeltaTime ); +} + +REGISTER_ACTOR_CLASS( SongBPMDisplay ); + +#include "LuaBinding.h" +/** @brief Allow Lua to have access to the BPMDisplay. */ +class LunaBPMDisplay: public Luna +{ +public: + static int SetFromGameState( T* p, lua_State *L ) { p->SetFromGameState(); return 0; } + static int SetFromSong( T* p, lua_State *L ) + { + if( lua_isnil(L,1) ) { p->NoBPM(); } + else + { + const Song* pSong = Luna::check( L, 1, true ); + p->SetBpmFromSong(pSong); + } + return 0; + } + static int SetFromSteps( T* p, lua_State *L ) + { + if( lua_isnil(L,1) ) { p->NoBPM(); } + else + { + const Steps* pSteps = Luna::check( L, 1, true ); + p->SetBpmFromSteps(pSteps); + } + return 0; + } + static int SetFromCourse( T* p, lua_State *L ) + { + if( lua_isnil(L,1) ) { p->NoBPM(); } + else + { + const Course* pCourse = Luna::check( L, 1, true ); + p->SetBpmFromCourse(pCourse); + } + return 0; + } + static int GetText( T* p, lua_State *L ) { lua_pushstring( L, p->GetText() ); return 1; } + + LunaBPMDisplay() + { + ADD_METHOD( SetFromGameState ); + ADD_METHOD( SetFromSong ); + ADD_METHOD( SetFromSteps ); + ADD_METHOD( SetFromCourse ); + ADD_METHOD( GetText ); + } +}; + +LUA_REGISTER_DERIVED_CLASS( BPMDisplay, BitmapText ) + +/* + * (c) 2001-2002 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. + */ diff --git a/src/Background.cpp b/src/Background.cpp index 4e3549420e..37c89b32bb 100644 --- a/src/Background.cpp +++ b/src/Background.cpp @@ -373,7 +373,7 @@ bool BackgroundImpl::Layer::CreateBackground( const Song *pSong, const Backgroun Actor *pActor = ActorUtil::MakeActor( sEffectFile ); - ASSERT( pActor != NULL ); + ASSERT( pActor != nullptr ); m_BGAnimations[bd] = pActor; for( unsigned i=0; iSupportsTextureFormat(pf) ); - ASSERT(m_pImage != NULL); + ASSERT(m_pImage != nullptr); m_uTexHandle = DISPLAY->CreateTexture( pf, m_pImage, false ); CreateFrameRects(); @@ -290,7 +290,7 @@ RageTextureID BannerCache::LoadCachedBanner( RString sBannerPath ) * when converting; this way, the conversion will end up in the map so we * only have to convert once. */ RageSurface *&pImage = g_BannerPathToImage[sBannerPath]; - ASSERT( pImage != NULL ); + ASSERT( pImage != nullptr ); int iSourceWidth = 0, iSourceHeight = 0; BannerData.GetValue( sBannerPath, "Width", iSourceWidth ); diff --git a/src/BeginnerHelper.cpp b/src/BeginnerHelper.cpp index 5fe57b0c64..6340e4d9ee 100644 --- a/src/BeginnerHelper.cpp +++ b/src/BeginnerHelper.cpp @@ -1,406 +1,406 @@ -#include "global.h" - -#include "ActorUtil.h" -#include "BeginnerHelper.h" -#include "GameState.h" -#include "PrefsManager.h" -#include "RageLog.h" -#include "RageDisplay.h" -#include "Steps.h" -#include "Style.h" -#include "Model.h" -#include "ScreenDimensions.h" -#include "ThemeManager.h" - -// "PLAYER_X" offsets are relative to the pad. -// ex: Setting this to 10, and the HELPER to 300, will put the dancer at 310. -#define PLAYER_X( px ) THEME->GetMetricF("BeginnerHelper",ssprintf("Player%dX",px+1)) - -// "HELPER" offsets effect the pad/dancer as a whole. -// Their relative Y cooridinates are hard-coded for each other. -#define HELPER_X THEME->GetMetricF("BeginnerHelper","HelperX") -#define HELPER_Y THEME->GetMetricF("BeginnerHelper","HelperY") - -#define ST_LEFT 0x01 -#define ST_DOWN 0x02 -#define ST_UP 0x04 -#define ST_RIGHT 0x08 -#define ST_JUMPLR (ST_LEFT | ST_RIGHT) -#define ST_JUMPUD (ST_UP | ST_DOWN) - -enum Animation -{ - ANIM_DANCE_PAD, - ANIM_DANCE_PADS, - ANIM_UP, - ANIM_DOWN, - ANIM_LEFT, - ANIM_RIGHT, - ANIM_JUMPLR, - NUM_ANIMATIONS -}; - -static const char *anims[NUM_ANIMATIONS] = -{ - "DancePad.txt", - "DancePads.txt", - "BeginnerHelper_step-up.bones.txt", - "BeginnerHelper_step-down.bones.txt", - "BeginnerHelper_step-left.bones.txt", - "BeginnerHelper_step-right.bones.txt", - "BeginnerHelper_step-jumplr.bones.txt" -}; - -static RString GetAnimPath( Animation a ) -{ - return RString( "Characters/" ) + anims[a]; -} - -BeginnerHelper::BeginnerHelper() -{ - m_bShowBackground = true; - m_bInitialized = false; - m_iLastRowChecked = m_iLastRowFlashed = 0; - FOREACH_PlayerNumber( pn ) - m_bPlayerEnabled[pn] = false; - FOREACH_PlayerNumber( pn ) - m_pDancer[pn] = new Model; - m_pDancePad = new Model; -} - -BeginnerHelper::~BeginnerHelper() -{ - FOREACH_PlayerNumber( pn ) - delete m_pDancer[pn]; - delete m_pDancePad; -} - -bool BeginnerHelper::Init( int iDancePadType ) -{ - ASSERT( !m_bInitialized ); - if( !CanUse() ) - return false; - - // If no players were successfully added, bail. - { - bool bAnyLoaded = false; - for( int pn=0; pnGetPathG("BeginnerHelper","background") ); - this->AddChild( m_sBackground ); - //m_sBackground.SetXY( 1, 1 ); - - m_sFlash.Load( THEME->GetPathG("BeginnerHelper","flash") ); - m_sFlash.SetXY( 0, 0 ); - m_sFlash.SetDiffuseAlpha( 0 ); - } - - // Load StepCircle graphics - for( int lsc=0; lscGetPathG("BeginnerHelper","stepcircle") ); - m_sStepCircle[lsc][lsce].SetZoom( 0 ); // Hide until needed. - this->AddChild(&m_sStepCircle[lsc][lsce]); - - // Set StepCircle coordinates - switch( lsce ) - { - case 0: m_sStepCircle[lsc][lsce].SetXY((HELPER_X+PLAYER_X(lsc)-80),HELPER_Y); break; // Left - case 1: m_sStepCircle[lsc][lsce].SetXY((HELPER_X+PLAYER_X(lsc)+80),HELPER_Y); break; // Right - case 2: m_sStepCircle[lsc][lsce].SetXY((HELPER_X+PLAYER_X(lsc)),(HELPER_Y-60)); break; // Up - case 3: m_sStepCircle[lsc][lsce].SetXY((HELPER_X+PLAYER_X(lsc)),(HELPER_Y+60)); break; // Down - } - } - } - - SHOW_DANCE_PAD.Load( "BeginnerHelper","ShowDancePad" ); - // Load the DancePad - if( SHOW_DANCE_PAD ) - { - switch( iDancePadType ) - { - case 0: break; // No pad - case 1: m_pDancePad->LoadMilkshapeAscii(GetAnimPath(ANIM_DANCE_PAD)); break; - case 2: m_pDancePad->LoadMilkshapeAscii(GetAnimPath(ANIM_DANCE_PADS)); break; - } - - m_pDancePad->SetName("DancePad"); - m_pDancePad->SetX( HELPER_X ); - m_pDancePad->SetY( HELPER_Y ); - ActorUtil::LoadAllCommands( m_pDancePad, "BeginnerHelper" ); - } - - for( int pl=0; plm_pCurCharacters[pl]; - ASSERT( Character != NULL ); - - m_pDancer[pl]->SetName( ssprintf("PlayerP%d",pl+1) ); - - // Load textures - m_pDancer[pl]->LoadMilkshapeAscii( Character->GetModelPath() ); - - // Load needed animations - m_pDancer[pl]->LoadMilkshapeAsciiBones( "Step-LEFT", GetAnimPath(ANIM_LEFT) ); - m_pDancer[pl]->LoadMilkshapeAsciiBones( "Step-DOWN", GetAnimPath(ANIM_DOWN) ); - m_pDancer[pl]->LoadMilkshapeAsciiBones( "Step-UP", GetAnimPath(ANIM_UP) ); - m_pDancer[pl]->LoadMilkshapeAsciiBones( "Step-RIGHT", GetAnimPath(ANIM_RIGHT) ); - m_pDancer[pl]->LoadMilkshapeAsciiBones( "Step-JUMPLR", GetAnimPath(ANIM_JUMPLR) ); - m_pDancer[pl]->LoadMilkshapeAsciiBones( "rest", Character->GetRestAnimationPath() ); - m_pDancer[pl]->SetDefaultAnimation( "rest" ); // Stay bouncing after a step has finished animating - m_pDancer[pl]->PlayAnimation( "rest" ); - m_pDancer[pl]->SetX( HELPER_X+PLAYER_X(pl) ); - m_pDancer[pl]->SetY( HELPER_Y+10 ); - ActorUtil::LoadAllCommandsAndOnCommand( m_pDancer[pl], "BeginnerHelper" ); - // many of the models floating around have the vertex order flipped, so force this. - m_pDancer[pl]->SetCullMode( CULL_NONE ); - } - - m_bInitialized = true; - return true; -} - -void BeginnerHelper::ShowStepCircle( PlayerNumber pn, int CSTEP ) -{ - int isc=0; // Save OR issues within array boundries.. it's worth the extra few bytes of memory. - switch(CSTEP) - { - case ST_LEFT: isc=0; break; - case ST_RIGHT: isc=1; break; - case ST_UP: isc=2; break; - case ST_DOWN: isc=3; break; - } - - m_sStepCircle[pn][isc].StopEffect(); - m_sStepCircle[pn][isc].SetZoom( 2 ); - m_sStepCircle[pn][isc].StopTweening(); - m_sStepCircle[pn][isc].BeginTweening( GAMESTATE->m_Position.m_fCurBPS/3, TWEEN_LINEAR ); - m_sStepCircle[pn][isc].SetZoom( 0 ); -} - -void BeginnerHelper::AddPlayer( PlayerNumber pn, const NoteData &ns ) -{ - ASSERT( !m_bInitialized ); - ASSERT( pn >= 0 && pn < NUM_PLAYERS ); - ASSERT( GAMESTATE->IsHumanPlayer(pn) ); - - if( !CanUse() ) - return; - - const Character *Character = GAMESTATE->m_pCurCharacters[pn]; - ASSERT( Character != NULL ); - if( !DoesFileExist(Character->GetModelPath()) ) - return; - - m_NoteData[pn].CopyAll( ns ); - m_bPlayerEnabled[pn] = true; -} - -bool BeginnerHelper::CanUse() -{ - for (int i=0; iGetCurrentStyle()->m_bCanUseBeginnerHelper; -} - -void BeginnerHelper::DrawPrimitives() -{ - // If not initialized, don't bother with this - if( !m_bInitialized ) - return; - - ActorFrame::DrawPrimitives(); - m_sFlash.Draw(); - - bool DrawCelShaded = PREFSMAN->m_bCelShadeModels; - // Draw Pad - if( SHOW_DANCE_PAD ) - { - if( DrawCelShaded ) - m_pDancePad->DrawCelShaded(); - else - { - DISPLAY->SetLighting( true ); - DISPLAY->SetLightDirectional( - 0, - RageColor(0.5f,0.5f,0.5f,1), - RageColor(1,1,1,1), - RageColor(0,0,0,1), - RageVector3(0, 0, 1) ); - - m_pDancePad->Draw(); - DISPLAY->ClearZBuffer(); // So character doesn't step "into" the dance pad. - DISPLAY->SetLightOff( 0 ); - DISPLAY->SetLighting( false ); - } - } - - // Draw StepCircles - for(int scd=0; scdIsHumanPlayer(pn) ) - m_pDancer[pn]->DrawCelShaded(); - } - else - { - DISPLAY->SetLighting( true ); - DISPLAY->SetLightDirectional( - 0, - RageColor(0.5f,0.5f,0.5f,1), - RageColor(1,1,1,1), - RageColor(0,0,0,1), - RageVector3(0, 0, 1) ); - - FOREACH_PlayerNumber( pn ) - if( GAMESTATE->IsHumanPlayer(pn) ) - m_pDancer[pn]->Draw(); - - DISPLAY->SetLightOff( 0 ); - DISPLAY->SetLighting( false ); - } -} - -void BeginnerHelper::Step( PlayerNumber pn, int CSTEP ) -{ - m_pDancer[pn]->StopTweening(); - m_pDancer[pn]->SetRotationY( 0 ); // Make sure we're not still inside of a JUMPUD tween. - - switch(CSTEP) - { - case ST_LEFT: - ShowStepCircle( pn, ST_LEFT ); - m_pDancer[pn]->PlayAnimation( "Step-LEFT", 1.5f ); - break; - case ST_RIGHT: - ShowStepCircle( pn, ST_RIGHT ); - m_pDancer[pn]->PlayAnimation( "Step-RIGHT", 1.5f ); - break; - case ST_UP: - ShowStepCircle( pn, ST_UP ); - m_pDancer[pn]->PlayAnimation( "Step-UP", 1.5f ); - break; - case ST_DOWN: - ShowStepCircle( pn, ST_DOWN ); - m_pDancer[pn]->PlayAnimation( "Step-DOWN", 1.5f ); - break; - case ST_JUMPLR: - ShowStepCircle( pn, ST_LEFT ); - ShowStepCircle( pn, ST_RIGHT ); - m_pDancer[pn]->PlayAnimation( "Step-JUMPLR", 1.5f ); - break; - case ST_JUMPUD: - ShowStepCircle( pn, ST_UP ); - ShowStepCircle( pn, ST_DOWN ); - m_pDancer[pn]->StopTweening(); - m_pDancer[pn]->PlayAnimation( "Step-JUMPLR", 1.5f ); - m_pDancer[pn]->BeginTweening( GAMESTATE->m_Position.m_fCurBPS/8, TWEEN_LINEAR ); - m_pDancer[pn]->SetRotationY( 90 ); - m_pDancer[pn]->BeginTweening( 1/(GAMESTATE->m_Position.m_fCurBPS * 2) ); //sleep between jump-frames - m_pDancer[pn]->BeginTweening( GAMESTATE->m_Position.m_fCurBPS /6, TWEEN_LINEAR ); - m_pDancer[pn]->SetRotationY( 0 ); - break; - } - - m_sFlash.StopEffect(); - m_sFlash.StopTweening(); - m_sFlash.Sleep( GAMESTATE->m_Position.m_fCurBPS/16 ); - m_sFlash.SetDiffuseAlpha( 1 ); - m_sFlash.BeginTweening( 1/GAMESTATE->m_Position.m_fCurBPS * 0.5f ); - m_sFlash.SetDiffuseAlpha( 0 ); -} - -void BeginnerHelper::Update( float fDeltaTime ) -{ - if( !m_bInitialized ) - return; - - // the row we want to check on this update - int iCurRow = BeatToNoteRowNotRounded( GAMESTATE->m_Position.m_fSongBeat + 0.4f ); - FOREACH_EnabledPlayer( pn ) - { - for( int iRow=m_iLastRowChecked; iRowStep( pn, iStep ); - } - } - - // Make sure we don't accidentally scan a row 2x - m_iLastRowChecked = iCurRow; - - // Update animations - ActorFrame::Update( fDeltaTime ); - m_pDancePad->Update( fDeltaTime ); - m_sFlash.Update( fDeltaTime ); - - float beat = fDeltaTime*GAMESTATE->m_Position.m_fCurBPS; - // If this is not a human player, the dancer is not shown - FOREACH_HumanPlayer( pu ) - { - // Update dancer's animation and StepCircles - m_pDancer[pu]->Update( beat ); - for( int scu=0; scuGetMetricF("BeginnerHelper",ssprintf("Player%dX",px+1)) + +// "HELPER" offsets effect the pad/dancer as a whole. +// Their relative Y cooridinates are hard-coded for each other. +#define HELPER_X THEME->GetMetricF("BeginnerHelper","HelperX") +#define HELPER_Y THEME->GetMetricF("BeginnerHelper","HelperY") + +#define ST_LEFT 0x01 +#define ST_DOWN 0x02 +#define ST_UP 0x04 +#define ST_RIGHT 0x08 +#define ST_JUMPLR (ST_LEFT | ST_RIGHT) +#define ST_JUMPUD (ST_UP | ST_DOWN) + +enum Animation +{ + ANIM_DANCE_PAD, + ANIM_DANCE_PADS, + ANIM_UP, + ANIM_DOWN, + ANIM_LEFT, + ANIM_RIGHT, + ANIM_JUMPLR, + NUM_ANIMATIONS +}; + +static const char *anims[NUM_ANIMATIONS] = +{ + "DancePad.txt", + "DancePads.txt", + "BeginnerHelper_step-up.bones.txt", + "BeginnerHelper_step-down.bones.txt", + "BeginnerHelper_step-left.bones.txt", + "BeginnerHelper_step-right.bones.txt", + "BeginnerHelper_step-jumplr.bones.txt" +}; + +static RString GetAnimPath( Animation a ) +{ + return RString( "Characters/" ) + anims[a]; +} + +BeginnerHelper::BeginnerHelper() +{ + m_bShowBackground = true; + m_bInitialized = false; + m_iLastRowChecked = m_iLastRowFlashed = 0; + FOREACH_PlayerNumber( pn ) + m_bPlayerEnabled[pn] = false; + FOREACH_PlayerNumber( pn ) + m_pDancer[pn] = new Model; + m_pDancePad = new Model; +} + +BeginnerHelper::~BeginnerHelper() +{ + FOREACH_PlayerNumber( pn ) + delete m_pDancer[pn]; + delete m_pDancePad; +} + +bool BeginnerHelper::Init( int iDancePadType ) +{ + ASSERT( !m_bInitialized ); + if( !CanUse() ) + return false; + + // If no players were successfully added, bail. + { + bool bAnyLoaded = false; + for( int pn=0; pnGetPathG("BeginnerHelper","background") ); + this->AddChild( m_sBackground ); + //m_sBackground.SetXY( 1, 1 ); + + m_sFlash.Load( THEME->GetPathG("BeginnerHelper","flash") ); + m_sFlash.SetXY( 0, 0 ); + m_sFlash.SetDiffuseAlpha( 0 ); + } + + // Load StepCircle graphics + for( int lsc=0; lscGetPathG("BeginnerHelper","stepcircle") ); + m_sStepCircle[lsc][lsce].SetZoom( 0 ); // Hide until needed. + this->AddChild(&m_sStepCircle[lsc][lsce]); + + // Set StepCircle coordinates + switch( lsce ) + { + case 0: m_sStepCircle[lsc][lsce].SetXY((HELPER_X+PLAYER_X(lsc)-80),HELPER_Y); break; // Left + case 1: m_sStepCircle[lsc][lsce].SetXY((HELPER_X+PLAYER_X(lsc)+80),HELPER_Y); break; // Right + case 2: m_sStepCircle[lsc][lsce].SetXY((HELPER_X+PLAYER_X(lsc)),(HELPER_Y-60)); break; // Up + case 3: m_sStepCircle[lsc][lsce].SetXY((HELPER_X+PLAYER_X(lsc)),(HELPER_Y+60)); break; // Down + } + } + } + + SHOW_DANCE_PAD.Load( "BeginnerHelper","ShowDancePad" ); + // Load the DancePad + if( SHOW_DANCE_PAD ) + { + switch( iDancePadType ) + { + case 0: break; // No pad + case 1: m_pDancePad->LoadMilkshapeAscii(GetAnimPath(ANIM_DANCE_PAD)); break; + case 2: m_pDancePad->LoadMilkshapeAscii(GetAnimPath(ANIM_DANCE_PADS)); break; + } + + m_pDancePad->SetName("DancePad"); + m_pDancePad->SetX( HELPER_X ); + m_pDancePad->SetY( HELPER_Y ); + ActorUtil::LoadAllCommands( m_pDancePad, "BeginnerHelper" ); + } + + for( int pl=0; plm_pCurCharacters[pl]; + ASSERT( Character != nullptr ); + + m_pDancer[pl]->SetName( ssprintf("PlayerP%d",pl+1) ); + + // Load textures + m_pDancer[pl]->LoadMilkshapeAscii( Character->GetModelPath() ); + + // Load needed animations + m_pDancer[pl]->LoadMilkshapeAsciiBones( "Step-LEFT", GetAnimPath(ANIM_LEFT) ); + m_pDancer[pl]->LoadMilkshapeAsciiBones( "Step-DOWN", GetAnimPath(ANIM_DOWN) ); + m_pDancer[pl]->LoadMilkshapeAsciiBones( "Step-UP", GetAnimPath(ANIM_UP) ); + m_pDancer[pl]->LoadMilkshapeAsciiBones( "Step-RIGHT", GetAnimPath(ANIM_RIGHT) ); + m_pDancer[pl]->LoadMilkshapeAsciiBones( "Step-JUMPLR", GetAnimPath(ANIM_JUMPLR) ); + m_pDancer[pl]->LoadMilkshapeAsciiBones( "rest", Character->GetRestAnimationPath() ); + m_pDancer[pl]->SetDefaultAnimation( "rest" ); // Stay bouncing after a step has finished animating + m_pDancer[pl]->PlayAnimation( "rest" ); + m_pDancer[pl]->SetX( HELPER_X+PLAYER_X(pl) ); + m_pDancer[pl]->SetY( HELPER_Y+10 ); + ActorUtil::LoadAllCommandsAndOnCommand( m_pDancer[pl], "BeginnerHelper" ); + // many of the models floating around have the vertex order flipped, so force this. + m_pDancer[pl]->SetCullMode( CULL_NONE ); + } + + m_bInitialized = true; + return true; +} + +void BeginnerHelper::ShowStepCircle( PlayerNumber pn, int CSTEP ) +{ + int isc=0; // Save OR issues within array boundries.. it's worth the extra few bytes of memory. + switch(CSTEP) + { + case ST_LEFT: isc=0; break; + case ST_RIGHT: isc=1; break; + case ST_UP: isc=2; break; + case ST_DOWN: isc=3; break; + } + + m_sStepCircle[pn][isc].StopEffect(); + m_sStepCircle[pn][isc].SetZoom( 2 ); + m_sStepCircle[pn][isc].StopTweening(); + m_sStepCircle[pn][isc].BeginTweening( GAMESTATE->m_Position.m_fCurBPS/3, TWEEN_LINEAR ); + m_sStepCircle[pn][isc].SetZoom( 0 ); +} + +void BeginnerHelper::AddPlayer( PlayerNumber pn, const NoteData &ns ) +{ + ASSERT( !m_bInitialized ); + ASSERT( pn >= 0 && pn < NUM_PLAYERS ); + ASSERT( GAMESTATE->IsHumanPlayer(pn) ); + + if( !CanUse() ) + return; + + const Character *Character = GAMESTATE->m_pCurCharacters[pn]; + ASSERT( Character != nullptr ); + if( !DoesFileExist(Character->GetModelPath()) ) + return; + + m_NoteData[pn].CopyAll( ns ); + m_bPlayerEnabled[pn] = true; +} + +bool BeginnerHelper::CanUse() +{ + for (int i=0; iGetCurrentStyle()->m_bCanUseBeginnerHelper; +} + +void BeginnerHelper::DrawPrimitives() +{ + // If not initialized, don't bother with this + if( !m_bInitialized ) + return; + + ActorFrame::DrawPrimitives(); + m_sFlash.Draw(); + + bool DrawCelShaded = PREFSMAN->m_bCelShadeModels; + // Draw Pad + if( SHOW_DANCE_PAD ) + { + if( DrawCelShaded ) + m_pDancePad->DrawCelShaded(); + else + { + DISPLAY->SetLighting( true ); + DISPLAY->SetLightDirectional( + 0, + RageColor(0.5f,0.5f,0.5f,1), + RageColor(1,1,1,1), + RageColor(0,0,0,1), + RageVector3(0, 0, 1) ); + + m_pDancePad->Draw(); + DISPLAY->ClearZBuffer(); // So character doesn't step "into" the dance pad. + DISPLAY->SetLightOff( 0 ); + DISPLAY->SetLighting( false ); + } + } + + // Draw StepCircles + for(int scd=0; scdIsHumanPlayer(pn) ) + m_pDancer[pn]->DrawCelShaded(); + } + else + { + DISPLAY->SetLighting( true ); + DISPLAY->SetLightDirectional( + 0, + RageColor(0.5f,0.5f,0.5f,1), + RageColor(1,1,1,1), + RageColor(0,0,0,1), + RageVector3(0, 0, 1) ); + + FOREACH_PlayerNumber( pn ) + if( GAMESTATE->IsHumanPlayer(pn) ) + m_pDancer[pn]->Draw(); + + DISPLAY->SetLightOff( 0 ); + DISPLAY->SetLighting( false ); + } +} + +void BeginnerHelper::Step( PlayerNumber pn, int CSTEP ) +{ + m_pDancer[pn]->StopTweening(); + m_pDancer[pn]->SetRotationY( 0 ); // Make sure we're not still inside of a JUMPUD tween. + + switch(CSTEP) + { + case ST_LEFT: + ShowStepCircle( pn, ST_LEFT ); + m_pDancer[pn]->PlayAnimation( "Step-LEFT", 1.5f ); + break; + case ST_RIGHT: + ShowStepCircle( pn, ST_RIGHT ); + m_pDancer[pn]->PlayAnimation( "Step-RIGHT", 1.5f ); + break; + case ST_UP: + ShowStepCircle( pn, ST_UP ); + m_pDancer[pn]->PlayAnimation( "Step-UP", 1.5f ); + break; + case ST_DOWN: + ShowStepCircle( pn, ST_DOWN ); + m_pDancer[pn]->PlayAnimation( "Step-DOWN", 1.5f ); + break; + case ST_JUMPLR: + ShowStepCircle( pn, ST_LEFT ); + ShowStepCircle( pn, ST_RIGHT ); + m_pDancer[pn]->PlayAnimation( "Step-JUMPLR", 1.5f ); + break; + case ST_JUMPUD: + ShowStepCircle( pn, ST_UP ); + ShowStepCircle( pn, ST_DOWN ); + m_pDancer[pn]->StopTweening(); + m_pDancer[pn]->PlayAnimation( "Step-JUMPLR", 1.5f ); + m_pDancer[pn]->BeginTweening( GAMESTATE->m_Position.m_fCurBPS/8, TWEEN_LINEAR ); + m_pDancer[pn]->SetRotationY( 90 ); + m_pDancer[pn]->BeginTweening( 1/(GAMESTATE->m_Position.m_fCurBPS * 2) ); //sleep between jump-frames + m_pDancer[pn]->BeginTweening( GAMESTATE->m_Position.m_fCurBPS /6, TWEEN_LINEAR ); + m_pDancer[pn]->SetRotationY( 0 ); + break; + } + + m_sFlash.StopEffect(); + m_sFlash.StopTweening(); + m_sFlash.Sleep( GAMESTATE->m_Position.m_fCurBPS/16 ); + m_sFlash.SetDiffuseAlpha( 1 ); + m_sFlash.BeginTweening( 1/GAMESTATE->m_Position.m_fCurBPS * 0.5f ); + m_sFlash.SetDiffuseAlpha( 0 ); +} + +void BeginnerHelper::Update( float fDeltaTime ) +{ + if( !m_bInitialized ) + return; + + // the row we want to check on this update + int iCurRow = BeatToNoteRowNotRounded( GAMESTATE->m_Position.m_fSongBeat + 0.4f ); + FOREACH_EnabledPlayer( pn ) + { + for( int iRow=m_iLastRowChecked; iRowStep( pn, iStep ); + } + } + + // Make sure we don't accidentally scan a row 2x + m_iLastRowChecked = iCurRow; + + // Update animations + ActorFrame::Update( fDeltaTime ); + m_pDancePad->Update( fDeltaTime ); + m_sFlash.Update( fDeltaTime ); + + float beat = fDeltaTime*GAMESTATE->m_Position.m_fCurBPS; + // If this is not a human player, the dancer is not shown + FOREACH_HumanPlayer( pu ) + { + // Update dancer's animation and StepCircles + m_pDancer[pu]->Update( beat ); + for( int scu=0; scuUnloadFont( m_pFont ); - if( cpy.m_pFont != NULL ) + if( cpy.m_pFont != nullptr ) m_pFont = FONT->CopyFont( cpy.m_pFont ); else m_pFont = NULL; @@ -363,7 +363,7 @@ void BitmapText::DrawChars( bool bUseStrokeTexture ) * in sAlternateText, too, just use sText. */ void BitmapText::SetText( const RString& _sText, const RString& _sAlternateText, int iWrapWidthPixels ) { - ASSERT( m_pFont != NULL ); + ASSERT( m_pFont != nullptr ); RString sNewText = StringWillUseAlternate(_sText,_sAlternateText) ? _sAlternateText : _sText; @@ -505,7 +505,7 @@ void BitmapText::UpdateBaseZoom() bool BitmapText::StringWillUseAlternate( const RString& sText, const RString& sAlternateText ) const { - ASSERT( m_pFont != NULL ); + ASSERT( m_pFont != nullptr ); // Can't use the alternate if there isn't one. if( !sAlternateText.size() ) @@ -711,7 +711,7 @@ void BitmapText::SetHorizAlign( float f ) void BitmapText::SetWrapWidthPixels( int iWrapWidthPixels ) { - ASSERT( m_pFont != NULL ); // always load a font first + ASSERT( m_pFont != nullptr ); // always load a font first if( m_iWrapWidthPixels == iWrapWidthPixels ) return; m_iWrapWidthPixels = iWrapWidthPixels; diff --git a/src/CharacterManager.cpp b/src/CharacterManager.cpp index 7bec33d08a..707189354d 100644 --- a/src/CharacterManager.cpp +++ b/src/CharacterManager.cpp @@ -127,7 +127,7 @@ public: static int GetCharacter( T* p, lua_State *L ) { Character *pCharacter = p->GetCharacterFromID(SArg(1)); - if( pCharacter != NULL ) + if( pCharacter != nullptr ) pCharacter->PushSelf( L ); else lua_pushnil( L ); @@ -137,7 +137,7 @@ public: static int GetRandomCharacter( T* p, lua_State *L ) { Character *pCharacter = p->GetRandomCharacter(); - if( pCharacter != NULL ) + if( pCharacter != nullptr ) pCharacter->PushSelf( L ); else lua_pushnil( L ); diff --git a/src/Course.cpp b/src/Course.cpp index 1b04e39895..b507706be9 100644 --- a/src/Course.cpp +++ b/src/Course.cpp @@ -690,7 +690,7 @@ int Course::GetMeter( StepsType st, CourseDifficulty cd ) const if( m_iCustomMeter[cd] != -1 ) return m_iCustomMeter[cd]; const Trail* pTrail = GetTrail( st ); - if( pTrail != NULL ) + if( pTrail != nullptr ) return pTrail->GetMeter(); return 0; } @@ -893,7 +893,7 @@ void Course::UpdateCourseStats( StepsType st ) for(unsigned i = 0; i < m_vEntries.size(); i++) { Song *pSong = m_vEntries[i].songID.ToSong(); - if( pSong != NULL ) + if( pSong != nullptr ) continue; if ( m_SortOrder_Ranking == 2 ) @@ -904,7 +904,7 @@ void Course::UpdateCourseStats( StepsType st ) const Trail* pTrail = GetTrail( st, Difficulty_Medium ); - m_SortOrder_TotalDifficulty += pTrail != NULL? pTrail->GetTotalMeter():0; + m_SortOrder_TotalDifficulty += pTrail != nullptr? pTrail->GetTotalMeter():0; // OPTIMIZATION: Ranking info isn't dependent on style, so call it // sparingly. It's handled on startup and when themes change. diff --git a/src/CourseUtil.cpp b/src/CourseUtil.cpp index 17397ba92c..8d88f3212c 100644 --- a/src/CourseUtil.cpp +++ b/src/CourseUtil.cpp @@ -195,7 +195,7 @@ void CourseUtil::SortCoursePointerArrayByNumPlays( vector &vpCoursesInO void CourseUtil::SortCoursePointerArrayByNumPlays( vector &vpCoursesInOut, const Profile* pProfile, bool bDescending ) { - ASSERT( pProfile != NULL ); + ASSERT( pProfile != nullptr ); for(unsigned i = 0; i < vpCoursesInOut.size(); ++i) course_sort_val[vpCoursesInOut[i]] = ssprintf( "%09i", pProfile->GetCourseNumTimesPlayed(vpCoursesInOut[i]) ); stable_sort( vpCoursesInOut.begin(), vpCoursesInOut.end(), bDescending ? CompareCoursePointersBySortValueDescending : CompareCoursePointersBySortValueAscending ); @@ -422,7 +422,7 @@ bool EditCourseUtil::ValidateEditCourseName( const RString &sAnswer, RString &sE } static const RString sInvalidChars = "\\/:*?\"<>|"; - if( strpbrk(sAnswer, sInvalidChars) != NULL ) + if( strpbrk(sAnswer, sInvalidChars) != nullptr ) { sErrorOut = ssprintf( EDIT_NAME_CANNOT_CONTAIN.GetValue(), sInvalidChars.c_str() ); return false; @@ -448,7 +448,7 @@ bool EditCourseUtil::ValidateEditCourseName( const RString &sAnswer, RString &sE void EditCourseUtil::UpdateAndSetTrail() { - ASSERT( GAMESTATE->m_pCurStyle != NULL ); + ASSERT( GAMESTATE->m_pCurStyle != nullptr ); StepsType st = GAMESTATE->m_pCurStyle->m_StepsType; Trail *pTrail = NULL; if( GAMESTATE->m_pCurCourse ) diff --git a/src/CreateZip.cpp b/src/CreateZip.cpp index 1911f3ec19..9faec3c8f0 100644 --- a/src/CreateZip.cpp +++ b/src/CreateZip.cpp @@ -631,7 +631,7 @@ unsigned TZip::swrite(void *param,const char *buf, unsigned size) unsigned int TZip::write(const char *buf,unsigned int size) { const char *srcbuf=buf; - if (pfout != NULL) + if (pfout != nullptr) { unsigned long writ = pfout->Write( srcbuf, size ); return writ; diff --git a/src/DancingCharacters.cpp b/src/DancingCharacters.cpp index 9c4c9bb4fe..7599479016 100644 --- a/src/DancingCharacters.cpp +++ b/src/DancingCharacters.cpp @@ -1,417 +1,417 @@ -#include "global.h" -#include "DancingCharacters.h" -#include "GameConstantsAndTypes.h" -#include "RageDisplay.h" -#include "RageUtil.h" -#include "RageMath.h" -#include "GameState.h" -#include "Song.h" -#include "Character.h" -#include "StatsManager.h" -#include "PrefsManager.h" -#include "Model.h" - -int Neg1OrPos1(); - -#define DC_X( choice ) THEME->GetMetricF("DancingCharacters",ssprintf("2DCharacterXP%d",choice+1)) -#define DC_Y( choice ) THEME->GetMetricF("DancingCharacters",ssprintf("2DCharacterYP%d",choice+1)) - -/* - * TODO: - * - Metrics/Lua for lighting and camera sweeping. - * - Ability to load secondary elements i.e. stages. - * - Remove support for 2D characters (Lua can do it). - * - Cleanup! - * - * -- Colby - */ -const float CAMERA_REST_DISTANCE = 32.f; -const float CAMERA_REST_LOOK_AT_HEIGHT = -11.f; - -const float CAMERA_SWEEP_DISTANCE = 28.f; -const float CAMERA_SWEEP_DISTANCE_VARIANCE = 4.f; -const float CAMERA_SWEEP_HEIGHT_VARIANCE = 7.f; -const float CAMERA_SWEEP_PAN_Y_RANGE_DEGREES = 45.f; -const float CAMERA_SWEEP_PAN_Y_VARIANCE_DEGREES = 60.f; -const float CAMERA_SWEEP_LOOK_AT_HEIGHT = -11.f; - -const float CAMERA_STILL_DISTANCE = 26.f; -const float CAMERA_STILL_DISTANCE_VARIANCE = 3.f; -const float CAMERA_STILL_PAN_Y_RANGE_DEGREES = 120.f; -const float CAMERA_STILL_HEIGHT_VARIANCE = 5.f; -const float CAMERA_STILL_LOOK_AT_HEIGHT = -10.f; - -const float MODEL_X_ONE_PLAYER = 0; -const float MODEL_X_TWO_PLAYERS[NUM_PLAYERS] = { +8, -8 }; -const float MODEL_ROTATIONY_TWO_PLAYERS[NUM_PLAYERS] = { -90, 90 }; - -DancingCharacters::DancingCharacters(): m_bDrawDangerLight(false), - m_CameraDistance(0), m_CameraPanYStart(0), m_CameraPanYEnd(0), - m_fLookAtHeight(0), m_fCameraHeightStart(0), m_fCameraHeightEnd(0), - m_fThisCameraStartBeat(0), m_fThisCameraEndBeat(0) -{ - FOREACH_PlayerNumber( p ) - { - m_pCharacter[p] = new Model; - m_2DIdleTimer[p].SetZero(); - m_i2DAnimState[p] = AS2D_IDLE; // start on idle state - if( !GAMESTATE->IsPlayerEnabled(p) ) - continue; - - Character* pChar = GAMESTATE->m_pCurCharacters[p]; - if( !pChar ) - continue; - - // load in any potential 2D stuff - RString sCharacterDirectory = pChar->m_sCharDir; - RString sCurrentAnim; - sCurrentAnim = sCharacterDirectory + "2DIdle"; - if( DoesFileExist(sCurrentAnim + "/BGAnimation.ini") ) // check 2D Idle BGAnim exists - { - m_bgIdle[p].Load( sCurrentAnim ); - m_bgIdle[p]->SetXY(DC_X(p),DC_Y(p)); - } - - sCurrentAnim = sCharacterDirectory + "2DMiss"; - if( DoesFileExist(sCurrentAnim + "/BGAnimation.ini") ) // check 2D Idle BGAnim exists - { - m_bgMiss[p].Load( sCurrentAnim ); - m_bgMiss[p]->SetXY(DC_X(p),DC_Y(p)); - } - - sCurrentAnim = sCharacterDirectory + "2DGood"; - if( DoesFileExist(sCurrentAnim + "/BGAnimation.ini") ) // check 2D Idle BGAnim exists - { - m_bgGood[p].Load( sCurrentAnim ); - m_bgGood[p]->SetXY(DC_X(p),DC_Y(p)); - } - - sCurrentAnim = sCharacterDirectory + "2DGreat"; - if( DoesFileExist(sCurrentAnim + "/BGAnimation.ini") ) // check 2D Idle BGAnim exists - { - m_bgGreat[p].Load( sCurrentAnim ); - m_bgGreat[p]->SetXY(DC_X(p),DC_Y(p)); - } - - sCurrentAnim = sCharacterDirectory + "2DFever"; - if( DoesFileExist(sCurrentAnim + "/BGAnimation.ini") ) // check 2D Idle BGAnim exists - { - m_bgFever[p].Load( sCurrentAnim ); - m_bgFever[p]->SetXY(DC_X(p),DC_Y(p)); - } - - sCurrentAnim = sCharacterDirectory + "2DFail"; - if( DoesFileExist(sCurrentAnim + "/BGAnimation.ini") ) // check 2D Idle BGAnim exists - { - m_bgFail[p].Load( sCurrentAnim ); - m_bgFail[p]->SetXY(DC_X(p),DC_Y(p)); - } - - sCurrentAnim = sCharacterDirectory + "2DWin"; - if( DoesFileExist(sCurrentAnim + "/BGAnimation.ini") ) // check 2D Idle BGAnim exists - { - m_bgWin[p].Load( sCurrentAnim ); - m_bgWin[p]->SetXY(DC_X(p),DC_Y(p)); - } - - sCurrentAnim = sCharacterDirectory + "2DWinFever"; - if( DoesFileExist(sCurrentAnim + "/BGAnimation.ini") ) // check 2D Idle BGAnim exists - { - m_bgWinFever[p].Load( sCurrentAnim ); - m_bgWinFever[p]->SetXY(DC_X(p),DC_Y(p)); - } - - if( pChar->GetModelPath().empty() ) - continue; - - if( GAMESTATE->GetNumPlayersEnabled()==2 ) - m_pCharacter[p]->SetX( MODEL_X_TWO_PLAYERS[p] ); - else - m_pCharacter[p]->SetX( MODEL_X_ONE_PLAYER ); - - switch( GAMESTATE->m_PlayMode ) - { - case PLAY_MODE_BATTLE: - case PLAY_MODE_RAVE: - m_pCharacter[p]->SetRotationY( MODEL_ROTATIONY_TWO_PLAYERS[p] ); - default: - break; - } - - m_pCharacter[p]->LoadMilkshapeAscii( pChar->GetModelPath() ); - m_pCharacter[p]->LoadMilkshapeAsciiBones( "rest", pChar->GetRestAnimationPath() ); - m_pCharacter[p]->LoadMilkshapeAsciiBones( "warmup", pChar->GetWarmUpAnimationPath() ); - m_pCharacter[p]->LoadMilkshapeAsciiBones( "dance", pChar->GetDanceAnimationPath() ); - m_pCharacter[p]->SetCullMode( CULL_NONE ); // many of the models floating around have the vertex order flipped - - m_pCharacter[p]->RunCommands( pChar->m_cmdInit ); - } -} - -DancingCharacters::~DancingCharacters() -{ - FOREACH_PlayerNumber( p ) - delete m_pCharacter[p]; -} - -void DancingCharacters::LoadNextSong() -{ - // initial camera sweep is still - m_CameraDistance = CAMERA_REST_DISTANCE; - m_CameraPanYStart = 0; - m_CameraPanYEnd = 0; - m_fCameraHeightStart = CAMERA_REST_LOOK_AT_HEIGHT; - m_fCameraHeightEnd = CAMERA_REST_LOOK_AT_HEIGHT; - m_fLookAtHeight = CAMERA_REST_LOOK_AT_HEIGHT; - m_fThisCameraStartBeat = 0; - m_fThisCameraEndBeat = 0; - - ASSERT( GAMESTATE->m_pCurSong != NULL ); - m_fThisCameraEndBeat = GAMESTATE->m_pCurSong->GetFirstBeat(); - - FOREACH_PlayerNumber( p ) - if( GAMESTATE->IsPlayerEnabled(p) ) - m_pCharacter[p]->PlayAnimation( "rest" ); -} - -int Neg1OrPos1() { return RandomInt( 2 ) ? -1 : +1; } - -void DancingCharacters::Update( float fDelta ) -{ - if( GAMESTATE->m_Position.m_bFreeze || GAMESTATE->m_Position.m_bDelay ) - { - // spin the camera Matrix-style - m_CameraPanYStart += fDelta*40; - m_CameraPanYEnd += fDelta*40; - } - else - { - // make the characters move - float fBPM = GAMESTATE->m_Position.m_fCurBPS*60; - float fUpdateScale = SCALE( fBPM, 60.f, 300.f, 0.75f, 1.5f ); - CLAMP( fUpdateScale, 0.75f, 1.5f ); - - /* It's OK for the animation to go slower than natural when we're - * at a very low music rate. */ - fUpdateScale *= GAMESTATE->m_SongOptions.GetCurrent().m_fMusicRate; - - FOREACH_PlayerNumber( p ) - { - if( GAMESTATE->IsPlayerEnabled(p) ) - m_pCharacter[p]->Update( fDelta*fUpdateScale ); - } - } - - static bool bWasGameplayStarting = false; - bool bGameplayStarting = GAMESTATE->m_bGameplayLeadIn; - if( !bWasGameplayStarting && bGameplayStarting ) - { - FOREACH_PlayerNumber( p ) - if( GAMESTATE->IsPlayerEnabled(p) ) - m_pCharacter[p]->PlayAnimation( "warmup" ); - } - bWasGameplayStarting = bGameplayStarting; - - static float fLastBeat = GAMESTATE->m_Position.m_fSongBeat; - float firstBeat = GAMESTATE->m_pCurSong->GetFirstBeat(); - float fThisBeat = GAMESTATE->m_Position.m_fSongBeat; - if( fLastBeat < firstBeat && fThisBeat >= firstBeat ) - { - FOREACH_PlayerNumber( p ) - m_pCharacter[p]->PlayAnimation( "dance" ); - } - fLastBeat = fThisBeat; - - // time for a new sweep? - if( GAMESTATE->m_Position.m_fSongBeat > m_fThisCameraEndBeat ) - { - if( RandomInt(6) >= 4 ) - { - // sweeping camera - m_CameraDistance = CAMERA_SWEEP_DISTANCE + RandomInt(-1,1) * CAMERA_SWEEP_DISTANCE_VARIANCE; - m_CameraPanYStart = m_CameraPanYEnd = RandomInt(-1,1) * CAMERA_SWEEP_PAN_Y_RANGE_DEGREES; - m_fCameraHeightStart = m_fCameraHeightEnd = CAMERA_STILL_LOOK_AT_HEIGHT; - - m_CameraPanYEnd += RandomInt(-1,1) * CAMERA_SWEEP_PAN_Y_VARIANCE_DEGREES; - m_fCameraHeightStart = m_fCameraHeightEnd = m_fCameraHeightStart + RandomInt(-1,1) * CAMERA_SWEEP_HEIGHT_VARIANCE; - - float fCameraHeightVariance = RandomInt(-1,1) * CAMERA_SWEEP_HEIGHT_VARIANCE; - m_fCameraHeightStart -= fCameraHeightVariance; - m_fCameraHeightEnd += fCameraHeightVariance; - - m_fLookAtHeight = CAMERA_SWEEP_LOOK_AT_HEIGHT; - } - else - { - // still camera - m_CameraDistance = CAMERA_STILL_DISTANCE + RandomInt(-1,1) * CAMERA_STILL_DISTANCE_VARIANCE; - m_CameraPanYStart = m_CameraPanYEnd = Neg1OrPos1() * CAMERA_STILL_PAN_Y_RANGE_DEGREES; - m_fCameraHeightStart = m_fCameraHeightEnd = CAMERA_SWEEP_LOOK_AT_HEIGHT + Neg1OrPos1() * CAMERA_STILL_HEIGHT_VARIANCE; - - m_fLookAtHeight = CAMERA_STILL_LOOK_AT_HEIGHT; - } - - int iCurBeat = (int)GAMESTATE->m_Position.m_fSongBeat; - iCurBeat -= iCurBeat%8; - - m_fThisCameraStartBeat = (float) iCurBeat; - m_fThisCameraEndBeat = float(iCurBeat + 8); - } - /* - // is there any of this still around? This block of code is _ugly_. -Colby - // update any 2D stuff - FOREACH_PlayerNumber( p ) - { - if( m_bgIdle[p].IsLoaded() ) - { - if( m_bgIdle[p].IsLoaded() && m_i2DAnimState[p] == AS2D_IDLE ) - m_bgIdle[p]->Update( fDelta ); - if( m_bgMiss[p].IsLoaded() && m_i2DAnimState[p] == AS2D_MISS ) - m_bgMiss[p]->Update( fDelta ); - if( m_bgGood[p].IsLoaded() && m_i2DAnimState[p] == AS2D_GOOD ) - m_bgGood[p]->Update( fDelta ); - if( m_bgGreat[p].IsLoaded() && m_i2DAnimState[p] == AS2D_GREAT ) - m_bgGreat[p]->Update( fDelta ); - if( m_bgFever[p].IsLoaded() && m_i2DAnimState[p] == AS2D_FEVER ) - m_bgFever[p]->Update( fDelta ); - if( m_bgFail[p].IsLoaded() && m_i2DAnimState[p] == AS2D_FAIL ) - m_bgFail[p]->Update( fDelta ); - if( m_bgWin[p].IsLoaded() && m_i2DAnimState[p] == AS2D_WIN ) - m_bgWin[p]->Update( fDelta ); - if( m_bgWinFever[p].IsLoaded() && m_i2DAnimState[p] == AS2D_WINFEVER ) - m_bgWinFever[p]->Update(fDelta); - - if(m_i2DAnimState[p] != AS2D_IDLE) // if we're not in idle state, start a timer to return us to idle - { - // never return to idle state if we have failed / passed (i.e. completed) the song - if(m_i2DAnimState[p] != AS2D_WINFEVER && m_i2DAnimState[p] != AS2D_FAIL && m_i2DAnimState[p] != AS2D_WIN) - { - if(m_2DIdleTimer[p].IsZero()) - m_2DIdleTimer[p].Touch(); - if(!m_2DIdleTimer[p].IsZero() && m_2DIdleTimer[p].Ago() > 1.0f) - { - m_2DIdleTimer[p].SetZero(); - m_i2DAnimState[p] = AS2D_IDLE; - } - } - } - } - } - */ -} - -void DancingCharacters::Change2DAnimState( PlayerNumber pn, int iState ) -{ - ASSERT( pn < NUM_PLAYERS ); - ASSERT( iState < AS2D_MAXSTATES ); - - m_i2DAnimState[pn] = iState; -} - -void DancingCharacters::DrawPrimitives() -{ - DISPLAY->CameraPushMatrix(); - - float fPercentIntoSweep; - if(m_fThisCameraStartBeat == m_fThisCameraEndBeat) - fPercentIntoSweep = 0; - else - fPercentIntoSweep = SCALE(GAMESTATE->m_Position.m_fSongBeat, m_fThisCameraStartBeat, m_fThisCameraEndBeat, 0.f, 1.f ); - float fCameraPanY = SCALE( fPercentIntoSweep, 0.f, 1.f, m_CameraPanYStart, m_CameraPanYEnd ); - float fCameraHeight = SCALE( fPercentIntoSweep, 0.f, 1.f, m_fCameraHeightStart, m_fCameraHeightEnd ); - - RageVector3 m_CameraPoint( 0, fCameraHeight, -m_CameraDistance ); - RageMatrix CameraRot; - RageMatrixRotationY( &CameraRot, fCameraPanY ); - RageVec3TransformCoord( &m_CameraPoint, &m_CameraPoint, &CameraRot ); - - RageVector3 m_LookAt( 0, m_fLookAtHeight, 0 ); - - DISPLAY->LoadLookAt( 45, - m_CameraPoint, - m_LookAt, - RageVector3(0,1,0) ); - - FOREACH_EnabledPlayer( p ) - { - bool bFailed = STATSMAN->m_CurStageStats.m_player[p].m_bFailed; - bool bDanger = m_bDrawDangerLight; - - DISPLAY->SetLighting( true ); - - RageColor ambient = bFailed ? RageColor(0.2f,0.1f,0.1f,1) : (bDanger ? RageColor(0.4f,0.1f,0.1f,1) : RageColor(0.4f,0.4f,0.4f,1)); - RageColor diffuse = bFailed ? RageColor(0.4f,0.1f,0.1f,1) : (bDanger ? RageColor(0.8f,0.1f,0.1f,1) : RageColor(1,0.95f,0.925f,1)); - RageColor specular = RageColor(0.8f,0.8f,0.8f,1); - DISPLAY->SetLightDirectional( - 0, - ambient, - diffuse, - specular, - RageVector3(-3, -7.5f, +9) ); - - if( PREFSMAN->m_bCelShadeModels ) - { - m_pCharacter[p]->DrawCelShaded(); - - DISPLAY->SetLightOff( 0 ); - DISPLAY->SetLighting( false ); - continue; - } - - m_pCharacter[p]->Draw(); - - DISPLAY->SetLightOff( 0 ); - DISPLAY->SetLighting( false ); - } - - DISPLAY->CameraPopMatrix(); - - /* - // Ugly! -Colby - // now draw any potential 2D stuff - FOREACH_PlayerNumber( p ) - { - if(m_bgIdle[p].IsLoaded() && m_i2DAnimState[p] == AS2D_IDLE) - m_bgIdle[p]->Draw(); - if(m_bgMiss[p].IsLoaded() && m_i2DAnimState[p] == AS2D_MISS) - m_bgMiss[p]->Draw(); - if(m_bgGood[p].IsLoaded() && m_i2DAnimState[p] == AS2D_GOOD) - m_bgGood[p]->Draw(); - if(m_bgGreat[p].IsLoaded() && m_i2DAnimState[p] == AS2D_GREAT) - m_bgGreat[p]->Draw(); - if(m_bgFever[p].IsLoaded() && m_i2DAnimState[p] == AS2D_FEVER) - m_bgFever[p]->Draw(); - if(m_bgWinFever[p].IsLoaded() && m_i2DAnimState[p] == AS2D_WINFEVER) - m_bgWinFever[p]->Draw(); - if(m_bgWin[p].IsLoaded() && m_i2DAnimState[p] == AS2D_WIN) - m_bgWin[p]->Draw(); - if(m_bgFail[p].IsLoaded() && m_i2DAnimState[p] == AS2D_FAIL) - m_bgFail[p]->Draw(); - } - */ -} - -/* - * (c) 2003-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. - */ +#include "global.h" +#include "DancingCharacters.h" +#include "GameConstantsAndTypes.h" +#include "RageDisplay.h" +#include "RageUtil.h" +#include "RageMath.h" +#include "GameState.h" +#include "Song.h" +#include "Character.h" +#include "StatsManager.h" +#include "PrefsManager.h" +#include "Model.h" + +int Neg1OrPos1(); + +#define DC_X( choice ) THEME->GetMetricF("DancingCharacters",ssprintf("2DCharacterXP%d",choice+1)) +#define DC_Y( choice ) THEME->GetMetricF("DancingCharacters",ssprintf("2DCharacterYP%d",choice+1)) + +/* + * TODO: + * - Metrics/Lua for lighting and camera sweeping. + * - Ability to load secondary elements i.e. stages. + * - Remove support for 2D characters (Lua can do it). + * - Cleanup! + * + * -- Colby + */ +const float CAMERA_REST_DISTANCE = 32.f; +const float CAMERA_REST_LOOK_AT_HEIGHT = -11.f; + +const float CAMERA_SWEEP_DISTANCE = 28.f; +const float CAMERA_SWEEP_DISTANCE_VARIANCE = 4.f; +const float CAMERA_SWEEP_HEIGHT_VARIANCE = 7.f; +const float CAMERA_SWEEP_PAN_Y_RANGE_DEGREES = 45.f; +const float CAMERA_SWEEP_PAN_Y_VARIANCE_DEGREES = 60.f; +const float CAMERA_SWEEP_LOOK_AT_HEIGHT = -11.f; + +const float CAMERA_STILL_DISTANCE = 26.f; +const float CAMERA_STILL_DISTANCE_VARIANCE = 3.f; +const float CAMERA_STILL_PAN_Y_RANGE_DEGREES = 120.f; +const float CAMERA_STILL_HEIGHT_VARIANCE = 5.f; +const float CAMERA_STILL_LOOK_AT_HEIGHT = -10.f; + +const float MODEL_X_ONE_PLAYER = 0; +const float MODEL_X_TWO_PLAYERS[NUM_PLAYERS] = { +8, -8 }; +const float MODEL_ROTATIONY_TWO_PLAYERS[NUM_PLAYERS] = { -90, 90 }; + +DancingCharacters::DancingCharacters(): m_bDrawDangerLight(false), + m_CameraDistance(0), m_CameraPanYStart(0), m_CameraPanYEnd(0), + m_fLookAtHeight(0), m_fCameraHeightStart(0), m_fCameraHeightEnd(0), + m_fThisCameraStartBeat(0), m_fThisCameraEndBeat(0) +{ + FOREACH_PlayerNumber( p ) + { + m_pCharacter[p] = new Model; + m_2DIdleTimer[p].SetZero(); + m_i2DAnimState[p] = AS2D_IDLE; // start on idle state + if( !GAMESTATE->IsPlayerEnabled(p) ) + continue; + + Character* pChar = GAMESTATE->m_pCurCharacters[p]; + if( !pChar ) + continue; + + // load in any potential 2D stuff + RString sCharacterDirectory = pChar->m_sCharDir; + RString sCurrentAnim; + sCurrentAnim = sCharacterDirectory + "2DIdle"; + if( DoesFileExist(sCurrentAnim + "/BGAnimation.ini") ) // check 2D Idle BGAnim exists + { + m_bgIdle[p].Load( sCurrentAnim ); + m_bgIdle[p]->SetXY(DC_X(p),DC_Y(p)); + } + + sCurrentAnim = sCharacterDirectory + "2DMiss"; + if( DoesFileExist(sCurrentAnim + "/BGAnimation.ini") ) // check 2D Idle BGAnim exists + { + m_bgMiss[p].Load( sCurrentAnim ); + m_bgMiss[p]->SetXY(DC_X(p),DC_Y(p)); + } + + sCurrentAnim = sCharacterDirectory + "2DGood"; + if( DoesFileExist(sCurrentAnim + "/BGAnimation.ini") ) // check 2D Idle BGAnim exists + { + m_bgGood[p].Load( sCurrentAnim ); + m_bgGood[p]->SetXY(DC_X(p),DC_Y(p)); + } + + sCurrentAnim = sCharacterDirectory + "2DGreat"; + if( DoesFileExist(sCurrentAnim + "/BGAnimation.ini") ) // check 2D Idle BGAnim exists + { + m_bgGreat[p].Load( sCurrentAnim ); + m_bgGreat[p]->SetXY(DC_X(p),DC_Y(p)); + } + + sCurrentAnim = sCharacterDirectory + "2DFever"; + if( DoesFileExist(sCurrentAnim + "/BGAnimation.ini") ) // check 2D Idle BGAnim exists + { + m_bgFever[p].Load( sCurrentAnim ); + m_bgFever[p]->SetXY(DC_X(p),DC_Y(p)); + } + + sCurrentAnim = sCharacterDirectory + "2DFail"; + if( DoesFileExist(sCurrentAnim + "/BGAnimation.ini") ) // check 2D Idle BGAnim exists + { + m_bgFail[p].Load( sCurrentAnim ); + m_bgFail[p]->SetXY(DC_X(p),DC_Y(p)); + } + + sCurrentAnim = sCharacterDirectory + "2DWin"; + if( DoesFileExist(sCurrentAnim + "/BGAnimation.ini") ) // check 2D Idle BGAnim exists + { + m_bgWin[p].Load( sCurrentAnim ); + m_bgWin[p]->SetXY(DC_X(p),DC_Y(p)); + } + + sCurrentAnim = sCharacterDirectory + "2DWinFever"; + if( DoesFileExist(sCurrentAnim + "/BGAnimation.ini") ) // check 2D Idle BGAnim exists + { + m_bgWinFever[p].Load( sCurrentAnim ); + m_bgWinFever[p]->SetXY(DC_X(p),DC_Y(p)); + } + + if( pChar->GetModelPath().empty() ) + continue; + + if( GAMESTATE->GetNumPlayersEnabled()==2 ) + m_pCharacter[p]->SetX( MODEL_X_TWO_PLAYERS[p] ); + else + m_pCharacter[p]->SetX( MODEL_X_ONE_PLAYER ); + + switch( GAMESTATE->m_PlayMode ) + { + case PLAY_MODE_BATTLE: + case PLAY_MODE_RAVE: + m_pCharacter[p]->SetRotationY( MODEL_ROTATIONY_TWO_PLAYERS[p] ); + default: + break; + } + + m_pCharacter[p]->LoadMilkshapeAscii( pChar->GetModelPath() ); + m_pCharacter[p]->LoadMilkshapeAsciiBones( "rest", pChar->GetRestAnimationPath() ); + m_pCharacter[p]->LoadMilkshapeAsciiBones( "warmup", pChar->GetWarmUpAnimationPath() ); + m_pCharacter[p]->LoadMilkshapeAsciiBones( "dance", pChar->GetDanceAnimationPath() ); + m_pCharacter[p]->SetCullMode( CULL_NONE ); // many of the models floating around have the vertex order flipped + + m_pCharacter[p]->RunCommands( pChar->m_cmdInit ); + } +} + +DancingCharacters::~DancingCharacters() +{ + FOREACH_PlayerNumber( p ) + delete m_pCharacter[p]; +} + +void DancingCharacters::LoadNextSong() +{ + // initial camera sweep is still + m_CameraDistance = CAMERA_REST_DISTANCE; + m_CameraPanYStart = 0; + m_CameraPanYEnd = 0; + m_fCameraHeightStart = CAMERA_REST_LOOK_AT_HEIGHT; + m_fCameraHeightEnd = CAMERA_REST_LOOK_AT_HEIGHT; + m_fLookAtHeight = CAMERA_REST_LOOK_AT_HEIGHT; + m_fThisCameraStartBeat = 0; + m_fThisCameraEndBeat = 0; + + ASSERT( GAMESTATE->m_pCurSong != nullptr ); + m_fThisCameraEndBeat = GAMESTATE->m_pCurSong->GetFirstBeat(); + + FOREACH_PlayerNumber( p ) + if( GAMESTATE->IsPlayerEnabled(p) ) + m_pCharacter[p]->PlayAnimation( "rest" ); +} + +int Neg1OrPos1() { return RandomInt( 2 ) ? -1 : +1; } + +void DancingCharacters::Update( float fDelta ) +{ + if( GAMESTATE->m_Position.m_bFreeze || GAMESTATE->m_Position.m_bDelay ) + { + // spin the camera Matrix-style + m_CameraPanYStart += fDelta*40; + m_CameraPanYEnd += fDelta*40; + } + else + { + // make the characters move + float fBPM = GAMESTATE->m_Position.m_fCurBPS*60; + float fUpdateScale = SCALE( fBPM, 60.f, 300.f, 0.75f, 1.5f ); + CLAMP( fUpdateScale, 0.75f, 1.5f ); + + /* It's OK for the animation to go slower than natural when we're + * at a very low music rate. */ + fUpdateScale *= GAMESTATE->m_SongOptions.GetCurrent().m_fMusicRate; + + FOREACH_PlayerNumber( p ) + { + if( GAMESTATE->IsPlayerEnabled(p) ) + m_pCharacter[p]->Update( fDelta*fUpdateScale ); + } + } + + static bool bWasGameplayStarting = false; + bool bGameplayStarting = GAMESTATE->m_bGameplayLeadIn; + if( !bWasGameplayStarting && bGameplayStarting ) + { + FOREACH_PlayerNumber( p ) + if( GAMESTATE->IsPlayerEnabled(p) ) + m_pCharacter[p]->PlayAnimation( "warmup" ); + } + bWasGameplayStarting = bGameplayStarting; + + static float fLastBeat = GAMESTATE->m_Position.m_fSongBeat; + float firstBeat = GAMESTATE->m_pCurSong->GetFirstBeat(); + float fThisBeat = GAMESTATE->m_Position.m_fSongBeat; + if( fLastBeat < firstBeat && fThisBeat >= firstBeat ) + { + FOREACH_PlayerNumber( p ) + m_pCharacter[p]->PlayAnimation( "dance" ); + } + fLastBeat = fThisBeat; + + // time for a new sweep? + if( GAMESTATE->m_Position.m_fSongBeat > m_fThisCameraEndBeat ) + { + if( RandomInt(6) >= 4 ) + { + // sweeping camera + m_CameraDistance = CAMERA_SWEEP_DISTANCE + RandomInt(-1,1) * CAMERA_SWEEP_DISTANCE_VARIANCE; + m_CameraPanYStart = m_CameraPanYEnd = RandomInt(-1,1) * CAMERA_SWEEP_PAN_Y_RANGE_DEGREES; + m_fCameraHeightStart = m_fCameraHeightEnd = CAMERA_STILL_LOOK_AT_HEIGHT; + + m_CameraPanYEnd += RandomInt(-1,1) * CAMERA_SWEEP_PAN_Y_VARIANCE_DEGREES; + m_fCameraHeightStart = m_fCameraHeightEnd = m_fCameraHeightStart + RandomInt(-1,1) * CAMERA_SWEEP_HEIGHT_VARIANCE; + + float fCameraHeightVariance = RandomInt(-1,1) * CAMERA_SWEEP_HEIGHT_VARIANCE; + m_fCameraHeightStart -= fCameraHeightVariance; + m_fCameraHeightEnd += fCameraHeightVariance; + + m_fLookAtHeight = CAMERA_SWEEP_LOOK_AT_HEIGHT; + } + else + { + // still camera + m_CameraDistance = CAMERA_STILL_DISTANCE + RandomInt(-1,1) * CAMERA_STILL_DISTANCE_VARIANCE; + m_CameraPanYStart = m_CameraPanYEnd = Neg1OrPos1() * CAMERA_STILL_PAN_Y_RANGE_DEGREES; + m_fCameraHeightStart = m_fCameraHeightEnd = CAMERA_SWEEP_LOOK_AT_HEIGHT + Neg1OrPos1() * CAMERA_STILL_HEIGHT_VARIANCE; + + m_fLookAtHeight = CAMERA_STILL_LOOK_AT_HEIGHT; + } + + int iCurBeat = (int)GAMESTATE->m_Position.m_fSongBeat; + iCurBeat -= iCurBeat%8; + + m_fThisCameraStartBeat = (float) iCurBeat; + m_fThisCameraEndBeat = float(iCurBeat + 8); + } + /* + // is there any of this still around? This block of code is _ugly_. -Colby + // update any 2D stuff + FOREACH_PlayerNumber( p ) + { + if( m_bgIdle[p].IsLoaded() ) + { + if( m_bgIdle[p].IsLoaded() && m_i2DAnimState[p] == AS2D_IDLE ) + m_bgIdle[p]->Update( fDelta ); + if( m_bgMiss[p].IsLoaded() && m_i2DAnimState[p] == AS2D_MISS ) + m_bgMiss[p]->Update( fDelta ); + if( m_bgGood[p].IsLoaded() && m_i2DAnimState[p] == AS2D_GOOD ) + m_bgGood[p]->Update( fDelta ); + if( m_bgGreat[p].IsLoaded() && m_i2DAnimState[p] == AS2D_GREAT ) + m_bgGreat[p]->Update( fDelta ); + if( m_bgFever[p].IsLoaded() && m_i2DAnimState[p] == AS2D_FEVER ) + m_bgFever[p]->Update( fDelta ); + if( m_bgFail[p].IsLoaded() && m_i2DAnimState[p] == AS2D_FAIL ) + m_bgFail[p]->Update( fDelta ); + if( m_bgWin[p].IsLoaded() && m_i2DAnimState[p] == AS2D_WIN ) + m_bgWin[p]->Update( fDelta ); + if( m_bgWinFever[p].IsLoaded() && m_i2DAnimState[p] == AS2D_WINFEVER ) + m_bgWinFever[p]->Update(fDelta); + + if(m_i2DAnimState[p] != AS2D_IDLE) // if we're not in idle state, start a timer to return us to idle + { + // never return to idle state if we have failed / passed (i.e. completed) the song + if(m_i2DAnimState[p] != AS2D_WINFEVER && m_i2DAnimState[p] != AS2D_FAIL && m_i2DAnimState[p] != AS2D_WIN) + { + if(m_2DIdleTimer[p].IsZero()) + m_2DIdleTimer[p].Touch(); + if(!m_2DIdleTimer[p].IsZero() && m_2DIdleTimer[p].Ago() > 1.0f) + { + m_2DIdleTimer[p].SetZero(); + m_i2DAnimState[p] = AS2D_IDLE; + } + } + } + } + } + */ +} + +void DancingCharacters::Change2DAnimState( PlayerNumber pn, int iState ) +{ + ASSERT( pn < NUM_PLAYERS ); + ASSERT( iState < AS2D_MAXSTATES ); + + m_i2DAnimState[pn] = iState; +} + +void DancingCharacters::DrawPrimitives() +{ + DISPLAY->CameraPushMatrix(); + + float fPercentIntoSweep; + if(m_fThisCameraStartBeat == m_fThisCameraEndBeat) + fPercentIntoSweep = 0; + else + fPercentIntoSweep = SCALE(GAMESTATE->m_Position.m_fSongBeat, m_fThisCameraStartBeat, m_fThisCameraEndBeat, 0.f, 1.f ); + float fCameraPanY = SCALE( fPercentIntoSweep, 0.f, 1.f, m_CameraPanYStart, m_CameraPanYEnd ); + float fCameraHeight = SCALE( fPercentIntoSweep, 0.f, 1.f, m_fCameraHeightStart, m_fCameraHeightEnd ); + + RageVector3 m_CameraPoint( 0, fCameraHeight, -m_CameraDistance ); + RageMatrix CameraRot; + RageMatrixRotationY( &CameraRot, fCameraPanY ); + RageVec3TransformCoord( &m_CameraPoint, &m_CameraPoint, &CameraRot ); + + RageVector3 m_LookAt( 0, m_fLookAtHeight, 0 ); + + DISPLAY->LoadLookAt( 45, + m_CameraPoint, + m_LookAt, + RageVector3(0,1,0) ); + + FOREACH_EnabledPlayer( p ) + { + bool bFailed = STATSMAN->m_CurStageStats.m_player[p].m_bFailed; + bool bDanger = m_bDrawDangerLight; + + DISPLAY->SetLighting( true ); + + RageColor ambient = bFailed ? RageColor(0.2f,0.1f,0.1f,1) : (bDanger ? RageColor(0.4f,0.1f,0.1f,1) : RageColor(0.4f,0.4f,0.4f,1)); + RageColor diffuse = bFailed ? RageColor(0.4f,0.1f,0.1f,1) : (bDanger ? RageColor(0.8f,0.1f,0.1f,1) : RageColor(1,0.95f,0.925f,1)); + RageColor specular = RageColor(0.8f,0.8f,0.8f,1); + DISPLAY->SetLightDirectional( + 0, + ambient, + diffuse, + specular, + RageVector3(-3, -7.5f, +9) ); + + if( PREFSMAN->m_bCelShadeModels ) + { + m_pCharacter[p]->DrawCelShaded(); + + DISPLAY->SetLightOff( 0 ); + DISPLAY->SetLighting( false ); + continue; + } + + m_pCharacter[p]->Draw(); + + DISPLAY->SetLightOff( 0 ); + DISPLAY->SetLighting( false ); + } + + DISPLAY->CameraPopMatrix(); + + /* + // Ugly! -Colby + // now draw any potential 2D stuff + FOREACH_PlayerNumber( p ) + { + if(m_bgIdle[p].IsLoaded() && m_i2DAnimState[p] == AS2D_IDLE) + m_bgIdle[p]->Draw(); + if(m_bgMiss[p].IsLoaded() && m_i2DAnimState[p] == AS2D_MISS) + m_bgMiss[p]->Draw(); + if(m_bgGood[p].IsLoaded() && m_i2DAnimState[p] == AS2D_GOOD) + m_bgGood[p]->Draw(); + if(m_bgGreat[p].IsLoaded() && m_i2DAnimState[p] == AS2D_GREAT) + m_bgGreat[p]->Draw(); + if(m_bgFever[p].IsLoaded() && m_i2DAnimState[p] == AS2D_FEVER) + m_bgFever[p]->Draw(); + if(m_bgWinFever[p].IsLoaded() && m_i2DAnimState[p] == AS2D_WINFEVER) + m_bgWinFever[p]->Draw(); + if(m_bgWin[p].IsLoaded() && m_i2DAnimState[p] == AS2D_WIN) + m_bgWin[p]->Draw(); + if(m_bgFail[p].IsLoaded() && m_i2DAnimState[p] == AS2D_FAIL) + m_bgFail[p]->Draw(); + } + */ +} + +/* + * (c) 2003-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. + */ diff --git a/src/EditMenu.cpp b/src/EditMenu.cpp index 46cca2b847..e94c8056a9 100644 --- a/src/EditMenu.cpp +++ b/src/EditMenu.cpp @@ -493,7 +493,7 @@ void EditMenu::OnRowValueChanged( EditMenuRow row ) if( dc != Difficulty_Edit ) { Steps *pSteps = SongUtil::GetStepsByDifficulty( GetSelectedSong(), GetSelectedSourceStepsType(), dc ); - if( pSteps != NULL ) + if( pSteps != nullptr ) m_vpSourceSteps.push_back( StepsAndDifficulty(pSteps,dc) ); } else diff --git a/src/Font.cpp b/src/Font.cpp index e0c3841bbc..2b6b85c0a9 100644 --- a/src/Font.cpp +++ b/src/Font.cpp @@ -26,7 +26,7 @@ void FontPage::Load( const FontPageSettings &cfg ) ID1.AdditionalTextureHints = cfg.m_sTextureHints; m_FontPageTextures.m_pTextureMain = TEXTUREMAN->LoadTexture( ID1 ); - ASSERT( m_FontPageTextures.m_pTextureMain != NULL ); + ASSERT( m_FontPageTextures.m_pTextureMain != nullptr ); RageTextureID ID2 = ID1; // "arial 20 16x16 [main].png" => "arial 20 16x16 [main-stroke].png" @@ -36,7 +36,7 @@ void FontPage::Load( const FontPageSettings &cfg ) if( IsAFile(ID2.filename) ) { m_FontPageTextures.m_pTextureStroke = TEXTUREMAN->LoadTexture( ID2 ); - ASSERT( m_FontPageTextures.m_pTextureStroke != NULL ); + ASSERT( m_FontPageTextures.m_pTextureStroke != nullptr ); ASSERT_M( m_FontPageTextures.m_pTextureMain->GetSourceFrameWidth() == m_FontPageTextures.m_pTextureStroke->GetSourceFrameWidth(), ssprintf("'%s' and '%s' must have the same frame widths", ID1.filename.c_str(), ID2.filename.c_str()) ); ASSERT_M( m_FontPageTextures.m_pTextureMain->GetNumFrames() == m_FontPageTextures.m_pTextureStroke->GetNumFrames(), ssprintf("'%s' and '%s' must have the same frame dimensions", ID1.filename.c_str(), ID2.filename.c_str()) ); } @@ -187,12 +187,12 @@ void FontPage::SetExtraPixels( int iDrawExtraPixelsLeft, int iDrawExtraPixelsRig FontPage::~FontPage() { - if( m_FontPageTextures.m_pTextureMain != NULL ) + if( m_FontPageTextures.m_pTextureMain != nullptr ) { TEXTUREMAN->UnloadTexture( m_FontPageTextures.m_pTextureMain ); m_FontPageTextures.m_pTextureMain = NULL; } - if( m_FontPageTextures.m_pTextureStroke != NULL ) + if( m_FontPageTextures.m_pTextureStroke != nullptr ) { TEXTUREMAN->UnloadTexture( m_FontPageTextures.m_pTextureStroke ); m_FontPageTextures.m_pTextureStroke = NULL; @@ -346,7 +346,7 @@ void Font::CapsOnly() void Font::SetDefaultGlyph( FontPage *pPage ) { - ASSERT( pPage != NULL ); + ASSERT( pPage != nullptr ); ASSERT( !pPage->m_aGlyphs.empty() ); m_pDefault = pPage; } diff --git a/src/GameCommand.cpp b/src/GameCommand.cpp index f8342b2e78..ac9bba9dd7 100644 --- a/src/GameCommand.cpp +++ b/src/GameCommand.cpp @@ -258,7 +258,7 @@ void GameCommand::LoadOne( const Command& cmd ) // This must be processed after "song" and "style" commands. if( !m_bInvalid ) { - Song *pSong = (m_pSong != NULL)? m_pSong:GAMESTATE->m_pCurSong; + Song *pSong = (m_pSong != nullptr)? m_pSong:GAMESTATE->m_pCurSong; const Style *pStyle = m_pStyle ? m_pStyle : GAMESTATE->GetCurrentStyle(); if( pSong == NULL || pStyle == NULL ) RageException::Throw( "Must set Song and Style to set Steps." ); @@ -293,7 +293,7 @@ void GameCommand::LoadOne( const Command& cmd ) // This must be processed after "course" and "style" commands. if( !m_bInvalid ) { - Course *pCourse = (m_pCourse != NULL)? m_pCourse:GAMESTATE->m_pCurCourse; + Course *pCourse = (m_pCourse != nullptr)? m_pCourse:GAMESTATE->m_pCurCourse; const Style *pStyle = m_pStyle ? m_pStyle : GAMESTATE->GetCurrentStyle(); if( pCourse == NULL || pStyle == NULL ) RageException::Throw( "Must set Course and Style to set Steps." ); @@ -464,10 +464,10 @@ bool GameCommand::IsPlayable( RString *why ) const /* Don't allow a PlayMode that's incompatible with our current Style (if set), * and vice versa. */ - if( m_pm != PlayMode_Invalid || m_pStyle != NULL ) + if( m_pm != PlayMode_Invalid || m_pStyle != nullptr ) { const PlayMode pm = (m_pm != PlayMode_Invalid) ? m_pm : GAMESTATE->m_PlayMode; - const Style *style = (m_pStyle != NULL)? m_pStyle: GAMESTATE->GetCurrentStyle(); + const Style *style = (m_pStyle != nullptr)? m_pStyle: GAMESTATE->GetCurrentStyle(); if( !AreStyleAndPlayModeCompatible( style, pm ) ) { if( why ) @@ -572,7 +572,7 @@ void GameCommand::ApplySelf( const vector &vpns ) const if( m_pm != PlayMode_Invalid ) GAMESTATE->m_PlayMode.Set( m_pm ); - if( m_pStyle != NULL ) + if( m_pStyle != nullptr ) { GAMESTATE->SetCurrentStyle( m_pStyle ); @@ -718,16 +718,16 @@ void GameCommand::ApplySelf( const vector &vpns ) const bool GameCommand::IsZero() const { if( m_pm != PlayMode_Invalid || - m_pStyle != NULL || + m_pStyle != nullptr || m_dc != Difficulty_Invalid || m_sAnnouncer != "" || m_sPreferredModifiers != "" || m_sStageModifiers != "" || - m_pSong != NULL || - m_pSteps != NULL || - m_pCourse != NULL || - m_pTrail != NULL || - m_pCharacter != NULL || + m_pSong != nullptr || + m_pSteps != nullptr || + m_pCourse != nullptr || + m_pTrail != nullptr || + m_pCharacter != nullptr || m_CourseDifficulty != Difficulty_Invalid || !m_sSongGroup.empty() || m_SortOrder != SortOrder_Invalid || diff --git a/src/GameLoop.cpp b/src/GameLoop.cpp index 2cd7d2042d..29e394468f 100644 --- a/src/GameLoop.cpp +++ b/src/GameLoop.cpp @@ -281,7 +281,7 @@ void ConcurrentRenderer::Stop() void ConcurrentRenderer::RenderThread() { - ASSERT( SCREENMAN != NULL ); + ASSERT( SCREENMAN != nullptr ); while( !m_bShutdown ) { diff --git a/src/GameSoundManager.cpp b/src/GameSoundManager.cpp index e7256efed2..b327fee26a 100644 --- a/src/GameSoundManager.cpp +++ b/src/GameSoundManager.cpp @@ -1,884 +1,884 @@ -#include "global.h" -#include "RageSoundManager.h" -#include "GameSoundManager.h" -#include "RageSound.h" -#include "RageLog.h" -#include "RageUtil.h" -#include "GameState.h" -#include "TimingData.h" -#include "NotesLoaderSSC.h" -#include "NotesLoaderSM.h" -#include "PrefsManager.h" -#include "RageDisplay.h" -#include "AnnouncerManager.h" -#include "NoteData.h" -#include "Song.h" -#include "Steps.h" -#include "LightsManager.h" -#include "SongUtil.h" -#include "LuaManager.h" - -GameSoundManager *SOUND = NULL; - -/* - * When playing music, automatically search for an SM file for timing data. If one is - * found, automatically handle GAMESTATE->m_fSongBeat, etc. - * - * modf(GAMESTATE->m_fSongBeat) should always be continuously moving from 0 to 1. To do - * this, wait before starting a sound until the fractional portion of the beat will be - * the same. - * - * If PlayMusic(fLengthSeconds) is set, peek at the beat, and extend the length so we'll be - * on the same fractional beat when we loop. - */ - -/* Lock this before touching g_UpdatingTimer or g_Playing. */ -static RageEvent *g_Mutex; -static bool g_UpdatingTimer; -static bool g_Shutdown; -static bool g_bFlushing = false; - -enum FadeState { FADE_NONE, FADE_OUT, FADE_WAIT, FADE_IN }; -static FadeState g_FadeState = FADE_NONE; -static float g_fDimVolume = 1.0f; -static float g_fOriginalVolume = 1.0f; -static float g_fDimDurationRemaining = 0.0f; -static bool g_bWasPlayingOnLastUpdate = false; - -struct MusicPlaying -{ - bool m_bTimingDelayed; - bool m_bHasTiming; - bool m_bApplyMusicRate; - // The timing data that we're currently using. - TimingData m_Timing; - NoteData m_Lights; - - /* If m_bTimingDelayed is true, this will be the timing data for the - * song that's starting. We'll copy it to m_Timing once sound is heard. */ - TimingData m_NewTiming; - RageSound *m_Music; - MusicPlaying( RageSound *Music ) - { - m_Timing.AddSegment( BPMSegment(0,120) ); - m_NewTiming.AddSegment( BPMSegment(0,120) ); - m_bHasTiming = false; - m_bTimingDelayed = false; - m_bApplyMusicRate = false; - m_Music = Music; - } - - ~MusicPlaying() - { - delete m_Music; - } -}; - -static MusicPlaying *g_Playing; - -static RageThread MusicThread; - -vector g_SoundsToPlayOnce; -vector g_SoundsToPlayOnceFromDir; -vector g_SoundsToPlayOnceFromAnnouncer; - -struct MusicToPlay -{ - RString m_sFile, m_sTimingFile; - bool HasTiming; - TimingData m_TimingData; - NoteData m_LightsData; - bool bForceLoop; - float fStartSecond, fLengthSeconds, fFadeInLengthSeconds, fFadeOutLengthSeconds; - bool bAlignBeat, bApplyMusicRate; - MusicToPlay() - { - HasTiming = false; - } -}; -vector g_MusicsToPlay; -static GameSoundManager::PlayMusicParams g_FallbackMusicParams; - -static void StartMusic( MusicToPlay &ToPlay ) -{ - LockMutex L( *g_Mutex ); - if( g_Playing->m_Music->IsPlaying() && g_Playing->m_Music->GetLoadedFilePath().EqualsNoCase(ToPlay.m_sFile) ) - return; - - /* We're changing or stopping the music. If we were dimming, reset. */ - g_FadeState = FADE_NONE; - - if( ToPlay.m_sFile.empty() ) - { - /* StopPlaying() can take a while, so don't hold the lock while we stop the sound. - * Be sure to leave the rest of g_Playing in place. */ - RageSound *pOldSound = g_Playing->m_Music; - g_Playing->m_Music = new RageSound; - L.Unlock(); - - delete pOldSound; - return; - } - - /* Unlock, load the sound here, and relock. Loading may take a while if we're - * reading from CD and we have to seek far, which can throw off the timing below. */ - MusicPlaying *NewMusic; - { - g_Mutex->Unlock(); - RageSound *pSound = new RageSound; - RageSoundLoadParams params; - params.m_bSupportRateChanging = ToPlay.bApplyMusicRate; - pSound->Load( ToPlay.m_sFile, false, ¶ms ); - g_Mutex->Lock(); - - NewMusic = new MusicPlaying( pSound ); - } - - NewMusic->m_Timing = g_Playing->m_Timing; - NewMusic->m_Lights = g_Playing->m_Lights; - - /* See if we can find timing data, if it's not already loaded. */ - if( !ToPlay.HasTiming && IsAFile(ToPlay.m_sTimingFile) ) - { - LOG->Trace( "Found '%s'", ToPlay.m_sTimingFile.c_str() ); - Song song; - SSCLoader loaderSSC; - SMLoader loaderSM; - if(GetExtension(ToPlay.m_sTimingFile) == ".ssc" && - loaderSSC.LoadFromSimfile(ToPlay.m_sTimingFile, song) ) - { - ToPlay.HasTiming = true; - ToPlay.m_TimingData = song.m_SongTiming; - // get cabinet lights if any - Steps *pStepsCabinetLights = SongUtil::GetOneSteps( &song, StepsType_lights_cabinet ); - if( pStepsCabinetLights ) - pStepsCabinetLights->GetNoteData( ToPlay.m_LightsData ); - } - else if(GetExtension(ToPlay.m_sTimingFile) == ".sm" && - loaderSM.LoadFromSimfile(ToPlay.m_sTimingFile, song) ) - { - ToPlay.HasTiming = true; - ToPlay.m_TimingData = song.m_SongTiming; - // get cabinet lights if any - Steps *pStepsCabinetLights = SongUtil::GetOneSteps( &song, StepsType_lights_cabinet ); - if( pStepsCabinetLights ) - pStepsCabinetLights->GetNoteData( ToPlay.m_LightsData ); - } - } - - if( ToPlay.HasTiming ) - { - NewMusic->m_NewTiming = ToPlay.m_TimingData; - NewMusic->m_Lights = ToPlay.m_LightsData; - } - - if( ToPlay.bAlignBeat && ToPlay.HasTiming && ToPlay.bForceLoop && ToPlay.fLengthSeconds != -1 ) - { - /* Extend the loop period so it always starts and ends on the same fractional - * beat. That is, if it starts on beat 1.5, and ends on beat 10.2, extend it - * to end on beat 10.5. This way, effects always loop cleanly. */ - float fStartBeat = NewMusic->m_NewTiming.GetBeatFromElapsedTimeNoOffset( ToPlay.fStartSecond ); - float fEndSec = ToPlay.fStartSecond + ToPlay.fLengthSeconds; - float fEndBeat = NewMusic->m_NewTiming.GetBeatFromElapsedTimeNoOffset( fEndSec ); - - const float fStartBeatFraction = fmodfp( fStartBeat, 1 ); - const float fEndBeatFraction = fmodfp( fEndBeat, 1 ); - - float fBeatDifference = fStartBeatFraction - fEndBeatFraction; - if( fBeatDifference < 0 ) - fBeatDifference += 1.0f; /* unwrap */ - - fEndBeat += fBeatDifference; - - const float fRealEndSec = NewMusic->m_NewTiming.GetElapsedTimeFromBeatNoOffset( fEndBeat ); - const float fNewLengthSec = fRealEndSec - ToPlay.fStartSecond; - - /* Extend fFadeOutLengthSeconds, so the added time is faded out. */ - ToPlay.fFadeOutLengthSeconds += fNewLengthSec - ToPlay.fLengthSeconds; - ToPlay.fLengthSeconds = fNewLengthSec; - } - - bool StartImmediately = false; - if( !ToPlay.HasTiming ) - { - /* This song has no real timing data. The offset is arbitrary. Change it so - * the beat will line up to where we are now, so we don't have to delay. */ - float fDestBeat = fmodfp( GAMESTATE->m_Position.m_fSongBeatNoOffset, 1 ); - float fTime = NewMusic->m_NewTiming.GetElapsedTimeFromBeatNoOffset( fDestBeat ); - - NewMusic->m_NewTiming.m_fBeat0OffsetInSeconds = fTime; - - StartImmediately = true; - } - - /* If we have an active timer, try to start on the next update. Otherwise, - * start now. */ - if( !g_Playing->m_bHasTiming && !g_UpdatingTimer ) - StartImmediately = true; - if( !ToPlay.bAlignBeat ) - StartImmediately = true; - - RageTimer when; /* zero */ - if( !StartImmediately ) - { - /* GetPlayLatency returns the minimum time until a sound starts. That's - * common when starting a precached sound, but our sound isn't, so it'll - * probably take a little longer. Nudge the latency up. */ - const float fPresumedLatency = SOUNDMAN->GetPlayLatency() + 0.040f; - const float fCurSecond = GAMESTATE->m_Position.m_fMusicSeconds + fPresumedLatency; - const float fCurBeat = g_Playing->m_Timing.GetBeatFromElapsedTimeNoOffset( fCurSecond ); - - /* The beat that the new sound will start on. */ - const float fStartBeat = NewMusic->m_NewTiming.GetBeatFromElapsedTimeNoOffset( ToPlay.fStartSecond ); - const float fStartBeatFraction = fmodfp( fStartBeat, 1 ); - - float fCurBeatToStartOn = truncf(fCurBeat) + fStartBeatFraction; - if( fCurBeatToStartOn < fCurBeat ) - fCurBeatToStartOn += 1.0f; - - const float fSecondToStartOn = g_Playing->m_Timing.GetElapsedTimeFromBeatNoOffset( fCurBeatToStartOn ); - const float fMaximumDistance = 2; - const float fDistance = min( fSecondToStartOn - GAMESTATE->m_Position.m_fMusicSeconds, fMaximumDistance ); - - when = GAMESTATE->m_Position.m_LastBeatUpdate + fDistance; - } - - /* Important: don't hold the mutex while we load and seek the actual sound. */ - L.Unlock(); - { - NewMusic->m_bHasTiming = ToPlay.HasTiming; - if( ToPlay.HasTiming ) - NewMusic->m_NewTiming = ToPlay.m_TimingData; - NewMusic->m_bTimingDelayed = true; - NewMusic->m_bApplyMusicRate = ToPlay.bApplyMusicRate; -// NewMusic->m_Music->Load( ToPlay.m_sFile, false ); - - RageSoundParams p; - p.m_StartSecond = ToPlay.fStartSecond; - p.m_LengthSeconds = ToPlay.fLengthSeconds; - p.m_fFadeInSeconds = ToPlay.fFadeInLengthSeconds; - p.m_fFadeOutSeconds = ToPlay.fFadeOutLengthSeconds; - p.m_StartTime = when; - if( ToPlay.bForceLoop ) - p.StopMode = RageSoundParams::M_LOOP; - NewMusic->m_Music->SetParams( p ); - NewMusic->m_Music->StartPlaying(); - } - - LockMut( *g_Mutex ); - delete g_Playing; - g_Playing = NewMusic; -} - -static void DoPlayOnce( RString sPath ) -{ - /* We want this to start quickly, so don't try to prebuffer it. */ - RageSound *pSound = new RageSound; - pSound->Load( sPath, false ); - - pSound->Play(); - pSound->DeleteSelfWhenFinishedPlaying(); -} - -static void DoPlayOnceFromDir( RString sPath ) -{ - if( sPath == "" ) - return; - - // make sure there's a slash at the end of this path - if( sPath.Right(1) != "/" ) - sPath += "/"; - - vector arraySoundFiles; - GetDirListing( sPath + "*.mp3", arraySoundFiles ); - GetDirListing( sPath + "*.wav", arraySoundFiles ); - GetDirListing( sPath + "*.ogg", arraySoundFiles ); - GetDirListing( sPath + "*.oga", arraySoundFiles ); - - if( arraySoundFiles.empty() ) - return; - - int index = RandomInt( arraySoundFiles.size( )); - DoPlayOnce( sPath + arraySoundFiles[index] ); -} - -static bool SoundWaiting() -{ - return !g_SoundsToPlayOnce.empty() || - !g_SoundsToPlayOnceFromDir.empty() || - !g_SoundsToPlayOnceFromAnnouncer.empty() || - !g_MusicsToPlay.empty(); -} - - -static void StartQueuedSounds() -{ - g_Mutex->Lock(); - vector aSoundsToPlayOnce = g_SoundsToPlayOnce; - g_SoundsToPlayOnce.clear(); - vector aSoundsToPlayOnceFromDir = g_SoundsToPlayOnceFromDir; - g_SoundsToPlayOnceFromDir.clear(); - vector aSoundsToPlayOnceFromAnnouncer = g_SoundsToPlayOnceFromAnnouncer; - g_SoundsToPlayOnceFromAnnouncer.clear(); - vector aMusicsToPlay = g_MusicsToPlay; - g_MusicsToPlay.clear(); - g_Mutex->Unlock(); - - for( unsigned i = 0; i < aSoundsToPlayOnce.size(); ++i ) - if( aSoundsToPlayOnce[i] != "" ) - DoPlayOnce( aSoundsToPlayOnce[i] ); - - for( unsigned i = 0; i < aSoundsToPlayOnceFromDir.size(); ++i ) - DoPlayOnceFromDir( aSoundsToPlayOnceFromDir[i] ); - - for( unsigned i = 0; i < aSoundsToPlayOnceFromAnnouncer.size(); ++i ) - { - RString sPath = aSoundsToPlayOnceFromAnnouncer[i]; - if( sPath != "" ) - { - sPath = ANNOUNCER->GetPathTo( sPath ); - DoPlayOnceFromDir( sPath ); - } - } - - for( unsigned i = 0; i < aMusicsToPlay.size(); ++i ) - { - /* Don't bother starting this music if there's another one in the queue after it. */ - /* Actually, it's a little trickier: the editor gives us a stop and then a sound in - * quick succession; if we ignore the stop, we won't rewind the sound if it was - * already playing. We don't want to waste time loading a sound if it's going - * to be replaced immediately, though. So, if we have more music in the queue, - * then forcibly stop the current sound. */ - if( i+1 == aMusicsToPlay.size() ) - StartMusic( aMusicsToPlay[i] ); - else - { - CHECKPOINT; - /* StopPlaying() can take a while, so don't hold the lock while we stop the sound. */ - g_Mutex->Lock(); - RageSound *pOldSound = g_Playing->m_Music; - g_Playing->m_Music = new RageSound; - g_Mutex->Unlock(); - - delete pOldSound; - } - } -} - -void GameSoundManager::Flush() -{ - g_Mutex->Lock(); - g_bFlushing = true; - - g_Mutex->Broadcast(); - - while( g_bFlushing ) - g_Mutex->Wait(); - g_Mutex->Unlock(); -} - -int MusicThread_start( void *p ) -{ - while( !g_Shutdown ) - { - g_Mutex->Lock(); - while( !SoundWaiting() && !g_Shutdown && !g_bFlushing ) - g_Mutex->Wait(); - g_Mutex->Unlock(); - - /* This is a little hack: we want to make sure that we go through - * StartQueuedSounds after Flush() is called, to be sure we're flushed, - * so check g_bFlushing before calling. This won't work if more than - * one thread might call Flush(), but only the main thread is allowed - * to make SOUND calls. */ - bool bFlushing = g_bFlushing; - - StartQueuedSounds(); - - if( bFlushing ) - { - g_Mutex->Lock(); - g_Mutex->Signal(); - g_bFlushing = false; - g_Mutex->Unlock(); - } - } - - return 0; -} - -GameSoundManager::GameSoundManager() -{ - /* Init RageSoundMan first: */ - ASSERT( SOUNDMAN != NULL ); - - g_Mutex = new RageEvent("GameSoundManager"); - g_Playing = new MusicPlaying( new RageSound ); - - g_UpdatingTimer = true; - - g_Shutdown = false; - MusicThread.SetName( "Music thread" ); - MusicThread.Create( MusicThread_start, this ); - - // Register with Lua. - { - Lua *L = LUA->Get(); - lua_pushstring( L, "SOUND" ); - this->PushSelf( L ); - lua_settable( L, LUA_GLOBALSINDEX ); - LUA->Release( L ); - } -} - -GameSoundManager::~GameSoundManager() -{ - // Unregister with Lua. - LUA->UnsetGlobal( "SOUND" ); - - /* Signal the mixing thread to quit. */ - LOG->Trace("Shutting down music start thread ..."); - g_Mutex->Lock(); - g_Shutdown = true; - g_Mutex->Broadcast(); - g_Mutex->Unlock(); - MusicThread.Wait(); - LOG->Trace("Music start thread shut down."); - - SAFE_DELETE( g_Playing ); - SAFE_DELETE( g_Mutex ); -} - -float GameSoundManager::GetFrameTimingAdjustment( float fDeltaTime ) -{ - /* - * We get one update per frame, and we're updated early, almost immediately after vsync, - * near the beginning of the game loop. However, it's very likely that we'll lose the - * scheduler while waiting for vsync, and some other thread will be working. Especially - * with a low-resolution scheduler (Linux 2.4, Win9x), we may not get the scheduler back - * immediately after the vsync; there may be up to a ~10ms delay. This can cause jitter - * in the rendered arrows. - * - * Compensate. If vsync is enabled, and we're maintaining the refresh rate consistently, - * we should have a very precise game loop interval. If we have that, but we're off by - * a small amount (less than the interval), adjust the time to line it up. As long as we - * adjust both the sound time and the timestamp, this won't adversely affect input timing. - * If we're off by more than that, we probably had a frame skip, in which case we have - * bigger skip problems, so don't adjust. - */ - static int iLastFPS = 0; - int iThisFPS = DISPLAY->GetFPS(); - - if( iThisFPS != DISPLAY->GetActualVideoModeParams().rate || iThisFPS != iLastFPS ) - { - iLastFPS = iThisFPS; - return 0; - } - - const float fExpectedDelay = 1.0f / iThisFPS; - const float fExtraDelay = fDeltaTime - fExpectedDelay; - if( fabsf(fExtraDelay) >= fExpectedDelay/2 ) - return 0; - - /* Subtract the extra delay. */ - return min( -fExtraDelay, 0 ); -} - -void GameSoundManager::Update( float fDeltaTime ) -{ - { - g_Mutex->Lock(); - if( g_Playing->m_bApplyMusicRate ) - { - RageSoundParams p = g_Playing->m_Music->GetParams(); - float fRate = GAMESTATE->m_SongOptions.GetPreferred().m_fMusicRate; - if( p.m_fSpeed != fRate ) - { - p.m_fSpeed = fRate; - g_Playing->m_Music->SetParams( p ); - } - } - - bool bIsPlaying = g_Playing->m_Music->IsPlaying(); - g_Mutex->Unlock(); - if( !bIsPlaying && g_bWasPlayingOnLastUpdate && !g_FallbackMusicParams.sFile.empty() ) - { - PlayMusic( g_FallbackMusicParams ); - - g_FallbackMusicParams.sFile = ""; - } - g_bWasPlayingOnLastUpdate = bIsPlaying; - } - - LockMut( *g_Mutex ); - - { - /* Duration of the fade-in and fade-out: */ - //const float fFadeInSpeed = 1.5f; - //const float fFadeOutSpeed = 0.3f; - float fFadeInSpeed = g_Playing->m_Music->GetParams().m_fFadeInSeconds; - float fFadeOutSpeed = g_Playing->m_Music->GetParams().m_fFadeOutSeconds; - float fVolume = g_Playing->m_Music->GetParams().m_Volume; - switch( g_FadeState ) - { - case FADE_NONE: break; - case FADE_OUT: - fapproach( fVolume, g_fDimVolume, fDeltaTime/fFadeOutSpeed ); - if( fabsf(fVolume-g_fDimVolume) < 0.001f ) - g_FadeState = FADE_WAIT; - break; - case FADE_WAIT: - g_fDimDurationRemaining -= fDeltaTime; - if( g_fDimDurationRemaining <= 0 ) - g_FadeState = FADE_IN; - break; - case FADE_IN: - fapproach( fVolume, g_fOriginalVolume, fDeltaTime/fFadeInSpeed ); - if( fabsf(fVolume-g_fOriginalVolume) < 0.001f ) - g_FadeState = FADE_NONE; - break; - } - - RageSoundParams p = g_Playing->m_Music->GetParams(); - if( p.m_Volume != fVolume ) - { - p.m_Volume = fVolume; - g_Playing->m_Music->SetParams( p ); - } - } - - if( !g_UpdatingTimer ) - return; - - const float fAdjust = GetFrameTimingAdjustment( fDeltaTime ); - if( !g_Playing->m_Music->IsPlaying() ) - { - /* There's no song playing. Fake it. */ - CHECKPOINT_M( ssprintf("%f, delta %f", GAMESTATE->m_Position.m_fMusicSeconds, fDeltaTime) ); - GAMESTATE->UpdateSongPosition( GAMESTATE->m_Position.m_fMusicSeconds + fDeltaTime, g_Playing->m_Timing ); - return; - } - - /* There's a delay between us calling Play() and the sound actually playing. - * During this time, m_bApproximate will be true. Keep using the previous timing - * data until we get a non-approximate time, indicating that the sound has actually - * started playing. */ - bool m_bApproximate; - RageTimer tm; - const float fSeconds = g_Playing->m_Music->GetPositionSeconds( &m_bApproximate, &tm ); - - // - // Check for song timing skips. - // - if( PREFSMAN->m_bLogSkips && !g_Playing->m_bTimingDelayed ) - { - const float fExpectedTimePassed = (tm - GAMESTATE->m_Position.m_LastBeatUpdate) * g_Playing->m_Music->GetPlaybackRate(); - const float fSoundTimePassed = fSeconds - GAMESTATE->m_Position.m_fMusicSeconds; - const float fDiff = fExpectedTimePassed - fSoundTimePassed; - - static RString sLastFile = ""; - const RString ThisFile = g_Playing->m_Music->GetLoadedFilePath(); - - /* If fSoundTimePassed < 0, the sound has probably looped. */ - if( sLastFile == ThisFile && fSoundTimePassed >= 0 && fabsf(fDiff) > 0.003f ) - LOG->Trace("Song position skip in %s: expected %.3f, got %.3f (cur %f, prev %f) (%.3f difference)", - Basename(ThisFile).c_str(), fExpectedTimePassed, fSoundTimePassed, fSeconds, GAMESTATE->m_Position.m_fMusicSeconds, fDiff ); - sLastFile = ThisFile; - } - - // - // If g_Playing->m_bTimingDelayed, we're waiting for the new music to actually start - // playing. - // - if( g_Playing->m_bTimingDelayed && !m_bApproximate ) - { - /* Load up the new timing data. */ - g_Playing->m_Timing = g_Playing->m_NewTiming; - g_Playing->m_bTimingDelayed = false; - } - - if( g_Playing->m_bTimingDelayed ) - { - /* We're still waiting for the new sound to start playing, so keep using the - * old timing data and fake the time. */ - GAMESTATE->UpdateSongPosition( GAMESTATE->m_Position.m_fMusicSeconds + fDeltaTime, g_Playing->m_Timing ); - } - else - { - GAMESTATE->UpdateSongPosition( fSeconds + fAdjust, g_Playing->m_Timing, tm + fAdjust ); - } - - - // - // Send crossed messages - // - if( GAMESTATE->m_pCurSong ) - { - static int iBeatLastCrossed = 0; - - float fSongBeat = GAMESTATE->m_Position.m_fSongBeat; - - int iRowNow = BeatToNoteRowNotRounded( fSongBeat ); - iRowNow = max( 0, iRowNow ); - - int iBeatNow = iRowNow / ROWS_PER_BEAT; - - for( int iBeat = iBeatLastCrossed+1; iBeat<=iBeatNow; ++iBeat ) - { - Message msg("CrossedBeat"); - msg.SetParam( "Beat", iBeat ); - MESSAGEMAN->Broadcast( msg ); - } - - iBeatLastCrossed = iBeatNow; - } - - - // - // Update lights - // - NoteData &lights = g_Playing->m_Lights; - if( lights.GetNumTracks() > 0 ) // lights data was loaded - { - const float fSongBeat = GAMESTATE->m_Position.m_fLightSongBeat; - const int iSongRow = BeatToNoteRowNotRounded( fSongBeat ); - - static int iRowLastCrossed = 0; - - FOREACH_CabinetLight( cl ) - { - // Are we "holding" the light? - if( lights.IsHoldNoteAtRow( cl, iSongRow ) ) - { - LIGHTSMAN->BlinkCabinetLight( cl ); - continue; - } - - // Otherwise, for each index we crossed since the last update: - FOREACH_NONEMPTY_ROW_IN_TRACK_RANGE( lights, cl, r, iRowLastCrossed+1, iSongRow+1 ) - { - if( lights.GetTapNote( cl, r ).type != TapNote::empty ) - { - LIGHTSMAN->BlinkCabinetLight( cl ); - break; - } - } - } - - iRowLastCrossed = iSongRow; - } -} - - -RString GameSoundManager::GetMusicPath() const -{ - LockMut( *g_Mutex ); - return g_Playing->m_Music->GetLoadedFilePath(); -} - -void GameSoundManager::PlayMusic( - RString sFile, - const TimingData *pTiming, - bool bForceLoop, - float fStartSecond, - float fLengthSeconds, - float fFadeInLengthSeconds, - float fFadeOutLengthSeconds, - bool bAlignBeat - ) -{ - PlayMusicParams params; - params.sFile = sFile; - params.pTiming = pTiming; - params.bForceLoop = bForceLoop; - params.fStartSecond = fStartSecond; - params.fLengthSeconds = fLengthSeconds; - params.fFadeInLengthSeconds = fFadeInLengthSeconds; - params.fFadeOutLengthSeconds = fFadeOutLengthSeconds; - params.bAlignBeat = bAlignBeat; - params.bApplyMusicRate = false; - PlayMusic( params ); -} - -void GameSoundManager::PlayMusic( PlayMusicParams params, PlayMusicParams FallbackMusicParams ) -{ - g_FallbackMusicParams = FallbackMusicParams; - - // LOG->Trace("play '%s' (current '%s')", file.c_str(), g_Playing->m_Music->GetLoadedFilePath().c_str()); - - MusicToPlay ToPlay; - - ToPlay.m_sFile = params.sFile; - if( params.pTiming ) - { - ToPlay.HasTiming = true; - ToPlay.m_TimingData = *params.pTiming; - } - else - { - /* If no timing data was provided, look for it in the same place as the music file. */ - // todo: allow loading .ssc files as well -aj - ToPlay.m_sTimingFile = SetExtension( params.sFile, "sm" ); - } - - ToPlay.bForceLoop = params.bForceLoop; - ToPlay.fStartSecond = params.fStartSecond; - ToPlay.fLengthSeconds = params.fLengthSeconds; - ToPlay.fFadeInLengthSeconds = params.fFadeInLengthSeconds; - ToPlay.fFadeOutLengthSeconds = params.fFadeOutLengthSeconds; - ToPlay.bAlignBeat = params.bAlignBeat; - ToPlay.bApplyMusicRate = params.bApplyMusicRate; - - /* Add the MusicToPlay to the g_MusicsToPlay queue. */ - g_Mutex->Lock(); - g_MusicsToPlay.push_back( ToPlay ); - g_Mutex->Broadcast(); - g_Mutex->Unlock(); -} - -void GameSoundManager::DimMusic( float fVolume, float fDurationSeconds ) -{ - LockMut( *g_Mutex ); - - if( g_FadeState == FADE_NONE ) - g_fOriginalVolume = g_Playing->m_Music->GetParams().m_Volume; - // otherwise, g_fOriginalVolume is already set and m_Volume will be the - // current state, not the original state - - g_fDimDurationRemaining = fDurationSeconds; - g_fDimVolume = fVolume; - g_FadeState = FADE_OUT; -} - -void GameSoundManager::HandleSongTimer( bool on ) -{ - LockMut( *g_Mutex ); - g_UpdatingTimer = on; -} - -void GameSoundManager::PlayOnce( RString sPath ) -{ - /* Add the sound to the g_SoundsToPlayOnce queue. */ - g_Mutex->Lock(); - g_SoundsToPlayOnce.push_back( sPath ); - g_Mutex->Broadcast(); - g_Mutex->Unlock(); -} - -void GameSoundManager::PlayOnceFromDir( RString sPath ) -{ - /* Add the path to the g_SoundsToPlayOnceFromDir queue. */ - g_Mutex->Lock(); - g_SoundsToPlayOnceFromDir.push_back( sPath ); - g_Mutex->Broadcast(); - g_Mutex->Unlock(); -} - -void GameSoundManager::PlayOnceFromAnnouncer( RString sPath ) -{ - /* Add the path to the g_SoundsToPlayOnceFromAnnouncer queue. */ - g_Mutex->Lock(); - g_SoundsToPlayOnceFromAnnouncer.push_back( sPath ); - g_Mutex->Broadcast(); - g_Mutex->Unlock(); -} - -float GameSoundManager::GetPlayerBalance( PlayerNumber pn ) -{ - /* If two players are active, play sounds on each players' side. */ - if( GAMESTATE->GetNumPlayersEnabled() == 2 ) - return (pn == PLAYER_1)? -1.0f:1.0f; - else - return 0; -} - - -#include "LuaBinding.h" - -/** @brief Allow Lua to have access to the GameSoundManager. */ -class LunaGameSoundManager: public Luna -{ -public: - static int DimMusic( T* p, lua_State *L ) - { - float fVolume = FArg(1); - float fDurationSeconds = FArg(2); - p->DimMusic( fVolume, fDurationSeconds ); - return 0; - } - static int PlayOnce( T* p, lua_State *L ) - { - RString sPath = SArg(1); - p->PlayOnce( sPath ); - return 0; - } - static int PlayAnnouncer( T* p, lua_State *L ) - { - RString sPath = SArg(1); - p->PlayOnceFromAnnouncer( sPath ); - return 0; - } - static int GetPlayerBalance( T* p, lua_State *L ) - { - PlayerNumber pn = Enum::Check(L, 1); - lua_pushnumber( L, p->GetPlayerBalance(pn) ); - return 1; - } - static int PlayMusicPart( T* p, lua_State *L ) - { - RString musicPath = SArg(1); - float musicStart = FArg(2); - float musicLength = FArg(3); - float fadeIn = 0; - float fadeOut = 0; - if (lua_gettop(L) >= 4 && !lua_isnil(L,4)) - { - fadeIn = FArg(4); - if (lua_gettop(L) >= 5 && !lua_isnil(L,5)) - { - fadeOut = FArg(5); - } - } - p->PlayMusic(musicPath, NULL, false, musicStart, musicLength, - fadeIn, fadeOut); - return 0; - } - - LunaGameSoundManager() - { - ADD_METHOD( DimMusic ); - ADD_METHOD( PlayOnce ); - ADD_METHOD( PlayAnnouncer ); - ADD_METHOD( GetPlayerBalance ); - ADD_METHOD( PlayMusicPart ); - } -}; - -LUA_REGISTER_CLASS( GameSoundManager ) - - -/* - * Copyright (c) 2003-2005 Glenn Maynard - * 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. - */ - +#include "global.h" +#include "RageSoundManager.h" +#include "GameSoundManager.h" +#include "RageSound.h" +#include "RageLog.h" +#include "RageUtil.h" +#include "GameState.h" +#include "TimingData.h" +#include "NotesLoaderSSC.h" +#include "NotesLoaderSM.h" +#include "PrefsManager.h" +#include "RageDisplay.h" +#include "AnnouncerManager.h" +#include "NoteData.h" +#include "Song.h" +#include "Steps.h" +#include "LightsManager.h" +#include "SongUtil.h" +#include "LuaManager.h" + +GameSoundManager *SOUND = NULL; + +/* + * When playing music, automatically search for an SM file for timing data. If one is + * found, automatically handle GAMESTATE->m_fSongBeat, etc. + * + * modf(GAMESTATE->m_fSongBeat) should always be continuously moving from 0 to 1. To do + * this, wait before starting a sound until the fractional portion of the beat will be + * the same. + * + * If PlayMusic(fLengthSeconds) is set, peek at the beat, and extend the length so we'll be + * on the same fractional beat when we loop. + */ + +/* Lock this before touching g_UpdatingTimer or g_Playing. */ +static RageEvent *g_Mutex; +static bool g_UpdatingTimer; +static bool g_Shutdown; +static bool g_bFlushing = false; + +enum FadeState { FADE_NONE, FADE_OUT, FADE_WAIT, FADE_IN }; +static FadeState g_FadeState = FADE_NONE; +static float g_fDimVolume = 1.0f; +static float g_fOriginalVolume = 1.0f; +static float g_fDimDurationRemaining = 0.0f; +static bool g_bWasPlayingOnLastUpdate = false; + +struct MusicPlaying +{ + bool m_bTimingDelayed; + bool m_bHasTiming; + bool m_bApplyMusicRate; + // The timing data that we're currently using. + TimingData m_Timing; + NoteData m_Lights; + + /* If m_bTimingDelayed is true, this will be the timing data for the + * song that's starting. We'll copy it to m_Timing once sound is heard. */ + TimingData m_NewTiming; + RageSound *m_Music; + MusicPlaying( RageSound *Music ) + { + m_Timing.AddSegment( BPMSegment(0,120) ); + m_NewTiming.AddSegment( BPMSegment(0,120) ); + m_bHasTiming = false; + m_bTimingDelayed = false; + m_bApplyMusicRate = false; + m_Music = Music; + } + + ~MusicPlaying() + { + delete m_Music; + } +}; + +static MusicPlaying *g_Playing; + +static RageThread MusicThread; + +vector g_SoundsToPlayOnce; +vector g_SoundsToPlayOnceFromDir; +vector g_SoundsToPlayOnceFromAnnouncer; + +struct MusicToPlay +{ + RString m_sFile, m_sTimingFile; + bool HasTiming; + TimingData m_TimingData; + NoteData m_LightsData; + bool bForceLoop; + float fStartSecond, fLengthSeconds, fFadeInLengthSeconds, fFadeOutLengthSeconds; + bool bAlignBeat, bApplyMusicRate; + MusicToPlay() + { + HasTiming = false; + } +}; +vector g_MusicsToPlay; +static GameSoundManager::PlayMusicParams g_FallbackMusicParams; + +static void StartMusic( MusicToPlay &ToPlay ) +{ + LockMutex L( *g_Mutex ); + if( g_Playing->m_Music->IsPlaying() && g_Playing->m_Music->GetLoadedFilePath().EqualsNoCase(ToPlay.m_sFile) ) + return; + + /* We're changing or stopping the music. If we were dimming, reset. */ + g_FadeState = FADE_NONE; + + if( ToPlay.m_sFile.empty() ) + { + /* StopPlaying() can take a while, so don't hold the lock while we stop the sound. + * Be sure to leave the rest of g_Playing in place. */ + RageSound *pOldSound = g_Playing->m_Music; + g_Playing->m_Music = new RageSound; + L.Unlock(); + + delete pOldSound; + return; + } + + /* Unlock, load the sound here, and relock. Loading may take a while if we're + * reading from CD and we have to seek far, which can throw off the timing below. */ + MusicPlaying *NewMusic; + { + g_Mutex->Unlock(); + RageSound *pSound = new RageSound; + RageSoundLoadParams params; + params.m_bSupportRateChanging = ToPlay.bApplyMusicRate; + pSound->Load( ToPlay.m_sFile, false, ¶ms ); + g_Mutex->Lock(); + + NewMusic = new MusicPlaying( pSound ); + } + + NewMusic->m_Timing = g_Playing->m_Timing; + NewMusic->m_Lights = g_Playing->m_Lights; + + /* See if we can find timing data, if it's not already loaded. */ + if( !ToPlay.HasTiming && IsAFile(ToPlay.m_sTimingFile) ) + { + LOG->Trace( "Found '%s'", ToPlay.m_sTimingFile.c_str() ); + Song song; + SSCLoader loaderSSC; + SMLoader loaderSM; + if(GetExtension(ToPlay.m_sTimingFile) == ".ssc" && + loaderSSC.LoadFromSimfile(ToPlay.m_sTimingFile, song) ) + { + ToPlay.HasTiming = true; + ToPlay.m_TimingData = song.m_SongTiming; + // get cabinet lights if any + Steps *pStepsCabinetLights = SongUtil::GetOneSteps( &song, StepsType_lights_cabinet ); + if( pStepsCabinetLights ) + pStepsCabinetLights->GetNoteData( ToPlay.m_LightsData ); + } + else if(GetExtension(ToPlay.m_sTimingFile) == ".sm" && + loaderSM.LoadFromSimfile(ToPlay.m_sTimingFile, song) ) + { + ToPlay.HasTiming = true; + ToPlay.m_TimingData = song.m_SongTiming; + // get cabinet lights if any + Steps *pStepsCabinetLights = SongUtil::GetOneSteps( &song, StepsType_lights_cabinet ); + if( pStepsCabinetLights ) + pStepsCabinetLights->GetNoteData( ToPlay.m_LightsData ); + } + } + + if( ToPlay.HasTiming ) + { + NewMusic->m_NewTiming = ToPlay.m_TimingData; + NewMusic->m_Lights = ToPlay.m_LightsData; + } + + if( ToPlay.bAlignBeat && ToPlay.HasTiming && ToPlay.bForceLoop && ToPlay.fLengthSeconds != -1 ) + { + /* Extend the loop period so it always starts and ends on the same fractional + * beat. That is, if it starts on beat 1.5, and ends on beat 10.2, extend it + * to end on beat 10.5. This way, effects always loop cleanly. */ + float fStartBeat = NewMusic->m_NewTiming.GetBeatFromElapsedTimeNoOffset( ToPlay.fStartSecond ); + float fEndSec = ToPlay.fStartSecond + ToPlay.fLengthSeconds; + float fEndBeat = NewMusic->m_NewTiming.GetBeatFromElapsedTimeNoOffset( fEndSec ); + + const float fStartBeatFraction = fmodfp( fStartBeat, 1 ); + const float fEndBeatFraction = fmodfp( fEndBeat, 1 ); + + float fBeatDifference = fStartBeatFraction - fEndBeatFraction; + if( fBeatDifference < 0 ) + fBeatDifference += 1.0f; /* unwrap */ + + fEndBeat += fBeatDifference; + + const float fRealEndSec = NewMusic->m_NewTiming.GetElapsedTimeFromBeatNoOffset( fEndBeat ); + const float fNewLengthSec = fRealEndSec - ToPlay.fStartSecond; + + /* Extend fFadeOutLengthSeconds, so the added time is faded out. */ + ToPlay.fFadeOutLengthSeconds += fNewLengthSec - ToPlay.fLengthSeconds; + ToPlay.fLengthSeconds = fNewLengthSec; + } + + bool StartImmediately = false; + if( !ToPlay.HasTiming ) + { + /* This song has no real timing data. The offset is arbitrary. Change it so + * the beat will line up to where we are now, so we don't have to delay. */ + float fDestBeat = fmodfp( GAMESTATE->m_Position.m_fSongBeatNoOffset, 1 ); + float fTime = NewMusic->m_NewTiming.GetElapsedTimeFromBeatNoOffset( fDestBeat ); + + NewMusic->m_NewTiming.m_fBeat0OffsetInSeconds = fTime; + + StartImmediately = true; + } + + /* If we have an active timer, try to start on the next update. Otherwise, + * start now. */ + if( !g_Playing->m_bHasTiming && !g_UpdatingTimer ) + StartImmediately = true; + if( !ToPlay.bAlignBeat ) + StartImmediately = true; + + RageTimer when; /* zero */ + if( !StartImmediately ) + { + /* GetPlayLatency returns the minimum time until a sound starts. That's + * common when starting a precached sound, but our sound isn't, so it'll + * probably take a little longer. Nudge the latency up. */ + const float fPresumedLatency = SOUNDMAN->GetPlayLatency() + 0.040f; + const float fCurSecond = GAMESTATE->m_Position.m_fMusicSeconds + fPresumedLatency; + const float fCurBeat = g_Playing->m_Timing.GetBeatFromElapsedTimeNoOffset( fCurSecond ); + + /* The beat that the new sound will start on. */ + const float fStartBeat = NewMusic->m_NewTiming.GetBeatFromElapsedTimeNoOffset( ToPlay.fStartSecond ); + const float fStartBeatFraction = fmodfp( fStartBeat, 1 ); + + float fCurBeatToStartOn = truncf(fCurBeat) + fStartBeatFraction; + if( fCurBeatToStartOn < fCurBeat ) + fCurBeatToStartOn += 1.0f; + + const float fSecondToStartOn = g_Playing->m_Timing.GetElapsedTimeFromBeatNoOffset( fCurBeatToStartOn ); + const float fMaximumDistance = 2; + const float fDistance = min( fSecondToStartOn - GAMESTATE->m_Position.m_fMusicSeconds, fMaximumDistance ); + + when = GAMESTATE->m_Position.m_LastBeatUpdate + fDistance; + } + + /* Important: don't hold the mutex while we load and seek the actual sound. */ + L.Unlock(); + { + NewMusic->m_bHasTiming = ToPlay.HasTiming; + if( ToPlay.HasTiming ) + NewMusic->m_NewTiming = ToPlay.m_TimingData; + NewMusic->m_bTimingDelayed = true; + NewMusic->m_bApplyMusicRate = ToPlay.bApplyMusicRate; +// NewMusic->m_Music->Load( ToPlay.m_sFile, false ); + + RageSoundParams p; + p.m_StartSecond = ToPlay.fStartSecond; + p.m_LengthSeconds = ToPlay.fLengthSeconds; + p.m_fFadeInSeconds = ToPlay.fFadeInLengthSeconds; + p.m_fFadeOutSeconds = ToPlay.fFadeOutLengthSeconds; + p.m_StartTime = when; + if( ToPlay.bForceLoop ) + p.StopMode = RageSoundParams::M_LOOP; + NewMusic->m_Music->SetParams( p ); + NewMusic->m_Music->StartPlaying(); + } + + LockMut( *g_Mutex ); + delete g_Playing; + g_Playing = NewMusic; +} + +static void DoPlayOnce( RString sPath ) +{ + /* We want this to start quickly, so don't try to prebuffer it. */ + RageSound *pSound = new RageSound; + pSound->Load( sPath, false ); + + pSound->Play(); + pSound->DeleteSelfWhenFinishedPlaying(); +} + +static void DoPlayOnceFromDir( RString sPath ) +{ + if( sPath == "" ) + return; + + // make sure there's a slash at the end of this path + if( sPath.Right(1) != "/" ) + sPath += "/"; + + vector arraySoundFiles; + GetDirListing( sPath + "*.mp3", arraySoundFiles ); + GetDirListing( sPath + "*.wav", arraySoundFiles ); + GetDirListing( sPath + "*.ogg", arraySoundFiles ); + GetDirListing( sPath + "*.oga", arraySoundFiles ); + + if( arraySoundFiles.empty() ) + return; + + int index = RandomInt( arraySoundFiles.size( )); + DoPlayOnce( sPath + arraySoundFiles[index] ); +} + +static bool SoundWaiting() +{ + return !g_SoundsToPlayOnce.empty() || + !g_SoundsToPlayOnceFromDir.empty() || + !g_SoundsToPlayOnceFromAnnouncer.empty() || + !g_MusicsToPlay.empty(); +} + + +static void StartQueuedSounds() +{ + g_Mutex->Lock(); + vector aSoundsToPlayOnce = g_SoundsToPlayOnce; + g_SoundsToPlayOnce.clear(); + vector aSoundsToPlayOnceFromDir = g_SoundsToPlayOnceFromDir; + g_SoundsToPlayOnceFromDir.clear(); + vector aSoundsToPlayOnceFromAnnouncer = g_SoundsToPlayOnceFromAnnouncer; + g_SoundsToPlayOnceFromAnnouncer.clear(); + vector aMusicsToPlay = g_MusicsToPlay; + g_MusicsToPlay.clear(); + g_Mutex->Unlock(); + + for( unsigned i = 0; i < aSoundsToPlayOnce.size(); ++i ) + if( aSoundsToPlayOnce[i] != "" ) + DoPlayOnce( aSoundsToPlayOnce[i] ); + + for( unsigned i = 0; i < aSoundsToPlayOnceFromDir.size(); ++i ) + DoPlayOnceFromDir( aSoundsToPlayOnceFromDir[i] ); + + for( unsigned i = 0; i < aSoundsToPlayOnceFromAnnouncer.size(); ++i ) + { + RString sPath = aSoundsToPlayOnceFromAnnouncer[i]; + if( sPath != "" ) + { + sPath = ANNOUNCER->GetPathTo( sPath ); + DoPlayOnceFromDir( sPath ); + } + } + + for( unsigned i = 0; i < aMusicsToPlay.size(); ++i ) + { + /* Don't bother starting this music if there's another one in the queue after it. */ + /* Actually, it's a little trickier: the editor gives us a stop and then a sound in + * quick succession; if we ignore the stop, we won't rewind the sound if it was + * already playing. We don't want to waste time loading a sound if it's going + * to be replaced immediately, though. So, if we have more music in the queue, + * then forcibly stop the current sound. */ + if( i+1 == aMusicsToPlay.size() ) + StartMusic( aMusicsToPlay[i] ); + else + { + CHECKPOINT; + /* StopPlaying() can take a while, so don't hold the lock while we stop the sound. */ + g_Mutex->Lock(); + RageSound *pOldSound = g_Playing->m_Music; + g_Playing->m_Music = new RageSound; + g_Mutex->Unlock(); + + delete pOldSound; + } + } +} + +void GameSoundManager::Flush() +{ + g_Mutex->Lock(); + g_bFlushing = true; + + g_Mutex->Broadcast(); + + while( g_bFlushing ) + g_Mutex->Wait(); + g_Mutex->Unlock(); +} + +int MusicThread_start( void *p ) +{ + while( !g_Shutdown ) + { + g_Mutex->Lock(); + while( !SoundWaiting() && !g_Shutdown && !g_bFlushing ) + g_Mutex->Wait(); + g_Mutex->Unlock(); + + /* This is a little hack: we want to make sure that we go through + * StartQueuedSounds after Flush() is called, to be sure we're flushed, + * so check g_bFlushing before calling. This won't work if more than + * one thread might call Flush(), but only the main thread is allowed + * to make SOUND calls. */ + bool bFlushing = g_bFlushing; + + StartQueuedSounds(); + + if( bFlushing ) + { + g_Mutex->Lock(); + g_Mutex->Signal(); + g_bFlushing = false; + g_Mutex->Unlock(); + } + } + + return 0; +} + +GameSoundManager::GameSoundManager() +{ + /* Init RageSoundMan first: */ + ASSERT( SOUNDMAN != nullptr ); + + g_Mutex = new RageEvent("GameSoundManager"); + g_Playing = new MusicPlaying( new RageSound ); + + g_UpdatingTimer = true; + + g_Shutdown = false; + MusicThread.SetName( "Music thread" ); + MusicThread.Create( MusicThread_start, this ); + + // Register with Lua. + { + Lua *L = LUA->Get(); + lua_pushstring( L, "SOUND" ); + this->PushSelf( L ); + lua_settable( L, LUA_GLOBALSINDEX ); + LUA->Release( L ); + } +} + +GameSoundManager::~GameSoundManager() +{ + // Unregister with Lua. + LUA->UnsetGlobal( "SOUND" ); + + /* Signal the mixing thread to quit. */ + LOG->Trace("Shutting down music start thread ..."); + g_Mutex->Lock(); + g_Shutdown = true; + g_Mutex->Broadcast(); + g_Mutex->Unlock(); + MusicThread.Wait(); + LOG->Trace("Music start thread shut down."); + + SAFE_DELETE( g_Playing ); + SAFE_DELETE( g_Mutex ); +} + +float GameSoundManager::GetFrameTimingAdjustment( float fDeltaTime ) +{ + /* + * We get one update per frame, and we're updated early, almost immediately after vsync, + * near the beginning of the game loop. However, it's very likely that we'll lose the + * scheduler while waiting for vsync, and some other thread will be working. Especially + * with a low-resolution scheduler (Linux 2.4, Win9x), we may not get the scheduler back + * immediately after the vsync; there may be up to a ~10ms delay. This can cause jitter + * in the rendered arrows. + * + * Compensate. If vsync is enabled, and we're maintaining the refresh rate consistently, + * we should have a very precise game loop interval. If we have that, but we're off by + * a small amount (less than the interval), adjust the time to line it up. As long as we + * adjust both the sound time and the timestamp, this won't adversely affect input timing. + * If we're off by more than that, we probably had a frame skip, in which case we have + * bigger skip problems, so don't adjust. + */ + static int iLastFPS = 0; + int iThisFPS = DISPLAY->GetFPS(); + + if( iThisFPS != DISPLAY->GetActualVideoModeParams().rate || iThisFPS != iLastFPS ) + { + iLastFPS = iThisFPS; + return 0; + } + + const float fExpectedDelay = 1.0f / iThisFPS; + const float fExtraDelay = fDeltaTime - fExpectedDelay; + if( fabsf(fExtraDelay) >= fExpectedDelay/2 ) + return 0; + + /* Subtract the extra delay. */ + return min( -fExtraDelay, 0 ); +} + +void GameSoundManager::Update( float fDeltaTime ) +{ + { + g_Mutex->Lock(); + if( g_Playing->m_bApplyMusicRate ) + { + RageSoundParams p = g_Playing->m_Music->GetParams(); + float fRate = GAMESTATE->m_SongOptions.GetPreferred().m_fMusicRate; + if( p.m_fSpeed != fRate ) + { + p.m_fSpeed = fRate; + g_Playing->m_Music->SetParams( p ); + } + } + + bool bIsPlaying = g_Playing->m_Music->IsPlaying(); + g_Mutex->Unlock(); + if( !bIsPlaying && g_bWasPlayingOnLastUpdate && !g_FallbackMusicParams.sFile.empty() ) + { + PlayMusic( g_FallbackMusicParams ); + + g_FallbackMusicParams.sFile = ""; + } + g_bWasPlayingOnLastUpdate = bIsPlaying; + } + + LockMut( *g_Mutex ); + + { + /* Duration of the fade-in and fade-out: */ + //const float fFadeInSpeed = 1.5f; + //const float fFadeOutSpeed = 0.3f; + float fFadeInSpeed = g_Playing->m_Music->GetParams().m_fFadeInSeconds; + float fFadeOutSpeed = g_Playing->m_Music->GetParams().m_fFadeOutSeconds; + float fVolume = g_Playing->m_Music->GetParams().m_Volume; + switch( g_FadeState ) + { + case FADE_NONE: break; + case FADE_OUT: + fapproach( fVolume, g_fDimVolume, fDeltaTime/fFadeOutSpeed ); + if( fabsf(fVolume-g_fDimVolume) < 0.001f ) + g_FadeState = FADE_WAIT; + break; + case FADE_WAIT: + g_fDimDurationRemaining -= fDeltaTime; + if( g_fDimDurationRemaining <= 0 ) + g_FadeState = FADE_IN; + break; + case FADE_IN: + fapproach( fVolume, g_fOriginalVolume, fDeltaTime/fFadeInSpeed ); + if( fabsf(fVolume-g_fOriginalVolume) < 0.001f ) + g_FadeState = FADE_NONE; + break; + } + + RageSoundParams p = g_Playing->m_Music->GetParams(); + if( p.m_Volume != fVolume ) + { + p.m_Volume = fVolume; + g_Playing->m_Music->SetParams( p ); + } + } + + if( !g_UpdatingTimer ) + return; + + const float fAdjust = GetFrameTimingAdjustment( fDeltaTime ); + if( !g_Playing->m_Music->IsPlaying() ) + { + /* There's no song playing. Fake it. */ + CHECKPOINT_M( ssprintf("%f, delta %f", GAMESTATE->m_Position.m_fMusicSeconds, fDeltaTime) ); + GAMESTATE->UpdateSongPosition( GAMESTATE->m_Position.m_fMusicSeconds + fDeltaTime, g_Playing->m_Timing ); + return; + } + + /* There's a delay between us calling Play() and the sound actually playing. + * During this time, m_bApproximate will be true. Keep using the previous timing + * data until we get a non-approximate time, indicating that the sound has actually + * started playing. */ + bool m_bApproximate; + RageTimer tm; + const float fSeconds = g_Playing->m_Music->GetPositionSeconds( &m_bApproximate, &tm ); + + // + // Check for song timing skips. + // + if( PREFSMAN->m_bLogSkips && !g_Playing->m_bTimingDelayed ) + { + const float fExpectedTimePassed = (tm - GAMESTATE->m_Position.m_LastBeatUpdate) * g_Playing->m_Music->GetPlaybackRate(); + const float fSoundTimePassed = fSeconds - GAMESTATE->m_Position.m_fMusicSeconds; + const float fDiff = fExpectedTimePassed - fSoundTimePassed; + + static RString sLastFile = ""; + const RString ThisFile = g_Playing->m_Music->GetLoadedFilePath(); + + /* If fSoundTimePassed < 0, the sound has probably looped. */ + if( sLastFile == ThisFile && fSoundTimePassed >= 0 && fabsf(fDiff) > 0.003f ) + LOG->Trace("Song position skip in %s: expected %.3f, got %.3f (cur %f, prev %f) (%.3f difference)", + Basename(ThisFile).c_str(), fExpectedTimePassed, fSoundTimePassed, fSeconds, GAMESTATE->m_Position.m_fMusicSeconds, fDiff ); + sLastFile = ThisFile; + } + + // + // If g_Playing->m_bTimingDelayed, we're waiting for the new music to actually start + // playing. + // + if( g_Playing->m_bTimingDelayed && !m_bApproximate ) + { + /* Load up the new timing data. */ + g_Playing->m_Timing = g_Playing->m_NewTiming; + g_Playing->m_bTimingDelayed = false; + } + + if( g_Playing->m_bTimingDelayed ) + { + /* We're still waiting for the new sound to start playing, so keep using the + * old timing data and fake the time. */ + GAMESTATE->UpdateSongPosition( GAMESTATE->m_Position.m_fMusicSeconds + fDeltaTime, g_Playing->m_Timing ); + } + else + { + GAMESTATE->UpdateSongPosition( fSeconds + fAdjust, g_Playing->m_Timing, tm + fAdjust ); + } + + + // + // Send crossed messages + // + if( GAMESTATE->m_pCurSong ) + { + static int iBeatLastCrossed = 0; + + float fSongBeat = GAMESTATE->m_Position.m_fSongBeat; + + int iRowNow = BeatToNoteRowNotRounded( fSongBeat ); + iRowNow = max( 0, iRowNow ); + + int iBeatNow = iRowNow / ROWS_PER_BEAT; + + for( int iBeat = iBeatLastCrossed+1; iBeat<=iBeatNow; ++iBeat ) + { + Message msg("CrossedBeat"); + msg.SetParam( "Beat", iBeat ); + MESSAGEMAN->Broadcast( msg ); + } + + iBeatLastCrossed = iBeatNow; + } + + + // + // Update lights + // + NoteData &lights = g_Playing->m_Lights; + if( lights.GetNumTracks() > 0 ) // lights data was loaded + { + const float fSongBeat = GAMESTATE->m_Position.m_fLightSongBeat; + const int iSongRow = BeatToNoteRowNotRounded( fSongBeat ); + + static int iRowLastCrossed = 0; + + FOREACH_CabinetLight( cl ) + { + // Are we "holding" the light? + if( lights.IsHoldNoteAtRow( cl, iSongRow ) ) + { + LIGHTSMAN->BlinkCabinetLight( cl ); + continue; + } + + // Otherwise, for each index we crossed since the last update: + FOREACH_NONEMPTY_ROW_IN_TRACK_RANGE( lights, cl, r, iRowLastCrossed+1, iSongRow+1 ) + { + if( lights.GetTapNote( cl, r ).type != TapNote::empty ) + { + LIGHTSMAN->BlinkCabinetLight( cl ); + break; + } + } + } + + iRowLastCrossed = iSongRow; + } +} + + +RString GameSoundManager::GetMusicPath() const +{ + LockMut( *g_Mutex ); + return g_Playing->m_Music->GetLoadedFilePath(); +} + +void GameSoundManager::PlayMusic( + RString sFile, + const TimingData *pTiming, + bool bForceLoop, + float fStartSecond, + float fLengthSeconds, + float fFadeInLengthSeconds, + float fFadeOutLengthSeconds, + bool bAlignBeat + ) +{ + PlayMusicParams params; + params.sFile = sFile; + params.pTiming = pTiming; + params.bForceLoop = bForceLoop; + params.fStartSecond = fStartSecond; + params.fLengthSeconds = fLengthSeconds; + params.fFadeInLengthSeconds = fFadeInLengthSeconds; + params.fFadeOutLengthSeconds = fFadeOutLengthSeconds; + params.bAlignBeat = bAlignBeat; + params.bApplyMusicRate = false; + PlayMusic( params ); +} + +void GameSoundManager::PlayMusic( PlayMusicParams params, PlayMusicParams FallbackMusicParams ) +{ + g_FallbackMusicParams = FallbackMusicParams; + + // LOG->Trace("play '%s' (current '%s')", file.c_str(), g_Playing->m_Music->GetLoadedFilePath().c_str()); + + MusicToPlay ToPlay; + + ToPlay.m_sFile = params.sFile; + if( params.pTiming ) + { + ToPlay.HasTiming = true; + ToPlay.m_TimingData = *params.pTiming; + } + else + { + /* If no timing data was provided, look for it in the same place as the music file. */ + // todo: allow loading .ssc files as well -aj + ToPlay.m_sTimingFile = SetExtension( params.sFile, "sm" ); + } + + ToPlay.bForceLoop = params.bForceLoop; + ToPlay.fStartSecond = params.fStartSecond; + ToPlay.fLengthSeconds = params.fLengthSeconds; + ToPlay.fFadeInLengthSeconds = params.fFadeInLengthSeconds; + ToPlay.fFadeOutLengthSeconds = params.fFadeOutLengthSeconds; + ToPlay.bAlignBeat = params.bAlignBeat; + ToPlay.bApplyMusicRate = params.bApplyMusicRate; + + /* Add the MusicToPlay to the g_MusicsToPlay queue. */ + g_Mutex->Lock(); + g_MusicsToPlay.push_back( ToPlay ); + g_Mutex->Broadcast(); + g_Mutex->Unlock(); +} + +void GameSoundManager::DimMusic( float fVolume, float fDurationSeconds ) +{ + LockMut( *g_Mutex ); + + if( g_FadeState == FADE_NONE ) + g_fOriginalVolume = g_Playing->m_Music->GetParams().m_Volume; + // otherwise, g_fOriginalVolume is already set and m_Volume will be the + // current state, not the original state + + g_fDimDurationRemaining = fDurationSeconds; + g_fDimVolume = fVolume; + g_FadeState = FADE_OUT; +} + +void GameSoundManager::HandleSongTimer( bool on ) +{ + LockMut( *g_Mutex ); + g_UpdatingTimer = on; +} + +void GameSoundManager::PlayOnce( RString sPath ) +{ + /* Add the sound to the g_SoundsToPlayOnce queue. */ + g_Mutex->Lock(); + g_SoundsToPlayOnce.push_back( sPath ); + g_Mutex->Broadcast(); + g_Mutex->Unlock(); +} + +void GameSoundManager::PlayOnceFromDir( RString sPath ) +{ + /* Add the path to the g_SoundsToPlayOnceFromDir queue. */ + g_Mutex->Lock(); + g_SoundsToPlayOnceFromDir.push_back( sPath ); + g_Mutex->Broadcast(); + g_Mutex->Unlock(); +} + +void GameSoundManager::PlayOnceFromAnnouncer( RString sPath ) +{ + /* Add the path to the g_SoundsToPlayOnceFromAnnouncer queue. */ + g_Mutex->Lock(); + g_SoundsToPlayOnceFromAnnouncer.push_back( sPath ); + g_Mutex->Broadcast(); + g_Mutex->Unlock(); +} + +float GameSoundManager::GetPlayerBalance( PlayerNumber pn ) +{ + /* If two players are active, play sounds on each players' side. */ + if( GAMESTATE->GetNumPlayersEnabled() == 2 ) + return (pn == PLAYER_1)? -1.0f:1.0f; + else + return 0; +} + + +#include "LuaBinding.h" + +/** @brief Allow Lua to have access to the GameSoundManager. */ +class LunaGameSoundManager: public Luna +{ +public: + static int DimMusic( T* p, lua_State *L ) + { + float fVolume = FArg(1); + float fDurationSeconds = FArg(2); + p->DimMusic( fVolume, fDurationSeconds ); + return 0; + } + static int PlayOnce( T* p, lua_State *L ) + { + RString sPath = SArg(1); + p->PlayOnce( sPath ); + return 0; + } + static int PlayAnnouncer( T* p, lua_State *L ) + { + RString sPath = SArg(1); + p->PlayOnceFromAnnouncer( sPath ); + return 0; + } + static int GetPlayerBalance( T* p, lua_State *L ) + { + PlayerNumber pn = Enum::Check(L, 1); + lua_pushnumber( L, p->GetPlayerBalance(pn) ); + return 1; + } + static int PlayMusicPart( T* p, lua_State *L ) + { + RString musicPath = SArg(1); + float musicStart = FArg(2); + float musicLength = FArg(3); + float fadeIn = 0; + float fadeOut = 0; + if (lua_gettop(L) >= 4 && !lua_isnil(L,4)) + { + fadeIn = FArg(4); + if (lua_gettop(L) >= 5 && !lua_isnil(L,5)) + { + fadeOut = FArg(5); + } + } + p->PlayMusic(musicPath, NULL, false, musicStart, musicLength, + fadeIn, fadeOut); + return 0; + } + + LunaGameSoundManager() + { + ADD_METHOD( DimMusic ); + ADD_METHOD( PlayOnce ); + ADD_METHOD( PlayAnnouncer ); + ADD_METHOD( GetPlayerBalance ); + ADD_METHOD( PlayMusicPart ); + } +}; + +LUA_REGISTER_CLASS( GameSoundManager ) + + +/* + * Copyright (c) 2003-2005 Glenn Maynard + * 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. + */ + diff --git a/src/GameState.cpp b/src/GameState.cpp index 1a247ebd67..eb1c3bab34 100644 --- a/src/GameState.cpp +++ b/src/GameState.cpp @@ -275,7 +275,7 @@ void GameState::Reset() FOREACH_PlayerNumber( pn ) UnjoinPlayer( pn ); - ASSERT( THEME != NULL ); + ASSERT( THEME != nullptr ); m_timeGameStarted.SetZero(); SetCurrentStyle( NULL ); @@ -335,7 +335,7 @@ void GameState::Reset() m_pCurCharacters[p] = CHARMAN->GetRandomCharacter(); else m_pCurCharacters[p] = CHARMAN->GetDefaultCharacter(); - ASSERT( m_pCurCharacters[p] != NULL ); + ASSERT( m_pCurCharacters[p] != nullptr ); } m_bTemporaryEventMode = false; @@ -375,7 +375,7 @@ void GameState::JoinPlayer( PlayerNumber pn ) } // Set the current style to something appropriate for the new number of joined players. - if( ALLOW_LATE_JOIN && m_pCurStyle != NULL ) + if( ALLOW_LATE_JOIN && m_pCurStyle != nullptr ) { const Style *pStyle; // Only use one player for StyleType_OnePlayerTwoSides and StepsTypes @@ -592,7 +592,7 @@ int GameState::GetNumStagesMultiplierForSong( const Song* pSong ) { int iNumStages = 1; - ASSERT( pSong != NULL ); + ASSERT( pSong != nullptr ); if( pSong->IsMarathon() ) iNumStages *= 3; if( pSong->IsLong() ) @@ -1115,7 +1115,7 @@ int GameState::GetNumSidesJoined() const const Game* GameState::GetCurrentGame() { - ASSERT( m_pCurGame != NULL ); // the game must be set before calling this + ASSERT( m_pCurGame != nullptr ); // the game must be set before calling this return m_pCurGame; } @@ -1424,7 +1424,7 @@ void GameState::GetAllUsedNoteSkins( vector &out ) const if( IsCourseMode() ) { const Trail *pTrail = m_pCurTrail[pn]; - ASSERT( pTrail != NULL ); + ASSERT( pTrail != nullptr ); for (TrailEntry const &e : pTrail->m_vEntries) { @@ -1555,11 +1555,11 @@ void GameState::GetRankingFeats( PlayerNumber pn, vector &asFeatsOu SongAndSteps sas; ASSERT( !STATSMAN->m_vPlayedStageStats[i].m_vpPlayedSongs.empty() ); sas.pSong = STATSMAN->m_vPlayedStageStats[i].m_vpPlayedSongs[0]; - ASSERT( sas.pSong != NULL ); + ASSERT( sas.pSong != nullptr ); if( STATSMAN->m_vPlayedStageStats[i].m_player[pn].m_vpPossibleSteps.empty() ) continue; sas.pSteps = STATSMAN->m_vPlayedStageStats[i].m_player[pn].m_vpPossibleSteps[0]; - ASSERT( sas.pSteps != NULL ); + ASSERT( sas.pSteps != nullptr ); vSongAndSteps.push_back( sas ); } CHECKPOINT; @@ -1697,9 +1697,9 @@ void GameState::GetRankingFeats( PlayerNumber pn, vector &asFeatsOu { CHECKPOINT; Course* pCourse = m_pCurCourse; - ASSERT( pCourse != NULL ); + ASSERT( pCourse != nullptr ); Trail *pTrail = m_pCurTrail[pn]; - ASSERT( pTrail != NULL ); + ASSERT( pTrail != nullptr ); CourseDifficulty cd = pTrail->m_CourseDifficulty; // Find Machine Records diff --git a/src/IniFile.cpp b/src/IniFile.cpp index 8c4e6f5004..80f7b62244 100644 --- a/src/IniFile.cpp +++ b/src/IniFile.cpp @@ -165,7 +165,7 @@ bool IniFile::DeleteKey(const RString &keyname) bool IniFile::RenameKey(const RString &from, const RString &to) { // If to already exists, do nothing. - if( GetChild(to) != NULL ) + if( GetChild(to) != nullptr ) return false; XNode* pNode = GetChild( from ); diff --git a/src/InputFilter.cpp b/src/InputFilter.cpp index fdd0a78535..8cecfa13bb 100644 --- a/src/InputFilter.cpp +++ b/src/InputFilter.cpp @@ -347,7 +347,7 @@ bool InputFilter::IsBeingPressed( const DeviceInput &di, const DeviceInputList * if( pButtonState == NULL ) pButtonState = &g_CurrentState; const DeviceInput *pDI = FindItemBinarySearch( pButtonState->begin(), pButtonState->end(), di ); - return pDI != NULL && pDI->bDown; + return pDI != nullptr && pDI->bDown; } float InputFilter::GetSecsHeld( const DeviceInput &di, const DeviceInputList *pButtonState ) const diff --git a/src/InputMapper.cpp b/src/InputMapper.cpp index a0debb56ad..9e7f9c3059 100644 --- a/src/InputMapper.cpp +++ b/src/InputMapper.cpp @@ -1111,7 +1111,7 @@ void InputMappings::WriteMappings( const InputScheme *pInputScheme, RString sFil ini.DeleteKey( pInputScheme->m_szName ); XNode *pKey = ini.GetChild( pInputScheme->m_szName ); - if( pKey != NULL ) + if( pKey != nullptr ) ini.RemoveChild( pKey ); pKey = ini.AppendChild( pInputScheme->m_szName ); diff --git a/src/InputQueue.cpp b/src/InputQueue.cpp index e43b6574eb..d5b507c078 100644 --- a/src/InputQueue.cpp +++ b/src/InputQueue.cpp @@ -38,7 +38,7 @@ bool InputQueue::WasPressedRecently( GameController c, const GameButton button, if( iep.GameI.button != button ) continue; - if( pIEP != NULL ) + if( pIEP != nullptr ) *pIEP = iep; return true; diff --git a/src/LifeMeterTime.cpp b/src/LifeMeterTime.cpp index a113d26fae..2d96eb4eb8 100644 --- a/src/LifeMeterTime.cpp +++ b/src/LifeMeterTime.cpp @@ -96,7 +96,7 @@ void LifeMeterTime::OnLoadSong() return; Course* pCourse = GAMESTATE->m_pCurCourse; - ASSERT( pCourse != NULL ); + ASSERT( pCourse != nullptr ); float fOldLife = m_fLifeTotalLostSeconds; float fGainSeconds = pCourse->m_vEntries[GAMESTATE->GetCourseSongIndex()].fGainSeconds; diff --git a/src/LuaManager.cpp b/src/LuaManager.cpp index 98ffeaf7c6..ace157b413 100644 --- a/src/LuaManager.cpp +++ b/src/LuaManager.cpp @@ -86,12 +86,12 @@ namespace LuaHelpers { size_t iLen; const char *pStr = lua_tolstring( L, iOffset, &iLen ); - if( pStr != NULL ) + if( pStr != nullptr ) Object.assign( pStr, iLen ); else Object.clear(); - return pStr != NULL; + return pStr != nullptr; } } @@ -174,7 +174,7 @@ static int GetLuaStack( lua_State *L ) if( !strcmp(ar.what, "C") ) { - for( int i = 1; i <= ar.nups && (name = lua_getupvalue(L, -1, i)) != NULL; ++i ) + for( int i = 1; i <= ar.nups && (name = lua_getupvalue(L, -1, i)) != nullptr; ++i ) { vArgs.push_back( ssprintf("%s = %s", name, lua_tostring(L, -1)) ); lua_pop( L, 1 ); // pop value @@ -182,7 +182,7 @@ static int GetLuaStack( lua_State *L ) } else { - for( int i = 1; (name = lua_getlocal(L, &ar, i)) != NULL; ++i ) + for( int i = 1; (name = lua_getlocal(L, &ar, i)) != nullptr; ++i ) { vArgs.push_back( ssprintf("%s = %s", name, lua_tostring(L, -1)) ); lua_pop( L, 1 ); // pop value @@ -243,7 +243,7 @@ LuaManager::LuaManager() LUA = this; // so that LUA is available when we call the Register functions lua_State *L = lua_open(); - ASSERT( L != NULL ); + ASSERT( L != nullptr ); lua_atpanic( L, LuaPanic ); m_pLuaMain = L; diff --git a/src/LuaReference.cpp b/src/LuaReference.cpp index 537337ca09..ac12bd60a7 100644 --- a/src/LuaReference.cpp +++ b/src/LuaReference.cpp @@ -1,236 +1,236 @@ -#include "global.h" -#include "LuaReference.h" - -REGISTER_CLASS_TRAITS( LuaReference, new LuaReference(*pCopy) ) - -LuaReference::LuaReference() -{ - m_iReference = LUA_NOREF; -} - -LuaReference::~LuaReference() -{ - Unregister(); -} - -LuaReference::LuaReference( const LuaReference &cpy ) -{ - if( cpy.m_iReference == LUA_NOREF || cpy.m_iReference == LUA_REFNIL ) - { - m_iReference = cpy.m_iReference; - } - else - { - /* Make a new reference. */ - Lua *L = LUA->Get(); - lua_rawgeti( L, LUA_REGISTRYINDEX, cpy.m_iReference ); - m_iReference = luaL_ref( L, LUA_REGISTRYINDEX ); - LUA->Release( L ); - } -} - -LuaReference &LuaReference::operator=( const LuaReference &cpy ) -{ - if( this == &cpy ) - return *this; - - Unregister(); - - if( cpy.m_iReference == LUA_NOREF || cpy.m_iReference == LUA_REFNIL ) - { - m_iReference = cpy.m_iReference; - } - else - { - /* Make a new reference. */ - Lua *L = LUA->Get(); - lua_rawgeti( L, LUA_REGISTRYINDEX, cpy.m_iReference ); - m_iReference = luaL_ref( L, LUA_REGISTRYINDEX ); - LUA->Release( L ); - } - - return *this; -} - -void LuaReference::SetFromStack( Lua *L ) -{ - if( m_iReference != LUA_NOREF ) - luaL_unref( L, LUA_REGISTRYINDEX, m_iReference ); - m_iReference = luaL_ref( L, LUA_REGISTRYINDEX ); -} - -void LuaReference::SetFromNil() -{ - Unregister(); - m_iReference = LUA_REFNIL; -} - -void LuaReference::DeepCopy() -{ - /* Call DeepCopy(t, u), where t is our referenced object and u is the new table. */ - Lua *L = LUA->Get(); - - /* Arg 1 (t): */ - this->PushSelf( L ); - - /* Arg 2 (u): */ - lua_newtable( L ); - - lua_pushvalue( L, -1 ); - this->SetFromStack( L ); - - LuaHelpers::DeepCopy( L ); - - LUA->Release( L ); -} - -void LuaReference::PushSelf( lua_State *L ) const -{ - lua_rawgeti( L, LUA_REGISTRYINDEX, m_iReference ); -} - -bool LuaReference::IsSet() const -{ - return m_iReference != LUA_NOREF; -} - -bool LuaReference::IsNil() const -{ - return m_iReference == LUA_REFNIL; -} - -int LuaReference::GetLuaType() const -{ - Lua *L = LUA->Get(); - this->PushSelf( L ); - int iRet = lua_type( L, -1 ); - lua_pop( L, 1 ); - LUA->Release( L ); - - return iRet; -} - -void LuaReference::Unregister() -{ - if( LUA == NULL || m_iReference == LUA_NOREF ) - return; // nothing to do - - Lua *L = LUA->Get(); - luaL_unref( L, LUA_REGISTRYINDEX, m_iReference ); - LUA->Release( L ); - m_iReference = LUA_NOREF; -} - -bool LuaReference::SetFromExpression( const RString &sExpression ) -{ - Lua *L = LUA->Get(); - - bool bSuccess = LuaHelpers::RunExpression( L, sExpression ); - this->SetFromStack( L ); - - LUA->Release( L ); - return bSuccess; -} - -RString LuaReference::Serialize() const -{ - /* Call Serialize(t), where t is our referenced object. */ - Lua *L = LUA->Get(); - lua_getglobal( L, "Serialize" ); - - ASSERT_M( !lua_isnil(L, -1), "Serialize() missing" ); - ASSERT_M( lua_isfunction(L, -1), "Serialize() not a function" ); - - /* Arg 1 (t): */ - this->PushSelf( L ); - - lua_call( L, 1, 1 ); - - /* The return value is a string, which we store in m_sSerializedData. */ - const char *pString = lua_tostring( L, -1 ); - ASSERT_M( pString != NULL, "Serialize() didn't return a string" ); - - RString sRet = pString; - lua_pop( L, 1 ); - - LUA->Release( L ); - - return sRet; -} - -/** @brief Utilities for working with Lua. */ -namespace LuaHelpers -{ - template<> bool FromStack( lua_State *L, LuaReference &Object, int iOffset ) - { - lua_pushvalue( L, iOffset ); - Object.SetFromStack( L ); - return true; - } - - template<> bool FromStack( lua_State *L, apActorCommands &Object, int iOffset ) - { - LuaReference *pRef = new LuaReference; - FromStack( L, *pRef, iOffset ); - Object = apActorCommands( pRef ); - return true; - } -} - -LuaTable::LuaTable() -{ - Lua *L = LUA->Get(); - lua_newtable( L ); - this->SetFromStack(L); - LUA->Release( L ); -} - -void LuaTable::Set( Lua *L, const RString &sKey ) -{ - int iTop = lua_gettop( L ); - this->PushSelf( L ); - lua_pushvalue( L, iTop ); // push the value - lua_setfield( L, -2, sKey ); - lua_settop( L, iTop-1 ); // remove all of the above -} - -void LuaTable::Get( Lua *L, const RString &sKey ) -{ - this->PushSelf( L ); - lua_getfield( L, -1, sKey ); - lua_remove( L, -2 ); // remove self -} - -/** @brief Utilities for working with Lua. */ -namespace LuaHelpers -{ - template<> void Push( lua_State *L, const LuaReference &Object ) - { - Object.PushSelf( L ); - } -} - -/* - * (c) 2005 Glenn Maynard, 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. - */ +#include "global.h" +#include "LuaReference.h" + +REGISTER_CLASS_TRAITS( LuaReference, new LuaReference(*pCopy) ) + +LuaReference::LuaReference() +{ + m_iReference = LUA_NOREF; +} + +LuaReference::~LuaReference() +{ + Unregister(); +} + +LuaReference::LuaReference( const LuaReference &cpy ) +{ + if( cpy.m_iReference == LUA_NOREF || cpy.m_iReference == LUA_REFNIL ) + { + m_iReference = cpy.m_iReference; + } + else + { + /* Make a new reference. */ + Lua *L = LUA->Get(); + lua_rawgeti( L, LUA_REGISTRYINDEX, cpy.m_iReference ); + m_iReference = luaL_ref( L, LUA_REGISTRYINDEX ); + LUA->Release( L ); + } +} + +LuaReference &LuaReference::operator=( const LuaReference &cpy ) +{ + if( this == &cpy ) + return *this; + + Unregister(); + + if( cpy.m_iReference == LUA_NOREF || cpy.m_iReference == LUA_REFNIL ) + { + m_iReference = cpy.m_iReference; + } + else + { + /* Make a new reference. */ + Lua *L = LUA->Get(); + lua_rawgeti( L, LUA_REGISTRYINDEX, cpy.m_iReference ); + m_iReference = luaL_ref( L, LUA_REGISTRYINDEX ); + LUA->Release( L ); + } + + return *this; +} + +void LuaReference::SetFromStack( Lua *L ) +{ + if( m_iReference != LUA_NOREF ) + luaL_unref( L, LUA_REGISTRYINDEX, m_iReference ); + m_iReference = luaL_ref( L, LUA_REGISTRYINDEX ); +} + +void LuaReference::SetFromNil() +{ + Unregister(); + m_iReference = LUA_REFNIL; +} + +void LuaReference::DeepCopy() +{ + /* Call DeepCopy(t, u), where t is our referenced object and u is the new table. */ + Lua *L = LUA->Get(); + + /* Arg 1 (t): */ + this->PushSelf( L ); + + /* Arg 2 (u): */ + lua_newtable( L ); + + lua_pushvalue( L, -1 ); + this->SetFromStack( L ); + + LuaHelpers::DeepCopy( L ); + + LUA->Release( L ); +} + +void LuaReference::PushSelf( lua_State *L ) const +{ + lua_rawgeti( L, LUA_REGISTRYINDEX, m_iReference ); +} + +bool LuaReference::IsSet() const +{ + return m_iReference != LUA_NOREF; +} + +bool LuaReference::IsNil() const +{ + return m_iReference == LUA_REFNIL; +} + +int LuaReference::GetLuaType() const +{ + Lua *L = LUA->Get(); + this->PushSelf( L ); + int iRet = lua_type( L, -1 ); + lua_pop( L, 1 ); + LUA->Release( L ); + + return iRet; +} + +void LuaReference::Unregister() +{ + if( LUA == NULL || m_iReference == LUA_NOREF ) + return; // nothing to do + + Lua *L = LUA->Get(); + luaL_unref( L, LUA_REGISTRYINDEX, m_iReference ); + LUA->Release( L ); + m_iReference = LUA_NOREF; +} + +bool LuaReference::SetFromExpression( const RString &sExpression ) +{ + Lua *L = LUA->Get(); + + bool bSuccess = LuaHelpers::RunExpression( L, sExpression ); + this->SetFromStack( L ); + + LUA->Release( L ); + return bSuccess; +} + +RString LuaReference::Serialize() const +{ + /* Call Serialize(t), where t is our referenced object. */ + Lua *L = LUA->Get(); + lua_getglobal( L, "Serialize" ); + + ASSERT_M( !lua_isnil(L, -1), "Serialize() missing" ); + ASSERT_M( lua_isfunction(L, -1), "Serialize() not a function" ); + + /* Arg 1 (t): */ + this->PushSelf( L ); + + lua_call( L, 1, 1 ); + + /* The return value is a string, which we store in m_sSerializedData. */ + const char *pString = lua_tostring( L, -1 ); + ASSERT_M( pString != nullptr, "Serialize() didn't return a string" ); + + RString sRet = pString; + lua_pop( L, 1 ); + + LUA->Release( L ); + + return sRet; +} + +/** @brief Utilities for working with Lua. */ +namespace LuaHelpers +{ + template<> bool FromStack( lua_State *L, LuaReference &Object, int iOffset ) + { + lua_pushvalue( L, iOffset ); + Object.SetFromStack( L ); + return true; + } + + template<> bool FromStack( lua_State *L, apActorCommands &Object, int iOffset ) + { + LuaReference *pRef = new LuaReference; + FromStack( L, *pRef, iOffset ); + Object = apActorCommands( pRef ); + return true; + } +} + +LuaTable::LuaTable() +{ + Lua *L = LUA->Get(); + lua_newtable( L ); + this->SetFromStack(L); + LUA->Release( L ); +} + +void LuaTable::Set( Lua *L, const RString &sKey ) +{ + int iTop = lua_gettop( L ); + this->PushSelf( L ); + lua_pushvalue( L, iTop ); // push the value + lua_setfield( L, -2, sKey ); + lua_settop( L, iTop-1 ); // remove all of the above +} + +void LuaTable::Get( Lua *L, const RString &sKey ) +{ + this->PushSelf( L ); + lua_getfield( L, -1, sKey ); + lua_remove( L, -2 ); // remove self +} + +/** @brief Utilities for working with Lua. */ +namespace LuaHelpers +{ + template<> void Push( lua_State *L, const LuaReference &Object ) + { + Object.PushSelf( L ); + } +} + +/* + * (c) 2005 Glenn Maynard, 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. + */ diff --git a/src/MemoryCardManager.cpp b/src/MemoryCardManager.cpp index b80f163385..41d4b17074 100644 --- a/src/MemoryCardManager.cpp +++ b/src/MemoryCardManager.cpp @@ -298,7 +298,7 @@ MemoryCardManager::~MemoryCardManager() // Unregister with Lua. LUA->UnsetGlobal( "MEMCARDMAN" ); - ASSERT( g_pWorker != NULL ); + ASSERT( g_pWorker != nullptr ); SAFE_DELETE(g_pWorker); FOREACH_PlayerNumber( pn ) diff --git a/src/MeterDisplay.cpp b/src/MeterDisplay.cpp index cd8c2faebb..4ef8a6188b 100644 --- a/src/MeterDisplay.cpp +++ b/src/MeterDisplay.cpp @@ -1,127 +1,127 @@ -#include "global.h" -#include "MeterDisplay.h" -#include "RageUtil.h" -#include "GameState.h" -#include "Song.h" -#include "ActorUtil.h" -#include "XmlFile.h" -#include "RageLog.h" -#include "LuaManager.h" - -REGISTER_ACTOR_CLASS(MeterDisplay); -REGISTER_ACTOR_CLASS(SongMeterDisplay); - -MeterDisplay::MeterDisplay() -{ -} - -void MeterDisplay::Load( RString sStreamPath, float fStreamWidth, RString sTipPath ) -{ - m_sprStream.Load( sStreamPath ); - this->AddChild( m_sprStream ); - - m_sprTip.Load( sTipPath ); - this->AddChild( m_sprTip ); - - SetStreamWidth( fStreamWidth ); - SetPercent( 0.5f ); -} - -void MeterDisplay::LoadFromNode( const XNode* pNode ) -{ - LOG->Trace( "MeterDisplay::LoadFromNode(%s)", ActorUtil::GetWhere(pNode).c_str() ); - - const XNode *pStream = pNode->GetChild( "Stream" ); - if( pStream == NULL ) - RageException::Throw( "%s: MeterDisplay: missing the \"Stream\" attribute", ActorUtil::GetWhere(pNode).c_str() ); - m_sprStream.LoadActorFromNode( pStream, this ); - this->AddChild( m_sprStream ); - - const XNode* pChild = pNode->GetChild( "Tip" ); - if( pChild != NULL ) - { - m_sprTip.LoadActorFromNode( pChild, this ); - this->AddChild( m_sprTip ); - } - - float fStreamWidth = 0; - pNode->GetAttrValue( "StreamWidth", fStreamWidth ); - SetStreamWidth( fStreamWidth ); - - SetPercent( 0.5f ); - - ActorFrame::LoadFromNode( pNode ); -} - -void MeterDisplay::SetPercent( float fPercent ) -{ - ASSERT( fPercent >= 0 && fPercent <= 1 ); - - m_sprStream->SetCropRight( 1-fPercent ); - - if( m_sprTip.IsLoaded() ) - m_sprTip->SetX( SCALE(fPercent, 0.f, 1.f, -m_fStreamWidth/2, m_fStreamWidth/2) ); -} - -void MeterDisplay::SetStreamWidth( float fStreamWidth ) -{ - m_fStreamWidth = fStreamWidth; - m_sprStream->SetZoomX( m_fStreamWidth / m_sprStream->GetUnzoomedWidth() ); -} - -void SongMeterDisplay::Update( float fDeltaTime ) -{ - if( GAMESTATE->m_pCurSong ) - { - float fSongStartSeconds = GAMESTATE->m_pCurSong->GetFirstSecond(); - float fSongEndSeconds = GAMESTATE->m_pCurSong->GetLastSecond(); - float fPercentPositionSong = SCALE( GAMESTATE->m_Position.m_fMusicSeconds, fSongStartSeconds, fSongEndSeconds, 0.0f, 1.0f ); - CLAMP( fPercentPositionSong, 0, 1 ); - - SetPercent( fPercentPositionSong ); - } - - MeterDisplay::Update( fDeltaTime ); -} - -// lua start -#include "LuaBinding.h" - -class LunaMeterDisplay: public Luna -{ -public: - static int SetStreamWidth( T* p, lua_State *L ) { p->SetStreamWidth(FArg(1)); return 0; } - - LunaMeterDisplay() - { - ADD_METHOD( SetStreamWidth ); - } -}; - -LUA_REGISTER_DERIVED_CLASS( MeterDisplay, ActorFrame ) -// lua end - -/* - * (c) 2003-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. - */ +#include "global.h" +#include "MeterDisplay.h" +#include "RageUtil.h" +#include "GameState.h" +#include "Song.h" +#include "ActorUtil.h" +#include "XmlFile.h" +#include "RageLog.h" +#include "LuaManager.h" + +REGISTER_ACTOR_CLASS(MeterDisplay); +REGISTER_ACTOR_CLASS(SongMeterDisplay); + +MeterDisplay::MeterDisplay() +{ +} + +void MeterDisplay::Load( RString sStreamPath, float fStreamWidth, RString sTipPath ) +{ + m_sprStream.Load( sStreamPath ); + this->AddChild( m_sprStream ); + + m_sprTip.Load( sTipPath ); + this->AddChild( m_sprTip ); + + SetStreamWidth( fStreamWidth ); + SetPercent( 0.5f ); +} + +void MeterDisplay::LoadFromNode( const XNode* pNode ) +{ + LOG->Trace( "MeterDisplay::LoadFromNode(%s)", ActorUtil::GetWhere(pNode).c_str() ); + + const XNode *pStream = pNode->GetChild( "Stream" ); + if( pStream == NULL ) + RageException::Throw( "%s: MeterDisplay: missing the \"Stream\" attribute", ActorUtil::GetWhere(pNode).c_str() ); + m_sprStream.LoadActorFromNode( pStream, this ); + this->AddChild( m_sprStream ); + + const XNode* pChild = pNode->GetChild( "Tip" ); + if( pChild != nullptr ) + { + m_sprTip.LoadActorFromNode( pChild, this ); + this->AddChild( m_sprTip ); + } + + float fStreamWidth = 0; + pNode->GetAttrValue( "StreamWidth", fStreamWidth ); + SetStreamWidth( fStreamWidth ); + + SetPercent( 0.5f ); + + ActorFrame::LoadFromNode( pNode ); +} + +void MeterDisplay::SetPercent( float fPercent ) +{ + ASSERT( fPercent >= 0 && fPercent <= 1 ); + + m_sprStream->SetCropRight( 1-fPercent ); + + if( m_sprTip.IsLoaded() ) + m_sprTip->SetX( SCALE(fPercent, 0.f, 1.f, -m_fStreamWidth/2, m_fStreamWidth/2) ); +} + +void MeterDisplay::SetStreamWidth( float fStreamWidth ) +{ + m_fStreamWidth = fStreamWidth; + m_sprStream->SetZoomX( m_fStreamWidth / m_sprStream->GetUnzoomedWidth() ); +} + +void SongMeterDisplay::Update( float fDeltaTime ) +{ + if( GAMESTATE->m_pCurSong ) + { + float fSongStartSeconds = GAMESTATE->m_pCurSong->GetFirstSecond(); + float fSongEndSeconds = GAMESTATE->m_pCurSong->GetLastSecond(); + float fPercentPositionSong = SCALE( GAMESTATE->m_Position.m_fMusicSeconds, fSongStartSeconds, fSongEndSeconds, 0.0f, 1.0f ); + CLAMP( fPercentPositionSong, 0, 1 ); + + SetPercent( fPercentPositionSong ); + } + + MeterDisplay::Update( fDeltaTime ); +} + +// lua start +#include "LuaBinding.h" + +class LunaMeterDisplay: public Luna +{ +public: + static int SetStreamWidth( T* p, lua_State *L ) { p->SetStreamWidth(FArg(1)); return 0; } + + LunaMeterDisplay() + { + ADD_METHOD( SetStreamWidth ); + } +}; + +LUA_REGISTER_DERIVED_CLASS( MeterDisplay, ActorFrame ) +// lua end + +/* + * (c) 2003-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. + */ diff --git a/src/Model.cpp b/src/Model.cpp index 71398bcdfc..d73b1e91fb 100644 --- a/src/Model.cpp +++ b/src/Model.cpp @@ -620,7 +620,7 @@ void Model::SetBones( const msAnimation* pAnimation, float fFrame, vectorfTime, pThisPositionKey->fTime, 0, 1 ); vPos = pLastPositionKey->Position + (pThisPositionKey->Position - pLastPositionKey->Position) * s; @@ -644,7 +644,7 @@ void Model::SetBones( const msAnimation* pAnimation, float fFrame, vectorfTime, pThisRotationKey->fTime, 0, 1 ); RageQuatSlerp( &vRot, pLastRotationKey->Rotation, pThisRotationKey->Rotation, s ); diff --git a/src/MusicWheel.cpp b/src/MusicWheel.cpp index e1b79ab77b..1c336ab9ec 100644 --- a/src/MusicWheel.cpp +++ b/src/MusicWheel.cpp @@ -151,7 +151,7 @@ void MusicWheel::BeginScreen() const vector &from = getWheelItemsData(SORT_MODE_MENU); for( unsigned i=0; im_pAction != NULL ); + ASSERT( &*from[i]->m_pAction != nullptr ); if( from[i]->m_pAction->DescribesCurrentModeForAllPlayers() ) { m_sLastModeMenuItem = from[i]->m_pAction->m_sName; @@ -184,7 +184,7 @@ void MusicWheel::BeginScreen() /* Invalidate current Song if it can't be played * because there are not enough stages remaining. */ - if( GAMESTATE->m_pCurSong != NULL && + if( GAMESTATE->m_pCurSong != nullptr && GameState::GetNumStagesMultiplierForSong( GAMESTATE->m_pCurSong ) > GAMESTATE->GetSmallestNumStagesLeftForAnyHumanPlayer() ) { GAMESTATE->m_pCurSong.Set( NULL ); @@ -194,10 +194,10 @@ void MusicWheel::BeginScreen() * because there are not enough stages remaining. */ FOREACH_ENUM( PlayerNumber, p ) { - if( GAMESTATE->m_pCurSteps[p] != NULL ) + if( GAMESTATE->m_pCurSteps[p] != nullptr ) { vector vpPossibleSteps; - if( GAMESTATE->m_pCurSong != NULL ) + if( GAMESTATE->m_pCurSong != nullptr ) SongUtil::GetPlayableSteps( GAMESTATE->m_pCurSong, vpPossibleSteps ); bool bStepsIsPossible = find( vpPossibleSteps.begin(), vpPossibleSteps.end(), GAMESTATE->m_pCurSteps[p] ) == vpPossibleSteps.end(); if( !bStepsIsPossible ) @@ -849,7 +849,7 @@ void MusicWheel::BuildWheelItemDatas( vector &arrayWheelIt // init music status icons for (MusicWheelItemData *WID : arrayWheelItemDatas) { - if( WID->m_pSong != NULL ) + if( WID->m_pSong != nullptr ) { WID->m_Flags.bHasBeginnerOr1Meter = WID->m_pSong->IsEasy( GAMESTATE->GetCurrentStyle()->m_StepsType ) && SHOW_EASY_FLAG; WID->m_Flags.bEdits = false; @@ -859,7 +859,7 @@ void MusicWheel::BuildWheelItemDatas( vector &arrayWheelIt WID->m_Flags.bEdits |= WID->m_pSong->HasEdits( type ); WID->m_Flags.iStagesForSong = GameState::GetNumStagesMultiplierForSong( WID->m_pSong ); } - else if( WID->m_pCourse != NULL ) + else if( WID->m_pCourse != nullptr ) { WID->m_Flags.bHasBeginnerOr1Meter = false; WID->m_Flags.bEdits = WID->m_pCourse->IsAnEdit(); diff --git a/src/MusicWheelItem.cpp b/src/MusicWheelItem.cpp index c179a79a54..ddc53a49e7 100644 --- a/src/MusicWheelItem.cpp +++ b/src/MusicWheelItem.cpp @@ -1,414 +1,414 @@ -#include "global.h" -#include "MusicWheelItem.h" -#include "RageUtil.h" -#include "SongManager.h" -#include "GameManager.h" -#include "RageLog.h" -#include "GameConstantsAndTypes.h" -#include "GameState.h" -#include "ThemeManager.h" -#include "Steps.h" -#include "Song.h" -#include "Course.h" -#include "ProfileManager.h" -#include "Profile.h" -#include "Style.h" -#include "ActorUtil.h" -#include "ThemeMetric.h" -#include "HighScore.h" -#include "ScreenSelectMusic.h" -#include "ScreenManager.h" - -static const char *MusicWheelItemTypeNames[] = { - "Song", - "SectionExpanded", - "SectionCollapsed", - "Roulette", - "Course", - "Sort", - "Mode", - "Random", - "Portal", - "Custom", -}; -XToString( MusicWheelItemType ); - -MusicWheelItemData::MusicWheelItemData( WheelItemDataType type, Song* pSong, - RString sSectionName, Course* pCourse, - RageColor color, int iSectionCount ): - WheelItemBaseData(type, sSectionName, color), - m_pCourse(pCourse), m_pSong(pSong), m_Flags(WheelNotifyIcon::Flags()), - m_iSectionCount(iSectionCount), m_sLabel(""), m_pAction() {} - -MusicWheelItem::MusicWheelItem( RString sType ): - WheelItemBase( sType ) -{ - GRADES_SHOW_MACHINE.Load( sType, "GradesShowMachine" ); - - FOREACH_ENUM( MusicWheelItemType, i ) - { - m_sprColorPart[i].Load( THEME->GetPathG(sType,MusicWheelItemTypeToString(i)+" ColorPart") ); - m_sprColorPart[i]->SetName( MusicWheelItemTypeToString(i)+"ColorPart" ); - ActorUtil::LoadAllCommands(m_sprColorPart[i],"MusicWheelItem"); - this->AddChild( m_sprColorPart[i] ); - - m_sprNormalPart[i].Load( THEME->GetPathG(sType,MusicWheelItemTypeToString(i)+" NormalPart") ); - m_sprNormalPart[i]->SetName( MusicWheelItemTypeToString(i)+"NormalPart" ); - ActorUtil::LoadAllCommands(m_sprNormalPart[i],"MusicWheelItem"); - this->AddChild( m_sprNormalPart[i] ); - } - - m_TextBanner.SetName( "SongName" ); - ActorUtil::LoadAllCommands( m_TextBanner, "MusicWheelItem" ); - m_TextBanner.Load( "TextBanner" ); - ActorUtil::SetXY( m_TextBanner, "MusicWheelItem" ); - m_TextBanner.PlayCommand( "On" ); - this->AddChild( &m_TextBanner ); - - FOREACH_ENUM( MusicWheelItemType, i ) - { - m_sprOverPart[i].Load( THEME->GetPathG(sType,MusicWheelItemTypeToString(i)+" OverPart") ); - m_sprOverPart[i]->SetName( MusicWheelItemTypeToString(i)+"OverPart" ); - ActorUtil::LoadAllCommands(m_sprOverPart[i],"MusicWheelItem"); - this->AddChild( m_sprOverPart[i] ); - } - - FOREACH_ENUM( MusicWheelItemType, i ) - { - m_pText[i] = NULL; - - // Don't init text for Type_Song. It uses a TextBanner. - if( i == MusicWheelItemType_Song ) - continue; - - m_pText[i] = new BitmapText; - m_pText[i]->SetName( MusicWheelItemTypeToString(i) ); - ActorUtil::LoadAllCommands( m_pText[i], "MusicWheelItem" ); - m_pText[i]->LoadFromFont( THEME->GetPathF(sType,MusicWheelItemTypeToString(i)) ); - ActorUtil::SetXY( m_pText[i], "MusicWheelItem" ); - m_pText[i]->PlayCommand( "On" ); - this->AddChild( m_pText[i] ); - } - - m_pTextSectionCount = new BitmapText; - m_pTextSectionCount->SetName( "SectionCount" ); - ActorUtil::LoadAllCommands( m_pTextSectionCount, "MusicWheelItem" ); - m_pTextSectionCount->LoadFromFont( THEME->GetPathF(sType,"SectionCount") ); - ActorUtil::SetXY( m_pTextSectionCount, "MusicWheelItem" ); - m_pTextSectionCount->PlayCommand( "On" ); - this->AddChild( m_pTextSectionCount ); - - m_WheelNotifyIcon.SetName( "WheelNotifyIcon" ); - ActorUtil::LoadAllCommands( m_WheelNotifyIcon, "MusicWheelItem" ); - ActorUtil::SetXY( m_WheelNotifyIcon, "MusicWheelItem" ); - m_WheelNotifyIcon.PlayCommand( "On" ); - this->AddChild( &m_WheelNotifyIcon ); - - FOREACH_PlayerNumber( p ) - { - m_pGradeDisplay[p].Load( THEME->GetPathG(sType,"grades") ); - m_pGradeDisplay[p]->SetName( ssprintf("GradeP%d",int(p+1)) ); - this->AddChild( m_pGradeDisplay[p] ); - LOAD_ALL_COMMANDS_AND_SET_XY( m_pGradeDisplay[p] ); - } - - this->SubscribeToMessage( Message_CurrentStepsP1Changed ); - this->SubscribeToMessage( Message_CurrentStepsP2Changed ); - this->SubscribeToMessage( Message_CurrentTrailP1Changed ); - this->SubscribeToMessage( Message_CurrentTrailP2Changed ); - this->SubscribeToMessage( Message_PreferredDifficultyP1Changed ); - this->SubscribeToMessage( Message_PreferredDifficultyP2Changed ); -} - -MusicWheelItem::MusicWheelItem( const MusicWheelItem &cpy ): - WheelItemBase( cpy ), - GRADES_SHOW_MACHINE( cpy.GRADES_SHOW_MACHINE ), - m_TextBanner( cpy.m_TextBanner ), - m_WheelNotifyIcon( cpy.m_WheelNotifyIcon ) -{ - FOREACH_ENUM( MusicWheelItemType, i ) - { - m_sprColorPart[i] = cpy.m_sprColorPart[i]; - this->AddChild( m_sprColorPart[i] ); - - m_sprNormalPart[i] = cpy.m_sprNormalPart[i]; - this->AddChild( m_sprNormalPart[i] ); - } - - this->AddChild( &m_TextBanner ); - - FOREACH_ENUM( MusicWheelItemType, i ) - { - m_sprOverPart[i] = cpy.m_sprOverPart[i]; - this->AddChild( m_sprOverPart[i] ); - } - - FOREACH_ENUM( MusicWheelItemType, i ) - { - if( cpy.m_pText[i] == NULL ) - { - m_pText[i] = NULL; - } - else - { - m_pText[i] = new BitmapText( *cpy.m_pText[i] ); - this->AddChild( m_pText[i] ); - } - } - - m_pTextSectionCount = new BitmapText( *cpy.m_pTextSectionCount ); - this->AddChild( m_pTextSectionCount ); - - this->AddChild( &m_WheelNotifyIcon ); - - FOREACH_PlayerNumber( p ) - { - m_pGradeDisplay[p] = cpy.m_pGradeDisplay[p]; - this->AddChild( m_pGradeDisplay[p] ); - } -} - -MusicWheelItem::~MusicWheelItem() -{ - FOREACH_ENUM( MusicWheelItemType, i ) - { - SAFE_DELETE(m_pText[i]); - } - delete m_pTextSectionCount; -} - -void MusicWheelItem::LoadFromWheelItemData( const WheelItemBaseData *pData, int iIndex, bool bHasFocus, int iDrawIndex ) -{ - WheelItemBase::LoadFromWheelItemData( pData, iIndex, bHasFocus, iDrawIndex ); - - const MusicWheelItemData *pWID = dynamic_cast( pData ); - - // hide all - FOREACH_ENUM( MusicWheelItemType, i ) - { - m_sprColorPart[i]->SetVisible( false ); - m_sprNormalPart[i]->SetVisible( false ); - m_sprOverPart[i]->SetVisible( false ); - } - m_TextBanner.SetVisible( false ); - FOREACH_ENUM( MusicWheelItemType, i ) - if( m_pText[i] ) - m_pText[i]->SetVisible( false ); - m_pTextSectionCount->SetVisible( false ); - m_WheelNotifyIcon.SetVisible( false ); - FOREACH_PlayerNumber( p ) - m_pGradeDisplay[p]->SetVisible( false ); - - - // Fill these in below - RString sDisplayName, sTranslitName; - MusicWheelItemType type = MusicWheelItemType_Invalid; - - switch( pWID->m_Type ) - { - DEFAULT_FAIL( pWID->m_Type ); - case WheelItemDataType_Song: - type = MusicWheelItemType_Song; - - m_TextBanner.SetFromSong( pWID->m_pSong ); - // We can do this manually if we wanted... maybe have a metric for overrides? -aj - m_TextBanner.SetDiffuse( pWID->m_color ); - m_TextBanner.SetVisible( true ); - - m_WheelNotifyIcon.SetFlags( pWID->m_Flags ); - m_WheelNotifyIcon.SetVisible( true ); - RefreshGrades(); - break; - case WheelItemDataType_Section: - { - sDisplayName = SONGMAN->ShortenGroupName(pWID->m_sText); - - if( GAMESTATE->sExpandedSectionName == pWID->m_sText ) - type = MusicWheelItemType_SectionExpanded; - else - type = MusicWheelItemType_SectionCollapsed; - - m_pTextSectionCount->SetText( ssprintf("%d",pWID->m_iSectionCount) ); - m_pTextSectionCount->SetVisible( true ); - } - break; - case WheelItemDataType_Course: - sDisplayName = pWID->m_pCourse->GetDisplayFullTitle(); - sTranslitName = pWID->m_pCourse->GetTranslitFullTitle(); - type = MusicWheelItemType_Course; - m_WheelNotifyIcon.SetFlags( pWID->m_Flags ); - m_WheelNotifyIcon.SetVisible( true ); - break; - case WheelItemDataType_Sort: - sDisplayName = pWID->m_sLabel; - // hack to get mode items working. -freem - if( pWID->m_pAction->m_pm != PlayMode_Invalid ) - type = MusicWheelItemType_Mode; - else - type = MusicWheelItemType_Sort; - break; - case WheelItemDataType_Roulette: - sDisplayName = THEME->GetString("MusicWheel","Roulette"); - type = MusicWheelItemType_Roulette; - break; - case WheelItemDataType_Random: - sDisplayName = THEME->GetString("MusicWheel","Random"); - type = MusicWheelItemType_Random; - break; - case WheelItemDataType_Portal: - sDisplayName = THEME->GetString("MusicWheel","Portal"); - type = MusicWheelItemType_Portal; - break; - case WheelItemDataType_Custom: - sDisplayName = pWID->m_sLabel; - type = MusicWheelItemType_Custom; - break; - } - - m_sprColorPart[type]->SetVisible( true ); - m_sprColorPart[type]->SetDiffuse( pWID->m_color ); - m_sprNormalPart[type]->SetVisible( true ); - m_sprOverPart[type]->SetVisible( true ); - BitmapText *bt = m_pText[type]; - if( bt ) - { - bt->SetText( sDisplayName, sTranslitName ); - bt->SetDiffuse( pWID->m_color ); - bt->SetVisible( true ); - } - - FOREACH_ENUM( MusicWheelItemType, i ) - { - if( m_sprColorPart[i]->GetVisible() ) - { - SetGrayBar( m_sprColorPart[i] ); - break; - } - } - - // Call "Set" so that elements like TextBanner react to the change in song. - { - Message msg( "Set" ); - msg.SetParam( "Song", pWID->m_pSong ); - msg.SetParam( "Course", pWID->m_pCourse ); - msg.SetParam( "Index", iIndex ); - msg.SetParam( "HasFocus", bHasFocus ); - msg.SetParam( "Text", pWID->m_sText ); - msg.SetParam( "DrawIndex", iDrawIndex ); - msg.SetParam( "Type", MusicWheelItemTypeToString(type) ); - msg.SetParam( "Color", pWID->m_color ); - msg.SetParam( "Label", pWID->m_sLabel ); - - this->HandleMessage( msg ); - } -} - -void MusicWheelItem::RefreshGrades() -{ - const MusicWheelItemData *pWID = dynamic_cast( m_pData ); - - if( pWID == NULL ) - return; // LoadFromWheelItemData() hasn't been called yet. - FOREACH_HumanPlayer( p ) - { - m_pGradeDisplay[p]->SetVisible( false ); - - if( pWID->m_pSong == NULL && pWID->m_pCourse == NULL ) - continue; - - Difficulty dc; - if( GAMESTATE->m_pCurSteps[p] ) - dc = GAMESTATE->m_pCurSteps[p]->GetDifficulty(); - else if( GAMESTATE->m_pCurTrail[p] ) - dc = GAMESTATE->m_pCurTrail[p]->m_CourseDifficulty; - else - dc = GAMESTATE->m_PreferredDifficulty[p]; - - ProfileSlot ps; - if( PROFILEMAN->IsPersistentProfile(p) ) - ps = (ProfileSlot)p; - else if( GRADES_SHOW_MACHINE ) - ps = ProfileSlot_Machine; - else - continue; - - StepsType st; - if( GAMESTATE->m_pCurSteps[p] ) - st = GAMESTATE->m_pCurSteps[p]->m_StepsType; - else if( GAMESTATE->m_pCurTrail[p] ) - st = GAMESTATE->m_pCurTrail[p]->m_StepsType; - else - st = GAMESTATE->m_pCurStyle->m_StepsType; - - m_pGradeDisplay[p]->SetVisible( true ); - - - Profile *pProfile = PROFILEMAN->GetProfile(ps); - - HighScoreList *pHSL = NULL; - if( PROFILEMAN->IsPersistentProfile(ps) && dc != Difficulty_Invalid ) - { - if( pWID->m_pSong ) - { - const Steps* pSteps = SongUtil::GetStepsByDifficulty( pWID->m_pSong, st, dc ); - if( pSteps != NULL ) - pHSL = &pProfile->GetStepsHighScoreList(pWID->m_pSong, pSteps); - } - else if( pWID->m_pCourse ) - { - const Trail *pTrail = pWID->m_pCourse->GetTrail( st, dc ); - if( pTrail != NULL ) - pHSL = &pProfile->GetCourseHighScoreList( pWID->m_pCourse, pTrail ); - } - } - - Message msg( "SetGrade" ); - msg.SetParam( "PlayerNumber", p ); - if( pHSL ) - { - msg.SetParam( "Grade", pHSL->HighGrade ); - msg.SetParam( "NumTimesPlayed", pHSL->GetNumTimesPlayed() ); - } - m_pGradeDisplay[p]->HandleMessage( msg ); - } -} - -void MusicWheelItem::HandleMessage( const Message &msg ) -{ - if( msg == Message_CurrentStepsP1Changed || - msg == Message_CurrentStepsP2Changed || - msg == Message_CurrentTrailP1Changed || - msg == Message_CurrentTrailP2Changed || - msg == Message_PreferredDifficultyP1Changed || - msg == Message_PreferredDifficultyP2Changed ) - { - RefreshGrades(); - } - - WheelItemBase::HandleMessage( msg ); -} - -/* - * (c) 2001-2004 Chris Danford, Chris Gomez, Glenn Maynard - * 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. - */ +#include "global.h" +#include "MusicWheelItem.h" +#include "RageUtil.h" +#include "SongManager.h" +#include "GameManager.h" +#include "RageLog.h" +#include "GameConstantsAndTypes.h" +#include "GameState.h" +#include "ThemeManager.h" +#include "Steps.h" +#include "Song.h" +#include "Course.h" +#include "ProfileManager.h" +#include "Profile.h" +#include "Style.h" +#include "ActorUtil.h" +#include "ThemeMetric.h" +#include "HighScore.h" +#include "ScreenSelectMusic.h" +#include "ScreenManager.h" + +static const char *MusicWheelItemTypeNames[] = { + "Song", + "SectionExpanded", + "SectionCollapsed", + "Roulette", + "Course", + "Sort", + "Mode", + "Random", + "Portal", + "Custom", +}; +XToString( MusicWheelItemType ); + +MusicWheelItemData::MusicWheelItemData( WheelItemDataType type, Song* pSong, + RString sSectionName, Course* pCourse, + RageColor color, int iSectionCount ): + WheelItemBaseData(type, sSectionName, color), + m_pCourse(pCourse), m_pSong(pSong), m_Flags(WheelNotifyIcon::Flags()), + m_iSectionCount(iSectionCount), m_sLabel(""), m_pAction() {} + +MusicWheelItem::MusicWheelItem( RString sType ): + WheelItemBase( sType ) +{ + GRADES_SHOW_MACHINE.Load( sType, "GradesShowMachine" ); + + FOREACH_ENUM( MusicWheelItemType, i ) + { + m_sprColorPart[i].Load( THEME->GetPathG(sType,MusicWheelItemTypeToString(i)+" ColorPart") ); + m_sprColorPart[i]->SetName( MusicWheelItemTypeToString(i)+"ColorPart" ); + ActorUtil::LoadAllCommands(m_sprColorPart[i],"MusicWheelItem"); + this->AddChild( m_sprColorPart[i] ); + + m_sprNormalPart[i].Load( THEME->GetPathG(sType,MusicWheelItemTypeToString(i)+" NormalPart") ); + m_sprNormalPart[i]->SetName( MusicWheelItemTypeToString(i)+"NormalPart" ); + ActorUtil::LoadAllCommands(m_sprNormalPart[i],"MusicWheelItem"); + this->AddChild( m_sprNormalPart[i] ); + } + + m_TextBanner.SetName( "SongName" ); + ActorUtil::LoadAllCommands( m_TextBanner, "MusicWheelItem" ); + m_TextBanner.Load( "TextBanner" ); + ActorUtil::SetXY( m_TextBanner, "MusicWheelItem" ); + m_TextBanner.PlayCommand( "On" ); + this->AddChild( &m_TextBanner ); + + FOREACH_ENUM( MusicWheelItemType, i ) + { + m_sprOverPart[i].Load( THEME->GetPathG(sType,MusicWheelItemTypeToString(i)+" OverPart") ); + m_sprOverPart[i]->SetName( MusicWheelItemTypeToString(i)+"OverPart" ); + ActorUtil::LoadAllCommands(m_sprOverPart[i],"MusicWheelItem"); + this->AddChild( m_sprOverPart[i] ); + } + + FOREACH_ENUM( MusicWheelItemType, i ) + { + m_pText[i] = NULL; + + // Don't init text for Type_Song. It uses a TextBanner. + if( i == MusicWheelItemType_Song ) + continue; + + m_pText[i] = new BitmapText; + m_pText[i]->SetName( MusicWheelItemTypeToString(i) ); + ActorUtil::LoadAllCommands( m_pText[i], "MusicWheelItem" ); + m_pText[i]->LoadFromFont( THEME->GetPathF(sType,MusicWheelItemTypeToString(i)) ); + ActorUtil::SetXY( m_pText[i], "MusicWheelItem" ); + m_pText[i]->PlayCommand( "On" ); + this->AddChild( m_pText[i] ); + } + + m_pTextSectionCount = new BitmapText; + m_pTextSectionCount->SetName( "SectionCount" ); + ActorUtil::LoadAllCommands( m_pTextSectionCount, "MusicWheelItem" ); + m_pTextSectionCount->LoadFromFont( THEME->GetPathF(sType,"SectionCount") ); + ActorUtil::SetXY( m_pTextSectionCount, "MusicWheelItem" ); + m_pTextSectionCount->PlayCommand( "On" ); + this->AddChild( m_pTextSectionCount ); + + m_WheelNotifyIcon.SetName( "WheelNotifyIcon" ); + ActorUtil::LoadAllCommands( m_WheelNotifyIcon, "MusicWheelItem" ); + ActorUtil::SetXY( m_WheelNotifyIcon, "MusicWheelItem" ); + m_WheelNotifyIcon.PlayCommand( "On" ); + this->AddChild( &m_WheelNotifyIcon ); + + FOREACH_PlayerNumber( p ) + { + m_pGradeDisplay[p].Load( THEME->GetPathG(sType,"grades") ); + m_pGradeDisplay[p]->SetName( ssprintf("GradeP%d",int(p+1)) ); + this->AddChild( m_pGradeDisplay[p] ); + LOAD_ALL_COMMANDS_AND_SET_XY( m_pGradeDisplay[p] ); + } + + this->SubscribeToMessage( Message_CurrentStepsP1Changed ); + this->SubscribeToMessage( Message_CurrentStepsP2Changed ); + this->SubscribeToMessage( Message_CurrentTrailP1Changed ); + this->SubscribeToMessage( Message_CurrentTrailP2Changed ); + this->SubscribeToMessage( Message_PreferredDifficultyP1Changed ); + this->SubscribeToMessage( Message_PreferredDifficultyP2Changed ); +} + +MusicWheelItem::MusicWheelItem( const MusicWheelItem &cpy ): + WheelItemBase( cpy ), + GRADES_SHOW_MACHINE( cpy.GRADES_SHOW_MACHINE ), + m_TextBanner( cpy.m_TextBanner ), + m_WheelNotifyIcon( cpy.m_WheelNotifyIcon ) +{ + FOREACH_ENUM( MusicWheelItemType, i ) + { + m_sprColorPart[i] = cpy.m_sprColorPart[i]; + this->AddChild( m_sprColorPart[i] ); + + m_sprNormalPart[i] = cpy.m_sprNormalPart[i]; + this->AddChild( m_sprNormalPart[i] ); + } + + this->AddChild( &m_TextBanner ); + + FOREACH_ENUM( MusicWheelItemType, i ) + { + m_sprOverPart[i] = cpy.m_sprOverPart[i]; + this->AddChild( m_sprOverPart[i] ); + } + + FOREACH_ENUM( MusicWheelItemType, i ) + { + if( cpy.m_pText[i] == NULL ) + { + m_pText[i] = NULL; + } + else + { + m_pText[i] = new BitmapText( *cpy.m_pText[i] ); + this->AddChild( m_pText[i] ); + } + } + + m_pTextSectionCount = new BitmapText( *cpy.m_pTextSectionCount ); + this->AddChild( m_pTextSectionCount ); + + this->AddChild( &m_WheelNotifyIcon ); + + FOREACH_PlayerNumber( p ) + { + m_pGradeDisplay[p] = cpy.m_pGradeDisplay[p]; + this->AddChild( m_pGradeDisplay[p] ); + } +} + +MusicWheelItem::~MusicWheelItem() +{ + FOREACH_ENUM( MusicWheelItemType, i ) + { + SAFE_DELETE(m_pText[i]); + } + delete m_pTextSectionCount; +} + +void MusicWheelItem::LoadFromWheelItemData( const WheelItemBaseData *pData, int iIndex, bool bHasFocus, int iDrawIndex ) +{ + WheelItemBase::LoadFromWheelItemData( pData, iIndex, bHasFocus, iDrawIndex ); + + const MusicWheelItemData *pWID = dynamic_cast( pData ); + + // hide all + FOREACH_ENUM( MusicWheelItemType, i ) + { + m_sprColorPart[i]->SetVisible( false ); + m_sprNormalPart[i]->SetVisible( false ); + m_sprOverPart[i]->SetVisible( false ); + } + m_TextBanner.SetVisible( false ); + FOREACH_ENUM( MusicWheelItemType, i ) + if( m_pText[i] ) + m_pText[i]->SetVisible( false ); + m_pTextSectionCount->SetVisible( false ); + m_WheelNotifyIcon.SetVisible( false ); + FOREACH_PlayerNumber( p ) + m_pGradeDisplay[p]->SetVisible( false ); + + + // Fill these in below + RString sDisplayName, sTranslitName; + MusicWheelItemType type = MusicWheelItemType_Invalid; + + switch( pWID->m_Type ) + { + DEFAULT_FAIL( pWID->m_Type ); + case WheelItemDataType_Song: + type = MusicWheelItemType_Song; + + m_TextBanner.SetFromSong( pWID->m_pSong ); + // We can do this manually if we wanted... maybe have a metric for overrides? -aj + m_TextBanner.SetDiffuse( pWID->m_color ); + m_TextBanner.SetVisible( true ); + + m_WheelNotifyIcon.SetFlags( pWID->m_Flags ); + m_WheelNotifyIcon.SetVisible( true ); + RefreshGrades(); + break; + case WheelItemDataType_Section: + { + sDisplayName = SONGMAN->ShortenGroupName(pWID->m_sText); + + if( GAMESTATE->sExpandedSectionName == pWID->m_sText ) + type = MusicWheelItemType_SectionExpanded; + else + type = MusicWheelItemType_SectionCollapsed; + + m_pTextSectionCount->SetText( ssprintf("%d",pWID->m_iSectionCount) ); + m_pTextSectionCount->SetVisible( true ); + } + break; + case WheelItemDataType_Course: + sDisplayName = pWID->m_pCourse->GetDisplayFullTitle(); + sTranslitName = pWID->m_pCourse->GetTranslitFullTitle(); + type = MusicWheelItemType_Course; + m_WheelNotifyIcon.SetFlags( pWID->m_Flags ); + m_WheelNotifyIcon.SetVisible( true ); + break; + case WheelItemDataType_Sort: + sDisplayName = pWID->m_sLabel; + // hack to get mode items working. -freem + if( pWID->m_pAction->m_pm != PlayMode_Invalid ) + type = MusicWheelItemType_Mode; + else + type = MusicWheelItemType_Sort; + break; + case WheelItemDataType_Roulette: + sDisplayName = THEME->GetString("MusicWheel","Roulette"); + type = MusicWheelItemType_Roulette; + break; + case WheelItemDataType_Random: + sDisplayName = THEME->GetString("MusicWheel","Random"); + type = MusicWheelItemType_Random; + break; + case WheelItemDataType_Portal: + sDisplayName = THEME->GetString("MusicWheel","Portal"); + type = MusicWheelItemType_Portal; + break; + case WheelItemDataType_Custom: + sDisplayName = pWID->m_sLabel; + type = MusicWheelItemType_Custom; + break; + } + + m_sprColorPart[type]->SetVisible( true ); + m_sprColorPart[type]->SetDiffuse( pWID->m_color ); + m_sprNormalPart[type]->SetVisible( true ); + m_sprOverPart[type]->SetVisible( true ); + BitmapText *bt = m_pText[type]; + if( bt ) + { + bt->SetText( sDisplayName, sTranslitName ); + bt->SetDiffuse( pWID->m_color ); + bt->SetVisible( true ); + } + + FOREACH_ENUM( MusicWheelItemType, i ) + { + if( m_sprColorPart[i]->GetVisible() ) + { + SetGrayBar( m_sprColorPart[i] ); + break; + } + } + + // Call "Set" so that elements like TextBanner react to the change in song. + { + Message msg( "Set" ); + msg.SetParam( "Song", pWID->m_pSong ); + msg.SetParam( "Course", pWID->m_pCourse ); + msg.SetParam( "Index", iIndex ); + msg.SetParam( "HasFocus", bHasFocus ); + msg.SetParam( "Text", pWID->m_sText ); + msg.SetParam( "DrawIndex", iDrawIndex ); + msg.SetParam( "Type", MusicWheelItemTypeToString(type) ); + msg.SetParam( "Color", pWID->m_color ); + msg.SetParam( "Label", pWID->m_sLabel ); + + this->HandleMessage( msg ); + } +} + +void MusicWheelItem::RefreshGrades() +{ + const MusicWheelItemData *pWID = dynamic_cast( m_pData ); + + if( pWID == NULL ) + return; // LoadFromWheelItemData() hasn't been called yet. + FOREACH_HumanPlayer( p ) + { + m_pGradeDisplay[p]->SetVisible( false ); + + if( pWID->m_pSong == NULL && pWID->m_pCourse == NULL ) + continue; + + Difficulty dc; + if( GAMESTATE->m_pCurSteps[p] ) + dc = GAMESTATE->m_pCurSteps[p]->GetDifficulty(); + else if( GAMESTATE->m_pCurTrail[p] ) + dc = GAMESTATE->m_pCurTrail[p]->m_CourseDifficulty; + else + dc = GAMESTATE->m_PreferredDifficulty[p]; + + ProfileSlot ps; + if( PROFILEMAN->IsPersistentProfile(p) ) + ps = (ProfileSlot)p; + else if( GRADES_SHOW_MACHINE ) + ps = ProfileSlot_Machine; + else + continue; + + StepsType st; + if( GAMESTATE->m_pCurSteps[p] ) + st = GAMESTATE->m_pCurSteps[p]->m_StepsType; + else if( GAMESTATE->m_pCurTrail[p] ) + st = GAMESTATE->m_pCurTrail[p]->m_StepsType; + else + st = GAMESTATE->m_pCurStyle->m_StepsType; + + m_pGradeDisplay[p]->SetVisible( true ); + + + Profile *pProfile = PROFILEMAN->GetProfile(ps); + + HighScoreList *pHSL = NULL; + if( PROFILEMAN->IsPersistentProfile(ps) && dc != Difficulty_Invalid ) + { + if( pWID->m_pSong ) + { + const Steps* pSteps = SongUtil::GetStepsByDifficulty( pWID->m_pSong, st, dc ); + if( pSteps != nullptr ) + pHSL = &pProfile->GetStepsHighScoreList(pWID->m_pSong, pSteps); + } + else if( pWID->m_pCourse ) + { + const Trail *pTrail = pWID->m_pCourse->GetTrail( st, dc ); + if( pTrail != nullptr ) + pHSL = &pProfile->GetCourseHighScoreList( pWID->m_pCourse, pTrail ); + } + } + + Message msg( "SetGrade" ); + msg.SetParam( "PlayerNumber", p ); + if( pHSL ) + { + msg.SetParam( "Grade", pHSL->HighGrade ); + msg.SetParam( "NumTimesPlayed", pHSL->GetNumTimesPlayed() ); + } + m_pGradeDisplay[p]->HandleMessage( msg ); + } +} + +void MusicWheelItem::HandleMessage( const Message &msg ) +{ + if( msg == Message_CurrentStepsP1Changed || + msg == Message_CurrentStepsP2Changed || + msg == Message_CurrentTrailP1Changed || + msg == Message_CurrentTrailP2Changed || + msg == Message_PreferredDifficultyP1Changed || + msg == Message_PreferredDifficultyP2Changed ) + { + RefreshGrades(); + } + + WheelItemBase::HandleMessage( msg ); +} + +/* + * (c) 2001-2004 Chris Danford, Chris Gomez, Glenn Maynard + * 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. + */ diff --git a/src/NetworkSyncManager.cpp b/src/NetworkSyncManager.cpp index 4e17edb08a..d5e2eb3101 100644 --- a/src/NetworkSyncManager.cpp +++ b/src/NetworkSyncManager.cpp @@ -1,1017 +1,1017 @@ -#include "global.h" -#include "NetworkSyncManager.h" -#include "LuaManager.h" -#include "LocalizedString.h" -#include - -NetworkSyncManager *NSMAN; - -// Aldo: used by GetCurrentSMVersion() -#if defined(HAVE_VERSION_INFO) -extern unsigned long version_num; -#endif - -#if defined(WITHOUT_NETWORKING) -NetworkSyncManager::NetworkSyncManager( LoadingWindow *ld ) { useSMserver=false; isSMOnline = false; } -NetworkSyncManager::~NetworkSyncManager () { } -void NetworkSyncManager::CloseConnection() { } -void NetworkSyncManager::PostStartUp( const RString& ServerIP ) { } -bool NetworkSyncManager::Connect( const RString& addy, unsigned short port ) { return false; } -RString NetworkSyncManager::GetServerName() { return RString(); } -void NetworkSyncManager::ReportNSSOnOff( int i ) { } -void NetworkSyncManager::ReportScore( int playerID, int step, int score, int combo, float offset ) { } -void NetworkSyncManager::ReportSongOver() { } -void NetworkSyncManager::ReportStyle() {} -void NetworkSyncManager::StartRequest( short position ) { } -void NetworkSyncManager::DisplayStartupStatus() { } -void NetworkSyncManager::Update( float fDeltaTime ) { } -bool NetworkSyncManager::ChangedScoreboard( int Column ) { return false; } -void NetworkSyncManager::SendChat( const RString& message ) { } -void NetworkSyncManager::SelectUserSong() { } -RString NetworkSyncManager::MD5Hex( const RString &sInput ) { return RString(); } -int NetworkSyncManager::GetSMOnlineSalt() { return 0; } -void NetworkSyncManager::GetListOfLANServers( vector& AllServers ) { } - #if defined(HAVE_VERSION_INFO) - unsigned long NetworkSyncManager::GetCurrentSMBuild( LoadingWindow* ld ) { return version_num; } - #else - unsigned long NetworkSyncManager::GetCurrentSMBuild( LoadingWindow* ld ) { return 0; } - #endif -#else -#include "ezsockets.h" -#include "ProfileManager.h" -#include "RageLog.h" -#include "ScreenManager.h" -#include "Song.h" -#include "Course.h" -#include "GameState.h" -#include "StatsManager.h" -#include "Steps.h" -#include "ProductInfo.h" -#include "ScreenMessage.h" -#include "GameManager.h" -#include "MessageManager.h" -#include "arch/LoadingWindow/LoadingWindow.h" -#include "PlayerState.h" -#include "CryptManager.h" - -AutoScreenMessage( SM_AddToChat ); -AutoScreenMessage( SM_ChangeSong ); -AutoScreenMessage( SM_GotEval ); -AutoScreenMessage( SM_UsersUpdate ); -AutoScreenMessage( SM_SMOnlinePack ); - -int NetworkSyncManager::GetSMOnlineSalt() -{ - return m_iSalt; -} - -static LocalizedString INITIALIZING_CLIENT_NETWORK ( "NetworkSyncManager", "Initializing Client Network..." ); -NetworkSyncManager::NetworkSyncManager( LoadingWindow *ld ) -{ - LANserver = NULL; //So we know if it has been created yet - BroadcastReception = NULL; - - ld->SetText( INITIALIZING_CLIENT_NETWORK ); - NetPlayerClient = new EzSockets; - NetPlayerClient->blocking = false; - m_ServerVersion = 0; - - useSMserver = false; - isSMOnline = false; - FOREACH_PlayerNumber( pn ) - isSMOLoggedIn[pn] = false; - - m_startupStatus = 0; //By default, connection not tried. - - m_ActivePlayers = 0; - - StartUp(); -} - -NetworkSyncManager::~NetworkSyncManager () -{ - //Close Connection to server nicely. - if( useSMserver ) - NetPlayerClient->close(); - SAFE_DELETE( NetPlayerClient ); - - if ( BroadcastReception ) - { - BroadcastReception->close(); - SAFE_DELETE( BroadcastReception ); - } -} - -void NetworkSyncManager::CloseConnection() -{ - if( !useSMserver ) - return; - m_ServerVersion = 0; - useSMserver = false; - isSMOnline = false; - FOREACH_PlayerNumber( pn ) - isSMOLoggedIn[pn] = false; - m_startupStatus = 0; - NetPlayerClient->close(); -} - -void NetworkSyncManager::PostStartUp( const RString& ServerIP ) -{ - RString sAddress; - unsigned short iPort; - - size_t cLoc = ServerIP.find( ':' ); - if( ServerIP.find( ':' ) != RString::npos ) - { - sAddress = ServerIP.substr( 0, cLoc ); - char* cEnd; - errno = 0; - iPort = (unsigned short)strtol( ServerIP.substr( cLoc + 1 ).c_str(), &cEnd, 10 ); - if( *cEnd != 0 || errno != 0 ) - { - m_startupStatus = 2; - LOG->Warn( "Invalid port" ); - return; - } - } - else - { - iPort = 8765; - sAddress = ServerIP; - } - - LOG->Info( "Attempting to connect to: %s, Port: %i", sAddress.c_str(), iPort ); - - CloseConnection(); - if( !Connect(sAddress.c_str(), iPort) ) - { - m_startupStatus = 2; - LOG->Warn( "Network Sync Manager failed to connect" ); - return; - } - - FOREACH_PlayerNumber( pn ) - isSMOLoggedIn[pn] = false; - - useSMserver = true; - - m_startupStatus = 1; // Connection attepmpt successful - - // If network play is desired and the connection works, - // halt until we know what server version we're dealing with - - m_packet.ClearPacket(); - m_packet.Write1( NSCHello ); //Hello Packet - - m_packet.Write1( NETPROTOCOLVERSION ); - - m_packet.WriteNT( RString(PRODUCT_ID_VER) ); - - /* Block until response is received. - * Move mode to blocking in order to give CPU back to the system, - * and not wait. - */ - - bool dontExit = true; - - NetPlayerClient->blocking = true; - - // The following packet must get through, so we block for it. - // If we are serving, we do not block for this. - NetPlayerClient->SendPack( (char*)m_packet.Data, m_packet.Position ); - - m_packet.ClearPacket(); - - while( dontExit ) - { - m_packet.ClearPacket(); - if( NetPlayerClient->ReadPack((char *)&m_packet, NETMAXBUFFERSIZE)<1 ) - dontExit=false; // Also allow exit if there is a problem on the socket - if( m_packet.Read1() == NSServerOffset + NSCHello ) - dontExit=false; - // Only allow passing on handshake; otherwise scoreboard updates and - // such will confuse us. - } - - NetPlayerClient->blocking = false; - - m_ServerVersion = m_packet.Read1(); - if( m_ServerVersion >= 128 ) - isSMOnline = true; - - m_ServerName = m_packet.ReadNT(); - m_iSalt = m_packet.Read4(); - LOG->Info( "Server Version: %d %s", m_ServerVersion, m_ServerName.c_str() ); -} - - -void NetworkSyncManager::StartUp() -{ - RString ServerIP; - - if( GetCommandlineArgument( "netip", &ServerIP ) ) - PostStartUp( ServerIP ); - - BroadcastReception = new EzSockets; - BroadcastReception->create( IPPROTO_UDP ); - BroadcastReception->bind( 8765 ); - BroadcastReception->blocking = false; -} - - -bool NetworkSyncManager::Connect( const RString& addy, unsigned short port ) -{ - LOG->Info( "Beginning to connect" ); - - NetPlayerClient->create(); // Initialize Socket - LOG->Info( "Calling EzSockets:connect()" ); - useSMserver = NetPlayerClient->connect( addy, port ); - LOG->Info( "Back from ezsockets..." ); - return useSMserver; -} - -void NetworkSyncManager::ReportNSSOnOff(int i) -{ - m_packet.ClearPacket(); - m_packet.Write1( NSCSMS ); - m_packet.Write1( (uint8_t) i ); - NetPlayerClient->SendPack( (char*)m_packet.Data, m_packet.Position ); -} - -RString NetworkSyncManager::GetServerName() -{ - return m_ServerName; -} - -void NetworkSyncManager::ReportScore(int playerID, int step, int score, int combo, float offset) -{ - if( !useSMserver ) //Make sure that we are using the network - return; - - LOG->Trace( "Player ID %i combo = %i", playerID, combo ); - m_packet.ClearPacket(); - - m_packet.Write1( NSCGSU ); - step = TranslateStepType(step); - uint8_t ctr = (uint8_t) (playerID * 16 + step - ( SMOST_HITMINE - 1 ) ); - m_packet.Write1( ctr ); - - ctr = uint8_t( STATSMAN->m_CurStageStats.m_player[playerID].GetGrade()*16 ); - - if ( STATSMAN->m_CurStageStats.m_player[playerID].m_bFailed ) - ctr = uint8_t( 112 ); //Code for failed (failed constant seems not to work) - - m_packet.Write1( ctr ); - m_packet.Write4( score ); - m_packet.Write2( (uint16_t)combo ); - m_packet.Write2( (uint16_t)m_playerLife[playerID] ); - - // Offset Info - // Note: if a 0 is sent, then disregard data. - - // ASSUMED: No step will be more than 16 seconds off center. - // If this assumption is false, read 16 seconds in either direction. - int iOffset = int( (offset+16.384)*2000.0f ); - - if( iOffset>65535 ) - iOffset=65535; - if( iOffset<1 ) - iOffset=1; - - // Report 0 if hold, or miss (don't forget mines should report) - if( step == SMOST_HITMINE || step > SMOST_W1 ) - iOffset = 0; - - m_packet.Write2( (uint16_t)iOffset ); - - NetPlayerClient->SendPack( (char*)m_packet.Data, m_packet.Position ); - -} - -void NetworkSyncManager::ReportSongOver() -{ - if ( !useSMserver ) //Make sure that we are using the network - return; - - m_packet.ClearPacket(); - - m_packet.Write1( NSCGON ); - - NetPlayerClient->SendPack( (char*)&m_packet.Data, m_packet.Position ); - return; -} - -void NetworkSyncManager::ReportStyle() -{ - LOG->Trace( "Sending \"Style\" to server" ); - - if( !useSMserver ) - return; - m_packet.ClearPacket(); - m_packet.Write1( NSCSU ); - m_packet.Write1( (int8_t)GAMESTATE->GetNumPlayersEnabled() ); - - FOREACH_EnabledPlayer( pn ) - { - m_packet.Write1( (uint8_t)pn ); - m_packet.WriteNT( GAMESTATE->GetPlayerDisplayName(pn) ); - } - - NetPlayerClient->SendPack( (char*)&m_packet.Data, m_packet.Position ); -} - -void NetworkSyncManager::StartRequest( short position ) -{ - if( !useSMserver ) - return; - - if( GAMESTATE->m_bDemonstrationOrJukebox ) - return; - - LOG->Trace( "Requesting Start from Server." ); - - m_packet.ClearPacket(); - - m_packet.Write1( NSCGSR ); - - unsigned char ctr=0; - - Steps * tSteps; - tSteps = GAMESTATE->m_pCurSteps[PLAYER_1]; - if( tSteps!=NULL && GAMESTATE->IsPlayerEnabled(PLAYER_1) ) - ctr = uint8_t(ctr+tSteps->GetMeter()*16); - - tSteps = GAMESTATE->m_pCurSteps[PLAYER_2]; - if( tSteps!=NULL && GAMESTATE->IsPlayerEnabled(PLAYER_2) ) - ctr = uint8_t( ctr+tSteps->GetMeter() ); - - m_packet.Write1( ctr ); - - ctr=0; - - tSteps = GAMESTATE->m_pCurSteps[PLAYER_1]; - if( tSteps!=NULL && GAMESTATE->IsPlayerEnabled(PLAYER_1) ) - ctr = uint8_t( ctr + (int)tSteps->GetDifficulty()*16 ); - - tSteps = GAMESTATE->m_pCurSteps[PLAYER_2]; - if( tSteps!=NULL && GAMESTATE->IsPlayerEnabled(PLAYER_2) ) - ctr = uint8_t( ctr + (int)tSteps->GetDifficulty() ); - - m_packet.Write1( ctr ); - - //Notify server if this is for sync or not. - ctr = char( position*16 ); - m_packet.Write1( ctr ); - - if( GAMESTATE->m_pCurSong != NULL ) - { - m_packet.WriteNT( GAMESTATE->m_pCurSong->m_sMainTitle ); - m_packet.WriteNT( GAMESTATE->m_pCurSong->m_sSubTitle ); - m_packet.WriteNT( GAMESTATE->m_pCurSong->m_sArtist ); - } - else - { - m_packet.WriteNT( "" ); - m_packet.WriteNT( "" ); - m_packet.WriteNT( "" ); - } - - if( GAMESTATE->m_pCurCourse != NULL ) - m_packet.WriteNT( GAMESTATE->m_pCurCourse->GetDisplayFullTitle() ); - else - m_packet.WriteNT( RString() ); - - //Send Player (and song) Options - m_packet.WriteNT( GAMESTATE->m_SongOptions.GetCurrent().GetString() ); - - int players=0; - FOREACH_PlayerNumber( p ) - { - ++players; - m_packet.WriteNT( GAMESTATE->m_pPlayerState[p]->m_PlayerOptions.GetCurrent().GetString() ); - } - for (int i=0; i<2-players; ++i) - m_packet.WriteNT(""); //Write a NULL if no player - - //This needs to be reset before ScreenEvaluation could possibly be called - m_EvalPlayerData.clear(); - - //Block until go is recieved. - //Switch to blocking mode (this is the only - //way I know how to get precievably instantanious results - - bool dontExit=true; - - NetPlayerClient->blocking=true; - - //The following packet HAS to get through, so we turn blocking on for it as well - //Don't block if we are serving - NetPlayerClient->SendPack((char*)&m_packet.Data, m_packet.Position); - - LOG->Trace("Waiting for RECV"); - - m_packet.ClearPacket(); - - while (dontExit) - { - m_packet.ClearPacket(); - if (NetPlayerClient->ReadPack((char *)&m_packet, NETMAXBUFFERSIZE)<1) - dontExit=false; // Also allow exit if there is a problem on the socket - // Only do if we are not the server, otherwise the sync - // gets hosed up due to non blocking mode. - - if (m_packet.Read1() == (NSServerOffset + NSCGSR)) - dontExit=false; - // Only allow passing on Start request; otherwise scoreboard updates - // and such will confuse us. - - } - NetPlayerClient->blocking=false; - -} - -static LocalizedString CONNECTION_SUCCESSFUL( "NetworkSyncManager", "Connection to '%s' successful." ); -static LocalizedString CONNECTION_FAILED ( "NetworkSyncManager", "Connection failed." ); -void NetworkSyncManager::DisplayStartupStatus() -{ - RString sMessage(""); - - switch (m_startupStatus) - { - case 0: - //Networking wasn't attepmpted - return; - case 1: - sMessage = ssprintf( CONNECTION_SUCCESSFUL.GetValue(), m_ServerName.c_str() ); - break; - case 2: - sMessage = CONNECTION_FAILED.GetValue(); - break; - } - SCREENMAN->SystemMessage( sMessage ); -} - -void NetworkSyncManager::Update(float fDeltaTime) -{ - if (useSMserver) - ProcessInput(); - - PacketFunctions BroadIn; - if ( BroadcastReception->ReadPack( (char*)&BroadIn.Data, 1020 ) ) - { - NetServerInfo ThisServer; - BroadIn.Position = 0; - if ( BroadIn.Read1() == 141 ) // SMLS_Info? - { - ThisServer.Name = BroadIn.ReadNT(); - int port = BroadIn.Read2(); - BroadIn.Read2(); //Num players connected. - uint32_t addy = EzSockets::LongFromAddrIn(BroadcastReception->fromAddr); - ThisServer.Address = ssprintf( "%u.%u.%u.%u:%d", - (addy<<0)>>24, (addy<<8)>>24, (addy<<16)>>24, (addy<<24)>>24, port ); - - //It's fairly safe to assume that users will not be on networks with more than - //30 or 40 servers. Until this point, maps would be slower than vectors. - //So I am going to use a vector to store all of the servers. - // - //In this situation, I will traverse the vector to find the element that - //contains the corresponding server. - - unsigned int i; - for ( i = 0; i < m_vAllLANServers.size(); i++ ) - { - if ( m_vAllLANServers[i].Address == ThisServer.Address ) - { - m_vAllLANServers[i].Name = ThisServer.Name; - break; - } - } - if ( i >= m_vAllLANServers.size() ) - m_vAllLANServers.push_back( ThisServer ); - } - } -} - -static LocalizedString CONNECTION_DROPPED( "NetworkSyncManager", "Connection to server dropped." ); -void NetworkSyncManager::ProcessInput() -{ - //If we're disconnected, just exit - if ((NetPlayerClient->state!=NetPlayerClient->skCONNECTED) || - NetPlayerClient->IsError()) - { - SCREENMAN->SystemMessageNoAnimate( CONNECTION_DROPPED ); - useSMserver=false; - isSMOnline = false; - FOREACH_PlayerNumber(pn) - isSMOLoggedIn[pn] = false; - NetPlayerClient->close(); - return; - } - - // load new data into buffer - NetPlayerClient->update(); - m_packet.ClearPacket(); - - int packetSize; - while ( (packetSize = NetPlayerClient->ReadPack((char *)&m_packet, NETMAXBUFFERSIZE) ) > 0 ) - { - m_packet.size = packetSize; - int command = m_packet.Read1(); - //Check to make sure command is valid from server - if (command < NSServerOffset) - { - LOG->Trace("CMD (below 128) Invalid> %d",command); - break; - } - - command = command - NSServerOffset; - - switch (command) - { - case NSCPing: // Ping packet responce - m_packet.ClearPacket(); - m_packet.Write1( NSCPingR ); - NetPlayerClient->SendPack((char*)m_packet.Data,m_packet.Position); - break; - case NSCPingR: // These are in response to when/if we send packet 0's - case NSCHello: // This is already taken care of by the blocking code earlier - case NSCGSR: // This is taken care of by the blocking start code - break; - case NSCGON: - { - int PlayersInPack = m_packet.Read1(); - m_EvalPlayerData.resize(PlayersInPack); - for (int i=0; iSendMessageToTopScreen( SM_GotEval ); - } - break; - case NSCGSU: // Scoreboard Update - { //Ease scope - int ColumnNumber=m_packet.Read1(); - int NumberPlayers=m_packet.Read1(); - RString ColumnData; - - switch (ColumnNumber) - { - case NSSB_NAMES: - ColumnData = "Names\n"; - for (int i=0; iBroadcast( msg ); - */ - } - break; - case NSCSU: //System message from server - { - RString SysMSG = m_packet.ReadNT(); - SCREENMAN->SystemMessage( SysMSG ); - } - break; - case NSCCM: //Chat message from server - { - m_sChatText += m_packet.ReadNT() + " \n "; - //10000 chars backlog should be more than enough - m_sChatText = m_sChatText.Right(10000); - SCREENMAN->SendMessageToTopScreen( SM_AddToChat ); - } - break; - case NSCRSG: //Select Song/Play song - { - m_iSelectMode = m_packet.Read1(); - m_sMainTitle = m_packet.ReadNT(); - m_sArtist = m_packet.ReadNT(); - m_sSubTitle = m_packet.ReadNT(); - SCREENMAN->SendMessageToTopScreen( SM_ChangeSong ); - } - break; - case NSCUUL: - { - /*int ServerMaxPlayers=*/m_packet.Read1(); - int PlayersInThisPacket=m_packet.Read1(); - m_ActivePlayer.clear(); - m_PlayerStatus.clear(); - m_PlayerNames.clear(); - m_ActivePlayers = 0; - for (int i=0; i 0 ) - { - m_ActivePlayers++; - m_ActivePlayer.push_back( i ); - } - m_PlayerStatus.push_back( PStatus ); - m_PlayerNames.push_back( m_packet.ReadNT() ); - } - SCREENMAN->SendMessageToTopScreen( SM_UsersUpdate ); - } - break; - case NSCSMS: - { - RString StyleName, GameName; - GameName = m_packet.ReadNT(); - StyleName = m_packet.ReadNT(); - - GAMESTATE->SetCurGame( GAMEMAN->StringToGame(GameName) ); - GAMESTATE->SetCurrentStyle( GAMEMAN->GameAndStringToStyle(GAMESTATE->m_pCurGame,StyleName) ); - - SCREENMAN->SetNewScreen( "ScreenNetSelectMusic" ); //Should this be metric'd out? - } - break; - case NSCSMOnline: - { - m_SMOnlinePacket.size = packetSize - 1; - m_SMOnlinePacket.Position = 0; - memcpy( m_SMOnlinePacket.Data, (m_packet.Data + 1), packetSize-1 ); - LOG->Trace( "Received SMOnline Command: %d, size:%d", command, packetSize - 1 ); - SCREENMAN->SendMessageToTopScreen( SM_SMOnlinePack ); - } - break; - case NSCAttack: - { - PlayerNumber iPlayerNumber = (PlayerNumber)m_packet.Read1(); - - if( GAMESTATE->IsPlayerEnabled( iPlayerNumber ) ) // Only attack if the player can be attacked. - { - Attack a; - a.fSecsRemaining = float( m_packet.Read4() ) / 1000.0f; - a.bGlobal = false; - a.sModifiers = m_packet.ReadNT(); - GAMESTATE->m_pPlayerState[iPlayerNumber]->LaunchAttack( a ); - } - m_packet.ClearPacket(); - } - break; - } - m_packet.ClearPacket(); - } -} - -bool NetworkSyncManager::ChangedScoreboard(int Column) -{ - if (!m_scoreboardchange[Column]) - return false; - m_scoreboardchange[Column]=false; - return true; -} - -void NetworkSyncManager::SendChat(const RString& message) -{ - m_packet.ClearPacket(); - m_packet.Write1( NSCCM ); - m_packet.WriteNT( message ); - NetPlayerClient->SendPack((char*)&m_packet.Data, m_packet.Position); -} - -void NetworkSyncManager::ReportPlayerOptions() -{ - m_packet.ClearPacket(); - m_packet.Write1( NSCUPOpts ); - FOREACH_PlayerNumber (pn) - m_packet.WriteNT( GAMESTATE->m_pPlayerState[pn]->m_PlayerOptions.GetCurrent().GetString() ); - NetPlayerClient->SendPack((char*)&m_packet.Data, m_packet.Position); -} - -void NetworkSyncManager::SelectUserSong() -{ - m_packet.ClearPacket(); - m_packet.Write1( NSCRSG ); - m_packet.Write1( (uint8_t) m_iSelectMode ); - m_packet.WriteNT( m_sMainTitle ); - m_packet.WriteNT( m_sArtist ); - m_packet.WriteNT( m_sSubTitle ); - NetPlayerClient->SendPack( (char*)&m_packet.Data, m_packet.Position ); -} - -void NetworkSyncManager::SendSMOnline( ) -{ - m_packet.Position = m_SMOnlinePacket.Position + 1; - memcpy( (m_packet.Data + 1), m_SMOnlinePacket.Data, m_SMOnlinePacket.Position ); - m_packet.Data[0] = NSCSMOnline; - NetPlayerClient->SendPack( (char*)&m_packet.Data , m_packet.Position ); -} - -SMOStepType NetworkSyncManager::TranslateStepType(int score) -{ - /* Translate from Stepmania's constantly changing TapNoteScore - * to SMO's note scores */ - switch(score) - { - case TNS_HitMine: - return SMOST_HITMINE; - case TNS_AvoidMine: - return SMOST_AVOIDMINE; - case TNS_Miss: - return SMOST_MISS; - case TNS_W5: - return SMOST_W5; - case TNS_W4: - return SMOST_W4; - case TNS_W3: - return SMOST_W3; - case TNS_W2: - return SMOST_W2; - case TNS_W1: - return SMOST_W1; - case HNS_LetGo+TapNoteScore_Invalid: - return SMOST_LETGO; - case HNS_Held+TapNoteScore_Invalid: - return SMOST_HELD; - default: - return SMOST_UNUSED; - } -} - -// Packet functions -uint8_t PacketFunctions::Read1() -{ - if (Position>=NETMAXBUFFERSIZE) - return 0; - - return Data[Position++]; -} - -uint16_t PacketFunctions::Read2() -{ - if (Position>=NETMAXBUFFERSIZE-1) - return 0; - - uint16_t Temp; - memcpy( &Temp, Data + Position,2 ); - Position+=2; - return ntohs(Temp); -} - -uint32_t PacketFunctions::Read4() -{ - if (Position>=NETMAXBUFFERSIZE-3) - return 0; - - uint32_t Temp; - memcpy( &Temp, Data + Position,4 ); - Position+=4; - return ntohl(Temp); -} - -RString PacketFunctions::ReadNT() -{ - //int Orig=Packet.Position; - RString TempStr; - while ((Position=NETMAXBUFFERSIZE) - return; - memcpy( &Data[Position], &data, 1 ); - ++Position; -} - -void PacketFunctions::Write2(uint16_t data) -{ - if (Position>=NETMAXBUFFERSIZE-1) - return; - data = htons(data); - memcpy( &Data[Position], &data, 2 ); - Position+=2; -} - -void PacketFunctions::Write4(uint32_t data) -{ - if (Position>=NETMAXBUFFERSIZE-3) - return ; - - data = htonl(data); - memcpy( &Data[Position], &data, 4 ); - Position+=4; -} - -void PacketFunctions::WriteNT(const RString& data) -{ - size_t index=0; - while( Position& AllServers ) -{ - AllServers = m_vAllLANServers; -} - -// Aldo: Please move this method to a new class, I didn't want to create new files because I don't know how to properly update the files for each platform. -// I preferred to misplace code rather than cause unneeded headaches to non-windows users, although it would be nice to have in the wiki which files to -// update when adding new files and how (Xcode/stepmania_xcode4.3.xcodeproj has a really crazy structure :/). -#if !defined(HAVE_VERSION_INFO) -unsigned long NetworkSyncManager::GetCurrentSMBuild( LoadingWindow* ld ) { return 0; } -#else -unsigned long NetworkSyncManager::GetCurrentSMBuild( LoadingWindow* ld ) -{ - // Aldo: Using my own host by now, upload update_check/check_sm5.php to an official URL and change the following constants accordingly: - const RString sHost = "aldo.mx"; - const unsigned short uPort = 80; - const RString sResource = "/stepmania/check_sm5.php"; - const RString sUserAgent = PRODUCT_ID; - const RString sReferer = "http://aldo.mx/stepmania/"; - - if( ld ) - { - ld->SetIndeterminate( true ); - ld->SetText("Checking for updates..."); - } - - unsigned long uCurrentSMBuild = version_num; - bool bSuccess = false; - EzSockets* socket = new EzSockets(); - socket->create(); - socket->blocking = true; - - if( socket->connect(sHost, uPort) ) - { - RString sHTTPRequest = ssprintf( - "GET %s HTTP/1.1" "\r\n" - "Host: %s" "\r\n" - "User-Agent: %s" "\r\n" - "Cache-Control: no-cache" "\r\n" - "Referer: %s" "\r\n" - "X-SM-Build: %lu" "\r\n" - "\r\n", - sResource.c_str(), sHost.c_str(), - sUserAgent.c_str(), sReferer.c_str(), - version_num - ); - - socket->SendData(sHTTPRequest); - - // Aldo: EzSocket::pReadData() is a lower level function, I used it because I was having issues - // with EzSocket::ReadData() in 3.9, feel free to refactor this function, the low lever character - // manipulation might look scary to people not used to it. - char* cBuffer = new char[NETMAXBUFFERSIZE]; - // Reading the first NETMAXBUFFERSIZE bytes (usually 1024), should be enough to get the HTTP Header only - int iBytes = socket->pReadData(cBuffer); - if( iBytes ) - { - // \r\n\r\n = Separator from HTTP Header and Body - char* cBodyStart = strstr(cBuffer, "\r\n\r\n"); - if( cBodyStart != NULL ) - { - // Get the HTTP Header only - int iHeaderLength = cBodyStart - cBuffer; - char* cHeader = new char[iHeaderLength+1]; - strncpy( cHeader, cBuffer, iHeaderLength ); - cHeader[iHeaderLength] = '\0'; // needed to make it a valid C String - - RString sHTTPHeader( cHeader ); - SAFE_DELETE( cHeader ); - Trim( sHTTPHeader ); - //LOG->Trace( sHTTPHeader.c_str() ); - - vector svResponse; - split( sHTTPHeader, "\r\n", svResponse ); - - // Check for 200 OK - if( svResponse[0].find("200") != RString::npos ) - { - // Iterate through every field until an X-SM-Build field is found - for( unsigned h=1; hconnect(sHost, uPort) ) - - socket->close(); - SAFE_DELETE( socket ); - - if( ld ) - { - ld->SetIndeterminate(false); - ld->SetTotalWork(100); - - if( bSuccess ) - { - ld->SetProgress(100); - ld->SetText("Checking for updates... OK"); - } - else - { - ld->SetProgress(0); - ld->SetText("Checking for updates... ERROR"); - } - } - - return uCurrentSMBuild; -} -#endif - -static bool ConnectToServer( const RString &t ) -{ - NSMAN->PostStartUp( t ); - NSMAN->DisplayStartupStatus(); - return true; -} - -extern Preference g_sLastServer; - -LuaFunction( ConnectToServer, ConnectToServer( ( RString(SArg(1)).length()==0 ) ? RString(g_sLastServer) : RString(SArg(1) ) ) ) - -#endif - -static bool ReportStyle() { NSMAN->ReportStyle(); return true; } -static bool CloseNetworkConnection() { NSMAN->CloseConnection(); return true; } - -LuaFunction( IsSMOnlineLoggedIn, NSMAN->isSMOLoggedIn[Enum::Check(L, 1)] ) -LuaFunction( IsNetConnected, NSMAN->useSMserver ) -LuaFunction( IsNetSMOnline, NSMAN->isSMOnline ) -LuaFunction( ReportStyle, ReportStyle() ) -LuaFunction( GetServerName, NSMAN->GetServerName() ) -LuaFunction( CloseConnection, CloseNetworkConnection() ) - -/* - * (c) 2003-2004 Charles Lohr, Joshua Allen - * 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. - */ +#include "global.h" +#include "NetworkSyncManager.h" +#include "LuaManager.h" +#include "LocalizedString.h" +#include + +NetworkSyncManager *NSMAN; + +// Aldo: used by GetCurrentSMVersion() +#if defined(HAVE_VERSION_INFO) +extern unsigned long version_num; +#endif + +#if defined(WITHOUT_NETWORKING) +NetworkSyncManager::NetworkSyncManager( LoadingWindow *ld ) { useSMserver=false; isSMOnline = false; } +NetworkSyncManager::~NetworkSyncManager () { } +void NetworkSyncManager::CloseConnection() { } +void NetworkSyncManager::PostStartUp( const RString& ServerIP ) { } +bool NetworkSyncManager::Connect( const RString& addy, unsigned short port ) { return false; } +RString NetworkSyncManager::GetServerName() { return RString(); } +void NetworkSyncManager::ReportNSSOnOff( int i ) { } +void NetworkSyncManager::ReportScore( int playerID, int step, int score, int combo, float offset ) { } +void NetworkSyncManager::ReportSongOver() { } +void NetworkSyncManager::ReportStyle() {} +void NetworkSyncManager::StartRequest( short position ) { } +void NetworkSyncManager::DisplayStartupStatus() { } +void NetworkSyncManager::Update( float fDeltaTime ) { } +bool NetworkSyncManager::ChangedScoreboard( int Column ) { return false; } +void NetworkSyncManager::SendChat( const RString& message ) { } +void NetworkSyncManager::SelectUserSong() { } +RString NetworkSyncManager::MD5Hex( const RString &sInput ) { return RString(); } +int NetworkSyncManager::GetSMOnlineSalt() { return 0; } +void NetworkSyncManager::GetListOfLANServers( vector& AllServers ) { } + #if defined(HAVE_VERSION_INFO) + unsigned long NetworkSyncManager::GetCurrentSMBuild( LoadingWindow* ld ) { return version_num; } + #else + unsigned long NetworkSyncManager::GetCurrentSMBuild( LoadingWindow* ld ) { return 0; } + #endif +#else +#include "ezsockets.h" +#include "ProfileManager.h" +#include "RageLog.h" +#include "ScreenManager.h" +#include "Song.h" +#include "Course.h" +#include "GameState.h" +#include "StatsManager.h" +#include "Steps.h" +#include "ProductInfo.h" +#include "ScreenMessage.h" +#include "GameManager.h" +#include "MessageManager.h" +#include "arch/LoadingWindow/LoadingWindow.h" +#include "PlayerState.h" +#include "CryptManager.h" + +AutoScreenMessage( SM_AddToChat ); +AutoScreenMessage( SM_ChangeSong ); +AutoScreenMessage( SM_GotEval ); +AutoScreenMessage( SM_UsersUpdate ); +AutoScreenMessage( SM_SMOnlinePack ); + +int NetworkSyncManager::GetSMOnlineSalt() +{ + return m_iSalt; +} + +static LocalizedString INITIALIZING_CLIENT_NETWORK ( "NetworkSyncManager", "Initializing Client Network..." ); +NetworkSyncManager::NetworkSyncManager( LoadingWindow *ld ) +{ + LANserver = NULL; //So we know if it has been created yet + BroadcastReception = NULL; + + ld->SetText( INITIALIZING_CLIENT_NETWORK ); + NetPlayerClient = new EzSockets; + NetPlayerClient->blocking = false; + m_ServerVersion = 0; + + useSMserver = false; + isSMOnline = false; + FOREACH_PlayerNumber( pn ) + isSMOLoggedIn[pn] = false; + + m_startupStatus = 0; //By default, connection not tried. + + m_ActivePlayers = 0; + + StartUp(); +} + +NetworkSyncManager::~NetworkSyncManager () +{ + //Close Connection to server nicely. + if( useSMserver ) + NetPlayerClient->close(); + SAFE_DELETE( NetPlayerClient ); + + if ( BroadcastReception ) + { + BroadcastReception->close(); + SAFE_DELETE( BroadcastReception ); + } +} + +void NetworkSyncManager::CloseConnection() +{ + if( !useSMserver ) + return; + m_ServerVersion = 0; + useSMserver = false; + isSMOnline = false; + FOREACH_PlayerNumber( pn ) + isSMOLoggedIn[pn] = false; + m_startupStatus = 0; + NetPlayerClient->close(); +} + +void NetworkSyncManager::PostStartUp( const RString& ServerIP ) +{ + RString sAddress; + unsigned short iPort; + + size_t cLoc = ServerIP.find( ':' ); + if( ServerIP.find( ':' ) != RString::npos ) + { + sAddress = ServerIP.substr( 0, cLoc ); + char* cEnd; + errno = 0; + iPort = (unsigned short)strtol( ServerIP.substr( cLoc + 1 ).c_str(), &cEnd, 10 ); + if( *cEnd != 0 || errno != 0 ) + { + m_startupStatus = 2; + LOG->Warn( "Invalid port" ); + return; + } + } + else + { + iPort = 8765; + sAddress = ServerIP; + } + + LOG->Info( "Attempting to connect to: %s, Port: %i", sAddress.c_str(), iPort ); + + CloseConnection(); + if( !Connect(sAddress.c_str(), iPort) ) + { + m_startupStatus = 2; + LOG->Warn( "Network Sync Manager failed to connect" ); + return; + } + + FOREACH_PlayerNumber( pn ) + isSMOLoggedIn[pn] = false; + + useSMserver = true; + + m_startupStatus = 1; // Connection attepmpt successful + + // If network play is desired and the connection works, + // halt until we know what server version we're dealing with + + m_packet.ClearPacket(); + m_packet.Write1( NSCHello ); //Hello Packet + + m_packet.Write1( NETPROTOCOLVERSION ); + + m_packet.WriteNT( RString(PRODUCT_ID_VER) ); + + /* Block until response is received. + * Move mode to blocking in order to give CPU back to the system, + * and not wait. + */ + + bool dontExit = true; + + NetPlayerClient->blocking = true; + + // The following packet must get through, so we block for it. + // If we are serving, we do not block for this. + NetPlayerClient->SendPack( (char*)m_packet.Data, m_packet.Position ); + + m_packet.ClearPacket(); + + while( dontExit ) + { + m_packet.ClearPacket(); + if( NetPlayerClient->ReadPack((char *)&m_packet, NETMAXBUFFERSIZE)<1 ) + dontExit=false; // Also allow exit if there is a problem on the socket + if( m_packet.Read1() == NSServerOffset + NSCHello ) + dontExit=false; + // Only allow passing on handshake; otherwise scoreboard updates and + // such will confuse us. + } + + NetPlayerClient->blocking = false; + + m_ServerVersion = m_packet.Read1(); + if( m_ServerVersion >= 128 ) + isSMOnline = true; + + m_ServerName = m_packet.ReadNT(); + m_iSalt = m_packet.Read4(); + LOG->Info( "Server Version: %d %s", m_ServerVersion, m_ServerName.c_str() ); +} + + +void NetworkSyncManager::StartUp() +{ + RString ServerIP; + + if( GetCommandlineArgument( "netip", &ServerIP ) ) + PostStartUp( ServerIP ); + + BroadcastReception = new EzSockets; + BroadcastReception->create( IPPROTO_UDP ); + BroadcastReception->bind( 8765 ); + BroadcastReception->blocking = false; +} + + +bool NetworkSyncManager::Connect( const RString& addy, unsigned short port ) +{ + LOG->Info( "Beginning to connect" ); + + NetPlayerClient->create(); // Initialize Socket + LOG->Info( "Calling EzSockets:connect()" ); + useSMserver = NetPlayerClient->connect( addy, port ); + LOG->Info( "Back from ezsockets..." ); + return useSMserver; +} + +void NetworkSyncManager::ReportNSSOnOff(int i) +{ + m_packet.ClearPacket(); + m_packet.Write1( NSCSMS ); + m_packet.Write1( (uint8_t) i ); + NetPlayerClient->SendPack( (char*)m_packet.Data, m_packet.Position ); +} + +RString NetworkSyncManager::GetServerName() +{ + return m_ServerName; +} + +void NetworkSyncManager::ReportScore(int playerID, int step, int score, int combo, float offset) +{ + if( !useSMserver ) //Make sure that we are using the network + return; + + LOG->Trace( "Player ID %i combo = %i", playerID, combo ); + m_packet.ClearPacket(); + + m_packet.Write1( NSCGSU ); + step = TranslateStepType(step); + uint8_t ctr = (uint8_t) (playerID * 16 + step - ( SMOST_HITMINE - 1 ) ); + m_packet.Write1( ctr ); + + ctr = uint8_t( STATSMAN->m_CurStageStats.m_player[playerID].GetGrade()*16 ); + + if ( STATSMAN->m_CurStageStats.m_player[playerID].m_bFailed ) + ctr = uint8_t( 112 ); //Code for failed (failed constant seems not to work) + + m_packet.Write1( ctr ); + m_packet.Write4( score ); + m_packet.Write2( (uint16_t)combo ); + m_packet.Write2( (uint16_t)m_playerLife[playerID] ); + + // Offset Info + // Note: if a 0 is sent, then disregard data. + + // ASSUMED: No step will be more than 16 seconds off center. + // If this assumption is false, read 16 seconds in either direction. + int iOffset = int( (offset+16.384)*2000.0f ); + + if( iOffset>65535 ) + iOffset=65535; + if( iOffset<1 ) + iOffset=1; + + // Report 0 if hold, or miss (don't forget mines should report) + if( step == SMOST_HITMINE || step > SMOST_W1 ) + iOffset = 0; + + m_packet.Write2( (uint16_t)iOffset ); + + NetPlayerClient->SendPack( (char*)m_packet.Data, m_packet.Position ); + +} + +void NetworkSyncManager::ReportSongOver() +{ + if ( !useSMserver ) //Make sure that we are using the network + return; + + m_packet.ClearPacket(); + + m_packet.Write1( NSCGON ); + + NetPlayerClient->SendPack( (char*)&m_packet.Data, m_packet.Position ); + return; +} + +void NetworkSyncManager::ReportStyle() +{ + LOG->Trace( "Sending \"Style\" to server" ); + + if( !useSMserver ) + return; + m_packet.ClearPacket(); + m_packet.Write1( NSCSU ); + m_packet.Write1( (int8_t)GAMESTATE->GetNumPlayersEnabled() ); + + FOREACH_EnabledPlayer( pn ) + { + m_packet.Write1( (uint8_t)pn ); + m_packet.WriteNT( GAMESTATE->GetPlayerDisplayName(pn) ); + } + + NetPlayerClient->SendPack( (char*)&m_packet.Data, m_packet.Position ); +} + +void NetworkSyncManager::StartRequest( short position ) +{ + if( !useSMserver ) + return; + + if( GAMESTATE->m_bDemonstrationOrJukebox ) + return; + + LOG->Trace( "Requesting Start from Server." ); + + m_packet.ClearPacket(); + + m_packet.Write1( NSCGSR ); + + unsigned char ctr=0; + + Steps * tSteps; + tSteps = GAMESTATE->m_pCurSteps[PLAYER_1]; + if( tSteps!=NULL && GAMESTATE->IsPlayerEnabled(PLAYER_1) ) + ctr = uint8_t(ctr+tSteps->GetMeter()*16); + + tSteps = GAMESTATE->m_pCurSteps[PLAYER_2]; + if( tSteps!=NULL && GAMESTATE->IsPlayerEnabled(PLAYER_2) ) + ctr = uint8_t( ctr+tSteps->GetMeter() ); + + m_packet.Write1( ctr ); + + ctr=0; + + tSteps = GAMESTATE->m_pCurSteps[PLAYER_1]; + if( tSteps!=NULL && GAMESTATE->IsPlayerEnabled(PLAYER_1) ) + ctr = uint8_t( ctr + (int)tSteps->GetDifficulty()*16 ); + + tSteps = GAMESTATE->m_pCurSteps[PLAYER_2]; + if( tSteps!=NULL && GAMESTATE->IsPlayerEnabled(PLAYER_2) ) + ctr = uint8_t( ctr + (int)tSteps->GetDifficulty() ); + + m_packet.Write1( ctr ); + + //Notify server if this is for sync or not. + ctr = char( position*16 ); + m_packet.Write1( ctr ); + + if( GAMESTATE->m_pCurSong != nullptr ) + { + m_packet.WriteNT( GAMESTATE->m_pCurSong->m_sMainTitle ); + m_packet.WriteNT( GAMESTATE->m_pCurSong->m_sSubTitle ); + m_packet.WriteNT( GAMESTATE->m_pCurSong->m_sArtist ); + } + else + { + m_packet.WriteNT( "" ); + m_packet.WriteNT( "" ); + m_packet.WriteNT( "" ); + } + + if( GAMESTATE->m_pCurCourse != nullptr ) + m_packet.WriteNT( GAMESTATE->m_pCurCourse->GetDisplayFullTitle() ); + else + m_packet.WriteNT( RString() ); + + //Send Player (and song) Options + m_packet.WriteNT( GAMESTATE->m_SongOptions.GetCurrent().GetString() ); + + int players=0; + FOREACH_PlayerNumber( p ) + { + ++players; + m_packet.WriteNT( GAMESTATE->m_pPlayerState[p]->m_PlayerOptions.GetCurrent().GetString() ); + } + for (int i=0; i<2-players; ++i) + m_packet.WriteNT(""); //Write a NULL if no player + + //This needs to be reset before ScreenEvaluation could possibly be called + m_EvalPlayerData.clear(); + + //Block until go is recieved. + //Switch to blocking mode (this is the only + //way I know how to get precievably instantanious results + + bool dontExit=true; + + NetPlayerClient->blocking=true; + + //The following packet HAS to get through, so we turn blocking on for it as well + //Don't block if we are serving + NetPlayerClient->SendPack((char*)&m_packet.Data, m_packet.Position); + + LOG->Trace("Waiting for RECV"); + + m_packet.ClearPacket(); + + while (dontExit) + { + m_packet.ClearPacket(); + if (NetPlayerClient->ReadPack((char *)&m_packet, NETMAXBUFFERSIZE)<1) + dontExit=false; // Also allow exit if there is a problem on the socket + // Only do if we are not the server, otherwise the sync + // gets hosed up due to non blocking mode. + + if (m_packet.Read1() == (NSServerOffset + NSCGSR)) + dontExit=false; + // Only allow passing on Start request; otherwise scoreboard updates + // and such will confuse us. + + } + NetPlayerClient->blocking=false; + +} + +static LocalizedString CONNECTION_SUCCESSFUL( "NetworkSyncManager", "Connection to '%s' successful." ); +static LocalizedString CONNECTION_FAILED ( "NetworkSyncManager", "Connection failed." ); +void NetworkSyncManager::DisplayStartupStatus() +{ + RString sMessage(""); + + switch (m_startupStatus) + { + case 0: + //Networking wasn't attepmpted + return; + case 1: + sMessage = ssprintf( CONNECTION_SUCCESSFUL.GetValue(), m_ServerName.c_str() ); + break; + case 2: + sMessage = CONNECTION_FAILED.GetValue(); + break; + } + SCREENMAN->SystemMessage( sMessage ); +} + +void NetworkSyncManager::Update(float fDeltaTime) +{ + if (useSMserver) + ProcessInput(); + + PacketFunctions BroadIn; + if ( BroadcastReception->ReadPack( (char*)&BroadIn.Data, 1020 ) ) + { + NetServerInfo ThisServer; + BroadIn.Position = 0; + if ( BroadIn.Read1() == 141 ) // SMLS_Info? + { + ThisServer.Name = BroadIn.ReadNT(); + int port = BroadIn.Read2(); + BroadIn.Read2(); //Num players connected. + uint32_t addy = EzSockets::LongFromAddrIn(BroadcastReception->fromAddr); + ThisServer.Address = ssprintf( "%u.%u.%u.%u:%d", + (addy<<0)>>24, (addy<<8)>>24, (addy<<16)>>24, (addy<<24)>>24, port ); + + //It's fairly safe to assume that users will not be on networks with more than + //30 or 40 servers. Until this point, maps would be slower than vectors. + //So I am going to use a vector to store all of the servers. + // + //In this situation, I will traverse the vector to find the element that + //contains the corresponding server. + + unsigned int i; + for ( i = 0; i < m_vAllLANServers.size(); i++ ) + { + if ( m_vAllLANServers[i].Address == ThisServer.Address ) + { + m_vAllLANServers[i].Name = ThisServer.Name; + break; + } + } + if ( i >= m_vAllLANServers.size() ) + m_vAllLANServers.push_back( ThisServer ); + } + } +} + +static LocalizedString CONNECTION_DROPPED( "NetworkSyncManager", "Connection to server dropped." ); +void NetworkSyncManager::ProcessInput() +{ + //If we're disconnected, just exit + if ((NetPlayerClient->state!=NetPlayerClient->skCONNECTED) || + NetPlayerClient->IsError()) + { + SCREENMAN->SystemMessageNoAnimate( CONNECTION_DROPPED ); + useSMserver=false; + isSMOnline = false; + FOREACH_PlayerNumber(pn) + isSMOLoggedIn[pn] = false; + NetPlayerClient->close(); + return; + } + + // load new data into buffer + NetPlayerClient->update(); + m_packet.ClearPacket(); + + int packetSize; + while ( (packetSize = NetPlayerClient->ReadPack((char *)&m_packet, NETMAXBUFFERSIZE) ) > 0 ) + { + m_packet.size = packetSize; + int command = m_packet.Read1(); + //Check to make sure command is valid from server + if (command < NSServerOffset) + { + LOG->Trace("CMD (below 128) Invalid> %d",command); + break; + } + + command = command - NSServerOffset; + + switch (command) + { + case NSCPing: // Ping packet responce + m_packet.ClearPacket(); + m_packet.Write1( NSCPingR ); + NetPlayerClient->SendPack((char*)m_packet.Data,m_packet.Position); + break; + case NSCPingR: // These are in response to when/if we send packet 0's + case NSCHello: // This is already taken care of by the blocking code earlier + case NSCGSR: // This is taken care of by the blocking start code + break; + case NSCGON: + { + int PlayersInPack = m_packet.Read1(); + m_EvalPlayerData.resize(PlayersInPack); + for (int i=0; iSendMessageToTopScreen( SM_GotEval ); + } + break; + case NSCGSU: // Scoreboard Update + { //Ease scope + int ColumnNumber=m_packet.Read1(); + int NumberPlayers=m_packet.Read1(); + RString ColumnData; + + switch (ColumnNumber) + { + case NSSB_NAMES: + ColumnData = "Names\n"; + for (int i=0; iBroadcast( msg ); + */ + } + break; + case NSCSU: //System message from server + { + RString SysMSG = m_packet.ReadNT(); + SCREENMAN->SystemMessage( SysMSG ); + } + break; + case NSCCM: //Chat message from server + { + m_sChatText += m_packet.ReadNT() + " \n "; + //10000 chars backlog should be more than enough + m_sChatText = m_sChatText.Right(10000); + SCREENMAN->SendMessageToTopScreen( SM_AddToChat ); + } + break; + case NSCRSG: //Select Song/Play song + { + m_iSelectMode = m_packet.Read1(); + m_sMainTitle = m_packet.ReadNT(); + m_sArtist = m_packet.ReadNT(); + m_sSubTitle = m_packet.ReadNT(); + SCREENMAN->SendMessageToTopScreen( SM_ChangeSong ); + } + break; + case NSCUUL: + { + /*int ServerMaxPlayers=*/m_packet.Read1(); + int PlayersInThisPacket=m_packet.Read1(); + m_ActivePlayer.clear(); + m_PlayerStatus.clear(); + m_PlayerNames.clear(); + m_ActivePlayers = 0; + for (int i=0; i 0 ) + { + m_ActivePlayers++; + m_ActivePlayer.push_back( i ); + } + m_PlayerStatus.push_back( PStatus ); + m_PlayerNames.push_back( m_packet.ReadNT() ); + } + SCREENMAN->SendMessageToTopScreen( SM_UsersUpdate ); + } + break; + case NSCSMS: + { + RString StyleName, GameName; + GameName = m_packet.ReadNT(); + StyleName = m_packet.ReadNT(); + + GAMESTATE->SetCurGame( GAMEMAN->StringToGame(GameName) ); + GAMESTATE->SetCurrentStyle( GAMEMAN->GameAndStringToStyle(GAMESTATE->m_pCurGame,StyleName) ); + + SCREENMAN->SetNewScreen( "ScreenNetSelectMusic" ); //Should this be metric'd out? + } + break; + case NSCSMOnline: + { + m_SMOnlinePacket.size = packetSize - 1; + m_SMOnlinePacket.Position = 0; + memcpy( m_SMOnlinePacket.Data, (m_packet.Data + 1), packetSize-1 ); + LOG->Trace( "Received SMOnline Command: %d, size:%d", command, packetSize - 1 ); + SCREENMAN->SendMessageToTopScreen( SM_SMOnlinePack ); + } + break; + case NSCAttack: + { + PlayerNumber iPlayerNumber = (PlayerNumber)m_packet.Read1(); + + if( GAMESTATE->IsPlayerEnabled( iPlayerNumber ) ) // Only attack if the player can be attacked. + { + Attack a; + a.fSecsRemaining = float( m_packet.Read4() ) / 1000.0f; + a.bGlobal = false; + a.sModifiers = m_packet.ReadNT(); + GAMESTATE->m_pPlayerState[iPlayerNumber]->LaunchAttack( a ); + } + m_packet.ClearPacket(); + } + break; + } + m_packet.ClearPacket(); + } +} + +bool NetworkSyncManager::ChangedScoreboard(int Column) +{ + if (!m_scoreboardchange[Column]) + return false; + m_scoreboardchange[Column]=false; + return true; +} + +void NetworkSyncManager::SendChat(const RString& message) +{ + m_packet.ClearPacket(); + m_packet.Write1( NSCCM ); + m_packet.WriteNT( message ); + NetPlayerClient->SendPack((char*)&m_packet.Data, m_packet.Position); +} + +void NetworkSyncManager::ReportPlayerOptions() +{ + m_packet.ClearPacket(); + m_packet.Write1( NSCUPOpts ); + FOREACH_PlayerNumber (pn) + m_packet.WriteNT( GAMESTATE->m_pPlayerState[pn]->m_PlayerOptions.GetCurrent().GetString() ); + NetPlayerClient->SendPack((char*)&m_packet.Data, m_packet.Position); +} + +void NetworkSyncManager::SelectUserSong() +{ + m_packet.ClearPacket(); + m_packet.Write1( NSCRSG ); + m_packet.Write1( (uint8_t) m_iSelectMode ); + m_packet.WriteNT( m_sMainTitle ); + m_packet.WriteNT( m_sArtist ); + m_packet.WriteNT( m_sSubTitle ); + NetPlayerClient->SendPack( (char*)&m_packet.Data, m_packet.Position ); +} + +void NetworkSyncManager::SendSMOnline( ) +{ + m_packet.Position = m_SMOnlinePacket.Position + 1; + memcpy( (m_packet.Data + 1), m_SMOnlinePacket.Data, m_SMOnlinePacket.Position ); + m_packet.Data[0] = NSCSMOnline; + NetPlayerClient->SendPack( (char*)&m_packet.Data , m_packet.Position ); +} + +SMOStepType NetworkSyncManager::TranslateStepType(int score) +{ + /* Translate from Stepmania's constantly changing TapNoteScore + * to SMO's note scores */ + switch(score) + { + case TNS_HitMine: + return SMOST_HITMINE; + case TNS_AvoidMine: + return SMOST_AVOIDMINE; + case TNS_Miss: + return SMOST_MISS; + case TNS_W5: + return SMOST_W5; + case TNS_W4: + return SMOST_W4; + case TNS_W3: + return SMOST_W3; + case TNS_W2: + return SMOST_W2; + case TNS_W1: + return SMOST_W1; + case HNS_LetGo+TapNoteScore_Invalid: + return SMOST_LETGO; + case HNS_Held+TapNoteScore_Invalid: + return SMOST_HELD; + default: + return SMOST_UNUSED; + } +} + +// Packet functions +uint8_t PacketFunctions::Read1() +{ + if (Position>=NETMAXBUFFERSIZE) + return 0; + + return Data[Position++]; +} + +uint16_t PacketFunctions::Read2() +{ + if (Position>=NETMAXBUFFERSIZE-1) + return 0; + + uint16_t Temp; + memcpy( &Temp, Data + Position,2 ); + Position+=2; + return ntohs(Temp); +} + +uint32_t PacketFunctions::Read4() +{ + if (Position>=NETMAXBUFFERSIZE-3) + return 0; + + uint32_t Temp; + memcpy( &Temp, Data + Position,4 ); + Position+=4; + return ntohl(Temp); +} + +RString PacketFunctions::ReadNT() +{ + //int Orig=Packet.Position; + RString TempStr; + while ((Position=NETMAXBUFFERSIZE) + return; + memcpy( &Data[Position], &data, 1 ); + ++Position; +} + +void PacketFunctions::Write2(uint16_t data) +{ + if (Position>=NETMAXBUFFERSIZE-1) + return; + data = htons(data); + memcpy( &Data[Position], &data, 2 ); + Position+=2; +} + +void PacketFunctions::Write4(uint32_t data) +{ + if (Position>=NETMAXBUFFERSIZE-3) + return ; + + data = htonl(data); + memcpy( &Data[Position], &data, 4 ); + Position+=4; +} + +void PacketFunctions::WriteNT(const RString& data) +{ + size_t index=0; + while( Position& AllServers ) +{ + AllServers = m_vAllLANServers; +} + +// Aldo: Please move this method to a new class, I didn't want to create new files because I don't know how to properly update the files for each platform. +// I preferred to misplace code rather than cause unneeded headaches to non-windows users, although it would be nice to have in the wiki which files to +// update when adding new files and how (Xcode/stepmania_xcode4.3.xcodeproj has a really crazy structure :/). +#if !defined(HAVE_VERSION_INFO) +unsigned long NetworkSyncManager::GetCurrentSMBuild( LoadingWindow* ld ) { return 0; } +#else +unsigned long NetworkSyncManager::GetCurrentSMBuild( LoadingWindow* ld ) +{ + // Aldo: Using my own host by now, upload update_check/check_sm5.php to an official URL and change the following constants accordingly: + const RString sHost = "aldo.mx"; + const unsigned short uPort = 80; + const RString sResource = "/stepmania/check_sm5.php"; + const RString sUserAgent = PRODUCT_ID; + const RString sReferer = "http://aldo.mx/stepmania/"; + + if( ld ) + { + ld->SetIndeterminate( true ); + ld->SetText("Checking for updates..."); + } + + unsigned long uCurrentSMBuild = version_num; + bool bSuccess = false; + EzSockets* socket = new EzSockets(); + socket->create(); + socket->blocking = true; + + if( socket->connect(sHost, uPort) ) + { + RString sHTTPRequest = ssprintf( + "GET %s HTTP/1.1" "\r\n" + "Host: %s" "\r\n" + "User-Agent: %s" "\r\n" + "Cache-Control: no-cache" "\r\n" + "Referer: %s" "\r\n" + "X-SM-Build: %lu" "\r\n" + "\r\n", + sResource.c_str(), sHost.c_str(), + sUserAgent.c_str(), sReferer.c_str(), + version_num + ); + + socket->SendData(sHTTPRequest); + + // Aldo: EzSocket::pReadData() is a lower level function, I used it because I was having issues + // with EzSocket::ReadData() in 3.9, feel free to refactor this function, the low lever character + // manipulation might look scary to people not used to it. + char* cBuffer = new char[NETMAXBUFFERSIZE]; + // Reading the first NETMAXBUFFERSIZE bytes (usually 1024), should be enough to get the HTTP Header only + int iBytes = socket->pReadData(cBuffer); + if( iBytes ) + { + // \r\n\r\n = Separator from HTTP Header and Body + char* cBodyStart = strstr(cBuffer, "\r\n\r\n"); + if( cBodyStart != nullptr ) + { + // Get the HTTP Header only + int iHeaderLength = cBodyStart - cBuffer; + char* cHeader = new char[iHeaderLength+1]; + strncpy( cHeader, cBuffer, iHeaderLength ); + cHeader[iHeaderLength] = '\0'; // needed to make it a valid C String + + RString sHTTPHeader( cHeader ); + SAFE_DELETE( cHeader ); + Trim( sHTTPHeader ); + //LOG->Trace( sHTTPHeader.c_str() ); + + vector svResponse; + split( sHTTPHeader, "\r\n", svResponse ); + + // Check for 200 OK + if( svResponse[0].find("200") != RString::npos ) + { + // Iterate through every field until an X-SM-Build field is found + for( unsigned h=1; hconnect(sHost, uPort) ) + + socket->close(); + SAFE_DELETE( socket ); + + if( ld ) + { + ld->SetIndeterminate(false); + ld->SetTotalWork(100); + + if( bSuccess ) + { + ld->SetProgress(100); + ld->SetText("Checking for updates... OK"); + } + else + { + ld->SetProgress(0); + ld->SetText("Checking for updates... ERROR"); + } + } + + return uCurrentSMBuild; +} +#endif + +static bool ConnectToServer( const RString &t ) +{ + NSMAN->PostStartUp( t ); + NSMAN->DisplayStartupStatus(); + return true; +} + +extern Preference g_sLastServer; + +LuaFunction( ConnectToServer, ConnectToServer( ( RString(SArg(1)).length()==0 ) ? RString(g_sLastServer) : RString(SArg(1) ) ) ) + +#endif + +static bool ReportStyle() { NSMAN->ReportStyle(); return true; } +static bool CloseNetworkConnection() { NSMAN->CloseConnection(); return true; } + +LuaFunction( IsSMOnlineLoggedIn, NSMAN->isSMOLoggedIn[Enum::Check(L, 1)] ) +LuaFunction( IsNetConnected, NSMAN->useSMserver ) +LuaFunction( IsNetSMOnline, NSMAN->isSMOnline ) +LuaFunction( ReportStyle, ReportStyle() ) +LuaFunction( GetServerName, NSMAN->GetServerName() ) +LuaFunction( CloseConnection, CloseNetworkConnection() ) + +/* + * (c) 2003-2004 Charles Lohr, Joshua Allen + * 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. + */ diff --git a/src/NoteData.cpp b/src/NoteData.cpp index 33b43908ab..ba92c6a95c 100644 --- a/src/NoteData.cpp +++ b/src/NoteData.cpp @@ -351,7 +351,7 @@ bool NoteData::IsHoldHeadOrBodyAtRow( int iTrack, int iRow, int *pHeadRow ) cons const TapNote &tn = GetTapNote( iTrack, iRow ); if( tn.type == TapNote::hold_head ) { - if( pHeadRow != NULL ) + if( pHeadRow != nullptr ) *pHeadRow = iRow; return true; } diff --git a/src/NoteDisplay.cpp b/src/NoteDisplay.cpp index 9a2d2d7548..eb9eb42d9b 100644 --- a/src/NoteDisplay.cpp +++ b/src/NoteDisplay.cpp @@ -145,7 +145,7 @@ static NoteResource *MakeNoteResource( const RString &sButton, const RString &sE NoteResource *pRes = new NoteResource( nsap ); pRes->m_pActor = NOTESKIN->LoadActor( sButton, sElement, NULL, bSpriteOnly ); - ASSERT( pRes->m_pActor != NULL ); + ASSERT( pRes->m_pActor != nullptr ); g_NoteResource[nsap] = pRes; it = g_NoteResource.find( nsap ); @@ -158,7 +158,7 @@ static NoteResource *MakeNoteResource( const RString &sButton, const RString &sE static void DeleteNoteResource( NoteResource *pRes ) { - ASSERT( pRes != NULL ); + ASSERT( pRes != nullptr ); ASSERT_M( pRes->m_iRefCount > 0, ssprintf("RefCount %i > 0", pRes->m_iRefCount) ); --pRes->m_iRefCount; diff --git a/src/NoteField.cpp b/src/NoteField.cpp index e30c830725..40364bf915 100644 --- a/src/NoteField.cpp +++ b/src/NoteField.cpp @@ -175,7 +175,7 @@ void NoteField::Load( int iDrawDistanceAfterTargetsPixels, int iDrawDistanceBeforeTargetsPixels ) { - ASSERT( pNoteData != NULL ); + ASSERT( pNoteData != nullptr ); m_pNoteData = pNoteData; m_iDrawDistanceAfterTargetsPixels = iDrawDistanceAfterTargetsPixels; m_iDrawDistanceBeforeTargetsPixels = iDrawDistanceBeforeTargetsPixels; @@ -814,7 +814,7 @@ void NoteField::DrawPrimitives() //LOG->Trace( "NoteField::DrawPrimitives()" ); // This should be filled in on the first update. - ASSERT( m_pCurDisplay != NULL ); + ASSERT( m_pCurDisplay != nullptr ); ArrowEffects::Update(); @@ -873,7 +873,7 @@ void NoteField::DrawPrimitives() unsigned i = 0; // Draw beat bars - if( ( GAMESTATE->IsEditing() || SHOW_BEAT_BARS ) && pTiming != NULL ) + if( ( GAMESTATE->IsEditing() || SHOW_BEAT_BARS ) && pTiming != nullptr ) { const vector &tSigs = *segs[SEGMENT_TIME_SIG]; int iMeasureIndex = 0; @@ -913,9 +913,9 @@ void NoteField::DrawPrimitives() } } - if( GAMESTATE->IsEditing() && pTiming != NULL ) + if( GAMESTATE->IsEditing() && pTiming != nullptr ) { - ASSERT(GAMESTATE->m_pCurSong != NULL); + ASSERT(GAMESTATE->m_pCurSong != nullptr); const TimingData &timing = *pTiming; diff --git a/src/NotesLoaderBMS.cpp b/src/NotesLoaderBMS.cpp index ef49a5130c..b1e9c81589 100644 --- a/src/NotesLoaderBMS.cpp +++ b/src/NotesLoaderBMS.cpp @@ -1,1307 +1,1307 @@ -#include "global.h" -#include "NotesLoaderBMS.h" -#include "NoteData.h" -#include "GameConstantsAndTypes.h" -#include "RageLog.h" -#include "GameManager.h" -#include "SongManager.h" -#include "RageFile.h" -#include "SongUtil.h" -#include "StepsUtil.h" -#include "Song.h" -#include "Steps.h" -#include "RageUtil_CharConversions.h" -#include "NoteTypes.h" -#include "NotesLoader.h" -#include "PrefsManager.h" -#include "BackgroundUtil.h" - -/* BMS encoding: tap-hold - * 4&8panel: Player1 Player2 - * Left 11-51 21-61 - * Down 13-53 23-63 - * Up 15-55 25-65 - * Right 16-56 26-66 - * - * 6panel: Player1 - * Left 11-51 - * Left+Up 12-52 - * Down 13-53 - * Up 14-54 - * Up+Right 15-55 - * Right 16-56 - * - * Notice that 15 and 25 have double meanings! What were they thinking??? - * While reading in, use the 6 panel mapping. After reading in, detect if - * only 4 notes are used. If so, shift the Up+Right column back to the Up - * column - * - * BMSes are used for games besides dance and so we're borking up BMSes that are for popn/beat/etc. - * - * popn-nine: 11-15,22-25 - * popn-five: 13-15,21-22 - * beat-single5: 11-16 - * beat-double5: 11-16,21-26 - * beat-single7: 11-16,18-19 - * beat-double7: 11-16,18-19,21-26,28-29 - * - * So the magics for these are: - * popn-nine: nothing >5, with 12, 14, 22 and/or 24 - * popn-five: nothing >5, with 14 and/or 22 - * beat-*: can't tell difference between beat-single and dance-solo - * 18/19 marks beat-single7, 28/29 marks beat-double7 - * beat-double uses 21-26. -*/ - -// Find the largest common substring at the start of both strings. -static RString FindLargestInitialSubstring( const RString &string1, const RString &string2 ) -{ - // First see if the whole first string matches an appropriately-sized - // substring of the second, then keep chopping off the last character of - // each until they match. - unsigned i; - for( i = 0; i < string1.size() && i < string2.size(); ++i ) - if( string1[i] != string2[i] ) - break; - - return string1.substr( 0, i ); -} - -static void SearchForDifficulty( RString sTag, Steps *pOut ) -{ - sTag.MakeLower(); - - // Only match "Light" in parentheses. - if( sTag.find( "(light" ) != sTag.npos ) - { - pOut->SetDifficulty( Difficulty_Easy ); - } - else if( sTag.find( "another" ) != sTag.npos ) - { - pOut->SetDifficulty( Difficulty_Hard ); - } - else if( sTag.find( "(solo)" ) != sTag.npos ) - { - pOut->SetDescription( "Solo" ); - pOut->SetDifficulty( Difficulty_Edit ); - } - - LOG->Trace( "Tag \"%s\" is %s", sTag.c_str(), DifficultyToString(pOut->GetDifficulty()).c_str() ); -} - -static void SlideDuplicateDifficulties( Song &p ) -{ - /* BMS files have to guess the Difficulty from the meter; this is inaccurate, - * and often leads to duplicates. Slide duplicate difficulties upwards. - * We only do this with BMS files, since a very common bug was having *all* - * difficulties slid upwards due to (for example) having two beginner steps. - * We do a second pass in Song::TidyUpData to eliminate any remaining duplicates - * after this. */ - FOREACH_ENUM( StepsType,st ) - { - FOREACH_ENUM( Difficulty, dc ) - { - if( dc == Difficulty_Edit ) - continue; - - vector vSteps; - SongUtil::GetSteps( &p, vSteps, st, dc ); - - StepsUtil::SortNotesArrayByDifficulty( vSteps ); - for( unsigned k=1; kSetDifficulty( dc2 ); - } - } - } -} - -void BMSLoader::GetApplicableFiles( const RString &sPath, vector &out ) -{ - GetDirListing( sPath + RString("*.bms"), out ); - GetDirListing( sPath + RString("*.bme"), out ); - GetDirListing( sPath + RString("*.bml"), out ); - GetDirListing( sPath + RString("*.pms"), out ); -} - -/*===========================================================================*/ - -struct BMSObject -{ - int channel; - int measure; - float position; - bool flag; - RString value; - bool operator<(const BMSObject &other) const - { - if( measure < other.measure ) return true; - if( measure > other.measure ) return false; - if( position < other.position ) return true; - if( position > other.position ) return false; - if( channel == 1 ) return false; - if( other.channel == 1 ) return true; - return channel < other.channel; - } -}; - -struct BMSMeasure -{ - float size; -}; - -typedef map BMSHeaders; -typedef map BMSMeasures; -typedef vector BMSObjects; - -class BMSChart -{ - -public: - BMSChart(); - bool Load( const RString &path ); - bool GetHeader( const RString &header, RString &out ); - RString path; - - BMSObjects objects; - BMSHeaders headers; - BMSMeasures measures; - - void TidyUpData(); -}; - -BMSChart::BMSChart() -{ -} - -bool BMSChart::GetHeader( const RString &header, RString &out ) -{ - if( headers.find(header) == headers.end() ) return false; - out = headers[header]; - return true; -} - -bool BMSChart::Load( const RString &chartPath ) -{ - path = chartPath; - - RageFile file; - if( !file.Open(path) ) - { - LOG->UserLog( "Song file", path, "couldn't be opened: %s", file.GetError().c_str() ); - return false; - } - - while( !file.AtEOF() ) - { - RString line; - if( file.GetLine(line) == -1 ) - { - LOG->UserLog( "Song file", path, "had a read error: %s", file.GetError().c_str() ); - return false; - } - - StripCrnl( line ); - - if( line.size() > 1 && line[0] == '#' ) - { - if( line.size() >= 7 && - ( '0' <= line[1] && line[1] <= '9' ) && - ( '0' <= line[2] && line[2] <= '9' ) && - ( '0' <= line[3] && line[3] <= '9' ) && - ( '0' <= line[4] && line[4] <= '9' ) && - ( '0' <= line[5] && line[5] <= '9' ) && - line[6] == ':' ) - { - RString data = line.substr(7); - int measure = atoi( line.substr(1, 3).c_str() ); - int channel = atoi( line.substr(4, 2).c_str() ); - bool flag = false; - if( channel == 2 ) - { - // special channel: time signature - BMSMeasure m = { StringToFloat(data) }; - this->measures[measure] = m; - } - else - { - if( channel >= 51 ) - { - channel -= 40; - flag = true; - } - int count = data.size() / 2; - for( int i = 0; i < count; i ++ ) - { - RString value = data.substr( 2 * i, 2 ); - if( value != "00" ) - { - value.MakeLower(); - BMSObject o = { channel, measure, (float)i / count, flag, value }; - objects.push_back( o ); - } - } - } - } - else - { - size_t space = line.find(' '); - RString name = line.substr( 0, space ); - RString value = ""; - if( space != line.npos ) - value = line.substr( space+1 ); - name.MakeLower(); - headers[name] = value; - } - } - } - - TidyUpData(); - - return true; -} - -void BMSChart::TidyUpData() -{ - sort( objects.begin(), objects.end() ); -} - -class BMSSong { - - map mapKeysoundToIndex; - Song *out; - - bool backgroundsPrecached; - void PrecacheBackgrounds(const RString &dir); - map mapBackground; - -public: - BMSSong( Song *song ); - int AllocateKeysound( RString filename, RString path ); - bool GetBackground( RString filename, RString path, RString &bgfile ); - Song *GetSong(); -}; - -BMSSong::BMSSong( Song *song ) -{ - out = song; - backgroundsPrecached = false; - - // import existing keysounds from song - for( unsigned i = 0; i < out->m_vsKeysoundFile.size(); i ++ ) - { - mapKeysoundToIndex[out->m_vsKeysoundFile[i]] = i; - } -} - -Song *BMSSong::GetSong() -{ - return out; -} - -int BMSSong::AllocateKeysound( RString filename, RString path ) -{ - if( mapKeysoundToIndex.find( filename ) != mapKeysoundToIndex.end() ) - { - return mapKeysoundToIndex[filename]; - } - - // try to normalize the filename first! - - // FIXME: garbled file names seem to crash the app. - // this might not be the best place to put this code. - if( !utf8_is_valid(filename) ) - return -1; - - /* Due to bugs in some programs, many BMS files have a "WAV" extension - * on files in the BMS for files that actually have some other extension. - * Do a search. Don't do a wildcard search; if sData is "song.wav", - * we might also have "song.png", which we shouldn't match. */ - RString normalizedFilename = filename; - RString dir = out->GetSongDir(); - - if (dir.empty()) - dir = Dirname(path); - - if( !IsAFile(dir + normalizedFilename) ) - { - const char *exts[] = { "oga", "ogg", "wav", "mp3", NULL }; // XXX: stop duplicating these everywhere - for( unsigned i = 0; exts[i] != NULL; ++i ) - { - RString fn = SetExtension( normalizedFilename, exts[i] ); - if( IsAFile(dir + fn) ) - { - normalizedFilename = fn; - break; - } - } - } - - if( !IsAFile(dir + normalizedFilename) ) - { - mapKeysoundToIndex[filename] = -1; - LOG->UserLog( "Song file", dir, "references key \"%s\" that can't be found", normalizedFilename.c_str() ); - return -1; - } - - if( mapKeysoundToIndex.find( normalizedFilename ) != mapKeysoundToIndex.end() ) - { - mapKeysoundToIndex[filename] = mapKeysoundToIndex[normalizedFilename]; - return mapKeysoundToIndex[normalizedFilename]; - } - - unsigned index = out->m_vsKeysoundFile.size(); - out->m_vsKeysoundFile.push_back( normalizedFilename ); - mapKeysoundToIndex[filename] = index; - mapKeysoundToIndex[normalizedFilename] = index; - return index; - -} - -bool BMSSong::GetBackground( RString filename, RString path, RString &bgfile ) -{ - // Check for already tried backgrounds - if( mapBackground.find( filename ) != mapBackground.end() ) - { - RString bg = mapBackground[filename]; - if( bg == "" ) - { - return false; - } - bgfile = bg; - return true; - } - - // FIXME: garbled file names seem to crash the app. - // this might not be the best place to put this code. - if( !utf8_is_valid(filename) ) - return false; - - RString normalizedFilename = filename; - RString dir = out->GetSongDir(); - - if (dir.empty()) - dir = Dirname(path); - - if( !backgroundsPrecached ) - { - PrecacheBackgrounds(dir); - } - - if( !IsAFile(dir + normalizedFilename) ) - { - const char *exts[] = { "ogv", "avi", "mpg", "mpeg", "bmp", "png", "jpeg", NULL }; // XXX: stop duplicating these everywhere - for( unsigned i = 0; exts[i] != NULL; ++i ) - { - RString fn = SetExtension( normalizedFilename, exts[i] ); - if( IsAFile(dir + fn) ) - { - normalizedFilename = fn; - break; - } - } - } - - if( !IsAFile(dir + normalizedFilename) ) - { - mapBackground[filename] = ""; - LOG->UserLog( "Song file", dir, "references bmp \"%s\" that can't be found", normalizedFilename.c_str() ); - return false; - } - - mapBackground[filename] = normalizedFilename; - bgfile = normalizedFilename; - return true; - -} - -void BMSSong::PrecacheBackgrounds(const RString &dir) -{ - if( backgroundsPrecached ) return; - backgroundsPrecached = true; - vector arrayPossibleFiles; - - const char *exts[] = { "ogv", "avi", "mpg", "mpeg", "bmp", "png", "jpeg", NULL }; // XXX: stop duplicating these everywhere - - for( unsigned i = 0; exts[i] != NULL; ++i ) - { - GetDirListing( dir + RString("*.") + RString(exts[i]), arrayPossibleFiles ); - } - - for( unsigned i = 0; i < arrayPossibleFiles.size(); i++ ) - { - for( unsigned j = 0; exts[j] != NULL; ++j ) - { - RString fn = SetExtension( arrayPossibleFiles[i], exts[j] ); - mapBackground[fn] = arrayPossibleFiles[i]; - } - mapBackground[arrayPossibleFiles[i]] = arrayPossibleFiles[i]; - } -} - -struct BMSChartInfo { - RString title; - RString artist; - RString genre; - - RString bannerFile; - RString backgroundFile; - RString stageFile; - RString musicFile; - - map backgroundChanges; -}; - -class BMSChartReader { - - BMSChart *in; - Steps *out; - BMSSong *song; - - void ReadHeaders(); - void CalculateStepsType(); - bool ReadNoteData(); - - StepsType DetermineStepsType(); - - int lntype; - RString lnobj; - - int nonEmptyTracksCount; - map nonEmptyTracks; - - int GetKeysound( const BMSObject &obj ); - - map mapValueToKeysoundIndex; - -public: - BMSChartReader( BMSChart *chart, Steps *steps, BMSSong *song ); - bool Read(); - - Steps *GetSteps(); - - BMSChartInfo info; - int player; - float initialBPM; - -}; - -BMSChartReader::BMSChartReader( BMSChart *chart, Steps *steps, BMSSong *bmsSong ) -{ - this->in = chart; - this->out = steps; - this->song = bmsSong; -} - -bool BMSChartReader::Read() -{ - ReadHeaders(); - CalculateStepsType(); - if( !ReadNoteData() ) return false; - return true; -} - -void BMSChartReader::ReadHeaders() -{ - lntype = 1; - player = 1; - for( BMSHeaders::iterator it = in->headers.begin(); it != in->headers.end(); it ++ ) - { - if( it->first == "#player" ) - { - player = atoi(it->second.c_str()); - } - else if( it->first == "#title" ) - { - info.title = it->second; - } - else if( it->first == "#artist" ) - { - info.artist = it->second; - } - else if( it->first == "#genre" ) - { - info.genre = it->second; - } - else if( it->first == "#banner" ) - { - info.bannerFile = it->second; - } - else if( it->first == "#backbmp" ) - { - /* XXX: don't use #backbmp if StepsType is beat-*. - * incorrectly used in other simulators; see - * http://www.geocities.jp/red_without_right_stick/backbmp/ */ - info.backgroundFile = it->second; - } - else if( it->first == "#stagefile" ) - { - info.stageFile = it->second; - } - else if( it->first == "#wav" ) - { - info.musicFile = it->second; - } - else if( it->first == "#bpm" ) - { - initialBPM = StringToFloat(it->second); - } - else if( it->first == "#lntype" ) - { - int myLntype = atoi(it->second.c_str()); - if( myLntype == 1 ) - { - lntype = myLntype; - // XXX: we only support #LNTYPE 1 for now. - } - } - else if( it->first == "#lnobj" ) - { - lnobj = it->second; - lnobj.MakeLower(); - } - else if( it->first == "#playlevel" ) - { - out->SetMeter( StringToInt(it->second) ); - } - } -} - -void BMSChartReader::CalculateStepsType() -{ - for( unsigned i = 0; i < in->objects.size(); i ++ ) - { - BMSObject &obj = in->objects[i]; - int channel = obj.channel; - if( (11 <= channel && channel <= 19) || (21 <= channel && channel <= 29) ) - { - nonEmptyTracks[channel] = true; - } - } - - nonEmptyTracksCount = nonEmptyTracks.size(); - out->m_StepsType = DetermineStepsType(); -} - -enum BmsRawChannel -{ - BMS_RAW_P1_KEY1 = 11, - BMS_RAW_P1_KEY2 = 12, - BMS_RAW_P1_KEY3 = 13, - BMS_RAW_P1_KEY4 = 14, - BMS_RAW_P1_KEY5 = 15, - BMS_RAW_P1_TURN = 16, - BMS_RAW_P1_KEY6 = 18, - BMS_RAW_P1_KEY7 = 19, - BMS_RAW_P2_KEY1 = 21, - BMS_RAW_P2_KEY2 = 22, - BMS_RAW_P2_KEY3 = 23, - BMS_RAW_P2_KEY4 = 24, - BMS_RAW_P2_KEY5 = 25, - BMS_RAW_P2_TURN = 26, - BMS_RAW_P2_KEY6 = 28, - BMS_RAW_P2_KEY7 = 29 -}; - -StepsType BMSChartReader::DetermineStepsType() -{ - switch( player ) - { - case 1: // "1 player" - switch( nonEmptyTracksCount ) - { - case 4: return StepsType_dance_single; - case 5: - if( nonEmptyTracks.find(BMS_RAW_P2_KEY2) != nonEmptyTracks.end() ) return StepsType_popn_five; - case 6: - // FIXME: There's no way to distinguish between these types. - // They use the same number of tracks. Assume it's a Beat - // type, since they are more common. - //return StepsType_dance_solo; - return StepsType_beat_single5; - case 7: - case 8: return StepsType_beat_single7; - case 9: return StepsType_popn_nine; - // XXX: Some double files doesn't have #player. - case 12: return StepsType_beat_double5; - case 16: return StepsType_beat_double7; - default: - if( nonEmptyTracksCount > 8 ) - return StepsType_beat_double7; - else - return StepsType_beat_single7; - } - case 2: // couple/battle - return StepsType_dance_couple; - case 3: // double - switch( nonEmptyTracksCount ) - { - case 8: return StepsType_dance_double; - case 12: return StepsType_beat_double5; - case 16: return StepsType_beat_double7; - case 5: return StepsType_popn_five; - case 9: return StepsType_popn_nine; - default: return StepsType_beat_double7; - } - default: - LOG->UserLog( "Song file", in->path, "has an invalid #PLAYER value %d.", player ); - return StepsType_Invalid; - } -} - -int BMSChartReader::GetKeysound( const BMSObject &obj ) -{ - map::iterator it = mapValueToKeysoundIndex.find(obj.value); - if( it == mapValueToKeysoundIndex.end() ) - { - int index = -1; - BMSHeaders::iterator iu = in->headers.find("#wav" + obj.value); - if( iu != in->headers.end() ) - { - index = song->AllocateKeysound(iu->second, in->path); - } - mapValueToKeysoundIndex[obj.value] = index; - return index; - } - else - { - return it->second; - } -} - -struct BMSAutoKeysound { - int row; - int index; -}; - -bool BMSChartReader::ReadNoteData() -{ - if( out->m_StepsType == StepsType_Invalid ) - { - LOG->UserLog( "Song file", in->path, "has an unknown steps type" ); - return false; - } - - float currentBPM; - int tracks = GAMEMAN->GetStepsTypeInfo( out->m_StepsType ).iNumTracks; - - NoteData nd; - TimingData td; - - nd.SetNumTracks( tracks ); - td.SetBPMAtRow( 0, currentBPM = initialBPM ); - - // set up note transformation vector. - int *transform = new int[tracks]; - int *holdStart = new int[tracks]; - int *lastNote = new int[tracks]; - - for( int i = 0; i < tracks; i ++ ) holdStart[i] = -1; - for( int i = 0; i < tracks; i ++ ) lastNote[i] = -1; - - switch( out->m_StepsType ) - { - case StepsType_dance_single: - transform[0] = BMS_RAW_P1_KEY1; - transform[1] = BMS_RAW_P1_KEY3; - transform[2] = BMS_RAW_P1_KEY5; - transform[3] = BMS_RAW_P1_TURN; - break; - case StepsType_dance_double: - case StepsType_dance_couple: - transform[0] = BMS_RAW_P1_KEY1; - transform[1] = BMS_RAW_P1_KEY3; - transform[2] = BMS_RAW_P1_KEY5; - transform[3] = BMS_RAW_P1_TURN; - transform[4] = BMS_RAW_P2_KEY1; - transform[5] = BMS_RAW_P2_KEY3; - transform[6] = BMS_RAW_P2_KEY5; - transform[7] = BMS_RAW_P2_TURN; - break; - case StepsType_dance_solo: - case StepsType_beat_single5: - // Hey! Why are these exactly the same? :-) - transform[0] = BMS_RAW_P1_KEY1; - transform[1] = BMS_RAW_P1_KEY2; - transform[2] = BMS_RAW_P1_KEY3; - transform[3] = BMS_RAW_P1_KEY4; - transform[4] = BMS_RAW_P1_KEY5; - transform[5] = BMS_RAW_P1_TURN; - break; - case StepsType_popn_five: - transform[0] = BMS_RAW_P1_KEY3; - transform[1] = BMS_RAW_P1_KEY4; - transform[2] = BMS_RAW_P1_KEY5; - // fix these columns! - transform[3] = BMS_RAW_P2_KEY2; - transform[4] = BMS_RAW_P2_KEY3; - break; - case StepsType_popn_nine: - transform[0] = BMS_RAW_P1_KEY1; // lwhite - transform[1] = BMS_RAW_P1_KEY2; // lyellow - transform[2] = BMS_RAW_P1_KEY3; // lgreen - transform[3] = BMS_RAW_P1_KEY4; // lblue - transform[4] = BMS_RAW_P1_KEY5; // red - // fix these columns! - transform[5] = BMS_RAW_P2_KEY2; // rblue - transform[6] = BMS_RAW_P2_KEY3; // rgreen - transform[7] = BMS_RAW_P2_KEY4; // ryellow - transform[8] = BMS_RAW_P2_KEY5; // rwhite - break; - case StepsType_beat_double5: - transform[0] = BMS_RAW_P1_KEY1; - transform[1] = BMS_RAW_P1_KEY2; - transform[2] = BMS_RAW_P1_KEY3; - transform[3] = BMS_RAW_P1_KEY4; - transform[4] = BMS_RAW_P1_KEY5; - transform[5] = BMS_RAW_P1_TURN; - transform[6] = BMS_RAW_P2_KEY1; - transform[7] = BMS_RAW_P2_KEY2; - transform[8] = BMS_RAW_P2_KEY3; - transform[9] = BMS_RAW_P2_KEY4; - transform[10] = BMS_RAW_P2_KEY5; - transform[11] = BMS_RAW_P2_TURN; - break; - case StepsType_beat_single7: - if( nonEmptyTracks.find(BMS_RAW_P1_KEY7) == nonEmptyTracks.end() - && nonEmptyTracks.find(BMS_RAW_P1_TURN) != nonEmptyTracks.end() ) - { - /* special case for o2mania style charts: - * the turntable is used for first key while the real 7th key is not used. */ - transform[0] = BMS_RAW_P1_TURN; - transform[1] = BMS_RAW_P1_KEY1; - transform[2] = BMS_RAW_P1_KEY2; - transform[3] = BMS_RAW_P1_KEY3; - transform[4] = BMS_RAW_P1_KEY4; - transform[5] = BMS_RAW_P1_KEY5; - transform[6] = BMS_RAW_P1_KEY6; - transform[7] = BMS_RAW_P1_KEY7; - } - else - { - transform[0] = BMS_RAW_P1_KEY1; - transform[1] = BMS_RAW_P1_KEY2; - transform[2] = BMS_RAW_P1_KEY3; - transform[3] = BMS_RAW_P1_KEY4; - transform[4] = BMS_RAW_P1_KEY5; - transform[5] = BMS_RAW_P1_KEY6; - transform[6] = BMS_RAW_P1_KEY7; - transform[7] = BMS_RAW_P1_TURN; - } - break; - case StepsType_beat_double7: - transform[0] = BMS_RAW_P1_KEY1; - transform[1] = BMS_RAW_P1_KEY2; - transform[2] = BMS_RAW_P1_KEY3; - transform[3] = BMS_RAW_P1_KEY4; - transform[4] = BMS_RAW_P1_KEY5; - transform[5] = BMS_RAW_P1_KEY6; - transform[6] = BMS_RAW_P1_KEY7; - transform[7] = BMS_RAW_P1_TURN; - transform[8] = BMS_RAW_P2_KEY1; - transform[9] = BMS_RAW_P2_KEY2; - transform[10] = BMS_RAW_P2_KEY3; - transform[11] = BMS_RAW_P2_KEY4; - transform[12] = BMS_RAW_P2_KEY5; - transform[13] = BMS_RAW_P2_KEY6; - transform[14] = BMS_RAW_P2_KEY7; - transform[15] = BMS_RAW_P2_TURN; - break; - default: - ASSERT_M(0, ssprintf("Invalid StepsType when parsing BMS file %s!", in->path.c_str())); - } - - int reverseTransform[30]; - for( int i = 0; i < 30; i ++ ) reverseTransform[i] = -1; - for( int i = 0; i < tracks; i ++ ) reverseTransform[transform[i]] = i; - - int trackMeasure = -1; - float measureStartBeat = 0.0f; - float measureSize = 0.0f; - float adjustedMeasureSize = 0.0f; - float measureAdjust = 1.0f; - int firstNoteMeasure = 0; - - for( unsigned i = 0; i < in->objects.size(); i ++ ) - { - BMSObject &obj = in->objects[i]; - int channel = obj.channel; - firstNoteMeasure = obj.measure; - if( channel == 3 || channel == 8 || channel == 9 || channel == 1 || (11 <= channel && channel <= 19) || (21 <= channel && channel <= 29) ) - { - break; - } - } - - vector autos; - - for( unsigned i = 0; i < in->objects.size(); i ++ ) - { - BMSObject &obj = in->objects[i]; - while( trackMeasure < obj.measure ) - { - trackMeasure ++; - measureStartBeat += adjustedMeasureSize; - measureSize = 4.0f; - BMSMeasures::iterator it = in->measures.find(trackMeasure); - if( it != in->measures.end() ) measureSize = it->second.size * 4.0f; - adjustedMeasureSize = measureSize; - if( trackMeasure < firstNoteMeasure ) adjustedMeasureSize = measureSize = 4.0f; - - // measure size adjustment - // XXX: need more testing / fine-tuning! - int sixteenths = lrintf(measureSize * 4.0f); - if( sixteenths > 1 ) adjustedMeasureSize = (float)sixteenths / 4.0f; - measureAdjust = adjustedMeasureSize / measureSize; - td.SetBPMAtRow( BeatToNoteRow(measureStartBeat), measureAdjust * currentBPM ); - - { - int num = sixteenths; - int den = 16; - while (den > 4 && num % 2 == 0 && den % 2 == 0) { - num /= 2; - den /= 2; - } - td.SetTimeSignatureAtRow( BeatToNoteRow(measureStartBeat), num, den ); - } - // end measure size adjustment - } - - int row = BeatToNoteRow( measureStartBeat + adjustedMeasureSize * obj.position ); - int channel = obj.channel; - bool hold = obj.flag; - - if( channel == 3 ) // bpm change - { - int bpm; - if( sscanf(obj.value, "%x", &bpm) == 1 ) - { - if( bpm > 0 ) td.SetBPMAtRow( row, measureAdjust * (currentBPM = bpm) ); - } - } - else if( channel == 4 ) // bga change - { - /* - if( !bgaFound ) - { - info.bgaRow = row; - bgaFound = true; - } - */ - RString search = ssprintf( "#bga%s", obj.value.c_str() ); - BMSHeaders::iterator it = in->headers.find( search ); - if( it != in->headers.end() ) - { - // TODO: #BGA isn't supported yet. - } - else - { - search = ssprintf( "#bmp%s", obj.value.c_str() ); - it = in->headers.find( search ); - - RString bg; - if (song->GetBackground(it->second, in->path, bg)) - { - info.backgroundChanges[row] = bg; - } - } - } - else if( channel == 8 ) // bpm change (extended) - { - RString search = ssprintf( "#bpm%s", obj.value.c_str() ); - BMSHeaders::iterator it = in->headers.find( search ); - if( it != in->headers.end() ) - { - td.SetBPMAtRow( row, measureAdjust * (currentBPM = StringToFloat(it->second)) ); - } - else - { - LOG->UserLog( "Song file", in->path.c_str(), "has tag \"%s\" which cannot be found.", search.c_str() ); - } - } - else if( channel == 9 ) // stops - { - RString search = ssprintf( "#stop%s", obj.value.c_str() ); - BMSHeaders::iterator it = in->headers.find( search ); - if( it != in->headers.end() ) - { - td.SetStopAtRow( row, (StringToFloat(it->second) / 48.0f) * (60.0f / currentBPM) ); - } - else - { - LOG->UserLog( "Song file", in->path.c_str(), "has tag \"%s\" which cannot be found.", search.c_str() ); - } - } - else if( channel < 30 && reverseTransform[channel] != -1 ) // player notes! - { - int track = reverseTransform[channel]; - if( holdStart[track] != -1 ) - { - // this object is the end of the hold note. - TapNote tn = nd.GetTapNote(track, holdStart[track]); - tn.type = TapNote::hold_head; - tn.subType = TapNote::hold_head_hold; - nd.AddHoldNote( track, holdStart[track], row, tn ); - holdStart[track] = -1; - lastNote[track] = -1; - } - else if( obj.value == lnobj && lastNote[track] != -1 ) - { - // this object is the end of the hold note. - // lnobj: set last note to hold head. - TapNote tn = nd.GetTapNote(track, lastNote[track]); - tn.type = TapNote::hold_head; - tn.subType = TapNote::hold_head_hold; - nd.AddHoldNote( track, lastNote[track], row, tn ); - holdStart[track] = -1; - lastNote[track] = -1; - } - else - { - TapNote tn = TAP_ORIGINAL_TAP; - tn.iKeysoundIndex = GetKeysound(obj); - nd.SetTapNote( track, row, tn ); - if( hold ) holdStart[track] = row; - lastNote[track] = row; - } - } - else if( channel == 1 || (11 <= channel && channel <= 19) || (21 <= channel && channel <= 29) ) // auto-keysound and other notes - { - BMSAutoKeysound ak = { row, GetKeysound(obj) }; - autos.push_back( ak ); - } - } - - int rowsToLook[3] = { 0, -1, 1 }; - for( unsigned i = 0; i < autos.size(); i ++ ) - { - BMSAutoKeysound &ak = autos[i]; - bool found = false; - for( int j = 0; j < 3; j ++ ) - { - int row = ak.row + rowsToLook[j]; - for( int t = 0; t < tracks; t ++ ) - { - if( nd.GetTapNote( t, row ) == TAP_EMPTY && !nd.IsHoldNoteAtRow( t, row ) ) - { - TapNote tn = TAP_ORIGINAL_AUTO_KEYSOUND; - tn.iKeysoundIndex = ak.index; - nd.SetTapNote( t, row, tn ); - found = true; - break; - } - } - if( found ) break; - } - } - - delete transform; - delete holdStart; - delete lastNote; - - td.TidyUpData( false ); - out->SetNoteData(nd); - out->m_Timing = td; - out->TidyUpData(); - out->SetSavedToDisk( true ); // we're loading from disk, so this is by definintion already saved - - return true; -} - -Steps *BMSChartReader::GetSteps() -{ - return out; -} - -struct BMSStepsInfo { - Steps *steps; - BMSChartInfo info; -}; - -class BMSSongLoader -{ - RString dir; - BMSSong song; - vector loadedSteps; -public: - BMSSongLoader( RString songDir, Song *outSong ); - bool Load( RString fileName ); - void AddToSong(); -}; - -BMSSongLoader::BMSSongLoader( RString songDir, Song *outSong ): dir(songDir), song(outSong) -{ -} - -bool BMSSongLoader::Load( RString fileName ) -{ - // before doing anything else, load the chart first! - BMSChart chart; - if( !chart.Load( dir + fileName ) ) return false; - - // and then read the chart into the steps. - Steps *steps = song.GetSong()->CreateSteps(); - steps->SetFilename( dir + fileName ); - - BMSChartReader reader( &chart, steps, &song ); - if( !reader.Read() ) - { - delete steps; - return false; - } - - // add it to our song - song.GetSong()->AddSteps( steps ); - - // add the chart reader instance to our list. - BMSStepsInfo si = { steps, reader.info }; - loadedSteps.push_back(si); - - return true; - -} - -void BMSSongLoader::AddToSong() -{ - if( loadedSteps.size() == 0 ) - { - return; - } - - RString commonSubstring = ""; - - { - bool found = false; - for( unsigned i = 0; i < loadedSteps.size(); i ++ ) - { - if( loadedSteps[i].info.title == "" ) continue; - if( !found ) - { - commonSubstring = loadedSteps[i].info.title; - found = true; - } - else - { - commonSubstring = FindLargestInitialSubstring( commonSubstring, loadedSteps[i].info.title ); - } - } - if( commonSubstring == "" ) - { - // All bets are off; the titles don't match at all. - // At this rate we're lucky if we even get the title right. - LOG->UserLog( "Song", dir, "has BMS files with inconsistent titles." ); - } - } - - if( commonSubstring == "" ) - { - // As said before, all bets are off. - // From here on in, it's nothing but guesswork. - - // Try to figure out the difficulty of each file. - for( unsigned i = 0; i < loadedSteps.size(); i ++ ) - { - Steps *steps = loadedSteps[i].steps; - steps->SetDifficulty( Difficulty_Medium ); - - RString title = loadedSteps[i].info.title; - - // XXX: Is this really effective if Common Substring parsing failed? - if( title != "" ) SearchForDifficulty( title, steps ); - } - } - else - { - // Now, with our fancy little substring, trim the titles and - // figure out where each goes. - for( unsigned i = 0; i < loadedSteps.size(); i ++ ) - { - Steps *steps = loadedSteps[i].steps; - steps->SetDifficulty( Difficulty_Medium ); - - RString title = loadedSteps[i].info.title; - - if( title != "" && title.size() != commonSubstring.size() ) - { - RString tag = title.substr( commonSubstring.size(), title.size() - commonSubstring.size() ); - tag.MakeLower(); - - // XXX: We should do this with filenames too, I have plenty of examples. - // however, filenames will be trickier, as stuff at the beginning AND - // end change per-file, so we'll need a fancier FindLargestInitialSubstring() - - // XXX: This matches (double), but I haven't seen it used. Again, MORE EXAMPLES NEEDED - if( tag.find('l') != tag.npos ) - { - unsigned pos = tag.find('l'); - if( pos > 2 && tag.substr(pos - 2, 4) == "solo" ) - { - // (solo) -- an edit, apparently (Thanks Glenn!) - steps->SetDifficulty( Difficulty_Edit ); - } - else - { - // Any of [L7] [L14] (LIGHT7) (LIGHT14) (LIGHT) [L] ... you get the idea. - steps->SetDifficulty( Difficulty_Easy ); - } - } - // [A] (A) [ANOTHER] (ANOTHER) (ANOTHER7) Another (DP ANOTHER) (Another) -ANOTHER- [A7] [A14] etc etc etc - else if( tag.find('a') != tag.npos ) - steps->SetDifficulty( Difficulty_Hard ); - // XXX: Can also match (double), but should match [B] or [B7] - else if( tag.find('b') != tag.npos ) - steps->SetDifficulty( Difficulty_Beginner ); - // Other tags I've seen here include (5KEYS) (10KEYS) (7keys) (14keys) (dp) [MIX] [14] (14 Keys Mix) - // XXX: I'm sure [MIX] means something... anyone know? - } - } - } - - /* Prefer to read global tags from a Difficulty_Medium file. These tend to - * have the least cruft in the #TITLE tag, so it's more likely to get a clean - * title. */ - int mainIndex = 0; - for( unsigned i = 0; i < loadedSteps.size(); i ++ ) - if( loadedSteps[i].steps->GetDifficulty() == Difficulty_Medium ) - mainIndex = i; - - Song *out = song.GetSong(); - - { - const BMSStepsInfo &main = loadedSteps[mainIndex]; - out->m_sSongFileName = main.steps->GetFilename(); - if( main.info.title != "" ) - NotesLoader::GetMainAndSubTitlesFromFullTitle( main.info.title, out->m_sMainTitle, out->m_sSubTitle ); - out->m_sArtist = main.info.artist; - out->m_sGenre = main.info.genre; - out->m_sBannerFile = main.info.bannerFile; - - switch( main.steps->m_StepsType ) - { - case StepsType_beat_single5: - case StepsType_beat_single7: - case StepsType_beat_double5: - case StepsType_beat_double7: - case StepsType_beat_versus5: - case StepsType_beat_versus7: - out->m_sBackgroundFile = main.info.stageFile; - break; - default: - if ( main.info.backgroundFile != "" ) - out->m_sBackgroundFile = main.info.backgroundFile; - else - out->m_sBackgroundFile = main.info.stageFile; - break; - } - - map::const_iterator it = main.info.backgroundChanges.begin(); - - for (; it != main.info.backgroundChanges.end(); it++) - { - out->AddBackgroundChange(BACKGROUND_LAYER_1, - BackgroundChange(NoteRowToBeat(it->first), - it->second, - "", - 1.f, - SBE_StretchNoLoop)); - } - - out->m_sMusicFile = main.info.musicFile; - out->m_SongTiming = main.steps->m_Timing; - } - - // The brackets before the difficulty are in common substring, so remove them if it's found. - if( commonSubstring.size() > 2 && commonSubstring[commonSubstring.size() - 2] == ' ' ) - { - switch( commonSubstring[commonSubstring.size() - 1] ) - { - case '[': - case '(': - case '<': - commonSubstring = commonSubstring.substr(0, commonSubstring.size() - 2); - } - } - - // Override what that global tag said about the title if we have a good substring. - // Prevents clobbering and catches "MySong (7keys)" / "MySong (Another) (7keys)" - // Also catches "MySong (7keys)" / "MySong (14keys)" - if( commonSubstring != "" ) - NotesLoader::GetMainAndSubTitlesFromFullTitle( commonSubstring, out->m_sMainTitle, out->m_sSubTitle ); - - SlideDuplicateDifficulties( *out ); - - ConvertString( out->m_sMainTitle, "utf-8,japanese" ); - ConvertString( out->m_sArtist, "utf-8,japanese" ); - ConvertString( out->m_sGenre, "utf-8,japanese" ); - -} - -/*===========================================================================*/ - -bool BMSLoader::LoadNoteDataFromSimfile( const RString & cachePath, Steps & out ) -{ - Song *pSong = out.m_pSong; - - // before doing anything else, load the chart first! - BMSChart chart; - if( !chart.Load( cachePath ) ) return false; - - BMSSong song(pSong); - - BMSChartReader reader( &chart, &out, &song ); - if( !reader.Read() ) return false; - - return true; -} - -bool BMSLoader::LoadFromDir( const RString &sDir, Song &out ) -{ - LOG->Trace( "Song::LoadFromBMSDir(%s)", sDir.c_str() ); - - ASSERT( out.m_vsKeysoundFile.empty() ); - - vector arrayBMSFileNames; - GetApplicableFiles( sDir, arrayBMSFileNames ); - - /* We should have at least one; if we had none, we shouldn't have been - * called to begin with. */ - ASSERT( arrayBMSFileNames.size() != 0 ); - - BMSSongLoader loader( sDir, &out ); - for( unsigned i=0; i5, with 12, 14, 22 and/or 24 + * popn-five: nothing >5, with 14 and/or 22 + * beat-*: can't tell difference between beat-single and dance-solo + * 18/19 marks beat-single7, 28/29 marks beat-double7 + * beat-double uses 21-26. +*/ + +// Find the largest common substring at the start of both strings. +static RString FindLargestInitialSubstring( const RString &string1, const RString &string2 ) +{ + // First see if the whole first string matches an appropriately-sized + // substring of the second, then keep chopping off the last character of + // each until they match. + unsigned i; + for( i = 0; i < string1.size() && i < string2.size(); ++i ) + if( string1[i] != string2[i] ) + break; + + return string1.substr( 0, i ); +} + +static void SearchForDifficulty( RString sTag, Steps *pOut ) +{ + sTag.MakeLower(); + + // Only match "Light" in parentheses. + if( sTag.find( "(light" ) != sTag.npos ) + { + pOut->SetDifficulty( Difficulty_Easy ); + } + else if( sTag.find( "another" ) != sTag.npos ) + { + pOut->SetDifficulty( Difficulty_Hard ); + } + else if( sTag.find( "(solo)" ) != sTag.npos ) + { + pOut->SetDescription( "Solo" ); + pOut->SetDifficulty( Difficulty_Edit ); + } + + LOG->Trace( "Tag \"%s\" is %s", sTag.c_str(), DifficultyToString(pOut->GetDifficulty()).c_str() ); +} + +static void SlideDuplicateDifficulties( Song &p ) +{ + /* BMS files have to guess the Difficulty from the meter; this is inaccurate, + * and often leads to duplicates. Slide duplicate difficulties upwards. + * We only do this with BMS files, since a very common bug was having *all* + * difficulties slid upwards due to (for example) having two beginner steps. + * We do a second pass in Song::TidyUpData to eliminate any remaining duplicates + * after this. */ + FOREACH_ENUM( StepsType,st ) + { + FOREACH_ENUM( Difficulty, dc ) + { + if( dc == Difficulty_Edit ) + continue; + + vector vSteps; + SongUtil::GetSteps( &p, vSteps, st, dc ); + + StepsUtil::SortNotesArrayByDifficulty( vSteps ); + for( unsigned k=1; kSetDifficulty( dc2 ); + } + } + } +} + +void BMSLoader::GetApplicableFiles( const RString &sPath, vector &out ) +{ + GetDirListing( sPath + RString("*.bms"), out ); + GetDirListing( sPath + RString("*.bme"), out ); + GetDirListing( sPath + RString("*.bml"), out ); + GetDirListing( sPath + RString("*.pms"), out ); +} + +/*===========================================================================*/ + +struct BMSObject +{ + int channel; + int measure; + float position; + bool flag; + RString value; + bool operator<(const BMSObject &other) const + { + if( measure < other.measure ) return true; + if( measure > other.measure ) return false; + if( position < other.position ) return true; + if( position > other.position ) return false; + if( channel == 1 ) return false; + if( other.channel == 1 ) return true; + return channel < other.channel; + } +}; + +struct BMSMeasure +{ + float size; +}; + +typedef map BMSHeaders; +typedef map BMSMeasures; +typedef vector BMSObjects; + +class BMSChart +{ + +public: + BMSChart(); + bool Load( const RString &path ); + bool GetHeader( const RString &header, RString &out ); + RString path; + + BMSObjects objects; + BMSHeaders headers; + BMSMeasures measures; + + void TidyUpData(); +}; + +BMSChart::BMSChart() +{ +} + +bool BMSChart::GetHeader( const RString &header, RString &out ) +{ + if( headers.find(header) == headers.end() ) return false; + out = headers[header]; + return true; +} + +bool BMSChart::Load( const RString &chartPath ) +{ + path = chartPath; + + RageFile file; + if( !file.Open(path) ) + { + LOG->UserLog( "Song file", path, "couldn't be opened: %s", file.GetError().c_str() ); + return false; + } + + while( !file.AtEOF() ) + { + RString line; + if( file.GetLine(line) == -1 ) + { + LOG->UserLog( "Song file", path, "had a read error: %s", file.GetError().c_str() ); + return false; + } + + StripCrnl( line ); + + if( line.size() > 1 && line[0] == '#' ) + { + if( line.size() >= 7 && + ( '0' <= line[1] && line[1] <= '9' ) && + ( '0' <= line[2] && line[2] <= '9' ) && + ( '0' <= line[3] && line[3] <= '9' ) && + ( '0' <= line[4] && line[4] <= '9' ) && + ( '0' <= line[5] && line[5] <= '9' ) && + line[6] == ':' ) + { + RString data = line.substr(7); + int measure = atoi( line.substr(1, 3).c_str() ); + int channel = atoi( line.substr(4, 2).c_str() ); + bool flag = false; + if( channel == 2 ) + { + // special channel: time signature + BMSMeasure m = { StringToFloat(data) }; + this->measures[measure] = m; + } + else + { + if( channel >= 51 ) + { + channel -= 40; + flag = true; + } + int count = data.size() / 2; + for( int i = 0; i < count; i ++ ) + { + RString value = data.substr( 2 * i, 2 ); + if( value != "00" ) + { + value.MakeLower(); + BMSObject o = { channel, measure, (float)i / count, flag, value }; + objects.push_back( o ); + } + } + } + } + else + { + size_t space = line.find(' '); + RString name = line.substr( 0, space ); + RString value = ""; + if( space != line.npos ) + value = line.substr( space+1 ); + name.MakeLower(); + headers[name] = value; + } + } + } + + TidyUpData(); + + return true; +} + +void BMSChart::TidyUpData() +{ + sort( objects.begin(), objects.end() ); +} + +class BMSSong { + + map mapKeysoundToIndex; + Song *out; + + bool backgroundsPrecached; + void PrecacheBackgrounds(const RString &dir); + map mapBackground; + +public: + BMSSong( Song *song ); + int AllocateKeysound( RString filename, RString path ); + bool GetBackground( RString filename, RString path, RString &bgfile ); + Song *GetSong(); +}; + +BMSSong::BMSSong( Song *song ) +{ + out = song; + backgroundsPrecached = false; + + // import existing keysounds from song + for( unsigned i = 0; i < out->m_vsKeysoundFile.size(); i ++ ) + { + mapKeysoundToIndex[out->m_vsKeysoundFile[i]] = i; + } +} + +Song *BMSSong::GetSong() +{ + return out; +} + +int BMSSong::AllocateKeysound( RString filename, RString path ) +{ + if( mapKeysoundToIndex.find( filename ) != mapKeysoundToIndex.end() ) + { + return mapKeysoundToIndex[filename]; + } + + // try to normalize the filename first! + + // FIXME: garbled file names seem to crash the app. + // this might not be the best place to put this code. + if( !utf8_is_valid(filename) ) + return -1; + + /* Due to bugs in some programs, many BMS files have a "WAV" extension + * on files in the BMS for files that actually have some other extension. + * Do a search. Don't do a wildcard search; if sData is "song.wav", + * we might also have "song.png", which we shouldn't match. */ + RString normalizedFilename = filename; + RString dir = out->GetSongDir(); + + if (dir.empty()) + dir = Dirname(path); + + if( !IsAFile(dir + normalizedFilename) ) + { + const char *exts[] = { "oga", "ogg", "wav", "mp3", NULL }; // XXX: stop duplicating these everywhere + for( unsigned i = 0; exts[i] != nullptr; ++i ) + { + RString fn = SetExtension( normalizedFilename, exts[i] ); + if( IsAFile(dir + fn) ) + { + normalizedFilename = fn; + break; + } + } + } + + if( !IsAFile(dir + normalizedFilename) ) + { + mapKeysoundToIndex[filename] = -1; + LOG->UserLog( "Song file", dir, "references key \"%s\" that can't be found", normalizedFilename.c_str() ); + return -1; + } + + if( mapKeysoundToIndex.find( normalizedFilename ) != mapKeysoundToIndex.end() ) + { + mapKeysoundToIndex[filename] = mapKeysoundToIndex[normalizedFilename]; + return mapKeysoundToIndex[normalizedFilename]; + } + + unsigned index = out->m_vsKeysoundFile.size(); + out->m_vsKeysoundFile.push_back( normalizedFilename ); + mapKeysoundToIndex[filename] = index; + mapKeysoundToIndex[normalizedFilename] = index; + return index; + +} + +bool BMSSong::GetBackground( RString filename, RString path, RString &bgfile ) +{ + // Check for already tried backgrounds + if( mapBackground.find( filename ) != mapBackground.end() ) + { + RString bg = mapBackground[filename]; + if( bg == "" ) + { + return false; + } + bgfile = bg; + return true; + } + + // FIXME: garbled file names seem to crash the app. + // this might not be the best place to put this code. + if( !utf8_is_valid(filename) ) + return false; + + RString normalizedFilename = filename; + RString dir = out->GetSongDir(); + + if (dir.empty()) + dir = Dirname(path); + + if( !backgroundsPrecached ) + { + PrecacheBackgrounds(dir); + } + + if( !IsAFile(dir + normalizedFilename) ) + { + const char *exts[] = { "ogv", "avi", "mpg", "mpeg", "bmp", "png", "jpeg", NULL }; // XXX: stop duplicating these everywhere + for( unsigned i = 0; exts[i] != nullptr; ++i ) + { + RString fn = SetExtension( normalizedFilename, exts[i] ); + if( IsAFile(dir + fn) ) + { + normalizedFilename = fn; + break; + } + } + } + + if( !IsAFile(dir + normalizedFilename) ) + { + mapBackground[filename] = ""; + LOG->UserLog( "Song file", dir, "references bmp \"%s\" that can't be found", normalizedFilename.c_str() ); + return false; + } + + mapBackground[filename] = normalizedFilename; + bgfile = normalizedFilename; + return true; + +} + +void BMSSong::PrecacheBackgrounds(const RString &dir) +{ + if( backgroundsPrecached ) return; + backgroundsPrecached = true; + vector arrayPossibleFiles; + + const char *exts[] = { "ogv", "avi", "mpg", "mpeg", "bmp", "png", "jpeg", NULL }; // XXX: stop duplicating these everywhere + + for( unsigned i = 0; exts[i] != nullptr; ++i ) + { + GetDirListing( dir + RString("*.") + RString(exts[i]), arrayPossibleFiles ); + } + + for( unsigned i = 0; i < arrayPossibleFiles.size(); i++ ) + { + for( unsigned j = 0; exts[j] != nullptr; ++j ) + { + RString fn = SetExtension( arrayPossibleFiles[i], exts[j] ); + mapBackground[fn] = arrayPossibleFiles[i]; + } + mapBackground[arrayPossibleFiles[i]] = arrayPossibleFiles[i]; + } +} + +struct BMSChartInfo { + RString title; + RString artist; + RString genre; + + RString bannerFile; + RString backgroundFile; + RString stageFile; + RString musicFile; + + map backgroundChanges; +}; + +class BMSChartReader { + + BMSChart *in; + Steps *out; + BMSSong *song; + + void ReadHeaders(); + void CalculateStepsType(); + bool ReadNoteData(); + + StepsType DetermineStepsType(); + + int lntype; + RString lnobj; + + int nonEmptyTracksCount; + map nonEmptyTracks; + + int GetKeysound( const BMSObject &obj ); + + map mapValueToKeysoundIndex; + +public: + BMSChartReader( BMSChart *chart, Steps *steps, BMSSong *song ); + bool Read(); + + Steps *GetSteps(); + + BMSChartInfo info; + int player; + float initialBPM; + +}; + +BMSChartReader::BMSChartReader( BMSChart *chart, Steps *steps, BMSSong *bmsSong ) +{ + this->in = chart; + this->out = steps; + this->song = bmsSong; +} + +bool BMSChartReader::Read() +{ + ReadHeaders(); + CalculateStepsType(); + if( !ReadNoteData() ) return false; + return true; +} + +void BMSChartReader::ReadHeaders() +{ + lntype = 1; + player = 1; + for( BMSHeaders::iterator it = in->headers.begin(); it != in->headers.end(); it ++ ) + { + if( it->first == "#player" ) + { + player = atoi(it->second.c_str()); + } + else if( it->first == "#title" ) + { + info.title = it->second; + } + else if( it->first == "#artist" ) + { + info.artist = it->second; + } + else if( it->first == "#genre" ) + { + info.genre = it->second; + } + else if( it->first == "#banner" ) + { + info.bannerFile = it->second; + } + else if( it->first == "#backbmp" ) + { + /* XXX: don't use #backbmp if StepsType is beat-*. + * incorrectly used in other simulators; see + * http://www.geocities.jp/red_without_right_stick/backbmp/ */ + info.backgroundFile = it->second; + } + else if( it->first == "#stagefile" ) + { + info.stageFile = it->second; + } + else if( it->first == "#wav" ) + { + info.musicFile = it->second; + } + else if( it->first == "#bpm" ) + { + initialBPM = StringToFloat(it->second); + } + else if( it->first == "#lntype" ) + { + int myLntype = atoi(it->second.c_str()); + if( myLntype == 1 ) + { + lntype = myLntype; + // XXX: we only support #LNTYPE 1 for now. + } + } + else if( it->first == "#lnobj" ) + { + lnobj = it->second; + lnobj.MakeLower(); + } + else if( it->first == "#playlevel" ) + { + out->SetMeter( StringToInt(it->second) ); + } + } +} + +void BMSChartReader::CalculateStepsType() +{ + for( unsigned i = 0; i < in->objects.size(); i ++ ) + { + BMSObject &obj = in->objects[i]; + int channel = obj.channel; + if( (11 <= channel && channel <= 19) || (21 <= channel && channel <= 29) ) + { + nonEmptyTracks[channel] = true; + } + } + + nonEmptyTracksCount = nonEmptyTracks.size(); + out->m_StepsType = DetermineStepsType(); +} + +enum BmsRawChannel +{ + BMS_RAW_P1_KEY1 = 11, + BMS_RAW_P1_KEY2 = 12, + BMS_RAW_P1_KEY3 = 13, + BMS_RAW_P1_KEY4 = 14, + BMS_RAW_P1_KEY5 = 15, + BMS_RAW_P1_TURN = 16, + BMS_RAW_P1_KEY6 = 18, + BMS_RAW_P1_KEY7 = 19, + BMS_RAW_P2_KEY1 = 21, + BMS_RAW_P2_KEY2 = 22, + BMS_RAW_P2_KEY3 = 23, + BMS_RAW_P2_KEY4 = 24, + BMS_RAW_P2_KEY5 = 25, + BMS_RAW_P2_TURN = 26, + BMS_RAW_P2_KEY6 = 28, + BMS_RAW_P2_KEY7 = 29 +}; + +StepsType BMSChartReader::DetermineStepsType() +{ + switch( player ) + { + case 1: // "1 player" + switch( nonEmptyTracksCount ) + { + case 4: return StepsType_dance_single; + case 5: + if( nonEmptyTracks.find(BMS_RAW_P2_KEY2) != nonEmptyTracks.end() ) return StepsType_popn_five; + case 6: + // FIXME: There's no way to distinguish between these types. + // They use the same number of tracks. Assume it's a Beat + // type, since they are more common. + //return StepsType_dance_solo; + return StepsType_beat_single5; + case 7: + case 8: return StepsType_beat_single7; + case 9: return StepsType_popn_nine; + // XXX: Some double files doesn't have #player. + case 12: return StepsType_beat_double5; + case 16: return StepsType_beat_double7; + default: + if( nonEmptyTracksCount > 8 ) + return StepsType_beat_double7; + else + return StepsType_beat_single7; + } + case 2: // couple/battle + return StepsType_dance_couple; + case 3: // double + switch( nonEmptyTracksCount ) + { + case 8: return StepsType_dance_double; + case 12: return StepsType_beat_double5; + case 16: return StepsType_beat_double7; + case 5: return StepsType_popn_five; + case 9: return StepsType_popn_nine; + default: return StepsType_beat_double7; + } + default: + LOG->UserLog( "Song file", in->path, "has an invalid #PLAYER value %d.", player ); + return StepsType_Invalid; + } +} + +int BMSChartReader::GetKeysound( const BMSObject &obj ) +{ + map::iterator it = mapValueToKeysoundIndex.find(obj.value); + if( it == mapValueToKeysoundIndex.end() ) + { + int index = -1; + BMSHeaders::iterator iu = in->headers.find("#wav" + obj.value); + if( iu != in->headers.end() ) + { + index = song->AllocateKeysound(iu->second, in->path); + } + mapValueToKeysoundIndex[obj.value] = index; + return index; + } + else + { + return it->second; + } +} + +struct BMSAutoKeysound { + int row; + int index; +}; + +bool BMSChartReader::ReadNoteData() +{ + if( out->m_StepsType == StepsType_Invalid ) + { + LOG->UserLog( "Song file", in->path, "has an unknown steps type" ); + return false; + } + + float currentBPM; + int tracks = GAMEMAN->GetStepsTypeInfo( out->m_StepsType ).iNumTracks; + + NoteData nd; + TimingData td; + + nd.SetNumTracks( tracks ); + td.SetBPMAtRow( 0, currentBPM = initialBPM ); + + // set up note transformation vector. + int *transform = new int[tracks]; + int *holdStart = new int[tracks]; + int *lastNote = new int[tracks]; + + for( int i = 0; i < tracks; i ++ ) holdStart[i] = -1; + for( int i = 0; i < tracks; i ++ ) lastNote[i] = -1; + + switch( out->m_StepsType ) + { + case StepsType_dance_single: + transform[0] = BMS_RAW_P1_KEY1; + transform[1] = BMS_RAW_P1_KEY3; + transform[2] = BMS_RAW_P1_KEY5; + transform[3] = BMS_RAW_P1_TURN; + break; + case StepsType_dance_double: + case StepsType_dance_couple: + transform[0] = BMS_RAW_P1_KEY1; + transform[1] = BMS_RAW_P1_KEY3; + transform[2] = BMS_RAW_P1_KEY5; + transform[3] = BMS_RAW_P1_TURN; + transform[4] = BMS_RAW_P2_KEY1; + transform[5] = BMS_RAW_P2_KEY3; + transform[6] = BMS_RAW_P2_KEY5; + transform[7] = BMS_RAW_P2_TURN; + break; + case StepsType_dance_solo: + case StepsType_beat_single5: + // Hey! Why are these exactly the same? :-) + transform[0] = BMS_RAW_P1_KEY1; + transform[1] = BMS_RAW_P1_KEY2; + transform[2] = BMS_RAW_P1_KEY3; + transform[3] = BMS_RAW_P1_KEY4; + transform[4] = BMS_RAW_P1_KEY5; + transform[5] = BMS_RAW_P1_TURN; + break; + case StepsType_popn_five: + transform[0] = BMS_RAW_P1_KEY3; + transform[1] = BMS_RAW_P1_KEY4; + transform[2] = BMS_RAW_P1_KEY5; + // fix these columns! + transform[3] = BMS_RAW_P2_KEY2; + transform[4] = BMS_RAW_P2_KEY3; + break; + case StepsType_popn_nine: + transform[0] = BMS_RAW_P1_KEY1; // lwhite + transform[1] = BMS_RAW_P1_KEY2; // lyellow + transform[2] = BMS_RAW_P1_KEY3; // lgreen + transform[3] = BMS_RAW_P1_KEY4; // lblue + transform[4] = BMS_RAW_P1_KEY5; // red + // fix these columns! + transform[5] = BMS_RAW_P2_KEY2; // rblue + transform[6] = BMS_RAW_P2_KEY3; // rgreen + transform[7] = BMS_RAW_P2_KEY4; // ryellow + transform[8] = BMS_RAW_P2_KEY5; // rwhite + break; + case StepsType_beat_double5: + transform[0] = BMS_RAW_P1_KEY1; + transform[1] = BMS_RAW_P1_KEY2; + transform[2] = BMS_RAW_P1_KEY3; + transform[3] = BMS_RAW_P1_KEY4; + transform[4] = BMS_RAW_P1_KEY5; + transform[5] = BMS_RAW_P1_TURN; + transform[6] = BMS_RAW_P2_KEY1; + transform[7] = BMS_RAW_P2_KEY2; + transform[8] = BMS_RAW_P2_KEY3; + transform[9] = BMS_RAW_P2_KEY4; + transform[10] = BMS_RAW_P2_KEY5; + transform[11] = BMS_RAW_P2_TURN; + break; + case StepsType_beat_single7: + if( nonEmptyTracks.find(BMS_RAW_P1_KEY7) == nonEmptyTracks.end() + && nonEmptyTracks.find(BMS_RAW_P1_TURN) != nonEmptyTracks.end() ) + { + /* special case for o2mania style charts: + * the turntable is used for first key while the real 7th key is not used. */ + transform[0] = BMS_RAW_P1_TURN; + transform[1] = BMS_RAW_P1_KEY1; + transform[2] = BMS_RAW_P1_KEY2; + transform[3] = BMS_RAW_P1_KEY3; + transform[4] = BMS_RAW_P1_KEY4; + transform[5] = BMS_RAW_P1_KEY5; + transform[6] = BMS_RAW_P1_KEY6; + transform[7] = BMS_RAW_P1_KEY7; + } + else + { + transform[0] = BMS_RAW_P1_KEY1; + transform[1] = BMS_RAW_P1_KEY2; + transform[2] = BMS_RAW_P1_KEY3; + transform[3] = BMS_RAW_P1_KEY4; + transform[4] = BMS_RAW_P1_KEY5; + transform[5] = BMS_RAW_P1_KEY6; + transform[6] = BMS_RAW_P1_KEY7; + transform[7] = BMS_RAW_P1_TURN; + } + break; + case StepsType_beat_double7: + transform[0] = BMS_RAW_P1_KEY1; + transform[1] = BMS_RAW_P1_KEY2; + transform[2] = BMS_RAW_P1_KEY3; + transform[3] = BMS_RAW_P1_KEY4; + transform[4] = BMS_RAW_P1_KEY5; + transform[5] = BMS_RAW_P1_KEY6; + transform[6] = BMS_RAW_P1_KEY7; + transform[7] = BMS_RAW_P1_TURN; + transform[8] = BMS_RAW_P2_KEY1; + transform[9] = BMS_RAW_P2_KEY2; + transform[10] = BMS_RAW_P2_KEY3; + transform[11] = BMS_RAW_P2_KEY4; + transform[12] = BMS_RAW_P2_KEY5; + transform[13] = BMS_RAW_P2_KEY6; + transform[14] = BMS_RAW_P2_KEY7; + transform[15] = BMS_RAW_P2_TURN; + break; + default: + ASSERT_M(0, ssprintf("Invalid StepsType when parsing BMS file %s!", in->path.c_str())); + } + + int reverseTransform[30]; + for( int i = 0; i < 30; i ++ ) reverseTransform[i] = -1; + for( int i = 0; i < tracks; i ++ ) reverseTransform[transform[i]] = i; + + int trackMeasure = -1; + float measureStartBeat = 0.0f; + float measureSize = 0.0f; + float adjustedMeasureSize = 0.0f; + float measureAdjust = 1.0f; + int firstNoteMeasure = 0; + + for( unsigned i = 0; i < in->objects.size(); i ++ ) + { + BMSObject &obj = in->objects[i]; + int channel = obj.channel; + firstNoteMeasure = obj.measure; + if( channel == 3 || channel == 8 || channel == 9 || channel == 1 || (11 <= channel && channel <= 19) || (21 <= channel && channel <= 29) ) + { + break; + } + } + + vector autos; + + for( unsigned i = 0; i < in->objects.size(); i ++ ) + { + BMSObject &obj = in->objects[i]; + while( trackMeasure < obj.measure ) + { + trackMeasure ++; + measureStartBeat += adjustedMeasureSize; + measureSize = 4.0f; + BMSMeasures::iterator it = in->measures.find(trackMeasure); + if( it != in->measures.end() ) measureSize = it->second.size * 4.0f; + adjustedMeasureSize = measureSize; + if( trackMeasure < firstNoteMeasure ) adjustedMeasureSize = measureSize = 4.0f; + + // measure size adjustment + // XXX: need more testing / fine-tuning! + int sixteenths = lrintf(measureSize * 4.0f); + if( sixteenths > 1 ) adjustedMeasureSize = (float)sixteenths / 4.0f; + measureAdjust = adjustedMeasureSize / measureSize; + td.SetBPMAtRow( BeatToNoteRow(measureStartBeat), measureAdjust * currentBPM ); + + { + int num = sixteenths; + int den = 16; + while (den > 4 && num % 2 == 0 && den % 2 == 0) { + num /= 2; + den /= 2; + } + td.SetTimeSignatureAtRow( BeatToNoteRow(measureStartBeat), num, den ); + } + // end measure size adjustment + } + + int row = BeatToNoteRow( measureStartBeat + adjustedMeasureSize * obj.position ); + int channel = obj.channel; + bool hold = obj.flag; + + if( channel == 3 ) // bpm change + { + int bpm; + if( sscanf(obj.value, "%x", &bpm) == 1 ) + { + if( bpm > 0 ) td.SetBPMAtRow( row, measureAdjust * (currentBPM = bpm) ); + } + } + else if( channel == 4 ) // bga change + { + /* + if( !bgaFound ) + { + info.bgaRow = row; + bgaFound = true; + } + */ + RString search = ssprintf( "#bga%s", obj.value.c_str() ); + BMSHeaders::iterator it = in->headers.find( search ); + if( it != in->headers.end() ) + { + // TODO: #BGA isn't supported yet. + } + else + { + search = ssprintf( "#bmp%s", obj.value.c_str() ); + it = in->headers.find( search ); + + RString bg; + if (song->GetBackground(it->second, in->path, bg)) + { + info.backgroundChanges[row] = bg; + } + } + } + else if( channel == 8 ) // bpm change (extended) + { + RString search = ssprintf( "#bpm%s", obj.value.c_str() ); + BMSHeaders::iterator it = in->headers.find( search ); + if( it != in->headers.end() ) + { + td.SetBPMAtRow( row, measureAdjust * (currentBPM = StringToFloat(it->second)) ); + } + else + { + LOG->UserLog( "Song file", in->path.c_str(), "has tag \"%s\" which cannot be found.", search.c_str() ); + } + } + else if( channel == 9 ) // stops + { + RString search = ssprintf( "#stop%s", obj.value.c_str() ); + BMSHeaders::iterator it = in->headers.find( search ); + if( it != in->headers.end() ) + { + td.SetStopAtRow( row, (StringToFloat(it->second) / 48.0f) * (60.0f / currentBPM) ); + } + else + { + LOG->UserLog( "Song file", in->path.c_str(), "has tag \"%s\" which cannot be found.", search.c_str() ); + } + } + else if( channel < 30 && reverseTransform[channel] != -1 ) // player notes! + { + int track = reverseTransform[channel]; + if( holdStart[track] != -1 ) + { + // this object is the end of the hold note. + TapNote tn = nd.GetTapNote(track, holdStart[track]); + tn.type = TapNote::hold_head; + tn.subType = TapNote::hold_head_hold; + nd.AddHoldNote( track, holdStart[track], row, tn ); + holdStart[track] = -1; + lastNote[track] = -1; + } + else if( obj.value == lnobj && lastNote[track] != -1 ) + { + // this object is the end of the hold note. + // lnobj: set last note to hold head. + TapNote tn = nd.GetTapNote(track, lastNote[track]); + tn.type = TapNote::hold_head; + tn.subType = TapNote::hold_head_hold; + nd.AddHoldNote( track, lastNote[track], row, tn ); + holdStart[track] = -1; + lastNote[track] = -1; + } + else + { + TapNote tn = TAP_ORIGINAL_TAP; + tn.iKeysoundIndex = GetKeysound(obj); + nd.SetTapNote( track, row, tn ); + if( hold ) holdStart[track] = row; + lastNote[track] = row; + } + } + else if( channel == 1 || (11 <= channel && channel <= 19) || (21 <= channel && channel <= 29) ) // auto-keysound and other notes + { + BMSAutoKeysound ak = { row, GetKeysound(obj) }; + autos.push_back( ak ); + } + } + + int rowsToLook[3] = { 0, -1, 1 }; + for( unsigned i = 0; i < autos.size(); i ++ ) + { + BMSAutoKeysound &ak = autos[i]; + bool found = false; + for( int j = 0; j < 3; j ++ ) + { + int row = ak.row + rowsToLook[j]; + for( int t = 0; t < tracks; t ++ ) + { + if( nd.GetTapNote( t, row ) == TAP_EMPTY && !nd.IsHoldNoteAtRow( t, row ) ) + { + TapNote tn = TAP_ORIGINAL_AUTO_KEYSOUND; + tn.iKeysoundIndex = ak.index; + nd.SetTapNote( t, row, tn ); + found = true; + break; + } + } + if( found ) break; + } + } + + delete transform; + delete holdStart; + delete lastNote; + + td.TidyUpData( false ); + out->SetNoteData(nd); + out->m_Timing = td; + out->TidyUpData(); + out->SetSavedToDisk( true ); // we're loading from disk, so this is by definintion already saved + + return true; +} + +Steps *BMSChartReader::GetSteps() +{ + return out; +} + +struct BMSStepsInfo { + Steps *steps; + BMSChartInfo info; +}; + +class BMSSongLoader +{ + RString dir; + BMSSong song; + vector loadedSteps; +public: + BMSSongLoader( RString songDir, Song *outSong ); + bool Load( RString fileName ); + void AddToSong(); +}; + +BMSSongLoader::BMSSongLoader( RString songDir, Song *outSong ): dir(songDir), song(outSong) +{ +} + +bool BMSSongLoader::Load( RString fileName ) +{ + // before doing anything else, load the chart first! + BMSChart chart; + if( !chart.Load( dir + fileName ) ) return false; + + // and then read the chart into the steps. + Steps *steps = song.GetSong()->CreateSteps(); + steps->SetFilename( dir + fileName ); + + BMSChartReader reader( &chart, steps, &song ); + if( !reader.Read() ) + { + delete steps; + return false; + } + + // add it to our song + song.GetSong()->AddSteps( steps ); + + // add the chart reader instance to our list. + BMSStepsInfo si = { steps, reader.info }; + loadedSteps.push_back(si); + + return true; + +} + +void BMSSongLoader::AddToSong() +{ + if( loadedSteps.size() == 0 ) + { + return; + } + + RString commonSubstring = ""; + + { + bool found = false; + for( unsigned i = 0; i < loadedSteps.size(); i ++ ) + { + if( loadedSteps[i].info.title == "" ) continue; + if( !found ) + { + commonSubstring = loadedSteps[i].info.title; + found = true; + } + else + { + commonSubstring = FindLargestInitialSubstring( commonSubstring, loadedSteps[i].info.title ); + } + } + if( commonSubstring == "" ) + { + // All bets are off; the titles don't match at all. + // At this rate we're lucky if we even get the title right. + LOG->UserLog( "Song", dir, "has BMS files with inconsistent titles." ); + } + } + + if( commonSubstring == "" ) + { + // As said before, all bets are off. + // From here on in, it's nothing but guesswork. + + // Try to figure out the difficulty of each file. + for( unsigned i = 0; i < loadedSteps.size(); i ++ ) + { + Steps *steps = loadedSteps[i].steps; + steps->SetDifficulty( Difficulty_Medium ); + + RString title = loadedSteps[i].info.title; + + // XXX: Is this really effective if Common Substring parsing failed? + if( title != "" ) SearchForDifficulty( title, steps ); + } + } + else + { + // Now, with our fancy little substring, trim the titles and + // figure out where each goes. + for( unsigned i = 0; i < loadedSteps.size(); i ++ ) + { + Steps *steps = loadedSteps[i].steps; + steps->SetDifficulty( Difficulty_Medium ); + + RString title = loadedSteps[i].info.title; + + if( title != "" && title.size() != commonSubstring.size() ) + { + RString tag = title.substr( commonSubstring.size(), title.size() - commonSubstring.size() ); + tag.MakeLower(); + + // XXX: We should do this with filenames too, I have plenty of examples. + // however, filenames will be trickier, as stuff at the beginning AND + // end change per-file, so we'll need a fancier FindLargestInitialSubstring() + + // XXX: This matches (double), but I haven't seen it used. Again, MORE EXAMPLES NEEDED + if( tag.find('l') != tag.npos ) + { + unsigned pos = tag.find('l'); + if( pos > 2 && tag.substr(pos - 2, 4) == "solo" ) + { + // (solo) -- an edit, apparently (Thanks Glenn!) + steps->SetDifficulty( Difficulty_Edit ); + } + else + { + // Any of [L7] [L14] (LIGHT7) (LIGHT14) (LIGHT) [L] ... you get the idea. + steps->SetDifficulty( Difficulty_Easy ); + } + } + // [A] (A) [ANOTHER] (ANOTHER) (ANOTHER7) Another (DP ANOTHER) (Another) -ANOTHER- [A7] [A14] etc etc etc + else if( tag.find('a') != tag.npos ) + steps->SetDifficulty( Difficulty_Hard ); + // XXX: Can also match (double), but should match [B] or [B7] + else if( tag.find('b') != tag.npos ) + steps->SetDifficulty( Difficulty_Beginner ); + // Other tags I've seen here include (5KEYS) (10KEYS) (7keys) (14keys) (dp) [MIX] [14] (14 Keys Mix) + // XXX: I'm sure [MIX] means something... anyone know? + } + } + } + + /* Prefer to read global tags from a Difficulty_Medium file. These tend to + * have the least cruft in the #TITLE tag, so it's more likely to get a clean + * title. */ + int mainIndex = 0; + for( unsigned i = 0; i < loadedSteps.size(); i ++ ) + if( loadedSteps[i].steps->GetDifficulty() == Difficulty_Medium ) + mainIndex = i; + + Song *out = song.GetSong(); + + { + const BMSStepsInfo &main = loadedSteps[mainIndex]; + out->m_sSongFileName = main.steps->GetFilename(); + if( main.info.title != "" ) + NotesLoader::GetMainAndSubTitlesFromFullTitle( main.info.title, out->m_sMainTitle, out->m_sSubTitle ); + out->m_sArtist = main.info.artist; + out->m_sGenre = main.info.genre; + out->m_sBannerFile = main.info.bannerFile; + + switch( main.steps->m_StepsType ) + { + case StepsType_beat_single5: + case StepsType_beat_single7: + case StepsType_beat_double5: + case StepsType_beat_double7: + case StepsType_beat_versus5: + case StepsType_beat_versus7: + out->m_sBackgroundFile = main.info.stageFile; + break; + default: + if ( main.info.backgroundFile != "" ) + out->m_sBackgroundFile = main.info.backgroundFile; + else + out->m_sBackgroundFile = main.info.stageFile; + break; + } + + map::const_iterator it = main.info.backgroundChanges.begin(); + + for (; it != main.info.backgroundChanges.end(); it++) + { + out->AddBackgroundChange(BACKGROUND_LAYER_1, + BackgroundChange(NoteRowToBeat(it->first), + it->second, + "", + 1.f, + SBE_StretchNoLoop)); + } + + out->m_sMusicFile = main.info.musicFile; + out->m_SongTiming = main.steps->m_Timing; + } + + // The brackets before the difficulty are in common substring, so remove them if it's found. + if( commonSubstring.size() > 2 && commonSubstring[commonSubstring.size() - 2] == ' ' ) + { + switch( commonSubstring[commonSubstring.size() - 1] ) + { + case '[': + case '(': + case '<': + commonSubstring = commonSubstring.substr(0, commonSubstring.size() - 2); + } + } + + // Override what that global tag said about the title if we have a good substring. + // Prevents clobbering and catches "MySong (7keys)" / "MySong (Another) (7keys)" + // Also catches "MySong (7keys)" / "MySong (14keys)" + if( commonSubstring != "" ) + NotesLoader::GetMainAndSubTitlesFromFullTitle( commonSubstring, out->m_sMainTitle, out->m_sSubTitle ); + + SlideDuplicateDifficulties( *out ); + + ConvertString( out->m_sMainTitle, "utf-8,japanese" ); + ConvertString( out->m_sArtist, "utf-8,japanese" ); + ConvertString( out->m_sGenre, "utf-8,japanese" ); + +} + +/*===========================================================================*/ + +bool BMSLoader::LoadNoteDataFromSimfile( const RString & cachePath, Steps & out ) +{ + Song *pSong = out.m_pSong; + + // before doing anything else, load the chart first! + BMSChart chart; + if( !chart.Load( cachePath ) ) return false; + + BMSSong song(pSong); + + BMSChartReader reader( &chart, &out, &song ); + if( !reader.Read() ) return false; + + return true; +} + +bool BMSLoader::LoadFromDir( const RString &sDir, Song &out ) +{ + LOG->Trace( "Song::LoadFromBMSDir(%s)", sDir.c_str() ); + + ASSERT( out.m_vsKeysoundFile.empty() ); + + vector arrayBMSFileNames; + GetApplicableFiles( sDir, arrayBMSFileNames ); + + /* We should have at least one; if we had none, we shouldn't have been + * called to begin with. */ + ASSERT( arrayBMSFileNames.size() != 0 ); + + BMSSongLoader loader( sDir, &out ); + for( unsigned i=0; im_vsReloadRowMessages) MESSAGEMAN->Unsubscribe( this, m ); @@ -197,7 +197,7 @@ RString OptionRow::GetRowTitle() const if( GAMESTATE->m_pCurCourse ) { const Trail* pTrail = GAMESTATE->m_pCurTrail[GAMESTATE->GetMasterPlayerNumber()]; - ASSERT( pTrail != NULL ); + ASSERT( pTrail != nullptr ); const int iNumCourseEntries = pTrail->m_vEntries.size(); if( iNumCourseEntries > CommonMetrics::MAX_COURSE_ENTRIES_BEFORE_VARIOUS ) bShowBpmInSpeedTitle = false; @@ -216,7 +216,7 @@ RString OptionRow::GetRowTitle() const const Course *pCourse = GAMESTATE->m_pCurCourse; StepsType st = GAMESTATE->GetCurrentStyle()->m_StepsType; const Trail* pTrail = pCourse->GetTrail( st ); - ASSERT( pTrail != NULL ); + ASSERT( pTrail != nullptr ); pTrail->GetDisplayBpms( bpms ); } @@ -651,7 +651,7 @@ void OptionRow::SetModIcon( PlayerNumber pn, const RString &sText, GameCommand & msg.SetParam( "GameCommand", &gc ); msg.SetParam( "Text", sText ); m_sprFrame->HandleMessage( msg ); - if( m_ModIcons[pn] != NULL ) + if( m_ModIcons[pn] != nullptr ) m_ModIcons[pn]->Set( sText ); } diff --git a/src/OptionRowHandler.cpp b/src/OptionRowHandler.cpp index a904b10f84..19c4f3d703 100644 --- a/src/OptionRowHandler.cpp +++ b/src/OptionRowHandler.cpp @@ -536,7 +536,7 @@ public: m_ppStepsToFill = &GAMESTATE->m_pEditSourceSteps; m_pst = &GAMESTATE->m_stEditSource; m_vsReloadRowMessages.push_back( MessageIDToString(Message_EditSourceStepsTypeChanged) ); - if( GAMESTATE->m_pCurSteps[0].Get() != NULL ) + if( GAMESTATE->m_pCurSteps[0].Get() != nullptr ) m_Def.m_vEnabledForPlayers.clear(); // hide row } else @@ -1120,7 +1120,7 @@ public: { LOG->Warn( "Invalid Conf type \"%s\"", sParam.c_str() ); pConfOption = ConfOption::Find( "Invalid" ); - ASSERT_M( pConfOption != NULL, "ConfOption::Find(Invalid)" ); + ASSERT_M( pConfOption != nullptr, "ConfOption::Find(Invalid)" ); } pConfOption->UpdateAvailableOptions(); @@ -1201,7 +1201,7 @@ public: m_pstToFill = &GAMESTATE->m_stEditSource; m_vsReloadRowMessages.push_back( MessageIDToString(Message_CurrentStepsP1Changed) ); m_vsReloadRowMessages.push_back( MessageIDToString(Message_EditStepsTypeChanged) ); - if( GAMESTATE->m_pCurSteps[0].Get() != NULL ) + if( GAMESTATE->m_pCurSteps[0].Get() != nullptr ) m_Def.m_vEnabledForPlayers.clear(); // hide row } else diff --git a/src/OptionsList.cpp b/src/OptionsList.cpp index 7b16875348..2ed78bac48 100644 --- a/src/OptionsList.cpp +++ b/src/OptionsList.cpp @@ -101,7 +101,7 @@ void OptionListRow::SetFromHandler( const OptionRowHandler *pHandler ) void OptionListRow::SetTextFromHandler( const OptionRowHandler *pHandler ) { - ASSERT( pHandler != NULL ); + ASSERT( pHandler != nullptr ); for( unsigned i = 0; i < pHandler->m_Def.m_vsChoices.size(); ++i ) { // init text @@ -632,7 +632,7 @@ void OptionsList::SelectionsChanged( const RString &sRowName ) const OptionRowHandler *pHandler = m_Rows[sRowName]; vector &bSelections = m_bSelections[sRowName]; - if( pHandler->m_Def.m_bOneChoiceForAllPlayers && m_pLinked != NULL ) + if( pHandler->m_Def.m_bOneChoiceForAllPlayers && m_pLinked != nullptr ) { vector &bLinkedSelections = m_pLinked->m_bSelections[sRowName]; bLinkedSelections = bSelections; diff --git a/src/PercentageDisplay.cpp b/src/PercentageDisplay.cpp index e932844e3f..a9a16d7c88 100644 --- a/src/PercentageDisplay.cpp +++ b/src/PercentageDisplay.cpp @@ -1,235 +1,235 @@ -#include "global.h" - -#include "PercentageDisplay.h" -#include "GameState.h" -#include "ThemeManager.h" -#include "PrefsManager.h" -#include "ActorUtil.h" -#include "RageLog.h" -#include "StageStats.h" -#include "PlayerState.h" -#include "XmlFile.h" -#include "Course.h" - - -REGISTER_ACTOR_CLASS( PercentageDisplay ); - -PercentageDisplay::PercentageDisplay() -{ - m_pPlayerState = NULL; - m_pPlayerStageStats = NULL; - - m_Last = -1; - m_LastMax = -1; - m_iDancePointsDigits = 0; - m_bUseRemainder = false; - m_bAutoRefresh = false; - m_FormatPercentScore.SetFromExpression( "FormatPercentScore" ); -} - -void PercentageDisplay::LoadFromNode( const XNode* pNode ) -{ - pNode->GetAttrValue( "DancePointsDigits", m_iDancePointsDigits ); - pNode->GetAttrValue( "AutoRefresh", m_bAutoRefresh ); - { - Lua *L = LUA->Get(); - if( pNode->PushAttrValue(L, "FormatPercentScore") ) - m_FormatPercentScore.SetFromStack( L ); - else - lua_pop(L, 1); - LUA->Release(L); - } - - const XNode *pChild = pNode->GetChild( "Percent" ); - if( pChild == NULL ) - RageException::Throw( "%s: PercentageDisplay: missing the node \"Percent\"", ActorUtil::GetWhere(pNode).c_str() ); - m_textPercent.LoadFromNode( pChild ); - this->AddChild( &m_textPercent ); - - pChild = pNode->GetChild( "PercentRemainder" ); - if( !ShowDancePointsNotPercentage() && pChild != NULL ) - { - m_bUseRemainder = true; - m_textPercentRemainder.LoadFromNode( pChild ); - this->AddChild( &m_textPercentRemainder ); - } - - // only run the Init command after we load Fonts. - ActorFrame::LoadFromNode( pNode ); -} - -void PercentageDisplay::Load( const PlayerState *pPlayerState, const PlayerStageStats *pPlayerStageStats ) -{ - m_pPlayerState = pPlayerState; - m_pPlayerStageStats = pPlayerStageStats; - - Refresh(); -} - -void PercentageDisplay::Load( const PlayerState *pPlayerState, const PlayerStageStats *pPlayerStageStats, const RString &sMetricsGroup, bool bAutoRefresh ) -{ - m_pPlayerState = pPlayerState; - m_pPlayerStageStats = pPlayerStageStats; - m_bAutoRefresh = bAutoRefresh; - - m_iDancePointsDigits = THEME->GetMetricI( sMetricsGroup, "DancePointsDigits" ); - m_bUseRemainder = THEME->GetMetricB( sMetricsGroup, "PercentUseRemainder" ); - m_FormatPercentScore = THEME->GetMetricR( sMetricsGroup, "Format" ); - - m_sPercentFormat = THEME->GetMetric( sMetricsGroup, "PercentFormat" ); - m_sRemainderFormat = THEME->GetMetric( sMetricsGroup, "RemainderFormat" ); - - if( m_FormatPercentScore.IsNil() ) - { - LOG->Trace( "Format is nil in [%s]. Defaulting to 'FormatPercentScore'.", sMetricsGroup.c_str() ); - m_FormatPercentScore.SetFromExpression( "FormatPercentScore" ); - } - - if( ShowDancePointsNotPercentage() ) - m_textPercent.SetName( "DancePoints" + PlayerNumberToString(m_pPlayerState->m_PlayerNumber) ); - else - m_textPercent.SetName( "Percent" + PlayerNumberToString(m_pPlayerState->m_PlayerNumber) ); - - m_textPercent.LoadFromFont( THEME->GetPathF(sMetricsGroup,"text") ); - ActorUtil::SetXY( m_textPercent, sMetricsGroup ); - ActorUtil::LoadAllCommands( m_textPercent, sMetricsGroup ); - this->AddChild( &m_textPercent ); - - if( !ShowDancePointsNotPercentage() && m_bUseRemainder ) - { - m_textPercentRemainder.SetName( "PercentRemainder" + PlayerNumberToString(m_pPlayerState->m_PlayerNumber) ); - m_textPercentRemainder.LoadFromFont( THEME->GetPathF(sMetricsGroup,"remainder") ); - ActorUtil::SetXY( m_textPercentRemainder, sMetricsGroup ); - ActorUtil::LoadAllCommands( m_textPercentRemainder, sMetricsGroup ); - ASSERT( m_textPercentRemainder.HasCommand("Off") ); - m_textPercentRemainder.SetText( "456" ); - this->AddChild( &m_textPercentRemainder ); - } - - Refresh(); -} - -void PercentageDisplay::Update( float fDeltaTime ) -{ - ActorFrame::Update( fDeltaTime ); - - if( m_bAutoRefresh ) - Refresh(); -} - -void PercentageDisplay::Refresh() -{ - const int iActualDancePoints = m_pPlayerStageStats->m_iActualDancePoints; - const int iCurPossibleDancePoints = m_pPlayerStageStats->m_iCurPossibleDancePoints; - - if( iActualDancePoints == m_Last && iCurPossibleDancePoints == m_LastMax ) - return; - - m_Last = iActualDancePoints; - m_LastMax = iCurPossibleDancePoints; - - RString sNumToDisplay; - - if( ShowDancePointsNotPercentage() ) - { - sNumToDisplay = ssprintf( "%*d", m_iDancePointsDigits, max( 0, iActualDancePoints ) ); - } - else - { - float fPercentDancePoints = m_pPlayerStageStats->GetPercentDancePoints(); - - // clamp percentage - feedback is that negative numbers look weird here. - CLAMP( fPercentDancePoints, 0.f, 1.f ); - - if( m_bUseRemainder ) - { - int iPercentWhole = int(fPercentDancePoints*100); - int iPercentRemainder = int( (fPercentDancePoints*100 - int(fPercentDancePoints*100)) * 10 ); - sNumToDisplay = ssprintf( m_sPercentFormat, iPercentWhole ); - m_textPercentRemainder.SetText( ssprintf(m_sRemainderFormat, iPercentRemainder) ); - } - else - { - Lua *L = LUA->Get(); - m_FormatPercentScore.PushSelf( L ); - ASSERT( !lua_isnil(L, -1) ); - LuaHelpers::Push( L, fPercentDancePoints ); - RString sError; - if( !LuaHelpers::RunScriptOnStack(L, sError, 1, 1) ) // 1 arg, 1 result - LOG->Warn( "Error running FormatPercentScore: %s", sError.c_str() ); - LuaHelpers::Pop( L, sNumToDisplay ); - LUA->Release(L); - - // HACK: Use the last frame in the numbers texture as '-' - sNumToDisplay.Replace('-','x'); - } - } - - m_textPercent.SetText( sNumToDisplay ); -} - -bool PercentageDisplay::ShowDancePointsNotPercentage() const -{ - // Use staight dance points in workout because the percentage denominator isn't accurate - we don't know when the players are going to stop. - - if( GAMESTATE->m_pCurCourse ) - { - if( GAMESTATE->m_pCurCourse->m_fGoalSeconds > 0 ) - return true; - } - - if( PREFSMAN->m_bDancePointsForOni ) - return true; - - return false; -} - - -#include "LuaBinding.h" - -/** @brief Allow Lua to have access to the PercentageDisplay. */ -class LunaPercentageDisplay: public Luna -{ -public: - static int LoadFromStats( T* p, lua_State *L ) - { - const PlayerState *pStageStats = Luna::check( L, 1 ); - const PlayerStageStats *pPlayerStageStats = Luna::check( L, 2 ); - p->Load( pStageStats, pPlayerStageStats ); - return 0; - } - - LunaPercentageDisplay() - { - ADD_METHOD( LoadFromStats ); - } -}; - -LUA_REGISTER_DERIVED_CLASS( PercentageDisplay, ActorFrame ) -// lua end - - -/* - * (c) 2001-2003 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. - */ +#include "global.h" + +#include "PercentageDisplay.h" +#include "GameState.h" +#include "ThemeManager.h" +#include "PrefsManager.h" +#include "ActorUtil.h" +#include "RageLog.h" +#include "StageStats.h" +#include "PlayerState.h" +#include "XmlFile.h" +#include "Course.h" + + +REGISTER_ACTOR_CLASS( PercentageDisplay ); + +PercentageDisplay::PercentageDisplay() +{ + m_pPlayerState = NULL; + m_pPlayerStageStats = NULL; + + m_Last = -1; + m_LastMax = -1; + m_iDancePointsDigits = 0; + m_bUseRemainder = false; + m_bAutoRefresh = false; + m_FormatPercentScore.SetFromExpression( "FormatPercentScore" ); +} + +void PercentageDisplay::LoadFromNode( const XNode* pNode ) +{ + pNode->GetAttrValue( "DancePointsDigits", m_iDancePointsDigits ); + pNode->GetAttrValue( "AutoRefresh", m_bAutoRefresh ); + { + Lua *L = LUA->Get(); + if( pNode->PushAttrValue(L, "FormatPercentScore") ) + m_FormatPercentScore.SetFromStack( L ); + else + lua_pop(L, 1); + LUA->Release(L); + } + + const XNode *pChild = pNode->GetChild( "Percent" ); + if( pChild == NULL ) + RageException::Throw( "%s: PercentageDisplay: missing the node \"Percent\"", ActorUtil::GetWhere(pNode).c_str() ); + m_textPercent.LoadFromNode( pChild ); + this->AddChild( &m_textPercent ); + + pChild = pNode->GetChild( "PercentRemainder" ); + if( !ShowDancePointsNotPercentage() && pChild != nullptr ) + { + m_bUseRemainder = true; + m_textPercentRemainder.LoadFromNode( pChild ); + this->AddChild( &m_textPercentRemainder ); + } + + // only run the Init command after we load Fonts. + ActorFrame::LoadFromNode( pNode ); +} + +void PercentageDisplay::Load( const PlayerState *pPlayerState, const PlayerStageStats *pPlayerStageStats ) +{ + m_pPlayerState = pPlayerState; + m_pPlayerStageStats = pPlayerStageStats; + + Refresh(); +} + +void PercentageDisplay::Load( const PlayerState *pPlayerState, const PlayerStageStats *pPlayerStageStats, const RString &sMetricsGroup, bool bAutoRefresh ) +{ + m_pPlayerState = pPlayerState; + m_pPlayerStageStats = pPlayerStageStats; + m_bAutoRefresh = bAutoRefresh; + + m_iDancePointsDigits = THEME->GetMetricI( sMetricsGroup, "DancePointsDigits" ); + m_bUseRemainder = THEME->GetMetricB( sMetricsGroup, "PercentUseRemainder" ); + m_FormatPercentScore = THEME->GetMetricR( sMetricsGroup, "Format" ); + + m_sPercentFormat = THEME->GetMetric( sMetricsGroup, "PercentFormat" ); + m_sRemainderFormat = THEME->GetMetric( sMetricsGroup, "RemainderFormat" ); + + if( m_FormatPercentScore.IsNil() ) + { + LOG->Trace( "Format is nil in [%s]. Defaulting to 'FormatPercentScore'.", sMetricsGroup.c_str() ); + m_FormatPercentScore.SetFromExpression( "FormatPercentScore" ); + } + + if( ShowDancePointsNotPercentage() ) + m_textPercent.SetName( "DancePoints" + PlayerNumberToString(m_pPlayerState->m_PlayerNumber) ); + else + m_textPercent.SetName( "Percent" + PlayerNumberToString(m_pPlayerState->m_PlayerNumber) ); + + m_textPercent.LoadFromFont( THEME->GetPathF(sMetricsGroup,"text") ); + ActorUtil::SetXY( m_textPercent, sMetricsGroup ); + ActorUtil::LoadAllCommands( m_textPercent, sMetricsGroup ); + this->AddChild( &m_textPercent ); + + if( !ShowDancePointsNotPercentage() && m_bUseRemainder ) + { + m_textPercentRemainder.SetName( "PercentRemainder" + PlayerNumberToString(m_pPlayerState->m_PlayerNumber) ); + m_textPercentRemainder.LoadFromFont( THEME->GetPathF(sMetricsGroup,"remainder") ); + ActorUtil::SetXY( m_textPercentRemainder, sMetricsGroup ); + ActorUtil::LoadAllCommands( m_textPercentRemainder, sMetricsGroup ); + ASSERT( m_textPercentRemainder.HasCommand("Off") ); + m_textPercentRemainder.SetText( "456" ); + this->AddChild( &m_textPercentRemainder ); + } + + Refresh(); +} + +void PercentageDisplay::Update( float fDeltaTime ) +{ + ActorFrame::Update( fDeltaTime ); + + if( m_bAutoRefresh ) + Refresh(); +} + +void PercentageDisplay::Refresh() +{ + const int iActualDancePoints = m_pPlayerStageStats->m_iActualDancePoints; + const int iCurPossibleDancePoints = m_pPlayerStageStats->m_iCurPossibleDancePoints; + + if( iActualDancePoints == m_Last && iCurPossibleDancePoints == m_LastMax ) + return; + + m_Last = iActualDancePoints; + m_LastMax = iCurPossibleDancePoints; + + RString sNumToDisplay; + + if( ShowDancePointsNotPercentage() ) + { + sNumToDisplay = ssprintf( "%*d", m_iDancePointsDigits, max( 0, iActualDancePoints ) ); + } + else + { + float fPercentDancePoints = m_pPlayerStageStats->GetPercentDancePoints(); + + // clamp percentage - feedback is that negative numbers look weird here. + CLAMP( fPercentDancePoints, 0.f, 1.f ); + + if( m_bUseRemainder ) + { + int iPercentWhole = int(fPercentDancePoints*100); + int iPercentRemainder = int( (fPercentDancePoints*100 - int(fPercentDancePoints*100)) * 10 ); + sNumToDisplay = ssprintf( m_sPercentFormat, iPercentWhole ); + m_textPercentRemainder.SetText( ssprintf(m_sRemainderFormat, iPercentRemainder) ); + } + else + { + Lua *L = LUA->Get(); + m_FormatPercentScore.PushSelf( L ); + ASSERT( !lua_isnil(L, -1) ); + LuaHelpers::Push( L, fPercentDancePoints ); + RString sError; + if( !LuaHelpers::RunScriptOnStack(L, sError, 1, 1) ) // 1 arg, 1 result + LOG->Warn( "Error running FormatPercentScore: %s", sError.c_str() ); + LuaHelpers::Pop( L, sNumToDisplay ); + LUA->Release(L); + + // HACK: Use the last frame in the numbers texture as '-' + sNumToDisplay.Replace('-','x'); + } + } + + m_textPercent.SetText( sNumToDisplay ); +} + +bool PercentageDisplay::ShowDancePointsNotPercentage() const +{ + // Use staight dance points in workout because the percentage denominator isn't accurate - we don't know when the players are going to stop. + + if( GAMESTATE->m_pCurCourse ) + { + if( GAMESTATE->m_pCurCourse->m_fGoalSeconds > 0 ) + return true; + } + + if( PREFSMAN->m_bDancePointsForOni ) + return true; + + return false; +} + + +#include "LuaBinding.h" + +/** @brief Allow Lua to have access to the PercentageDisplay. */ +class LunaPercentageDisplay: public Luna +{ +public: + static int LoadFromStats( T* p, lua_State *L ) + { + const PlayerState *pStageStats = Luna::check( L, 1 ); + const PlayerStageStats *pPlayerStageStats = Luna::check( L, 2 ); + p->Load( pStageStats, pPlayerStageStats ); + return 0; + } + + LunaPercentageDisplay() + { + ADD_METHOD( LoadFromStats ); + } +}; + +LUA_REGISTER_DERIVED_CLASS( PercentageDisplay, ActorFrame ) +// lua end + + +/* + * (c) 2001-2003 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. + */ diff --git a/src/Player.cpp b/src/Player.cpp index b2bbedfd26..f431db3321 100644 --- a/src/Player.cpp +++ b/src/Player.cpp @@ -416,12 +416,12 @@ void Player::Init( if( GAMESTATE->IsCourseMode() ) { - ASSERT( GAMESTATE->m_pCurTrail[pn] != NULL ); + ASSERT( GAMESTATE->m_pCurTrail[pn] != nullptr ); GAMESTATE->m_pCurTrail[pn]->GetDisplayBpms( bpms ); } else { - ASSERT( GAMESTATE->m_pCurSong != NULL ); + ASSERT( GAMESTATE->m_pCurSong != nullptr ); GAMESTATE->m_pCurSong->GetDisplayBpms( bpms ); } @@ -891,14 +891,14 @@ void Player::Update( float fDeltaTime ) const bool bReverse = m_pPlayerState->m_PlayerOptions.GetCurrent().GetReversePercentForColumn(0) == 1; float fPercentCentered = m_pPlayerState->m_PlayerOptions.GetCurrent().m_fScrolls[PlayerOptions::SCROLL_CENTERED]; - if( m_pActorWithJudgmentPosition != NULL ) + if( m_pActorWithJudgmentPosition != nullptr ) { const Actor::TweenState &ts1 = m_tsJudgment[bReverse?1:0][0]; const Actor::TweenState &ts2 = m_tsJudgment[bReverse?1:0][1]; Actor::TweenState::MakeWeightedAverage( m_pActorWithJudgmentPosition->DestTweenState(), ts1, ts2, fPercentCentered ); } - if( m_pActorWithComboPosition != NULL ) + if( m_pActorWithComboPosition != nullptr ) { const Actor::TweenState &ts1 = m_tsCombo[bReverse?1:0][0]; const Actor::TweenState &ts2 = m_tsCombo[bReverse?1:0][1]; @@ -908,9 +908,9 @@ void Player::Update( float fDeltaTime ) float fNoteFieldZoom = 1 - fMiniPercent*0.5f; if( m_pNoteField ) m_pNoteField->SetZoom( fNoteFieldZoom ); - if( m_pActorWithJudgmentPosition != NULL ) + if( m_pActorWithJudgmentPosition != nullptr ) m_pActorWithJudgmentPosition->SetZoom( m_pActorWithJudgmentPosition->GetZoom() * fJudgmentZoom ); - if( m_pActorWithComboPosition != NULL ) + if( m_pActorWithComboPosition != nullptr ) m_pActorWithComboPosition->SetZoom( m_pActorWithComboPosition->GetZoom() * fJudgmentZoom ); } @@ -931,7 +931,7 @@ void Player::Update( float fDeltaTime ) ASSERT_M( iNumCols <= MAX_COLS_PER_PLAYER, ssprintf("%i > %i", iNumCols, MAX_COLS_PER_PLAYER) ); for( int col=0; col < iNumCols; ++col ) { - ASSERT( m_pPlayerState != NULL ); + ASSERT( m_pPlayerState != nullptr ); // TODO: Remove use of PlayerNumber. GameInput GameI = GAMESTATE->GetCurrentStyle()->StyleInputToGameInput( col, m_pPlayerState->m_PlayerNumber ); @@ -1216,7 +1216,7 @@ void Player::UpdateHoldNotes( int iSongRow, float fDeltaTime, vectorm_PlayerController == PC_AUTOPLAY ) { STATSMAN->m_CurStageStats.m_bUsedAutoplay = true; - if( m_pPlayerStageStats != NULL ) + if( m_pPlayerStageStats != nullptr ) m_pPlayerStageStats->m_bDisqualified = true; } } @@ -3162,9 +3162,9 @@ void Player::HandleTapRowScore( unsigned row ) m_pSecondaryScoreKeeper->HandleTapScore( tn ); } - if( m_pPrimaryScoreKeeper != NULL ) + if( m_pPrimaryScoreKeeper != nullptr ) m_pPrimaryScoreKeeper->HandleTapRowScore( m_NoteData, row ); - if( m_pSecondaryScoreKeeper != NULL ) + if( m_pSecondaryScoreKeeper != nullptr ) m_pSecondaryScoreKeeper->HandleTapRowScore( m_NoteData, row ); const int iCurCombo = m_pPlayerStageStats ? m_pPlayerStageStats->m_iCurCombo : 0; diff --git a/src/Player.h b/src/Player.h index df5ee9e081..8a645a776e 100644 --- a/src/Player.h +++ b/src/Player.h @@ -1,271 +1,271 @@ -#ifndef PLAYER_H -#define PLAYER_H - -#include "ActorFrame.h" -#include "HoldJudgment.h" -#include "NoteDataWithScoring.h" -#include "RageSound.h" -#include "AttackDisplay.h" -#include "NoteData.h" -#include "ScreenMessage.h" -#include "ThemeMetric.h" -#include "InputEventPlus.h" -#include "TimingData.h" - -class ScoreDisplay; -class LifeMeter; -class CombinedLifeMeter; -class ScoreKeeper; -class Inventory; -class RageTimer; -class NoteField; -class PlayerStageStats; -class JudgedRows; - -// todo: replace these with a Message and MESSAGEMAN? -aj -AutoScreenMessage( SM_100Combo ); -AutoScreenMessage( SM_200Combo ); -AutoScreenMessage( SM_300Combo ); -AutoScreenMessage( SM_400Combo ); -AutoScreenMessage( SM_500Combo ); -AutoScreenMessage( SM_600Combo ); -AutoScreenMessage( SM_700Combo ); -AutoScreenMessage( SM_800Combo ); -AutoScreenMessage( SM_900Combo ); -AutoScreenMessage( SM_1000Combo ); -AutoScreenMessage( SM_ComboStopped ); -AutoScreenMessage( SM_ComboContinuing ); - -/** @brief Accepts input, knocks down TapNotes that were stepped on, and keeps score for the player. */ -class Player: public ActorFrame -{ -public: - // The passed in NoteData isn't touched until Load() is called. - Player( NoteData &nd, bool bVisibleParts = true ); - ~Player(); - - virtual void Update( float fDeltaTime ); - virtual void DrawPrimitives(); - - struct TrackRowTapNote - { - int iTrack; - int iRow; - TapNote *pTN; - }; - void UpdateHoldNotes( int iSongRow, float fDeltaTime, vector &vTN ); - - void Init( - const RString &sType, - PlayerState* pPlayerState, - PlayerStageStats* pPlayerStageStats, - LifeMeter* pLM, - CombinedLifeMeter* pCombinedLM, - ScoreDisplay* pScoreDisplay, - ScoreDisplay* pSecondaryScoreDisplay, - Inventory* pInventory, - ScoreKeeper* pPrimaryScoreKeeper, - ScoreKeeper* pSecondaryScoreKeeper ); - void Load(); - void CrossedRows( int iLastRowCrossed, const RageTimer &now ); - bool IsOniDead() const; - - /** - * @brief Retrieve the Player's TimingData. - * - * This is primarily for a lua hook. - * @return the TimingData in question. */ - TimingData GetPlayerTimingData() const - { - return *(this->m_Timing); - } - - // Called when a fret, step, or strum type button changes - void Fret( int col, int row, const RageTimer &tm, bool bHeld, bool bRelease ); - - // Called when the strum bar is pressed down - void Strum( int col, int row, const RageTimer &tm, bool bHeld, bool bRelease ); - - // Called when the strum window passes without a row being hit - void DoStrumMiss(); - void ScoreAllActiveHoldsLetGo(); - void DoTapScoreNone(); - - enum ButtonType { ButtonType_Step, ButtonType_StrumFretsChanged, ButtonType_Hopo }; - void StepStrumHopo( int col, int row, const RageTimer &tm, bool bHeld, bool bRelease, ButtonType gbt ); - void Step( int col, int row, const RageTimer &tm, bool bHeld, bool bRelease ) { StepStrumHopo(col, row, tm, bHeld, bRelease, ButtonType_Step); } - - // called by Fret for Hammer-ons and Pull-offs - void Hopo( int col, int row, const RageTimer &tm, bool bHeld, bool bRelease ) { StepStrumHopo(col, row, tm, bHeld, bRelease, ButtonType_Hopo); } - - void RandomizeNotes( int iNoteRow ); - void FadeToFail(); - void CacheAllUsedNoteSkins(); - TapNoteScore GetLastTapNoteScore() const { return m_LastTapNoteScore; } - void ApplyWaitingTransforms(); - void SetPaused( bool bPaused ) { m_bPaused = bPaused; } - - static float GetMaxStepDistanceSeconds(); - static float GetWindowSeconds( TimingWindow tw ); - const NoteData &GetNoteData() const { return m_NoteData; } - bool HasVisibleParts() const { return m_pNoteField != NULL; } - - void SetActorWithJudgmentPosition( Actor *pActor ) { m_pActorWithJudgmentPosition = pActor; } - void SetActorWithComboPosition( Actor *pActor ) { m_pActorWithComboPosition = pActor; } - - void SetSendJudgmentAndComboMessages( bool b ) { m_bSendJudgmentAndComboMessages = b; } - - // Lua - virtual void PushSelf( lua_State *L ); - - PlayerState * GetPlayerState() { return this->m_pPlayerState; } - -protected: - void UpdateTapNotesMissedOlderThan( float fMissIfOlderThanThisBeat ); - void UpdateJudgedRows(); - void FlashGhostRow( int iRow ); - void HandleTapRowScore( unsigned row ); - void HandleHoldScore( const TapNote &tn ); - void HandleHoldCheckpoint( int iRow, int iNumHoldsHeldThisRow, int iNumHoldsMissedThisRow, const vector &viColsWithHold ); - void DrawTapJudgments(); - void DrawHoldJudgments(); - void SendComboMessages( int iOldCombo, int iOldMissCombo ); - void PlayKeysound( const TapNote &tn, TapNoteScore score ); - - void SetMineJudgment( TapNoteScore tns ); - void SetJudgment( TapNoteScore tns, int iFirstTrack, float fTapNoteOffset ); // -1 if no track as in TNS_Miss - void SetHoldJudgment( TapNoteScore tns, HoldNoteScore hns, int iTrack ); - void SetCombo( int iCombo, int iMisses ); - - void ChangeLife( TapNoteScore tns ); - void ChangeLife( HoldNoteScore hns, TapNoteScore tns ); - void ChangeLifeRecord(); - - int GetClosestNoteDirectional( int col, int iStartRow, int iMaxRowsAhead, bool bAllowGraded, bool bForward ) const; - int GetClosestNote( int col, int iNoteRow, int iMaxRowsAhead, int iMaxRowsBehind, bool bAllowGraded ) const; - int GetClosestNonEmptyRowDirectional( int iStartRow, int iMaxRowsAhead, bool bAllowGraded, bool bForward ) const; - int GetClosestNonEmptyRow( int iNoteRow, int iMaxRowsAhead, int iMaxRowsBehind, bool bAllowGraded ) const; - - RString ApplyRandomAttack(); - - inline void HideNote( int col, int row ) - { - NoteData::iterator iter = m_NoteData.FindTapNote( col, row ); - if( iter != m_NoteData.end(col) ) - iter->second.result.bHidden = true; - } - - bool m_bLoaded; - - /** @brief The player's present state. */ - PlayerState *m_pPlayerState; - /** @brief The player's present stage stats. */ - PlayerStageStats *m_pPlayerStageStats; - TimingData *m_Timing; - float m_fNoteFieldHeight; - - bool m_bPaused; - bool m_bDelay; - - NoteData &m_NoteData; - NoteField *m_pNoteField; - - vector m_vpHoldJudgment; - - AutoActor m_sprJudgment; - AutoActor m_sprCombo; - Actor *m_pActorWithJudgmentPosition; - Actor *m_pActorWithComboPosition; - - AttackDisplay *m_pAttackDisplay; - - TapNoteScore m_LastTapNoteScore; - LifeMeter *m_pLifeMeter; - CombinedLifeMeter *m_pCombinedLifeMeter; - ScoreDisplay *m_pScoreDisplay; - ScoreDisplay *m_pSecondaryScoreDisplay; - ScoreKeeper *m_pPrimaryScoreKeeper; - ScoreKeeper *m_pSecondaryScoreKeeper; - Inventory *m_pInventory; - - int m_iFirstUncrossedRow; // used by hold checkpoints logic - NoteData::all_tracks_iterator *m_pIterNeedsTapJudging; - NoteData::all_tracks_iterator *m_pIterNeedsHoldJudging; - NoteData::all_tracks_iterator *m_pIterUncrossedRows; - NoteData::all_tracks_iterator *m_pIterUnjudgedRows; - NoteData::all_tracks_iterator *m_pIterUnjudgedMineRows; - int m_iLastSeenCombo; - JudgedRows *m_pJudgedRows; - - RageSound m_soundMine; - RageSound m_soundAttackLaunch; - RageSound m_soundAttackEnding; - - float m_fActiveRandomAttackStart; - - vector m_vbFretIsDown; - - vector m_vKeysounds; - - ThemeMetric GRAY_ARROWS_Y_STANDARD; - ThemeMetric GRAY_ARROWS_Y_REVERSE; - ThemeMetric2D ATTACK_DISPLAY_X; - ThemeMetric ATTACK_DISPLAY_Y; - ThemeMetric ATTACK_DISPLAY_Y_REVERSE; - ThemeMetric HOLD_JUDGMENT_Y_STANDARD; - ThemeMetric HOLD_JUDGMENT_Y_REVERSE; - ThemeMetric BRIGHT_GHOST_COMBO_THRESHOLD; - ThemeMetric TAP_JUDGMENTS_UNDER_FIELD; - ThemeMetric HOLD_JUDGMENTS_UNDER_FIELD; - ThemeMetric COMBO_UNDER_FIELD; - ThemeMetric DRAW_DISTANCE_AFTER_TARGET_PIXELS; - ThemeMetric DRAW_DISTANCE_BEFORE_TARGET_PIXELS; - -#define NUM_REVERSE 2 -#define NUM_CENTERED 2 - TweenState m_tsJudgment[NUM_REVERSE][NUM_CENTERED]; - TweenState m_tsCombo[NUM_REVERSE][NUM_CENTERED]; - - bool m_bSendJudgmentAndComboMessages; -}; - -class PlayerPlus -{ - Player *m_pPlayer; - NoteData m_NoteData; -public: - PlayerPlus() { m_pPlayer = new Player(m_NoteData); } - ~PlayerPlus() { delete m_pPlayer; } - void Load( const NoteData &nd ) { m_NoteData = nd; m_pPlayer->Load(); } - Player *operator->() { return m_pPlayer; } - const Player *operator->() const { return m_pPlayer; } - operator Player*() { return m_pPlayer; } - operator const Player*() const { return m_pPlayer; } -}; - -#endif - -/* - * (c) 2001-2006 Chris Danford, Steve Checkoway - * 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. - */ +#ifndef PLAYER_H +#define PLAYER_H + +#include "ActorFrame.h" +#include "HoldJudgment.h" +#include "NoteDataWithScoring.h" +#include "RageSound.h" +#include "AttackDisplay.h" +#include "NoteData.h" +#include "ScreenMessage.h" +#include "ThemeMetric.h" +#include "InputEventPlus.h" +#include "TimingData.h" + +class ScoreDisplay; +class LifeMeter; +class CombinedLifeMeter; +class ScoreKeeper; +class Inventory; +class RageTimer; +class NoteField; +class PlayerStageStats; +class JudgedRows; + +// todo: replace these with a Message and MESSAGEMAN? -aj +AutoScreenMessage( SM_100Combo ); +AutoScreenMessage( SM_200Combo ); +AutoScreenMessage( SM_300Combo ); +AutoScreenMessage( SM_400Combo ); +AutoScreenMessage( SM_500Combo ); +AutoScreenMessage( SM_600Combo ); +AutoScreenMessage( SM_700Combo ); +AutoScreenMessage( SM_800Combo ); +AutoScreenMessage( SM_900Combo ); +AutoScreenMessage( SM_1000Combo ); +AutoScreenMessage( SM_ComboStopped ); +AutoScreenMessage( SM_ComboContinuing ); + +/** @brief Accepts input, knocks down TapNotes that were stepped on, and keeps score for the player. */ +class Player: public ActorFrame +{ +public: + // The passed in NoteData isn't touched until Load() is called. + Player( NoteData &nd, bool bVisibleParts = true ); + ~Player(); + + virtual void Update( float fDeltaTime ); + virtual void DrawPrimitives(); + + struct TrackRowTapNote + { + int iTrack; + int iRow; + TapNote *pTN; + }; + void UpdateHoldNotes( int iSongRow, float fDeltaTime, vector &vTN ); + + void Init( + const RString &sType, + PlayerState* pPlayerState, + PlayerStageStats* pPlayerStageStats, + LifeMeter* pLM, + CombinedLifeMeter* pCombinedLM, + ScoreDisplay* pScoreDisplay, + ScoreDisplay* pSecondaryScoreDisplay, + Inventory* pInventory, + ScoreKeeper* pPrimaryScoreKeeper, + ScoreKeeper* pSecondaryScoreKeeper ); + void Load(); + void CrossedRows( int iLastRowCrossed, const RageTimer &now ); + bool IsOniDead() const; + + /** + * @brief Retrieve the Player's TimingData. + * + * This is primarily for a lua hook. + * @return the TimingData in question. */ + TimingData GetPlayerTimingData() const + { + return *(this->m_Timing); + } + + // Called when a fret, step, or strum type button changes + void Fret( int col, int row, const RageTimer &tm, bool bHeld, bool bRelease ); + + // Called when the strum bar is pressed down + void Strum( int col, int row, const RageTimer &tm, bool bHeld, bool bRelease ); + + // Called when the strum window passes without a row being hit + void DoStrumMiss(); + void ScoreAllActiveHoldsLetGo(); + void DoTapScoreNone(); + + enum ButtonType { ButtonType_Step, ButtonType_StrumFretsChanged, ButtonType_Hopo }; + void StepStrumHopo( int col, int row, const RageTimer &tm, bool bHeld, bool bRelease, ButtonType gbt ); + void Step( int col, int row, const RageTimer &tm, bool bHeld, bool bRelease ) { StepStrumHopo(col, row, tm, bHeld, bRelease, ButtonType_Step); } + + // called by Fret for Hammer-ons and Pull-offs + void Hopo( int col, int row, const RageTimer &tm, bool bHeld, bool bRelease ) { StepStrumHopo(col, row, tm, bHeld, bRelease, ButtonType_Hopo); } + + void RandomizeNotes( int iNoteRow ); + void FadeToFail(); + void CacheAllUsedNoteSkins(); + TapNoteScore GetLastTapNoteScore() const { return m_LastTapNoteScore; } + void ApplyWaitingTransforms(); + void SetPaused( bool bPaused ) { m_bPaused = bPaused; } + + static float GetMaxStepDistanceSeconds(); + static float GetWindowSeconds( TimingWindow tw ); + const NoteData &GetNoteData() const { return m_NoteData; } + bool HasVisibleParts() const { return m_pNoteField != nullptr; } + + void SetActorWithJudgmentPosition( Actor *pActor ) { m_pActorWithJudgmentPosition = pActor; } + void SetActorWithComboPosition( Actor *pActor ) { m_pActorWithComboPosition = pActor; } + + void SetSendJudgmentAndComboMessages( bool b ) { m_bSendJudgmentAndComboMessages = b; } + + // Lua + virtual void PushSelf( lua_State *L ); + + PlayerState * GetPlayerState() { return this->m_pPlayerState; } + +protected: + void UpdateTapNotesMissedOlderThan( float fMissIfOlderThanThisBeat ); + void UpdateJudgedRows(); + void FlashGhostRow( int iRow ); + void HandleTapRowScore( unsigned row ); + void HandleHoldScore( const TapNote &tn ); + void HandleHoldCheckpoint( int iRow, int iNumHoldsHeldThisRow, int iNumHoldsMissedThisRow, const vector &viColsWithHold ); + void DrawTapJudgments(); + void DrawHoldJudgments(); + void SendComboMessages( int iOldCombo, int iOldMissCombo ); + void PlayKeysound( const TapNote &tn, TapNoteScore score ); + + void SetMineJudgment( TapNoteScore tns ); + void SetJudgment( TapNoteScore tns, int iFirstTrack, float fTapNoteOffset ); // -1 if no track as in TNS_Miss + void SetHoldJudgment( TapNoteScore tns, HoldNoteScore hns, int iTrack ); + void SetCombo( int iCombo, int iMisses ); + + void ChangeLife( TapNoteScore tns ); + void ChangeLife( HoldNoteScore hns, TapNoteScore tns ); + void ChangeLifeRecord(); + + int GetClosestNoteDirectional( int col, int iStartRow, int iMaxRowsAhead, bool bAllowGraded, bool bForward ) const; + int GetClosestNote( int col, int iNoteRow, int iMaxRowsAhead, int iMaxRowsBehind, bool bAllowGraded ) const; + int GetClosestNonEmptyRowDirectional( int iStartRow, int iMaxRowsAhead, bool bAllowGraded, bool bForward ) const; + int GetClosestNonEmptyRow( int iNoteRow, int iMaxRowsAhead, int iMaxRowsBehind, bool bAllowGraded ) const; + + RString ApplyRandomAttack(); + + inline void HideNote( int col, int row ) + { + NoteData::iterator iter = m_NoteData.FindTapNote( col, row ); + if( iter != m_NoteData.end(col) ) + iter->second.result.bHidden = true; + } + + bool m_bLoaded; + + /** @brief The player's present state. */ + PlayerState *m_pPlayerState; + /** @brief The player's present stage stats. */ + PlayerStageStats *m_pPlayerStageStats; + TimingData *m_Timing; + float m_fNoteFieldHeight; + + bool m_bPaused; + bool m_bDelay; + + NoteData &m_NoteData; + NoteField *m_pNoteField; + + vector m_vpHoldJudgment; + + AutoActor m_sprJudgment; + AutoActor m_sprCombo; + Actor *m_pActorWithJudgmentPosition; + Actor *m_pActorWithComboPosition; + + AttackDisplay *m_pAttackDisplay; + + TapNoteScore m_LastTapNoteScore; + LifeMeter *m_pLifeMeter; + CombinedLifeMeter *m_pCombinedLifeMeter; + ScoreDisplay *m_pScoreDisplay; + ScoreDisplay *m_pSecondaryScoreDisplay; + ScoreKeeper *m_pPrimaryScoreKeeper; + ScoreKeeper *m_pSecondaryScoreKeeper; + Inventory *m_pInventory; + + int m_iFirstUncrossedRow; // used by hold checkpoints logic + NoteData::all_tracks_iterator *m_pIterNeedsTapJudging; + NoteData::all_tracks_iterator *m_pIterNeedsHoldJudging; + NoteData::all_tracks_iterator *m_pIterUncrossedRows; + NoteData::all_tracks_iterator *m_pIterUnjudgedRows; + NoteData::all_tracks_iterator *m_pIterUnjudgedMineRows; + int m_iLastSeenCombo; + JudgedRows *m_pJudgedRows; + + RageSound m_soundMine; + RageSound m_soundAttackLaunch; + RageSound m_soundAttackEnding; + + float m_fActiveRandomAttackStart; + + vector m_vbFretIsDown; + + vector m_vKeysounds; + + ThemeMetric GRAY_ARROWS_Y_STANDARD; + ThemeMetric GRAY_ARROWS_Y_REVERSE; + ThemeMetric2D ATTACK_DISPLAY_X; + ThemeMetric ATTACK_DISPLAY_Y; + ThemeMetric ATTACK_DISPLAY_Y_REVERSE; + ThemeMetric HOLD_JUDGMENT_Y_STANDARD; + ThemeMetric HOLD_JUDGMENT_Y_REVERSE; + ThemeMetric BRIGHT_GHOST_COMBO_THRESHOLD; + ThemeMetric TAP_JUDGMENTS_UNDER_FIELD; + ThemeMetric HOLD_JUDGMENTS_UNDER_FIELD; + ThemeMetric COMBO_UNDER_FIELD; + ThemeMetric DRAW_DISTANCE_AFTER_TARGET_PIXELS; + ThemeMetric DRAW_DISTANCE_BEFORE_TARGET_PIXELS; + +#define NUM_REVERSE 2 +#define NUM_CENTERED 2 + TweenState m_tsJudgment[NUM_REVERSE][NUM_CENTERED]; + TweenState m_tsCombo[NUM_REVERSE][NUM_CENTERED]; + + bool m_bSendJudgmentAndComboMessages; +}; + +class PlayerPlus +{ + Player *m_pPlayer; + NoteData m_NoteData; +public: + PlayerPlus() { m_pPlayer = new Player(m_NoteData); } + ~PlayerPlus() { delete m_pPlayer; } + void Load( const NoteData &nd ) { m_NoteData = nd; m_pPlayer->Load(); } + Player *operator->() { return m_pPlayer; } + const Player *operator->() const { return m_pPlayer; } + operator Player*() { return m_pPlayer; } + operator const Player*() const { return m_pPlayer; } +}; + +#endif + +/* + * (c) 2001-2006 Chris Danford, Steve Checkoway + * 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. + */ diff --git a/src/PlayerOptions.cpp b/src/PlayerOptions.cpp index 2eaadb93d7..5b501e7ebe 100644 --- a/src/PlayerOptions.cpp +++ b/src/PlayerOptions.cpp @@ -282,7 +282,7 @@ void PlayerOptions::FromString( const RString &sMultipleMods ) bool PlayerOptions::FromOneModString( const RString &sOneMod, RString &sErrorOut ) { - ASSERT_M( NOTESKIN != NULL, "The Noteskin Manager must be loaded in order to process mods." ); + ASSERT_M( NOTESKIN != nullptr, "The Noteskin Manager must be loaded in order to process mods." ); RString sBit = sOneMod; sBit.MakeLower(); @@ -764,8 +764,8 @@ bool PlayerOptions::IsEasierForSongAndSteps( Song* pSong, Steps* pSteps, PlayerN bool PlayerOptions::IsEasierForCourseAndTrail( Course* pCourse, Trail* pTrail ) const { - ASSERT( pCourse != NULL ); - ASSERT( pTrail != NULL ); + ASSERT( pCourse != nullptr ); + ASSERT( pTrail != nullptr ); return std::any_of(pTrail->m_vEntries.begin(), pTrail->m_vEntries.end(), [&](TrailEntry const &e) { return e.pSong && IsEasierForSongAndSteps(e.pSong, e.pSteps, PLAYER_1); diff --git a/src/Preference.cpp b/src/Preference.cpp index c16927cd87..1a7ce1e4a2 100644 --- a/src/Preference.cpp +++ b/src/Preference.cpp @@ -40,7 +40,7 @@ void IPreference::LoadAllDefaults() void IPreference::ReadAllPrefsFromNode( const XNode* pNode, bool bIsStatic ) { - ASSERT( pNode != NULL ); + ASSERT( pNode != nullptr ); for (IPreference *p : *m_Subscribers.m_pSubscribers) p->ReadFrom( pNode, bIsStatic ); } diff --git a/src/Profile.cpp b/src/Profile.cpp index e84435f1f8..307f47b5a9 100644 --- a/src/Profile.cpp +++ b/src/Profile.cpp @@ -1367,7 +1367,7 @@ XNode* Profile::SaveSongScoresCreateNode() const CHECKPOINT; const Profile* pProfile = this; - ASSERT( pProfile != NULL ); + ASSERT( pProfile != nullptr ); XNode* pNode = new XNode( "SongScores" ); @@ -1448,7 +1448,7 @@ XNode* Profile::SaveCourseScoresCreateNode() const CHECKPOINT; const Profile* pProfile = this; - ASSERT( pProfile != NULL ); + ASSERT( pProfile != nullptr ); XNode* pNode = new XNode( "CourseScores" ); @@ -1556,7 +1556,7 @@ XNode* Profile::SaveCategoryScoresCreateNode() const CHECKPOINT; const Profile* pProfile = this; - ASSERT( pProfile != NULL ); + ASSERT( pProfile != nullptr ); XNode* pNode = new XNode( "CategoryScores" ); @@ -1628,7 +1628,7 @@ void Profile::LoadCategoryScoresFromNode( const XNode* pCategoryScores ) void Profile::SaveStatsWebPageToDir( RString sDir ) const { - ASSERT( PROFILEMAN != NULL ); + ASSERT( PROFILEMAN != nullptr ); } void Profile::SaveMachinePublicKeyToDir( RString sDir ) const @@ -1664,7 +1664,7 @@ XNode* Profile::SaveScreenshotDataCreateNode() const CHECKPOINT; const Profile* pProfile = this; - ASSERT( pProfile != NULL ); + ASSERT( pProfile != nullptr ); XNode* pNode = new XNode( "ScreenshotData" ); @@ -1706,7 +1706,7 @@ XNode* Profile::SaveCalorieDataCreateNode() const CHECKPOINT; const Profile* pProfile = this; - ASSERT( pProfile != NULL ); + ASSERT( pProfile != nullptr ); XNode* pNode = new XNode( "CalorieData" ); diff --git a/src/ProfileManager.cpp b/src/ProfileManager.cpp index 9a944cb623..fdc21a92dc 100644 --- a/src/ProfileManager.cpp +++ b/src/ProfileManager.cpp @@ -114,12 +114,12 @@ void ProfileManager::Init() { RString sCharacterID = FIXED_PROFILE_CHARACTER_ID( i ); Character *pCharacter = CHARMAN->GetCharacterFromID( sCharacterID ); - ASSERT_M( pCharacter != NULL, sCharacterID ); + ASSERT_M( pCharacter != nullptr, sCharacterID ); RString sProfileID; bool b = CreateLocalProfile( pCharacter->GetDisplayName(), sProfileID ); ASSERT( b ); Profile* pProfile = GetLocalProfile( sProfileID ); - ASSERT_M( pProfile != NULL, sProfileID ); + ASSERT_M( pProfile != nullptr, sProfileID ); pProfile->m_sCharacterID = sCharacterID; SaveLocalProfile( sProfileID ); } @@ -341,7 +341,7 @@ bool ProfileManager::SaveProfile( PlayerNumber pn ) const bool ProfileManager::SaveLocalProfile( RString sProfileID ) { const Profile *pProfile = GetLocalProfile( sProfileID ); - ASSERT( pProfile != NULL ); + ASSERT( pProfile != nullptr ); RString sDir = LocalProfileIDToDir( sProfileID ); bool b = pProfile->SaveAllToDir( sDir, PREFSMAN->m_bSignProfileData ); return b; @@ -473,7 +473,7 @@ bool ProfileManager::RenameLocalProfile( RString sProfileID, RString sNewName ) ASSERT( !sProfileID.empty() ); Profile *pProfile = ProfileManager::GetLocalProfile( sProfileID ); - ASSERT( pProfile != NULL ); + ASSERT( pProfile != nullptr ); pProfile->m_sDisplayName = sNewName; RString sProfileDir = LocalProfileIDToDir( sProfileID ); @@ -483,7 +483,7 @@ bool ProfileManager::RenameLocalProfile( RString sProfileID, RString sNewName ) bool ProfileManager::DeleteLocalProfile( RString sProfileID ) { Profile *pProfile = ProfileManager::GetLocalProfile( sProfileID ); - ASSERT( pProfile != NULL ); + ASSERT( pProfile != nullptr ); RString sProfileDir = LocalProfileIDToDir( sProfileID ); // flush directory cache in an attempt to get this working @@ -821,7 +821,7 @@ class LunaProfileManager: public Luna { public: static int IsPersistentProfile( T* p, lua_State *L ) { lua_pushboolean(L, p->IsPersistentProfile(Enum::Check(L, 1)) ); return 1; } - static int GetProfile( T* p, lua_State *L ) { PlayerNumber pn = Enum::Check(L, 1); Profile* pP = p->GetProfile(pn); ASSERT(pP != NULL); pP->PushSelf(L); return 1; } + static int GetProfile( T* p, lua_State *L ) { PlayerNumber pn = Enum::Check(L, 1); Profile* pP = p->GetProfile(pn); ASSERT(pP != nullptr); pP->PushSelf(L); return 1; } static int GetMachineProfile( T* p, lua_State *L ) { p->GetMachineProfile()->PushSelf(L); return 1; } static int SaveMachineProfile( T* p, lua_State * ) { p->SaveMachineProfile(); return 0; } static int GetLocalProfile( T* p, lua_State *L ) @@ -833,7 +833,7 @@ public: lua_pushnil(L); return 1; } - static int GetLocalProfileFromIndex( T* p, lua_State *L ) { Profile *pProfile = p->GetLocalProfileFromIndex(IArg(1)); ASSERT(pProfile != NULL); pProfile->PushSelf(L); return 1; } + static int GetLocalProfileFromIndex( T* p, lua_State *L ) { Profile *pProfile = p->GetLocalProfileFromIndex(IArg(1)); ASSERT(pProfile != nullptr); pProfile->PushSelf(L); return 1; } static int GetLocalProfileIDFromIndex( T* p, lua_State *L ) { lua_pushstring(L, p->GetLocalProfileIDFromIndex(IArg(1)) ); return 1; } static int GetLocalProfileIndexFromID( T* p, lua_State *L ) { lua_pushnumber(L, p->GetLocalProfileIndexFromID(SArg(1)) ); return 1; } static int GetNumLocalProfiles( T* p, lua_State *L ) { lua_pushnumber(L, p->GetNumLocalProfiles() ); return 1; } diff --git a/src/RageBitmapTexture.cpp b/src/RageBitmapTexture.cpp index 07d0ca16a5..ffafa05f1d 100644 --- a/src/RageBitmapTexture.cpp +++ b/src/RageBitmapTexture.cpp @@ -1,361 +1,361 @@ -#include "global.h" -#include "RageBitmapTexture.h" -#include "RageUtil.h" -#include "RageLog.h" -#include "RageTextureManager.h" -#include "RageDisplay.h" -#include "RageTypes.h" -#include "RageSurface.h" -#include "RageSurfaceUtils.h" -#include "RageSurfaceUtils_Zoom.h" -#include "RageSurfaceUtils_Dither.h" -#include "RageSurface_Load.h" -#include "arch/Dialog/Dialog.h" -#include "StepMania.h" - -static void GetResolutionFromFileName( RString sPath, int &iWidth, int &iHeight ) -{ - /* Match: - * Foo (res 512x128).png - * Also allow, eg: - * Foo (dither, res 512x128).png - * Be careful that this doesn't get mixed up with frame dimensions. */ - static Regex re( "\\([^\\)]*res ([0-9]+)x([0-9]+).*\\)" ); - - vector asMatches; - if( !re.Compare(sPath, asMatches) ) - return; - - iWidth = StringToInt( asMatches[0] ); - iHeight = StringToInt( asMatches[1] ); -} - -RageBitmapTexture::RageBitmapTexture( RageTextureID name ) : - RageTexture( name ), m_uTexHandle(0) -{ - Create(); -} - -RageBitmapTexture::~RageBitmapTexture() -{ - Destroy(); -} - -void RageBitmapTexture::Reload() -{ - Destroy(); - Create(); -} - -/* - * Each dwMaxSize, dwTextureColorDepth and iAlphaBits are maximums; we may - * use less. iAlphaBits must be 0, 1 or 4. - * - * XXX: change iAlphaBits == 4 to iAlphaBits == 8 to indicate "as much alpha - * as needed", since that's what it really is; still only use 4 in 16-bit textures. - * - * Dither forces dithering when loading 16-bit textures. - * Stretch forces the loaded image to fill the texture completely. - */ -void RageBitmapTexture::Create() -{ - RageTextureID actualID = GetID(); - - ASSERT( actualID.filename != "" ); - - /* Load the image into a RageSurface. */ - RString error; - RageSurface *pImg = RageSurfaceUtils::LoadFile( actualID.filename, error ); - - /* Tolerate corrupt/unknown images. */ - if( pImg == NULL ) - { - RString sWarning = ssprintf( "RageBitmapTexture: Couldn't load %s: %s", actualID.filename.c_str(), error.c_str() ); - Dialog::OK( sWarning ); - pImg = RageSurfaceUtils::MakeDummySurface( 64, 64 ); - ASSERT( pImg != NULL ); - } - - if( actualID.bHotPinkColorKey ) - RageSurfaceUtils::ApplyHotPinkColorKey( pImg ); - - { - /* Do this after setting the color key for paletted images; it'll also return - * TRAIT_NO_TRANSPARENCY if the color key is never used. */ - int iTraits = RageSurfaceUtils::FindSurfaceTraits( pImg ); - if( iTraits & RageSurfaceUtils::TRAIT_NO_TRANSPARENCY ) - actualID.iAlphaBits = 0; - else if( iTraits & RageSurfaceUtils::TRAIT_BOOL_TRANSPARENCY ) - actualID.iAlphaBits = 1; - } - - // look in the file name for a format hints - RString sHintString = GetID().filename + actualID.AdditionalTextureHints; - sHintString.MakeLower(); - - if( sHintString.find("32bpp") != string::npos ) actualID.iColorDepth = 32; - else if( sHintString.find("16bpp") != string::npos ) actualID.iColorDepth = 16; - if( sHintString.find("dither") != string::npos ) actualID.bDither = true; - if( sHintString.find("stretch") != string::npos ) actualID.bStretch = true; - if( sHintString.find("mipmaps") != string::npos ) actualID.bMipMaps = true; - if( sHintString.find("nomipmaps") != string::npos ) actualID.bMipMaps = false; // check for "nomipmaps" after "mipmaps" - - /* If the image is marked grayscale, then use all bits not used for alpha - * for the intensity. This way, if an image has no alpha, you get an 8-bit - * grayscale; if it only has boolean transparency, you get a 7-bit grayscale. */ - if( sHintString.find("grayscale") != string::npos ) actualID.iGrayscaleBits = 8-actualID.iAlphaBits; - - /* This indicates that the only component in the texture is alpha; assume all - * color is white. */ - if( sHintString.find("alphamap") != string::npos ) actualID.iGrayscaleBits = 0; - - /* No iGrayscaleBits for images that are already paletted. We don't support - * that; and that hint is intended for use on images that are already grayscale, - * it's not intended to change a color image into a grayscale image. */ - if( actualID.iGrayscaleBits != -1 && pImg->format->BitsPerPixel == 8 ) - actualID.iGrayscaleBits = -1; - - /* Cap the max texture size to the hardware max. */ - actualID.iMaxSize = min( actualID.iMaxSize, DISPLAY->GetMaxTextureSize() ); - - /* Save information about the source. */ - m_iSourceWidth = pImg->w; - m_iSourceHeight = pImg->h; - - /* in-game imsage dimensions are the same as the source graphic */ - m_iImageWidth = m_iSourceWidth; - m_iImageHeight = m_iSourceHeight; - - /* if "doubleres" (high resolution) and we're not allowing high res textures, then image dimensions are half of the source */ - if( sHintString.find("doubleres") != string::npos ) - { - if( !StepMania::GetHighResolutionTextures() ) - { - m_iImageWidth = m_iImageWidth / 2; - m_iImageHeight = m_iImageHeight / 2; - } - } - - /* image size cannot exceed max size */ - m_iImageWidth = min( m_iImageWidth, actualID.iMaxSize ); - m_iImageHeight = min( m_iImageHeight, actualID.iMaxSize ); - - /* Texture dimensions need to be a power of two; jump to the next. */ - m_iTextureWidth = power_of_two(m_iImageWidth); - m_iTextureHeight = power_of_two(m_iImageHeight); - - /* If we're under 8x8, increase it, to avoid filtering problems on odd hardware. */ - if( m_iTextureWidth < 8 || m_iTextureHeight < 8 ) - { - actualID.bStretch = true; - m_iTextureWidth = max( 8, m_iTextureWidth ); - m_iTextureHeight = max( 8, m_iTextureHeight ); - } - - ASSERT_M( m_iTextureWidth <= actualID.iMaxSize, ssprintf("w %i, %i", m_iTextureWidth, actualID.iMaxSize) ); - ASSERT_M( m_iTextureHeight <= actualID.iMaxSize, ssprintf("h %i, %i", m_iTextureHeight, actualID.iMaxSize) ); - - if( actualID.bStretch ) - { - /* The hints asked for the image to be stretched to the texture size, - * probably for tiling. */ - m_iImageWidth = m_iTextureWidth; - m_iImageHeight = m_iTextureHeight; - } - - if( pImg->w != m_iImageWidth || pImg->h != m_iImageHeight ) - RageSurfaceUtils::Zoom( pImg, m_iImageWidth, m_iImageHeight ); - - if( actualID.iGrayscaleBits != -1 && DISPLAY->SupportsTextureFormat(RagePixelFormat_PAL) ) - { - RageSurface *pGrayscale = RageSurfaceUtils::PalettizeToGrayscale( pImg, actualID.iGrayscaleBits, actualID.iAlphaBits ); - - delete pImg; - pImg = pGrayscale; - } - - // Figure out which texture format we want the renderer to use. - RagePixelFormat pixfmt; - - // If the source is palleted, always load as paletted if supported. - if( pImg->format->BitsPerPixel == 8 && DISPLAY->SupportsTextureFormat(RagePixelFormat_PAL) ) - { - pixfmt = RagePixelFormat_PAL; - } - else - { - // not paletted - switch( actualID.iColorDepth ) - { - case 16: - { - // Bits of alpha in the source: - int iSourceAlphaBits = 8 - pImg->format->Loss[3]; - - // Don't use more than we were hinted to. - iSourceAlphaBits = min( actualID.iAlphaBits, iSourceAlphaBits ); - - switch( iSourceAlphaBits ) - { - case 0: - case 1: - pixfmt = RagePixelFormat_RGB5A1; - break; - default: - pixfmt = RagePixelFormat_RGBA4; - break; - } - } - break; - case 32: - pixfmt = RagePixelFormat_RGBA8; - break; - default: FAIL_M( ssprintf("%i", actualID.iColorDepth) ); - } - } - - // Make we're using a supported format. Every card supports either RGBA8 or RGBA4. - if( !DISPLAY->SupportsTextureFormat(pixfmt) ) - { - pixfmt = RagePixelFormat_RGBA8; - if( !DISPLAY->SupportsTextureFormat(pixfmt) ) - pixfmt = RagePixelFormat_RGBA4; - } - - /* Dither if appropriate. - * XXX: This is a special case: don't bother dithering to RGBA8888. - * We actually want to dither only if the destination has greater color depth - * on at least one color channel than the source. For example, it doesn't - * make sense to do this when pixfmt is RGBA5551 if the image is only RGBA555. */ - if( actualID.bDither && - (pixfmt==RagePixelFormat_RGBA4 || pixfmt==RagePixelFormat_RGB5A1) ) - { - // Dither down to the destination format. - const RageDisplay::RagePixelFormatDesc *pfd = DISPLAY->GetPixelFormatDesc(pixfmt); - RageSurface *dst = CreateSurface( pImg->w, pImg->h, pfd->bpp, - pfd->masks[0], pfd->masks[1], pfd->masks[2], pfd->masks[3] ); - - RageSurfaceUtils::ErrorDiffusionDither( pImg, dst ); - delete pImg; - pImg = dst; - } - - /* This needs to be done *after* the final resize, since that resize - * may introduce new alpha bits that need to be set. It needs to be - * done *before* we set up the palette, since it might change it. */ - RageSurfaceUtils::FixHiddenAlpha( pImg ); - - /* Scale up to the texture size, if needed. */ - RageSurfaceUtils::ConvertSurface( pImg, m_iTextureWidth, m_iTextureHeight, - pImg->fmt.BitsPerPixel, pImg->fmt.Mask[0], pImg->fmt.Mask[1], pImg->fmt.Mask[2], pImg->fmt.Mask[3] ); - - m_uTexHandle = DISPLAY->CreateTexture( pixfmt, pImg, actualID.bMipMaps ); - - CreateFrameRects(); - - - { - // Enforce frames in the image have even dimensions. - // Otherwise, pixel/texel alignment will be off. - int iDimensionMultiple = 2; - - if( sHintString.find("doubleres") != string::npos ) - { - iDimensionMultiple = 4; - } - - bool bRunCheck = true; - - // Don't check if the artist intentionally blanked the image by making it very tiny. - if( this->GetSourceWidth()<=iDimensionMultiple || this->GetSourceHeight()<=iDimensionMultiple ) - bRunCheck = false; - - // HACK: Don't check song graphics. Many of them are weird dimensions. - if( !TEXTUREMAN->GetOddDimensionWarning() ) - bRunCheck = false; - - if( bRunCheck ) - { - float fFrameWidth = this->GetSourceWidth() / (float)this->GetFramesWide(); - float fFrameHeight = this->GetSourceHeight() / (float)this->GetFramesHigh(); - float fBetterFrameWidth = ceilf(fFrameWidth/iDimensionMultiple) * iDimensionMultiple; - float fBetterFrameHeight = ceilf(fFrameHeight/iDimensionMultiple) * iDimensionMultiple; - float fBetterSourceWidth = this->GetFramesWide() * fBetterFrameWidth; - float fBetterSourceHeight = this->GetFramesHigh() * fBetterFrameHeight; - if( fFrameWidth!=fBetterFrameWidth || fFrameHeight!=fBetterFrameHeight ) - { - RString sWarning = ssprintf( - "The graphic '%s' has frame dimensions that aren't a multiple of %d.\n" - "The entire image is %dx%d and frame size is %.1fx%.1f.\n" - "Image quality will be much improved if you resize the graphic to %.0fx%.0f, which is a frame size of %.0fx%.0f.", - actualID.filename.c_str(), - iDimensionMultiple, - this->GetSourceWidth(), this->GetSourceHeight(), - fFrameWidth, fFrameHeight, - fBetterSourceWidth, fBetterSourceHeight, - fBetterFrameWidth, fBetterFrameHeight ); - LOG->Warn( "%s", sWarning.c_str() ); - Dialog::OK( sWarning, "FRAME_DIMENSIONS_WARNING" ); - } - } - } - - - delete pImg; - - // Check for hints that override the apparent "size". - GetResolutionFromFileName( actualID.filename, m_iSourceWidth, m_iSourceHeight ); - - /* if "doubleres" (high resolution) then we want the image to appear in-game - * with dimensions 1/2 of the source. So, cut down the source dimension here - * after everything above is finished operating with the real image - * source dimensions. */ - if( sHintString.find("doubleres") != string::npos ) - { - m_iSourceWidth = m_iSourceWidth / 2; - m_iSourceHeight = m_iSourceHeight / 2; - } - - - RString sProperties; - sProperties += RagePixelFormatToString( pixfmt ) + " "; - if( actualID.iAlphaBits == 0 ) sProperties += "opaque "; - if( actualID.iAlphaBits == 1 ) sProperties += "matte "; - if( actualID.bStretch ) sProperties += "stretch "; - if( actualID.bDither ) sProperties += "dither "; - sProperties.erase( sProperties.size()-1 ); - LOG->Trace( "RageBitmapTexture: Loaded '%s' (%ux%u); %s, source %d,%d; image %d,%d.", - actualID.filename.c_str(), GetTextureWidth(), GetTextureHeight(), - sProperties.c_str(), m_iSourceWidth, m_iSourceHeight, - m_iImageWidth, m_iImageHeight ); -} - -void RageBitmapTexture::Destroy() -{ - DISPLAY->DeleteTexture( m_uTexHandle ); -} - -/* - * Copyright (c) 2001-2004 Chris Danford, Glenn Maynard - * 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. - */ +#include "global.h" +#include "RageBitmapTexture.h" +#include "RageUtil.h" +#include "RageLog.h" +#include "RageTextureManager.h" +#include "RageDisplay.h" +#include "RageTypes.h" +#include "RageSurface.h" +#include "RageSurfaceUtils.h" +#include "RageSurfaceUtils_Zoom.h" +#include "RageSurfaceUtils_Dither.h" +#include "RageSurface_Load.h" +#include "arch/Dialog/Dialog.h" +#include "StepMania.h" + +static void GetResolutionFromFileName( RString sPath, int &iWidth, int &iHeight ) +{ + /* Match: + * Foo (res 512x128).png + * Also allow, eg: + * Foo (dither, res 512x128).png + * Be careful that this doesn't get mixed up with frame dimensions. */ + static Regex re( "\\([^\\)]*res ([0-9]+)x([0-9]+).*\\)" ); + + vector asMatches; + if( !re.Compare(sPath, asMatches) ) + return; + + iWidth = StringToInt( asMatches[0] ); + iHeight = StringToInt( asMatches[1] ); +} + +RageBitmapTexture::RageBitmapTexture( RageTextureID name ) : + RageTexture( name ), m_uTexHandle(0) +{ + Create(); +} + +RageBitmapTexture::~RageBitmapTexture() +{ + Destroy(); +} + +void RageBitmapTexture::Reload() +{ + Destroy(); + Create(); +} + +/* + * Each dwMaxSize, dwTextureColorDepth and iAlphaBits are maximums; we may + * use less. iAlphaBits must be 0, 1 or 4. + * + * XXX: change iAlphaBits == 4 to iAlphaBits == 8 to indicate "as much alpha + * as needed", since that's what it really is; still only use 4 in 16-bit textures. + * + * Dither forces dithering when loading 16-bit textures. + * Stretch forces the loaded image to fill the texture completely. + */ +void RageBitmapTexture::Create() +{ + RageTextureID actualID = GetID(); + + ASSERT( actualID.filename != "" ); + + /* Load the image into a RageSurface. */ + RString error; + RageSurface *pImg = RageSurfaceUtils::LoadFile( actualID.filename, error ); + + /* Tolerate corrupt/unknown images. */ + if( pImg == NULL ) + { + RString sWarning = ssprintf( "RageBitmapTexture: Couldn't load %s: %s", actualID.filename.c_str(), error.c_str() ); + Dialog::OK( sWarning ); + pImg = RageSurfaceUtils::MakeDummySurface( 64, 64 ); + ASSERT( pImg != nullptr ); + } + + if( actualID.bHotPinkColorKey ) + RageSurfaceUtils::ApplyHotPinkColorKey( pImg ); + + { + /* Do this after setting the color key for paletted images; it'll also return + * TRAIT_NO_TRANSPARENCY if the color key is never used. */ + int iTraits = RageSurfaceUtils::FindSurfaceTraits( pImg ); + if( iTraits & RageSurfaceUtils::TRAIT_NO_TRANSPARENCY ) + actualID.iAlphaBits = 0; + else if( iTraits & RageSurfaceUtils::TRAIT_BOOL_TRANSPARENCY ) + actualID.iAlphaBits = 1; + } + + // look in the file name for a format hints + RString sHintString = GetID().filename + actualID.AdditionalTextureHints; + sHintString.MakeLower(); + + if( sHintString.find("32bpp") != string::npos ) actualID.iColorDepth = 32; + else if( sHintString.find("16bpp") != string::npos ) actualID.iColorDepth = 16; + if( sHintString.find("dither") != string::npos ) actualID.bDither = true; + if( sHintString.find("stretch") != string::npos ) actualID.bStretch = true; + if( sHintString.find("mipmaps") != string::npos ) actualID.bMipMaps = true; + if( sHintString.find("nomipmaps") != string::npos ) actualID.bMipMaps = false; // check for "nomipmaps" after "mipmaps" + + /* If the image is marked grayscale, then use all bits not used for alpha + * for the intensity. This way, if an image has no alpha, you get an 8-bit + * grayscale; if it only has boolean transparency, you get a 7-bit grayscale. */ + if( sHintString.find("grayscale") != string::npos ) actualID.iGrayscaleBits = 8-actualID.iAlphaBits; + + /* This indicates that the only component in the texture is alpha; assume all + * color is white. */ + if( sHintString.find("alphamap") != string::npos ) actualID.iGrayscaleBits = 0; + + /* No iGrayscaleBits for images that are already paletted. We don't support + * that; and that hint is intended for use on images that are already grayscale, + * it's not intended to change a color image into a grayscale image. */ + if( actualID.iGrayscaleBits != -1 && pImg->format->BitsPerPixel == 8 ) + actualID.iGrayscaleBits = -1; + + /* Cap the max texture size to the hardware max. */ + actualID.iMaxSize = min( actualID.iMaxSize, DISPLAY->GetMaxTextureSize() ); + + /* Save information about the source. */ + m_iSourceWidth = pImg->w; + m_iSourceHeight = pImg->h; + + /* in-game imsage dimensions are the same as the source graphic */ + m_iImageWidth = m_iSourceWidth; + m_iImageHeight = m_iSourceHeight; + + /* if "doubleres" (high resolution) and we're not allowing high res textures, then image dimensions are half of the source */ + if( sHintString.find("doubleres") != string::npos ) + { + if( !StepMania::GetHighResolutionTextures() ) + { + m_iImageWidth = m_iImageWidth / 2; + m_iImageHeight = m_iImageHeight / 2; + } + } + + /* image size cannot exceed max size */ + m_iImageWidth = min( m_iImageWidth, actualID.iMaxSize ); + m_iImageHeight = min( m_iImageHeight, actualID.iMaxSize ); + + /* Texture dimensions need to be a power of two; jump to the next. */ + m_iTextureWidth = power_of_two(m_iImageWidth); + m_iTextureHeight = power_of_two(m_iImageHeight); + + /* If we're under 8x8, increase it, to avoid filtering problems on odd hardware. */ + if( m_iTextureWidth < 8 || m_iTextureHeight < 8 ) + { + actualID.bStretch = true; + m_iTextureWidth = max( 8, m_iTextureWidth ); + m_iTextureHeight = max( 8, m_iTextureHeight ); + } + + ASSERT_M( m_iTextureWidth <= actualID.iMaxSize, ssprintf("w %i, %i", m_iTextureWidth, actualID.iMaxSize) ); + ASSERT_M( m_iTextureHeight <= actualID.iMaxSize, ssprintf("h %i, %i", m_iTextureHeight, actualID.iMaxSize) ); + + if( actualID.bStretch ) + { + /* The hints asked for the image to be stretched to the texture size, + * probably for tiling. */ + m_iImageWidth = m_iTextureWidth; + m_iImageHeight = m_iTextureHeight; + } + + if( pImg->w != m_iImageWidth || pImg->h != m_iImageHeight ) + RageSurfaceUtils::Zoom( pImg, m_iImageWidth, m_iImageHeight ); + + if( actualID.iGrayscaleBits != -1 && DISPLAY->SupportsTextureFormat(RagePixelFormat_PAL) ) + { + RageSurface *pGrayscale = RageSurfaceUtils::PalettizeToGrayscale( pImg, actualID.iGrayscaleBits, actualID.iAlphaBits ); + + delete pImg; + pImg = pGrayscale; + } + + // Figure out which texture format we want the renderer to use. + RagePixelFormat pixfmt; + + // If the source is palleted, always load as paletted if supported. + if( pImg->format->BitsPerPixel == 8 && DISPLAY->SupportsTextureFormat(RagePixelFormat_PAL) ) + { + pixfmt = RagePixelFormat_PAL; + } + else + { + // not paletted + switch( actualID.iColorDepth ) + { + case 16: + { + // Bits of alpha in the source: + int iSourceAlphaBits = 8 - pImg->format->Loss[3]; + + // Don't use more than we were hinted to. + iSourceAlphaBits = min( actualID.iAlphaBits, iSourceAlphaBits ); + + switch( iSourceAlphaBits ) + { + case 0: + case 1: + pixfmt = RagePixelFormat_RGB5A1; + break; + default: + pixfmt = RagePixelFormat_RGBA4; + break; + } + } + break; + case 32: + pixfmt = RagePixelFormat_RGBA8; + break; + default: FAIL_M( ssprintf("%i", actualID.iColorDepth) ); + } + } + + // Make we're using a supported format. Every card supports either RGBA8 or RGBA4. + if( !DISPLAY->SupportsTextureFormat(pixfmt) ) + { + pixfmt = RagePixelFormat_RGBA8; + if( !DISPLAY->SupportsTextureFormat(pixfmt) ) + pixfmt = RagePixelFormat_RGBA4; + } + + /* Dither if appropriate. + * XXX: This is a special case: don't bother dithering to RGBA8888. + * We actually want to dither only if the destination has greater color depth + * on at least one color channel than the source. For example, it doesn't + * make sense to do this when pixfmt is RGBA5551 if the image is only RGBA555. */ + if( actualID.bDither && + (pixfmt==RagePixelFormat_RGBA4 || pixfmt==RagePixelFormat_RGB5A1) ) + { + // Dither down to the destination format. + const RageDisplay::RagePixelFormatDesc *pfd = DISPLAY->GetPixelFormatDesc(pixfmt); + RageSurface *dst = CreateSurface( pImg->w, pImg->h, pfd->bpp, + pfd->masks[0], pfd->masks[1], pfd->masks[2], pfd->masks[3] ); + + RageSurfaceUtils::ErrorDiffusionDither( pImg, dst ); + delete pImg; + pImg = dst; + } + + /* This needs to be done *after* the final resize, since that resize + * may introduce new alpha bits that need to be set. It needs to be + * done *before* we set up the palette, since it might change it. */ + RageSurfaceUtils::FixHiddenAlpha( pImg ); + + /* Scale up to the texture size, if needed. */ + RageSurfaceUtils::ConvertSurface( pImg, m_iTextureWidth, m_iTextureHeight, + pImg->fmt.BitsPerPixel, pImg->fmt.Mask[0], pImg->fmt.Mask[1], pImg->fmt.Mask[2], pImg->fmt.Mask[3] ); + + m_uTexHandle = DISPLAY->CreateTexture( pixfmt, pImg, actualID.bMipMaps ); + + CreateFrameRects(); + + + { + // Enforce frames in the image have even dimensions. + // Otherwise, pixel/texel alignment will be off. + int iDimensionMultiple = 2; + + if( sHintString.find("doubleres") != string::npos ) + { + iDimensionMultiple = 4; + } + + bool bRunCheck = true; + + // Don't check if the artist intentionally blanked the image by making it very tiny. + if( this->GetSourceWidth()<=iDimensionMultiple || this->GetSourceHeight()<=iDimensionMultiple ) + bRunCheck = false; + + // HACK: Don't check song graphics. Many of them are weird dimensions. + if( !TEXTUREMAN->GetOddDimensionWarning() ) + bRunCheck = false; + + if( bRunCheck ) + { + float fFrameWidth = this->GetSourceWidth() / (float)this->GetFramesWide(); + float fFrameHeight = this->GetSourceHeight() / (float)this->GetFramesHigh(); + float fBetterFrameWidth = ceilf(fFrameWidth/iDimensionMultiple) * iDimensionMultiple; + float fBetterFrameHeight = ceilf(fFrameHeight/iDimensionMultiple) * iDimensionMultiple; + float fBetterSourceWidth = this->GetFramesWide() * fBetterFrameWidth; + float fBetterSourceHeight = this->GetFramesHigh() * fBetterFrameHeight; + if( fFrameWidth!=fBetterFrameWidth || fFrameHeight!=fBetterFrameHeight ) + { + RString sWarning = ssprintf( + "The graphic '%s' has frame dimensions that aren't a multiple of %d.\n" + "The entire image is %dx%d and frame size is %.1fx%.1f.\n" + "Image quality will be much improved if you resize the graphic to %.0fx%.0f, which is a frame size of %.0fx%.0f.", + actualID.filename.c_str(), + iDimensionMultiple, + this->GetSourceWidth(), this->GetSourceHeight(), + fFrameWidth, fFrameHeight, + fBetterSourceWidth, fBetterSourceHeight, + fBetterFrameWidth, fBetterFrameHeight ); + LOG->Warn( "%s", sWarning.c_str() ); + Dialog::OK( sWarning, "FRAME_DIMENSIONS_WARNING" ); + } + } + } + + + delete pImg; + + // Check for hints that override the apparent "size". + GetResolutionFromFileName( actualID.filename, m_iSourceWidth, m_iSourceHeight ); + + /* if "doubleres" (high resolution) then we want the image to appear in-game + * with dimensions 1/2 of the source. So, cut down the source dimension here + * after everything above is finished operating with the real image + * source dimensions. */ + if( sHintString.find("doubleres") != string::npos ) + { + m_iSourceWidth = m_iSourceWidth / 2; + m_iSourceHeight = m_iSourceHeight / 2; + } + + + RString sProperties; + sProperties += RagePixelFormatToString( pixfmt ) + " "; + if( actualID.iAlphaBits == 0 ) sProperties += "opaque "; + if( actualID.iAlphaBits == 1 ) sProperties += "matte "; + if( actualID.bStretch ) sProperties += "stretch "; + if( actualID.bDither ) sProperties += "dither "; + sProperties.erase( sProperties.size()-1 ); + LOG->Trace( "RageBitmapTexture: Loaded '%s' (%ux%u); %s, source %d,%d; image %d,%d.", + actualID.filename.c_str(), GetTextureWidth(), GetTextureHeight(), + sProperties.c_str(), m_iSourceWidth, m_iSourceHeight, + m_iImageWidth, m_iImageHeight ); +} + +void RageBitmapTexture::Destroy() +{ + DISPLAY->DeleteTexture( m_uTexHandle ); +} + +/* + * Copyright (c) 2001-2004 Chris Danford, Glenn Maynard + * 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. + */ diff --git a/src/RageDisplay_D3D.cpp b/src/RageDisplay_D3D.cpp index e97865f5fd..205a93ada1 100644 --- a/src/RageDisplay_D3D.cpp +++ b/src/RageDisplay_D3D.cpp @@ -1,1472 +1,1472 @@ -#include "global.h" -#include "RageDisplay.h" -#include "RageDisplay_D3D.h" -#include "RageUtil.h" -#include "RageLog.h" -#include "RageTimer.h" -#include "RageException.h" -#include "RageMath.h" -#include "RageTypes.h" -#include "RageSurface.h" -#include "RageSurfaceUtils.h" -#include "EnumHelper.h" -#include "DisplayResolutions.h" -#include "LocalizedString.h" - -#include -#include -#include - -#include "archutils/Win32/GraphicsWindow.h" - -// Static libraries -// load Windows D3D9 dynamically -#if defined(_MSC_VER) - #pragma comment(lib, "d3d9.lib") - #pragma comment(lib, "d3dx9.lib") - #pragma comment(lib, "DxErr.lib") -#endif - -#include -#include - -RString GetErrorString( HRESULT hr ) -{ - return DXGetErrorString(hr); -} - -// Globals -HMODULE g_D3D9_Module = NULL; -LPDIRECT3D9 g_pd3d = NULL; -LPDIRECT3DDEVICE9 g_pd3dDevice = NULL; -D3DCAPS9 g_DeviceCaps; -D3DDISPLAYMODE g_DesktopMode; -D3DPRESENT_PARAMETERS g_d3dpp; -int g_ModelMatrixCnt=0; -static bool g_bSphereMapping[NUM_TextureUnit] = { false, false }; - -// TODO: Instead of defining this here, enumerate the possible formats and select whatever one we want to use. This format should -// be fine for the uses of this application though. -const D3DFORMAT g_DefaultAdapterFormat = D3DFMT_X8R8G8B8; - -/* Direct3D doesn't associate a palette with textures. Instead, we load a - * palette into a slot. We need to keep track of which texture's palette is - * stored in what slot. */ -map g_TexResourceToPaletteIndex; -list g_PaletteIndex; -struct TexturePalette { PALETTEENTRY p[256]; }; -map g_TexResourceToTexturePalette; - -// Load the palette, if any, for the given texture into a palette slot, and make it current. -static void SetPalette( unsigned TexResource ) -{ - // If the texture isn't paletted, we have nothing to do. - if( g_TexResourceToTexturePalette.find(TexResource) == g_TexResourceToTexturePalette.end() ) - return; - - // Is the palette already loaded? - if( g_TexResourceToPaletteIndex.find(TexResource) == g_TexResourceToPaletteIndex.end() ) - { - // It's not. Grab the least recently used slot. - int iPalIndex = g_PaletteIndex.front(); - - // If any other texture is currently using this slot, mark that palette unloaded. - for( map::iterator i = g_TexResourceToPaletteIndex.begin(); i != g_TexResourceToPaletteIndex.end(); ++i ) - { - if( i->second != iPalIndex ) - continue; - g_TexResourceToPaletteIndex.erase(i); - break; - } - - // Load it. - TexturePalette& pal = g_TexResourceToTexturePalette[TexResource]; - g_pd3dDevice->SetPaletteEntries( iPalIndex, pal.p ); - - g_TexResourceToPaletteIndex[TexResource] = iPalIndex; - } - - const int iPalIndex = g_TexResourceToPaletteIndex[TexResource]; - - // Find this palette index in the least-recently-used queue and move it to the end. - for(list::iterator i = g_PaletteIndex.begin(); i != g_PaletteIndex.end(); ++i) - { - if( *i != iPalIndex ) - continue; - g_PaletteIndex.erase(i); - g_PaletteIndex.push_back(iPalIndex); - break; - } - - g_pd3dDevice->SetCurrentTexturePalette( iPalIndex ); -} - -#define D3DFVF_RageSpriteVertex (D3DFVF_XYZ|D3DFVF_NORMAL|D3DFVF_DIFFUSE|D3DFVF_TEX1) -#define D3DFVF_RageModelVertex (D3DFVF_XYZ|D3DFVF_NORMAL|D3DFVF_TEX1) - - -static const RageDisplay::RagePixelFormatDesc PIXEL_FORMAT_DESC[NUM_RagePixelFormat] = { - { - /* A8B8G8R8 */ - 32, - { 0x00FF0000, - 0x0000FF00, - 0x000000FF, - 0xFF000000 } - }, { - 0, { 0,0,0,0 } - }, { - /* A4R4G4B4 */ - 16, - { 0x0F00, - 0x00F0, - 0x000F, - 0xF000 }, - }, { - /* A1B5G5R5 */ - 16, - { 0x7C00, - 0x03E0, - 0x001F, - 0x8000 }, - }, { - /* X1R5G5B5 */ - 16, - { 0x7C00, - 0x03E0, - 0x001F, - 0x0000 }, - }, { - /* B8G8R8 */ - 24, - { 0xFF0000, - 0x00FF00, - 0x0000FF, - 0x000000 } - }, { - /* Paletted */ - 8, - { 0,0,0,0 } /* N/A */ - }, { - /* BGR (N/A; OpenGL only) */ - 0, { 0,0,0,0 } - }, { - /* ABGR (N/A; OpenGL only) */ - 0, { 0,0,0,0 } - }, { - /* X1R5G5B5 */ - 0, { 0,0,0,0 } - } -}; - -static D3DFORMAT D3DFORMATS[NUM_RagePixelFormat] = -{ - D3DFMT_A8R8G8B8, - D3DFMT_UNKNOWN, - D3DFMT_A4R4G4B4, - D3DFMT_A1R5G5B5, - D3DFMT_X1R5G5B5, - D3DFMT_R8G8B8, - D3DFMT_P8, - D3DFMT_UNKNOWN, // no BGR - D3DFMT_UNKNOWN, // no ABGR - D3DFMT_UNKNOWN, // X1R5G5B5 -}; - -const RageDisplay::RagePixelFormatDesc *RageDisplay_D3D::GetPixelFormatDesc(RagePixelFormat pf) const -{ - ASSERT( pf < NUM_RagePixelFormat ); - return &PIXEL_FORMAT_DESC[pf]; -} - - -RageDisplay_D3D::RageDisplay_D3D() -{ - -} - -static LocalizedString D3D_NOT_INSTALLED ( "RageDisplay_D3D", "DirectX 9.0c or greater is not installed. You can download it from:" ); -const RString D3D_URL = "http://www.microsoft.com/en-us/download/details.aspx?id=8109"; -static LocalizedString HARDWARE_ACCELERATION_NOT_AVAILABLE ( "RageDisplay_D3D", - "Your system is reporting that Direct3D hardware acceleration is not available. Please obtain an updated driver from your video card manufacturer." ); -RString RageDisplay_D3D::Init( const VideoModeParams &p, bool /* bAllowUnacceleratedRenderer */ ) -{ - GraphicsWindow::Initialize( true ); - - LOG->Trace( "RageDisplay_D3D::RageDisplay_D3D()" ); - LOG->MapLog("renderer", "Current renderer: Direct3D"); - - g_pd3d = Direct3DCreate9(D3D_SDK_VERSION); - if(!g_pd3d) - { - LOG->Trace( "Direct3DCreate9 failed" ); - return D3D_NOT_INSTALLED.GetValue(); - } - - if( FAILED( g_pd3d->GetDeviceCaps(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, &g_DeviceCaps) ) ) - return HARDWARE_ACCELERATION_NOT_AVAILABLE.GetValue(); - - - D3DADAPTER_IDENTIFIER9 identifier; - g_pd3d->GetAdapterIdentifier( D3DADAPTER_DEFAULT, 0, &identifier ); - - LOG->Trace( - "Driver: %s\n" - "Description: %s\n" - "Max texture size: %d\n" - "Alpha in palette: %s\n", - identifier.Driver, - identifier.Description, - g_DeviceCaps.MaxTextureWidth, - (g_DeviceCaps.TextureCaps & D3DPTEXTURECAPS_ALPHAPALETTE) ? "yes" : "no" ); - - LOG->Trace( "This display adaptor supports the following modes:" ); - D3DDISPLAYMODE mode; - - UINT modeCount = g_pd3d->GetAdapterModeCount(D3DADAPTER_DEFAULT, g_DefaultAdapterFormat); - - for( UINT u=0; u < modeCount; u++ ) - if( SUCCEEDED( g_pd3d->EnumAdapterModes( D3DADAPTER_DEFAULT, g_DefaultAdapterFormat, u, &mode ) ) ) - LOG->Trace( " %ux%u %uHz, format %d", mode.Width, mode.Height, mode.RefreshRate, mode.Format ); - - g_PaletteIndex.clear(); - for( int i = 0; i < 256; ++i ) - g_PaletteIndex.push_back(i); - - // Save the original desktop format. - g_pd3d->GetAdapterDisplayMode( D3DADAPTER_DEFAULT, &g_DesktopMode ); - - /* Up until now, all we've done is set up g_pd3d and do some queries. Now, - * actually initialize the window. Do this after as many error conditions as - * possible, because if we have to shut it down again we'll flash a window briefly. */ - bool bIgnore = false; - return SetVideoMode( p, bIgnore ); -} - -RageDisplay_D3D::~RageDisplay_D3D() -{ - LOG->Trace( "RageDisplay_D3D::~RageDisplay()" ); - - GraphicsWindow::Shutdown(); - - if( g_pd3dDevice ) - { - g_pd3dDevice->Release(); - g_pd3dDevice = NULL; - } - - if( g_pd3d ) - { - g_pd3d->Release(); - g_pd3d = NULL; - } - - /* Even after we call Release(), D3D may still affect our window. It seems - * to subclass the window, and never release it. Free the DLL after - * destroying the window. */ - if( g_D3D9_Module ) - { - FreeLibrary( g_D3D9_Module ); - g_D3D9_Module = NULL; - } -} - -void RageDisplay_D3D::GetDisplayResolutions( DisplayResolutions &out ) const -{ - out.clear(); - int iCnt = g_pd3d->GetAdapterModeCount( D3DADAPTER_DEFAULT, g_DefaultAdapterFormat ); - - for( int i = 0; i < iCnt; ++i ) - { - D3DDISPLAYMODE mode; - g_pd3d->EnumAdapterModes( D3DADAPTER_DEFAULT, g_DefaultAdapterFormat, i, &mode ); - - DisplayResolution res = { mode.Width, mode.Height }; - out.insert( res ); - } -} - -D3DFORMAT FindBackBufferType(bool bWindowed, int iBPP) -{ - HRESULT hr; - - // If windowed, then bpp is ignored. Use whatever works. - vector vBackBufferFormats; // throw all possibilities in here - - // When windowed, add all formats; otherwise add only formats that match dwBPP. - if( iBPP == 16 || bWindowed ) - { - vBackBufferFormats.push_back( D3DFMT_R5G6B5 ); - vBackBufferFormats.push_back( D3DFMT_X1R5G5B5 ); - vBackBufferFormats.push_back( D3DFMT_A1R5G5B5 ); - } - if( iBPP == 32 || bWindowed ) - { - vBackBufferFormats.push_back( D3DFMT_R8G8B8 ); - vBackBufferFormats.push_back( D3DFMT_X8R8G8B8 ); - vBackBufferFormats.push_back( D3DFMT_A8R8G8B8 ); - } - - if( !bWindowed && iBPP != 16 && iBPP != 32 ) - { - GraphicsWindow::Shutdown(); - RageException::Throw( "Invalid BPP '%i' specified", iBPP ); - } - - // Test each back buffer format until we find something that works. - for( unsigned i=0; i < vBackBufferFormats.size(); i++ ) - { - D3DFORMAT fmtBackBuffer = vBackBufferFormats[i]; - - D3DFORMAT fmtDisplay; - if( bWindowed ) - fmtDisplay = g_DesktopMode.Format; - else // Fullscreen - fmtDisplay = vBackBufferFormats[i]; - - LOG->Trace( "Testing format: display %d, back buffer %d, windowed %d...", - fmtDisplay, fmtBackBuffer, bWindowed ); - - hr = g_pd3d->CheckDeviceType( D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, - fmtDisplay, fmtBackBuffer, bWindowed ); - - if( FAILED(hr) ) - continue; // skip - - // done searching - LOG->Trace( "This will work." ); - return fmtBackBuffer; - } - - LOG->Trace( "Couldn't find an appropriate back buffer format." ); - return D3DFMT_UNKNOWN; -} - -RString SetD3DParams( bool &bNewDeviceOut ) -{ - if( g_pd3dDevice == NULL ) // device is not yet created. We need to create it - { - bNewDeviceOut = true; - HRESULT hr = g_pd3d->CreateDevice( - D3DADAPTER_DEFAULT, - D3DDEVTYPE_HAL, - GraphicsWindow::GetHwnd(), - D3DCREATE_SOFTWARE_VERTEXPROCESSING | D3DCREATE_MULTITHREADED, - &g_d3dpp, - &g_pd3dDevice ); - if( FAILED(hr) ) - { - // Likely D3D_ERR_INVALIDCALL. The driver probably doesn't support this video mode. - return ssprintf( "CreateDevice failed: '%s'", GetErrorString(hr).c_str() ); - } - } - else - { - bNewDeviceOut = false; - //LOG->Warn( "Resetting D3D device" ); - HRESULT hr = g_pd3dDevice->Reset( &g_d3dpp ); - if( FAILED(hr) ) - { - // Likely D3D_ERR_INVALIDCALL. The driver probably doesn't support this video mode. - return ssprintf("g_pd3dDevice->Reset failed: '%s'", GetErrorString(hr).c_str() ); - } - } - - g_pd3dDevice->SetRenderState( D3DRS_NORMALIZENORMALS, TRUE ); - - // Palettes were lost by Reset(), so mark them unloaded. - g_TexResourceToPaletteIndex.clear(); - - return RString(); -} - -// If the given parameters have failed, try to lower them. -static bool D3DReduceParams( D3DPRESENT_PARAMETERS *pp ) -{ - D3DDISPLAYMODE current; - current.Format = pp->BackBufferFormat; - current.Height = pp->BackBufferHeight; - current.Width = pp->BackBufferWidth; - current.RefreshRate = pp->FullScreen_RefreshRateInHz; - - const int iCnt = g_pd3d->GetAdapterModeCount( D3DADAPTER_DEFAULT, g_DefaultAdapterFormat ); - int iBest = -1; - int iBestScore = 0; - LOG->Trace( "cur: %ux%u %uHz, format %i", current.Width, current.Height, current.RefreshRate, current.Format ); - for( int i = 0; i < iCnt; ++i ) - { - D3DDISPLAYMODE mode; - g_pd3d->EnumAdapterModes( D3DADAPTER_DEFAULT, g_DefaultAdapterFormat, i, &mode ); - - // Never change the format. - if( mode.Format != current.Format ) - continue; - // Never increase the parameters. - if( mode.Height > current.Height || mode.Width > current.Width || mode.RefreshRate > current.RefreshRate ) - continue; - - // Never go below 640x480 unless we already are. - if( (current.Width >= 640 && current.Height >= 480) && (mode.Width < 640 || mode.Height < 480) ) - continue; - - // Never go below 60Hz. - if( mode.RefreshRate && mode.RefreshRate < 60 ) - continue; - - /* If mode.RefreshRate is 0, it means "default". We don't know what - * that means; assume it's 60Hz. */ - - // Higher scores are better. - int iScore = 0; - if( current.RefreshRate >= 70 && mode.RefreshRate < 70 ) - { - /* Top priority: we really want to avoid dropping to a refresh rate - * that's below 70Hz. */ - iScore -= 100000; - } - else if( mode.RefreshRate < current.RefreshRate ) - { - /* Low priority: We're lowering the refresh rate, but not too far. - * current.RefreshRate might be 0, in which case this simply gives - * points for higher refresh rates. */ - iScore += (mode.RefreshRate - current.RefreshRate); - } - - // Medium priority: - int iResolutionDiff = (current.Height - mode.Height) + (current.Width - mode.Width); - iScore -= iResolutionDiff * 100; - - if( iBest == -1 || iScore > iBestScore ) - { - iBest = i; - iBestScore = iScore; - } - - LOG->Trace( "try: %ux%u %uHz, format %i: score %i", mode.Width, mode.Height, mode.RefreshRate, mode.Format, iScore ); - } - - if( iBest == -1 ) - return false; - - D3DDISPLAYMODE BestMode; - g_pd3d->EnumAdapterModes( D3DADAPTER_DEFAULT, g_DefaultAdapterFormat, iBest, &BestMode ); - pp->BackBufferHeight = BestMode.Height; - pp->BackBufferWidth = BestMode.Width; - pp->FullScreen_RefreshRateInHz = BestMode.RefreshRate; - - return true; -} - - -static void SetPresentParametersFromVideoModeParams( const VideoModeParams &p, D3DPRESENT_PARAMETERS *pD3Dpp ) -{ - ZERO( *pD3Dpp ); - - pD3Dpp->BackBufferWidth = p.width; - pD3Dpp->BackBufferHeight = p.height; - pD3Dpp->BackBufferFormat = FindBackBufferType( p.windowed, p.bpp ); - pD3Dpp->BackBufferCount = 1; - pD3Dpp->MultiSampleType = D3DMULTISAMPLE_NONE; - pD3Dpp->SwapEffect = D3DSWAPEFFECT_DISCARD; - pD3Dpp->hDeviceWindow = GraphicsWindow::GetHwnd(); - pD3Dpp->Windowed = p.windowed; - pD3Dpp->EnableAutoDepthStencil = TRUE; - pD3Dpp->AutoDepthStencilFormat = D3DFMT_D16; - - if( p.windowed ) - pD3Dpp->PresentationInterval = D3DPRESENT_INTERVAL_DEFAULT; - else - pD3Dpp->PresentationInterval = p.vsync ? D3DPRESENT_INTERVAL_ONE : D3DPRESENT_INTERVAL_IMMEDIATE; - - pD3Dpp->FullScreen_RefreshRateInHz = D3DPRESENT_RATE_DEFAULT; - if( !p.windowed && p.rate != REFRESH_DEFAULT ) - pD3Dpp->FullScreen_RefreshRateInHz = p.rate; - - pD3Dpp->Flags = 0; - - LOG->Trace( "Present Parameters: %d, %d, %d, %d, %d, %d, %d, %d, %d, %d, %d, %d, %d", - pD3Dpp->BackBufferWidth, pD3Dpp->BackBufferHeight, pD3Dpp->BackBufferFormat, - pD3Dpp->BackBufferCount, - pD3Dpp->MultiSampleType, pD3Dpp->SwapEffect, pD3Dpp->hDeviceWindow, - pD3Dpp->Windowed, pD3Dpp->EnableAutoDepthStencil, pD3Dpp->AutoDepthStencilFormat, - pD3Dpp->Flags, pD3Dpp->FullScreen_RefreshRateInHz, - pD3Dpp->PresentationInterval - ); -} - -// Set the video mode. -RString RageDisplay_D3D::TryVideoMode( const VideoModeParams &_p, bool &bNewDeviceOut ) -{ - VideoModeParams p = _p; - LOG->Warn( "RageDisplay_D3D::TryVideoMode( %d, %d, %d, %d, %d, %d )", p.windowed, p.width, p.height, p.bpp, p.rate, p.vsync ); - - if( FindBackBufferType( p.windowed, p.bpp ) == D3DFMT_UNKNOWN ) // no possible back buffer formats - return ssprintf( "FindBackBufferType(%i,%i) failed", p.windowed, p.bpp ); // failed to set mode - - /* Set up and display the window before setting up D3D. If we don't do this, - * then setting up a fullscreen window (when we're not coming from windowed) - * causes all other windows on the system to be resized to the new resolution. */ - GraphicsWindow::CreateGraphicsWindow( p ); - - SetPresentParametersFromVideoModeParams( p, &g_d3dpp ); - - // Display the window immediately, so we don't display the desktop ... - while( 1 ) - { - // Try the video mode. - RString sErr = SetD3DParams( bNewDeviceOut ); - if( sErr.empty() ) - break; - - /* It failed. We're probably selecting a video mode that isn't supported. - * If we're fullscreen, search the mode list and find the nearest lower mode. */ - if( p.windowed || !D3DReduceParams( &g_d3dpp ) ) - return sErr; - - // Store the new settings we're about to try. - p.height = g_d3dpp.BackBufferHeight; - p.width = g_d3dpp.BackBufferWidth; - if( g_d3dpp.FullScreen_RefreshRateInHz == D3DPRESENT_RATE_DEFAULT ) - p.rate = REFRESH_DEFAULT; - else - p.rate = g_d3dpp.FullScreen_RefreshRateInHz; - } - - /* Call this again after changing the display mode. If we're going to a window - * from fullscreen, the first call can't set a larger window than the old - * fullscreen resolution or set the window position. */ - GraphicsWindow::CreateGraphicsWindow( p ); - - ResolutionChanged(); - - return RString(); // mode change successful -} - -void RageDisplay_D3D::ResolutionChanged() -{ - //LOG->Warn( "RageDisplay_D3D::ResolutionChanged" ); - - RageDisplay::ResolutionChanged(); -} - -int RageDisplay_D3D::GetMaxTextureSize() const -{ - return g_DeviceCaps.MaxTextureWidth; -} - -bool RageDisplay_D3D::BeginFrame() -{ - GraphicsWindow::Update(); - - switch( g_pd3dDevice->TestCooperativeLevel() ) - { - case D3DERR_DEVICELOST: - return false; - case D3DERR_DEVICENOTRESET: - { - bool bIgnore = false; - RString sError = SetD3DParams( bIgnore ); - if( sError != "" ) - RageException::Throw( sError ); - - break; - } - } - - g_pd3dDevice->Clear( 0, NULL, D3DCLEAR_TARGET|D3DCLEAR_ZBUFFER, - D3DCOLOR_XRGB(0,0,0), 1.0f, 0x00000000 ); - g_pd3dDevice->BeginScene(); - - return RageDisplay::BeginFrame(); -} - -static RageTimer g_LastFrameEndedAt( RageZeroTimer ); -void RageDisplay_D3D::EndFrame() -{ - g_pd3dDevice->EndScene(); - - FrameLimitBeforeVsync( GetActualVideoModeParams().rate ); - g_pd3dDevice->Present( 0, 0, 0, 0 ); - FrameLimitAfterVsync(); - - RageDisplay::EndFrame(); -} - -bool RageDisplay_D3D::SupportsTextureFormat( RagePixelFormat pixfmt, bool realtime ) -{ - // Some cards (Savage) don't support alpha in palettes. - // Don't allow paletted textures if this is the case. - if( pixfmt == RagePixelFormat_PAL && !(g_DeviceCaps.TextureCaps & D3DPTEXTURECAPS_ALPHAPALETTE) ) - return false; - - if( D3DFORMATS[pixfmt] == D3DFMT_UNKNOWN ) - return false; - - D3DFORMAT d3dfmt = D3DFORMATS[pixfmt]; - HRESULT hr = g_pd3d->CheckDeviceFormat( - D3DADAPTER_DEFAULT, - D3DDEVTYPE_HAL, - g_d3dpp.BackBufferFormat, - 0, - D3DRTYPE_TEXTURE, - d3dfmt); - - return SUCCEEDED( hr ); -} - -bool RageDisplay_D3D::SupportsThreadedRendering() -{ - return true; -} - -RageSurface* RageDisplay_D3D::CreateScreenshot() -{ - RageSurface * result = NULL; - - // Get the back buffer. - IDirect3DSurface9* pSurface; - if( SUCCEEDED( g_pd3dDevice->GetBackBuffer( 0, 0, D3DBACKBUFFER_TYPE_MONO, &pSurface ) ) ) - { - // Get the back buffer description. - D3DSURFACE_DESC desc; - pSurface->GetDesc( &desc ); - - // Copy the back buffer into a surface of a type we support. - IDirect3DSurface9* pCopy; - if( SUCCEEDED( g_pd3dDevice->CreateOffscreenPlainSurface( desc.Width, desc.Height, D3DFMT_A8R8G8B8, D3DPOOL_SCRATCH, &pCopy, NULL ) ) ) - { - if( SUCCEEDED( D3DXLoadSurfaceFromSurface( pCopy, NULL, NULL, pSurface, NULL, NULL, D3DX_FILTER_NONE, 0) ) ) - { - // Update desc from the copy. - pCopy->GetDesc( &desc ); - - D3DLOCKED_RECT lr; - - { - RECT rect; - rect.left = 0; - rect.top = 0; - rect.right = desc.Width; - rect.bottom = desc.Height; - - pCopy->LockRect( &lr, &rect, D3DLOCK_READONLY ); - } - - RageSurface *surface = CreateSurfaceFromPixfmt( RagePixelFormat_RGBA8, lr.pBits, desc.Width, desc.Height, lr.Pitch); - ASSERT( surface != NULL ); - - // We need to make a copy, since lr.pBits will go away when we call UnlockRect(). - result = - CreateSurface( surface->w, surface->h, - surface->format->BitsPerPixel, - surface->format->Rmask, surface->format->Gmask, - surface->format->Bmask, surface->format->Amask ); - RageSurfaceUtils::CopySurface( surface, result ); - delete surface; - - pCopy->UnlockRect(); - } - - pCopy->Release(); - } - - pSurface->Release(); - } - - return result; -} - -VideoModeParams RageDisplay_D3D::GetActualVideoModeParams() const -{ - VideoModeParams p = GraphicsWindow::GetParams(); - return p; -} - -void RageDisplay_D3D::SendCurrentMatrices() -{ - RageMatrix m; - RageMatrixMultiply( &m, GetCentering(), GetProjectionTop() ); - - // Convert to OpenGL-style "pixel-centered" coords - RageMatrix m2 = GetCenteringMatrix( -0.5f, -0.5f, 0, 0 ); - RageMatrix projection; - RageMatrixMultiply( &projection, &m2, &m ); - g_pd3dDevice->SetTransform( D3DTS_PROJECTION, (D3DMATRIX*)&projection ); - - g_pd3dDevice->SetTransform( D3DTS_VIEW, (D3DMATRIX*)GetViewTop() ); - g_pd3dDevice->SetTransform( D3DTS_WORLD, (D3DMATRIX*)GetWorldTop() ); - - FOREACH_ENUM( TextureUnit, tu ) - { - // Optimization opportunity: Turn off texture transform if not using texture coords. - g_pd3dDevice->SetTextureStageState( tu, D3DTSS_TEXTURETRANSFORMFLAGS, D3DTTFF_COUNT2 ); - - // If no texture is set for this texture unit, don't bother setting it up. - IDirect3DBaseTexture9* pTexture = NULL; - g_pd3dDevice->GetTexture( tu, &pTexture ); - if( pTexture == NULL ) - continue; - pTexture->Release(); - - if( g_bSphereMapping[tu] ) - { - static const RageMatrix tex = RageMatrix - ( - 0.5f, 0.0f, 0.0f, 0.0f, - 0.0f, -0.5f, 0.0f, 0.0f, - 0.0f, 0.0f, 0.0f, 0.0f, - 0.5f, -0.5f, 0.0f, 1.0f - ); - g_pd3dDevice->SetTransform( (D3DTRANSFORMSTATETYPE)(D3DTS_TEXTURE0+tu), (D3DMATRIX*)&tex ); - - // Tell D3D to use transformed reflection vectors as texture co-ordinate 0 - // and then transform this coordinate by the specified texture matrix. - g_pd3dDevice->SetTextureStageState( tu, D3DTSS_TEXCOORDINDEX, D3DTSS_TCI_CAMERASPACEREFLECTIONVECTOR ); - } - else - { - /* Direct3D is expecting a 3x3 matrix loaded into the 4x4 in order - * to transform the 2-component texture coordinates. We currently - * only use translate and scale, and ignore the z component entirely, - * so convert the texture matrix from 4x4 to 3x3 by dropping z. */ - - const RageMatrix &tex1 = *GetTextureTop(); - const RageMatrix tex2 = RageMatrix - ( - tex1.m[0][0], tex1.m[0][1], tex1.m[0][3], 0, - tex1.m[1][0], tex1.m[1][1], tex1.m[1][3], 0, - tex1.m[3][0], tex1.m[3][1], tex1.m[3][3], 0, - 0, 0, 0, 0 - ); - g_pd3dDevice->SetTransform( D3DTRANSFORMSTATETYPE(D3DTS_TEXTURE0+tu), (D3DMATRIX*)&tex2 ); - - g_pd3dDevice->SetTextureStageState( tu, D3DTSS_TEXCOORDINDEX, D3DTSS_TCI_PASSTHRU ); - } - } -} - -class RageCompiledGeometrySWD3D : public RageCompiledGeometry -{ -public: - void Allocate( const vector &vMeshes ) - { - m_vVertex.resize( max(1u, GetTotalVertices()) ); - m_vTriangles.resize( max(1u, GetTotalTriangles()) ); - } - void Change( const vector &vMeshes ) - { - for( unsigned i=0; i &Vertices = mesh.Vertices; - const vector &Triangles = mesh.Triangles; - - for( unsigned j=0; jGetTransform( D3DTS_TEXTURE0, (D3DMATRIX*)&m ); - - m.m[2][0] = 0; - m.m[2][1] = 0; - - g_pd3dDevice->SetTransform( D3DTS_TEXTURE0, (D3DMATRIX*)&m ); - } - - g_pd3dDevice->SetFVF( D3DFVF_RageModelVertex ); - g_pd3dDevice->DrawIndexedPrimitiveUP( - D3DPT_TRIANGLELIST, // PrimitiveType - meshInfo.iVertexStart, // MinIndex - meshInfo.iVertexCount, // NumVertices - meshInfo.iTriangleCount, // PrimitiveCount, - &m_vTriangles[0]+meshInfo.iTriangleStart,// pIndexData, - D3DFMT_INDEX16, // IndexDataFormat, - &m_vVertex[0], // pVertexStreamZeroData, - sizeof(m_vVertex[0]) // VertexStreamZeroStride - ); - } - -protected: - vector m_vVertex; - vector m_vTriangles; -}; - -RageCompiledGeometry* RageDisplay_D3D::CreateCompiledGeometry() -{ - return new RageCompiledGeometrySWD3D; -} - -void RageDisplay_D3D::DeleteCompiledGeometry( RageCompiledGeometry* p ) -{ - delete p; -} - -void RageDisplay_D3D::DrawQuadsInternal( const RageSpriteVertex v[], int iNumVerts ) -{ - // there isn't a quad primitive in D3D, so we have to fake it with indexed triangles - int iNumQuads = iNumVerts/4; - int iNumTriangles = iNumQuads*2; - int iNumIndices = iNumTriangles*3; - - // make a temporary index buffer - static vector vIndices; - unsigned uOldSize = vIndices.size(); - unsigned uNewSize = max(uOldSize,(unsigned)iNumIndices); - vIndices.resize( uNewSize ); - for( uint16_t i=(uint16_t)uOldSize/6; i<(uint16_t)iNumQuads; i++ ) - { - vIndices[i*6+0] = i*4+0; - vIndices[i*6+1] = i*4+1; - vIndices[i*6+2] = i*4+2; - vIndices[i*6+3] = i*4+2; - vIndices[i*6+4] = i*4+3; - vIndices[i*6+5] = i*4+0; - } - - g_pd3dDevice->SetFVF( D3DFVF_RageSpriteVertex ); - SendCurrentMatrices(); - g_pd3dDevice->DrawIndexedPrimitiveUP( - D3DPT_TRIANGLELIST, // PrimitiveType - 0, // MinIndex - iNumVerts, // NumVertices - iNumTriangles, // PrimitiveCount, - &vIndices[0], // pIndexData, - D3DFMT_INDEX16, // IndexDataFormat, - v, // pVertexStreamZeroData, - sizeof(RageSpriteVertex) // VertexStreamZeroStride - ); -} - -void RageDisplay_D3D::DrawQuadStripInternal( const RageSpriteVertex v[], int iNumVerts ) -{ - // there isn't a quad strip primitive in D3D, so we have to fake it with indexed triangles - int iNumQuads = (iNumVerts-2)/2; - int iNumTriangles = iNumQuads*2; - int iNumIndices = iNumTriangles*3; - - // make a temporary index buffer - static vector vIndices; - unsigned uOldSize = vIndices.size(); - unsigned uNewSize = max(uOldSize,(unsigned)iNumIndices); - vIndices.resize( uNewSize ); - for( uint16_t i=(uint16_t)uOldSize/6; i<(uint16_t)iNumQuads; i++ ) - { - vIndices[i*6+0] = i*2+0; - vIndices[i*6+1] = i*2+1; - vIndices[i*6+2] = i*2+2; - vIndices[i*6+3] = i*2+1; - vIndices[i*6+4] = i*2+2; - vIndices[i*6+5] = i*2+3; - } - - g_pd3dDevice->SetFVF( D3DFVF_RageSpriteVertex ); - SendCurrentMatrices(); - g_pd3dDevice->DrawIndexedPrimitiveUP( - D3DPT_TRIANGLELIST, // PrimitiveType - 0, // MinIndex - iNumVerts, // NumVertices - iNumTriangles, // PrimitiveCount, - &vIndices[0], // pIndexData, - D3DFMT_INDEX16, // IndexDataFormat, - v, // pVertexStreamZeroData, - sizeof(RageSpriteVertex) // VertexStreamZeroStride - ); -} - -void RageDisplay_D3D::DrawSymmetricQuadStripInternal( const RageSpriteVertex v[], int iNumVerts ) -{ - int iNumPieces = (iNumVerts-3)/3; - int iNumTriangles = iNumPieces*4; - int iNumIndices = iNumTriangles*3; - - // make a temporary index buffer - static vector vIndices; - unsigned uOldSize = vIndices.size(); - unsigned uNewSize = max(uOldSize,(unsigned)iNumIndices); - vIndices.resize( uNewSize ); - for( uint16_t i=(uint16_t)uOldSize/12; i<(uint16_t)iNumPieces; i++ ) - { - // { 1, 3, 0 } { 1, 4, 3 } { 1, 5, 4 } { 1, 2, 5 } - vIndices[i*12+0] = i*3+1; - vIndices[i*12+1] = i*3+3; - vIndices[i*12+2] = i*3+0; - vIndices[i*12+3] = i*3+1; - vIndices[i*12+4] = i*3+4; - vIndices[i*12+5] = i*3+3; - vIndices[i*12+6] = i*3+1; - vIndices[i*12+7] = i*3+5; - vIndices[i*12+8] = i*3+4; - vIndices[i*12+9] = i*3+1; - vIndices[i*12+10] = i*3+2; - vIndices[i*12+11] = i*3+5; - } - - g_pd3dDevice->SetFVF( D3DFVF_RageSpriteVertex ); - SendCurrentMatrices(); - g_pd3dDevice->DrawIndexedPrimitiveUP( - D3DPT_TRIANGLELIST, // PrimitiveType - 0, // MinIndex - iNumVerts, // NumVertices - iNumTriangles, // PrimitiveCount, - &vIndices[0], // pIndexData, - D3DFMT_INDEX16, // IndexDataFormat, - v, // pVertexStreamZeroData, - sizeof(RageSpriteVertex) // VertexStreamZeroStride - ); -} - -void RageDisplay_D3D::DrawFanInternal( const RageSpriteVertex v[], int iNumVerts ) -{ - g_pd3dDevice->SetFVF( D3DFVF_RageSpriteVertex ); - SendCurrentMatrices(); - g_pd3dDevice->DrawPrimitiveUP( - D3DPT_TRIANGLEFAN, // PrimitiveType - iNumVerts-2, // PrimitiveCount, - v, // pVertexStreamZeroData, - sizeof(RageSpriteVertex) - ); -} - -void RageDisplay_D3D::DrawStripInternal( const RageSpriteVertex v[], int iNumVerts ) -{ - g_pd3dDevice->SetFVF( D3DFVF_RageSpriteVertex ); - SendCurrentMatrices(); - g_pd3dDevice->DrawPrimitiveUP( - D3DPT_TRIANGLESTRIP, // PrimitiveType - iNumVerts-2, // PrimitiveCount, - v, // pVertexStreamZeroData, - sizeof(RageSpriteVertex) - ); -} - -void RageDisplay_D3D::DrawTrianglesInternal( const RageSpriteVertex v[], int iNumVerts ) -{ - g_pd3dDevice->SetFVF( D3DFVF_RageSpriteVertex ); - SendCurrentMatrices(); - g_pd3dDevice->DrawPrimitiveUP( - D3DPT_TRIANGLELIST, // PrimitiveType - iNumVerts/3, // PrimitiveCount, - v, // pVertexStreamZeroData, - sizeof(RageSpriteVertex) - ); -} - -void RageDisplay_D3D::DrawCompiledGeometryInternal( const RageCompiledGeometry *p, int iMeshIndex ) -{ - SendCurrentMatrices(); - - /* If lighting is off, then the current material will have no effect. We - * want to still be able to color models with lighting off, so shove the - * material color in texture factor and modify the texture stage to use it - * instead of the vertex color (our models don't have vertex coloring anyway). */ - DWORD bLighting; - g_pd3dDevice->GetRenderState( D3DRS_LIGHTING, &bLighting ); - - if( !bLighting ) - { - g_pd3dDevice->SetTextureStageState( 0, D3DTSS_COLORARG2, D3DTA_TFACTOR ); - g_pd3dDevice->SetTextureStageState( 0, D3DTSS_ALPHAARG2, D3DTA_TFACTOR ); - } - - p->Draw( iMeshIndex ); - - if( !bLighting ) - { - g_pd3dDevice->SetTextureStageState( 0, D3DTSS_COLORARG2, D3DTA_CURRENT ); - g_pd3dDevice->SetTextureStageState( 0, D3DTSS_ALPHAARG2, D3DTA_CURRENT ); - } -} - -/* Use the default poly-based implementation. D3D lines apparently don't support - * AA with greater-than-one widths. */ -/* -void RageDisplay_D3D::DrawLineStrip( const RageSpriteVertex v[], int iNumVerts, float LineWidth ) -{ - ASSERT( iNumVerts >= 2 ); - g_pd3dDevice->SetRenderState( D3DRS_POINTSIZE, *((DWORD*)&LineWidth) ); // funky cast. See D3DRENDERSTATETYPE doc - g_pd3dDevice->SetVertexShader( D3DFVF_RageSpriteVertex ); - SendCurrentMatrices(); - g_pd3dDevice->DrawPrimitiveUP( - D3DPT_LINESTRIP, // PrimitiveType - iNumVerts-1, // PrimitiveCount, - v, // pVertexStreamZeroData, - sizeof(RageSpriteVertex) - ); - StatsAddVerts( iNumVerts ); -} -*/ - -void RageDisplay_D3D::ClearAllTextures() -{ - FOREACH_ENUM( TextureUnit, i ) - SetTexture( i, 0 ); -} - -int RageDisplay_D3D::GetNumTextureUnits() -{ - return g_DeviceCaps.MaxSimultaneousTextures; -} - -void RageDisplay_D3D::SetTexture( TextureUnit tu, unsigned iTexture ) -{ -// g_DeviceCaps.MaxSimultaneousTextures = 1; - if( tu >= (int) g_DeviceCaps.MaxSimultaneousTextures ) // not supported - return; - - if( iTexture == 0 ) - { - g_pd3dDevice->SetTexture( tu, NULL ); - - /* Intentionally commented out. Don't mess with texture stage state - * when just setting the texture. Model sets its texture modes before - * setting the final texture. */ - //g_pd3dDevice->SetTextureStageState( tu, D3DTSS_COLOROP, D3DTOP_DISABLE ); - } - else - { - IDirect3DTexture9* pTex = (IDirect3DTexture9*) iTexture; - g_pd3dDevice->SetTexture( tu, pTex ); - - /* Intentionally commented out. Don't mess with texture stage state - * when just setting the texture. Model sets its texture modes before - * setting the final texture. */ - //g_pd3dDevice->SetTextureStageState( tu, D3DTSS_COLOROP, D3DTOP_MODULATE ); - - // Set palette (if any) - SetPalette( iTexture ); - } -} - -void RageDisplay_D3D::SetTextureMode( TextureUnit tu, TextureMode tm ) -{ - if( tu >= (int) g_DeviceCaps.MaxSimultaneousTextures ) // not supported - return; - - switch( tm ) - { - case TextureMode_Modulate: - // Use D3DTA_CURRENT instead of diffuse so that multitexturing works - // properly. For stage 0, D3DTA_CURRENT is the diffuse color. - - g_pd3dDevice->SetTextureStageState( tu, D3DTSS_COLORARG1, D3DTA_TEXTURE ); - g_pd3dDevice->SetTextureStageState( tu, D3DTSS_COLORARG2, D3DTA_CURRENT ); - g_pd3dDevice->SetTextureStageState( tu, D3DTSS_COLOROP, D3DTOP_MODULATE ); - g_pd3dDevice->SetTextureStageState( tu, D3DTSS_ALPHAARG1, D3DTA_TEXTURE ); - g_pd3dDevice->SetTextureStageState( tu, D3DTSS_ALPHAARG2, D3DTA_CURRENT ); - g_pd3dDevice->SetTextureStageState( tu, D3DTSS_ALPHAOP, D3DTOP_MODULATE ); - break; - case TextureMode_Add: - g_pd3dDevice->SetTextureStageState( tu, D3DTSS_COLORARG1, D3DTA_TEXTURE ); - g_pd3dDevice->SetTextureStageState( tu, D3DTSS_COLORARG2, D3DTA_CURRENT ); - g_pd3dDevice->SetTextureStageState( tu, D3DTSS_COLOROP, D3DTOP_ADD ); - g_pd3dDevice->SetTextureStageState( tu, D3DTSS_ALPHAARG1, D3DTA_TEXTURE ); - g_pd3dDevice->SetTextureStageState( tu, D3DTSS_ALPHAARG2, D3DTA_CURRENT ); - g_pd3dDevice->SetTextureStageState( tu, D3DTSS_ALPHAOP, D3DTOP_MODULATE ); - break; - case TextureMode_Glow: - g_pd3dDevice->SetTextureStageState( tu, D3DTSS_COLORARG1, D3DTA_TEXTURE ); - g_pd3dDevice->SetTextureStageState( tu, D3DTSS_COLORARG2, D3DTA_CURRENT ); - g_pd3dDevice->SetTextureStageState( tu, D3DTSS_COLOROP, D3DTOP_SELECTARG2 ); - g_pd3dDevice->SetTextureStageState( tu, D3DTSS_ALPHAARG1, D3DTA_TEXTURE ); - g_pd3dDevice->SetTextureStageState( tu, D3DTSS_ALPHAARG2, D3DTA_CURRENT ); - g_pd3dDevice->SetTextureStageState( tu, D3DTSS_ALPHAOP, D3DTOP_MODULATE ); - break; - } -} - -void RageDisplay_D3D::SetTextureFiltering( TextureUnit tu, bool b ) -{ - if( tu >= (int) g_DeviceCaps.MaxSimultaneousTextures ) // not supported - return; - - g_pd3dDevice->SetSamplerState( tu, D3DSAMP_MINFILTER, b ? D3DTEXF_LINEAR : D3DTEXF_POINT ); - g_pd3dDevice->SetSamplerState( tu, D3DSAMP_MAGFILTER, b ? D3DTEXF_LINEAR : D3DTEXF_POINT ); -} - -void RageDisplay_D3D::SetBlendMode( BlendMode mode ) -{ - g_pd3dDevice->SetRenderState( D3DRS_ALPHABLENDENABLE, TRUE ); - - if( mode == BLEND_INVERT_DEST ) - g_pd3dDevice->SetRenderState( D3DRS_BLENDOP, D3DBLENDOP_SUBTRACT ); - else - g_pd3dDevice->SetRenderState( D3DRS_BLENDOP, D3DBLENDOP_ADD ); - - switch( mode ) - { - case BLEND_NORMAL: - g_pd3dDevice->SetRenderState( D3DRS_SRCBLEND, D3DBLEND_SRCALPHA ); - g_pd3dDevice->SetRenderState( D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA ); - break; - case BLEND_ADD: - g_pd3dDevice->SetRenderState( D3DRS_SRCBLEND, D3DBLEND_SRCALPHA ); - g_pd3dDevice->SetRenderState( D3DRS_DESTBLEND, D3DBLEND_ONE ); - break; - case BLEND_MODULATE: - g_pd3dDevice->SetRenderState( D3DRS_SRCBLEND, D3DBLEND_ZERO ); - g_pd3dDevice->SetRenderState( D3DRS_DESTBLEND, D3DBLEND_SRCCOLOR ); - break; - case BLEND_COPY_SRC: - g_pd3dDevice->SetRenderState( D3DRS_SRCBLEND, D3DBLEND_ONE ); - g_pd3dDevice->SetRenderState( D3DRS_DESTBLEND, D3DBLEND_ZERO ); - break; - /* Effects currently missing in D3D: BLEND_ALPHA_MASK, BLEND_ALPHA_KNOCK_OUT - * These two may require DirectX9 since D3DRS_SRCALPHA and D3DRS_DESTALPHA - * don't seem to exist in DX8. -aj */ - case BLEND_ALPHA_MASK: - // RGB: iSourceRGB = GL_ZERO; iDestRGB = GL_ONE; - g_pd3dDevice->SetRenderState( D3DRS_SRCBLEND, D3DBLEND_ZERO ); - g_pd3dDevice->SetRenderState( D3DRS_DESTBLEND, D3DBLEND_ONE ); - // Alpha: iSourceAlpha = GL_ZERO; iDestAlpha = GL_SRC_ALPHA; - /* - g_pd3dDevice->SetRenderState( D3DRS_SRCALPHA, D3DBLEND_ZERO ); - g_pd3dDevice->SetRenderState( D3DRS_DESTALPHA, D3DBLEND_SRCALPHA ); - */ - break; - case BLEND_ALPHA_KNOCK_OUT: - // RGB: iSourceRGB = GL_ZERO; iDestRGB = GL_ONE; - g_pd3dDevice->SetRenderState( D3DRS_SRCBLEND, D3DBLEND_ZERO ); - g_pd3dDevice->SetRenderState( D3DRS_DESTBLEND, D3DBLEND_ONE ); - // Alpha: iSourceAlpha = GL_ZERO; iDestAlpha = GL_ONE_MINUS_SRC_ALPHA; - /* - g_pd3dDevice->SetRenderState( D3DRS_SRCBLENDALPHA, D3DBLEND_ZERO ); - g_pd3dDevice->SetRenderState( D3DRS_DESTBLENDALPHA, D3DBLEND_INVSRCALPHA ); - */ - break; - case BLEND_ALPHA_MULTIPLY: - g_pd3dDevice->SetRenderState( D3DRS_SRCBLEND, D3DBLEND_SRCALPHA ); - g_pd3dDevice->SetRenderState( D3DRS_DESTBLEND, D3DBLEND_ZERO ); - break; - case BLEND_WEIGHTED_MULTIPLY: - g_pd3dDevice->SetRenderState( D3DRS_SRCBLEND, D3DBLEND_DESTCOLOR ); - g_pd3dDevice->SetRenderState( D3DRS_DESTBLEND, D3DBLEND_SRCCOLOR ); - break; - case BLEND_INVERT_DEST: - g_pd3dDevice->SetRenderState( D3DRS_SRCBLEND, D3DBLEND_ONE ); - g_pd3dDevice->SetRenderState( D3DRS_DESTBLEND, D3DBLEND_ONE ); - break; - case BLEND_NO_EFFECT: - g_pd3dDevice->SetRenderState( D3DRS_SRCBLEND, D3DBLEND_ZERO ); - g_pd3dDevice->SetRenderState( D3DRS_DESTBLEND, D3DBLEND_ONE ); - break; - default: - FAIL_M(ssprintf("Invalid BlendMode: %i", mode)); - } -} - -bool RageDisplay_D3D::IsZWriteEnabled() const -{ - DWORD b; - g_pd3dDevice->GetRenderState( D3DRS_ZWRITEENABLE, &b ); - return b!=0; -} - -void RageDisplay_D3D::SetZBias( float f ) -{ - D3DVIEWPORT9 viewData; - g_pd3dDevice->GetViewport( &viewData ); - viewData.MinZ = SCALE( f, 0.0f, 1.0f, 0.05f, 0.0f ); - viewData.MaxZ = SCALE( f, 0.0f, 1.0f, 1.0f, 0.95f ); - g_pd3dDevice->SetViewport( &viewData ); -} - - -bool RageDisplay_D3D::IsZTestEnabled() const -{ - DWORD b; - g_pd3dDevice->GetRenderState( D3DRS_ZFUNC, &b ); - return b!=D3DCMP_ALWAYS; -} - -void RageDisplay_D3D::SetZWrite( bool b ) -{ - g_pd3dDevice->SetRenderState( D3DRS_ZWRITEENABLE, b ); -} - -void RageDisplay_D3D::SetZTestMode( ZTestMode mode ) -{ - g_pd3dDevice->SetRenderState( D3DRS_ZENABLE, D3DZB_TRUE ); - DWORD dw; - switch( mode ) - { - case ZTEST_OFF: dw = D3DCMP_ALWAYS; break; - case ZTEST_WRITE_ON_PASS: dw = D3DCMP_LESSEQUAL; break; - case ZTEST_WRITE_ON_FAIL: dw = D3DCMP_GREATER; break; - default: - dw = D3DCMP_NEVER; - FAIL_M(ssprintf("Invalid ZTestMode: %i", mode)); - } - g_pd3dDevice->SetRenderState( D3DRS_ZFUNC, dw ); -} - -void RageDisplay_D3D::ClearZBuffer() -{ - g_pd3dDevice->Clear( 0, NULL, D3DCLEAR_ZBUFFER, D3DCOLOR_XRGB(0,0,0), 1.0f, 0x00000000 ); -} - -void RageDisplay_D3D::SetTextureWrapping( TextureUnit tu, bool b ) -{ - if( tu >= (int) g_DeviceCaps.MaxSimultaneousTextures ) // not supported - return; - - int mode = b ? D3DTADDRESS_WRAP : D3DTADDRESS_CLAMP; - g_pd3dDevice->SetSamplerState( tu, D3DSAMP_ADDRESSU, mode ); - g_pd3dDevice->SetSamplerState( tu, D3DSAMP_ADDRESSV, mode ); -} - -void RageDisplay_D3D::SetMaterial( - const RageColor &emissive, - const RageColor &ambient, - const RageColor &diffuse, - const RageColor &specular, - float shininess - ) -{ - /* If lighting is off, then the current material will have no effect. - * We want to still be able to color models with lighting off, so shove the - * material color in texture factor and modify the texture stage to use it - * instead of the vertex color (our models don't have vertex coloring anyway). */ - DWORD bLighting; - g_pd3dDevice->GetRenderState( D3DRS_LIGHTING, &bLighting ); - - if( bLighting ) - { - D3DMATERIAL9 mat; - memcpy( &mat.Diffuse, diffuse, sizeof(float)*4 ); - memcpy( &mat.Ambient, ambient, sizeof(float)*4 ); - memcpy( &mat.Specular, specular, sizeof(float)*4 ); - memcpy( &mat.Emissive, emissive, sizeof(float)*4 ); - mat.Power = shininess; - g_pd3dDevice->SetMaterial( &mat ); - } - else - { - RageColor c = diffuse; - c.r += emissive.r + ambient.r; - c.g += emissive.g + ambient.g; - c.b += emissive.b + ambient.b; - RageVColor c2 = c; - DWORD c3 = *(DWORD*)&c2; - g_pd3dDevice->SetRenderState( D3DRS_TEXTUREFACTOR, c3 ); - } -} - -void RageDisplay_D3D::SetLighting( bool b ) -{ - g_pd3dDevice->SetRenderState( D3DRS_LIGHTING, b ); -} - -void RageDisplay_D3D::SetLightOff( int index ) -{ - g_pd3dDevice->LightEnable( index, false ); -} -void RageDisplay_D3D::SetLightDirectional( - int index, - const RageColor &ambient, - const RageColor &diffuse, - const RageColor &specular, - const RageVector3 &dir ) -{ - g_pd3dDevice->LightEnable( index, true ); - - D3DLIGHT9 light; - ZERO( light ); - light.Type = D3DLIGHT_DIRECTIONAL; - - /* Z for lighting is flipped for D3D compared to OpenGL. - * XXX: figure out exactly why this is needed. Our transforms are probably - * goofed up, but the Z test is the same for both API's, so I'm not sure - * why we don't see other weirdness. -Chris */ - float position[] = { dir.x, dir.y, -dir.z }; - memcpy( &light.Direction, position, sizeof(position) ); - memcpy( &light.Diffuse, diffuse, sizeof(diffuse) ); - memcpy( &light.Ambient, ambient, sizeof(ambient) ); - memcpy( &light.Specular, specular, sizeof(specular) ); - - // Same as OpenGL defaults. Not used in directional lights. -// light.Attenuation0 = 1; -// light.Attenuation1 = 0; -// light.Attenuation2 = 0; - - g_pd3dDevice->SetLight( index, &light ); -} - -void RageDisplay_D3D::SetCullMode( CullMode mode ) -{ - switch( mode ) - { - case CULL_BACK: - g_pd3dDevice->SetRenderState( D3DRS_CULLMODE, D3DCULL_CW ); - break; - case CULL_FRONT: - g_pd3dDevice->SetRenderState( D3DRS_CULLMODE, D3DCULL_CCW ); - break; - case CULL_NONE: - g_pd3dDevice->SetRenderState( D3DRS_CULLMODE, D3DCULL_NONE ); - break; - default: - FAIL_M(ssprintf("Invalid CullMode: %i", mode)); - } -} - -void RageDisplay_D3D::DeleteTexture( unsigned iTexHandle ) -{ - if( iTexHandle == 0 ) - return; - - IDirect3DTexture9* pTex = (IDirect3DTexture9*) iTexHandle; - pTex->Release(); - - // Delete palette (if any) - if( g_TexResourceToPaletteIndex.find(iTexHandle) != g_TexResourceToPaletteIndex.end() ) - g_TexResourceToPaletteIndex.erase( g_TexResourceToPaletteIndex.find(iTexHandle) ); - if( g_TexResourceToTexturePalette.find(iTexHandle) != g_TexResourceToTexturePalette.end() ) - g_TexResourceToTexturePalette.erase( g_TexResourceToTexturePalette.find(iTexHandle) ); -} - - -unsigned RageDisplay_D3D::CreateTexture( - RagePixelFormat pixfmt, - RageSurface* img, - bool bGenerateMipMaps ) -{ - HRESULT hr; - IDirect3DTexture9* pTex; - hr = g_pd3dDevice->CreateTexture( power_of_two(img->w), power_of_two(img->h), 1, 0, D3DFORMATS[pixfmt], D3DPOOL_MANAGED, &pTex, NULL ); - - if( FAILED(hr) ) - RageException::Throw( "CreateTexture(%i,%i,%s) failed: %s", - img->w, img->h, RagePixelFormatToString(pixfmt).c_str(), GetErrorString(hr).c_str() ); - - unsigned uTexHandle = (unsigned)pTex; - - if( pixfmt == RagePixelFormat_PAL ) - { - // Save palette - TexturePalette pal; - memset( pal.p, 0, sizeof(pal.p) ); - for( int i=0; iformat->palette->ncolors; i++ ) - { - RageSurfaceColor &c = img->format->palette->colors[i]; - pal.p[i].peRed = c.r; - pal.p[i].peGreen = c.g; - pal.p[i].peBlue = c.b; - pal.p[i].peFlags = c.a; - } - - ASSERT( g_TexResourceToTexturePalette.find(uTexHandle) == g_TexResourceToTexturePalette.end() ); - g_TexResourceToTexturePalette[uTexHandle] = pal; - } - - UpdateTexture( uTexHandle, img, 0, 0, img->w, img->h ); - - return uTexHandle; -} - -void RageDisplay_D3D::UpdateTexture( - unsigned uTexHandle, - RageSurface* img, - int xoffset, int yoffset, int width, int height ) -{ - IDirect3DTexture9* pTex = (IDirect3DTexture9*)uTexHandle; - ASSERT( pTex != NULL ); - - RECT rect; - rect.left = xoffset; - rect.top = yoffset; - rect.right = width - xoffset; - rect.bottom = height - yoffset; - - D3DLOCKED_RECT lr; - pTex->LockRect( 0, &lr, &rect, 0 ); - - D3DSURFACE_DESC desc; - pTex->GetLevelDesc(0, &desc); - ASSERT( xoffset+width <= int(desc.Width) ); - ASSERT( yoffset+height <= int(desc.Height) ); - - // Copy bits - int texpixfmt; - for(texpixfmt = 0; texpixfmt < NUM_RagePixelFormat; ++texpixfmt) - if(D3DFORMATS[texpixfmt] == desc.Format) break; - ASSERT( texpixfmt != NUM_RagePixelFormat ); - - RageSurface *Texture = CreateSurfaceFromPixfmt(RagePixelFormat(texpixfmt), lr.pBits, width, height, lr.Pitch); - ASSERT( Texture != NULL ); - RageSurfaceUtils::Blit( img, Texture, width, height ); - - delete Texture; - - pTex->UnlockRect( 0 ); -} - -void RageDisplay_D3D::SetAlphaTest( bool b ) -{ - g_pd3dDevice->SetRenderState( D3DRS_ALPHATESTENABLE, b ); - g_pd3dDevice->SetRenderState( D3DRS_ALPHAREF, 0 ); - g_pd3dDevice->SetRenderState( D3DRS_ALPHAFUNC, D3DCMP_GREATER ); -} - -RageMatrix RageDisplay_D3D::GetOrthoMatrix( float l, float r, float b, float t, float zn, float zf ) -{ - RageMatrix m = RageDisplay::GetOrthoMatrix( l, r, b, t, zn, zf ); - - // Convert from OpenGL's [-1,+1] Z values to D3D's [0,+1]. - RageMatrix tmp; - RageMatrixScaling( &tmp, 1, 1, 0.5f ); - RageMatrixMultiply( &m, &tmp, &m ); - - RageMatrixTranslation( &tmp, 0, 0, 0.5f ); - RageMatrixMultiply( &m, &tmp, &m ); - - return m; -} - -void RageDisplay_D3D::SetSphereEnvironmentMapping( TextureUnit tu, bool b ) -{ - g_bSphereMapping[tu] = b; -} - -void RageDisplay_D3D::SetCelShaded( int stage ) -{ - // todo: implement me! -} - -/* - * Copyright (c) 2001-2004 Chris Danford, Glenn Maynard - * 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. - */ +#include "global.h" +#include "RageDisplay.h" +#include "RageDisplay_D3D.h" +#include "RageUtil.h" +#include "RageLog.h" +#include "RageTimer.h" +#include "RageException.h" +#include "RageMath.h" +#include "RageTypes.h" +#include "RageSurface.h" +#include "RageSurfaceUtils.h" +#include "EnumHelper.h" +#include "DisplayResolutions.h" +#include "LocalizedString.h" + +#include +#include +#include + +#include "archutils/Win32/GraphicsWindow.h" + +// Static libraries +// load Windows D3D9 dynamically +#if defined(_MSC_VER) + #pragma comment(lib, "d3d9.lib") + #pragma comment(lib, "d3dx9.lib") + #pragma comment(lib, "DxErr.lib") +#endif + +#include +#include + +RString GetErrorString( HRESULT hr ) +{ + return DXGetErrorString(hr); +} + +// Globals +HMODULE g_D3D9_Module = NULL; +LPDIRECT3D9 g_pd3d = NULL; +LPDIRECT3DDEVICE9 g_pd3dDevice = NULL; +D3DCAPS9 g_DeviceCaps; +D3DDISPLAYMODE g_DesktopMode; +D3DPRESENT_PARAMETERS g_d3dpp; +int g_ModelMatrixCnt=0; +static bool g_bSphereMapping[NUM_TextureUnit] = { false, false }; + +// TODO: Instead of defining this here, enumerate the possible formats and select whatever one we want to use. This format should +// be fine for the uses of this application though. +const D3DFORMAT g_DefaultAdapterFormat = D3DFMT_X8R8G8B8; + +/* Direct3D doesn't associate a palette with textures. Instead, we load a + * palette into a slot. We need to keep track of which texture's palette is + * stored in what slot. */ +map g_TexResourceToPaletteIndex; +list g_PaletteIndex; +struct TexturePalette { PALETTEENTRY p[256]; }; +map g_TexResourceToTexturePalette; + +// Load the palette, if any, for the given texture into a palette slot, and make it current. +static void SetPalette( unsigned TexResource ) +{ + // If the texture isn't paletted, we have nothing to do. + if( g_TexResourceToTexturePalette.find(TexResource) == g_TexResourceToTexturePalette.end() ) + return; + + // Is the palette already loaded? + if( g_TexResourceToPaletteIndex.find(TexResource) == g_TexResourceToPaletteIndex.end() ) + { + // It's not. Grab the least recently used slot. + int iPalIndex = g_PaletteIndex.front(); + + // If any other texture is currently using this slot, mark that palette unloaded. + for( map::iterator i = g_TexResourceToPaletteIndex.begin(); i != g_TexResourceToPaletteIndex.end(); ++i ) + { + if( i->second != iPalIndex ) + continue; + g_TexResourceToPaletteIndex.erase(i); + break; + } + + // Load it. + TexturePalette& pal = g_TexResourceToTexturePalette[TexResource]; + g_pd3dDevice->SetPaletteEntries( iPalIndex, pal.p ); + + g_TexResourceToPaletteIndex[TexResource] = iPalIndex; + } + + const int iPalIndex = g_TexResourceToPaletteIndex[TexResource]; + + // Find this palette index in the least-recently-used queue and move it to the end. + for(list::iterator i = g_PaletteIndex.begin(); i != g_PaletteIndex.end(); ++i) + { + if( *i != iPalIndex ) + continue; + g_PaletteIndex.erase(i); + g_PaletteIndex.push_back(iPalIndex); + break; + } + + g_pd3dDevice->SetCurrentTexturePalette( iPalIndex ); +} + +#define D3DFVF_RageSpriteVertex (D3DFVF_XYZ|D3DFVF_NORMAL|D3DFVF_DIFFUSE|D3DFVF_TEX1) +#define D3DFVF_RageModelVertex (D3DFVF_XYZ|D3DFVF_NORMAL|D3DFVF_TEX1) + + +static const RageDisplay::RagePixelFormatDesc PIXEL_FORMAT_DESC[NUM_RagePixelFormat] = { + { + /* A8B8G8R8 */ + 32, + { 0x00FF0000, + 0x0000FF00, + 0x000000FF, + 0xFF000000 } + }, { + 0, { 0,0,0,0 } + }, { + /* A4R4G4B4 */ + 16, + { 0x0F00, + 0x00F0, + 0x000F, + 0xF000 }, + }, { + /* A1B5G5R5 */ + 16, + { 0x7C00, + 0x03E0, + 0x001F, + 0x8000 }, + }, { + /* X1R5G5B5 */ + 16, + { 0x7C00, + 0x03E0, + 0x001F, + 0x0000 }, + }, { + /* B8G8R8 */ + 24, + { 0xFF0000, + 0x00FF00, + 0x0000FF, + 0x000000 } + }, { + /* Paletted */ + 8, + { 0,0,0,0 } /* N/A */ + }, { + /* BGR (N/A; OpenGL only) */ + 0, { 0,0,0,0 } + }, { + /* ABGR (N/A; OpenGL only) */ + 0, { 0,0,0,0 } + }, { + /* X1R5G5B5 */ + 0, { 0,0,0,0 } + } +}; + +static D3DFORMAT D3DFORMATS[NUM_RagePixelFormat] = +{ + D3DFMT_A8R8G8B8, + D3DFMT_UNKNOWN, + D3DFMT_A4R4G4B4, + D3DFMT_A1R5G5B5, + D3DFMT_X1R5G5B5, + D3DFMT_R8G8B8, + D3DFMT_P8, + D3DFMT_UNKNOWN, // no BGR + D3DFMT_UNKNOWN, // no ABGR + D3DFMT_UNKNOWN, // X1R5G5B5 +}; + +const RageDisplay::RagePixelFormatDesc *RageDisplay_D3D::GetPixelFormatDesc(RagePixelFormat pf) const +{ + ASSERT( pf < NUM_RagePixelFormat ); + return &PIXEL_FORMAT_DESC[pf]; +} + + +RageDisplay_D3D::RageDisplay_D3D() +{ + +} + +static LocalizedString D3D_NOT_INSTALLED ( "RageDisplay_D3D", "DirectX 9.0c or greater is not installed. You can download it from:" ); +const RString D3D_URL = "http://www.microsoft.com/en-us/download/details.aspx?id=8109"; +static LocalizedString HARDWARE_ACCELERATION_NOT_AVAILABLE ( "RageDisplay_D3D", + "Your system is reporting that Direct3D hardware acceleration is not available. Please obtain an updated driver from your video card manufacturer." ); +RString RageDisplay_D3D::Init( const VideoModeParams &p, bool /* bAllowUnacceleratedRenderer */ ) +{ + GraphicsWindow::Initialize( true ); + + LOG->Trace( "RageDisplay_D3D::RageDisplay_D3D()" ); + LOG->MapLog("renderer", "Current renderer: Direct3D"); + + g_pd3d = Direct3DCreate9(D3D_SDK_VERSION); + if(!g_pd3d) + { + LOG->Trace( "Direct3DCreate9 failed" ); + return D3D_NOT_INSTALLED.GetValue(); + } + + if( FAILED( g_pd3d->GetDeviceCaps(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, &g_DeviceCaps) ) ) + return HARDWARE_ACCELERATION_NOT_AVAILABLE.GetValue(); + + + D3DADAPTER_IDENTIFIER9 identifier; + g_pd3d->GetAdapterIdentifier( D3DADAPTER_DEFAULT, 0, &identifier ); + + LOG->Trace( + "Driver: %s\n" + "Description: %s\n" + "Max texture size: %d\n" + "Alpha in palette: %s\n", + identifier.Driver, + identifier.Description, + g_DeviceCaps.MaxTextureWidth, + (g_DeviceCaps.TextureCaps & D3DPTEXTURECAPS_ALPHAPALETTE) ? "yes" : "no" ); + + LOG->Trace( "This display adaptor supports the following modes:" ); + D3DDISPLAYMODE mode; + + UINT modeCount = g_pd3d->GetAdapterModeCount(D3DADAPTER_DEFAULT, g_DefaultAdapterFormat); + + for( UINT u=0; u < modeCount; u++ ) + if( SUCCEEDED( g_pd3d->EnumAdapterModes( D3DADAPTER_DEFAULT, g_DefaultAdapterFormat, u, &mode ) ) ) + LOG->Trace( " %ux%u %uHz, format %d", mode.Width, mode.Height, mode.RefreshRate, mode.Format ); + + g_PaletteIndex.clear(); + for( int i = 0; i < 256; ++i ) + g_PaletteIndex.push_back(i); + + // Save the original desktop format. + g_pd3d->GetAdapterDisplayMode( D3DADAPTER_DEFAULT, &g_DesktopMode ); + + /* Up until now, all we've done is set up g_pd3d and do some queries. Now, + * actually initialize the window. Do this after as many error conditions as + * possible, because if we have to shut it down again we'll flash a window briefly. */ + bool bIgnore = false; + return SetVideoMode( p, bIgnore ); +} + +RageDisplay_D3D::~RageDisplay_D3D() +{ + LOG->Trace( "RageDisplay_D3D::~RageDisplay()" ); + + GraphicsWindow::Shutdown(); + + if( g_pd3dDevice ) + { + g_pd3dDevice->Release(); + g_pd3dDevice = NULL; + } + + if( g_pd3d ) + { + g_pd3d->Release(); + g_pd3d = NULL; + } + + /* Even after we call Release(), D3D may still affect our window. It seems + * to subclass the window, and never release it. Free the DLL after + * destroying the window. */ + if( g_D3D9_Module ) + { + FreeLibrary( g_D3D9_Module ); + g_D3D9_Module = NULL; + } +} + +void RageDisplay_D3D::GetDisplayResolutions( DisplayResolutions &out ) const +{ + out.clear(); + int iCnt = g_pd3d->GetAdapterModeCount( D3DADAPTER_DEFAULT, g_DefaultAdapterFormat ); + + for( int i = 0; i < iCnt; ++i ) + { + D3DDISPLAYMODE mode; + g_pd3d->EnumAdapterModes( D3DADAPTER_DEFAULT, g_DefaultAdapterFormat, i, &mode ); + + DisplayResolution res = { mode.Width, mode.Height }; + out.insert( res ); + } +} + +D3DFORMAT FindBackBufferType(bool bWindowed, int iBPP) +{ + HRESULT hr; + + // If windowed, then bpp is ignored. Use whatever works. + vector vBackBufferFormats; // throw all possibilities in here + + // When windowed, add all formats; otherwise add only formats that match dwBPP. + if( iBPP == 16 || bWindowed ) + { + vBackBufferFormats.push_back( D3DFMT_R5G6B5 ); + vBackBufferFormats.push_back( D3DFMT_X1R5G5B5 ); + vBackBufferFormats.push_back( D3DFMT_A1R5G5B5 ); + } + if( iBPP == 32 || bWindowed ) + { + vBackBufferFormats.push_back( D3DFMT_R8G8B8 ); + vBackBufferFormats.push_back( D3DFMT_X8R8G8B8 ); + vBackBufferFormats.push_back( D3DFMT_A8R8G8B8 ); + } + + if( !bWindowed && iBPP != 16 && iBPP != 32 ) + { + GraphicsWindow::Shutdown(); + RageException::Throw( "Invalid BPP '%i' specified", iBPP ); + } + + // Test each back buffer format until we find something that works. + for( unsigned i=0; i < vBackBufferFormats.size(); i++ ) + { + D3DFORMAT fmtBackBuffer = vBackBufferFormats[i]; + + D3DFORMAT fmtDisplay; + if( bWindowed ) + fmtDisplay = g_DesktopMode.Format; + else // Fullscreen + fmtDisplay = vBackBufferFormats[i]; + + LOG->Trace( "Testing format: display %d, back buffer %d, windowed %d...", + fmtDisplay, fmtBackBuffer, bWindowed ); + + hr = g_pd3d->CheckDeviceType( D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, + fmtDisplay, fmtBackBuffer, bWindowed ); + + if( FAILED(hr) ) + continue; // skip + + // done searching + LOG->Trace( "This will work." ); + return fmtBackBuffer; + } + + LOG->Trace( "Couldn't find an appropriate back buffer format." ); + return D3DFMT_UNKNOWN; +} + +RString SetD3DParams( bool &bNewDeviceOut ) +{ + if( g_pd3dDevice == NULL ) // device is not yet created. We need to create it + { + bNewDeviceOut = true; + HRESULT hr = g_pd3d->CreateDevice( + D3DADAPTER_DEFAULT, + D3DDEVTYPE_HAL, + GraphicsWindow::GetHwnd(), + D3DCREATE_SOFTWARE_VERTEXPROCESSING | D3DCREATE_MULTITHREADED, + &g_d3dpp, + &g_pd3dDevice ); + if( FAILED(hr) ) + { + // Likely D3D_ERR_INVALIDCALL. The driver probably doesn't support this video mode. + return ssprintf( "CreateDevice failed: '%s'", GetErrorString(hr).c_str() ); + } + } + else + { + bNewDeviceOut = false; + //LOG->Warn( "Resetting D3D device" ); + HRESULT hr = g_pd3dDevice->Reset( &g_d3dpp ); + if( FAILED(hr) ) + { + // Likely D3D_ERR_INVALIDCALL. The driver probably doesn't support this video mode. + return ssprintf("g_pd3dDevice->Reset failed: '%s'", GetErrorString(hr).c_str() ); + } + } + + g_pd3dDevice->SetRenderState( D3DRS_NORMALIZENORMALS, TRUE ); + + // Palettes were lost by Reset(), so mark them unloaded. + g_TexResourceToPaletteIndex.clear(); + + return RString(); +} + +// If the given parameters have failed, try to lower them. +static bool D3DReduceParams( D3DPRESENT_PARAMETERS *pp ) +{ + D3DDISPLAYMODE current; + current.Format = pp->BackBufferFormat; + current.Height = pp->BackBufferHeight; + current.Width = pp->BackBufferWidth; + current.RefreshRate = pp->FullScreen_RefreshRateInHz; + + const int iCnt = g_pd3d->GetAdapterModeCount( D3DADAPTER_DEFAULT, g_DefaultAdapterFormat ); + int iBest = -1; + int iBestScore = 0; + LOG->Trace( "cur: %ux%u %uHz, format %i", current.Width, current.Height, current.RefreshRate, current.Format ); + for( int i = 0; i < iCnt; ++i ) + { + D3DDISPLAYMODE mode; + g_pd3d->EnumAdapterModes( D3DADAPTER_DEFAULT, g_DefaultAdapterFormat, i, &mode ); + + // Never change the format. + if( mode.Format != current.Format ) + continue; + // Never increase the parameters. + if( mode.Height > current.Height || mode.Width > current.Width || mode.RefreshRate > current.RefreshRate ) + continue; + + // Never go below 640x480 unless we already are. + if( (current.Width >= 640 && current.Height >= 480) && (mode.Width < 640 || mode.Height < 480) ) + continue; + + // Never go below 60Hz. + if( mode.RefreshRate && mode.RefreshRate < 60 ) + continue; + + /* If mode.RefreshRate is 0, it means "default". We don't know what + * that means; assume it's 60Hz. */ + + // Higher scores are better. + int iScore = 0; + if( current.RefreshRate >= 70 && mode.RefreshRate < 70 ) + { + /* Top priority: we really want to avoid dropping to a refresh rate + * that's below 70Hz. */ + iScore -= 100000; + } + else if( mode.RefreshRate < current.RefreshRate ) + { + /* Low priority: We're lowering the refresh rate, but not too far. + * current.RefreshRate might be 0, in which case this simply gives + * points for higher refresh rates. */ + iScore += (mode.RefreshRate - current.RefreshRate); + } + + // Medium priority: + int iResolutionDiff = (current.Height - mode.Height) + (current.Width - mode.Width); + iScore -= iResolutionDiff * 100; + + if( iBest == -1 || iScore > iBestScore ) + { + iBest = i; + iBestScore = iScore; + } + + LOG->Trace( "try: %ux%u %uHz, format %i: score %i", mode.Width, mode.Height, mode.RefreshRate, mode.Format, iScore ); + } + + if( iBest == -1 ) + return false; + + D3DDISPLAYMODE BestMode; + g_pd3d->EnumAdapterModes( D3DADAPTER_DEFAULT, g_DefaultAdapterFormat, iBest, &BestMode ); + pp->BackBufferHeight = BestMode.Height; + pp->BackBufferWidth = BestMode.Width; + pp->FullScreen_RefreshRateInHz = BestMode.RefreshRate; + + return true; +} + + +static void SetPresentParametersFromVideoModeParams( const VideoModeParams &p, D3DPRESENT_PARAMETERS *pD3Dpp ) +{ + ZERO( *pD3Dpp ); + + pD3Dpp->BackBufferWidth = p.width; + pD3Dpp->BackBufferHeight = p.height; + pD3Dpp->BackBufferFormat = FindBackBufferType( p.windowed, p.bpp ); + pD3Dpp->BackBufferCount = 1; + pD3Dpp->MultiSampleType = D3DMULTISAMPLE_NONE; + pD3Dpp->SwapEffect = D3DSWAPEFFECT_DISCARD; + pD3Dpp->hDeviceWindow = GraphicsWindow::GetHwnd(); + pD3Dpp->Windowed = p.windowed; + pD3Dpp->EnableAutoDepthStencil = TRUE; + pD3Dpp->AutoDepthStencilFormat = D3DFMT_D16; + + if( p.windowed ) + pD3Dpp->PresentationInterval = D3DPRESENT_INTERVAL_DEFAULT; + else + pD3Dpp->PresentationInterval = p.vsync ? D3DPRESENT_INTERVAL_ONE : D3DPRESENT_INTERVAL_IMMEDIATE; + + pD3Dpp->FullScreen_RefreshRateInHz = D3DPRESENT_RATE_DEFAULT; + if( !p.windowed && p.rate != REFRESH_DEFAULT ) + pD3Dpp->FullScreen_RefreshRateInHz = p.rate; + + pD3Dpp->Flags = 0; + + LOG->Trace( "Present Parameters: %d, %d, %d, %d, %d, %d, %d, %d, %d, %d, %d, %d, %d", + pD3Dpp->BackBufferWidth, pD3Dpp->BackBufferHeight, pD3Dpp->BackBufferFormat, + pD3Dpp->BackBufferCount, + pD3Dpp->MultiSampleType, pD3Dpp->SwapEffect, pD3Dpp->hDeviceWindow, + pD3Dpp->Windowed, pD3Dpp->EnableAutoDepthStencil, pD3Dpp->AutoDepthStencilFormat, + pD3Dpp->Flags, pD3Dpp->FullScreen_RefreshRateInHz, + pD3Dpp->PresentationInterval + ); +} + +// Set the video mode. +RString RageDisplay_D3D::TryVideoMode( const VideoModeParams &_p, bool &bNewDeviceOut ) +{ + VideoModeParams p = _p; + LOG->Warn( "RageDisplay_D3D::TryVideoMode( %d, %d, %d, %d, %d, %d )", p.windowed, p.width, p.height, p.bpp, p.rate, p.vsync ); + + if( FindBackBufferType( p.windowed, p.bpp ) == D3DFMT_UNKNOWN ) // no possible back buffer formats + return ssprintf( "FindBackBufferType(%i,%i) failed", p.windowed, p.bpp ); // failed to set mode + + /* Set up and display the window before setting up D3D. If we don't do this, + * then setting up a fullscreen window (when we're not coming from windowed) + * causes all other windows on the system to be resized to the new resolution. */ + GraphicsWindow::CreateGraphicsWindow( p ); + + SetPresentParametersFromVideoModeParams( p, &g_d3dpp ); + + // Display the window immediately, so we don't display the desktop ... + while( 1 ) + { + // Try the video mode. + RString sErr = SetD3DParams( bNewDeviceOut ); + if( sErr.empty() ) + break; + + /* It failed. We're probably selecting a video mode that isn't supported. + * If we're fullscreen, search the mode list and find the nearest lower mode. */ + if( p.windowed || !D3DReduceParams( &g_d3dpp ) ) + return sErr; + + // Store the new settings we're about to try. + p.height = g_d3dpp.BackBufferHeight; + p.width = g_d3dpp.BackBufferWidth; + if( g_d3dpp.FullScreen_RefreshRateInHz == D3DPRESENT_RATE_DEFAULT ) + p.rate = REFRESH_DEFAULT; + else + p.rate = g_d3dpp.FullScreen_RefreshRateInHz; + } + + /* Call this again after changing the display mode. If we're going to a window + * from fullscreen, the first call can't set a larger window than the old + * fullscreen resolution or set the window position. */ + GraphicsWindow::CreateGraphicsWindow( p ); + + ResolutionChanged(); + + return RString(); // mode change successful +} + +void RageDisplay_D3D::ResolutionChanged() +{ + //LOG->Warn( "RageDisplay_D3D::ResolutionChanged" ); + + RageDisplay::ResolutionChanged(); +} + +int RageDisplay_D3D::GetMaxTextureSize() const +{ + return g_DeviceCaps.MaxTextureWidth; +} + +bool RageDisplay_D3D::BeginFrame() +{ + GraphicsWindow::Update(); + + switch( g_pd3dDevice->TestCooperativeLevel() ) + { + case D3DERR_DEVICELOST: + return false; + case D3DERR_DEVICENOTRESET: + { + bool bIgnore = false; + RString sError = SetD3DParams( bIgnore ); + if( sError != "" ) + RageException::Throw( sError ); + + break; + } + } + + g_pd3dDevice->Clear( 0, NULL, D3DCLEAR_TARGET|D3DCLEAR_ZBUFFER, + D3DCOLOR_XRGB(0,0,0), 1.0f, 0x00000000 ); + g_pd3dDevice->BeginScene(); + + return RageDisplay::BeginFrame(); +} + +static RageTimer g_LastFrameEndedAt( RageZeroTimer ); +void RageDisplay_D3D::EndFrame() +{ + g_pd3dDevice->EndScene(); + + FrameLimitBeforeVsync( GetActualVideoModeParams().rate ); + g_pd3dDevice->Present( 0, 0, 0, 0 ); + FrameLimitAfterVsync(); + + RageDisplay::EndFrame(); +} + +bool RageDisplay_D3D::SupportsTextureFormat( RagePixelFormat pixfmt, bool realtime ) +{ + // Some cards (Savage) don't support alpha in palettes. + // Don't allow paletted textures if this is the case. + if( pixfmt == RagePixelFormat_PAL && !(g_DeviceCaps.TextureCaps & D3DPTEXTURECAPS_ALPHAPALETTE) ) + return false; + + if( D3DFORMATS[pixfmt] == D3DFMT_UNKNOWN ) + return false; + + D3DFORMAT d3dfmt = D3DFORMATS[pixfmt]; + HRESULT hr = g_pd3d->CheckDeviceFormat( + D3DADAPTER_DEFAULT, + D3DDEVTYPE_HAL, + g_d3dpp.BackBufferFormat, + 0, + D3DRTYPE_TEXTURE, + d3dfmt); + + return SUCCEEDED( hr ); +} + +bool RageDisplay_D3D::SupportsThreadedRendering() +{ + return true; +} + +RageSurface* RageDisplay_D3D::CreateScreenshot() +{ + RageSurface * result = NULL; + + // Get the back buffer. + IDirect3DSurface9* pSurface; + if( SUCCEEDED( g_pd3dDevice->GetBackBuffer( 0, 0, D3DBACKBUFFER_TYPE_MONO, &pSurface ) ) ) + { + // Get the back buffer description. + D3DSURFACE_DESC desc; + pSurface->GetDesc( &desc ); + + // Copy the back buffer into a surface of a type we support. + IDirect3DSurface9* pCopy; + if( SUCCEEDED( g_pd3dDevice->CreateOffscreenPlainSurface( desc.Width, desc.Height, D3DFMT_A8R8G8B8, D3DPOOL_SCRATCH, &pCopy, NULL ) ) ) + { + if( SUCCEEDED( D3DXLoadSurfaceFromSurface( pCopy, NULL, NULL, pSurface, NULL, NULL, D3DX_FILTER_NONE, 0) ) ) + { + // Update desc from the copy. + pCopy->GetDesc( &desc ); + + D3DLOCKED_RECT lr; + + { + RECT rect; + rect.left = 0; + rect.top = 0; + rect.right = desc.Width; + rect.bottom = desc.Height; + + pCopy->LockRect( &lr, &rect, D3DLOCK_READONLY ); + } + + RageSurface *surface = CreateSurfaceFromPixfmt( RagePixelFormat_RGBA8, lr.pBits, desc.Width, desc.Height, lr.Pitch); + ASSERT( surface != nullptr ); + + // We need to make a copy, since lr.pBits will go away when we call UnlockRect(). + result = + CreateSurface( surface->w, surface->h, + surface->format->BitsPerPixel, + surface->format->Rmask, surface->format->Gmask, + surface->format->Bmask, surface->format->Amask ); + RageSurfaceUtils::CopySurface( surface, result ); + delete surface; + + pCopy->UnlockRect(); + } + + pCopy->Release(); + } + + pSurface->Release(); + } + + return result; +} + +VideoModeParams RageDisplay_D3D::GetActualVideoModeParams() const +{ + VideoModeParams p = GraphicsWindow::GetParams(); + return p; +} + +void RageDisplay_D3D::SendCurrentMatrices() +{ + RageMatrix m; + RageMatrixMultiply( &m, GetCentering(), GetProjectionTop() ); + + // Convert to OpenGL-style "pixel-centered" coords + RageMatrix m2 = GetCenteringMatrix( -0.5f, -0.5f, 0, 0 ); + RageMatrix projection; + RageMatrixMultiply( &projection, &m2, &m ); + g_pd3dDevice->SetTransform( D3DTS_PROJECTION, (D3DMATRIX*)&projection ); + + g_pd3dDevice->SetTransform( D3DTS_VIEW, (D3DMATRIX*)GetViewTop() ); + g_pd3dDevice->SetTransform( D3DTS_WORLD, (D3DMATRIX*)GetWorldTop() ); + + FOREACH_ENUM( TextureUnit, tu ) + { + // Optimization opportunity: Turn off texture transform if not using texture coords. + g_pd3dDevice->SetTextureStageState( tu, D3DTSS_TEXTURETRANSFORMFLAGS, D3DTTFF_COUNT2 ); + + // If no texture is set for this texture unit, don't bother setting it up. + IDirect3DBaseTexture9* pTexture = NULL; + g_pd3dDevice->GetTexture( tu, &pTexture ); + if( pTexture == NULL ) + continue; + pTexture->Release(); + + if( g_bSphereMapping[tu] ) + { + static const RageMatrix tex = RageMatrix + ( + 0.5f, 0.0f, 0.0f, 0.0f, + 0.0f, -0.5f, 0.0f, 0.0f, + 0.0f, 0.0f, 0.0f, 0.0f, + 0.5f, -0.5f, 0.0f, 1.0f + ); + g_pd3dDevice->SetTransform( (D3DTRANSFORMSTATETYPE)(D3DTS_TEXTURE0+tu), (D3DMATRIX*)&tex ); + + // Tell D3D to use transformed reflection vectors as texture co-ordinate 0 + // and then transform this coordinate by the specified texture matrix. + g_pd3dDevice->SetTextureStageState( tu, D3DTSS_TEXCOORDINDEX, D3DTSS_TCI_CAMERASPACEREFLECTIONVECTOR ); + } + else + { + /* Direct3D is expecting a 3x3 matrix loaded into the 4x4 in order + * to transform the 2-component texture coordinates. We currently + * only use translate and scale, and ignore the z component entirely, + * so convert the texture matrix from 4x4 to 3x3 by dropping z. */ + + const RageMatrix &tex1 = *GetTextureTop(); + const RageMatrix tex2 = RageMatrix + ( + tex1.m[0][0], tex1.m[0][1], tex1.m[0][3], 0, + tex1.m[1][0], tex1.m[1][1], tex1.m[1][3], 0, + tex1.m[3][0], tex1.m[3][1], tex1.m[3][3], 0, + 0, 0, 0, 0 + ); + g_pd3dDevice->SetTransform( D3DTRANSFORMSTATETYPE(D3DTS_TEXTURE0+tu), (D3DMATRIX*)&tex2 ); + + g_pd3dDevice->SetTextureStageState( tu, D3DTSS_TEXCOORDINDEX, D3DTSS_TCI_PASSTHRU ); + } + } +} + +class RageCompiledGeometrySWD3D : public RageCompiledGeometry +{ +public: + void Allocate( const vector &vMeshes ) + { + m_vVertex.resize( max(1u, GetTotalVertices()) ); + m_vTriangles.resize( max(1u, GetTotalTriangles()) ); + } + void Change( const vector &vMeshes ) + { + for( unsigned i=0; i &Vertices = mesh.Vertices; + const vector &Triangles = mesh.Triangles; + + for( unsigned j=0; jGetTransform( D3DTS_TEXTURE0, (D3DMATRIX*)&m ); + + m.m[2][0] = 0; + m.m[2][1] = 0; + + g_pd3dDevice->SetTransform( D3DTS_TEXTURE0, (D3DMATRIX*)&m ); + } + + g_pd3dDevice->SetFVF( D3DFVF_RageModelVertex ); + g_pd3dDevice->DrawIndexedPrimitiveUP( + D3DPT_TRIANGLELIST, // PrimitiveType + meshInfo.iVertexStart, // MinIndex + meshInfo.iVertexCount, // NumVertices + meshInfo.iTriangleCount, // PrimitiveCount, + &m_vTriangles[0]+meshInfo.iTriangleStart,// pIndexData, + D3DFMT_INDEX16, // IndexDataFormat, + &m_vVertex[0], // pVertexStreamZeroData, + sizeof(m_vVertex[0]) // VertexStreamZeroStride + ); + } + +protected: + vector m_vVertex; + vector m_vTriangles; +}; + +RageCompiledGeometry* RageDisplay_D3D::CreateCompiledGeometry() +{ + return new RageCompiledGeometrySWD3D; +} + +void RageDisplay_D3D::DeleteCompiledGeometry( RageCompiledGeometry* p ) +{ + delete p; +} + +void RageDisplay_D3D::DrawQuadsInternal( const RageSpriteVertex v[], int iNumVerts ) +{ + // there isn't a quad primitive in D3D, so we have to fake it with indexed triangles + int iNumQuads = iNumVerts/4; + int iNumTriangles = iNumQuads*2; + int iNumIndices = iNumTriangles*3; + + // make a temporary index buffer + static vector vIndices; + unsigned uOldSize = vIndices.size(); + unsigned uNewSize = max(uOldSize,(unsigned)iNumIndices); + vIndices.resize( uNewSize ); + for( uint16_t i=(uint16_t)uOldSize/6; i<(uint16_t)iNumQuads; i++ ) + { + vIndices[i*6+0] = i*4+0; + vIndices[i*6+1] = i*4+1; + vIndices[i*6+2] = i*4+2; + vIndices[i*6+3] = i*4+2; + vIndices[i*6+4] = i*4+3; + vIndices[i*6+5] = i*4+0; + } + + g_pd3dDevice->SetFVF( D3DFVF_RageSpriteVertex ); + SendCurrentMatrices(); + g_pd3dDevice->DrawIndexedPrimitiveUP( + D3DPT_TRIANGLELIST, // PrimitiveType + 0, // MinIndex + iNumVerts, // NumVertices + iNumTriangles, // PrimitiveCount, + &vIndices[0], // pIndexData, + D3DFMT_INDEX16, // IndexDataFormat, + v, // pVertexStreamZeroData, + sizeof(RageSpriteVertex) // VertexStreamZeroStride + ); +} + +void RageDisplay_D3D::DrawQuadStripInternal( const RageSpriteVertex v[], int iNumVerts ) +{ + // there isn't a quad strip primitive in D3D, so we have to fake it with indexed triangles + int iNumQuads = (iNumVerts-2)/2; + int iNumTriangles = iNumQuads*2; + int iNumIndices = iNumTriangles*3; + + // make a temporary index buffer + static vector vIndices; + unsigned uOldSize = vIndices.size(); + unsigned uNewSize = max(uOldSize,(unsigned)iNumIndices); + vIndices.resize( uNewSize ); + for( uint16_t i=(uint16_t)uOldSize/6; i<(uint16_t)iNumQuads; i++ ) + { + vIndices[i*6+0] = i*2+0; + vIndices[i*6+1] = i*2+1; + vIndices[i*6+2] = i*2+2; + vIndices[i*6+3] = i*2+1; + vIndices[i*6+4] = i*2+2; + vIndices[i*6+5] = i*2+3; + } + + g_pd3dDevice->SetFVF( D3DFVF_RageSpriteVertex ); + SendCurrentMatrices(); + g_pd3dDevice->DrawIndexedPrimitiveUP( + D3DPT_TRIANGLELIST, // PrimitiveType + 0, // MinIndex + iNumVerts, // NumVertices + iNumTriangles, // PrimitiveCount, + &vIndices[0], // pIndexData, + D3DFMT_INDEX16, // IndexDataFormat, + v, // pVertexStreamZeroData, + sizeof(RageSpriteVertex) // VertexStreamZeroStride + ); +} + +void RageDisplay_D3D::DrawSymmetricQuadStripInternal( const RageSpriteVertex v[], int iNumVerts ) +{ + int iNumPieces = (iNumVerts-3)/3; + int iNumTriangles = iNumPieces*4; + int iNumIndices = iNumTriangles*3; + + // make a temporary index buffer + static vector vIndices; + unsigned uOldSize = vIndices.size(); + unsigned uNewSize = max(uOldSize,(unsigned)iNumIndices); + vIndices.resize( uNewSize ); + for( uint16_t i=(uint16_t)uOldSize/12; i<(uint16_t)iNumPieces; i++ ) + { + // { 1, 3, 0 } { 1, 4, 3 } { 1, 5, 4 } { 1, 2, 5 } + vIndices[i*12+0] = i*3+1; + vIndices[i*12+1] = i*3+3; + vIndices[i*12+2] = i*3+0; + vIndices[i*12+3] = i*3+1; + vIndices[i*12+4] = i*3+4; + vIndices[i*12+5] = i*3+3; + vIndices[i*12+6] = i*3+1; + vIndices[i*12+7] = i*3+5; + vIndices[i*12+8] = i*3+4; + vIndices[i*12+9] = i*3+1; + vIndices[i*12+10] = i*3+2; + vIndices[i*12+11] = i*3+5; + } + + g_pd3dDevice->SetFVF( D3DFVF_RageSpriteVertex ); + SendCurrentMatrices(); + g_pd3dDevice->DrawIndexedPrimitiveUP( + D3DPT_TRIANGLELIST, // PrimitiveType + 0, // MinIndex + iNumVerts, // NumVertices + iNumTriangles, // PrimitiveCount, + &vIndices[0], // pIndexData, + D3DFMT_INDEX16, // IndexDataFormat, + v, // pVertexStreamZeroData, + sizeof(RageSpriteVertex) // VertexStreamZeroStride + ); +} + +void RageDisplay_D3D::DrawFanInternal( const RageSpriteVertex v[], int iNumVerts ) +{ + g_pd3dDevice->SetFVF( D3DFVF_RageSpriteVertex ); + SendCurrentMatrices(); + g_pd3dDevice->DrawPrimitiveUP( + D3DPT_TRIANGLEFAN, // PrimitiveType + iNumVerts-2, // PrimitiveCount, + v, // pVertexStreamZeroData, + sizeof(RageSpriteVertex) + ); +} + +void RageDisplay_D3D::DrawStripInternal( const RageSpriteVertex v[], int iNumVerts ) +{ + g_pd3dDevice->SetFVF( D3DFVF_RageSpriteVertex ); + SendCurrentMatrices(); + g_pd3dDevice->DrawPrimitiveUP( + D3DPT_TRIANGLESTRIP, // PrimitiveType + iNumVerts-2, // PrimitiveCount, + v, // pVertexStreamZeroData, + sizeof(RageSpriteVertex) + ); +} + +void RageDisplay_D3D::DrawTrianglesInternal( const RageSpriteVertex v[], int iNumVerts ) +{ + g_pd3dDevice->SetFVF( D3DFVF_RageSpriteVertex ); + SendCurrentMatrices(); + g_pd3dDevice->DrawPrimitiveUP( + D3DPT_TRIANGLELIST, // PrimitiveType + iNumVerts/3, // PrimitiveCount, + v, // pVertexStreamZeroData, + sizeof(RageSpriteVertex) + ); +} + +void RageDisplay_D3D::DrawCompiledGeometryInternal( const RageCompiledGeometry *p, int iMeshIndex ) +{ + SendCurrentMatrices(); + + /* If lighting is off, then the current material will have no effect. We + * want to still be able to color models with lighting off, so shove the + * material color in texture factor and modify the texture stage to use it + * instead of the vertex color (our models don't have vertex coloring anyway). */ + DWORD bLighting; + g_pd3dDevice->GetRenderState( D3DRS_LIGHTING, &bLighting ); + + if( !bLighting ) + { + g_pd3dDevice->SetTextureStageState( 0, D3DTSS_COLORARG2, D3DTA_TFACTOR ); + g_pd3dDevice->SetTextureStageState( 0, D3DTSS_ALPHAARG2, D3DTA_TFACTOR ); + } + + p->Draw( iMeshIndex ); + + if( !bLighting ) + { + g_pd3dDevice->SetTextureStageState( 0, D3DTSS_COLORARG2, D3DTA_CURRENT ); + g_pd3dDevice->SetTextureStageState( 0, D3DTSS_ALPHAARG2, D3DTA_CURRENT ); + } +} + +/* Use the default poly-based implementation. D3D lines apparently don't support + * AA with greater-than-one widths. */ +/* +void RageDisplay_D3D::DrawLineStrip( const RageSpriteVertex v[], int iNumVerts, float LineWidth ) +{ + ASSERT( iNumVerts >= 2 ); + g_pd3dDevice->SetRenderState( D3DRS_POINTSIZE, *((DWORD*)&LineWidth) ); // funky cast. See D3DRENDERSTATETYPE doc + g_pd3dDevice->SetVertexShader( D3DFVF_RageSpriteVertex ); + SendCurrentMatrices(); + g_pd3dDevice->DrawPrimitiveUP( + D3DPT_LINESTRIP, // PrimitiveType + iNumVerts-1, // PrimitiveCount, + v, // pVertexStreamZeroData, + sizeof(RageSpriteVertex) + ); + StatsAddVerts( iNumVerts ); +} +*/ + +void RageDisplay_D3D::ClearAllTextures() +{ + FOREACH_ENUM( TextureUnit, i ) + SetTexture( i, 0 ); +} + +int RageDisplay_D3D::GetNumTextureUnits() +{ + return g_DeviceCaps.MaxSimultaneousTextures; +} + +void RageDisplay_D3D::SetTexture( TextureUnit tu, unsigned iTexture ) +{ +// g_DeviceCaps.MaxSimultaneousTextures = 1; + if( tu >= (int) g_DeviceCaps.MaxSimultaneousTextures ) // not supported + return; + + if( iTexture == 0 ) + { + g_pd3dDevice->SetTexture( tu, NULL ); + + /* Intentionally commented out. Don't mess with texture stage state + * when just setting the texture. Model sets its texture modes before + * setting the final texture. */ + //g_pd3dDevice->SetTextureStageState( tu, D3DTSS_COLOROP, D3DTOP_DISABLE ); + } + else + { + IDirect3DTexture9* pTex = (IDirect3DTexture9*) iTexture; + g_pd3dDevice->SetTexture( tu, pTex ); + + /* Intentionally commented out. Don't mess with texture stage state + * when just setting the texture. Model sets its texture modes before + * setting the final texture. */ + //g_pd3dDevice->SetTextureStageState( tu, D3DTSS_COLOROP, D3DTOP_MODULATE ); + + // Set palette (if any) + SetPalette( iTexture ); + } +} + +void RageDisplay_D3D::SetTextureMode( TextureUnit tu, TextureMode tm ) +{ + if( tu >= (int) g_DeviceCaps.MaxSimultaneousTextures ) // not supported + return; + + switch( tm ) + { + case TextureMode_Modulate: + // Use D3DTA_CURRENT instead of diffuse so that multitexturing works + // properly. For stage 0, D3DTA_CURRENT is the diffuse color. + + g_pd3dDevice->SetTextureStageState( tu, D3DTSS_COLORARG1, D3DTA_TEXTURE ); + g_pd3dDevice->SetTextureStageState( tu, D3DTSS_COLORARG2, D3DTA_CURRENT ); + g_pd3dDevice->SetTextureStageState( tu, D3DTSS_COLOROP, D3DTOP_MODULATE ); + g_pd3dDevice->SetTextureStageState( tu, D3DTSS_ALPHAARG1, D3DTA_TEXTURE ); + g_pd3dDevice->SetTextureStageState( tu, D3DTSS_ALPHAARG2, D3DTA_CURRENT ); + g_pd3dDevice->SetTextureStageState( tu, D3DTSS_ALPHAOP, D3DTOP_MODULATE ); + break; + case TextureMode_Add: + g_pd3dDevice->SetTextureStageState( tu, D3DTSS_COLORARG1, D3DTA_TEXTURE ); + g_pd3dDevice->SetTextureStageState( tu, D3DTSS_COLORARG2, D3DTA_CURRENT ); + g_pd3dDevice->SetTextureStageState( tu, D3DTSS_COLOROP, D3DTOP_ADD ); + g_pd3dDevice->SetTextureStageState( tu, D3DTSS_ALPHAARG1, D3DTA_TEXTURE ); + g_pd3dDevice->SetTextureStageState( tu, D3DTSS_ALPHAARG2, D3DTA_CURRENT ); + g_pd3dDevice->SetTextureStageState( tu, D3DTSS_ALPHAOP, D3DTOP_MODULATE ); + break; + case TextureMode_Glow: + g_pd3dDevice->SetTextureStageState( tu, D3DTSS_COLORARG1, D3DTA_TEXTURE ); + g_pd3dDevice->SetTextureStageState( tu, D3DTSS_COLORARG2, D3DTA_CURRENT ); + g_pd3dDevice->SetTextureStageState( tu, D3DTSS_COLOROP, D3DTOP_SELECTARG2 ); + g_pd3dDevice->SetTextureStageState( tu, D3DTSS_ALPHAARG1, D3DTA_TEXTURE ); + g_pd3dDevice->SetTextureStageState( tu, D3DTSS_ALPHAARG2, D3DTA_CURRENT ); + g_pd3dDevice->SetTextureStageState( tu, D3DTSS_ALPHAOP, D3DTOP_MODULATE ); + break; + } +} + +void RageDisplay_D3D::SetTextureFiltering( TextureUnit tu, bool b ) +{ + if( tu >= (int) g_DeviceCaps.MaxSimultaneousTextures ) // not supported + return; + + g_pd3dDevice->SetSamplerState( tu, D3DSAMP_MINFILTER, b ? D3DTEXF_LINEAR : D3DTEXF_POINT ); + g_pd3dDevice->SetSamplerState( tu, D3DSAMP_MAGFILTER, b ? D3DTEXF_LINEAR : D3DTEXF_POINT ); +} + +void RageDisplay_D3D::SetBlendMode( BlendMode mode ) +{ + g_pd3dDevice->SetRenderState( D3DRS_ALPHABLENDENABLE, TRUE ); + + if( mode == BLEND_INVERT_DEST ) + g_pd3dDevice->SetRenderState( D3DRS_BLENDOP, D3DBLENDOP_SUBTRACT ); + else + g_pd3dDevice->SetRenderState( D3DRS_BLENDOP, D3DBLENDOP_ADD ); + + switch( mode ) + { + case BLEND_NORMAL: + g_pd3dDevice->SetRenderState( D3DRS_SRCBLEND, D3DBLEND_SRCALPHA ); + g_pd3dDevice->SetRenderState( D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA ); + break; + case BLEND_ADD: + g_pd3dDevice->SetRenderState( D3DRS_SRCBLEND, D3DBLEND_SRCALPHA ); + g_pd3dDevice->SetRenderState( D3DRS_DESTBLEND, D3DBLEND_ONE ); + break; + case BLEND_MODULATE: + g_pd3dDevice->SetRenderState( D3DRS_SRCBLEND, D3DBLEND_ZERO ); + g_pd3dDevice->SetRenderState( D3DRS_DESTBLEND, D3DBLEND_SRCCOLOR ); + break; + case BLEND_COPY_SRC: + g_pd3dDevice->SetRenderState( D3DRS_SRCBLEND, D3DBLEND_ONE ); + g_pd3dDevice->SetRenderState( D3DRS_DESTBLEND, D3DBLEND_ZERO ); + break; + /* Effects currently missing in D3D: BLEND_ALPHA_MASK, BLEND_ALPHA_KNOCK_OUT + * These two may require DirectX9 since D3DRS_SRCALPHA and D3DRS_DESTALPHA + * don't seem to exist in DX8. -aj */ + case BLEND_ALPHA_MASK: + // RGB: iSourceRGB = GL_ZERO; iDestRGB = GL_ONE; + g_pd3dDevice->SetRenderState( D3DRS_SRCBLEND, D3DBLEND_ZERO ); + g_pd3dDevice->SetRenderState( D3DRS_DESTBLEND, D3DBLEND_ONE ); + // Alpha: iSourceAlpha = GL_ZERO; iDestAlpha = GL_SRC_ALPHA; + /* + g_pd3dDevice->SetRenderState( D3DRS_SRCALPHA, D3DBLEND_ZERO ); + g_pd3dDevice->SetRenderState( D3DRS_DESTALPHA, D3DBLEND_SRCALPHA ); + */ + break; + case BLEND_ALPHA_KNOCK_OUT: + // RGB: iSourceRGB = GL_ZERO; iDestRGB = GL_ONE; + g_pd3dDevice->SetRenderState( D3DRS_SRCBLEND, D3DBLEND_ZERO ); + g_pd3dDevice->SetRenderState( D3DRS_DESTBLEND, D3DBLEND_ONE ); + // Alpha: iSourceAlpha = GL_ZERO; iDestAlpha = GL_ONE_MINUS_SRC_ALPHA; + /* + g_pd3dDevice->SetRenderState( D3DRS_SRCBLENDALPHA, D3DBLEND_ZERO ); + g_pd3dDevice->SetRenderState( D3DRS_DESTBLENDALPHA, D3DBLEND_INVSRCALPHA ); + */ + break; + case BLEND_ALPHA_MULTIPLY: + g_pd3dDevice->SetRenderState( D3DRS_SRCBLEND, D3DBLEND_SRCALPHA ); + g_pd3dDevice->SetRenderState( D3DRS_DESTBLEND, D3DBLEND_ZERO ); + break; + case BLEND_WEIGHTED_MULTIPLY: + g_pd3dDevice->SetRenderState( D3DRS_SRCBLEND, D3DBLEND_DESTCOLOR ); + g_pd3dDevice->SetRenderState( D3DRS_DESTBLEND, D3DBLEND_SRCCOLOR ); + break; + case BLEND_INVERT_DEST: + g_pd3dDevice->SetRenderState( D3DRS_SRCBLEND, D3DBLEND_ONE ); + g_pd3dDevice->SetRenderState( D3DRS_DESTBLEND, D3DBLEND_ONE ); + break; + case BLEND_NO_EFFECT: + g_pd3dDevice->SetRenderState( D3DRS_SRCBLEND, D3DBLEND_ZERO ); + g_pd3dDevice->SetRenderState( D3DRS_DESTBLEND, D3DBLEND_ONE ); + break; + default: + FAIL_M(ssprintf("Invalid BlendMode: %i", mode)); + } +} + +bool RageDisplay_D3D::IsZWriteEnabled() const +{ + DWORD b; + g_pd3dDevice->GetRenderState( D3DRS_ZWRITEENABLE, &b ); + return b!=0; +} + +void RageDisplay_D3D::SetZBias( float f ) +{ + D3DVIEWPORT9 viewData; + g_pd3dDevice->GetViewport( &viewData ); + viewData.MinZ = SCALE( f, 0.0f, 1.0f, 0.05f, 0.0f ); + viewData.MaxZ = SCALE( f, 0.0f, 1.0f, 1.0f, 0.95f ); + g_pd3dDevice->SetViewport( &viewData ); +} + + +bool RageDisplay_D3D::IsZTestEnabled() const +{ + DWORD b; + g_pd3dDevice->GetRenderState( D3DRS_ZFUNC, &b ); + return b!=D3DCMP_ALWAYS; +} + +void RageDisplay_D3D::SetZWrite( bool b ) +{ + g_pd3dDevice->SetRenderState( D3DRS_ZWRITEENABLE, b ); +} + +void RageDisplay_D3D::SetZTestMode( ZTestMode mode ) +{ + g_pd3dDevice->SetRenderState( D3DRS_ZENABLE, D3DZB_TRUE ); + DWORD dw; + switch( mode ) + { + case ZTEST_OFF: dw = D3DCMP_ALWAYS; break; + case ZTEST_WRITE_ON_PASS: dw = D3DCMP_LESSEQUAL; break; + case ZTEST_WRITE_ON_FAIL: dw = D3DCMP_GREATER; break; + default: + dw = D3DCMP_NEVER; + FAIL_M(ssprintf("Invalid ZTestMode: %i", mode)); + } + g_pd3dDevice->SetRenderState( D3DRS_ZFUNC, dw ); +} + +void RageDisplay_D3D::ClearZBuffer() +{ + g_pd3dDevice->Clear( 0, NULL, D3DCLEAR_ZBUFFER, D3DCOLOR_XRGB(0,0,0), 1.0f, 0x00000000 ); +} + +void RageDisplay_D3D::SetTextureWrapping( TextureUnit tu, bool b ) +{ + if( tu >= (int) g_DeviceCaps.MaxSimultaneousTextures ) // not supported + return; + + int mode = b ? D3DTADDRESS_WRAP : D3DTADDRESS_CLAMP; + g_pd3dDevice->SetSamplerState( tu, D3DSAMP_ADDRESSU, mode ); + g_pd3dDevice->SetSamplerState( tu, D3DSAMP_ADDRESSV, mode ); +} + +void RageDisplay_D3D::SetMaterial( + const RageColor &emissive, + const RageColor &ambient, + const RageColor &diffuse, + const RageColor &specular, + float shininess + ) +{ + /* If lighting is off, then the current material will have no effect. + * We want to still be able to color models with lighting off, so shove the + * material color in texture factor and modify the texture stage to use it + * instead of the vertex color (our models don't have vertex coloring anyway). */ + DWORD bLighting; + g_pd3dDevice->GetRenderState( D3DRS_LIGHTING, &bLighting ); + + if( bLighting ) + { + D3DMATERIAL9 mat; + memcpy( &mat.Diffuse, diffuse, sizeof(float)*4 ); + memcpy( &mat.Ambient, ambient, sizeof(float)*4 ); + memcpy( &mat.Specular, specular, sizeof(float)*4 ); + memcpy( &mat.Emissive, emissive, sizeof(float)*4 ); + mat.Power = shininess; + g_pd3dDevice->SetMaterial( &mat ); + } + else + { + RageColor c = diffuse; + c.r += emissive.r + ambient.r; + c.g += emissive.g + ambient.g; + c.b += emissive.b + ambient.b; + RageVColor c2 = c; + DWORD c3 = *(DWORD*)&c2; + g_pd3dDevice->SetRenderState( D3DRS_TEXTUREFACTOR, c3 ); + } +} + +void RageDisplay_D3D::SetLighting( bool b ) +{ + g_pd3dDevice->SetRenderState( D3DRS_LIGHTING, b ); +} + +void RageDisplay_D3D::SetLightOff( int index ) +{ + g_pd3dDevice->LightEnable( index, false ); +} +void RageDisplay_D3D::SetLightDirectional( + int index, + const RageColor &ambient, + const RageColor &diffuse, + const RageColor &specular, + const RageVector3 &dir ) +{ + g_pd3dDevice->LightEnable( index, true ); + + D3DLIGHT9 light; + ZERO( light ); + light.Type = D3DLIGHT_DIRECTIONAL; + + /* Z for lighting is flipped for D3D compared to OpenGL. + * XXX: figure out exactly why this is needed. Our transforms are probably + * goofed up, but the Z test is the same for both API's, so I'm not sure + * why we don't see other weirdness. -Chris */ + float position[] = { dir.x, dir.y, -dir.z }; + memcpy( &light.Direction, position, sizeof(position) ); + memcpy( &light.Diffuse, diffuse, sizeof(diffuse) ); + memcpy( &light.Ambient, ambient, sizeof(ambient) ); + memcpy( &light.Specular, specular, sizeof(specular) ); + + // Same as OpenGL defaults. Not used in directional lights. +// light.Attenuation0 = 1; +// light.Attenuation1 = 0; +// light.Attenuation2 = 0; + + g_pd3dDevice->SetLight( index, &light ); +} + +void RageDisplay_D3D::SetCullMode( CullMode mode ) +{ + switch( mode ) + { + case CULL_BACK: + g_pd3dDevice->SetRenderState( D3DRS_CULLMODE, D3DCULL_CW ); + break; + case CULL_FRONT: + g_pd3dDevice->SetRenderState( D3DRS_CULLMODE, D3DCULL_CCW ); + break; + case CULL_NONE: + g_pd3dDevice->SetRenderState( D3DRS_CULLMODE, D3DCULL_NONE ); + break; + default: + FAIL_M(ssprintf("Invalid CullMode: %i", mode)); + } +} + +void RageDisplay_D3D::DeleteTexture( unsigned iTexHandle ) +{ + if( iTexHandle == 0 ) + return; + + IDirect3DTexture9* pTex = (IDirect3DTexture9*) iTexHandle; + pTex->Release(); + + // Delete palette (if any) + if( g_TexResourceToPaletteIndex.find(iTexHandle) != g_TexResourceToPaletteIndex.end() ) + g_TexResourceToPaletteIndex.erase( g_TexResourceToPaletteIndex.find(iTexHandle) ); + if( g_TexResourceToTexturePalette.find(iTexHandle) != g_TexResourceToTexturePalette.end() ) + g_TexResourceToTexturePalette.erase( g_TexResourceToTexturePalette.find(iTexHandle) ); +} + + +unsigned RageDisplay_D3D::CreateTexture( + RagePixelFormat pixfmt, + RageSurface* img, + bool bGenerateMipMaps ) +{ + HRESULT hr; + IDirect3DTexture9* pTex; + hr = g_pd3dDevice->CreateTexture( power_of_two(img->w), power_of_two(img->h), 1, 0, D3DFORMATS[pixfmt], D3DPOOL_MANAGED, &pTex, NULL ); + + if( FAILED(hr) ) + RageException::Throw( "CreateTexture(%i,%i,%s) failed: %s", + img->w, img->h, RagePixelFormatToString(pixfmt).c_str(), GetErrorString(hr).c_str() ); + + unsigned uTexHandle = (unsigned)pTex; + + if( pixfmt == RagePixelFormat_PAL ) + { + // Save palette + TexturePalette pal; + memset( pal.p, 0, sizeof(pal.p) ); + for( int i=0; iformat->palette->ncolors; i++ ) + { + RageSurfaceColor &c = img->format->palette->colors[i]; + pal.p[i].peRed = c.r; + pal.p[i].peGreen = c.g; + pal.p[i].peBlue = c.b; + pal.p[i].peFlags = c.a; + } + + ASSERT( g_TexResourceToTexturePalette.find(uTexHandle) == g_TexResourceToTexturePalette.end() ); + g_TexResourceToTexturePalette[uTexHandle] = pal; + } + + UpdateTexture( uTexHandle, img, 0, 0, img->w, img->h ); + + return uTexHandle; +} + +void RageDisplay_D3D::UpdateTexture( + unsigned uTexHandle, + RageSurface* img, + int xoffset, int yoffset, int width, int height ) +{ + IDirect3DTexture9* pTex = (IDirect3DTexture9*)uTexHandle; + ASSERT( pTex != nullptr ); + + RECT rect; + rect.left = xoffset; + rect.top = yoffset; + rect.right = width - xoffset; + rect.bottom = height - yoffset; + + D3DLOCKED_RECT lr; + pTex->LockRect( 0, &lr, &rect, 0 ); + + D3DSURFACE_DESC desc; + pTex->GetLevelDesc(0, &desc); + ASSERT( xoffset+width <= int(desc.Width) ); + ASSERT( yoffset+height <= int(desc.Height) ); + + // Copy bits + int texpixfmt; + for(texpixfmt = 0; texpixfmt < NUM_RagePixelFormat; ++texpixfmt) + if(D3DFORMATS[texpixfmt] == desc.Format) break; + ASSERT( texpixfmt != NUM_RagePixelFormat ); + + RageSurface *Texture = CreateSurfaceFromPixfmt(RagePixelFormat(texpixfmt), lr.pBits, width, height, lr.Pitch); + ASSERT( Texture != nullptr ); + RageSurfaceUtils::Blit( img, Texture, width, height ); + + delete Texture; + + pTex->UnlockRect( 0 ); +} + +void RageDisplay_D3D::SetAlphaTest( bool b ) +{ + g_pd3dDevice->SetRenderState( D3DRS_ALPHATESTENABLE, b ); + g_pd3dDevice->SetRenderState( D3DRS_ALPHAREF, 0 ); + g_pd3dDevice->SetRenderState( D3DRS_ALPHAFUNC, D3DCMP_GREATER ); +} + +RageMatrix RageDisplay_D3D::GetOrthoMatrix( float l, float r, float b, float t, float zn, float zf ) +{ + RageMatrix m = RageDisplay::GetOrthoMatrix( l, r, b, t, zn, zf ); + + // Convert from OpenGL's [-1,+1] Z values to D3D's [0,+1]. + RageMatrix tmp; + RageMatrixScaling( &tmp, 1, 1, 0.5f ); + RageMatrixMultiply( &m, &tmp, &m ); + + RageMatrixTranslation( &tmp, 0, 0, 0.5f ); + RageMatrixMultiply( &m, &tmp, &m ); + + return m; +} + +void RageDisplay_D3D::SetSphereEnvironmentMapping( TextureUnit tu, bool b ) +{ + g_bSphereMapping[tu] = b; +} + +void RageDisplay_D3D::SetCelShaded( int stage ) +{ + // todo: implement me! +} + +/* + * Copyright (c) 2001-2004 Chris Danford, Glenn Maynard + * 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. + */ diff --git a/src/RageDisplay_GLES2.cpp b/src/RageDisplay_GLES2.cpp index 87da513fab..cd87c132dc 100644 --- a/src/RageDisplay_GLES2.cpp +++ b/src/RageDisplay_GLES2.cpp @@ -1,1005 +1,1005 @@ -#include "global.h" - -#include "RageDisplay.h" -#include "RageDisplay_GLES2.h" -#include "RageUtil.h" -#include "RageLog.h" -#include "RageTimer.h" -#include "RageMath.h" -#include "RageTypes.h" -#include "RageUtil.h" -#include "RageSurface.h" -#include "RageTextureManager.h" - -#include "DisplayResolutions.h" - -#include "arch/LowLevelWindow/LowLevelWindow.h" - -#include - -#ifdef NO_GL_FLUSH -#define glFlush() -#endif - -namespace -{ - RageDisplay::RagePixelFormatDesc - PIXEL_FORMAT_DESC[NUM_RagePixelFormat] = { - { - /* R8G8B8A8 */ - 32, - { 0xFF000000, - 0x00FF0000, - 0x0000FF00, - 0x000000FF } - }, { - /* B8G8R8A8 */ - 32, - { 0x0000FF00, - 0x00FF0000, - 0xFF000000, - 0x000000FF } - }, { - /* R4G4B4A4 */ - 16, - { 0xF000, - 0x0F00, - 0x00F0, - 0x000F }, - }, { - /* R5G5B5A1 */ - 16, - { 0xF800, - 0x07C0, - 0x003E, - 0x0001 }, - }, { - /* R5G5B5X1 */ - 16, - { 0xF800, - 0x07C0, - 0x003E, - 0x0000 }, - }, { - /* R8G8B8 */ - 24, - { 0xFF0000, - 0x00FF00, - 0x0000FF, - 0x000000 } - }, { - /* Paletted */ - 8, - { 0,0,0,0 } /* N/A */ - }, { - /* B8G8R8 */ - 24, - { 0x0000FF, - 0x00FF00, - 0xFF0000, - 0x000000 } - }, { - /* A1R5G5B5 */ - 16, - { 0x7C00, - 0x03E0, - 0x001F, - 0x8000 }, - }, { - /* X1R5G5B5 */ - 16, - { 0x7C00, - 0x03E0, - 0x001F, - 0x0000 }, - } - }; - - /* g_GLPixFmtInfo is used for both texture formats and surface formats. - * For example, it's fine to ask for a RagePixelFormat_RGB5 texture, but to - * supply a surface matching RagePixelFormat_RGB8. OpenGL will simply - * discard the extra bits. - * - * It's possible for a format to be supported as a texture format but - * not as a surface format. For example, if packed pixels aren't - * supported, we can still use GL_RGB5_A1, but we'll have to convert to - * a supported surface pixel format first. It's not ideal, since we'll - * convert to RGBA8 and OGL will convert back, but it works fine. - */ - struct GLPixFmtInfo_t { - GLenum internalfmt; /* target format */ - GLenum format; /* target format */ - GLenum type; /* data format */ - } const g_GLPixFmtInfo[NUM_RagePixelFormat] = { - { - /* R8G8B8A8 */ - GL_RGBA8, - GL_RGBA, - GL_UNSIGNED_BYTE, - }, { - /* R8G8B8A8 */ - GL_RGBA8, - GL_BGRA, - GL_UNSIGNED_BYTE, - }, { - /* B4G4R4A4 */ - GL_RGBA4, - GL_RGBA, - GL_UNSIGNED_SHORT_4_4_4_4, - }, { - /* B5G5R5A1 */ - GL_RGB5_A1, - GL_RGBA, - GL_UNSIGNED_SHORT_5_5_5_1, - }, { - /* B5G5R5 */ - GL_RGB5, - GL_RGBA, - GL_UNSIGNED_SHORT_5_5_5_1, - }, { - /* B8G8R8 */ - GL_RGB8, - GL_RGB, - GL_UNSIGNED_BYTE, - }, { - /* Paletted */ - GL_COLOR_INDEX8_EXT, - GL_COLOR_INDEX, - GL_UNSIGNED_BYTE, - }, { - /* B8G8R8 */ - GL_RGB8, - GL_BGR, - GL_UNSIGNED_BYTE, - }, { - // TODO: These don't work on ES2. Work out what needs to happen. - /* A1R5G5B5 (matches D3DFMT_A1R5G5B5) */ - GL_RGB5_A1, - GL_BGRA, - GL_UNSIGNED_SHORT_1_5_5_5_REV, - }, { - /* X1R5G5B5 */ - GL_RGB5, - GL_BGRA, - GL_UNSIGNED_SHORT_1_5_5_5_REV, - } - }; - - LowLevelWindow *g_pWind; - - void FixLittleEndian() - { -#if defined(ENDIAN_LITTLE) - static bool bInitialized = false; - if (bInitialized) - return; - bInitialized = true; - - for( int i = 0; i < NUM_RagePixelFormat; ++i ) - { - RageDisplay::RagePixelFormatDesc &pf = PIXEL_FORMAT_DESC[i]; - - /* OpenGL and RageSurface handle byte formats differently; we need - * to flip non-paletted masks to make them line up. */ - if (g_GLPixFmtInfo[i].type != GL_UNSIGNED_BYTE || pf.bpp == 8) - continue; - - for( int mask = 0; mask < 4; ++mask) - { - int m = pf.masks[mask]; - switch( pf.bpp ) - { - case 24: m = Swap24(m); break; - case 32: m = Swap32(m); break; - default: - FAIL_M(ssprintf("Unsupported BPP value: %i", pf.bpp)); - } - pf.masks[mask] = m; - } - } -#endif - } - namespace Caps - { - int iMaxTextureUnits = 1; - int iMaxTextureSize = 256; - } - namespace State - { - bool bZTestEnabled = false; - bool bZWriteEnabled = false; - bool bAlphaTestEnabled = false; - } -} - -RageDisplay_GLES2::RageDisplay_GLES2() -{ - LOG->Trace( "RageDisplay_GLES2::RageDisplay_GLES2()" ); - LOG->MapLog("renderer", "Current renderer: OpenGL ES 2.0"); - - FixLittleEndian(); -// RageDisplay_GLES2_Helpers::Init(); - - g_pWind = NULL; -} - -RString -RageDisplay_GLES2::Init( const VideoModeParams &p, bool bAllowUnacceleratedRenderer ) -{ - g_pWind = LowLevelWindow::Create(); - - bool bIgnore = false; - RString sError = SetVideoMode( p, bIgnore ); - if (sError != "") - return sError; - - // Get GPU capabilities up front so we don't have to query later. - glGetIntegerv( GL_MAX_TEXTURE_SIZE, &Caps::iMaxTextureSize ); - glGetIntegerv( GL_MAX_TEXTURE_IMAGE_UNITS, &Caps::iMaxTextureUnits ); - - // Log driver details - g_pWind->LogDebugInformation(); - LOG->Info( "OGL Vendor: %s", glGetString(GL_VENDOR) ); - LOG->Info( "OGL Renderer: %s", glGetString(GL_RENDERER) ); - LOG->Info( "OGL Version: %s", glGetString(GL_VERSION) ); - LOG->Info( "OGL Max texture size: %i", Caps::iMaxTextureSize ); - LOG->Info( "OGL Texture units: %i", Caps::iMaxTextureUnits ); - - /* Pretty-print the extension string: */ - LOG->Info( "OGL Extensions:" ); - { - // glGetString(GL_EXTENSIONS) doesn't work for GL3 core profiles. - // this will be useful in the future. -#if 0 - vector extensions; - const char *ext = 0; - for (int i = 0; (ext = (const char*)glGetStringi(GL_EXTENSIONS, i)); i++) - { - extensions.push_back(string(ext)); - } - - sort( extensions.begin(), extensions.end() ); - size_t next = 0; - while( next < extensions.size() ) - { - size_t last = next; - string type; - for( size_t i = next; i segments; - split(extensions[i], '_', segments); - string this_type; - if (segments.size() > 2) - this_type = join("_", segments.begin(), segments.begin()+2); - if (i > next && this_type != type) - break; - type = this_type; - last = i; - } - - if (next == last) - { - printf( " %s\n", extensions[next].c_str() ); - ++next; - continue; - } - - string sList = ssprintf( " %s: ", type.c_str() ); - while( next <= last ) - { - vector segments; - split( extensions[next], '_', segments ); - string ext_short = join( "_", segments.begin()+2, segments.end() ); - sList += ext_short; - if (next < last) - sList += ", "; - if (next == last || sList.size() + extensions[next+1].size() > 78) - { - printf( "%s\n", sList.c_str() ); - sList = " "; - } - ++next; - } - } -#else - const char *szExtensionString = (const char *) glGetString(GL_EXTENSIONS); - vector asExtensions; - split( szExtensionString, " ", asExtensions ); - sort( asExtensions.begin(), asExtensions.end() ); - size_t iNextToPrint = 0; - while( iNextToPrint < asExtensions.size() ) - { - size_t iLastToPrint = iNextToPrint; - RString sType; - for( size_t i = iNextToPrint; i asBits; - split( asExtensions[i], "_", asBits ); - RString sThisType; - if (asBits.size() > 2) - sThisType = join( "_", asBits.begin(), asBits.begin()+2 ); - if (i > iNextToPrint && sThisType != sType) - break; - sType = sThisType; - iLastToPrint = i; - } - - if (iNextToPrint == iLastToPrint) - { - LOG->Info( " %s", asExtensions[iNextToPrint].c_str() ); - ++iNextToPrint; - continue; - } - - RString sList = ssprintf( " %s: ", sType.c_str() ); - while( iNextToPrint <= iLastToPrint ) - { - vector asBits; - split( asExtensions[iNextToPrint], "_", asBits ); - RString sShortExt = join( "_", asBits.begin()+2, asBits.end() ); - sList += sShortExt; - if (iNextToPrint < iLastToPrint) - sList += ", "; - if (iNextToPrint == iLastToPrint || sList.size() + asExtensions[iNextToPrint+1].size() > 120) - { - LOG->Info( "%s", sList.c_str() ); - sList = " "; - } - ++iNextToPrint; - } -#endif - } - } - - glewExperimental = true; - glewInit(); - - /* Log this, so if people complain that the radar looks bad on their - * system we can compare them: */ - //glGetFloatv( GL_LINE_WIDTH_RANGE, g_line_range ); - //glGetFloatv( GL_POINT_SIZE_RANGE, g_point_range ); - - return RString(); -} - -// Return true if mode change was successful. -// bNewDeviceOut is set true if a new device was created and textures -// need to be reloaded. -RString RageDisplay_GLES2::TryVideoMode( const VideoModeParams &p, bool &bNewDeviceOut ) -{ - VideoModeParams vm = p; - vm.windowed = 1; // force windowed until I trust this thing. - LOG->Warn( "RageDisplay_GLES2::TryVideoMode( %d, %d, %d, %d, %d, %d )", - vm.windowed, vm.width, vm.height, vm.bpp, vm.rate, vm.vsync ); - - RString err = g_pWind->TryVideoMode( vm, bNewDeviceOut ); - if (err != "") - return err; // failed to set video mode - - if (bNewDeviceOut) - { - // NOTE: This isn't needed in an actual GLES2 context... - glewInit(); - - /* We have a new OpenGL context, so we have to tell our textures that - * their OpenGL texture number is invalid. */ - if (TEXTUREMAN) - TEXTUREMAN->InvalidateTextures(); - - /* Delete all render targets. They may have associated resources other than - * the texture itself. */ - //FOREACHM( unsigned, RenderTarget *, g_mapRenderTargets, rt ) - // delete rt->second; - //g_mapRenderTargets.clear(); - - /* Recreate all vertex buffers. */ - //InvalidateObjects(); - - //InitShaders(); - } - - ResolutionChanged(); - - return RString(); -} - -int RageDisplay_GLES2::GetMaxTextureSize() const -{ - return Caps::iMaxTextureSize; -} - -bool RageDisplay_GLES2::BeginFrame() -{ - /* We do this in here, rather than ResolutionChanged, or we won't update the - * viewport for the concurrent rendering context. */ - int fWidth = g_pWind->GetActualVideoModeParams().width; - int fHeight = g_pWind->GetActualVideoModeParams().height; - - glViewport( 0, 0, fWidth, fHeight ); - - glClearColor( 0.0f, 0.0f, 0.0f, 1.0f ); - SetZWrite( true ); - glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT ); - - return RageDisplay::BeginFrame(); -} - -void RageDisplay_GLES2::EndFrame() -{ - glFlush(); - - // XXX: This is broken on NVidia, as their xrandr sucks. - FrameLimitBeforeVsync( g_pWind->GetActualVideoModeParams().rate ); - g_pWind->SwapBuffers(); - FrameLimitAfterVsync(); - - g_pWind->Update(); - - RageDisplay::EndFrame(); -} - -RageDisplay_GLES2::~RageDisplay_GLES2() -{ - delete g_pWind; -} - -void -RageDisplay_GLES2::GetDisplayResolutions( DisplayResolutions &out ) const -{ - out.clear(); - g_pWind->GetDisplayResolutions( out ); -} - -RageSurface* -RageDisplay_GLES2::CreateScreenshot() -{ - const RagePixelFormatDesc &desc = PIXEL_FORMAT_DESC[RagePixelFormat_RGB8]; - RageSurface *image = CreateSurface( - 640, 480, desc.bpp, - desc.masks[0], desc.masks[1], desc.masks[2], desc.masks[3] ); - - memset( image->pixels, 0, 480*image->pitch ); - - return image; -} - -const RageDisplay::RagePixelFormatDesc* -RageDisplay_GLES2::GetPixelFormatDesc(RagePixelFormat pf) const -{ - ASSERT( pf >= 0 && pf < NUM_RagePixelFormat ); - return &PIXEL_FORMAT_DESC[pf]; -} - -RageMatrix -RageDisplay_GLES2::GetOrthoMatrix( float l, float r, float b, float t, float zn, float zf ) -{ - RageMatrix m( - 2/(r-l), 0, 0, 0, - 0, 2/(t-b), 0, 0, - 0, 0, -2/(zf-zn), 0, - -(r+l)/(r-l), -(t+b)/(t-b), -(zf+zn)/(zf-zn), 1 ); - return m; -} - -class RageCompiledGeometryGLES2 : public RageCompiledGeometry -{ -public: - - void Allocate( const vector &vMeshes ) - { - // TODO - } - void Change( const vector &vMeshes ) - { - // TODO - } - void Draw( int iMeshIndex ) const - { - // TOO - } -}; - -RageCompiledGeometry* -RageDisplay_GLES2::CreateCompiledGeometry() -{ - return new RageCompiledGeometryGLES2; -} - -void -RageDisplay_GLES2::DeleteCompiledGeometry( RageCompiledGeometry *p ) -{ - delete p; -} - -RString -RageDisplay_GLES2::GetApiDescription() const -{ - return "OpenGL ES 2.0"; -} - -VideoModeParams -RageDisplay_GLES2::GetActualVideoModeParams() const -{ - return g_pWind->GetActualVideoModeParams(); -} - -void -RageDisplay_GLES2::SetBlendMode( BlendMode mode ) -{ - // TODO -} - -bool -RageDisplay_GLES2::SupportsTextureFormat( RagePixelFormat pixfmt, bool realtime ) -{ - /* If we support a pixfmt for texture formats but not for surface formats, then - * we'll have to convert the texture to a supported surface format before uploading. - * This is too slow for dynamic textures. */ - if (realtime && !SupportsSurfaceFormat(pixfmt)) - return false; - - switch (g_GLPixFmtInfo[pixfmt].format) - { - case GL_COLOR_INDEX: - return false; - case GL_BGR: - case GL_BGRA: - //return !!GLEW_EXT_bgra; - return false; // no BGRA on ES2 (without exts) - default: - return true; - } - - return true; -} - -bool -RageDisplay_GLES2::SupportsPerVertexMatrixScale() -{ - return true; -} - -unsigned -RageDisplay_GLES2::CreateTexture( - RagePixelFormat pixfmt, - RageSurface* img, - bool bGenerateMipMaps - ) -{ - // TODO - return 1; -} - -void -RageDisplay_GLES2::UpdateTexture( - unsigned iTexHandle, - RageSurface* img, - int xoffset, int yoffset, int width, int height - ) -{ - // TODO -} - -void -RageDisplay_GLES2::DeleteTexture( unsigned iTexHandle ) -{ - // TODO -} - -void -RageDisplay_GLES2::ClearAllTextures() -{ - FOREACH_ENUM( TextureUnit, i ) - SetTexture( i, 0 ); - - // HACK: Reset the active texture to 0. - // TODO: Change all texture functions to take a stage number. - glActiveTexture(GL_TEXTURE0); -} - -int -RageDisplay_GLES2::GetNumTextureUnits() -{ - return Caps::iMaxTextureUnits; -} - -static bool -SetTextureUnit( TextureUnit tu ) -{ - if ((int) tu > Caps::iMaxTextureUnits) - return false; - glActiveTexture( enum_add2(GL_TEXTURE0, tu) ); - return true; -} - -void -RageDisplay_GLES2::SetTexture( TextureUnit tu, unsigned iTexture ) -{ - if (!SetTextureUnit( tu )) - return; - - if (iTexture) - { - glEnable( GL_TEXTURE_2D ); - glBindTexture( GL_TEXTURE_2D, iTexture ); - } - else - { - glDisable( GL_TEXTURE_2D ); - } -} - -void -RageDisplay_GLES2::SetTextureMode( TextureUnit tu, TextureMode tm ) -{ - // TODO -} - -void -RageDisplay_GLES2::SetTextureWrapping( TextureUnit tu, bool b ) -{ - // TODO -} - -void -RageDisplay_GLES2::SetTextureFiltering( TextureUnit tu, bool b ) -{ - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, b ? GL_LINEAR : GL_NEAREST); - - GLint iMinFilter = 0; - if (b) - { - GLint iWidth1 = -1; - GLint iWidth2 = -1; - glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &iWidth1); - glGetTexLevelParameteriv(GL_TEXTURE_2D, 1, GL_TEXTURE_WIDTH, &iWidth2); - if (iWidth1 > 1 && iWidth2 != 0) - { - /* Mipmaps are enabled. */ - if (g_pWind->GetActualVideoModeParams().bTrilinearFiltering) - iMinFilter = GL_LINEAR_MIPMAP_LINEAR; - else - iMinFilter = GL_LINEAR_MIPMAP_NEAREST; - } - else - { - iMinFilter = GL_LINEAR; - } - } - else - { - iMinFilter = GL_NEAREST; - } - - glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, iMinFilter ); -} - -bool -RageDisplay_GLES2::IsZWriteEnabled() const -{ - return State::bZWriteEnabled; -} - -bool -RageDisplay_GLES2::IsZTestEnabled() const -{ - return State::bZTestEnabled; -} - -void -RageDisplay_GLES2::SetZWrite( bool b ) -{ - if (State::bZWriteEnabled != b) - { - State::bZWriteEnabled = b; - glDepthMask( b ); - } -} - -void -RageDisplay_GLES2::SetZBias( float f ) -{ - float fNear = SCALE( f, 0.0f, 1.0f, 0.05f, 0.0f ); - float fFar = SCALE( f, 0.0f, 1.0f, 1.0f, 0.95f ); - - glDepthRange( fNear, fFar ); -} - -void -RageDisplay_GLES2::SetZTestMode( ZTestMode mode ) -{ - glEnable( GL_DEPTH_TEST ); - switch( mode ) - { - case ZTEST_OFF: - glDisable( GL_DEPTH_TEST ); - glDepthFunc( GL_ALWAYS ); - State::bZTestEnabled = false; - break; - case ZTEST_WRITE_ON_PASS: glDepthFunc( GL_LEQUAL ); break; - case ZTEST_WRITE_ON_FAIL: glDepthFunc( GL_GREATER ); break; - default: - FAIL_M(ssprintf("Invalid ZTestMode: %i", mode)); - } - State::bZTestEnabled = true; -} - - - - - -/* - - -void RageDisplay_Legacy::SetBlendMode( BlendMode mode ) -{ - glEnable(GL_BLEND); - - if (glBlendEquation != NULL) - { - if (mode == BLEND_INVERT_DEST) - glBlendEquation( GL_FUNC_SUBTRACT ); - else if (mode == BLEND_SUBTRACT) - glBlendEquation( GL_FUNC_REVERSE_SUBTRACT ); - else - glBlendEquation( GL_FUNC_ADD ); - } - - int iSourceRGB, iDestRGB; - int iSourceAlpha = GL_ONE, iDestAlpha = GL_ONE_MINUS_SRC_ALPHA; - switch( mode ) - { - case BLEND_NORMAL: - iSourceRGB = GL_SRC_ALPHA; iDestRGB = GL_ONE_MINUS_SRC_ALPHA; - break; - case BLEND_ADD: - iSourceRGB = GL_SRC_ALPHA; iDestRGB = GL_ONE; - break; - case BLEND_SUBTRACT: - iSourceRGB = GL_SRC_ALPHA; iDestRGB = GL_ONE_MINUS_SRC_ALPHA; - break; - case BLEND_MODULATE: - iSourceRGB = GL_ZERO; iDestRGB = GL_SRC_COLOR; - break; - case BLEND_COPY_SRC: - iSourceRGB = GL_ONE; iDestRGB = GL_ZERO; - iSourceAlpha = GL_ONE; iDestAlpha = GL_ZERO; - break; - case BLEND_ALPHA_MASK: - iSourceRGB = GL_ZERO; iDestRGB = GL_ONE; - iSourceAlpha = GL_ZERO; iDestAlpha = GL_SRC_ALPHA; - break; - case BLEND_ALPHA_KNOCK_OUT: - iSourceRGB = GL_ZERO; iDestRGB = GL_ONE; - iSourceAlpha = GL_ZERO; iDestAlpha = GL_ONE_MINUS_SRC_ALPHA; - break; - case BLEND_ALPHA_MULTIPLY: - iSourceRGB = GL_SRC_ALPHA; iDestRGB = GL_ZERO; - break; - case BLEND_WEIGHTED_MULTIPLY: - // output = 2*(dst*src). 0.5,0.5,0.5 is identity; darker colors darken the image, - // and brighter colors lighten the image. - iSourceRGB = GL_DST_COLOR; iDestRGB = GL_SRC_COLOR; - break; - case BLEND_INVERT_DEST: - // out = src - dst. The source color should almost always be #FFFFFF, to make it "1 - dst". - iSourceRGB = GL_ONE; iDestRGB = GL_ONE; - break; - case BLEND_NO_EFFECT: - iSourceRGB = GL_ZERO; iDestRGB = GL_ONE; - iSourceAlpha = GL_ZERO; iDestAlpha = GL_ONE; - break; - DEFAULT_FAIL( mode ); - } - - if (GLEW_EXT_blend_equation_separate) - glBlendFuncSeparateEXT( iSourceRGB, iDestRGB, iSourceAlpha, iDestAlpha ); - else - glBlendFunc( iSourceRGB, iDestRGB ); -} - -bool RageDisplay_Legacy::IsZWriteEnabled() const -{ - bool a; - glGetBooleanv( GL_DEPTH_WRITEMASK, (unsigned char*)&a ); - return a; -} - - -*/ - -void -RageDisplay_GLES2::ClearZBuffer() -{ - bool write = IsZWriteEnabled(); - SetZWrite( true ); - glClear( GL_DEPTH_BUFFER_BIT ); - SetZWrite( write ); -} - -void -RageDisplay_GLES2::SetCullMode( CullMode mode ) -{ - if (mode != CULL_NONE) - glEnable(GL_CULL_FACE); - switch( mode ) - { - case CULL_BACK: - glCullFace( GL_BACK ); - break; - case CULL_FRONT: - glCullFace( GL_FRONT ); - break; - case CULL_NONE: - glDisable( GL_CULL_FACE ); - break; - default: - FAIL_M(ssprintf("Invalid CullMode: %i", mode)); - } -} - -void -RageDisplay_GLES2::SetAlphaTest( bool b ) -{ - void (*toggle)(GLenum) = b ? glEnable : glDisable; - if (State::bAlphaTestEnabled != b) - { - State::bAlphaTestEnabled = b; - toggle(GL_ALPHA_TEST); - } -} - -void -RageDisplay_GLES2::SetMaterial( - const RageColor &emissive, - const RageColor &ambient, - const RageColor &diffuse, - const RageColor &specular, - float shininess - ) -{ - // TODO -} - -void -RageDisplay_GLES2::SetLineWidth(float fWidth) -{ - glLineWidth(fWidth); -} - -void -RageDisplay_GLES2::SetPolygonMode(PolygonMode pm) -{ - GLenum m; - switch (pm) - { - case POLYGON_FILL: m = GL_FILL; break; - case POLYGON_LINE: m = GL_LINE; break; - default: - FAIL_M(ssprintf("Invalid PolygonMode: %i", pm)); - } - glPolygonMode(GL_FRONT_AND_BACK, m); -} - -void -RageDisplay_GLES2::SetLighting( bool b ) -{ - // TODO -} - -void -RageDisplay_GLES2::SetLightOff( int index ) -{ - // TODO -} - -void -RageDisplay_GLES2::SetLightDirectional( - int index, - const RageColor &ambient, - const RageColor &diffuse, - const RageColor &specular, - const RageVector3 &dir ) -{ - // TODO -} - -void -RageDisplay_GLES2::SetSphereEnvironmentMapping( TextureUnit tu, bool b ) -{ - // TODO -} - -void -RageDisplay_GLES2::SetCelShaded( int stage ) -{ - // TODO -} - -void -RageDisplay_GLES2::DrawQuadsInternal( const RageSpriteVertex v[], int iNumVerts ) -{ - // TODO -} - -void -RageDisplay_GLES2::DrawQuadStripInternal( const RageSpriteVertex v[], int iNumVerts ) -{ - // TODO -} - -void -RageDisplay_GLES2::DrawFanInternal( const RageSpriteVertex v[], int iNumVerts ) -{ - // TODO -} - -void -RageDisplay_GLES2::DrawStripInternal( const RageSpriteVertex v[], int iNumVerts ) -{ - // TODO -} - -void -RageDisplay_GLES2::DrawTrianglesInternal( const RageSpriteVertex v[], int iNumVerts ) -{ - // TODO -} - -void -RageDisplay_GLES2::DrawCompiledGeometryInternal( const RageCompiledGeometry *p, int - iMeshIndex ) -{ - // TODO -} - -void -RageDisplay_GLES2::DrawLineStripInternal( const RageSpriteVertex v[], int iNumVerts, float LineWidth ) -{ - // TODO -} - -// Is this even used? -void -RageDisplay_GLES2::DrawSymmetricQuadStripInternal( const RageSpriteVertex v[], int iNumVerts ) -{ - // TODO -} - -bool -RageDisplay_GLES2::SupportsSurfaceFormat( RagePixelFormat pixfmt ) -{ - switch (g_GLPixFmtInfo[pixfmt].type) - { - case GL_UNSIGNED_SHORT_1_5_5_5_REV: - return false; - //return GLEW_EXT_bgra && g_bReversePackedPixelsWorks; - default: - return true; - } -} - -/* - * Copyright (c) 2012 Colby Klein - * 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. - */ +#include "global.h" + +#include "RageDisplay.h" +#include "RageDisplay_GLES2.h" +#include "RageUtil.h" +#include "RageLog.h" +#include "RageTimer.h" +#include "RageMath.h" +#include "RageTypes.h" +#include "RageUtil.h" +#include "RageSurface.h" +#include "RageTextureManager.h" + +#include "DisplayResolutions.h" + +#include "arch/LowLevelWindow/LowLevelWindow.h" + +#include + +#ifdef NO_GL_FLUSH +#define glFlush() +#endif + +namespace +{ + RageDisplay::RagePixelFormatDesc + PIXEL_FORMAT_DESC[NUM_RagePixelFormat] = { + { + /* R8G8B8A8 */ + 32, + { 0xFF000000, + 0x00FF0000, + 0x0000FF00, + 0x000000FF } + }, { + /* B8G8R8A8 */ + 32, + { 0x0000FF00, + 0x00FF0000, + 0xFF000000, + 0x000000FF } + }, { + /* R4G4B4A4 */ + 16, + { 0xF000, + 0x0F00, + 0x00F0, + 0x000F }, + }, { + /* R5G5B5A1 */ + 16, + { 0xF800, + 0x07C0, + 0x003E, + 0x0001 }, + }, { + /* R5G5B5X1 */ + 16, + { 0xF800, + 0x07C0, + 0x003E, + 0x0000 }, + }, { + /* R8G8B8 */ + 24, + { 0xFF0000, + 0x00FF00, + 0x0000FF, + 0x000000 } + }, { + /* Paletted */ + 8, + { 0,0,0,0 } /* N/A */ + }, { + /* B8G8R8 */ + 24, + { 0x0000FF, + 0x00FF00, + 0xFF0000, + 0x000000 } + }, { + /* A1R5G5B5 */ + 16, + { 0x7C00, + 0x03E0, + 0x001F, + 0x8000 }, + }, { + /* X1R5G5B5 */ + 16, + { 0x7C00, + 0x03E0, + 0x001F, + 0x0000 }, + } + }; + + /* g_GLPixFmtInfo is used for both texture formats and surface formats. + * For example, it's fine to ask for a RagePixelFormat_RGB5 texture, but to + * supply a surface matching RagePixelFormat_RGB8. OpenGL will simply + * discard the extra bits. + * + * It's possible for a format to be supported as a texture format but + * not as a surface format. For example, if packed pixels aren't + * supported, we can still use GL_RGB5_A1, but we'll have to convert to + * a supported surface pixel format first. It's not ideal, since we'll + * convert to RGBA8 and OGL will convert back, but it works fine. + */ + struct GLPixFmtInfo_t { + GLenum internalfmt; /* target format */ + GLenum format; /* target format */ + GLenum type; /* data format */ + } const g_GLPixFmtInfo[NUM_RagePixelFormat] = { + { + /* R8G8B8A8 */ + GL_RGBA8, + GL_RGBA, + GL_UNSIGNED_BYTE, + }, { + /* R8G8B8A8 */ + GL_RGBA8, + GL_BGRA, + GL_UNSIGNED_BYTE, + }, { + /* B4G4R4A4 */ + GL_RGBA4, + GL_RGBA, + GL_UNSIGNED_SHORT_4_4_4_4, + }, { + /* B5G5R5A1 */ + GL_RGB5_A1, + GL_RGBA, + GL_UNSIGNED_SHORT_5_5_5_1, + }, { + /* B5G5R5 */ + GL_RGB5, + GL_RGBA, + GL_UNSIGNED_SHORT_5_5_5_1, + }, { + /* B8G8R8 */ + GL_RGB8, + GL_RGB, + GL_UNSIGNED_BYTE, + }, { + /* Paletted */ + GL_COLOR_INDEX8_EXT, + GL_COLOR_INDEX, + GL_UNSIGNED_BYTE, + }, { + /* B8G8R8 */ + GL_RGB8, + GL_BGR, + GL_UNSIGNED_BYTE, + }, { + // TODO: These don't work on ES2. Work out what needs to happen. + /* A1R5G5B5 (matches D3DFMT_A1R5G5B5) */ + GL_RGB5_A1, + GL_BGRA, + GL_UNSIGNED_SHORT_1_5_5_5_REV, + }, { + /* X1R5G5B5 */ + GL_RGB5, + GL_BGRA, + GL_UNSIGNED_SHORT_1_5_5_5_REV, + } + }; + + LowLevelWindow *g_pWind; + + void FixLittleEndian() + { +#if defined(ENDIAN_LITTLE) + static bool bInitialized = false; + if (bInitialized) + return; + bInitialized = true; + + for( int i = 0; i < NUM_RagePixelFormat; ++i ) + { + RageDisplay::RagePixelFormatDesc &pf = PIXEL_FORMAT_DESC[i]; + + /* OpenGL and RageSurface handle byte formats differently; we need + * to flip non-paletted masks to make them line up. */ + if (g_GLPixFmtInfo[i].type != GL_UNSIGNED_BYTE || pf.bpp == 8) + continue; + + for( int mask = 0; mask < 4; ++mask) + { + int m = pf.masks[mask]; + switch( pf.bpp ) + { + case 24: m = Swap24(m); break; + case 32: m = Swap32(m); break; + default: + FAIL_M(ssprintf("Unsupported BPP value: %i", pf.bpp)); + } + pf.masks[mask] = m; + } + } +#endif + } + namespace Caps + { + int iMaxTextureUnits = 1; + int iMaxTextureSize = 256; + } + namespace State + { + bool bZTestEnabled = false; + bool bZWriteEnabled = false; + bool bAlphaTestEnabled = false; + } +} + +RageDisplay_GLES2::RageDisplay_GLES2() +{ + LOG->Trace( "RageDisplay_GLES2::RageDisplay_GLES2()" ); + LOG->MapLog("renderer", "Current renderer: OpenGL ES 2.0"); + + FixLittleEndian(); +// RageDisplay_GLES2_Helpers::Init(); + + g_pWind = NULL; +} + +RString +RageDisplay_GLES2::Init( const VideoModeParams &p, bool bAllowUnacceleratedRenderer ) +{ + g_pWind = LowLevelWindow::Create(); + + bool bIgnore = false; + RString sError = SetVideoMode( p, bIgnore ); + if (sError != "") + return sError; + + // Get GPU capabilities up front so we don't have to query later. + glGetIntegerv( GL_MAX_TEXTURE_SIZE, &Caps::iMaxTextureSize ); + glGetIntegerv( GL_MAX_TEXTURE_IMAGE_UNITS, &Caps::iMaxTextureUnits ); + + // Log driver details + g_pWind->LogDebugInformation(); + LOG->Info( "OGL Vendor: %s", glGetString(GL_VENDOR) ); + LOG->Info( "OGL Renderer: %s", glGetString(GL_RENDERER) ); + LOG->Info( "OGL Version: %s", glGetString(GL_VERSION) ); + LOG->Info( "OGL Max texture size: %i", Caps::iMaxTextureSize ); + LOG->Info( "OGL Texture units: %i", Caps::iMaxTextureUnits ); + + /* Pretty-print the extension string: */ + LOG->Info( "OGL Extensions:" ); + { + // glGetString(GL_EXTENSIONS) doesn't work for GL3 core profiles. + // this will be useful in the future. +#if 0 + vector extensions; + const char *ext = 0; + for (int i = 0; (ext = (const char*)glGetStringi(GL_EXTENSIONS, i)); i++) + { + extensions.push_back(string(ext)); + } + + sort( extensions.begin(), extensions.end() ); + size_t next = 0; + while( next < extensions.size() ) + { + size_t last = next; + string type; + for( size_t i = next; i segments; + split(extensions[i], '_', segments); + string this_type; + if (segments.size() > 2) + this_type = join("_", segments.begin(), segments.begin()+2); + if (i > next && this_type != type) + break; + type = this_type; + last = i; + } + + if (next == last) + { + printf( " %s\n", extensions[next].c_str() ); + ++next; + continue; + } + + string sList = ssprintf( " %s: ", type.c_str() ); + while( next <= last ) + { + vector segments; + split( extensions[next], '_', segments ); + string ext_short = join( "_", segments.begin()+2, segments.end() ); + sList += ext_short; + if (next < last) + sList += ", "; + if (next == last || sList.size() + extensions[next+1].size() > 78) + { + printf( "%s\n", sList.c_str() ); + sList = " "; + } + ++next; + } + } +#else + const char *szExtensionString = (const char *) glGetString(GL_EXTENSIONS); + vector asExtensions; + split( szExtensionString, " ", asExtensions ); + sort( asExtensions.begin(), asExtensions.end() ); + size_t iNextToPrint = 0; + while( iNextToPrint < asExtensions.size() ) + { + size_t iLastToPrint = iNextToPrint; + RString sType; + for( size_t i = iNextToPrint; i asBits; + split( asExtensions[i], "_", asBits ); + RString sThisType; + if (asBits.size() > 2) + sThisType = join( "_", asBits.begin(), asBits.begin()+2 ); + if (i > iNextToPrint && sThisType != sType) + break; + sType = sThisType; + iLastToPrint = i; + } + + if (iNextToPrint == iLastToPrint) + { + LOG->Info( " %s", asExtensions[iNextToPrint].c_str() ); + ++iNextToPrint; + continue; + } + + RString sList = ssprintf( " %s: ", sType.c_str() ); + while( iNextToPrint <= iLastToPrint ) + { + vector asBits; + split( asExtensions[iNextToPrint], "_", asBits ); + RString sShortExt = join( "_", asBits.begin()+2, asBits.end() ); + sList += sShortExt; + if (iNextToPrint < iLastToPrint) + sList += ", "; + if (iNextToPrint == iLastToPrint || sList.size() + asExtensions[iNextToPrint+1].size() > 120) + { + LOG->Info( "%s", sList.c_str() ); + sList = " "; + } + ++iNextToPrint; + } +#endif + } + } + + glewExperimental = true; + glewInit(); + + /* Log this, so if people complain that the radar looks bad on their + * system we can compare them: */ + //glGetFloatv( GL_LINE_WIDTH_RANGE, g_line_range ); + //glGetFloatv( GL_POINT_SIZE_RANGE, g_point_range ); + + return RString(); +} + +// Return true if mode change was successful. +// bNewDeviceOut is set true if a new device was created and textures +// need to be reloaded. +RString RageDisplay_GLES2::TryVideoMode( const VideoModeParams &p, bool &bNewDeviceOut ) +{ + VideoModeParams vm = p; + vm.windowed = 1; // force windowed until I trust this thing. + LOG->Warn( "RageDisplay_GLES2::TryVideoMode( %d, %d, %d, %d, %d, %d )", + vm.windowed, vm.width, vm.height, vm.bpp, vm.rate, vm.vsync ); + + RString err = g_pWind->TryVideoMode( vm, bNewDeviceOut ); + if (err != "") + return err; // failed to set video mode + + if (bNewDeviceOut) + { + // NOTE: This isn't needed in an actual GLES2 context... + glewInit(); + + /* We have a new OpenGL context, so we have to tell our textures that + * their OpenGL texture number is invalid. */ + if (TEXTUREMAN) + TEXTUREMAN->InvalidateTextures(); + + /* Delete all render targets. They may have associated resources other than + * the texture itself. */ + //FOREACHM( unsigned, RenderTarget *, g_mapRenderTargets, rt ) + // delete rt->second; + //g_mapRenderTargets.clear(); + + /* Recreate all vertex buffers. */ + //InvalidateObjects(); + + //InitShaders(); + } + + ResolutionChanged(); + + return RString(); +} + +int RageDisplay_GLES2::GetMaxTextureSize() const +{ + return Caps::iMaxTextureSize; +} + +bool RageDisplay_GLES2::BeginFrame() +{ + /* We do this in here, rather than ResolutionChanged, or we won't update the + * viewport for the concurrent rendering context. */ + int fWidth = g_pWind->GetActualVideoModeParams().width; + int fHeight = g_pWind->GetActualVideoModeParams().height; + + glViewport( 0, 0, fWidth, fHeight ); + + glClearColor( 0.0f, 0.0f, 0.0f, 1.0f ); + SetZWrite( true ); + glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT ); + + return RageDisplay::BeginFrame(); +} + +void RageDisplay_GLES2::EndFrame() +{ + glFlush(); + + // XXX: This is broken on NVidia, as their xrandr sucks. + FrameLimitBeforeVsync( g_pWind->GetActualVideoModeParams().rate ); + g_pWind->SwapBuffers(); + FrameLimitAfterVsync(); + + g_pWind->Update(); + + RageDisplay::EndFrame(); +} + +RageDisplay_GLES2::~RageDisplay_GLES2() +{ + delete g_pWind; +} + +void +RageDisplay_GLES2::GetDisplayResolutions( DisplayResolutions &out ) const +{ + out.clear(); + g_pWind->GetDisplayResolutions( out ); +} + +RageSurface* +RageDisplay_GLES2::CreateScreenshot() +{ + const RagePixelFormatDesc &desc = PIXEL_FORMAT_DESC[RagePixelFormat_RGB8]; + RageSurface *image = CreateSurface( + 640, 480, desc.bpp, + desc.masks[0], desc.masks[1], desc.masks[2], desc.masks[3] ); + + memset( image->pixels, 0, 480*image->pitch ); + + return image; +} + +const RageDisplay::RagePixelFormatDesc* +RageDisplay_GLES2::GetPixelFormatDesc(RagePixelFormat pf) const +{ + ASSERT( pf >= 0 && pf < NUM_RagePixelFormat ); + return &PIXEL_FORMAT_DESC[pf]; +} + +RageMatrix +RageDisplay_GLES2::GetOrthoMatrix( float l, float r, float b, float t, float zn, float zf ) +{ + RageMatrix m( + 2/(r-l), 0, 0, 0, + 0, 2/(t-b), 0, 0, + 0, 0, -2/(zf-zn), 0, + -(r+l)/(r-l), -(t+b)/(t-b), -(zf+zn)/(zf-zn), 1 ); + return m; +} + +class RageCompiledGeometryGLES2 : public RageCompiledGeometry +{ +public: + + void Allocate( const vector &vMeshes ) + { + // TODO + } + void Change( const vector &vMeshes ) + { + // TODO + } + void Draw( int iMeshIndex ) const + { + // TOO + } +}; + +RageCompiledGeometry* +RageDisplay_GLES2::CreateCompiledGeometry() +{ + return new RageCompiledGeometryGLES2; +} + +void +RageDisplay_GLES2::DeleteCompiledGeometry( RageCompiledGeometry *p ) +{ + delete p; +} + +RString +RageDisplay_GLES2::GetApiDescription() const +{ + return "OpenGL ES 2.0"; +} + +VideoModeParams +RageDisplay_GLES2::GetActualVideoModeParams() const +{ + return g_pWind->GetActualVideoModeParams(); +} + +void +RageDisplay_GLES2::SetBlendMode( BlendMode mode ) +{ + // TODO +} + +bool +RageDisplay_GLES2::SupportsTextureFormat( RagePixelFormat pixfmt, bool realtime ) +{ + /* If we support a pixfmt for texture formats but not for surface formats, then + * we'll have to convert the texture to a supported surface format before uploading. + * This is too slow for dynamic textures. */ + if (realtime && !SupportsSurfaceFormat(pixfmt)) + return false; + + switch (g_GLPixFmtInfo[pixfmt].format) + { + case GL_COLOR_INDEX: + return false; + case GL_BGR: + case GL_BGRA: + //return !!GLEW_EXT_bgra; + return false; // no BGRA on ES2 (without exts) + default: + return true; + } + + return true; +} + +bool +RageDisplay_GLES2::SupportsPerVertexMatrixScale() +{ + return true; +} + +unsigned +RageDisplay_GLES2::CreateTexture( + RagePixelFormat pixfmt, + RageSurface* img, + bool bGenerateMipMaps + ) +{ + // TODO + return 1; +} + +void +RageDisplay_GLES2::UpdateTexture( + unsigned iTexHandle, + RageSurface* img, + int xoffset, int yoffset, int width, int height + ) +{ + // TODO +} + +void +RageDisplay_GLES2::DeleteTexture( unsigned iTexHandle ) +{ + // TODO +} + +void +RageDisplay_GLES2::ClearAllTextures() +{ + FOREACH_ENUM( TextureUnit, i ) + SetTexture( i, 0 ); + + // HACK: Reset the active texture to 0. + // TODO: Change all texture functions to take a stage number. + glActiveTexture(GL_TEXTURE0); +} + +int +RageDisplay_GLES2::GetNumTextureUnits() +{ + return Caps::iMaxTextureUnits; +} + +static bool +SetTextureUnit( TextureUnit tu ) +{ + if ((int) tu > Caps::iMaxTextureUnits) + return false; + glActiveTexture( enum_add2(GL_TEXTURE0, tu) ); + return true; +} + +void +RageDisplay_GLES2::SetTexture( TextureUnit tu, unsigned iTexture ) +{ + if (!SetTextureUnit( tu )) + return; + + if (iTexture) + { + glEnable( GL_TEXTURE_2D ); + glBindTexture( GL_TEXTURE_2D, iTexture ); + } + else + { + glDisable( GL_TEXTURE_2D ); + } +} + +void +RageDisplay_GLES2::SetTextureMode( TextureUnit tu, TextureMode tm ) +{ + // TODO +} + +void +RageDisplay_GLES2::SetTextureWrapping( TextureUnit tu, bool b ) +{ + // TODO +} + +void +RageDisplay_GLES2::SetTextureFiltering( TextureUnit tu, bool b ) +{ + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, b ? GL_LINEAR : GL_NEAREST); + + GLint iMinFilter = 0; + if (b) + { + GLint iWidth1 = -1; + GLint iWidth2 = -1; + glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &iWidth1); + glGetTexLevelParameteriv(GL_TEXTURE_2D, 1, GL_TEXTURE_WIDTH, &iWidth2); + if (iWidth1 > 1 && iWidth2 != 0) + { + /* Mipmaps are enabled. */ + if (g_pWind->GetActualVideoModeParams().bTrilinearFiltering) + iMinFilter = GL_LINEAR_MIPMAP_LINEAR; + else + iMinFilter = GL_LINEAR_MIPMAP_NEAREST; + } + else + { + iMinFilter = GL_LINEAR; + } + } + else + { + iMinFilter = GL_NEAREST; + } + + glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, iMinFilter ); +} + +bool +RageDisplay_GLES2::IsZWriteEnabled() const +{ + return State::bZWriteEnabled; +} + +bool +RageDisplay_GLES2::IsZTestEnabled() const +{ + return State::bZTestEnabled; +} + +void +RageDisplay_GLES2::SetZWrite( bool b ) +{ + if (State::bZWriteEnabled != b) + { + State::bZWriteEnabled = b; + glDepthMask( b ); + } +} + +void +RageDisplay_GLES2::SetZBias( float f ) +{ + float fNear = SCALE( f, 0.0f, 1.0f, 0.05f, 0.0f ); + float fFar = SCALE( f, 0.0f, 1.0f, 1.0f, 0.95f ); + + glDepthRange( fNear, fFar ); +} + +void +RageDisplay_GLES2::SetZTestMode( ZTestMode mode ) +{ + glEnable( GL_DEPTH_TEST ); + switch( mode ) + { + case ZTEST_OFF: + glDisable( GL_DEPTH_TEST ); + glDepthFunc( GL_ALWAYS ); + State::bZTestEnabled = false; + break; + case ZTEST_WRITE_ON_PASS: glDepthFunc( GL_LEQUAL ); break; + case ZTEST_WRITE_ON_FAIL: glDepthFunc( GL_GREATER ); break; + default: + FAIL_M(ssprintf("Invalid ZTestMode: %i", mode)); + } + State::bZTestEnabled = true; +} + + + + + +/* + + +void RageDisplay_Legacy::SetBlendMode( BlendMode mode ) +{ + glEnable(GL_BLEND); + + if (glBlendEquation != nullptr) + { + if (mode == BLEND_INVERT_DEST) + glBlendEquation( GL_FUNC_SUBTRACT ); + else if (mode == BLEND_SUBTRACT) + glBlendEquation( GL_FUNC_REVERSE_SUBTRACT ); + else + glBlendEquation( GL_FUNC_ADD ); + } + + int iSourceRGB, iDestRGB; + int iSourceAlpha = GL_ONE, iDestAlpha = GL_ONE_MINUS_SRC_ALPHA; + switch( mode ) + { + case BLEND_NORMAL: + iSourceRGB = GL_SRC_ALPHA; iDestRGB = GL_ONE_MINUS_SRC_ALPHA; + break; + case BLEND_ADD: + iSourceRGB = GL_SRC_ALPHA; iDestRGB = GL_ONE; + break; + case BLEND_SUBTRACT: + iSourceRGB = GL_SRC_ALPHA; iDestRGB = GL_ONE_MINUS_SRC_ALPHA; + break; + case BLEND_MODULATE: + iSourceRGB = GL_ZERO; iDestRGB = GL_SRC_COLOR; + break; + case BLEND_COPY_SRC: + iSourceRGB = GL_ONE; iDestRGB = GL_ZERO; + iSourceAlpha = GL_ONE; iDestAlpha = GL_ZERO; + break; + case BLEND_ALPHA_MASK: + iSourceRGB = GL_ZERO; iDestRGB = GL_ONE; + iSourceAlpha = GL_ZERO; iDestAlpha = GL_SRC_ALPHA; + break; + case BLEND_ALPHA_KNOCK_OUT: + iSourceRGB = GL_ZERO; iDestRGB = GL_ONE; + iSourceAlpha = GL_ZERO; iDestAlpha = GL_ONE_MINUS_SRC_ALPHA; + break; + case BLEND_ALPHA_MULTIPLY: + iSourceRGB = GL_SRC_ALPHA; iDestRGB = GL_ZERO; + break; + case BLEND_WEIGHTED_MULTIPLY: + // output = 2*(dst*src). 0.5,0.5,0.5 is identity; darker colors darken the image, + // and brighter colors lighten the image. + iSourceRGB = GL_DST_COLOR; iDestRGB = GL_SRC_COLOR; + break; + case BLEND_INVERT_DEST: + // out = src - dst. The source color should almost always be #FFFFFF, to make it "1 - dst". + iSourceRGB = GL_ONE; iDestRGB = GL_ONE; + break; + case BLEND_NO_EFFECT: + iSourceRGB = GL_ZERO; iDestRGB = GL_ONE; + iSourceAlpha = GL_ZERO; iDestAlpha = GL_ONE; + break; + DEFAULT_FAIL( mode ); + } + + if (GLEW_EXT_blend_equation_separate) + glBlendFuncSeparateEXT( iSourceRGB, iDestRGB, iSourceAlpha, iDestAlpha ); + else + glBlendFunc( iSourceRGB, iDestRGB ); +} + +bool RageDisplay_Legacy::IsZWriteEnabled() const +{ + bool a; + glGetBooleanv( GL_DEPTH_WRITEMASK, (unsigned char*)&a ); + return a; +} + + +*/ + +void +RageDisplay_GLES2::ClearZBuffer() +{ + bool write = IsZWriteEnabled(); + SetZWrite( true ); + glClear( GL_DEPTH_BUFFER_BIT ); + SetZWrite( write ); +} + +void +RageDisplay_GLES2::SetCullMode( CullMode mode ) +{ + if (mode != CULL_NONE) + glEnable(GL_CULL_FACE); + switch( mode ) + { + case CULL_BACK: + glCullFace( GL_BACK ); + break; + case CULL_FRONT: + glCullFace( GL_FRONT ); + break; + case CULL_NONE: + glDisable( GL_CULL_FACE ); + break; + default: + FAIL_M(ssprintf("Invalid CullMode: %i", mode)); + } +} + +void +RageDisplay_GLES2::SetAlphaTest( bool b ) +{ + void (*toggle)(GLenum) = b ? glEnable : glDisable; + if (State::bAlphaTestEnabled != b) + { + State::bAlphaTestEnabled = b; + toggle(GL_ALPHA_TEST); + } +} + +void +RageDisplay_GLES2::SetMaterial( + const RageColor &emissive, + const RageColor &ambient, + const RageColor &diffuse, + const RageColor &specular, + float shininess + ) +{ + // TODO +} + +void +RageDisplay_GLES2::SetLineWidth(float fWidth) +{ + glLineWidth(fWidth); +} + +void +RageDisplay_GLES2::SetPolygonMode(PolygonMode pm) +{ + GLenum m; + switch (pm) + { + case POLYGON_FILL: m = GL_FILL; break; + case POLYGON_LINE: m = GL_LINE; break; + default: + FAIL_M(ssprintf("Invalid PolygonMode: %i", pm)); + } + glPolygonMode(GL_FRONT_AND_BACK, m); +} + +void +RageDisplay_GLES2::SetLighting( bool b ) +{ + // TODO +} + +void +RageDisplay_GLES2::SetLightOff( int index ) +{ + // TODO +} + +void +RageDisplay_GLES2::SetLightDirectional( + int index, + const RageColor &ambient, + const RageColor &diffuse, + const RageColor &specular, + const RageVector3 &dir ) +{ + // TODO +} + +void +RageDisplay_GLES2::SetSphereEnvironmentMapping( TextureUnit tu, bool b ) +{ + // TODO +} + +void +RageDisplay_GLES2::SetCelShaded( int stage ) +{ + // TODO +} + +void +RageDisplay_GLES2::DrawQuadsInternal( const RageSpriteVertex v[], int iNumVerts ) +{ + // TODO +} + +void +RageDisplay_GLES2::DrawQuadStripInternal( const RageSpriteVertex v[], int iNumVerts ) +{ + // TODO +} + +void +RageDisplay_GLES2::DrawFanInternal( const RageSpriteVertex v[], int iNumVerts ) +{ + // TODO +} + +void +RageDisplay_GLES2::DrawStripInternal( const RageSpriteVertex v[], int iNumVerts ) +{ + // TODO +} + +void +RageDisplay_GLES2::DrawTrianglesInternal( const RageSpriteVertex v[], int iNumVerts ) +{ + // TODO +} + +void +RageDisplay_GLES2::DrawCompiledGeometryInternal( const RageCompiledGeometry *p, int + iMeshIndex ) +{ + // TODO +} + +void +RageDisplay_GLES2::DrawLineStripInternal( const RageSpriteVertex v[], int iNumVerts, float LineWidth ) +{ + // TODO +} + +// Is this even used? +void +RageDisplay_GLES2::DrawSymmetricQuadStripInternal( const RageSpriteVertex v[], int iNumVerts ) +{ + // TODO +} + +bool +RageDisplay_GLES2::SupportsSurfaceFormat( RagePixelFormat pixfmt ) +{ + switch (g_GLPixFmtInfo[pixfmt].type) + { + case GL_UNSIGNED_SHORT_1_5_5_5_REV: + return false; + //return GLEW_EXT_bgra && g_bReversePackedPixelsWorks; + default: + return true; + } +} + +/* + * Copyright (c) 2012 Colby Klein + * 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. + */ diff --git a/src/RageDisplay_OGL.cpp b/src/RageDisplay_OGL.cpp index 4bb96a7595..379b9443ac 100644 --- a/src/RageDisplay_OGL.cpp +++ b/src/RageDisplay_OGL.cpp @@ -1359,7 +1359,7 @@ void RageCompiledGeometryHWOGL::Draw( int iMeshIndex ) const #define BUFFER_OFFSET(o) ((char*)(o)) - ASSERT( glDrawRangeElements != NULL ); + ASSERT( glDrawRangeElements != nullptr ); glDrawRangeElements( GL_TRIANGLES, meshInfo.iVertexStart, // minimum array index contained in indices @@ -1735,7 +1735,7 @@ void RageDisplay_Legacy::SetBlendMode( BlendMode mode ) { glEnable(GL_BLEND); - if (glBlendEquation != NULL) + if (glBlendEquation != nullptr) { if (mode == BLEND_INVERT_DEST) glBlendEquation( GL_FUNC_SUBTRACT ); @@ -2487,7 +2487,7 @@ void RageDisplay_Legacy::SetRenderTarget( unsigned iTexture, bool bPreserveTextu } /* If we already had a render target, disable it. */ - if (g_pCurrentRenderTarget != NULL) + if (g_pCurrentRenderTarget != nullptr) SetRenderTarget(0, true); /* Enable the new render target. */ diff --git a/src/RageException.cpp b/src/RageException.cpp index 3bd77a4d0f..25ef7f4cdb 100644 --- a/src/RageException.cpp +++ b/src/RageException.cpp @@ -55,7 +55,7 @@ void RageException::Throw( const char *sFmt, ... ) ASSERT_M( g_HandlerThreadID == RageThread::GetInvalidThreadID() || g_HandlerThreadID == RageThread::GetCurrentThreadID(), ssprintf("RageException::Throw() on another thread: %s", error.c_str()) ); - if( g_CleanupHandler != NULL ) + if( g_CleanupHandler != nullptr ) g_CleanupHandler( error ); exit(1); diff --git a/src/RageFile.cpp b/src/RageFile.cpp index 72d110634f..98271cde1c 100644 --- a/src/RageFile.cpp +++ b/src/RageFile.cpp @@ -1,468 +1,468 @@ -/* - * This provides an interface to open files in RageFileManager's namespace - * This is just a simple RageFileBasic wrapper on top of another RageFileBasic; - * when a file is open, is acts like the underlying RageFileBasic, except that - * a few extra sanity checks are made to check file modes. - */ - -#include "global.h" -#include "RageFileBasic.h" -#include "RageFile.h" -#include "RageUtil.h" -#include "RageFileDriver.h" - -RageFile::RageFile() -{ - m_File = NULL; -} - -RageFile::RageFile( const RageFile &cpy ): - RageFileBasic( cpy ) -{ - /* This will copy the file driver, including its internal file pointer. */ - m_File = cpy.m_File->Copy(); - m_Path = cpy.m_Path; - m_Mode = cpy.m_Mode; -} - -RageFile *RageFile::Copy() const -{ - return new RageFile( *this ); -} - -RString RageFile::GetPath() const -{ - if ( !IsOpen() ) - return RString(); - - RString sRet = m_File->GetDisplayPath(); - if( sRet != "" ) - return sRet; - - return GetRealPath(); -} - -bool RageFile::Open( const RString& path, int mode ) -{ - ASSERT( FILEMAN != NULL ); - Close(); - - m_Path = path; - FixSlashesInPlace(m_Path); - - m_Mode = mode; - - if( (m_Mode&READ) && (m_Mode&WRITE) ) - { - SetError( "Reading and writing are mutually exclusive" ); - return false; - } - - if( !(m_Mode&READ) && !(m_Mode&WRITE) ) - { - SetError( "Neither reading nor writing specified" ); - return false; - } - - int error; - m_File = FILEMAN->Open( path, mode, error ); - - if( m_File == NULL ) - { - SetError( strerror(error) ); - return false; - } - - return true; -} - -void RageFile::Close() -{ - if( m_File == NULL ) - return; - delete m_File; - if( m_Mode & WRITE ) - FILEMAN->CacheFile( m_File, m_Path ); - m_File = NULL; -} - -#define ASSERT_OPEN ASSERT_M( IsOpen(), ssprintf("\"%s\" is not open.", m_Path.c_str()) ); -#define ASSERT_READ ASSERT_OPEN; ASSERT_M( !!(m_Mode&READ), ssprintf("\"%s\" is not open for reading", m_Path.c_str()) ); -#define ASSERT_WRITE ASSERT_OPEN; ASSERT_M( !!(m_Mode&WRITE), ssprintf("\"%s\" is not open for writing", m_Path.c_str()) ); -int RageFile::GetLine( RString &out ) -{ - ASSERT_READ; - return m_File->GetLine( out ); -} - -int RageFile::PutLine( const RString &str ) -{ - ASSERT_WRITE; - return m_File->PutLine( str ); -} - -void RageFile::EnableCRC32( bool on ) -{ - ASSERT_OPEN; - m_File->EnableCRC32( on ); -} - -bool RageFile::GetCRC32( uint32_t *iRet ) -{ - ASSERT_OPEN; - return m_File->GetCRC32( iRet ); -} - - -bool RageFile::AtEOF() const -{ - ASSERT_READ; - return m_File->AtEOF(); -} - -void RageFile::ClearError() -{ - if( m_File != NULL ) - m_File->ClearError(); - m_sError = ""; -} - -RString RageFile::GetError() const -{ - if( m_File != NULL && m_File->GetError() != "" ) - return m_File->GetError(); - return m_sError; -} - - -void RageFile::SetError( const RString &err ) -{ - if( m_File != NULL ) - m_File->ClearError(); - m_sError = err; -} - -int RageFile::Read( void *pBuffer, size_t iBytes ) -{ - ASSERT_READ; - return m_File->Read( pBuffer, iBytes ); -} - -int RageFile::Seek( int offset ) -{ - ASSERT_READ; - return m_File->Seek( offset ); -} - -int RageFile::Tell() const -{ - ASSERT_READ; - return m_File->Tell(); -} - -int RageFile::GetFileSize() const -{ - ASSERT_READ; - return m_File->GetFileSize(); -} - -int RageFile::GetFD() -{ - ASSERT_READ; - return m_File->GetFD(); -} - -int RageFile::Read( RString &buffer, int bytes ) -{ - ASSERT_READ; - return m_File->Read( buffer, bytes ); -} - -int RageFile::Write( const void *buffer, size_t bytes ) -{ - ASSERT_WRITE; - return m_File->Write( buffer, bytes ); -} - - -int RageFile::Write( const void *buffer, size_t bytes, int nmemb ) -{ - ASSERT_WRITE; - return m_File->Write( buffer, bytes, nmemb ); -} - -int RageFile::Flush() -{ - if( !m_File ) - { - SetError( "Not open" ); - return -1; - } - - return m_File->Flush(); -} - -int RageFile::Read( void *buffer, size_t bytes, int nmemb ) -{ - ASSERT_READ; - return m_File->Read( buffer, bytes, nmemb ); -} - -int RageFile::Seek( int offset, int whence ) -{ - ASSERT_READ; - return m_File->Seek( offset, whence ); -} - -void FileReading::ReadBytes( RageFileBasic &f, void *buf, int size, RString &sError ) -{ - if( sError.size() != 0 ) - return; - - int ret = f.Read( buf, size ); - if( ret == -1 ) - sError = f.GetError(); - else if( ret < size ) - sError = "Unexpected end of file"; -} - -RString FileReading::ReadString( RageFileBasic &f, int size, RString &sError ) -{ - if( sError.size() != 0 ) - return RString(); - - RString sBuf; - int ret = f.Read( sBuf, size ); - if( ret == -1 ) - sError = f.GetError(); - else if( ret < size ) - sError = "Unexpected end of file"; - return sBuf; -} - -void FileReading::SkipBytes( RageFileBasic &f, int iBytes, RString &sError ) -{ - if( sError.size() != 0 ) - return; - - iBytes += f.Tell(); - FileReading::Seek( f, iBytes, sError ); -} - -void FileReading::Seek( RageFileBasic &f, int iOffset, RString &sError ) -{ - if( sError.size() != 0 ) - return; - - int iGot = f.Seek( iOffset ); - if( iGot == iOffset ) - return; - if( iGot == -1 ) - sError = f.GetError(); - else if( iGot < iOffset ) - sError = "Unexpected end of file"; -} - -uint8_t FileReading::read_8( RageFileBasic &f, RString &sError ) -{ - uint8_t val; - ReadBytes( f, &val, sizeof(uint8_t), sError ); - if( sError.size() == 0 ) - return val; - else - return 0; -} - -uint16_t FileReading::read_u16_le( RageFileBasic &f, RString &sError ) -{ - uint16_t val; - ReadBytes( f, &val, sizeof(uint16_t), sError ); - if( sError.size() == 0 ) - return Swap16LE( val ); - else - return 0; -} - -int16_t FileReading::read_16_le( RageFileBasic &f, RString &sError ) -{ - int16_t val; - ReadBytes( f, &val, sizeof(int16_t), sError ); - if( sError.size() == 0 ) - return Swap16LE( val ); - else - return 0; -} - -uint32_t FileReading::read_u32_le( RageFileBasic &f, RString &sError ) -{ - uint32_t val; - ReadBytes( f, &val, sizeof(uint32_t), sError ); - if( sError.size() == 0 ) - return Swap32LE( val ); - else - return 0; -} - -int32_t FileReading::read_32_le( RageFileBasic &f, RString &sError ) -{ - int32_t val; - ReadBytes( f, &val, sizeof(int32_t), sError ); - if( sError.size() == 0 ) - return Swap32LE( val ); - else - return 0; -} - -// lua start -#include "LuaBinding.h" - -/** @brief Allow Lua to have access to the RageFile. */ -class LunaRageFile: public Luna -{ -public: - static int destroy( T* p, lua_State *L ) - { - SAFE_DELETE(p); - return 1; - } - - static int Open( T* p, lua_State *L ) - { - lua_pushboolean( L, p->Open( SArg(1), IArg(2) ) ); - return 1; - } - - static int Close( T* p, lua_State *L ) - { - p->Close(); - return 1; - } - - static int Write( T* p, lua_State *L ) - { - lua_pushinteger( L, p->Write( SArg(1) ) ); - return 1; - } - - static int Read( T* p, lua_State *L ) - { - RString string; - p->Read(string); - lua_pushstring( L, string ); - return 1; - } - - static int ReadBytes( T* p, lua_State *L ) - { - RString string; - p->Read( string, IArg(1) ); - lua_pushstring( L, string ); - return 1; - } - - static int Seek( T* p, lua_State *L ) - { - lua_pushinteger( L, p->Seek( IArg(1) ) ); - return 1; - } - - static int Tell( T* p, lua_State *L ) - { - lua_pushinteger( L, p->Tell() ); - return 1; - } - - static int GetLine( T* p, lua_State *L ) - { - RString string; - p->GetLine(string); - lua_pushstring( L, string ); - return 1; - } - - static int PutLine( T* p, lua_State *L ) - { - lua_pushinteger( L, p->PutLine( SArg(1) ) ); - return 1; - } - - static int GetError( T* p, lua_State *L ) - { - RString error; - error = p->GetError(); - lua_pushstring( L, error ); - return 1; - } - - static int ClearError( T* p, lua_State *L ) - { - p->ClearError(); - return 1; - } - - static int AtEOF( T* p, lua_State *L ) - { - lua_pushboolean( L, p->AtEOF() ); - return 1; - } - - LunaRageFile() - { - ADD_METHOD( Open ); - ADD_METHOD( Close ); - ADD_METHOD( Write ); - ADD_METHOD( Read ); - ADD_METHOD( ReadBytes ); - ADD_METHOD( Seek ); - ADD_METHOD( Tell ); - ADD_METHOD( GetLine ); - ADD_METHOD( PutLine ); - ADD_METHOD( destroy ); - ADD_METHOD( GetError ); - ADD_METHOD( ClearError ); - ADD_METHOD( AtEOF ); - } -}; - -LUA_REGISTER_CLASS( RageFile ) -/** @brief Utilities for working with RageFiles. */ -namespace RageFileUtil -{ - int CreateRageFile( lua_State *L ) - { - RageFile *pFile = new RageFile; - pFile->PushSelf( L ); - return 1; - } - const luaL_Reg RageFileUtilTable[] = - { - LIST_METHOD( CreateRageFile ), - { NULL, NULL } - }; - LUA_REGISTER_NAMESPACE( RageFileUtil ); -} - -/* - * Copyright (c) 2003-2004 Glenn Maynard, 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. - */ +/* + * This provides an interface to open files in RageFileManager's namespace + * This is just a simple RageFileBasic wrapper on top of another RageFileBasic; + * when a file is open, is acts like the underlying RageFileBasic, except that + * a few extra sanity checks are made to check file modes. + */ + +#include "global.h" +#include "RageFileBasic.h" +#include "RageFile.h" +#include "RageUtil.h" +#include "RageFileDriver.h" + +RageFile::RageFile() +{ + m_File = NULL; +} + +RageFile::RageFile( const RageFile &cpy ): + RageFileBasic( cpy ) +{ + /* This will copy the file driver, including its internal file pointer. */ + m_File = cpy.m_File->Copy(); + m_Path = cpy.m_Path; + m_Mode = cpy.m_Mode; +} + +RageFile *RageFile::Copy() const +{ + return new RageFile( *this ); +} + +RString RageFile::GetPath() const +{ + if ( !IsOpen() ) + return RString(); + + RString sRet = m_File->GetDisplayPath(); + if( sRet != "" ) + return sRet; + + return GetRealPath(); +} + +bool RageFile::Open( const RString& path, int mode ) +{ + ASSERT( FILEMAN != nullptr ); + Close(); + + m_Path = path; + FixSlashesInPlace(m_Path); + + m_Mode = mode; + + if( (m_Mode&READ) && (m_Mode&WRITE) ) + { + SetError( "Reading and writing are mutually exclusive" ); + return false; + } + + if( !(m_Mode&READ) && !(m_Mode&WRITE) ) + { + SetError( "Neither reading nor writing specified" ); + return false; + } + + int error; + m_File = FILEMAN->Open( path, mode, error ); + + if( m_File == NULL ) + { + SetError( strerror(error) ); + return false; + } + + return true; +} + +void RageFile::Close() +{ + if( m_File == NULL ) + return; + delete m_File; + if( m_Mode & WRITE ) + FILEMAN->CacheFile( m_File, m_Path ); + m_File = NULL; +} + +#define ASSERT_OPEN ASSERT_M( IsOpen(), ssprintf("\"%s\" is not open.", m_Path.c_str()) ); +#define ASSERT_READ ASSERT_OPEN; ASSERT_M( !!(m_Mode&READ), ssprintf("\"%s\" is not open for reading", m_Path.c_str()) ); +#define ASSERT_WRITE ASSERT_OPEN; ASSERT_M( !!(m_Mode&WRITE), ssprintf("\"%s\" is not open for writing", m_Path.c_str()) ); +int RageFile::GetLine( RString &out ) +{ + ASSERT_READ; + return m_File->GetLine( out ); +} + +int RageFile::PutLine( const RString &str ) +{ + ASSERT_WRITE; + return m_File->PutLine( str ); +} + +void RageFile::EnableCRC32( bool on ) +{ + ASSERT_OPEN; + m_File->EnableCRC32( on ); +} + +bool RageFile::GetCRC32( uint32_t *iRet ) +{ + ASSERT_OPEN; + return m_File->GetCRC32( iRet ); +} + + +bool RageFile::AtEOF() const +{ + ASSERT_READ; + return m_File->AtEOF(); +} + +void RageFile::ClearError() +{ + if( m_File != nullptr ) + m_File->ClearError(); + m_sError = ""; +} + +RString RageFile::GetError() const +{ + if( m_File != nullptr && m_File->GetError() != "" ) + return m_File->GetError(); + return m_sError; +} + + +void RageFile::SetError( const RString &err ) +{ + if( m_File != nullptr ) + m_File->ClearError(); + m_sError = err; +} + +int RageFile::Read( void *pBuffer, size_t iBytes ) +{ + ASSERT_READ; + return m_File->Read( pBuffer, iBytes ); +} + +int RageFile::Seek( int offset ) +{ + ASSERT_READ; + return m_File->Seek( offset ); +} + +int RageFile::Tell() const +{ + ASSERT_READ; + return m_File->Tell(); +} + +int RageFile::GetFileSize() const +{ + ASSERT_READ; + return m_File->GetFileSize(); +} + +int RageFile::GetFD() +{ + ASSERT_READ; + return m_File->GetFD(); +} + +int RageFile::Read( RString &buffer, int bytes ) +{ + ASSERT_READ; + return m_File->Read( buffer, bytes ); +} + +int RageFile::Write( const void *buffer, size_t bytes ) +{ + ASSERT_WRITE; + return m_File->Write( buffer, bytes ); +} + + +int RageFile::Write( const void *buffer, size_t bytes, int nmemb ) +{ + ASSERT_WRITE; + return m_File->Write( buffer, bytes, nmemb ); +} + +int RageFile::Flush() +{ + if( !m_File ) + { + SetError( "Not open" ); + return -1; + } + + return m_File->Flush(); +} + +int RageFile::Read( void *buffer, size_t bytes, int nmemb ) +{ + ASSERT_READ; + return m_File->Read( buffer, bytes, nmemb ); +} + +int RageFile::Seek( int offset, int whence ) +{ + ASSERT_READ; + return m_File->Seek( offset, whence ); +} + +void FileReading::ReadBytes( RageFileBasic &f, void *buf, int size, RString &sError ) +{ + if( sError.size() != 0 ) + return; + + int ret = f.Read( buf, size ); + if( ret == -1 ) + sError = f.GetError(); + else if( ret < size ) + sError = "Unexpected end of file"; +} + +RString FileReading::ReadString( RageFileBasic &f, int size, RString &sError ) +{ + if( sError.size() != 0 ) + return RString(); + + RString sBuf; + int ret = f.Read( sBuf, size ); + if( ret == -1 ) + sError = f.GetError(); + else if( ret < size ) + sError = "Unexpected end of file"; + return sBuf; +} + +void FileReading::SkipBytes( RageFileBasic &f, int iBytes, RString &sError ) +{ + if( sError.size() != 0 ) + return; + + iBytes += f.Tell(); + FileReading::Seek( f, iBytes, sError ); +} + +void FileReading::Seek( RageFileBasic &f, int iOffset, RString &sError ) +{ + if( sError.size() != 0 ) + return; + + int iGot = f.Seek( iOffset ); + if( iGot == iOffset ) + return; + if( iGot == -1 ) + sError = f.GetError(); + else if( iGot < iOffset ) + sError = "Unexpected end of file"; +} + +uint8_t FileReading::read_8( RageFileBasic &f, RString &sError ) +{ + uint8_t val; + ReadBytes( f, &val, sizeof(uint8_t), sError ); + if( sError.size() == 0 ) + return val; + else + return 0; +} + +uint16_t FileReading::read_u16_le( RageFileBasic &f, RString &sError ) +{ + uint16_t val; + ReadBytes( f, &val, sizeof(uint16_t), sError ); + if( sError.size() == 0 ) + return Swap16LE( val ); + else + return 0; +} + +int16_t FileReading::read_16_le( RageFileBasic &f, RString &sError ) +{ + int16_t val; + ReadBytes( f, &val, sizeof(int16_t), sError ); + if( sError.size() == 0 ) + return Swap16LE( val ); + else + return 0; +} + +uint32_t FileReading::read_u32_le( RageFileBasic &f, RString &sError ) +{ + uint32_t val; + ReadBytes( f, &val, sizeof(uint32_t), sError ); + if( sError.size() == 0 ) + return Swap32LE( val ); + else + return 0; +} + +int32_t FileReading::read_32_le( RageFileBasic &f, RString &sError ) +{ + int32_t val; + ReadBytes( f, &val, sizeof(int32_t), sError ); + if( sError.size() == 0 ) + return Swap32LE( val ); + else + return 0; +} + +// lua start +#include "LuaBinding.h" + +/** @brief Allow Lua to have access to the RageFile. */ +class LunaRageFile: public Luna +{ +public: + static int destroy( T* p, lua_State *L ) + { + SAFE_DELETE(p); + return 1; + } + + static int Open( T* p, lua_State *L ) + { + lua_pushboolean( L, p->Open( SArg(1), IArg(2) ) ); + return 1; + } + + static int Close( T* p, lua_State *L ) + { + p->Close(); + return 1; + } + + static int Write( T* p, lua_State *L ) + { + lua_pushinteger( L, p->Write( SArg(1) ) ); + return 1; + } + + static int Read( T* p, lua_State *L ) + { + RString string; + p->Read(string); + lua_pushstring( L, string ); + return 1; + } + + static int ReadBytes( T* p, lua_State *L ) + { + RString string; + p->Read( string, IArg(1) ); + lua_pushstring( L, string ); + return 1; + } + + static int Seek( T* p, lua_State *L ) + { + lua_pushinteger( L, p->Seek( IArg(1) ) ); + return 1; + } + + static int Tell( T* p, lua_State *L ) + { + lua_pushinteger( L, p->Tell() ); + return 1; + } + + static int GetLine( T* p, lua_State *L ) + { + RString string; + p->GetLine(string); + lua_pushstring( L, string ); + return 1; + } + + static int PutLine( T* p, lua_State *L ) + { + lua_pushinteger( L, p->PutLine( SArg(1) ) ); + return 1; + } + + static int GetError( T* p, lua_State *L ) + { + RString error; + error = p->GetError(); + lua_pushstring( L, error ); + return 1; + } + + static int ClearError( T* p, lua_State *L ) + { + p->ClearError(); + return 1; + } + + static int AtEOF( T* p, lua_State *L ) + { + lua_pushboolean( L, p->AtEOF() ); + return 1; + } + + LunaRageFile() + { + ADD_METHOD( Open ); + ADD_METHOD( Close ); + ADD_METHOD( Write ); + ADD_METHOD( Read ); + ADD_METHOD( ReadBytes ); + ADD_METHOD( Seek ); + ADD_METHOD( Tell ); + ADD_METHOD( GetLine ); + ADD_METHOD( PutLine ); + ADD_METHOD( destroy ); + ADD_METHOD( GetError ); + ADD_METHOD( ClearError ); + ADD_METHOD( AtEOF ); + } +}; + +LUA_REGISTER_CLASS( RageFile ) +/** @brief Utilities for working with RageFiles. */ +namespace RageFileUtil +{ + int CreateRageFile( lua_State *L ) + { + RageFile *pFile = new RageFile; + pFile->PushSelf( L ); + return 1; + } + const luaL_Reg RageFileUtilTable[] = + { + LIST_METHOD( CreateRageFile ), + { NULL, NULL } + }; + LUA_REGISTER_NAMESPACE( RageFileUtil ); +} + +/* + * Copyright (c) 2003-2004 Glenn Maynard, 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. + */ diff --git a/src/RageFile.h b/src/RageFile.h index 95eb55c111..4d59835a8d 100644 --- a/src/RageFile.h +++ b/src/RageFile.h @@ -45,7 +45,7 @@ public: bool Open( const RString& path, int mode = READ ); void Close(); - bool IsOpen() const { return m_File != NULL; } + bool IsOpen() const { return m_File != nullptr; } bool AtEOF() const; RString GetError() const; diff --git a/src/RageFileBasic.cpp b/src/RageFileBasic.cpp index 8dce779506..21a72b900c 100644 --- a/src/RageFileBasic.cpp +++ b/src/RageFileBasic.cpp @@ -1,477 +1,477 @@ -#include "global.h" -#include "RageFileBasic.h" -#include "RageUtil.h" -#include "RageUtil_AutoPtr.h" - -REGISTER_CLASS_TRAITS( RageFileBasic, pCopy->Copy() ); - -RageFileObj::RageFileObj() -{ - m_pReadBuffer = NULL; - m_pWriteBuffer = NULL; - - ResetReadBuf(); - - m_iReadBufAvail = 0; - m_iWriteBufferPos = 0; - m_iWriteBufferUsed = 0; - m_bEOF = false; - m_iFilePos = 0; - m_bCRC32Enabled = false; - m_iCRC32 = 0; -} - -RageFileObj::RageFileObj( const RageFileObj &cpy ): - RageFileBasic(cpy) -{ - /* If the original file has a buffer, copy it. */ - if( cpy.m_pReadBuffer != NULL ) - { - m_pReadBuffer = new char[BSIZE]; - memcpy( m_pReadBuffer, cpy.m_pReadBuffer, BSIZE ); - - int iOffsetIntoBuffer = cpy.m_pReadBuf - cpy.m_pReadBuffer; - m_pReadBuf = m_pReadBuffer + iOffsetIntoBuffer; - } - else - { - m_pReadBuffer = NULL; - } - - if( cpy.m_pWriteBuffer != NULL ) - { - m_pWriteBuffer = new char[cpy.m_iWriteBufferSize]; - memcpy( m_pWriteBuffer, cpy.m_pWriteBuffer, m_iWriteBufferUsed ); - } - else - { - m_pWriteBuffer = NULL; - } - - m_iReadBufAvail = cpy.m_iReadBufAvail; - m_bEOF = cpy.m_bEOF; - m_iFilePos = cpy.m_iFilePos; - m_iWriteBufferPos = cpy.m_iWriteBufferPos; - m_iWriteBufferSize = cpy.m_iWriteBufferSize; - m_iWriteBufferUsed = cpy.m_iWriteBufferUsed; - m_bCRC32Enabled = cpy.m_bCRC32Enabled; - m_iCRC32 = cpy.m_iCRC32; -} - -RageFileObj::~RageFileObj() -{ - delete [] m_pReadBuffer; - delete [] m_pWriteBuffer; -} - -int RageFileObj::Seek( int iOffset ) -{ - /* If we're already at the requested position, short circuit and don't flush - * our buffer. */ - if( iOffset == m_iFilePos ) - return m_iFilePos; - - m_bEOF = false; - - /* If we're calculating a CRC32, disable it. */ - m_bCRC32Enabled = false; - - /* Note that seeks do not flush the write buffer. Instead, we flush lazily, on the next - * actual Write (or Flush). Seek is not allowed to fail, and users should not need to - * flush before seeking to do proper error checking. */ - ResetReadBuf(); - - int iPos = SeekInternal( iOffset ); - if( iPos != -1 ) - m_iFilePos = iPos; - return iPos; -} - -int RageFileObj::Seek( int offset, int whence ) -{ - switch( whence ) - { - case SEEK_CUR: - return Seek( Tell() + offset ); - case SEEK_END: - offset += GetFileSize(); - } - return Seek( (int) offset ); -} - -int RageFileObj::Read( void *pBuffer, size_t iBytes ) -{ - int iRet = 0; - - while( !m_bEOF && iBytes > 0 ) - { - if( m_pReadBuffer != NULL && m_iReadBufAvail ) - { - /* Copy data out of the buffer first. */ - int iFromBuffer = min( (int) iBytes, m_iReadBufAvail ); - memcpy( pBuffer, m_pReadBuf, iFromBuffer ); - if( m_bCRC32Enabled ) - CRC32( m_iCRC32, pBuffer, iFromBuffer ); - - iRet += iFromBuffer; - m_iFilePos += iFromBuffer; - iBytes -= iFromBuffer; - m_iReadBufAvail -= iFromBuffer; - m_pReadBuf += iFromBuffer; - - pBuffer = (char *) pBuffer + iFromBuffer; - } - - if( !iBytes ) - break; - - ASSERT( m_iReadBufAvail == 0 ); - - /* If buffering is disabled, or the block is bigger than the buffer, - * read the remainder of the data directly into the desteination buffer. */ - if( m_pReadBuffer == NULL || iBytes >= BSIZE ) - { - /* We have a lot more to read, so don't waste time copying it into the - * buffer. */ - int iFromFile = this->ReadInternal( pBuffer, iBytes ); - if( iFromFile == -1 ) - return -1; - - if( iFromFile == 0 ) - m_bEOF = true; - - if( m_bCRC32Enabled ) - CRC32( m_iCRC32, pBuffer, iFromFile ); - iRet += iFromFile; - m_iFilePos += iFromFile; - return iRet; - } - - /* If buffering is enabled, and we need more data, fill the buffer. */ - m_pReadBuf = m_pReadBuffer; - int iGot = FillReadBuf(); - if( iGot == -1 ) - return iGot; - if( iGot == 0 ) - m_bEOF = true; - } - - return iRet; -} - -int RageFileObj::Read( RString &sBuffer, int iBytes ) -{ - sBuffer.reserve( iBytes != -1? iBytes: this->GetFileSize() ); - - int iRet = 0; - char buf[4096]; - while( iBytes == -1 || iRet < iBytes ) - { - int ToRead = sizeof(buf); - if( iBytes != -1 ) - ToRead = min( ToRead, iBytes-iRet ); - - const int iGot = Read( buf, ToRead ); - if( iGot == 0 ) - break; - if( iGot == -1 ) - return -1; - - sBuffer.append( buf, iGot ); - iRet += iGot; - } - - sBuffer.erase( sBuffer.begin()+iRet, sBuffer.end() ); - - return iRet; -} - -int RageFileObj::Read( void *pBuffer, size_t iBytes, int iNmemb ) -{ - const int iRet = Read( pBuffer, iBytes*iNmemb ); - if( iRet == -1 ) - return -1; - - /* If we're reading 10-byte blocks, and we got 27 bytes, we have 7 extra bytes. - * Seek back. XXX: seeking is very slow for eg. deflated ZIPs. If the block is - * small enough, we may be able to stuff the extra data into the buffer. */ - const int iExtra = iRet % iBytes; - Seek( Tell()-iExtra ); - - return iRet/iBytes; -} - -/* Empty the write buffer to disk. Return -1 on error, 0 on success. */ -int RageFileObj::EmptyWriteBuf() -{ - if( m_pWriteBuffer == NULL ) - return 0; - - if( m_iWriteBufferUsed ) - { - /* The write buffer may not align with the actual file, if we've seeked. Only - * seek if needed. */ - bool bSeeked = (m_iWriteBufferPos+m_iWriteBufferUsed != m_iFilePos); - if( bSeeked ) - SeekInternal( m_iWriteBufferPos ); - - int iRet = WriteInternal( m_pWriteBuffer, m_iWriteBufferUsed ); - - if( bSeeked ) - SeekInternal( m_iFilePos ); - if( iRet == -1 ) - return iRet; - } - - m_iWriteBufferPos = m_iFilePos; - m_iWriteBufferUsed = 0; - return 0; -} - -int RageFileObj::Write( const void *pBuffer, size_t iBytes ) -{ - if( m_pWriteBuffer != NULL ) - { - /* If the file position has moved away from the write buffer, or the - * incoming data won't fit in the buffer, flush. */ - if( m_iWriteBufferPos+m_iWriteBufferUsed != m_iFilePos || m_iWriteBufferUsed + (int)iBytes > m_iWriteBufferSize ) - { - int iRet = EmptyWriteBuf(); - if( iRet == -1 ) - return iRet; - } - - if( m_iWriteBufferUsed + (int)iBytes <= m_iWriteBufferSize ) - { - memcpy( m_pWriteBuffer+m_iWriteBufferUsed, pBuffer, iBytes ); - m_iWriteBufferUsed += iBytes; - m_iFilePos += iBytes; - if( m_bCRC32Enabled ) - CRC32( m_iCRC32, pBuffer, iBytes ); - return iBytes; - } - - /* We're writing a lot of data, and it won't fit in the buffer. We already - * flushed above, so m_iWriteBufferUsed; fall through and write the block normally. */ - ASSERT_M( m_iWriteBufferUsed == 0, ssprintf("%i", m_iWriteBufferUsed) ); - } - - int iRet = WriteInternal( pBuffer, iBytes ); - if( iRet != -1 ) - { - m_iFilePos += iRet; - if( m_bCRC32Enabled ) - CRC32( m_iCRC32, pBuffer, iBytes ); - } - return iRet; -} - -int RageFileObj::Write( const void *pBuffer, size_t iBytes, int iNmemb ) -{ - /* Simple write. We never return partial writes. */ - int iRet = Write( pBuffer, iBytes*iNmemb ) / iBytes; - if( iRet == -1 ) - return -1; - return iRet / iBytes; -} - -int RageFileObj::Flush() -{ - int iRet = EmptyWriteBuf(); - if( iRet == -1 ) - return iRet; - return FlushInternal(); -} - -void RageFileObj::EnableReadBuffering() -{ - if( m_pReadBuffer == NULL ) - m_pReadBuffer = new char[BSIZE]; -} - -void RageFileObj::EnableWriteBuffering( int iBytes ) -{ - if( m_pWriteBuffer == NULL ) - { - m_pWriteBuffer = new char[iBytes]; - m_iWriteBufferPos = m_iFilePos; - m_iWriteBufferSize = iBytes; - } -} - -void RageFileObj::EnableCRC32( bool bOn ) -{ - if( !bOn ) - { - m_bCRC32Enabled = false; - return; - } - - m_bCRC32Enabled = true; - m_iCRC32 = 0; -} - -bool RageFileObj::GetCRC32( uint32_t *iRet ) -{ - if( !m_bCRC32Enabled ) - return false; - - *iRet = m_iCRC32; - return true; -} - -/* Read up to the next \n, and return it in out. Strip the \n. If the \n is - * preceded by a \r (DOS newline), strip that, too. */ -int RageFileObj::GetLine( RString &sOut ) -{ - sOut = ""; - - if( m_bEOF ) - return 0; - - EnableReadBuffering(); - - bool bGotData = false; - while( 1 ) - { - bool bDone = false; - - /* Find the end of the block we'll move to out. */ - char *p = (char *) memchr( m_pReadBuf, '\n', m_iReadBufAvail ); - bool bReAddCR = false; - if( p == NULL ) - { - /* Hack: If the last character of the buffer is \r, then it's likely that an - * \r\n has been split across buffers. Move everything else, then move the - * \r to the beginning of the buffer and handle it the next time around the loop. */ - if( m_iReadBufAvail && m_pReadBuf[m_iReadBufAvail-1] == '\r' ) - { - bReAddCR = true; - --m_iReadBufAvail; - } - - p = m_pReadBuf+m_iReadBufAvail; /* everything */ - } - else - { - bDone = true; - } - - if( p >= m_pReadBuf ) - { - char *RealEnd = p; - if( bDone && p > m_pReadBuf && p[-1] == '\r' ) - --RealEnd; /* not including \r */ - sOut.append( m_pReadBuf, RealEnd ); - - if( bDone ) - ++p; /* skip \n */ - - const int iUsed = p-m_pReadBuf; - if( iUsed ) - { - m_iReadBufAvail -= iUsed; - m_iFilePos += iUsed; - bGotData = true; - m_pReadBuf = p; - } - } - - if( bReAddCR ) - { - ASSERT( m_iReadBufAvail == 0 ); - m_pReadBuf = m_pReadBuffer; - m_pReadBuffer[m_iReadBufAvail] = '\r'; - ++m_iReadBufAvail; - } - - if( bDone ) - break; - - /* We need more data. */ - m_pReadBuf = m_pReadBuffer; - - const int iSize = FillReadBuf(); - - /* If we've read data already, then don't mark EOF yet. Wait until the - * next time we're called. */ - if( iSize == 0 && !bGotData ) - { - m_bEOF = true; - return 0; - } - if( iSize == -1 ) - return -1; // error - if( iSize == 0 ) - break; // EOF or error - } - return bGotData? 1:0; -} - -// Always use "\r\n". Even though the program may be running on Unix, the -// files written to a memory card are likely to be edited using Windows. -//#if defined(WIN32) -#define NEWLINE "\r\n" -//#else -//#define NEWLINE "\n" -//#endif - -int RageFileObj::PutLine( const RString &sStr ) -{ - if( Write(sStr) == -1 ) - return -1; - return Write( RString(NEWLINE) ); -} - -/* Fill the internal buffer. This never marks EOF, since this is an internal, hidden - * read; EOF should only be set as a result of a real read. (That is, disabling buffering - * shouldn't cause the results of AtEOF to change.) */ -int RageFileObj::FillReadBuf() -{ - /* Don't call this unless buffering is enabled. */ - ASSERT( m_pReadBuffer != NULL ); - - /* The buffer starts at m_Buffer; any data in it starts at m_pReadBuf; space between - * the two is old data that we've read. (Don't mangle that data; we can use it - * for seeking backwards.) */ - const int iBufAvail = BSIZE - (m_pReadBuf-m_pReadBuffer) - m_iReadBufAvail; - ASSERT_M( iBufAvail >= 0, ssprintf("%p, %p, %i", m_pReadBuf, m_pReadBuffer, (int) BSIZE ) ); - const int iSize = this->ReadInternal( m_pReadBuf+m_iReadBufAvail, iBufAvail ); - - if( iSize > 0 ) - m_iReadBufAvail += iSize; - - return iSize; -} - -void RageFileObj::ResetReadBuf() -{ - m_iReadBufAvail = 0; - m_pReadBuf = m_pReadBuffer; -} - -/* - * Copyright (c) 2003-2004 Glenn Maynard - * 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. - */ - +#include "global.h" +#include "RageFileBasic.h" +#include "RageUtil.h" +#include "RageUtil_AutoPtr.h" + +REGISTER_CLASS_TRAITS( RageFileBasic, pCopy->Copy() ); + +RageFileObj::RageFileObj() +{ + m_pReadBuffer = NULL; + m_pWriteBuffer = NULL; + + ResetReadBuf(); + + m_iReadBufAvail = 0; + m_iWriteBufferPos = 0; + m_iWriteBufferUsed = 0; + m_bEOF = false; + m_iFilePos = 0; + m_bCRC32Enabled = false; + m_iCRC32 = 0; +} + +RageFileObj::RageFileObj( const RageFileObj &cpy ): + RageFileBasic(cpy) +{ + /* If the original file has a buffer, copy it. */ + if( cpy.m_pReadBuffer != nullptr ) + { + m_pReadBuffer = new char[BSIZE]; + memcpy( m_pReadBuffer, cpy.m_pReadBuffer, BSIZE ); + + int iOffsetIntoBuffer = cpy.m_pReadBuf - cpy.m_pReadBuffer; + m_pReadBuf = m_pReadBuffer + iOffsetIntoBuffer; + } + else + { + m_pReadBuffer = NULL; + } + + if( cpy.m_pWriteBuffer != nullptr ) + { + m_pWriteBuffer = new char[cpy.m_iWriteBufferSize]; + memcpy( m_pWriteBuffer, cpy.m_pWriteBuffer, m_iWriteBufferUsed ); + } + else + { + m_pWriteBuffer = NULL; + } + + m_iReadBufAvail = cpy.m_iReadBufAvail; + m_bEOF = cpy.m_bEOF; + m_iFilePos = cpy.m_iFilePos; + m_iWriteBufferPos = cpy.m_iWriteBufferPos; + m_iWriteBufferSize = cpy.m_iWriteBufferSize; + m_iWriteBufferUsed = cpy.m_iWriteBufferUsed; + m_bCRC32Enabled = cpy.m_bCRC32Enabled; + m_iCRC32 = cpy.m_iCRC32; +} + +RageFileObj::~RageFileObj() +{ + delete [] m_pReadBuffer; + delete [] m_pWriteBuffer; +} + +int RageFileObj::Seek( int iOffset ) +{ + /* If we're already at the requested position, short circuit and don't flush + * our buffer. */ + if( iOffset == m_iFilePos ) + return m_iFilePos; + + m_bEOF = false; + + /* If we're calculating a CRC32, disable it. */ + m_bCRC32Enabled = false; + + /* Note that seeks do not flush the write buffer. Instead, we flush lazily, on the next + * actual Write (or Flush). Seek is not allowed to fail, and users should not need to + * flush before seeking to do proper error checking. */ + ResetReadBuf(); + + int iPos = SeekInternal( iOffset ); + if( iPos != -1 ) + m_iFilePos = iPos; + return iPos; +} + +int RageFileObj::Seek( int offset, int whence ) +{ + switch( whence ) + { + case SEEK_CUR: + return Seek( Tell() + offset ); + case SEEK_END: + offset += GetFileSize(); + } + return Seek( (int) offset ); +} + +int RageFileObj::Read( void *pBuffer, size_t iBytes ) +{ + int iRet = 0; + + while( !m_bEOF && iBytes > 0 ) + { + if( m_pReadBuffer != nullptr && m_iReadBufAvail ) + { + /* Copy data out of the buffer first. */ + int iFromBuffer = min( (int) iBytes, m_iReadBufAvail ); + memcpy( pBuffer, m_pReadBuf, iFromBuffer ); + if( m_bCRC32Enabled ) + CRC32( m_iCRC32, pBuffer, iFromBuffer ); + + iRet += iFromBuffer; + m_iFilePos += iFromBuffer; + iBytes -= iFromBuffer; + m_iReadBufAvail -= iFromBuffer; + m_pReadBuf += iFromBuffer; + + pBuffer = (char *) pBuffer + iFromBuffer; + } + + if( !iBytes ) + break; + + ASSERT( m_iReadBufAvail == 0 ); + + /* If buffering is disabled, or the block is bigger than the buffer, + * read the remainder of the data directly into the desteination buffer. */ + if( m_pReadBuffer == NULL || iBytes >= BSIZE ) + { + /* We have a lot more to read, so don't waste time copying it into the + * buffer. */ + int iFromFile = this->ReadInternal( pBuffer, iBytes ); + if( iFromFile == -1 ) + return -1; + + if( iFromFile == 0 ) + m_bEOF = true; + + if( m_bCRC32Enabled ) + CRC32( m_iCRC32, pBuffer, iFromFile ); + iRet += iFromFile; + m_iFilePos += iFromFile; + return iRet; + } + + /* If buffering is enabled, and we need more data, fill the buffer. */ + m_pReadBuf = m_pReadBuffer; + int iGot = FillReadBuf(); + if( iGot == -1 ) + return iGot; + if( iGot == 0 ) + m_bEOF = true; + } + + return iRet; +} + +int RageFileObj::Read( RString &sBuffer, int iBytes ) +{ + sBuffer.reserve( iBytes != -1? iBytes: this->GetFileSize() ); + + int iRet = 0; + char buf[4096]; + while( iBytes == -1 || iRet < iBytes ) + { + int ToRead = sizeof(buf); + if( iBytes != -1 ) + ToRead = min( ToRead, iBytes-iRet ); + + const int iGot = Read( buf, ToRead ); + if( iGot == 0 ) + break; + if( iGot == -1 ) + return -1; + + sBuffer.append( buf, iGot ); + iRet += iGot; + } + + sBuffer.erase( sBuffer.begin()+iRet, sBuffer.end() ); + + return iRet; +} + +int RageFileObj::Read( void *pBuffer, size_t iBytes, int iNmemb ) +{ + const int iRet = Read( pBuffer, iBytes*iNmemb ); + if( iRet == -1 ) + return -1; + + /* If we're reading 10-byte blocks, and we got 27 bytes, we have 7 extra bytes. + * Seek back. XXX: seeking is very slow for eg. deflated ZIPs. If the block is + * small enough, we may be able to stuff the extra data into the buffer. */ + const int iExtra = iRet % iBytes; + Seek( Tell()-iExtra ); + + return iRet/iBytes; +} + +/* Empty the write buffer to disk. Return -1 on error, 0 on success. */ +int RageFileObj::EmptyWriteBuf() +{ + if( m_pWriteBuffer == NULL ) + return 0; + + if( m_iWriteBufferUsed ) + { + /* The write buffer may not align with the actual file, if we've seeked. Only + * seek if needed. */ + bool bSeeked = (m_iWriteBufferPos+m_iWriteBufferUsed != m_iFilePos); + if( bSeeked ) + SeekInternal( m_iWriteBufferPos ); + + int iRet = WriteInternal( m_pWriteBuffer, m_iWriteBufferUsed ); + + if( bSeeked ) + SeekInternal( m_iFilePos ); + if( iRet == -1 ) + return iRet; + } + + m_iWriteBufferPos = m_iFilePos; + m_iWriteBufferUsed = 0; + return 0; +} + +int RageFileObj::Write( const void *pBuffer, size_t iBytes ) +{ + if( m_pWriteBuffer != nullptr ) + { + /* If the file position has moved away from the write buffer, or the + * incoming data won't fit in the buffer, flush. */ + if( m_iWriteBufferPos+m_iWriteBufferUsed != m_iFilePos || m_iWriteBufferUsed + (int)iBytes > m_iWriteBufferSize ) + { + int iRet = EmptyWriteBuf(); + if( iRet == -1 ) + return iRet; + } + + if( m_iWriteBufferUsed + (int)iBytes <= m_iWriteBufferSize ) + { + memcpy( m_pWriteBuffer+m_iWriteBufferUsed, pBuffer, iBytes ); + m_iWriteBufferUsed += iBytes; + m_iFilePos += iBytes; + if( m_bCRC32Enabled ) + CRC32( m_iCRC32, pBuffer, iBytes ); + return iBytes; + } + + /* We're writing a lot of data, and it won't fit in the buffer. We already + * flushed above, so m_iWriteBufferUsed; fall through and write the block normally. */ + ASSERT_M( m_iWriteBufferUsed == 0, ssprintf("%i", m_iWriteBufferUsed) ); + } + + int iRet = WriteInternal( pBuffer, iBytes ); + if( iRet != -1 ) + { + m_iFilePos += iRet; + if( m_bCRC32Enabled ) + CRC32( m_iCRC32, pBuffer, iBytes ); + } + return iRet; +} + +int RageFileObj::Write( const void *pBuffer, size_t iBytes, int iNmemb ) +{ + /* Simple write. We never return partial writes. */ + int iRet = Write( pBuffer, iBytes*iNmemb ) / iBytes; + if( iRet == -1 ) + return -1; + return iRet / iBytes; +} + +int RageFileObj::Flush() +{ + int iRet = EmptyWriteBuf(); + if( iRet == -1 ) + return iRet; + return FlushInternal(); +} + +void RageFileObj::EnableReadBuffering() +{ + if( m_pReadBuffer == NULL ) + m_pReadBuffer = new char[BSIZE]; +} + +void RageFileObj::EnableWriteBuffering( int iBytes ) +{ + if( m_pWriteBuffer == NULL ) + { + m_pWriteBuffer = new char[iBytes]; + m_iWriteBufferPos = m_iFilePos; + m_iWriteBufferSize = iBytes; + } +} + +void RageFileObj::EnableCRC32( bool bOn ) +{ + if( !bOn ) + { + m_bCRC32Enabled = false; + return; + } + + m_bCRC32Enabled = true; + m_iCRC32 = 0; +} + +bool RageFileObj::GetCRC32( uint32_t *iRet ) +{ + if( !m_bCRC32Enabled ) + return false; + + *iRet = m_iCRC32; + return true; +} + +/* Read up to the next \n, and return it in out. Strip the \n. If the \n is + * preceded by a \r (DOS newline), strip that, too. */ +int RageFileObj::GetLine( RString &sOut ) +{ + sOut = ""; + + if( m_bEOF ) + return 0; + + EnableReadBuffering(); + + bool bGotData = false; + while( 1 ) + { + bool bDone = false; + + /* Find the end of the block we'll move to out. */ + char *p = (char *) memchr( m_pReadBuf, '\n', m_iReadBufAvail ); + bool bReAddCR = false; + if( p == NULL ) + { + /* Hack: If the last character of the buffer is \r, then it's likely that an + * \r\n has been split across buffers. Move everything else, then move the + * \r to the beginning of the buffer and handle it the next time around the loop. */ + if( m_iReadBufAvail && m_pReadBuf[m_iReadBufAvail-1] == '\r' ) + { + bReAddCR = true; + --m_iReadBufAvail; + } + + p = m_pReadBuf+m_iReadBufAvail; /* everything */ + } + else + { + bDone = true; + } + + if( p >= m_pReadBuf ) + { + char *RealEnd = p; + if( bDone && p > m_pReadBuf && p[-1] == '\r' ) + --RealEnd; /* not including \r */ + sOut.append( m_pReadBuf, RealEnd ); + + if( bDone ) + ++p; /* skip \n */ + + const int iUsed = p-m_pReadBuf; + if( iUsed ) + { + m_iReadBufAvail -= iUsed; + m_iFilePos += iUsed; + bGotData = true; + m_pReadBuf = p; + } + } + + if( bReAddCR ) + { + ASSERT( m_iReadBufAvail == 0 ); + m_pReadBuf = m_pReadBuffer; + m_pReadBuffer[m_iReadBufAvail] = '\r'; + ++m_iReadBufAvail; + } + + if( bDone ) + break; + + /* We need more data. */ + m_pReadBuf = m_pReadBuffer; + + const int iSize = FillReadBuf(); + + /* If we've read data already, then don't mark EOF yet. Wait until the + * next time we're called. */ + if( iSize == 0 && !bGotData ) + { + m_bEOF = true; + return 0; + } + if( iSize == -1 ) + return -1; // error + if( iSize == 0 ) + break; // EOF or error + } + return bGotData? 1:0; +} + +// Always use "\r\n". Even though the program may be running on Unix, the +// files written to a memory card are likely to be edited using Windows. +//#if defined(WIN32) +#define NEWLINE "\r\n" +//#else +//#define NEWLINE "\n" +//#endif + +int RageFileObj::PutLine( const RString &sStr ) +{ + if( Write(sStr) == -1 ) + return -1; + return Write( RString(NEWLINE) ); +} + +/* Fill the internal buffer. This never marks EOF, since this is an internal, hidden + * read; EOF should only be set as a result of a real read. (That is, disabling buffering + * shouldn't cause the results of AtEOF to change.) */ +int RageFileObj::FillReadBuf() +{ + /* Don't call this unless buffering is enabled. */ + ASSERT( m_pReadBuffer != nullptr ); + + /* The buffer starts at m_Buffer; any data in it starts at m_pReadBuf; space between + * the two is old data that we've read. (Don't mangle that data; we can use it + * for seeking backwards.) */ + const int iBufAvail = BSIZE - (m_pReadBuf-m_pReadBuffer) - m_iReadBufAvail; + ASSERT_M( iBufAvail >= 0, ssprintf("%p, %p, %i", m_pReadBuf, m_pReadBuffer, (int) BSIZE ) ); + const int iSize = this->ReadInternal( m_pReadBuf+m_iReadBufAvail, iBufAvail ); + + if( iSize > 0 ) + m_iReadBufAvail += iSize; + + return iSize; +} + +void RageFileObj::ResetReadBuf() +{ + m_iReadBufAvail = 0; + m_pReadBuf = m_pReadBuffer; +} + +/* + * Copyright (c) 2003-2004 Glenn Maynard + * 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. + */ + diff --git a/src/RageFileDriverDeflate.cpp b/src/RageFileDriverDeflate.cpp index 45f834e201..d8a7c1550a 100644 --- a/src/RageFileDriverDeflate.cpp +++ b/src/RageFileDriverDeflate.cpp @@ -1,568 +1,568 @@ -#include "global.h" -#include "RageFileDriverDeflate.h" -#include "RageFileDriverSlice.h" -#include "RageFile.h" -#include "RageLog.h" -#include "RageUtil.h" -#include - -#if defined(_WINDOWS) -#include "zlib.h" -#if defined(_MSC_VER) -#pragma comment(lib, "zdll.lib") -#endif -#elif defined(MACOSX) -#include "zlib.h" -#else -#include -#endif - -RageFileObjInflate::RageFileObjInflate( RageFileBasic *pFile, int iUncompressedSize ) -{ - m_bFileOwned = false; - m_pFile = pFile; - decomp_buf_avail = 0; - m_pInflate = new z_stream; - memset( m_pInflate, 0, sizeof(z_stream) ); - - m_iUncompressedSize = iUncompressedSize; - - int err = inflateInit2( m_pInflate, -MAX_WBITS ); - if( err == Z_MEM_ERROR ) - RageException::Throw( "inflateInit2( %i ): out of memory.", -MAX_WBITS ); - if( err != Z_OK ) - WARN( ssprintf("Huh? inflateInit2() err = %i", err) ); - - decomp_buf_ptr = decomp_buf; - m_iFilePos = 0; -} - -RageFileObjInflate::RageFileObjInflate( const RageFileObjInflate &cpy ): - RageFileObj( cpy ) -{ - /* XXX completely untested */ - /* Copy the entire decode state. */ - m_pFile = cpy.m_pFile->Copy(); - m_bFileOwned = true; - m_pInflate = new z_stream; - m_iUncompressedSize = cpy.m_iUncompressedSize; - m_iFilePos = cpy.m_iFilePos; - inflateCopy( m_pInflate, const_cast(cpy.m_pInflate) ); - - decomp_buf_ptr = decomp_buf + (cpy.decomp_buf_ptr - cpy.decomp_buf); - decomp_buf_avail = cpy.decomp_buf_avail; - memcpy( decomp_buf, cpy.decomp_buf, decomp_buf_avail ); -} - -RageFileObjInflate *RageFileObjInflate::Copy() const -{ - return new RageFileObjInflate( *this ); -} - - -RageFileObjInflate::~RageFileObjInflate() -{ - if( m_bFileOwned ) - delete m_pFile; - - int err = inflateEnd( m_pInflate ); - if( err != Z_OK ) - WARN( ssprintf("Huh? inflateEnd() err = %i", err) ); - - delete m_pInflate; -} - -int RageFileObjInflate::ReadInternal( void *buf, size_t bytes ) -{ - /* Don't read more than m_iUncompressedSize of data. If we don't do this, it's - * possible for a .gz to contain a header claiming 500k of data, but to actually - * contain much more deflated data. */ - ASSERT_M( m_iFilePos <= m_iUncompressedSize, ssprintf("%i, %i",m_iFilePos, m_iUncompressedSize) ); - bytes = min( bytes, size_t(m_iUncompressedSize-m_iFilePos) ); - - bool done=false; - int ret = 0; - while( bytes && !done ) - { - if ( !decomp_buf_avail ) - { - decomp_buf_ptr = decomp_buf; - decomp_buf_avail = 0; - int got = m_pFile->Read( decomp_buf, sizeof(decomp_buf) ); - if( got == -1 ) - { - SetError( m_pFile->GetError() ); - return -1; - } - - decomp_buf_avail = got; - } - - m_pInflate->next_in = (Bytef *) decomp_buf_ptr; - m_pInflate->avail_in = decomp_buf_avail; - m_pInflate->next_out = (Bytef *) buf; - m_pInflate->avail_out = bytes; - - - int err = inflate( m_pInflate, Z_SYNC_FLUSH ); - switch( err ) - { - case Z_DATA_ERROR: - SetError( "Data error" ); - return -1; - case Z_MEM_ERROR: - SetError( "out of memory" ); - return -1; - case Z_BUF_ERROR: - SetError( "file truncated" ); - return -1; - case Z_STREAM_END: - done = true; - break; - case Z_OK: - break; - default: - WARN( ssprintf("Huh? inflate err %i", err) ); - } - - const int used = (char *)m_pInflate->next_in - decomp_buf_ptr; - decomp_buf_ptr += used; - decomp_buf_avail -= used; - - const int got = (char *)m_pInflate->next_out - (char *)buf; - m_iFilePos += got; - ret += got; - buf = (char *)buf + got; - bytes -= got; - } - - return ret; -} - -int RageFileObjInflate::SeekInternal( int iPos ) -{ - /* Optimization: if offset is the end of the file, it's a lseek(0,SEEK_END). Don't - * decode anything. */ - if( iPos >= m_iUncompressedSize ) - { - m_iFilePos = m_iUncompressedSize; - m_pFile->Seek( m_pFile->GetFileSize() ); - decomp_buf_ptr = decomp_buf; - decomp_buf_avail = 0; - inflateReset( m_pInflate ); - return m_iUncompressedSize; - } - - if( iPos < m_iFilePos ) - { - inflateReset( m_pInflate ); - decomp_buf_ptr = decomp_buf; - decomp_buf_avail = 0; - - m_pFile->Seek( 0 ); - m_iFilePos = 0; - } - - int iOffset = iPos - m_iFilePos; - - /* Can this be optimized? */ - char buf[1024*4]; - while( iOffset ) - { - int got = ReadInternal( buf, min( (int) sizeof(buf), iOffset ) ); - if( got == -1 ) - return -1; - - if( got == 0 ) - break; - iOffset -= got; - } - - return m_iFilePos; -} - -RageFileObjDeflate::RageFileObjDeflate( RageFileBasic *pFile ) -{ - m_pFile = pFile; - m_bFileOwned = false; - - m_pDeflate = new z_stream; - memset( m_pDeflate, 0, sizeof(z_stream) ); - - int err = deflateInit2( m_pDeflate, - 3, - Z_DEFLATED, - -15, // windowBits - 8, // memLevel - Z_DEFAULT_STRATEGY ); - - if( err == Z_MEM_ERROR ) - RageException::Throw( "inflateInit2( %i ): out of memory.", -MAX_WBITS ); - if( err != Z_OK ) - WARN( ssprintf("Huh? inflateInit2() err = %i", err) ); - -} - -RageFileObjDeflate::~RageFileObjDeflate() -{ - FlushInternal(); - - if( m_bFileOwned ) - delete m_pFile; - - int err = deflateEnd( m_pDeflate ); - if( err != Z_OK ) - WARN( ssprintf("Huh? deflateEnd() err = %i", err) ); - - delete m_pDeflate; -} - -int RageFileObjDeflate::WriteInternal( const void *pBuffer, size_t iBytes ) -{ - if( iBytes == 0 ) - return 0; - - m_pDeflate->next_in = (Bytef*) pBuffer; - m_pDeflate->avail_in = iBytes; - - while( 1 ) - { - char buf[1024*4]; - m_pDeflate->next_out = (Bytef *) buf; - m_pDeflate->avail_out = sizeof(buf); - - int err = deflate( m_pDeflate, Z_NO_FLUSH ); - - if( err != Z_OK ) - FAIL_M( ssprintf("deflate: err %i", err) ); - - if( m_pDeflate->avail_out < sizeof(buf) ) - { - int lBytes = sizeof(buf)-m_pDeflate->avail_out; - int iRet = m_pFile->Write( buf, lBytes ); - if( iRet == -1 ) - { - SetError( m_pFile->GetError() ); - return -1; - } - if( iRet < lBytes ) - { - SetError( "Partial write" ); - return -1; - } - } - - if( m_pDeflate->avail_in == 0 && m_pDeflate->avail_out != 0 ) - break; - } - - return iBytes; -} - -/* Note that flushing clears compression state, so (unlike most Flush() calls) - * calling this *does* change the result of the output if you continue writing - * data. */ -int RageFileObjDeflate::FlushInternal() -{ - m_pDeflate->avail_in = 0; - - while( 1 ) - { - char buf[1024*4]; - m_pDeflate->next_out = (Bytef *) buf; - m_pDeflate->avail_out = sizeof(buf); - - int err = deflate( m_pDeflate, Z_FINISH ); - if( err != Z_OK && err != Z_STREAM_END ) - FAIL_M( ssprintf("deflate: err %i", err) ); - - if( m_pDeflate->avail_out < sizeof(buf) ) - { - int iBytes = sizeof(buf)-m_pDeflate->avail_out; - int iRet = m_pFile->Write( buf, iBytes ); - if( iRet == -1 ) - { - SetError( m_pFile->GetError() ); - return -1; - } - if( iRet < iBytes ) - { - SetError( "Partial write" ); - return -1; - } - } - - if( err == Z_STREAM_END && m_pDeflate->avail_out != 0 ) - return m_pFile->Flush(); - } -} - -/* - * Parse a .gz file, check the header CRC16 if present, and return the data - * CRC32 and a decompressor. pFile will be deleted. - */ -RageFileObjInflate *GunzipFile( RageFileBasic *pFile_, RString &sError, uint32_t *iCRC32 ) -{ - auto_ptr pFile(pFile_); - - sError = ""; - - pFile->Seek(0); - pFile->EnableCRC32( true ); - - { - char magic[2]; - FileReading::ReadBytes( *pFile, magic, 2, sError ); - if( sError != "" ) - return NULL; - - if( magic[0] != '\x1f' || magic[1] != '\x8b' ) - { - sError = "Not a gzipped file"; - return NULL; - } - } - - uint8_t iCompressionMethod = FileReading::read_8( *pFile, sError ); - uint8_t iFlags = FileReading::read_8( *pFile, sError ); - FileReading::read_32_le( *pFile, sError ); /* time */ - FileReading::read_8( *pFile, sError ); /* xfl */ - FileReading::read_8( *pFile, sError ); /* os */ - if( sError != "" ) - return NULL; - -#define FTEXT 1<<0 -#define FHCRC 1<<1 -#define FEXTRA 1<<2 -#define FNAME 1<<3 -#define FCOMMENT 1<<4 -#define UNSUPPORTED_MASK ~((1<<5)-1) - if( iCompressionMethod != 8 ) - { - sError = ssprintf( "Unsupported compression: %i", iCompressionMethod ); - return NULL; - } - - /* Warning: flags other than FNAME are untested, since gzip doesn't - * actually output them. */ - if( iFlags & UNSUPPORTED_MASK ) - { - sError = ssprintf( "Unsupported flags: %x", iFlags ); - return NULL; - } - - if( iFlags & FEXTRA ) - { - int16_t iSize = FileReading::read_16_le( *pFile, sError ); - FileReading::SkipBytes( *pFile, iSize, sError ); - } - - if( iFlags & FNAME ) - while( sError == "" && FileReading::read_8( *pFile, sError ) != 0 ) - ; - if( iFlags & FCOMMENT ) - while( sError == "" && FileReading::read_8( *pFile, sError ) != 0 ) - ; - - if( iFlags & FHCRC ) - { - /* Get the CRC of the data read so far. Be sure to do this before - * reading iExpectedCRC16. */ - uint32_t iActualCRC32; - bool bOK = pFile->GetCRC32( &iActualCRC32 ); - ASSERT( bOK ); - - uint16_t iExpectedCRC16 = FileReading::read_u16_le( *pFile, sError ); - uint16_t iActualCRC16 = int16_t( iActualCRC32 & 0xFFFF ); - if( sError != "" ) - return NULL; - - if( iActualCRC16 != iExpectedCRC16 ) - { - sError = "Header CRC error"; - return NULL; - } - } - - /* We only need CRC checking on the raw data for the header, so disable - * it. */ - pFile->EnableCRC32( false ); - - if( sError != "" ) - return NULL; - - int iDataPos = pFile->Tell(); - - /* Seek to the end, and grab the uncompressed flie size and CRC. */ - int iFooterPos = pFile->GetFileSize() - 8; - - FileReading::Seek( *pFile, iFooterPos, sError ); - - uint32_t iExpectedCRC32 = FileReading::read_u32_le( *pFile, sError ); - uint32_t iUncompressedSize = FileReading::read_u32_le( *pFile, sError ); - if( iCRC32 != NULL ) - *iCRC32 = iExpectedCRC32; - - FileReading::Seek( *pFile, iDataPos, sError ); - - if( sError != "" ) - return NULL; - - RageFileDriverSlice *pSliceFile = new RageFileDriverSlice( pFile.release(), iDataPos, iFooterPos-iDataPos ); - pSliceFile->DeleteFileWhenFinished(); - RageFileObjInflate *pInflateFile = new RageFileObjInflate( pSliceFile, iUncompressedSize ); - pInflateFile->DeleteFileWhenFinished(); - - /* Enable CRC calculation only if the caller is interested. */ - if( iCRC32 != NULL ) - pInflateFile->EnableCRC32(); - - return pInflateFile; -} - -/* - * Usage: - * - * RageFile output; - * output.Open( "hello.gz", RageFile::WRITE ); - * - * RageFileObjGzip gzip( &output ); - * gzip.Start(); - * gzip.Write( "data" ); - * gzip.Finish(); - */ -RageFileObjGzip::RageFileObjGzip( RageFileBasic *pFile ): - RageFileObjDeflate( pFile ) -{ - m_iDataStartOffset = -1; -} - -/* Write the gzip header. */ -int RageFileObjGzip::Start() -{ - /* We should be at the start of the file. */ - ASSERT( this->Tell() == 0 ); - - static const char header[] = - { - '\x1f', '\x8b', // magic - 8, // method: deflate - 0, // no flags - 0, 0, 0, 0, // no time - 0, // no extra flags - '\xFF' // unknown os - }; - - if( m_pFile->Write( header, sizeof(header) ) == -1 ) - return -1; - - m_iDataStartOffset = Tell(); - - /* Enable and reset the CRC32 for the uncompressed data about to be - * written to this file. */ - this->EnableCRC32( true ); - - return 0; -} - -/* Write the gzip footer. */ -int RageFileObjGzip::Finish() -{ - /* We're about to write to the underlying file (so the footer isn't - * compressed). Flush the compressed data first. */ - if( this->Flush() == -1 ) - return -1; - - /* Read the CRC of the data that's been written. */ - uint32_t iCRC; - bool bOK = this->GetCRC32( &iCRC ); - ASSERT( bOK ); - - /* Figure out the size of the data. */ - uint32_t iSize = Tell() - m_iDataStartOffset; - - /* Write the CRC and size directly to the file, so they don't get compressed. */ - iCRC = Swap32LE( iCRC ); - if( m_pFile->Write( &iCRC, sizeof(iCRC) ) == -1 ) - { - SetError( m_pFile->GetError() ); - return -1; - } - - /* Write the size. */ - iSize = Swap32LE( iSize ); - if( m_pFile->Write( &iSize, sizeof(iSize) ) == -1 ) - { - SetError( m_pFile->GetError() ); - return -1; - } - - /* Flush the CRC and wize that we just wrote directly to the file. */ - return m_pFile->Flush(); -} - -#include "RageFileDriverMemory.h" - -void GzipString( const RString &sIn, RString &sOut ) -{ - /* Gzip it. */ - RageFileObjMem mem; - RageFileObjGzip gzip( &mem ); - gzip.Start(); - gzip.Write( sIn ); - gzip.Finish(); - - sOut = mem.GetString(); -} - -bool GunzipString( const RString &sIn, RString &sOut, RString &sError ) -{ - RageFileObjMem *mem = new RageFileObjMem; - mem->PutString( sIn ); - - uint32_t iCRC32; - RageFileBasic *pFile = GunzipFile( mem, sError, &iCRC32 ); - if( pFile == NULL ) - return false; - - pFile->Read( sOut ); - - /* Check the CRC. */ - unsigned iRet; - ASSERT( pFile->GetCRC32( &iRet ) ); - SAFE_DELETE( pFile ); - - if( iRet != iCRC32 ) - { - sError = "CRC error"; - return false; - } - - return true; -} - -/* - * Copyright (c) 2003-2004 Glenn Maynard - * 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. - */ - +#include "global.h" +#include "RageFileDriverDeflate.h" +#include "RageFileDriverSlice.h" +#include "RageFile.h" +#include "RageLog.h" +#include "RageUtil.h" +#include + +#if defined(_WINDOWS) +#include "zlib.h" +#if defined(_MSC_VER) +#pragma comment(lib, "zdll.lib") +#endif +#elif defined(MACOSX) +#include "zlib.h" +#else +#include +#endif + +RageFileObjInflate::RageFileObjInflate( RageFileBasic *pFile, int iUncompressedSize ) +{ + m_bFileOwned = false; + m_pFile = pFile; + decomp_buf_avail = 0; + m_pInflate = new z_stream; + memset( m_pInflate, 0, sizeof(z_stream) ); + + m_iUncompressedSize = iUncompressedSize; + + int err = inflateInit2( m_pInflate, -MAX_WBITS ); + if( err == Z_MEM_ERROR ) + RageException::Throw( "inflateInit2( %i ): out of memory.", -MAX_WBITS ); + if( err != Z_OK ) + WARN( ssprintf("Huh? inflateInit2() err = %i", err) ); + + decomp_buf_ptr = decomp_buf; + m_iFilePos = 0; +} + +RageFileObjInflate::RageFileObjInflate( const RageFileObjInflate &cpy ): + RageFileObj( cpy ) +{ + /* XXX completely untested */ + /* Copy the entire decode state. */ + m_pFile = cpy.m_pFile->Copy(); + m_bFileOwned = true; + m_pInflate = new z_stream; + m_iUncompressedSize = cpy.m_iUncompressedSize; + m_iFilePos = cpy.m_iFilePos; + inflateCopy( m_pInflate, const_cast(cpy.m_pInflate) ); + + decomp_buf_ptr = decomp_buf + (cpy.decomp_buf_ptr - cpy.decomp_buf); + decomp_buf_avail = cpy.decomp_buf_avail; + memcpy( decomp_buf, cpy.decomp_buf, decomp_buf_avail ); +} + +RageFileObjInflate *RageFileObjInflate::Copy() const +{ + return new RageFileObjInflate( *this ); +} + + +RageFileObjInflate::~RageFileObjInflate() +{ + if( m_bFileOwned ) + delete m_pFile; + + int err = inflateEnd( m_pInflate ); + if( err != Z_OK ) + WARN( ssprintf("Huh? inflateEnd() err = %i", err) ); + + delete m_pInflate; +} + +int RageFileObjInflate::ReadInternal( void *buf, size_t bytes ) +{ + /* Don't read more than m_iUncompressedSize of data. If we don't do this, it's + * possible for a .gz to contain a header claiming 500k of data, but to actually + * contain much more deflated data. */ + ASSERT_M( m_iFilePos <= m_iUncompressedSize, ssprintf("%i, %i",m_iFilePos, m_iUncompressedSize) ); + bytes = min( bytes, size_t(m_iUncompressedSize-m_iFilePos) ); + + bool done=false; + int ret = 0; + while( bytes && !done ) + { + if ( !decomp_buf_avail ) + { + decomp_buf_ptr = decomp_buf; + decomp_buf_avail = 0; + int got = m_pFile->Read( decomp_buf, sizeof(decomp_buf) ); + if( got == -1 ) + { + SetError( m_pFile->GetError() ); + return -1; + } + + decomp_buf_avail = got; + } + + m_pInflate->next_in = (Bytef *) decomp_buf_ptr; + m_pInflate->avail_in = decomp_buf_avail; + m_pInflate->next_out = (Bytef *) buf; + m_pInflate->avail_out = bytes; + + + int err = inflate( m_pInflate, Z_SYNC_FLUSH ); + switch( err ) + { + case Z_DATA_ERROR: + SetError( "Data error" ); + return -1; + case Z_MEM_ERROR: + SetError( "out of memory" ); + return -1; + case Z_BUF_ERROR: + SetError( "file truncated" ); + return -1; + case Z_STREAM_END: + done = true; + break; + case Z_OK: + break; + default: + WARN( ssprintf("Huh? inflate err %i", err) ); + } + + const int used = (char *)m_pInflate->next_in - decomp_buf_ptr; + decomp_buf_ptr += used; + decomp_buf_avail -= used; + + const int got = (char *)m_pInflate->next_out - (char *)buf; + m_iFilePos += got; + ret += got; + buf = (char *)buf + got; + bytes -= got; + } + + return ret; +} + +int RageFileObjInflate::SeekInternal( int iPos ) +{ + /* Optimization: if offset is the end of the file, it's a lseek(0,SEEK_END). Don't + * decode anything. */ + if( iPos >= m_iUncompressedSize ) + { + m_iFilePos = m_iUncompressedSize; + m_pFile->Seek( m_pFile->GetFileSize() ); + decomp_buf_ptr = decomp_buf; + decomp_buf_avail = 0; + inflateReset( m_pInflate ); + return m_iUncompressedSize; + } + + if( iPos < m_iFilePos ) + { + inflateReset( m_pInflate ); + decomp_buf_ptr = decomp_buf; + decomp_buf_avail = 0; + + m_pFile->Seek( 0 ); + m_iFilePos = 0; + } + + int iOffset = iPos - m_iFilePos; + + /* Can this be optimized? */ + char buf[1024*4]; + while( iOffset ) + { + int got = ReadInternal( buf, min( (int) sizeof(buf), iOffset ) ); + if( got == -1 ) + return -1; + + if( got == 0 ) + break; + iOffset -= got; + } + + return m_iFilePos; +} + +RageFileObjDeflate::RageFileObjDeflate( RageFileBasic *pFile ) +{ + m_pFile = pFile; + m_bFileOwned = false; + + m_pDeflate = new z_stream; + memset( m_pDeflate, 0, sizeof(z_stream) ); + + int err = deflateInit2( m_pDeflate, + 3, + Z_DEFLATED, + -15, // windowBits + 8, // memLevel + Z_DEFAULT_STRATEGY ); + + if( err == Z_MEM_ERROR ) + RageException::Throw( "inflateInit2( %i ): out of memory.", -MAX_WBITS ); + if( err != Z_OK ) + WARN( ssprintf("Huh? inflateInit2() err = %i", err) ); + +} + +RageFileObjDeflate::~RageFileObjDeflate() +{ + FlushInternal(); + + if( m_bFileOwned ) + delete m_pFile; + + int err = deflateEnd( m_pDeflate ); + if( err != Z_OK ) + WARN( ssprintf("Huh? deflateEnd() err = %i", err) ); + + delete m_pDeflate; +} + +int RageFileObjDeflate::WriteInternal( const void *pBuffer, size_t iBytes ) +{ + if( iBytes == 0 ) + return 0; + + m_pDeflate->next_in = (Bytef*) pBuffer; + m_pDeflate->avail_in = iBytes; + + while( 1 ) + { + char buf[1024*4]; + m_pDeflate->next_out = (Bytef *) buf; + m_pDeflate->avail_out = sizeof(buf); + + int err = deflate( m_pDeflate, Z_NO_FLUSH ); + + if( err != Z_OK ) + FAIL_M( ssprintf("deflate: err %i", err) ); + + if( m_pDeflate->avail_out < sizeof(buf) ) + { + int lBytes = sizeof(buf)-m_pDeflate->avail_out; + int iRet = m_pFile->Write( buf, lBytes ); + if( iRet == -1 ) + { + SetError( m_pFile->GetError() ); + return -1; + } + if( iRet < lBytes ) + { + SetError( "Partial write" ); + return -1; + } + } + + if( m_pDeflate->avail_in == 0 && m_pDeflate->avail_out != 0 ) + break; + } + + return iBytes; +} + +/* Note that flushing clears compression state, so (unlike most Flush() calls) + * calling this *does* change the result of the output if you continue writing + * data. */ +int RageFileObjDeflate::FlushInternal() +{ + m_pDeflate->avail_in = 0; + + while( 1 ) + { + char buf[1024*4]; + m_pDeflate->next_out = (Bytef *) buf; + m_pDeflate->avail_out = sizeof(buf); + + int err = deflate( m_pDeflate, Z_FINISH ); + if( err != Z_OK && err != Z_STREAM_END ) + FAIL_M( ssprintf("deflate: err %i", err) ); + + if( m_pDeflate->avail_out < sizeof(buf) ) + { + int iBytes = sizeof(buf)-m_pDeflate->avail_out; + int iRet = m_pFile->Write( buf, iBytes ); + if( iRet == -1 ) + { + SetError( m_pFile->GetError() ); + return -1; + } + if( iRet < iBytes ) + { + SetError( "Partial write" ); + return -1; + } + } + + if( err == Z_STREAM_END && m_pDeflate->avail_out != 0 ) + return m_pFile->Flush(); + } +} + +/* + * Parse a .gz file, check the header CRC16 if present, and return the data + * CRC32 and a decompressor. pFile will be deleted. + */ +RageFileObjInflate *GunzipFile( RageFileBasic *pFile_, RString &sError, uint32_t *iCRC32 ) +{ + auto_ptr pFile(pFile_); + + sError = ""; + + pFile->Seek(0); + pFile->EnableCRC32( true ); + + { + char magic[2]; + FileReading::ReadBytes( *pFile, magic, 2, sError ); + if( sError != "" ) + return NULL; + + if( magic[0] != '\x1f' || magic[1] != '\x8b' ) + { + sError = "Not a gzipped file"; + return NULL; + } + } + + uint8_t iCompressionMethod = FileReading::read_8( *pFile, sError ); + uint8_t iFlags = FileReading::read_8( *pFile, sError ); + FileReading::read_32_le( *pFile, sError ); /* time */ + FileReading::read_8( *pFile, sError ); /* xfl */ + FileReading::read_8( *pFile, sError ); /* os */ + if( sError != "" ) + return NULL; + +#define FTEXT 1<<0 +#define FHCRC 1<<1 +#define FEXTRA 1<<2 +#define FNAME 1<<3 +#define FCOMMENT 1<<4 +#define UNSUPPORTED_MASK ~((1<<5)-1) + if( iCompressionMethod != 8 ) + { + sError = ssprintf( "Unsupported compression: %i", iCompressionMethod ); + return NULL; + } + + /* Warning: flags other than FNAME are untested, since gzip doesn't + * actually output them. */ + if( iFlags & UNSUPPORTED_MASK ) + { + sError = ssprintf( "Unsupported flags: %x", iFlags ); + return NULL; + } + + if( iFlags & FEXTRA ) + { + int16_t iSize = FileReading::read_16_le( *pFile, sError ); + FileReading::SkipBytes( *pFile, iSize, sError ); + } + + if( iFlags & FNAME ) + while( sError == "" && FileReading::read_8( *pFile, sError ) != 0 ) + ; + if( iFlags & FCOMMENT ) + while( sError == "" && FileReading::read_8( *pFile, sError ) != 0 ) + ; + + if( iFlags & FHCRC ) + { + /* Get the CRC of the data read so far. Be sure to do this before + * reading iExpectedCRC16. */ + uint32_t iActualCRC32; + bool bOK = pFile->GetCRC32( &iActualCRC32 ); + ASSERT( bOK ); + + uint16_t iExpectedCRC16 = FileReading::read_u16_le( *pFile, sError ); + uint16_t iActualCRC16 = int16_t( iActualCRC32 & 0xFFFF ); + if( sError != "" ) + return NULL; + + if( iActualCRC16 != iExpectedCRC16 ) + { + sError = "Header CRC error"; + return NULL; + } + } + + /* We only need CRC checking on the raw data for the header, so disable + * it. */ + pFile->EnableCRC32( false ); + + if( sError != "" ) + return NULL; + + int iDataPos = pFile->Tell(); + + /* Seek to the end, and grab the uncompressed flie size and CRC. */ + int iFooterPos = pFile->GetFileSize() - 8; + + FileReading::Seek( *pFile, iFooterPos, sError ); + + uint32_t iExpectedCRC32 = FileReading::read_u32_le( *pFile, sError ); + uint32_t iUncompressedSize = FileReading::read_u32_le( *pFile, sError ); + if( iCRC32 != nullptr ) + *iCRC32 = iExpectedCRC32; + + FileReading::Seek( *pFile, iDataPos, sError ); + + if( sError != "" ) + return NULL; + + RageFileDriverSlice *pSliceFile = new RageFileDriverSlice( pFile.release(), iDataPos, iFooterPos-iDataPos ); + pSliceFile->DeleteFileWhenFinished(); + RageFileObjInflate *pInflateFile = new RageFileObjInflate( pSliceFile, iUncompressedSize ); + pInflateFile->DeleteFileWhenFinished(); + + /* Enable CRC calculation only if the caller is interested. */ + if( iCRC32 != nullptr ) + pInflateFile->EnableCRC32(); + + return pInflateFile; +} + +/* + * Usage: + * + * RageFile output; + * output.Open( "hello.gz", RageFile::WRITE ); + * + * RageFileObjGzip gzip( &output ); + * gzip.Start(); + * gzip.Write( "data" ); + * gzip.Finish(); + */ +RageFileObjGzip::RageFileObjGzip( RageFileBasic *pFile ): + RageFileObjDeflate( pFile ) +{ + m_iDataStartOffset = -1; +} + +/* Write the gzip header. */ +int RageFileObjGzip::Start() +{ + /* We should be at the start of the file. */ + ASSERT( this->Tell() == 0 ); + + static const char header[] = + { + '\x1f', '\x8b', // magic + 8, // method: deflate + 0, // no flags + 0, 0, 0, 0, // no time + 0, // no extra flags + '\xFF' // unknown os + }; + + if( m_pFile->Write( header, sizeof(header) ) == -1 ) + return -1; + + m_iDataStartOffset = Tell(); + + /* Enable and reset the CRC32 for the uncompressed data about to be + * written to this file. */ + this->EnableCRC32( true ); + + return 0; +} + +/* Write the gzip footer. */ +int RageFileObjGzip::Finish() +{ + /* We're about to write to the underlying file (so the footer isn't + * compressed). Flush the compressed data first. */ + if( this->Flush() == -1 ) + return -1; + + /* Read the CRC of the data that's been written. */ + uint32_t iCRC; + bool bOK = this->GetCRC32( &iCRC ); + ASSERT( bOK ); + + /* Figure out the size of the data. */ + uint32_t iSize = Tell() - m_iDataStartOffset; + + /* Write the CRC and size directly to the file, so they don't get compressed. */ + iCRC = Swap32LE( iCRC ); + if( m_pFile->Write( &iCRC, sizeof(iCRC) ) == -1 ) + { + SetError( m_pFile->GetError() ); + return -1; + } + + /* Write the size. */ + iSize = Swap32LE( iSize ); + if( m_pFile->Write( &iSize, sizeof(iSize) ) == -1 ) + { + SetError( m_pFile->GetError() ); + return -1; + } + + /* Flush the CRC and wize that we just wrote directly to the file. */ + return m_pFile->Flush(); +} + +#include "RageFileDriverMemory.h" + +void GzipString( const RString &sIn, RString &sOut ) +{ + /* Gzip it. */ + RageFileObjMem mem; + RageFileObjGzip gzip( &mem ); + gzip.Start(); + gzip.Write( sIn ); + gzip.Finish(); + + sOut = mem.GetString(); +} + +bool GunzipString( const RString &sIn, RString &sOut, RString &sError ) +{ + RageFileObjMem *mem = new RageFileObjMem; + mem->PutString( sIn ); + + uint32_t iCRC32; + RageFileBasic *pFile = GunzipFile( mem, sError, &iCRC32 ); + if( pFile == NULL ) + return false; + + pFile->Read( sOut ); + + /* Check the CRC. */ + unsigned iRet; + ASSERT( pFile->GetCRC32( &iRet ) ); + SAFE_DELETE( pFile ); + + if( iRet != iCRC32 ) + { + sError = "CRC error"; + return false; + } + + return true; +} + +/* + * Copyright (c) 2003-2004 Glenn Maynard + * 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. + */ + diff --git a/src/RageFileDriverTimeout.cpp b/src/RageFileDriverTimeout.cpp index b2c8003c31..fdcd2064e0 100644 --- a/src/RageFileDriverTimeout.cpp +++ b/src/RageFileDriverTimeout.cpp @@ -1,966 +1,966 @@ -/* - * This is a filesystem wrapper driver. To use it, mount it on top of another filesystem at a - * different mountpoint. For example, mount the local path "d:/" as a normal directory to - * /cdrom-native: - * - * FILEMAN->Mount( "dir", "d:/", "/cdrom-native" ); - * - * and then mount a timeout filesystem on top of that: - * - * FILEMAN->Mount( "timeout", "/cdrom-native", "/cdrom-timeout" ); - * - * and do all access to the device at /cdrom-timeout. - * - * A common problem with accessing devices, such as CDROMs or flaky pen drives, is that they - * can take a very long time to fail under error conditions. A single request may have a - * timeout of 500ms, but a single operation may do many requests. The system will usually - * retry operations, and we want to allow it to do so, but we don't want the high-level - * operation to take too long as a whole. - * - * There's no portable way to abort a running filesystem operation. In Unix, you may be - * able to use SIGALRM, which should cause a running operation to fail with EINTR, but - * I don't trust that, and it's not portable. - * - * This driver abstracts all access to another driver, and enforces high-level timeouts. - * Operations are run in a separate thread. If an operation takes too long to complete, - * the main thread will see it fail. The operation in the thread will actually continue; - * it will time out for real eventually. To prevent accumulation of failed threads, if - * an operation times out, we'll refuse all further access until all operations have - * finished and exited. (Load a separate driver for each device, so if one device fails, - * others continue to function.) - * - * All operations must run in the thread, including retrieving directory lists, Open() - * and deleting file objects. Read/write operations are copied through an intermediate - * buffer, so we don't clobber stuff if the operation times out, the call returns and the - * operation then completes. - * - * Unmounting the filesystem will wait for all timed-out operations to complete. - * - * This class is not threadsafe; do not access files on this filesystem from multiple - * threads simultaneously. - */ - -#include "global.h" -#include "RageFileDriverTimeout.h" -#include "RageFile.h" -#include "RageUtil.h" -#include "RageUtil_FileDB.h" -#include "RageUtil_WorkerThread.h" -#include "RageLog.h" -#include - -enum ThreadRequest -{ - REQ_OPEN, - REQ_CLOSE, - REQ_GET_FILE_SIZE, - REQ_GET_FD, - REQ_READ, - REQ_WRITE, - REQ_SEEK, - REQ_FLUSH, - REQ_COPY, - REQ_POPULATE_FILE_SET, - REQ_FLUSH_DIR_CACHE, - REQ_MOVE, - REQ_REMOVE, -}; - -/* This is the class that does most of the work. */ -class ThreadedFileWorker: public RageWorkerThread -{ -public: - ThreadedFileWorker( RString sPath ); - ~ThreadedFileWorker(); - - /* Threaded operations. If a file operation times out, the caller loses all access - * to the file and should fail all future operations; this is because the thread - * is still trying to finish the operation. The thread will clean up afterwards. */ - RageFileBasic *Open( const RString &sPath, int iMode, int &iErr ); - void Close( RageFileBasic *pFile ); - int GetFileSize( RageFileBasic *&pFile ); - int GetFD( RageFileBasic *&pFile ); - int Seek( RageFileBasic *&pFile, int iPos, RString &sError ); - int Read( RageFileBasic *&pFile, void *pBuf, int iSize, RString &sError ); - int Write( RageFileBasic *&pFile, const void *pBuf, int iSize, RString &sError ); - int Flush( RageFileBasic *&pFile, RString &sError ); - RageFileBasic *Copy( RageFileBasic *&pFile, RString &sError ); - - bool FlushDirCache( const RString &sPath ); - int Move( const RString &sOldPath, const RString &sNewPath ); - int Remove( const RString &sPath ); - bool PopulateFileSet( FileSet &fs, const RString &sPath ); - -protected: - void HandleRequest( int iRequest ); - void RequestTimedOut(); - -private: - - /* All requests: */ - RageFileDriver *m_pChildDriver; - - /* List of files to delete: */ - vector m_apDeletedFiles; - RageMutex m_DeletedFilesLock; - - /* REQ_OPEN, REQ_POPULATE_FILE_SET, REQ_FLUSH_DIR_CACHE, REQ_REMOVE, REQ_MOVE: */ - RString m_sRequestPath; /* in */ - - /* REQ_MOVE: */ - RString m_sRequestPath2; /* in */ - - /* REQ_OPEN, REQ_COPY: */ - RageFileBasic *m_pResultFile; /* out */ - - /* REQ_POPULATE_FILE_SET: */ - FileSet m_ResultFileSet; /* out */ - - /* REQ_OPEN: */ - int m_iRequestMode; /* in */ - - /* REQ_CLOSE, REQ_GET_FILE_SIZE, REQ_COPY: */ - RageFileBasic *m_pRequestFile; /* in */ - - /* REQ_OPEN, REQ_GET_FILE_SIZE, REQ_READ, REQ_SEEK */ - int m_iResultRequest; /* out */ - - /* REQ_READ, REQ_WRITE */ - int m_iRequestSize; /* in */ - RString m_sResultError; /* out */ - - /* REQ_SEEK */ - int m_iRequestPos; /* in */ - - /* REQ_READ */ - char *m_pResultBuffer; /* out */ - - /* REQ_WRITE */ - char *m_pRequestBuffer; /* in */ -}; - -static vector g_apWorkers; -static RageMutex g_apWorkersMutex("WorkersMutex"); - -/* Set the timeout length, and reset the timer. */ -void RageFileDriverTimeout::SetTimeout( float fSeconds ) -{ - g_apWorkersMutex.Lock(); - for( unsigned i = 0; i < g_apWorkers.size(); ++i ) - g_apWorkers[i]->SetTimeout( fSeconds ); - g_apWorkersMutex.Unlock(); -} - - -ThreadedFileWorker::ThreadedFileWorker( RString sPath ): - RageWorkerThread( sPath ), - m_DeletedFilesLock( sPath + "DeletedFilesLock" ) -{ - /* Grab a reference to the child driver. We'll operate on it directly. */ - m_pChildDriver = FILEMAN->GetFileDriver( sPath ); - if( m_pChildDriver == NULL ) - WARN( ssprintf("ThreadedFileWorker: Mountpoint \"%s\" not found", sPath.c_str()) ); - - m_pResultFile = NULL; - m_pRequestFile = NULL; - m_pResultBuffer = NULL; - m_pRequestBuffer = NULL; - - g_apWorkersMutex.Lock(); - g_apWorkers.push_back( this ); - g_apWorkersMutex.Unlock(); - - StartThread(); -} - -ThreadedFileWorker::~ThreadedFileWorker() -{ - StopThread(); - - if( m_pChildDriver != NULL ) - FILEMAN->ReleaseFileDriver( m_pChildDriver ); - - /* Unregister ourself. */ - g_apWorkersMutex.Lock(); - for( unsigned i = 0; i < g_apWorkers.size(); ++i ) - { - if( g_apWorkers[i] == this ) - { - g_apWorkers.erase( g_apWorkers.begin()+i ); - break; - } - } - g_apWorkersMutex.Unlock(); -} - -void ThreadedFileWorker::HandleRequest( int iRequest ) -{ - { - m_DeletedFilesLock.Lock(); - vector apDeletedFiles = m_apDeletedFiles; - m_apDeletedFiles.clear(); - m_DeletedFilesLock.Unlock(); - - for( unsigned i = 0; i < apDeletedFiles.size(); ++i ) - delete apDeletedFiles[i]; - } - - /* We have a request. */ - switch( iRequest ) - { - case REQ_OPEN: - ASSERT( m_pResultFile == NULL ); - ASSERT( !m_sRequestPath.empty() ); - m_iResultRequest = 0; - m_pResultFile = m_pChildDriver->Open( m_sRequestPath, m_iRequestMode, m_iResultRequest ); - break; - - case REQ_CLOSE: - ASSERT( m_pRequestFile != NULL ); - delete m_pRequestFile; - - /* Clear m_pRequestFile, so RequestTimedOut doesn't double-delete. */ - m_pRequestFile = NULL; - break; - - case REQ_GET_FILE_SIZE: - ASSERT( m_pRequestFile != NULL ); - m_iResultRequest = m_pRequestFile->GetFileSize(); - break; - - case REQ_SEEK: - ASSERT( m_pRequestFile != NULL ); - m_iResultRequest = m_pRequestFile->Seek( m_iRequestPos ); - m_sResultError = m_pRequestFile->GetError(); - break; - - case REQ_READ: - ASSERT( m_pRequestFile != NULL ); - ASSERT( m_pResultBuffer != NULL ); - m_iResultRequest = m_pRequestFile->Read( m_pResultBuffer, m_iRequestSize ); - m_sResultError = m_pRequestFile->GetError(); - break; - - case REQ_WRITE: - ASSERT( m_pRequestFile != NULL ); - ASSERT( m_pRequestBuffer != NULL ); - m_iResultRequest = m_pRequestFile->Write( m_pRequestBuffer, m_iRequestSize ); - m_sResultError = m_pRequestFile->GetError(); - break; - - case REQ_FLUSH: - ASSERT( m_pRequestFile != NULL ); - m_iResultRequest = m_pRequestFile->Flush(); - m_sResultError = m_pRequestFile->GetError(); - break; - - case REQ_COPY: - ASSERT( m_pRequestFile != NULL ); - m_pResultFile = m_pRequestFile->Copy(); - break; - - case REQ_POPULATE_FILE_SET: - ASSERT( !m_sRequestPath.empty() ); - m_ResultFileSet = FileSet(); - m_pChildDriver->FDB->GetFileSetCopy( m_sRequestPath, m_ResultFileSet ); - break; - - case REQ_FLUSH_DIR_CACHE: - m_pChildDriver->FlushDirCache( m_sRequestPath ); - break; - - case REQ_REMOVE: - ASSERT( !m_sRequestPath.empty() ); - m_iResultRequest = m_pChildDriver->Remove( m_sRequestPath )? 0:-1; - break; - - case REQ_MOVE: - ASSERT( !m_sRequestPath.empty() ); - ASSERT( !m_sRequestPath2.empty() ); - m_iResultRequest = m_pChildDriver->Move( m_sRequestPath, m_sRequestPath2 )? 0:-1; - break; - - default: - FAIL_M( ssprintf("%i", iRequest) ); - } -} - -void ThreadedFileWorker::RequestTimedOut() -{ - /* The event timed out. Clean up any residue from the last action. */ - SAFE_DELETE( m_pRequestFile ); - SAFE_DELETE( m_pResultFile ); - SAFE_DELETE_ARRAY( m_pRequestBuffer ); - SAFE_DELETE_ARRAY( m_pResultBuffer ); -} - -RageFileBasic *ThreadedFileWorker::Open( const RString &sPath, int iMode, int &iErr ) -{ - if( m_pChildDriver == NULL ) - { - iErr = ENODEV; - return NULL; - } - - /* If we're currently in a timed-out state, fail. */ - if( IsTimedOut() ) - { - iErr = EFAULT; /* Win32 has no ETIMEDOUT */ - return NULL; - } - - m_sRequestPath = sPath; - m_iRequestMode = iMode; - - if( !DoRequest(REQ_OPEN) ) - { - LOG->Trace( "Open(%s) timed out", sPath.c_str() ); - iErr = EFAULT; /* Win32 has no ETIMEDOUT */ - return NULL; - } - - iErr = m_iResultRequest; - RageFileBasic *pRet = m_pResultFile; - m_pResultFile = NULL; - - return pRet; -} - -void ThreadedFileWorker::Close( RageFileBasic *pFile ) -{ - ASSERT( m_pChildDriver != NULL ); /* how did you get a file to begin with? */ - - if( pFile == NULL ) - return; - - if( !IsTimedOut() ) - { - /* If we're not in a timed-out state, try to wait for the deletion to complete - * before continuing. */ - m_pRequestFile = pFile; - if( !DoRequest(REQ_CLOSE) ) - return; - m_pRequestFile = NULL; - } - else - { - /* Delete the file when the timeout completes. */ - m_DeletedFilesLock.Lock(); - m_apDeletedFiles.push_back( pFile ); - m_DeletedFilesLock.Unlock(); - } -} - -int ThreadedFileWorker::GetFileSize( RageFileBasic *&pFile ) -{ - ASSERT( m_pChildDriver != NULL ); /* how did you get a file to begin with? */ - - /* If we're currently in a timed-out state, fail. */ - if( IsTimedOut() ) - { - this->Close( pFile ); - pFile = NULL; - } - - if( pFile == NULL ) - return -1; - - m_pRequestFile = pFile; - - if( !DoRequest(REQ_GET_FILE_SIZE) ) - { - /* If we time out, we can no longer access pFile. */ - pFile = NULL; - return -1; - } - - m_pRequestFile = NULL; - - return m_iResultRequest; -} - -int ThreadedFileWorker::GetFD( RageFileBasic *&pFile ) -{ - ASSERT( m_pChildDriver != NULL ); /* how did you get a file to begin with? */ - - /* If we're currently in a timed-out state, fail. */ - if( IsTimedOut() ) - { - this->Close( pFile ); - pFile = NULL; - } - - if( pFile == NULL ) - return -1; - - m_pRequestFile = pFile; - - if( !DoRequest(REQ_GET_FD) ) - { - /* If we time out, we can no longer access pFile. */ - pFile = NULL; - return -1; - } - - m_pRequestFile = NULL; - - return m_iResultRequest; -} - -int ThreadedFileWorker::Seek( RageFileBasic *&pFile, int iPos, RString &sError ) -{ - ASSERT( m_pChildDriver != NULL ); /* how did you get a file to begin with? */ - - /* If we're currently in a timed-out state, fail. */ - if( IsTimedOut() ) - { - this->Close( pFile ); - pFile = NULL; - } - - if( pFile == NULL ) - { - sError = "Operation timed out"; - return -1; - } - - m_pRequestFile = pFile; - m_iRequestPos = iPos; /* in */ - - if( !DoRequest(REQ_SEEK) ) - { - /* If we time out, we can no longer access pFile. */ - sError = "Operation timed out"; - pFile = NULL; - return -1; - } - - if( m_iResultRequest == -1 ) - sError = m_sResultError; - m_pRequestFile = NULL; - - return m_iResultRequest; -} - -int ThreadedFileWorker::Read( RageFileBasic *&pFile, void *pBuf, int iSize, RString &sError ) -{ - ASSERT( m_pChildDriver != NULL ); /* how did you get a file to begin with? */ - - /* If we're currently in a timed-out state, fail. */ - if( IsTimedOut() ) - { - this->Close( pFile ); - pFile = NULL; - } - - if( pFile == NULL ) - { - sError = "Operation timed out"; - return -1; - } - - m_pRequestFile = pFile; - m_iRequestSize = iSize; - m_pResultBuffer = new char[iSize]; - - if( !DoRequest(REQ_READ) ) - { - /* If we time out, we can no longer access pFile. */ - sError = "Operation timed out"; - pFile = NULL; - return -1; - } - - int iGot = m_iResultRequest; - if( iGot == -1 ) - sError = m_sResultError; - else - memcpy( pBuf, m_pResultBuffer, iGot ); - - m_pRequestFile = NULL; - delete [] m_pResultBuffer; - m_pResultBuffer = NULL; - - return iGot; -} - -int ThreadedFileWorker::Write( RageFileBasic *&pFile, const void *pBuf, int iSize, RString &sError ) -{ - ASSERT( m_pChildDriver != NULL ); /* how did you get a file to begin with? */ - - /* If we're currently in a timed-out state, fail. */ - if( IsTimedOut() ) - { - this->Close( pFile ); - pFile = NULL; - } - - if( pFile == NULL ) - { - sError = "Operation timed out"; - return -1; - } - - m_pRequestFile = pFile; - m_iRequestSize = iSize; - m_pRequestBuffer = new char[iSize]; - memcpy( m_pRequestBuffer, pBuf, iSize ); - - if( !DoRequest(REQ_WRITE) ) - { - /* If we time out, we can no longer access pFile. */ - sError = "Operation timed out"; - pFile = NULL; - return -1; - } - - int iGot = m_iResultRequest; - if( m_iResultRequest == -1 ) - sError = m_sResultError; - - m_pRequestFile = NULL; - delete [] m_pRequestBuffer; - m_pRequestBuffer = NULL; - - return iGot; -} - -int ThreadedFileWorker::Flush( RageFileBasic *&pFile, RString &sError ) -{ - ASSERT( m_pChildDriver != NULL ); /* how did you get a file to begin with? */ - - /* If we're currently in a timed-out state, fail. */ - if( IsTimedOut() ) - { - this->Close( pFile ); - pFile = NULL; - } - - if( pFile == NULL ) - { - sError = "Operation timed out"; - return -1; - } - - m_pRequestFile = pFile; - - if( !DoRequest(REQ_FLUSH) ) - { - /* If we time out, we can no longer access pFile. */ - sError = "Operation timed out"; - pFile = NULL; - return -1; - } - - if( m_iResultRequest == -1 ) - sError = m_sResultError; - - m_pRequestFile = NULL; - - return m_iResultRequest; -} - -RageFileBasic *ThreadedFileWorker::Copy( RageFileBasic *&pFile, RString &sError ) -{ - ASSERT( m_pChildDriver != NULL ); /* how did you get a file to begin with? */ - - /* If we're currently in a timed-out state, fail. */ - if( IsTimedOut() ) - { - this->Close( pFile ); - pFile = NULL; - } - - if( pFile == NULL ) - { - sError = "Operation timed out"; - return NULL; - } - - m_pRequestFile = pFile; - if( !DoRequest(REQ_COPY) ) - { - /* If we time out, we can no longer access pFile. */ - sError = "Operation timed out"; - pFile = NULL; - return NULL; - } - - RageFileBasic *pRet = m_pResultFile; - m_pRequestFile = NULL; - m_pResultFile = NULL; - - return pRet; -} - - -bool ThreadedFileWorker::PopulateFileSet( FileSet &fs, const RString &sPath ) -{ - if( m_pChildDriver == NULL ) - return false; - - /* If we're currently in a timed-out state, fail. */ - if( IsTimedOut() ) - return false; - - /* Fill in a different FileSet; copy the results in on success. */ - m_sRequestPath = sPath; - - /* Kick off the worker thread, and wait for it to finish. */ - if( !DoRequest(REQ_POPULATE_FILE_SET) ) - { - LOG->Trace( "PopulateFileSet(%s) timed out", sPath.c_str() ); - return false; - } - - fs = m_ResultFileSet; - return true; -} - -int ThreadedFileWorker::Move( const RString &sOldPath, const RString &sNewPath ) -{ - ASSERT( m_pChildDriver != NULL ); /* how did you get a file to begin with? */ - - /* If we're currently in a timed-out state, fail. */ - if( IsTimedOut() ) - return -1; - - m_sRequestPath = sOldPath; - m_sRequestPath2 = sNewPath; - - if( !DoRequest(REQ_MOVE) ) - { - /* If we time out, we can no longer access pFile. */ - return -1; - } - - return m_iResultRequest; -} - -int ThreadedFileWorker::Remove( const RString &sPath ) -{ - ASSERT( m_pChildDriver != NULL ); /* how did you get a file to begin with? */ - - /* If we're currently in a timed-out state, fail. */ - if( IsTimedOut() ) - return -1; - - m_sRequestPath = sPath; - - if( !DoRequest(REQ_REMOVE) ) - { - /* If we time out, we can no longer access pFile. */ - return -1; - } - - return m_iResultRequest; -} - -bool ThreadedFileWorker::FlushDirCache( const RString &sPath ) -{ - /* FlushDirCache() is often called globally, on all drivers, which means it's called with - * no timeout. Temporarily enable a timeout if needed. */ - bool bTimeoutEnabled = TimeoutEnabled(); - if( !bTimeoutEnabled ) - SetTimeout(1); - - if( m_pChildDriver == NULL ) - return false; - - /* If we're currently in a timed-out state, fail. */ - if( IsTimedOut() ) - return false; - - m_sRequestPath = sPath; - - /* Kick off the worker thread, and wait for it to finish. */ - if( !DoRequest(REQ_FLUSH_DIR_CACHE) ) - { - if( !bTimeoutEnabled ) - SetTimeout(-1); - - LOG->Trace( "FlushDirCache(%s) timed out", sPath.c_str() ); - return false; - } - - if( !bTimeoutEnabled ) - SetTimeout(-1); - - return true; -} - - -class RageFileObjTimeout: public RageFileObj -{ -public: - /* pFile will be freed by passing it to pWorker. */ - RageFileObjTimeout( ThreadedFileWorker *pWorker, RageFileBasic *pFile, int iSize, int iMode ) - { - m_pWorker = pWorker; - m_pFile = pFile; - m_iFileSize = iSize; - m_iMode = iMode; - /* We have a lot of overhead per read and write operation, since we send - * commands to the worker thread. Buffer these operations. */ - if( iMode & RageFile::WRITE ) - EnableWriteBuffering(); - if( iMode & RageFile::READ ) - EnableReadBuffering(); - } - - ~RageFileObjTimeout() - { - if( m_pFile != NULL ) - { - Flush(); - m_pWorker->Close( m_pFile ); - } - } - - int GetFileSize() const - { - return m_iFileSize; - } - - int GetFD() - { - RString sError; - int iRet = m_pWorker->GetFD( m_pFile ); - - if( m_pFile == NULL ) - { - SetError( "Operation timed out" ); - return -1; - } - - if( iRet == -1 ) - SetError( sError ); - - return iRet; - } - - RageFileBasic *Copy() const - { - RString sError; - RageFileBasic *pCopy = m_pWorker->Copy( m_pFile, sError ); - - if( m_pFile == NULL ) - { -// SetError( "Operation timed out" ); - return NULL; - } - - if( pCopy == NULL ) - { -// SetError( sError ); - return NULL; - } - - return new RageFileObjTimeout( m_pWorker, pCopy, m_iFileSize, m_iMode ); - } - -protected: - int SeekInternal( int iPos ) - { - RString sError; - int iRet = m_pWorker->Seek( m_pFile, iPos, sError ); - - if( m_pFile == NULL ) - { - SetError( "Operation timed out" ); - return -1; - } - - if( iRet == -1 ) - SetError( sError ); - - return iRet; - } - - - int ReadInternal( void *pBuffer, size_t iBytes ) - { - RString sError; - int iRet = m_pWorker->Read( m_pFile, pBuffer, iBytes, sError ); - - if( m_pFile == NULL ) - { - SetError( "Operation timed out" ); - return -1; - } - - if( iRet == -1 ) - SetError( sError ); - - return iRet; - } - - int WriteInternal( const void *pBuffer, size_t iBytes ) - { - RString sError; - int iRet = m_pWorker->Write( m_pFile, pBuffer, iBytes, sError ); - - if( m_pFile == NULL ) - { - SetError( "Operation timed out" ); - return -1; - } - - if( iRet == -1 ) - SetError( sError ); - - return iRet; - } - - int FlushInternal() - { - RString sError; - int iRet = m_pWorker->Flush( m_pFile, sError ); - - if( m_pFile == NULL ) - { - SetError( "Operation timed out" ); - return -1; - } - - if( iRet == -1 ) - SetError( sError ); - - return iRet; - } - - /* Mutable because the const operation Copy() timing out results in the original - * object no longer being available. */ - mutable RageFileBasic *m_pFile; - - ThreadedFileWorker *m_pWorker; - - /* GetFileSize isn't allowed to fail, so cache the file size on load. */ - int m_iFileSize; - //cache filemode - int m_iMode; -}; - -/* This FilenameDB runs PopulateFileSet in the worker thread. */ -class TimedFilenameDB: public FilenameDB -{ -public: - TimedFilenameDB() - { - ExpireSeconds = -1; - m_pWorker = NULL; - } - - void SetWorker( ThreadedFileWorker *pWorker ) - { - ASSERT( pWorker != NULL ); - m_pWorker = pWorker; - } - - void PopulateFileSet( FileSet &fs, const RString &sPath ) - { - ASSERT( m_pWorker != NULL ); - m_pWorker->PopulateFileSet( fs, sPath ); - } - -private: - ThreadedFileWorker *m_pWorker; -}; - -RageFileDriverTimeout::RageFileDriverTimeout( const RString &sPath ): - RageFileDriver( new TimedFilenameDB() ) -{ - m_pWorker = new ThreadedFileWorker( sPath ); - - ((TimedFilenameDB *) FDB)->SetWorker( m_pWorker ); -} - -RageFileBasic *RageFileDriverTimeout::Open( const RString &sPath, int iMode, int &iErr ) -{ - RageFileBasic *pChildFile = m_pWorker->Open( sPath, iMode, iErr ); - if( pChildFile == NULL ) - return NULL; - - /* RageBasicFile::GetFileSize isn't allowed to fail, but we are; grab the file - * size now and store it. */ - int iSize = 0; - if( iMode & RageFile::READ ) - { - iSize = m_pWorker->GetFileSize( pChildFile ); - if( iSize == -1 ) - { - /* When m_pWorker->GetFileSize fails, it takes ownership of pChildFile. */ - ASSERT( pChildFile == NULL ); - iErr = EFAULT; - return NULL; - } - } - - return new RageFileObjTimeout( m_pWorker, pChildFile, iSize, iMode ); -} - -void RageFileDriverTimeout::FlushDirCache( const RString &sPath ) -{ - RageFileDriver::FlushDirCache( sPath ); - m_pWorker->FlushDirCache( sPath ); -} - -bool RageFileDriverTimeout::Move( const RString &sOldPath, const RString &sNewPath ) -{ - int iRet = m_pWorker->Move( sOldPath, sNewPath ); - if( iRet == -1 ) - { - WARN( ssprintf("RageFileDriverTimeout::Move(%s,%s) failed", sOldPath.c_str(), sNewPath.c_str()) ); - return false; - } - - return true; -} - -bool RageFileDriverTimeout::Remove( const RString &sPath ) -{ - int iRet = m_pWorker->Remove( sPath ); - if( iRet == -1 ) - { - WARN( ssprintf("RageFileDriverTimeout::Remove(%s) failed", sPath.c_str()) ); - return false; - } - - return true; -} - -RageFileDriverTimeout::~RageFileDriverTimeout() -{ - delete m_pWorker; -} - -static struct FileDriverEntry_Timeout: public FileDriverEntry -{ - FileDriverEntry_Timeout(): FileDriverEntry( "TIMEOUT" ) { } - RageFileDriver *Create( const RString &sRoot ) const { return new RageFileDriverTimeout( sRoot ); } -} const g_RegisterDriver; - -/* - * Copyright (c) 2005 Glenn Maynard - * 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. - */ +/* + * This is a filesystem wrapper driver. To use it, mount it on top of another filesystem at a + * different mountpoint. For example, mount the local path "d:/" as a normal directory to + * /cdrom-native: + * + * FILEMAN->Mount( "dir", "d:/", "/cdrom-native" ); + * + * and then mount a timeout filesystem on top of that: + * + * FILEMAN->Mount( "timeout", "/cdrom-native", "/cdrom-timeout" ); + * + * and do all access to the device at /cdrom-timeout. + * + * A common problem with accessing devices, such as CDROMs or flaky pen drives, is that they + * can take a very long time to fail under error conditions. A single request may have a + * timeout of 500ms, but a single operation may do many requests. The system will usually + * retry operations, and we want to allow it to do so, but we don't want the high-level + * operation to take too long as a whole. + * + * There's no portable way to abort a running filesystem operation. In Unix, you may be + * able to use SIGALRM, which should cause a running operation to fail with EINTR, but + * I don't trust that, and it's not portable. + * + * This driver abstracts all access to another driver, and enforces high-level timeouts. + * Operations are run in a separate thread. If an operation takes too long to complete, + * the main thread will see it fail. The operation in the thread will actually continue; + * it will time out for real eventually. To prevent accumulation of failed threads, if + * an operation times out, we'll refuse all further access until all operations have + * finished and exited. (Load a separate driver for each device, so if one device fails, + * others continue to function.) + * + * All operations must run in the thread, including retrieving directory lists, Open() + * and deleting file objects. Read/write operations are copied through an intermediate + * buffer, so we don't clobber stuff if the operation times out, the call returns and the + * operation then completes. + * + * Unmounting the filesystem will wait for all timed-out operations to complete. + * + * This class is not threadsafe; do not access files on this filesystem from multiple + * threads simultaneously. + */ + +#include "global.h" +#include "RageFileDriverTimeout.h" +#include "RageFile.h" +#include "RageUtil.h" +#include "RageUtil_FileDB.h" +#include "RageUtil_WorkerThread.h" +#include "RageLog.h" +#include + +enum ThreadRequest +{ + REQ_OPEN, + REQ_CLOSE, + REQ_GET_FILE_SIZE, + REQ_GET_FD, + REQ_READ, + REQ_WRITE, + REQ_SEEK, + REQ_FLUSH, + REQ_COPY, + REQ_POPULATE_FILE_SET, + REQ_FLUSH_DIR_CACHE, + REQ_MOVE, + REQ_REMOVE, +}; + +/* This is the class that does most of the work. */ +class ThreadedFileWorker: public RageWorkerThread +{ +public: + ThreadedFileWorker( RString sPath ); + ~ThreadedFileWorker(); + + /* Threaded operations. If a file operation times out, the caller loses all access + * to the file and should fail all future operations; this is because the thread + * is still trying to finish the operation. The thread will clean up afterwards. */ + RageFileBasic *Open( const RString &sPath, int iMode, int &iErr ); + void Close( RageFileBasic *pFile ); + int GetFileSize( RageFileBasic *&pFile ); + int GetFD( RageFileBasic *&pFile ); + int Seek( RageFileBasic *&pFile, int iPos, RString &sError ); + int Read( RageFileBasic *&pFile, void *pBuf, int iSize, RString &sError ); + int Write( RageFileBasic *&pFile, const void *pBuf, int iSize, RString &sError ); + int Flush( RageFileBasic *&pFile, RString &sError ); + RageFileBasic *Copy( RageFileBasic *&pFile, RString &sError ); + + bool FlushDirCache( const RString &sPath ); + int Move( const RString &sOldPath, const RString &sNewPath ); + int Remove( const RString &sPath ); + bool PopulateFileSet( FileSet &fs, const RString &sPath ); + +protected: + void HandleRequest( int iRequest ); + void RequestTimedOut(); + +private: + + /* All requests: */ + RageFileDriver *m_pChildDriver; + + /* List of files to delete: */ + vector m_apDeletedFiles; + RageMutex m_DeletedFilesLock; + + /* REQ_OPEN, REQ_POPULATE_FILE_SET, REQ_FLUSH_DIR_CACHE, REQ_REMOVE, REQ_MOVE: */ + RString m_sRequestPath; /* in */ + + /* REQ_MOVE: */ + RString m_sRequestPath2; /* in */ + + /* REQ_OPEN, REQ_COPY: */ + RageFileBasic *m_pResultFile; /* out */ + + /* REQ_POPULATE_FILE_SET: */ + FileSet m_ResultFileSet; /* out */ + + /* REQ_OPEN: */ + int m_iRequestMode; /* in */ + + /* REQ_CLOSE, REQ_GET_FILE_SIZE, REQ_COPY: */ + RageFileBasic *m_pRequestFile; /* in */ + + /* REQ_OPEN, REQ_GET_FILE_SIZE, REQ_READ, REQ_SEEK */ + int m_iResultRequest; /* out */ + + /* REQ_READ, REQ_WRITE */ + int m_iRequestSize; /* in */ + RString m_sResultError; /* out */ + + /* REQ_SEEK */ + int m_iRequestPos; /* in */ + + /* REQ_READ */ + char *m_pResultBuffer; /* out */ + + /* REQ_WRITE */ + char *m_pRequestBuffer; /* in */ +}; + +static vector g_apWorkers; +static RageMutex g_apWorkersMutex("WorkersMutex"); + +/* Set the timeout length, and reset the timer. */ +void RageFileDriverTimeout::SetTimeout( float fSeconds ) +{ + g_apWorkersMutex.Lock(); + for( unsigned i = 0; i < g_apWorkers.size(); ++i ) + g_apWorkers[i]->SetTimeout( fSeconds ); + g_apWorkersMutex.Unlock(); +} + + +ThreadedFileWorker::ThreadedFileWorker( RString sPath ): + RageWorkerThread( sPath ), + m_DeletedFilesLock( sPath + "DeletedFilesLock" ) +{ + /* Grab a reference to the child driver. We'll operate on it directly. */ + m_pChildDriver = FILEMAN->GetFileDriver( sPath ); + if( m_pChildDriver == NULL ) + WARN( ssprintf("ThreadedFileWorker: Mountpoint \"%s\" not found", sPath.c_str()) ); + + m_pResultFile = NULL; + m_pRequestFile = NULL; + m_pResultBuffer = NULL; + m_pRequestBuffer = NULL; + + g_apWorkersMutex.Lock(); + g_apWorkers.push_back( this ); + g_apWorkersMutex.Unlock(); + + StartThread(); +} + +ThreadedFileWorker::~ThreadedFileWorker() +{ + StopThread(); + + if( m_pChildDriver != nullptr ) + FILEMAN->ReleaseFileDriver( m_pChildDriver ); + + /* Unregister ourself. */ + g_apWorkersMutex.Lock(); + for( unsigned i = 0; i < g_apWorkers.size(); ++i ) + { + if( g_apWorkers[i] == this ) + { + g_apWorkers.erase( g_apWorkers.begin()+i ); + break; + } + } + g_apWorkersMutex.Unlock(); +} + +void ThreadedFileWorker::HandleRequest( int iRequest ) +{ + { + m_DeletedFilesLock.Lock(); + vector apDeletedFiles = m_apDeletedFiles; + m_apDeletedFiles.clear(); + m_DeletedFilesLock.Unlock(); + + for( unsigned i = 0; i < apDeletedFiles.size(); ++i ) + delete apDeletedFiles[i]; + } + + /* We have a request. */ + switch( iRequest ) + { + case REQ_OPEN: + ASSERT( m_pResultFile == NULL ); + ASSERT( !m_sRequestPath.empty() ); + m_iResultRequest = 0; + m_pResultFile = m_pChildDriver->Open( m_sRequestPath, m_iRequestMode, m_iResultRequest ); + break; + + case REQ_CLOSE: + ASSERT( m_pRequestFile != nullptr ); + delete m_pRequestFile; + + /* Clear m_pRequestFile, so RequestTimedOut doesn't double-delete. */ + m_pRequestFile = NULL; + break; + + case REQ_GET_FILE_SIZE: + ASSERT( m_pRequestFile != nullptr ); + m_iResultRequest = m_pRequestFile->GetFileSize(); + break; + + case REQ_SEEK: + ASSERT( m_pRequestFile != nullptr ); + m_iResultRequest = m_pRequestFile->Seek( m_iRequestPos ); + m_sResultError = m_pRequestFile->GetError(); + break; + + case REQ_READ: + ASSERT( m_pRequestFile != nullptr ); + ASSERT( m_pResultBuffer != nullptr ); + m_iResultRequest = m_pRequestFile->Read( m_pResultBuffer, m_iRequestSize ); + m_sResultError = m_pRequestFile->GetError(); + break; + + case REQ_WRITE: + ASSERT( m_pRequestFile != nullptr ); + ASSERT( m_pRequestBuffer != nullptr ); + m_iResultRequest = m_pRequestFile->Write( m_pRequestBuffer, m_iRequestSize ); + m_sResultError = m_pRequestFile->GetError(); + break; + + case REQ_FLUSH: + ASSERT( m_pRequestFile != nullptr ); + m_iResultRequest = m_pRequestFile->Flush(); + m_sResultError = m_pRequestFile->GetError(); + break; + + case REQ_COPY: + ASSERT( m_pRequestFile != nullptr ); + m_pResultFile = m_pRequestFile->Copy(); + break; + + case REQ_POPULATE_FILE_SET: + ASSERT( !m_sRequestPath.empty() ); + m_ResultFileSet = FileSet(); + m_pChildDriver->FDB->GetFileSetCopy( m_sRequestPath, m_ResultFileSet ); + break; + + case REQ_FLUSH_DIR_CACHE: + m_pChildDriver->FlushDirCache( m_sRequestPath ); + break; + + case REQ_REMOVE: + ASSERT( !m_sRequestPath.empty() ); + m_iResultRequest = m_pChildDriver->Remove( m_sRequestPath )? 0:-1; + break; + + case REQ_MOVE: + ASSERT( !m_sRequestPath.empty() ); + ASSERT( !m_sRequestPath2.empty() ); + m_iResultRequest = m_pChildDriver->Move( m_sRequestPath, m_sRequestPath2 )? 0:-1; + break; + + default: + FAIL_M( ssprintf("%i", iRequest) ); + } +} + +void ThreadedFileWorker::RequestTimedOut() +{ + /* The event timed out. Clean up any residue from the last action. */ + SAFE_DELETE( m_pRequestFile ); + SAFE_DELETE( m_pResultFile ); + SAFE_DELETE_ARRAY( m_pRequestBuffer ); + SAFE_DELETE_ARRAY( m_pResultBuffer ); +} + +RageFileBasic *ThreadedFileWorker::Open( const RString &sPath, int iMode, int &iErr ) +{ + if( m_pChildDriver == NULL ) + { + iErr = ENODEV; + return NULL; + } + + /* If we're currently in a timed-out state, fail. */ + if( IsTimedOut() ) + { + iErr = EFAULT; /* Win32 has no ETIMEDOUT */ + return NULL; + } + + m_sRequestPath = sPath; + m_iRequestMode = iMode; + + if( !DoRequest(REQ_OPEN) ) + { + LOG->Trace( "Open(%s) timed out", sPath.c_str() ); + iErr = EFAULT; /* Win32 has no ETIMEDOUT */ + return NULL; + } + + iErr = m_iResultRequest; + RageFileBasic *pRet = m_pResultFile; + m_pResultFile = NULL; + + return pRet; +} + +void ThreadedFileWorker::Close( RageFileBasic *pFile ) +{ + ASSERT( m_pChildDriver != nullptr ); /* how did you get a file to begin with? */ + + if( pFile == NULL ) + return; + + if( !IsTimedOut() ) + { + /* If we're not in a timed-out state, try to wait for the deletion to complete + * before continuing. */ + m_pRequestFile = pFile; + if( !DoRequest(REQ_CLOSE) ) + return; + m_pRequestFile = NULL; + } + else + { + /* Delete the file when the timeout completes. */ + m_DeletedFilesLock.Lock(); + m_apDeletedFiles.push_back( pFile ); + m_DeletedFilesLock.Unlock(); + } +} + +int ThreadedFileWorker::GetFileSize( RageFileBasic *&pFile ) +{ + ASSERT( m_pChildDriver != nullptr ); /* how did you get a file to begin with? */ + + /* If we're currently in a timed-out state, fail. */ + if( IsTimedOut() ) + { + this->Close( pFile ); + pFile = NULL; + } + + if( pFile == NULL ) + return -1; + + m_pRequestFile = pFile; + + if( !DoRequest(REQ_GET_FILE_SIZE) ) + { + /* If we time out, we can no longer access pFile. */ + pFile = NULL; + return -1; + } + + m_pRequestFile = NULL; + + return m_iResultRequest; +} + +int ThreadedFileWorker::GetFD( RageFileBasic *&pFile ) +{ + ASSERT( m_pChildDriver != nullptr ); /* how did you get a file to begin with? */ + + /* If we're currently in a timed-out state, fail. */ + if( IsTimedOut() ) + { + this->Close( pFile ); + pFile = NULL; + } + + if( pFile == NULL ) + return -1; + + m_pRequestFile = pFile; + + if( !DoRequest(REQ_GET_FD) ) + { + /* If we time out, we can no longer access pFile. */ + pFile = NULL; + return -1; + } + + m_pRequestFile = NULL; + + return m_iResultRequest; +} + +int ThreadedFileWorker::Seek( RageFileBasic *&pFile, int iPos, RString &sError ) +{ + ASSERT( m_pChildDriver != nullptr ); /* how did you get a file to begin with? */ + + /* If we're currently in a timed-out state, fail. */ + if( IsTimedOut() ) + { + this->Close( pFile ); + pFile = NULL; + } + + if( pFile == NULL ) + { + sError = "Operation timed out"; + return -1; + } + + m_pRequestFile = pFile; + m_iRequestPos = iPos; /* in */ + + if( !DoRequest(REQ_SEEK) ) + { + /* If we time out, we can no longer access pFile. */ + sError = "Operation timed out"; + pFile = NULL; + return -1; + } + + if( m_iResultRequest == -1 ) + sError = m_sResultError; + m_pRequestFile = NULL; + + return m_iResultRequest; +} + +int ThreadedFileWorker::Read( RageFileBasic *&pFile, void *pBuf, int iSize, RString &sError ) +{ + ASSERT( m_pChildDriver != nullptr ); /* how did you get a file to begin with? */ + + /* If we're currently in a timed-out state, fail. */ + if( IsTimedOut() ) + { + this->Close( pFile ); + pFile = NULL; + } + + if( pFile == NULL ) + { + sError = "Operation timed out"; + return -1; + } + + m_pRequestFile = pFile; + m_iRequestSize = iSize; + m_pResultBuffer = new char[iSize]; + + if( !DoRequest(REQ_READ) ) + { + /* If we time out, we can no longer access pFile. */ + sError = "Operation timed out"; + pFile = NULL; + return -1; + } + + int iGot = m_iResultRequest; + if( iGot == -1 ) + sError = m_sResultError; + else + memcpy( pBuf, m_pResultBuffer, iGot ); + + m_pRequestFile = NULL; + delete [] m_pResultBuffer; + m_pResultBuffer = NULL; + + return iGot; +} + +int ThreadedFileWorker::Write( RageFileBasic *&pFile, const void *pBuf, int iSize, RString &sError ) +{ + ASSERT( m_pChildDriver != nullptr ); /* how did you get a file to begin with? */ + + /* If we're currently in a timed-out state, fail. */ + if( IsTimedOut() ) + { + this->Close( pFile ); + pFile = NULL; + } + + if( pFile == NULL ) + { + sError = "Operation timed out"; + return -1; + } + + m_pRequestFile = pFile; + m_iRequestSize = iSize; + m_pRequestBuffer = new char[iSize]; + memcpy( m_pRequestBuffer, pBuf, iSize ); + + if( !DoRequest(REQ_WRITE) ) + { + /* If we time out, we can no longer access pFile. */ + sError = "Operation timed out"; + pFile = NULL; + return -1; + } + + int iGot = m_iResultRequest; + if( m_iResultRequest == -1 ) + sError = m_sResultError; + + m_pRequestFile = NULL; + delete [] m_pRequestBuffer; + m_pRequestBuffer = NULL; + + return iGot; +} + +int ThreadedFileWorker::Flush( RageFileBasic *&pFile, RString &sError ) +{ + ASSERT( m_pChildDriver != nullptr ); /* how did you get a file to begin with? */ + + /* If we're currently in a timed-out state, fail. */ + if( IsTimedOut() ) + { + this->Close( pFile ); + pFile = NULL; + } + + if( pFile == NULL ) + { + sError = "Operation timed out"; + return -1; + } + + m_pRequestFile = pFile; + + if( !DoRequest(REQ_FLUSH) ) + { + /* If we time out, we can no longer access pFile. */ + sError = "Operation timed out"; + pFile = NULL; + return -1; + } + + if( m_iResultRequest == -1 ) + sError = m_sResultError; + + m_pRequestFile = NULL; + + return m_iResultRequest; +} + +RageFileBasic *ThreadedFileWorker::Copy( RageFileBasic *&pFile, RString &sError ) +{ + ASSERT( m_pChildDriver != nullptr ); /* how did you get a file to begin with? */ + + /* If we're currently in a timed-out state, fail. */ + if( IsTimedOut() ) + { + this->Close( pFile ); + pFile = NULL; + } + + if( pFile == NULL ) + { + sError = "Operation timed out"; + return NULL; + } + + m_pRequestFile = pFile; + if( !DoRequest(REQ_COPY) ) + { + /* If we time out, we can no longer access pFile. */ + sError = "Operation timed out"; + pFile = NULL; + return NULL; + } + + RageFileBasic *pRet = m_pResultFile; + m_pRequestFile = NULL; + m_pResultFile = NULL; + + return pRet; +} + + +bool ThreadedFileWorker::PopulateFileSet( FileSet &fs, const RString &sPath ) +{ + if( m_pChildDriver == NULL ) + return false; + + /* If we're currently in a timed-out state, fail. */ + if( IsTimedOut() ) + return false; + + /* Fill in a different FileSet; copy the results in on success. */ + m_sRequestPath = sPath; + + /* Kick off the worker thread, and wait for it to finish. */ + if( !DoRequest(REQ_POPULATE_FILE_SET) ) + { + LOG->Trace( "PopulateFileSet(%s) timed out", sPath.c_str() ); + return false; + } + + fs = m_ResultFileSet; + return true; +} + +int ThreadedFileWorker::Move( const RString &sOldPath, const RString &sNewPath ) +{ + ASSERT( m_pChildDriver != nullptr ); /* how did you get a file to begin with? */ + + /* If we're currently in a timed-out state, fail. */ + if( IsTimedOut() ) + return -1; + + m_sRequestPath = sOldPath; + m_sRequestPath2 = sNewPath; + + if( !DoRequest(REQ_MOVE) ) + { + /* If we time out, we can no longer access pFile. */ + return -1; + } + + return m_iResultRequest; +} + +int ThreadedFileWorker::Remove( const RString &sPath ) +{ + ASSERT( m_pChildDriver != nullptr ); /* how did you get a file to begin with? */ + + /* If we're currently in a timed-out state, fail. */ + if( IsTimedOut() ) + return -1; + + m_sRequestPath = sPath; + + if( !DoRequest(REQ_REMOVE) ) + { + /* If we time out, we can no longer access pFile. */ + return -1; + } + + return m_iResultRequest; +} + +bool ThreadedFileWorker::FlushDirCache( const RString &sPath ) +{ + /* FlushDirCache() is often called globally, on all drivers, which means it's called with + * no timeout. Temporarily enable a timeout if needed. */ + bool bTimeoutEnabled = TimeoutEnabled(); + if( !bTimeoutEnabled ) + SetTimeout(1); + + if( m_pChildDriver == NULL ) + return false; + + /* If we're currently in a timed-out state, fail. */ + if( IsTimedOut() ) + return false; + + m_sRequestPath = sPath; + + /* Kick off the worker thread, and wait for it to finish. */ + if( !DoRequest(REQ_FLUSH_DIR_CACHE) ) + { + if( !bTimeoutEnabled ) + SetTimeout(-1); + + LOG->Trace( "FlushDirCache(%s) timed out", sPath.c_str() ); + return false; + } + + if( !bTimeoutEnabled ) + SetTimeout(-1); + + return true; +} + + +class RageFileObjTimeout: public RageFileObj +{ +public: + /* pFile will be freed by passing it to pWorker. */ + RageFileObjTimeout( ThreadedFileWorker *pWorker, RageFileBasic *pFile, int iSize, int iMode ) + { + m_pWorker = pWorker; + m_pFile = pFile; + m_iFileSize = iSize; + m_iMode = iMode; + /* We have a lot of overhead per read and write operation, since we send + * commands to the worker thread. Buffer these operations. */ + if( iMode & RageFile::WRITE ) + EnableWriteBuffering(); + if( iMode & RageFile::READ ) + EnableReadBuffering(); + } + + ~RageFileObjTimeout() + { + if( m_pFile != nullptr ) + { + Flush(); + m_pWorker->Close( m_pFile ); + } + } + + int GetFileSize() const + { + return m_iFileSize; + } + + int GetFD() + { + RString sError; + int iRet = m_pWorker->GetFD( m_pFile ); + + if( m_pFile == NULL ) + { + SetError( "Operation timed out" ); + return -1; + } + + if( iRet == -1 ) + SetError( sError ); + + return iRet; + } + + RageFileBasic *Copy() const + { + RString sError; + RageFileBasic *pCopy = m_pWorker->Copy( m_pFile, sError ); + + if( m_pFile == NULL ) + { +// SetError( "Operation timed out" ); + return NULL; + } + + if( pCopy == NULL ) + { +// SetError( sError ); + return NULL; + } + + return new RageFileObjTimeout( m_pWorker, pCopy, m_iFileSize, m_iMode ); + } + +protected: + int SeekInternal( int iPos ) + { + RString sError; + int iRet = m_pWorker->Seek( m_pFile, iPos, sError ); + + if( m_pFile == NULL ) + { + SetError( "Operation timed out" ); + return -1; + } + + if( iRet == -1 ) + SetError( sError ); + + return iRet; + } + + + int ReadInternal( void *pBuffer, size_t iBytes ) + { + RString sError; + int iRet = m_pWorker->Read( m_pFile, pBuffer, iBytes, sError ); + + if( m_pFile == NULL ) + { + SetError( "Operation timed out" ); + return -1; + } + + if( iRet == -1 ) + SetError( sError ); + + return iRet; + } + + int WriteInternal( const void *pBuffer, size_t iBytes ) + { + RString sError; + int iRet = m_pWorker->Write( m_pFile, pBuffer, iBytes, sError ); + + if( m_pFile == NULL ) + { + SetError( "Operation timed out" ); + return -1; + } + + if( iRet == -1 ) + SetError( sError ); + + return iRet; + } + + int FlushInternal() + { + RString sError; + int iRet = m_pWorker->Flush( m_pFile, sError ); + + if( m_pFile == NULL ) + { + SetError( "Operation timed out" ); + return -1; + } + + if( iRet == -1 ) + SetError( sError ); + + return iRet; + } + + /* Mutable because the const operation Copy() timing out results in the original + * object no longer being available. */ + mutable RageFileBasic *m_pFile; + + ThreadedFileWorker *m_pWorker; + + /* GetFileSize isn't allowed to fail, so cache the file size on load. */ + int m_iFileSize; + //cache filemode + int m_iMode; +}; + +/* This FilenameDB runs PopulateFileSet in the worker thread. */ +class TimedFilenameDB: public FilenameDB +{ +public: + TimedFilenameDB() + { + ExpireSeconds = -1; + m_pWorker = NULL; + } + + void SetWorker( ThreadedFileWorker *pWorker ) + { + ASSERT( pWorker != nullptr ); + m_pWorker = pWorker; + } + + void PopulateFileSet( FileSet &fs, const RString &sPath ) + { + ASSERT( m_pWorker != nullptr ); + m_pWorker->PopulateFileSet( fs, sPath ); + } + +private: + ThreadedFileWorker *m_pWorker; +}; + +RageFileDriverTimeout::RageFileDriverTimeout( const RString &sPath ): + RageFileDriver( new TimedFilenameDB() ) +{ + m_pWorker = new ThreadedFileWorker( sPath ); + + ((TimedFilenameDB *) FDB)->SetWorker( m_pWorker ); +} + +RageFileBasic *RageFileDriverTimeout::Open( const RString &sPath, int iMode, int &iErr ) +{ + RageFileBasic *pChildFile = m_pWorker->Open( sPath, iMode, iErr ); + if( pChildFile == NULL ) + return NULL; + + /* RageBasicFile::GetFileSize isn't allowed to fail, but we are; grab the file + * size now and store it. */ + int iSize = 0; + if( iMode & RageFile::READ ) + { + iSize = m_pWorker->GetFileSize( pChildFile ); + if( iSize == -1 ) + { + /* When m_pWorker->GetFileSize fails, it takes ownership of pChildFile. */ + ASSERT( pChildFile == NULL ); + iErr = EFAULT; + return NULL; + } + } + + return new RageFileObjTimeout( m_pWorker, pChildFile, iSize, iMode ); +} + +void RageFileDriverTimeout::FlushDirCache( const RString &sPath ) +{ + RageFileDriver::FlushDirCache( sPath ); + m_pWorker->FlushDirCache( sPath ); +} + +bool RageFileDriverTimeout::Move( const RString &sOldPath, const RString &sNewPath ) +{ + int iRet = m_pWorker->Move( sOldPath, sNewPath ); + if( iRet == -1 ) + { + WARN( ssprintf("RageFileDriverTimeout::Move(%s,%s) failed", sOldPath.c_str(), sNewPath.c_str()) ); + return false; + } + + return true; +} + +bool RageFileDriverTimeout::Remove( const RString &sPath ) +{ + int iRet = m_pWorker->Remove( sPath ); + if( iRet == -1 ) + { + WARN( ssprintf("RageFileDriverTimeout::Remove(%s) failed", sPath.c_str()) ); + return false; + } + + return true; +} + +RageFileDriverTimeout::~RageFileDriverTimeout() +{ + delete m_pWorker; +} + +static struct FileDriverEntry_Timeout: public FileDriverEntry +{ + FileDriverEntry_Timeout(): FileDriverEntry( "TIMEOUT" ) { } + RageFileDriver *Create( const RString &sRoot ) const { return new RageFileDriverTimeout( sRoot ); } +} const g_RegisterDriver; + +/* + * Copyright (c) 2005 Glenn Maynard + * 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. + */ diff --git a/src/RageFileManager.cpp b/src/RageFileManager.cpp index 4535c13f55..6eac362d69 100644 --- a/src/RageFileManager.cpp +++ b/src/RageFileManager.cpp @@ -92,7 +92,7 @@ RageFileDriver *RageFileManager::GetFileDriver( RString sMountpoint ) void RageFileManager::ReleaseFileDriver( RageFileDriver *pDriver ) { - ASSERT( pDriver != NULL ); + ASSERT( pDriver != nullptr ); g_Mutex->Lock(); unsigned i; diff --git a/src/RageInput.cpp b/src/RageInput.cpp index bfabb8f275..837c95ba44 100644 --- a/src/RageInput.cpp +++ b/src/RageInput.cpp @@ -101,7 +101,7 @@ void RageInput::WindowReset() void RageInput::AddHandler( InputHandler *pHandler ) { - ASSERT( pHandler != NULL ); + ASSERT( pHandler != nullptr ); LoadedInputHandler hand; hand.m_pDevice = pHandler; @@ -125,7 +125,7 @@ InputHandler *RageInput::GetHandlerForDevice( const InputDevice id ) RString RageInput::GetDeviceSpecificInputString( const DeviceInput &di ) { InputHandler *pDriver = GetHandlerForDevice( di.device ); - if( pDriver != NULL ) + if( pDriver != nullptr ) return pDriver->GetDeviceSpecificInputString(di); else return di.ToString(); @@ -134,7 +134,7 @@ RString RageInput::GetDeviceSpecificInputString( const DeviceInput &di ) RString RageInput::GetLocalizedInputString( const DeviceInput &di ) { InputHandler *pDriver = GetHandlerForDevice( di.device ); - if( pDriver != NULL ) + if( pDriver != nullptr ) return pDriver->GetLocalizedInputString(di); else return Capitalize( DeviceButtonToString(di.button) ); @@ -143,7 +143,7 @@ RString RageInput::GetLocalizedInputString( const DeviceInput &di ) wchar_t RageInput::DeviceInputToChar( DeviceInput di, bool bUseCurrentKeyModifiers ) { InputHandler *pDriver = GetHandlerForDevice( di.device ); - if( pDriver != NULL ) + if( pDriver != nullptr ) return pDriver->DeviceButtonToChar(di.button, bUseCurrentKeyModifiers); else return '\0'; @@ -152,7 +152,7 @@ wchar_t RageInput::DeviceInputToChar( DeviceInput di, bool bUseCurrentKeyModifie InputDeviceState RageInput::GetInputDeviceState( InputDevice id ) { InputHandler *pDriver = GetHandlerForDevice( id ); - if( pDriver != NULL ) + if( pDriver != nullptr ) return pDriver->GetInputDeviceState(id); else return InputDeviceState_NoInputHandler; diff --git a/src/RageSound.cpp b/src/RageSound.cpp index 462be50b15..dcb93e0c4b 100644 --- a/src/RageSound.cpp +++ b/src/RageSound.cpp @@ -1,734 +1,734 @@ -/* Handle loading and decoding of sounds. - * - * For small files, pre-decode the entire file into a regular buffer. We - * might want to play many samples at once, and we don't want to have to decode - * 5-10 mp3s simultaneously during play. - * - * For larger files, decode them on the fly. These are usually music, and there's - * usually only one of those playing at a time. When we get updates, decode data - * at the same rate we're playing it. If we don't do this, and we're being read - * in large chunks, we're forced to decode in larger chunks as well, which can - * cause framerate problems. - * - * Error handling: - * Decoding errors (eg. CRC failures) will be recovered from when possible. - * - * When they can't be recovered, the sound will stop (unless loop or !autostop) - * and the error will be available in GetError(). - * - * Seeking past the end of the file will throw a warning and rewind. - */ - -#include "global.h" -#include "RageSound.h" -#include "RageSoundManager.h" -#include "RageUtil.h" -#include "RageLog.h" -#include "PrefsManager.h" -#include "RageSoundUtil.h" - -#include "RageSoundReader_Extend.h" -#include "RageSoundReader_Pan.h" -#include "RageSoundReader_PitchChange.h" -#include "RageSoundReader_PostBuffering.h" -#include "RageSoundReader_Preload.h" -#include "RageSoundReader_Resample_Good.h" -#include "RageSoundReader_FileReader.h" -#include "RageSoundReader_ThreadedBuffer.h" - -#define samplerate() m_pSource->GetSampleRate() - -RageSoundParams::RageSoundParams(): - m_StartSecond(0), m_LengthSeconds(-1), m_fFadeInSeconds(0), - m_fFadeOutSeconds(0), m_Volume(1.0f), m_fAttractVolume(1.0f), - m_fPitch(1.0f), m_fSpeed(1.0f), m_StartTime( RageZeroTimer ), - StopMode(M_AUTO), m_bIsCriticalSound(false) {} - -RageSoundLoadParams::RageSoundLoadParams(): - m_bSupportRateChanging(false), m_bSupportPan(false) {} - -RageSound::RageSound(): - m_Mutex( "RageSound" ), m_pSource(NULL), - m_sFilePath(""), m_Param(), m_iStreamFrame(0), - m_iStoppedSourceFrame(0), m_bPlaying(false), - m_bDeleteWhenFinished(false), m_sError("") -{ - ASSERT( SOUNDMAN != NULL ); -} - -RageSound::~RageSound() -{ - Unload(); -} - -RageSound::RageSound( const RageSound &cpy ): - RageSoundBase( cpy ), - m_Mutex( "RageSound" ) -{ - ASSERT(SOUNDMAN != NULL); - - m_pSource = NULL; - - *this = cpy; -} - -RageSound &RageSound::operator=( const RageSound &cpy ) -{ - LockMut(cpy.m_Mutex); - - /* If m_bDeleteWhenFinished, then nobody that has a reference to the sound - * should be making copies. */ - ASSERT( !cpy.m_bDeleteWhenFinished ); - - m_Param = cpy.m_Param; - m_iStreamFrame = cpy.m_iStreamFrame; - m_iStoppedSourceFrame = cpy.m_iStoppedSourceFrame; - m_bPlaying = false; - m_bDeleteWhenFinished = false; - - delete m_pSource; - if( cpy.m_pSource ) - m_pSource = cpy.m_pSource->Copy(); - else - m_pSource = NULL; - - m_sFilePath = cpy.m_sFilePath; - - return *this; -} - -void RageSound::Unload() -{ - if( IsPlaying() ) - StopPlaying(); - - LockMut(m_Mutex); - - delete m_pSource; - m_pSource = NULL; - - m_sFilePath = ""; -} - -/* The sound will self-delete itself when it stops playing. If the sound is not - * playing, the sound will be deleted immediately. The caller loses ownership - * of the sound. */ -void RageSound::DeleteSelfWhenFinishedPlaying() -{ - m_Mutex.Lock(); - - if( !m_bPlaying ) - { - m_Mutex.Unlock(); - delete this; - return; - } - - m_bDeleteWhenFinished = true; - m_Mutex.Unlock(); -} - -bool RageSound::IsLoaded() const -{ - return m_pSource != NULL; -} - -class RageSoundReader_Silence: public RageSoundReader -{ -public: - int GetLength() const { return 0; } - int GetLength_Fast() const { return 0; } - int SetPosition( int iFrame ) { return 1; } - int Read( float *pBuf, int iFrames ) { return RageSoundReader::END_OF_FILE; } - RageSoundReader *Copy() const { return new RageSoundReader_Silence; } - int GetSampleRate() const { return 44100; } - unsigned GetNumChannels() const { return 1; } - int GetNextSourceFrame() const { return 0; } - float GetStreamToSourceRatio() const { return 1.0f; } - RString GetError() const { return ""; } -}; - - -bool RageSound::Load( RString sSoundFilePath ) -{ - /* Automatically determine whether to precache */ - /* TODO: Hook this up to a pref? */ - return Load( sSoundFilePath, false ); -} - -bool RageSound::Load( RString sSoundFilePath, bool bPrecache, const RageSoundLoadParams *pParams ) -{ - LOG->Trace( "RageSound: Load \"%s\" (precache: %i)", sSoundFilePath.c_str(), bPrecache ); - - if( pParams == NULL ) - { - static const RageSoundLoadParams Defaults; - pParams = &Defaults; - } - - /* If this sound is already preloaded and held by SOUNDMAN, just make a copy - * of that. Since RageSoundReader_Preload is refcounted, this is cheap. */ - RageSoundReader *pSound = SOUNDMAN->GetLoadedSound( sSoundFilePath ); - bool bNeedBuffer = true; - if( pSound == NULL ) - { - RString error; - bool bPrebuffer; - pSound = RageSoundReader_FileReader::OpenFile( sSoundFilePath, error, &bPrebuffer ); - if( pSound == NULL ) - { - LOG->Warn( "RageSound::Load: error opening sound \"%s\": %s", - sSoundFilePath.c_str(), error.c_str() ); - - pSound = new RageSoundReader_Silence; - } - - /* If the sound is prebuffered into memory, we don't need to buffer reads. */ - if( bPrebuffer ) - bNeedBuffer = false; - } - else - { - /* The sound we were given from SOUNDMAN is already preloaded. */ - bPrecache = false; - bNeedBuffer = false; - } - - LoadSoundReader( pSound ); - - /* Try to precache. Do this after calling LoadSoundReader() to put the - * sound in this->m_pSource, so we preload after resampling. */ - if( bPrecache ) - { - if( RageSoundReader_Preload::PreloadSound(m_pSource) ) - { - /* We've preloaded the sound. Pass it to SOUNDMAN, for reuse. */ - SOUNDMAN->AddLoadedSound( sSoundFilePath, (RageSoundReader_Preload *) m_pSource ); - } - bNeedBuffer = false; - } - - m_pSource = new RageSoundReader_Extend( m_pSource ); - if( bNeedBuffer ) - m_pSource = new RageSoundReader_ThreadedBuffer( m_pSource ); - m_pSource = new RageSoundReader_PostBuffering( m_pSource ); - - if( pParams->m_bSupportRateChanging ) - { - RageSoundReader_PitchChange *pRate = new RageSoundReader_PitchChange( m_pSource ); - m_pSource = pRate; - } - - if( pParams->m_bSupportPan ) - m_pSource = new RageSoundReader_Pan( m_pSource ); - - m_sFilePath = sSoundFilePath; - - m_Mutex.SetName( ssprintf("RageSound (%s)", Basename(sSoundFilePath).c_str() ) ); - - return true; -} - -void RageSound::LoadSoundReader( RageSoundReader *pSound ) -{ - Unload(); - - m_iStreamFrame = m_iStoppedSourceFrame = 0; - - const int iNeededRate = SOUNDMAN->GetDriverSampleRate(); - bool bSupportRateChange = false; - if( iNeededRate != pSound->GetSampleRate() || bSupportRateChange ) - { - RageSoundReader_Resample_Good *Resample = new RageSoundReader_Resample_Good( pSound, iNeededRate ); - pSound = Resample; - } - - m_pSource = pSound; -} - -/* - * Retrieve audio data, for mixing. At the time of this call, the frameno at which the - * sound will be played doesn't have to be known. Once committed, and the frameno - * is known, call CommitPCMData. - * - * RageSound::GetDataToPlay and RageSound::FillBuf are the main threaded API. These - * need to execute without blocking other threads from calling eg. GetPositionSeconds, - * since they may take some time to run. - * - * On underrun, if no data was read, returns WOULD_BLOCK. On end of file, if no - * data was read, returns END_OF_FILE. If any data is read, it is returned; these - * conditions are masked and will be seen on the next call. Otherwise, the requested - * number of frames will always be returned. - */ -int RageSound::GetDataToPlay( float *pBuffer, int iFrames, int64_t &iStreamFrame, int &iFramesStored ) -{ - /* We only update m_iStreamFrame; only take a shared lock, so we don't block the main thread. */ -// LockMut(m_Mutex); - - ASSERT_M( m_bPlaying, ssprintf("%p", this) ); - ASSERT( m_pSource != NULL ); - - iFramesStored = 0; - iStreamFrame = m_iStreamFrame; - - while( iFrames > 0 ) - { - float fRate = 1.0f; - int iSourceFrame; - - /* Read data from our source. */ - int iGotFrames = m_pSource->RetriedRead( pBuffer + (iFramesStored * m_pSource->GetNumChannels()), iFrames, &iSourceFrame, &fRate ); - - if( iGotFrames == RageSoundReader::ERROR ) - { - m_sError = m_pSource->GetError(); - LOG->Warn( "Decoding %s failed: %s", GetLoadedFilePath().c_str(), m_sError.c_str() ); - } - - if( iGotFrames < 0 ) - { - if( !iFramesStored ) - return iGotFrames; - else - break; - } - - m_Mutex.Lock(); - m_StreamToSourceMap.Insert( m_iStreamFrame, iGotFrames, iSourceFrame, fRate ); - m_Mutex.Unlock(); - - m_iStreamFrame += iGotFrames; - - iFramesStored += iGotFrames; - iFrames -= iGotFrames; - } - if( m_pSource->GetNumChannels() == 1 ) - RageSoundUtil::ConvertMonoToStereoInPlace( pBuffer, iFramesStored ); - - return iFramesStored; -} - -/* Indicate that a block of audio data has been written to the device. */ -void RageSound::CommitPlayingPosition( int64_t iHardwareFrame, int64_t iStreamFrame, int iGotFrames ) -{ - m_Mutex.Lock(); - m_HardwareToStreamMap.Insert( iHardwareFrame, iGotFrames, iStreamFrame ); - m_Mutex.Unlock(); -} - -/* Start playing from the current position. */ -void RageSound::StartPlaying() -{ - ASSERT( !m_bPlaying ); - - // Move to the start position. - SetPositionFrames( lrintf(m_Param.m_StartSecond * samplerate()) ); - - /* If m_StartTime is in the past, then we probably set a start time but took too - * long loading. We don't want that; log it, since it can be unobvious. */ - if( !m_Param.m_StartTime.IsZero() && m_Param.m_StartTime.Ago() > 0 ) - LOG->Trace("Sound \"%s\" has a start time %f seconds in the past", - GetLoadedFilePath().c_str(), m_Param.m_StartTime.Ago() ); - - /* Tell the sound manager to start mixing us. */ -// LOG->Trace("set playing true for %p (StartPlaying) (%s)", this, this->GetLoadedFilePath().c_str()); - - m_bPlaying = true; - - /* Save the attract volume, so changes don't affect previously played - * sounds. */ - m_Param.m_fAttractVolume = SOUNDMAN->GetVolumeOfNonCriticalSounds(); - ApplyParams(); - - /* Don't lock while calling SOUNDMAN driver calls. */ - ASSERT( !m_Mutex.IsLockedByThisThread() ); - - SOUNDMAN->StartMixing( this ); - -// LOG->Trace("StartPlaying %p finished (%s)", this, this->GetLoadedFilePath().c_str()); -} - -void RageSound::StopPlaying() -{ - /* Don't lock while calling SOUNDMAN driver calls. */ - ASSERT( !m_Mutex.IsLockedByThisThread() ); - - /* Tell the sound driver to stop mixing this sound. */ - SOUNDMAN->StopMixing(this); -} - -/* This is called by sound drivers when we're done playing. */ -void RageSound::SoundIsFinishedPlaying() -{ - if( !m_bPlaying ) - return; - - /* Get our current hardware position. */ - int64_t iCurrentHardwareFrame = SOUNDMAN->GetPosition( NULL ); - - m_Mutex.Lock(); - - if( m_bDeleteWhenFinished ) - { - m_bDeleteWhenFinished = false; - m_Mutex.Unlock(); - delete this; - return; - } - - /* Lock the mutex after calling SOUNDMAN->GetPosition(). We must not make driver - * calls with our mutex locked (driver mutex < sound mutex). */ - if( !m_HardwareToStreamMap.IsEmpty() && !m_StreamToSourceMap.IsEmpty() ) - m_iStoppedSourceFrame = (int) GetSourceFrameFromHardwareFrame( iCurrentHardwareFrame ); - -// LOG->Trace("set playing false for %p (SoundIsFinishedPlaying) (%s)", this, this->GetLoadedFilePath().c_str()); - m_bPlaying = false; - - m_HardwareToStreamMap.Clear(); - m_StreamToSourceMap.Clear(); - -// LOG->Trace("SoundIsFinishedPlaying %p finished (%s)", this, this->GetLoadedFilePath().c_str()); - - m_Mutex.Unlock(); -} - -void RageSound::Play( const RageSoundParams *pParams ) -{ - if( m_pSource == NULL ) - { - LOG->Warn( "RageSound::Play: sound not loaded" ); - return; - } - - if( IsPlaying() ) - { - PlayCopy( pParams ); - return; - } - - if( pParams ) - SetParams( *pParams ); - - StartPlaying(); -} - -void RageSound::PlayCopy( const RageSoundParams *pParams ) const -{ - RageSound *pSound = new RageSound( *this ); - - if( pParams ) - pSound->SetParams( *pParams ); - - pSound->StartPlaying(); - pSound->DeleteSelfWhenFinishedPlaying(); -} - -void RageSound::Stop() -{ - StopPlaying(); -} - -bool RageSound::Pause( bool bPause ) -{ - if( m_pSource == NULL ) - { - LOG->Warn( "RageSound::Pause: sound not loaded" ); - return false; - } - - return SOUNDMAN->Pause( this, bPause ); -} - -float RageSound::GetLengthSeconds() -{ - if( m_pSource == NULL ) - { - LOG->Warn( "RageSound::GetLengthSeconds: sound not loaded" ); - return -1; - } - - int iLength = m_pSource->GetLength(); - - if( iLength < 0 ) - { - LOG->Warn( "GetLengthSeconds failed on %s: %s", GetLoadedFilePath().c_str(), m_pSource->GetError().c_str() ); - return -1; - } - - return iLength / 1000.f; // ms -> secs -} - -int RageSound::GetSourceFrameFromHardwareFrame( int64_t iHardwareFrame, bool *bApproximate ) const -{ - if( m_HardwareToStreamMap.IsEmpty() || m_StreamToSourceMap.IsEmpty() ) - return 0; - - bool bApprox; - int64_t iStreamFrame = m_HardwareToStreamMap.Search( iHardwareFrame, &bApprox ); - if( bApproximate && bApprox ) - *bApproximate = true; - int64_t iSourceFrame = m_StreamToSourceMap.Search( iStreamFrame, &bApprox ); - if( bApproximate && bApprox ) - *bApproximate = true; - return (int) iSourceFrame; -} - -/* If non-NULL, approximate is set to true if the returned time is approximated because of - * underrun, the sound not having started (after Play()) or finished (after EOF) yet. - * - * If non-NULL, Timestamp is set to the real clock time associated with the returned sound - * position. We might take a variable amount of time before grabbing the timestamp (to - * lock SOUNDMAN); we might lose the scheduler after grabbing it, when releasing SOUNDMAN. - */ -float RageSound::GetPositionSeconds( bool *bApproximate, RageTimer *pTimestamp ) const -{ - /* Get our current hardware position. */ - int64_t iCurrentHardwareFrame = SOUNDMAN->GetPosition( pTimestamp ); - - /* Lock the mutex after calling SOUNDMAN->GetPosition(). We must not make driver - * calls with our mutex locked (driver mutex < sound mutex). */ - LockMut( m_Mutex ); - - if( bApproximate ) - *bApproximate = false; - - /* If we're not playing, just report the static position. */ - if( !IsPlaying() ) - return m_iStoppedSourceFrame / float(samplerate()); - - /* If we don't yet have any position data, CommitPlayingPosition hasn't yet been called at all, - * so guess what we think the real time is. */ - if( m_HardwareToStreamMap.IsEmpty() || m_StreamToSourceMap.IsEmpty() ) - { - // LOG->Trace( "no data yet; %i", m_iStoppedSourceFrame ); - if( bApproximate ) - *bApproximate = true; - return m_iStoppedSourceFrame / float(samplerate()); - } - - int iSourceFrame = GetSourceFrameFromHardwareFrame( iCurrentHardwareFrame, bApproximate ); - return iSourceFrame / float(samplerate()); -} - - -bool RageSound::SetPositionFrames( int iFrames ) -{ - LockMut( m_Mutex ); - - if( m_pSource == NULL ) - { - LOG->Warn( "RageSound::SetPositionFrames(%d): sound not loaded", iFrames ); - return false; - } - - int iRet = m_pSource->SetPosition( iFrames ); - if( iRet == -1 ) - { - m_sError = m_pSource->GetError(); - LOG->Warn( "SetPositionFrames: seek %s failed: %s", GetLoadedFilePath().c_str(), m_sError.c_str() ); - } - else if( iRet == 0 ) - { - /* Seeked past EOF. */ - LOG->Warn( "SetPositionFrames: %i samples is beyond EOF in %s", - iFrames, GetLoadedFilePath().c_str() ); - } - else - { - m_iStoppedSourceFrame = iFrames; - } - - return iRet == 1; -} - -float RageSound::GetPlaybackRate() const -{ - return m_Param.m_fSpeed; -} - -RageTimer RageSound::GetStartTime() const -{ - return m_Param.m_StartTime; -} - -void RageSound::SetParams( const RageSoundParams &p ) -{ - m_Param = p; - ApplyParams(); -} - -void RageSound::ApplyParams() -{ - if( m_pSource == NULL ) - return; - - m_pSource->SetProperty( "Pitch", m_Param.m_fPitch ); - m_pSource->SetProperty( "Speed", m_Param.m_fSpeed ); - m_pSource->SetProperty( "StartSecond", m_Param.m_StartSecond ); - m_pSource->SetProperty( "LengthSeconds", m_Param.m_LengthSeconds ); - m_pSource->SetProperty( "FadeInSeconds", m_Param.m_fFadeInSeconds ); - m_pSource->SetProperty( "FadeSeconds", m_Param.m_fFadeOutSeconds ); - - float fVolume = m_Param.m_Volume * SOUNDMAN->GetMixVolume(); - if( !m_Param.m_bIsCriticalSound ) - fVolume *= m_Param.m_fAttractVolume; - m_pSource->SetProperty( "Volume", fVolume ); - - switch( GetStopMode() ) - { - case RageSoundParams::M_LOOP: - m_pSource->SetProperty( "Loop", 1.0f ); - break; - case RageSoundParams::M_STOP: - m_pSource->SetProperty( "Stop", 1.0f ); - break; - case RageSoundParams::M_CONTINUE: - m_pSource->SetProperty( "Continue", 1.0f ); - break; - default: break; - } -} - -bool RageSound::SetProperty( const RString &sProperty, float fValue ) -{ - return m_pSource->SetProperty( sProperty, fValue ); -} - -RageSoundParams::StopMode_t RageSound::GetStopMode() const -{ - if( m_Param.StopMode != RageSoundParams::M_AUTO ) - return m_Param.StopMode; - - if( m_sFilePath.find("loop") != string::npos ) - return RageSoundParams::M_LOOP; - else - return RageSoundParams::M_STOP; -} - -void RageSound::SetStopModeFromString( const RString &sStopMode ) -{ - if( sStopMode.find("stop") != string::npos ) - { - m_Param.StopMode = RageSoundParams::M_STOP; - } - else if( sStopMode.find("loop") != string::npos ) - { - m_Param.StopMode = RageSoundParams::M_LOOP; - } - else if( sStopMode.find("continue") != string::npos ) - { - m_Param.StopMode = RageSoundParams::M_CONTINUE; - } - else if( sStopMode.find("auto") != string::npos ) - { - m_Param.StopMode = RageSoundParams::M_AUTO; - } - else - { - // error - } -} - - -// lua start -#include "LuaBinding.h" - -/** @brief Allow Lua to have access to the RageSound. */ -class LunaRageSound: public Luna -{ -public: - static int pitch( T* p, lua_State *L ) - { - RageSoundParams params( p->GetParams() ); - params.m_fPitch = FArg(1); - p->SetParams( params ); - return 0; - } - - static int speed( T* p, lua_State *L ) - { - RageSoundParams params( p->GetParams() ); - params.m_fSpeed = FArg(1); - p->SetParams( params ); - return 0; - } - - static int volume( T* p, lua_State *L ) - { - RageSoundParams params( p->GetParams() ); - params.m_Volume = FArg(1); - p->SetParams( params ); - return 0; - } - - static int SetProperty( T* p, lua_State *L ) - { - LuaHelpers::Push( L, p->SetProperty(SArg(1), FArg(2)) ); - return 1; - } - - // Rename me and deprecate the above one? -DaisuMaster - static int SetParam( T* p, lua_State *L ) - { - RageSoundParams params( p->GetParams() ); - - RString val = SArg(1); - if( val == "StartSecond" ) params.m_StartSecond = FArg(2); - else if( val == "LengthSeconds" ) params.m_LengthSeconds = FArg(2); - else if( val == "FadeInSeconds" ) params.m_fFadeInSeconds = FArg(2); - else if( val == "FadeSeconds" ) params.m_fFadeOutSeconds = FArg(2); - else if( val == "Pitch" ) params.m_fPitch = FArg(2); - else if( val == "Speed" ) params.m_fSpeed = FArg(2); - else if( val == "Volume" ) params.m_Volume = FArg(2); - - p->SetParams( params ); - return 0; - } - - /* - static int SetStopMode( T* p, lua_State *L ) - { - LuaHelpers::Push( L, p->SetStopModeFromString(SArg(1)) ); - return 1; - } - */ - - LunaRageSound() - { - ADD_METHOD( pitch ); - ADD_METHOD( speed ); - ADD_METHOD( volume ); - ADD_METHOD( SetProperty ); - ADD_METHOD( SetParam ); - //ADD_METHOD( SetStopMode ); - } -}; - -LUA_REGISTER_CLASS( RageSound ) -// lua end - -/* - * Copyright (c) 2002-2004 Glenn Maynard - * 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. - */ - +/* Handle loading and decoding of sounds. + * + * For small files, pre-decode the entire file into a regular buffer. We + * might want to play many samples at once, and we don't want to have to decode + * 5-10 mp3s simultaneously during play. + * + * For larger files, decode them on the fly. These are usually music, and there's + * usually only one of those playing at a time. When we get updates, decode data + * at the same rate we're playing it. If we don't do this, and we're being read + * in large chunks, we're forced to decode in larger chunks as well, which can + * cause framerate problems. + * + * Error handling: + * Decoding errors (eg. CRC failures) will be recovered from when possible. + * + * When they can't be recovered, the sound will stop (unless loop or !autostop) + * and the error will be available in GetError(). + * + * Seeking past the end of the file will throw a warning and rewind. + */ + +#include "global.h" +#include "RageSound.h" +#include "RageSoundManager.h" +#include "RageUtil.h" +#include "RageLog.h" +#include "PrefsManager.h" +#include "RageSoundUtil.h" + +#include "RageSoundReader_Extend.h" +#include "RageSoundReader_Pan.h" +#include "RageSoundReader_PitchChange.h" +#include "RageSoundReader_PostBuffering.h" +#include "RageSoundReader_Preload.h" +#include "RageSoundReader_Resample_Good.h" +#include "RageSoundReader_FileReader.h" +#include "RageSoundReader_ThreadedBuffer.h" + +#define samplerate() m_pSource->GetSampleRate() + +RageSoundParams::RageSoundParams(): + m_StartSecond(0), m_LengthSeconds(-1), m_fFadeInSeconds(0), + m_fFadeOutSeconds(0), m_Volume(1.0f), m_fAttractVolume(1.0f), + m_fPitch(1.0f), m_fSpeed(1.0f), m_StartTime( RageZeroTimer ), + StopMode(M_AUTO), m_bIsCriticalSound(false) {} + +RageSoundLoadParams::RageSoundLoadParams(): + m_bSupportRateChanging(false), m_bSupportPan(false) {} + +RageSound::RageSound(): + m_Mutex( "RageSound" ), m_pSource(NULL), + m_sFilePath(""), m_Param(), m_iStreamFrame(0), + m_iStoppedSourceFrame(0), m_bPlaying(false), + m_bDeleteWhenFinished(false), m_sError("") +{ + ASSERT( SOUNDMAN != nullptr ); +} + +RageSound::~RageSound() +{ + Unload(); +} + +RageSound::RageSound( const RageSound &cpy ): + RageSoundBase( cpy ), + m_Mutex( "RageSound" ) +{ + ASSERT(SOUNDMAN != nullptr); + + m_pSource = NULL; + + *this = cpy; +} + +RageSound &RageSound::operator=( const RageSound &cpy ) +{ + LockMut(cpy.m_Mutex); + + /* If m_bDeleteWhenFinished, then nobody that has a reference to the sound + * should be making copies. */ + ASSERT( !cpy.m_bDeleteWhenFinished ); + + m_Param = cpy.m_Param; + m_iStreamFrame = cpy.m_iStreamFrame; + m_iStoppedSourceFrame = cpy.m_iStoppedSourceFrame; + m_bPlaying = false; + m_bDeleteWhenFinished = false; + + delete m_pSource; + if( cpy.m_pSource ) + m_pSource = cpy.m_pSource->Copy(); + else + m_pSource = NULL; + + m_sFilePath = cpy.m_sFilePath; + + return *this; +} + +void RageSound::Unload() +{ + if( IsPlaying() ) + StopPlaying(); + + LockMut(m_Mutex); + + delete m_pSource; + m_pSource = NULL; + + m_sFilePath = ""; +} + +/* The sound will self-delete itself when it stops playing. If the sound is not + * playing, the sound will be deleted immediately. The caller loses ownership + * of the sound. */ +void RageSound::DeleteSelfWhenFinishedPlaying() +{ + m_Mutex.Lock(); + + if( !m_bPlaying ) + { + m_Mutex.Unlock(); + delete this; + return; + } + + m_bDeleteWhenFinished = true; + m_Mutex.Unlock(); +} + +bool RageSound::IsLoaded() const +{ + return m_pSource != nullptr; +} + +class RageSoundReader_Silence: public RageSoundReader +{ +public: + int GetLength() const { return 0; } + int GetLength_Fast() const { return 0; } + int SetPosition( int iFrame ) { return 1; } + int Read( float *pBuf, int iFrames ) { return RageSoundReader::END_OF_FILE; } + RageSoundReader *Copy() const { return new RageSoundReader_Silence; } + int GetSampleRate() const { return 44100; } + unsigned GetNumChannels() const { return 1; } + int GetNextSourceFrame() const { return 0; } + float GetStreamToSourceRatio() const { return 1.0f; } + RString GetError() const { return ""; } +}; + + +bool RageSound::Load( RString sSoundFilePath ) +{ + /* Automatically determine whether to precache */ + /* TODO: Hook this up to a pref? */ + return Load( sSoundFilePath, false ); +} + +bool RageSound::Load( RString sSoundFilePath, bool bPrecache, const RageSoundLoadParams *pParams ) +{ + LOG->Trace( "RageSound: Load \"%s\" (precache: %i)", sSoundFilePath.c_str(), bPrecache ); + + if( pParams == NULL ) + { + static const RageSoundLoadParams Defaults; + pParams = &Defaults; + } + + /* If this sound is already preloaded and held by SOUNDMAN, just make a copy + * of that. Since RageSoundReader_Preload is refcounted, this is cheap. */ + RageSoundReader *pSound = SOUNDMAN->GetLoadedSound( sSoundFilePath ); + bool bNeedBuffer = true; + if( pSound == NULL ) + { + RString error; + bool bPrebuffer; + pSound = RageSoundReader_FileReader::OpenFile( sSoundFilePath, error, &bPrebuffer ); + if( pSound == NULL ) + { + LOG->Warn( "RageSound::Load: error opening sound \"%s\": %s", + sSoundFilePath.c_str(), error.c_str() ); + + pSound = new RageSoundReader_Silence; + } + + /* If the sound is prebuffered into memory, we don't need to buffer reads. */ + if( bPrebuffer ) + bNeedBuffer = false; + } + else + { + /* The sound we were given from SOUNDMAN is already preloaded. */ + bPrecache = false; + bNeedBuffer = false; + } + + LoadSoundReader( pSound ); + + /* Try to precache. Do this after calling LoadSoundReader() to put the + * sound in this->m_pSource, so we preload after resampling. */ + if( bPrecache ) + { + if( RageSoundReader_Preload::PreloadSound(m_pSource) ) + { + /* We've preloaded the sound. Pass it to SOUNDMAN, for reuse. */ + SOUNDMAN->AddLoadedSound( sSoundFilePath, (RageSoundReader_Preload *) m_pSource ); + } + bNeedBuffer = false; + } + + m_pSource = new RageSoundReader_Extend( m_pSource ); + if( bNeedBuffer ) + m_pSource = new RageSoundReader_ThreadedBuffer( m_pSource ); + m_pSource = new RageSoundReader_PostBuffering( m_pSource ); + + if( pParams->m_bSupportRateChanging ) + { + RageSoundReader_PitchChange *pRate = new RageSoundReader_PitchChange( m_pSource ); + m_pSource = pRate; + } + + if( pParams->m_bSupportPan ) + m_pSource = new RageSoundReader_Pan( m_pSource ); + + m_sFilePath = sSoundFilePath; + + m_Mutex.SetName( ssprintf("RageSound (%s)", Basename(sSoundFilePath).c_str() ) ); + + return true; +} + +void RageSound::LoadSoundReader( RageSoundReader *pSound ) +{ + Unload(); + + m_iStreamFrame = m_iStoppedSourceFrame = 0; + + const int iNeededRate = SOUNDMAN->GetDriverSampleRate(); + bool bSupportRateChange = false; + if( iNeededRate != pSound->GetSampleRate() || bSupportRateChange ) + { + RageSoundReader_Resample_Good *Resample = new RageSoundReader_Resample_Good( pSound, iNeededRate ); + pSound = Resample; + } + + m_pSource = pSound; +} + +/* + * Retrieve audio data, for mixing. At the time of this call, the frameno at which the + * sound will be played doesn't have to be known. Once committed, and the frameno + * is known, call CommitPCMData. + * + * RageSound::GetDataToPlay and RageSound::FillBuf are the main threaded API. These + * need to execute without blocking other threads from calling eg. GetPositionSeconds, + * since they may take some time to run. + * + * On underrun, if no data was read, returns WOULD_BLOCK. On end of file, if no + * data was read, returns END_OF_FILE. If any data is read, it is returned; these + * conditions are masked and will be seen on the next call. Otherwise, the requested + * number of frames will always be returned. + */ +int RageSound::GetDataToPlay( float *pBuffer, int iFrames, int64_t &iStreamFrame, int &iFramesStored ) +{ + /* We only update m_iStreamFrame; only take a shared lock, so we don't block the main thread. */ +// LockMut(m_Mutex); + + ASSERT_M( m_bPlaying, ssprintf("%p", this) ); + ASSERT( m_pSource != nullptr ); + + iFramesStored = 0; + iStreamFrame = m_iStreamFrame; + + while( iFrames > 0 ) + { + float fRate = 1.0f; + int iSourceFrame; + + /* Read data from our source. */ + int iGotFrames = m_pSource->RetriedRead( pBuffer + (iFramesStored * m_pSource->GetNumChannels()), iFrames, &iSourceFrame, &fRate ); + + if( iGotFrames == RageSoundReader::ERROR ) + { + m_sError = m_pSource->GetError(); + LOG->Warn( "Decoding %s failed: %s", GetLoadedFilePath().c_str(), m_sError.c_str() ); + } + + if( iGotFrames < 0 ) + { + if( !iFramesStored ) + return iGotFrames; + else + break; + } + + m_Mutex.Lock(); + m_StreamToSourceMap.Insert( m_iStreamFrame, iGotFrames, iSourceFrame, fRate ); + m_Mutex.Unlock(); + + m_iStreamFrame += iGotFrames; + + iFramesStored += iGotFrames; + iFrames -= iGotFrames; + } + if( m_pSource->GetNumChannels() == 1 ) + RageSoundUtil::ConvertMonoToStereoInPlace( pBuffer, iFramesStored ); + + return iFramesStored; +} + +/* Indicate that a block of audio data has been written to the device. */ +void RageSound::CommitPlayingPosition( int64_t iHardwareFrame, int64_t iStreamFrame, int iGotFrames ) +{ + m_Mutex.Lock(); + m_HardwareToStreamMap.Insert( iHardwareFrame, iGotFrames, iStreamFrame ); + m_Mutex.Unlock(); +} + +/* Start playing from the current position. */ +void RageSound::StartPlaying() +{ + ASSERT( !m_bPlaying ); + + // Move to the start position. + SetPositionFrames( lrintf(m_Param.m_StartSecond * samplerate()) ); + + /* If m_StartTime is in the past, then we probably set a start time but took too + * long loading. We don't want that; log it, since it can be unobvious. */ + if( !m_Param.m_StartTime.IsZero() && m_Param.m_StartTime.Ago() > 0 ) + LOG->Trace("Sound \"%s\" has a start time %f seconds in the past", + GetLoadedFilePath().c_str(), m_Param.m_StartTime.Ago() ); + + /* Tell the sound manager to start mixing us. */ +// LOG->Trace("set playing true for %p (StartPlaying) (%s)", this, this->GetLoadedFilePath().c_str()); + + m_bPlaying = true; + + /* Save the attract volume, so changes don't affect previously played + * sounds. */ + m_Param.m_fAttractVolume = SOUNDMAN->GetVolumeOfNonCriticalSounds(); + ApplyParams(); + + /* Don't lock while calling SOUNDMAN driver calls. */ + ASSERT( !m_Mutex.IsLockedByThisThread() ); + + SOUNDMAN->StartMixing( this ); + +// LOG->Trace("StartPlaying %p finished (%s)", this, this->GetLoadedFilePath().c_str()); +} + +void RageSound::StopPlaying() +{ + /* Don't lock while calling SOUNDMAN driver calls. */ + ASSERT( !m_Mutex.IsLockedByThisThread() ); + + /* Tell the sound driver to stop mixing this sound. */ + SOUNDMAN->StopMixing(this); +} + +/* This is called by sound drivers when we're done playing. */ +void RageSound::SoundIsFinishedPlaying() +{ + if( !m_bPlaying ) + return; + + /* Get our current hardware position. */ + int64_t iCurrentHardwareFrame = SOUNDMAN->GetPosition( NULL ); + + m_Mutex.Lock(); + + if( m_bDeleteWhenFinished ) + { + m_bDeleteWhenFinished = false; + m_Mutex.Unlock(); + delete this; + return; + } + + /* Lock the mutex after calling SOUNDMAN->GetPosition(). We must not make driver + * calls with our mutex locked (driver mutex < sound mutex). */ + if( !m_HardwareToStreamMap.IsEmpty() && !m_StreamToSourceMap.IsEmpty() ) + m_iStoppedSourceFrame = (int) GetSourceFrameFromHardwareFrame( iCurrentHardwareFrame ); + +// LOG->Trace("set playing false for %p (SoundIsFinishedPlaying) (%s)", this, this->GetLoadedFilePath().c_str()); + m_bPlaying = false; + + m_HardwareToStreamMap.Clear(); + m_StreamToSourceMap.Clear(); + +// LOG->Trace("SoundIsFinishedPlaying %p finished (%s)", this, this->GetLoadedFilePath().c_str()); + + m_Mutex.Unlock(); +} + +void RageSound::Play( const RageSoundParams *pParams ) +{ + if( m_pSource == NULL ) + { + LOG->Warn( "RageSound::Play: sound not loaded" ); + return; + } + + if( IsPlaying() ) + { + PlayCopy( pParams ); + return; + } + + if( pParams ) + SetParams( *pParams ); + + StartPlaying(); +} + +void RageSound::PlayCopy( const RageSoundParams *pParams ) const +{ + RageSound *pSound = new RageSound( *this ); + + if( pParams ) + pSound->SetParams( *pParams ); + + pSound->StartPlaying(); + pSound->DeleteSelfWhenFinishedPlaying(); +} + +void RageSound::Stop() +{ + StopPlaying(); +} + +bool RageSound::Pause( bool bPause ) +{ + if( m_pSource == NULL ) + { + LOG->Warn( "RageSound::Pause: sound not loaded" ); + return false; + } + + return SOUNDMAN->Pause( this, bPause ); +} + +float RageSound::GetLengthSeconds() +{ + if( m_pSource == NULL ) + { + LOG->Warn( "RageSound::GetLengthSeconds: sound not loaded" ); + return -1; + } + + int iLength = m_pSource->GetLength(); + + if( iLength < 0 ) + { + LOG->Warn( "GetLengthSeconds failed on %s: %s", GetLoadedFilePath().c_str(), m_pSource->GetError().c_str() ); + return -1; + } + + return iLength / 1000.f; // ms -> secs +} + +int RageSound::GetSourceFrameFromHardwareFrame( int64_t iHardwareFrame, bool *bApproximate ) const +{ + if( m_HardwareToStreamMap.IsEmpty() || m_StreamToSourceMap.IsEmpty() ) + return 0; + + bool bApprox; + int64_t iStreamFrame = m_HardwareToStreamMap.Search( iHardwareFrame, &bApprox ); + if( bApproximate && bApprox ) + *bApproximate = true; + int64_t iSourceFrame = m_StreamToSourceMap.Search( iStreamFrame, &bApprox ); + if( bApproximate && bApprox ) + *bApproximate = true; + return (int) iSourceFrame; +} + +/* If non-NULL, approximate is set to true if the returned time is approximated because of + * underrun, the sound not having started (after Play()) or finished (after EOF) yet. + * + * If non-NULL, Timestamp is set to the real clock time associated with the returned sound + * position. We might take a variable amount of time before grabbing the timestamp (to + * lock SOUNDMAN); we might lose the scheduler after grabbing it, when releasing SOUNDMAN. + */ +float RageSound::GetPositionSeconds( bool *bApproximate, RageTimer *pTimestamp ) const +{ + /* Get our current hardware position. */ + int64_t iCurrentHardwareFrame = SOUNDMAN->GetPosition( pTimestamp ); + + /* Lock the mutex after calling SOUNDMAN->GetPosition(). We must not make driver + * calls with our mutex locked (driver mutex < sound mutex). */ + LockMut( m_Mutex ); + + if( bApproximate ) + *bApproximate = false; + + /* If we're not playing, just report the static position. */ + if( !IsPlaying() ) + return m_iStoppedSourceFrame / float(samplerate()); + + /* If we don't yet have any position data, CommitPlayingPosition hasn't yet been called at all, + * so guess what we think the real time is. */ + if( m_HardwareToStreamMap.IsEmpty() || m_StreamToSourceMap.IsEmpty() ) + { + // LOG->Trace( "no data yet; %i", m_iStoppedSourceFrame ); + if( bApproximate ) + *bApproximate = true; + return m_iStoppedSourceFrame / float(samplerate()); + } + + int iSourceFrame = GetSourceFrameFromHardwareFrame( iCurrentHardwareFrame, bApproximate ); + return iSourceFrame / float(samplerate()); +} + + +bool RageSound::SetPositionFrames( int iFrames ) +{ + LockMut( m_Mutex ); + + if( m_pSource == NULL ) + { + LOG->Warn( "RageSound::SetPositionFrames(%d): sound not loaded", iFrames ); + return false; + } + + int iRet = m_pSource->SetPosition( iFrames ); + if( iRet == -1 ) + { + m_sError = m_pSource->GetError(); + LOG->Warn( "SetPositionFrames: seek %s failed: %s", GetLoadedFilePath().c_str(), m_sError.c_str() ); + } + else if( iRet == 0 ) + { + /* Seeked past EOF. */ + LOG->Warn( "SetPositionFrames: %i samples is beyond EOF in %s", + iFrames, GetLoadedFilePath().c_str() ); + } + else + { + m_iStoppedSourceFrame = iFrames; + } + + return iRet == 1; +} + +float RageSound::GetPlaybackRate() const +{ + return m_Param.m_fSpeed; +} + +RageTimer RageSound::GetStartTime() const +{ + return m_Param.m_StartTime; +} + +void RageSound::SetParams( const RageSoundParams &p ) +{ + m_Param = p; + ApplyParams(); +} + +void RageSound::ApplyParams() +{ + if( m_pSource == NULL ) + return; + + m_pSource->SetProperty( "Pitch", m_Param.m_fPitch ); + m_pSource->SetProperty( "Speed", m_Param.m_fSpeed ); + m_pSource->SetProperty( "StartSecond", m_Param.m_StartSecond ); + m_pSource->SetProperty( "LengthSeconds", m_Param.m_LengthSeconds ); + m_pSource->SetProperty( "FadeInSeconds", m_Param.m_fFadeInSeconds ); + m_pSource->SetProperty( "FadeSeconds", m_Param.m_fFadeOutSeconds ); + + float fVolume = m_Param.m_Volume * SOUNDMAN->GetMixVolume(); + if( !m_Param.m_bIsCriticalSound ) + fVolume *= m_Param.m_fAttractVolume; + m_pSource->SetProperty( "Volume", fVolume ); + + switch( GetStopMode() ) + { + case RageSoundParams::M_LOOP: + m_pSource->SetProperty( "Loop", 1.0f ); + break; + case RageSoundParams::M_STOP: + m_pSource->SetProperty( "Stop", 1.0f ); + break; + case RageSoundParams::M_CONTINUE: + m_pSource->SetProperty( "Continue", 1.0f ); + break; + default: break; + } +} + +bool RageSound::SetProperty( const RString &sProperty, float fValue ) +{ + return m_pSource->SetProperty( sProperty, fValue ); +} + +RageSoundParams::StopMode_t RageSound::GetStopMode() const +{ + if( m_Param.StopMode != RageSoundParams::M_AUTO ) + return m_Param.StopMode; + + if( m_sFilePath.find("loop") != string::npos ) + return RageSoundParams::M_LOOP; + else + return RageSoundParams::M_STOP; +} + +void RageSound::SetStopModeFromString( const RString &sStopMode ) +{ + if( sStopMode.find("stop") != string::npos ) + { + m_Param.StopMode = RageSoundParams::M_STOP; + } + else if( sStopMode.find("loop") != string::npos ) + { + m_Param.StopMode = RageSoundParams::M_LOOP; + } + else if( sStopMode.find("continue") != string::npos ) + { + m_Param.StopMode = RageSoundParams::M_CONTINUE; + } + else if( sStopMode.find("auto") != string::npos ) + { + m_Param.StopMode = RageSoundParams::M_AUTO; + } + else + { + // error + } +} + + +// lua start +#include "LuaBinding.h" + +/** @brief Allow Lua to have access to the RageSound. */ +class LunaRageSound: public Luna +{ +public: + static int pitch( T* p, lua_State *L ) + { + RageSoundParams params( p->GetParams() ); + params.m_fPitch = FArg(1); + p->SetParams( params ); + return 0; + } + + static int speed( T* p, lua_State *L ) + { + RageSoundParams params( p->GetParams() ); + params.m_fSpeed = FArg(1); + p->SetParams( params ); + return 0; + } + + static int volume( T* p, lua_State *L ) + { + RageSoundParams params( p->GetParams() ); + params.m_Volume = FArg(1); + p->SetParams( params ); + return 0; + } + + static int SetProperty( T* p, lua_State *L ) + { + LuaHelpers::Push( L, p->SetProperty(SArg(1), FArg(2)) ); + return 1; + } + + // Rename me and deprecate the above one? -DaisuMaster + static int SetParam( T* p, lua_State *L ) + { + RageSoundParams params( p->GetParams() ); + + RString val = SArg(1); + if( val == "StartSecond" ) params.m_StartSecond = FArg(2); + else if( val == "LengthSeconds" ) params.m_LengthSeconds = FArg(2); + else if( val == "FadeInSeconds" ) params.m_fFadeInSeconds = FArg(2); + else if( val == "FadeSeconds" ) params.m_fFadeOutSeconds = FArg(2); + else if( val == "Pitch" ) params.m_fPitch = FArg(2); + else if( val == "Speed" ) params.m_fSpeed = FArg(2); + else if( val == "Volume" ) params.m_Volume = FArg(2); + + p->SetParams( params ); + return 0; + } + + /* + static int SetStopMode( T* p, lua_State *L ) + { + LuaHelpers::Push( L, p->SetStopModeFromString(SArg(1)) ); + return 1; + } + */ + + LunaRageSound() + { + ADD_METHOD( pitch ); + ADD_METHOD( speed ); + ADD_METHOD( volume ); + ADD_METHOD( SetProperty ); + ADD_METHOD( SetParam ); + //ADD_METHOD( SetStopMode ); + } +}; + +LUA_REGISTER_CLASS( RageSound ) +// lua end + +/* + * Copyright (c) 2002-2004 Glenn Maynard + * 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. + */ + diff --git a/src/RageSoundManager.cpp b/src/RageSoundManager.cpp index 7b64440daa..6dc207712c 100644 --- a/src/RageSoundManager.cpp +++ b/src/RageSoundManager.cpp @@ -69,13 +69,13 @@ void RageSoundManager::Shutdown() void RageSoundManager::StartMixing( RageSoundBase *pSound ) { - if( m_pDriver != NULL ) + if( m_pDriver != nullptr ) m_pDriver->StartMixing( pSound ); } void RageSoundManager::StopMixing( RageSoundBase *pSound ) { - if( m_pDriver != NULL ) + if( m_pDriver != nullptr ) m_pDriver->StopMixing( pSound ); } @@ -118,7 +118,7 @@ void RageSoundManager::Update() g_SoundManMutex.Unlock(); /* finished with m_mapPreloadedSounds */ - if( m_pDriver != NULL ) + if( m_pDriver != nullptr ) m_pDriver->Update(); } diff --git a/src/RageSoundReader_Extend.cpp b/src/RageSoundReader_Extend.cpp index c5e5e87f1c..fb04eff1fc 100644 --- a/src/RageSoundReader_Extend.cpp +++ b/src/RageSoundReader_Extend.cpp @@ -1,225 +1,225 @@ -#include "global.h" -#include "RageSoundReader_Extend.h" -#include "RageLog.h" -#include "RageSoundUtil.h" -#include "RageUtil.h" - -/* - * Add support for negative seeks (adding a delay), extending a sound - * beyond its end (m_LengthSeconds and M_CONTINUE), looping and fading. - * This filter is normally inserted before extended buffering, implementing - * properties that can seek the sound; this results in buffered seeks, but - * changes to these properties are delayed. - */ - -RageSoundReader_Extend::RageSoundReader_Extend( RageSoundReader *pSource ): - RageSoundReader_Filter( pSource ) -{ - ASSERT_M(pSource != NULL, "The music file was not found! Was it deleted or moved while the game was on?"); - m_iPositionFrames = pSource->GetNextSourceFrame(); - - m_StopMode = M_STOP; - m_iStartFrames = 0; - m_iLengthFrames = -1; - m_iFadeOutFrames = 0; - m_iFadeInFrames = 0; - m_bIgnoreFadeInFrames = false; -} - -int RageSoundReader_Extend::SetPosition( int iFrame ) -{ - m_bIgnoreFadeInFrames = false; - - m_iPositionFrames = iFrame; - int iRet = m_pSource->SetPosition( max(iFrame, 0) ); - if( iRet < 0 ) - return iRet; - - if( m_iLengthFrames != -1 ) - return m_iPositionFrames < GetEndFrame(); - - /* If we're in CONTINUE and we seek past the end of the file, don't return EOF. */ - if( m_StopMode == M_CONTINUE ) - return 1; - - return iRet; -} - -int RageSoundReader_Extend::GetEndFrame() const -{ - if( m_iLengthFrames == -1 ) - return -1; - - return m_iStartFrames + m_iLengthFrames; -} - -int RageSoundReader_Extend::GetData( float *pBuffer, int iFrames ) -{ - int iFramesToRead = iFrames; - if( m_iLengthFrames != -1 ) - { - int iFramesLeft = GetEndFrame() - m_iPositionFrames; - iFramesLeft = max( 0, iFramesLeft ); - iFramesToRead = min( iFramesToRead, iFramesLeft ); - } - - if( iFrames && !iFramesToRead ) - return RageSoundReader::END_OF_FILE; - - if( m_iPositionFrames < 0 ) - { - iFramesToRead = min( iFramesToRead, -m_iPositionFrames ); - memset( pBuffer, 0, iFramesToRead * sizeof(float) * this->GetNumChannels() ); - return iFramesToRead; - } - - int iNewPositionFrames = m_pSource->GetNextSourceFrame(); - int iRet = RageSoundReader_Filter::Read( pBuffer, iFramesToRead ); - - /* Update the position from the source. If the source is at EOF, skip this, - * so we'll extrapolate in M_CONTINUE. */ - if( iRet != RageSoundReader::END_OF_FILE ) - m_iPositionFrames = iNewPositionFrames; - return iRet; -} - -int RageSoundReader_Extend::Read( float *pBuffer, int iFrames ) -{ - int iFramesRead = GetData( pBuffer, iFrames ); - if( iFramesRead == RageSoundReader::END_OF_FILE ) - { - if( (m_iLengthFrames != -1 && m_iPositionFrames < GetEndFrame()) || - m_StopMode == M_CONTINUE ) - { - iFramesRead = iFrames; - if( m_StopMode != M_CONTINUE ) - iFramesRead = min( GetEndFrame() - m_iPositionFrames, iFramesRead ); - memset( pBuffer, 0, iFramesRead * sizeof(float) * this->GetNumChannels() ); - } - } - - if( iFramesRead > 0 ) - { - int iFullVolumePositionFrames = 0; - int iSilencePositionFrames = 0; - if( m_iFadeInFrames != 0 && !m_bIgnoreFadeInFrames ) - { - iSilencePositionFrames = 0; - iFullVolumePositionFrames = m_iFadeInFrames; - } - - /* We want to fade when there's m_iFadeFrames frames left, but if - * m_LengthFrames is -1, we don't know the length we're playing. - * (m_LengthFrames is the length to play, not the length of the - * source.) If we don't know the length, don't fade. */ - if( m_iFadeOutFrames != 0 && m_iLengthFrames != -1 ) - { - iSilencePositionFrames = GetEndFrame(); - iFullVolumePositionFrames = iSilencePositionFrames - m_iFadeOutFrames; - } - - if( iSilencePositionFrames != iFullVolumePositionFrames ) - { - const int iStartSecond = m_iPositionFrames; - const int iEndSecond = m_iPositionFrames + iFramesRead; - const float fStartVolume = SCALE( iStartSecond, iFullVolumePositionFrames, iSilencePositionFrames, 1.0f, 0.0f ); - const float fEndVolume = SCALE( iEndSecond, iFullVolumePositionFrames, iSilencePositionFrames, 1.0f, 0.0f ); - RageSoundUtil::Fade( pBuffer, iFramesRead, this->GetNumChannels(), fStartVolume, fEndVolume ); - } - - m_iPositionFrames += iFramesRead; - } - - if( iFramesRead == RageSoundReader::END_OF_FILE && m_StopMode == M_LOOP ) - { - this->SetPosition( m_iStartFrames ); - - /* If we're not fading out at the end, then only fade in once. Ignore - * m_iFadeInFrames until seeked, so we only fade in once. */ - if( m_iFadeOutFrames == 0 ) - m_bIgnoreFadeInFrames = true; - return STREAM_LOOPED; - } - - return iFramesRead; -} - -int RageSoundReader_Extend::GetNextSourceFrame() const -{ - return m_iPositionFrames; -} - -bool RageSoundReader_Extend::SetProperty( const RString &sProperty, float fValue ) -{ - if( sProperty == "StartSecond" ) - { - m_iStartFrames = lrintf( fValue * this->GetSampleRate() ); - return true; - } - - if( sProperty == "LengthSeconds" ) - { - if( fValue == -1 ) - m_iLengthFrames = -1; - else - m_iLengthFrames = lrintf( fValue * this->GetSampleRate() ); - return true; - } - - if( sProperty == "Loop" ) - { - m_StopMode = M_LOOP; - return true; - } - - if( sProperty == "Stop" ) - { - m_StopMode = M_STOP; - return true; - } - - if( sProperty == "Continue" ) - { - m_StopMode = M_CONTINUE; - return true; - } - - if( sProperty == "FadeInSeconds" ) - { - m_iFadeInFrames = lrintf( fValue * this->GetSampleRate() ); - return true; - } - - if( sProperty == "FadeSeconds" || sProperty == "FadeOutSeconds" ) - { - m_iFadeOutFrames = lrintf( fValue * this->GetSampleRate() ); - return true; - } - - return false; -} - -/* - * Copyright (c) 2003-2006 Glenn Maynard - * 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. - */ +#include "global.h" +#include "RageSoundReader_Extend.h" +#include "RageLog.h" +#include "RageSoundUtil.h" +#include "RageUtil.h" + +/* + * Add support for negative seeks (adding a delay), extending a sound + * beyond its end (m_LengthSeconds and M_CONTINUE), looping and fading. + * This filter is normally inserted before extended buffering, implementing + * properties that can seek the sound; this results in buffered seeks, but + * changes to these properties are delayed. + */ + +RageSoundReader_Extend::RageSoundReader_Extend( RageSoundReader *pSource ): + RageSoundReader_Filter( pSource ) +{ + ASSERT_M(pSource != nullptr, "The music file was not found! Was it deleted or moved while the game was on?"); + m_iPositionFrames = pSource->GetNextSourceFrame(); + + m_StopMode = M_STOP; + m_iStartFrames = 0; + m_iLengthFrames = -1; + m_iFadeOutFrames = 0; + m_iFadeInFrames = 0; + m_bIgnoreFadeInFrames = false; +} + +int RageSoundReader_Extend::SetPosition( int iFrame ) +{ + m_bIgnoreFadeInFrames = false; + + m_iPositionFrames = iFrame; + int iRet = m_pSource->SetPosition( max(iFrame, 0) ); + if( iRet < 0 ) + return iRet; + + if( m_iLengthFrames != -1 ) + return m_iPositionFrames < GetEndFrame(); + + /* If we're in CONTINUE and we seek past the end of the file, don't return EOF. */ + if( m_StopMode == M_CONTINUE ) + return 1; + + return iRet; +} + +int RageSoundReader_Extend::GetEndFrame() const +{ + if( m_iLengthFrames == -1 ) + return -1; + + return m_iStartFrames + m_iLengthFrames; +} + +int RageSoundReader_Extend::GetData( float *pBuffer, int iFrames ) +{ + int iFramesToRead = iFrames; + if( m_iLengthFrames != -1 ) + { + int iFramesLeft = GetEndFrame() - m_iPositionFrames; + iFramesLeft = max( 0, iFramesLeft ); + iFramesToRead = min( iFramesToRead, iFramesLeft ); + } + + if( iFrames && !iFramesToRead ) + return RageSoundReader::END_OF_FILE; + + if( m_iPositionFrames < 0 ) + { + iFramesToRead = min( iFramesToRead, -m_iPositionFrames ); + memset( pBuffer, 0, iFramesToRead * sizeof(float) * this->GetNumChannels() ); + return iFramesToRead; + } + + int iNewPositionFrames = m_pSource->GetNextSourceFrame(); + int iRet = RageSoundReader_Filter::Read( pBuffer, iFramesToRead ); + + /* Update the position from the source. If the source is at EOF, skip this, + * so we'll extrapolate in M_CONTINUE. */ + if( iRet != RageSoundReader::END_OF_FILE ) + m_iPositionFrames = iNewPositionFrames; + return iRet; +} + +int RageSoundReader_Extend::Read( float *pBuffer, int iFrames ) +{ + int iFramesRead = GetData( pBuffer, iFrames ); + if( iFramesRead == RageSoundReader::END_OF_FILE ) + { + if( (m_iLengthFrames != -1 && m_iPositionFrames < GetEndFrame()) || + m_StopMode == M_CONTINUE ) + { + iFramesRead = iFrames; + if( m_StopMode != M_CONTINUE ) + iFramesRead = min( GetEndFrame() - m_iPositionFrames, iFramesRead ); + memset( pBuffer, 0, iFramesRead * sizeof(float) * this->GetNumChannels() ); + } + } + + if( iFramesRead > 0 ) + { + int iFullVolumePositionFrames = 0; + int iSilencePositionFrames = 0; + if( m_iFadeInFrames != 0 && !m_bIgnoreFadeInFrames ) + { + iSilencePositionFrames = 0; + iFullVolumePositionFrames = m_iFadeInFrames; + } + + /* We want to fade when there's m_iFadeFrames frames left, but if + * m_LengthFrames is -1, we don't know the length we're playing. + * (m_LengthFrames is the length to play, not the length of the + * source.) If we don't know the length, don't fade. */ + if( m_iFadeOutFrames != 0 && m_iLengthFrames != -1 ) + { + iSilencePositionFrames = GetEndFrame(); + iFullVolumePositionFrames = iSilencePositionFrames - m_iFadeOutFrames; + } + + if( iSilencePositionFrames != iFullVolumePositionFrames ) + { + const int iStartSecond = m_iPositionFrames; + const int iEndSecond = m_iPositionFrames + iFramesRead; + const float fStartVolume = SCALE( iStartSecond, iFullVolumePositionFrames, iSilencePositionFrames, 1.0f, 0.0f ); + const float fEndVolume = SCALE( iEndSecond, iFullVolumePositionFrames, iSilencePositionFrames, 1.0f, 0.0f ); + RageSoundUtil::Fade( pBuffer, iFramesRead, this->GetNumChannels(), fStartVolume, fEndVolume ); + } + + m_iPositionFrames += iFramesRead; + } + + if( iFramesRead == RageSoundReader::END_OF_FILE && m_StopMode == M_LOOP ) + { + this->SetPosition( m_iStartFrames ); + + /* If we're not fading out at the end, then only fade in once. Ignore + * m_iFadeInFrames until seeked, so we only fade in once. */ + if( m_iFadeOutFrames == 0 ) + m_bIgnoreFadeInFrames = true; + return STREAM_LOOPED; + } + + return iFramesRead; +} + +int RageSoundReader_Extend::GetNextSourceFrame() const +{ + return m_iPositionFrames; +} + +bool RageSoundReader_Extend::SetProperty( const RString &sProperty, float fValue ) +{ + if( sProperty == "StartSecond" ) + { + m_iStartFrames = lrintf( fValue * this->GetSampleRate() ); + return true; + } + + if( sProperty == "LengthSeconds" ) + { + if( fValue == -1 ) + m_iLengthFrames = -1; + else + m_iLengthFrames = lrintf( fValue * this->GetSampleRate() ); + return true; + } + + if( sProperty == "Loop" ) + { + m_StopMode = M_LOOP; + return true; + } + + if( sProperty == "Stop" ) + { + m_StopMode = M_STOP; + return true; + } + + if( sProperty == "Continue" ) + { + m_StopMode = M_CONTINUE; + return true; + } + + if( sProperty == "FadeInSeconds" ) + { + m_iFadeInFrames = lrintf( fValue * this->GetSampleRate() ); + return true; + } + + if( sProperty == "FadeSeconds" || sProperty == "FadeOutSeconds" ) + { + m_iFadeOutFrames = lrintf( fValue * this->GetSampleRate() ); + return true; + } + + return false; +} + +/* + * Copyright (c) 2003-2006 Glenn Maynard + * 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. + */ diff --git a/src/RageSoundReader_MP3.cpp b/src/RageSoundReader_MP3.cpp index a85ecbc4e7..e2761ac43f 100644 --- a/src/RageSoundReader_MP3.cpp +++ b/src/RageSoundReader_MP3.cpp @@ -1,1177 +1,1177 @@ -/* MAD is available from: http://www.underbit.com/products/mad/ */ - -#include "global.h" -#include "RageSoundReader_MP3.h" -#include "RageLog.h" -#include "RageUtil.h" - -#include -#include -#include - -#if defined(_WINDOWS) || defined(MACOSX) -#include "../extern/mad-0.15.1b/mad.h" -#ifdef _MSC_VER -#pragma comment(lib, "libmad.lib") -#endif //_MSC_VER -#else -#include -#endif // _WINDOWS - -// ID3 code from libid3: -enum tagtype { - TAGTYPE_NONE = 0, - TAGTYPE_ID3V1, - TAGTYPE_ID3V2, - TAGTYPE_ID3V2_FOOTER -}; - -static const int ID3_TAG_FLAG_FOOTERPRESENT = 0x10; - -static tagtype tagtype( const unsigned char *data, id3_length_t length ) -{ - if (length >= 3 && - data[0] == 'T' && data[1] == 'A' && data[2] == 'G') - return TAGTYPE_ID3V1; - - if (length >= 10 && - ((data[0] == 'I' && data[1] == 'D' && data[2] == '3') || - (data[0] == '3' && data[1] == 'D' && data[2] == 'I')) && - data[3] < 0xff && data[4] < 0xff && - data[6] < 0x80 && data[7] < 0x80 && data[8] < 0x80 && data[9] < 0x80) - return data[0] == 'I' ? TAGTYPE_ID3V2 : TAGTYPE_ID3V2_FOOTER; - - return TAGTYPE_NONE; -} - -static unsigned long id3_parse_uint( const unsigned char **ptr, unsigned int bytes ) -{ - unsigned long value = 0; - - ASSERT(bytes >= 1 && bytes <= 4); - - switch (bytes) - { - case 4: value = (value << 8) | *(*ptr)++; - case 3: value = (value << 8) | *(*ptr)++; - case 2: value = (value << 8) | *(*ptr)++; - case 1: value = (value << 8) | *(*ptr)++; - } - - return value; -} - -static unsigned long id3_parse_syncsafe( const unsigned char **ptr, unsigned int bytes ) -{ - unsigned long value = 0; - - ASSERT(bytes == 4 || bytes == 5); - - switch (bytes) - { - case 5: - value = (value << 4) | (*(*ptr)++ & 0x0f); - case 4: - value = (value << 7) | (*(*ptr)++ & 0x7f); - value = (value << 7) | (*(*ptr)++ & 0x7f); - value = (value << 7) | (*(*ptr)++ & 0x7f); - value = (value << 7) | (*(*ptr)++ & 0x7f); - } - - return value; -} - - -static void parse_header(const unsigned char **ptr, - unsigned int *version, int *flags, id3_length_t *size) -{ - *ptr += 3; - - *version = id3_parse_uint(ptr, 2); - *flags = id3_parse_uint(ptr, 1); - *size = id3_parse_syncsafe(ptr, 4); -} - -/* - * NAME: tag->query() - * DESCRIPTION: if a tag begins at the given location, return its size - */ -signed long id3_tag_query( const unsigned char *data, id3_length_t length ) -{ - unsigned int version; - int flags; - id3_length_t size; - - switch (tagtype(data, length)) - { - case TAGTYPE_ID3V1: - return 128; - - case TAGTYPE_ID3V2: - parse_header(&data, &version, &flags, &size); - - if (flags & ID3_TAG_FLAG_FOOTERPRESENT) - size += 10; - - return 10 + size; - - case TAGTYPE_ID3V2_FOOTER: - parse_header(&data, &version, &flags, &size); - return -(int)size - 10; - - case TAGTYPE_NONE: - break; - } - - return 0; -} - - - - -/* XING code ripped out of madplay (header) - * - * mad - MPEG audio decoder - * Copyright (C) 2000-2001 Robert Leslie - */ -struct xing -{ - long flags; /* valid fields (see below) */ - unsigned long frames; /* total number of frames */ - unsigned long bytes; /* total number of bytes */ - unsigned char toc[100]; /* 100-point seek table */ - long scale; /* ?? */ - enum { XING, INFO } type; -}; - -enum { - XING_FRAMES = 0x00000001L, - XING_BYTES = 0x00000002L, - XING_TOC = 0x00000004L, - XING_SCALE = 0x00000008L -}; - -void xing_init( struct xing *xing ); -int xing_parse( struct xing *xing, struct mad_bitptr ptr, unsigned int bitlen ); - -/* end XING header */ - - -/* a -= b */ -static void mad_timer_sub(mad_timer_t *a, mad_timer_t b) -{ - /* a = a - b -> a = a + -b */ - mad_timer_negate(&b); - mad_timer_add(a, b); -} - - - - - - -/* internal->decoder_private field */ -struct madlib_t -{ - madlib_t() - { - outpos = 0; - outleft = 0; - memset( inbuf, 0, sizeof(inbuf) ); - memset( outbuf, 0, sizeof(outbuf) ); - memset( &Stream, 0, sizeof(Stream) ); - memset( &Frame, 0, sizeof(Frame) ); - memset( &Synth, 0, sizeof(Synth) ); - Timer = mad_timer_zero; - timer_accurate = false; - inbuf_filepos = 0; - filesize = 0; - header_bytes = 0; - first_frame = 0; - has_xing = 0; - memset( &xingtag, 0, sizeof(xingtag) ); - length = 0; - framelength = mad_timer_zero; - bitrate = 0; - } - - uint8_t inbuf[16384]; - float outbuf[8192]; - int outpos; - unsigned outleft; - - struct mad_stream Stream; - struct mad_frame Frame; - struct mad_synth Synth; - /* Timestamp of the next frame. */ - mad_timer_t Timer; - - /* Whether Timer is trusted; this is false after doing a quick seek. */ - int timer_accurate; - - /* - * Frame index of each percentage of the file. This is constructed - * as we read the file, so we can quickly seek back later. - */ - struct mad_timer_compare_lt - { - bool operator() ( const mad_timer_t &timer1, const mad_timer_t &timer2 ) const - { - return mad_timer_compare( timer1, timer2 ) < 0; - } - }; - - typedef map tocmap_t; - tocmap_t tocmap; - - /* Position in the file of inbuf: */ - int inbuf_filepos; - - /* File size. */ - int filesize; - - /* Number of bytes of header data at the beginning; used for seeking. */ - int header_bytes; - - /* Whether we've decoded the first frame of real audio yet. If this - * is false, we're somewhere before the first frame; we might be in - * the middle of headers. */ - int first_frame; - - /* This data is filled in when the first frame is decoded. */ - int has_xing; /* whether xingtag is valid */ - struct xing xingtag; - - /* If has_xing is true, this is filled in based on the xing header. - * If it's false, this is filled in based on the size of the file - * and the first frame. */ - int length; - mad_timer_t framelength; - - /* If we have a Xing tag, this is the average bitrate; otherwise it's - * the bitrate of the first frame. */ - int bitrate; -}; - - - - - - - - -static float scale(mad_fixed_t sample) -{ - return float( double(sample) / (1<inbuf_filepos; - - /* If we have a frame, adjust. */ - if( mad->Stream.this_frame != NULL ) - ret += mad->Stream.this_frame-mad->inbuf; - - return ret; -} - - -/* Called on the first frame decoded. Returns true if this frame - * should be ignored. */ -bool RageSoundReader_MP3::handle_first_frame() -{ - bool ret = false; - - /* Check for a XING tag. */ - xing_init( &mad->xingtag ); - if( xing_parse(&mad->xingtag, mad->Stream.anc_ptr, mad->Stream.anc_bitlen) == 0 ) - { - /* - * "Info" tags are written by some tools. They're just Xing tags, but for - * CBR files. - * - * However, DWI's decoder, BASS, doesn't understand this, and treats it as a - * corrupt frame, outputting a frame of silence. Let's ignore the tag, so - * it'll be treated as an invalid frame, so we match DWI sync. - * - * The information in it isn't very useful to us. The TOC is less accurate - * then computing it ourself (low resolution). The file length computation - * might be wrong, if the tag is incorrect, and we can compute that accurately - * ourself for CBR files. - */ - if( mad->xingtag.type == xing::INFO ) - return false; - - mad->header_bytes = max( mad->header_bytes, get_this_frame_byte(mad) ); - - mad->has_xing = true; - - mad_timer_t tm = mad->Frame.header.duration; - /* XXX: does this include the Xing header itself? */ - mad_timer_multiply( &tm, mad->xingtag.frames ); - mad->length = mad_timer_count( tm, MAD_UNITS_MILLISECONDS ); - - /* XXX: an id3v2 footer tag would throw this off a little. This also assumes - * the Xing tag is the last header; it always is, I think. */ - int bytes = mad->filesize - mad->header_bytes; - mad->bitrate = (int)(bytes * 8 / (mad->length/1000.f)); - - if( mad->xingtag.type == xing::XING ) - ret = 1; - } - - /* If there's no Xing tag, mad->length will be filled in by _open. */ - - return ret; -} - - -int RageSoundReader_MP3::fill_buffer() -{ - /* Need more data. */ - int inbytes = 0; - if( mad->Stream.next_frame != NULL ) - { - /* Pull out remaining data from the last buffer. */ - inbytes = mad->Stream.bufend-mad->Stream.next_frame; - memmove( mad->inbuf, mad->Stream.next_frame, inbytes ); - mad->inbuf_filepos += mad->Stream.next_frame - mad->inbuf; - } - - const bool bWasAtEOF = m_pFile->AtEOF(); - - int rc = m_pFile->Read( mad->inbuf + inbytes, sizeof(mad->inbuf)-inbytes-MAD_BUFFER_GUARD ); - if( rc < 0 ) - { - SetError( m_pFile->GetError() ); - return -1; - } - - if ( m_pFile->AtEOF() && !bWasAtEOF ) - { - /* We just reached EOF. Append MAD_BUFFER_GUARD bytes of NULs to the - * buffer, to ensure that the last frame is flushed. */ - memset( mad->inbuf + inbytes + rc, 0, MAD_BUFFER_GUARD ); - rc += MAD_BUFFER_GUARD; - } - - if ( rc == 0 ) - return 0; - - inbytes += rc; - mad_stream_buffer( &mad->Stream, mad->inbuf, inbytes ); - return rc; -} - -void fill_frame_index_cache( madlib_t *mad ) -{ - int pos; - - /* Only update the frame cache if our timer is consistent. */ - if(!mad->timer_accurate) return; - - /* the frame we just decoded: */ - pos = get_this_frame_byte(mad); - - /* Fill in the TOC percent. */ - if( !mad->tocmap.empty() ) - { - /* Don't add an entry if one already exists near this time. */ - madlib_t::tocmap_t::iterator it = mad->tocmap.upper_bound( mad->Timer ); - if( it != mad->tocmap.begin() ) - { - --it; - int iDiffSeconds = mad->Timer.seconds - it->first.seconds; - if( iDiffSeconds < 5 ) - return; - } - } - - mad->tocmap[mad->Timer] = pos; -} - -/* Handle first-stage decoding: extracting the MP3 frame data. */ -int RageSoundReader_MP3::do_mad_frame_decode( bool headers_only ) -{ - int bytes_read = 0; - - while(1) - { - int ret; - - /* Always actually decode the first packet, so we cleanly parse Xing tags. */ - if( headers_only && !mad->first_frame ) - ret=mad_header_decode( &mad->Frame.header,&mad->Stream ); - else - ret=mad_frame_decode( &mad->Frame,&mad->Stream ); - - if( ret == -1 && (mad->Stream.error == MAD_ERROR_BUFLEN || mad->Stream.error == MAD_ERROR_BUFPTR) ) - { - if( bytes_read > 25000 ) - { - /* We've read this much without actually getting a frame; error. */ - SetError( "Can't find data" ); - return -1; - } - - ret = fill_buffer(); - if( ret <= 0 ) - return ret; - bytes_read += ret; - - continue; - } - - if( ret == -1 && mad->Stream.error == MAD_ERROR_LOSTSYNC ) - { - /* This might be an ID3V2 tag. */ - const int tagsize = id3_tag_query(mad->Stream.this_frame, - mad->Stream.bufend - mad->Stream.this_frame); - - if( tagsize ) - { - mad_stream_skip(&mad->Stream, tagsize); - - /* Don't count the tagsize against the max-read-per-call figure. */ - bytes_read -= tagsize; - - continue; - } - } - - if( ret == -1 && mad->Stream.error == MAD_ERROR_BADDATAPTR ) - { - /* - * Something's corrupt. One cause of this is cutting an MP3 in the middle - * without reencoding; the first two frames will reference data from previous - * frames that have been removed. The frame is valid--we can get a header from - * it, we just can't synth useful data. - * - * BASS pretends the bad frames are silent. Emulate that, for compatibility. - */ - ret = 0; /* pretend success */ - } - - if( !ret ) - { - /* OK. */ - if( mad->first_frame ) - { - /* We're at the beginning. Is this a Xing tag? */ - if(handle_first_frame()) - { - /* The first frame contained a header. Continue searching. */ - continue; - } - - /* We've decoded the first frame of data. - * - * We want mad->Timer to represent the timestamp of the first sample of the - * currently decoded frame. Don't increment mad->Timer on the first frame, - * or it'll be the time of the *next* frame. (All frames have the same - * duration.) */ - mad->first_frame = false; - mad->Timer = mad_timer_zero; - mad->header_bytes = get_this_frame_byte(mad); - } - else - { - mad_timer_add( &mad->Timer,mad->Frame.header.duration ); - } - - fill_frame_index_cache( mad ); - - return 1; - } - - if( mad->Stream.error == MAD_ERROR_BADCRC ) - { - /* XXX untested */ - mad_frame_mute(&mad->Frame); - mad_synth_mute(&mad->Synth); - - continue; - } - - if( !MAD_RECOVERABLE(mad->Stream.error) ) - { - /* We've received an unrecoverable error. */ - SetError( mad_stream_errorstr(&mad->Stream) ); - return -1; - } - } -} - - -void RageSoundReader_MP3::synth_output() -{ - mad->outleft = mad->outpos = 0; - - if( MAD_NCHANNELS(&mad->Frame.header) != this->Channels ) - { - /* This frame contains a different number of channels than the first. - * I've never actually seen this--it's just to prevent exploding if - * it happens--and we could continue on easily enough, so if it happens, - * just continue. */ - return; - } - - mad_synth_frame(&mad->Synth, &mad->Frame); - for(int i=0; i < mad->Synth.pcm.length; i++) - { - for(int chan = 0; chan < this->Channels; ++chan) - { - float Sample = scale(mad->Synth.pcm.samples[chan][i]); - mad->outbuf[mad->outleft] = Sample; - ++mad->outleft; - } - } -} - -/* Seek to a byte in the file. If you're going to put the file position - * back when you're done, and not going to read any data, you don't have - * to use this. */ -int RageSoundReader_MP3::seek_stream_to_byte( int byte ) -{ - if( m_pFile->Seek(byte) == -1 ) - { - SetError( strerror(errno) ); - return 0; - } - - mad_frame_mute(&mad->Frame); - mad_synth_mute(&mad->Synth); - mad_stream_finish(&mad->Stream); - mad_stream_init(&mad->Stream); - - mad->outleft = mad->outpos = 0; - - mad->inbuf_filepos = byte; - - /* If the position is <= the position of the first audio sample, then - * we're at the beginning. */ - if( !mad->tocmap.empty() ) - mad->first_frame = ( byte <= mad->tocmap.begin()->second ); - - return 1; -} - - -#if 0 -/* Call this after seeking the stream. We'll back up a bit and reread - * frames until we're back where we started, so the next read is aligned - * to a frame and synced. This must never leave the position ahead of where - * it way, since that can confuse the seek optimizations. */ -int RageSoundReader_MP3::resync() -{ - /* Save the timer; decoding will change it, and we need to put it back. */ - mad_timer_t orig = mad->Timer; - - /* Seek backwards up to 4k. */ - const int origpos = mad->inbuf_filepos; - const int seekpos = max( 0, origpos - 1024*4 ); - seek_stream_to_byte( seekpos ); - - /* Agh. This is annoying. We want to decode enough so that the next frame - * read will be the first frame after the current file pointer. If we just - * read until the file pointer is >= what it was, we've passed it already. - * So, read until it's >= what it was, counting the number of times we had - * to read; then back up again and read n-1 times. Gross. */ - int reads = 0; - do - { - if( do_mad_frame_decode() <= 0 ) /* XXX eof */ - return -1; /* it set the error */ - - reads++; - } while( get_this_frame_byte(mad) < origpos ); - - seek_stream_to_byte( seekpos ); - - reads--; - while( reads-- > 0 ) - { - if( do_mad_frame_decode() <= 0 ) /* XXX eof */ - return -1; /* it set the error */ - } - - /* Restore the timer. */ - mad->Timer = orig; - mad->outpos = mad->outleft = 0; - - return 1; -} -#endif - -RageSoundReader_MP3::RageSoundReader_MP3() -{ - mad = new madlib_t; - m_bAccurateSync = false; - - mad_stream_init( &mad->Stream ); - mad_frame_init( &mad->Frame ); - mad_synth_init( &mad->Synth ); - - mad_frame_mute( &mad->Frame ); - mad_timer_reset( &mad->Timer ); - mad->length = -1; - mad->inbuf_filepos = 0; - mad->header_bytes = 0; - mad->has_xing = false; - mad->timer_accurate = 1; - mad->bitrate = -1; - mad->first_frame = true; -} - -RageSoundReader_MP3::~RageSoundReader_MP3() -{ - mad_synth_finish( &mad->Synth ); - mad_frame_finish( &mad->Frame ); - mad_stream_finish( &mad->Stream ); - - delete mad; -} - -RageSoundReader_FileReader::OpenResult RageSoundReader_MP3::Open( RageFileBasic *pFile ) -{ - m_pFile = pFile; - - mad->filesize = m_pFile->GetFileSize(); - ASSERT( mad->filesize != -1 ); - - /* Make sure we can decode at least one frame. This will also fill in header info. */ - mad->outpos = 0; - - int ret = do_mad_frame_decode(); - switch(ret) - { - case 0: - SetError( "Failed to read any data at all" ); - return OPEN_UNKNOWN_FILE_FORMAT; - case -1: - SetError( GetError() + " (not an MP3 stream?)" ); - return OPEN_UNKNOWN_FILE_FORMAT; - } - - /* Store the bitrate of the frame we just got. */ - if( mad->bitrate == -1 ) /* might have been set by a Xing tag */ - mad->bitrate = mad->Frame.header.bitrate; - - SampleRate = mad->Frame.header.samplerate; - mad->framelength = mad->Frame.header.duration; - this->Channels = MAD_NCHANNELS( &mad->Frame.header ); - - /* Since we've already decoded a frame, just synth it instead of rewinding - * the stream. */ - synth_output(); - - if(mad->length == -1) - { - /* If vbr and !xing, this is just an estimate. */ - int bps = mad->bitrate / 8; - float secs = (float)(mad->filesize - mad->header_bytes) / bps; - mad->length = (int)(secs * 1000.f); - } - - return OPEN_OK; -} - - -RageSoundReader_MP3 *RageSoundReader_MP3::Copy() const -{ - RageSoundReader_MP3 *ret = new RageSoundReader_MP3; - - ret->m_pFile = m_pFile->Copy(); - ret->m_pFile->Seek( 0 ); - ret->m_bAccurateSync = m_bAccurateSync; - ret->mad->filesize = mad->filesize; - ret->mad->bitrate = mad->bitrate; - ret->SampleRate = SampleRate; - ret->mad->framelength = mad->framelength; - ret->Channels = Channels; - ret->mad->length = mad->length; - -// int n = ret->do_mad_frame_decode(); -// ASSERT( n > 0 ); -// ret->synth_output(); - - return ret; -} - -int RageSoundReader_MP3::Read( float *buf, int iFrames ) -{ - int iFramesWritten = 0; - - while( iFrames > 0 ) - { - if( mad->outleft > 0 ) - { - int iFramesToCopy = min( iFrames, int(mad->outleft / GetNumChannels()) ); - const int iSamplesToCopy = iFramesToCopy * GetNumChannels(); - const int iBytesToCopy = iSamplesToCopy * sizeof(float); - - memcpy( buf, mad->outbuf + mad->outpos, iBytesToCopy ); - - buf += iSamplesToCopy; - iFrames -= iFramesToCopy; - iFramesWritten += iFramesToCopy; - mad->outpos += iSamplesToCopy; - mad->outleft -= iSamplesToCopy; - continue; - } - - /* Decode more from the MP3 stream. */ - int ret = do_mad_frame_decode(); - if( ret == 0 ) - return END_OF_FILE; - if( ret == -1 ) - return ERROR; - - synth_output(); - } - - return iFramesWritten; -} - -bool RageSoundReader_MP3::MADLIB_rewind() -{ - m_pFile->Seek(0); - - mad_frame_mute(&mad->Frame); - mad_synth_mute(&mad->Synth); - mad_timer_reset(&mad->Timer); - mad->outpos = mad->outleft = 0; - - mad_stream_finish(&mad->Stream); - mad_stream_init(&mad->Stream); - mad_stream_buffer(&mad->Stream, NULL, 0); - mad->inbuf_filepos = 0; - - /* Be careful. We need to leave header_bytes alone, so if we try to SetPosition_estimate - * immediately after this, we still know the header size. However, we need to set first_frame - * to true, since the first frame is handled specially in do_mad_frame_decode; if we don't - * set it, then we'll be desynced by a frame after an accurate seek. */ -// mad->header_bytes = 0; - mad->first_frame = true; - mad->Stream.this_frame = NULL; - - return true; -} - -/* Methods of seeking: - * - * 1. We can jump based on a TOC. We potentially have two; the Xing TOC and our - * own index. The Xing TOC is only accurate to 1/256th of the file size, - * so it's unsuitable for precise seeks. Our own TOC is byte-accurate. - * (SetPosition_toc) - * - * 2. We can jump based on the bitrate. This is fast, but not accurate. - * (SetPosition_estimate) - * - * 3. We can seek from any position to any higher position by decoding headers. - * (SetPosition_hard) - * - * Both 1 and 2 will leave the position behind the actual requested position; - * combine them with 3 to catch up. Never do 3 alone in "fast" mode, since it's - * slow if it ends up seeking from the beginning of the file. Never do 2 in - * "precise" mode. - */ - -/* Returns actual position on success, 0 if past EOF, -1 on error. */ -int RageSoundReader_MP3::SetPosition_toc( int iFrame, bool Xing ) -{ - ASSERT( !Xing || mad->has_xing ); - ASSERT( mad->length != -1 ); - - /* This leaves our timer accurate if we're using our own TOC, and inaccurate - * if we're using Xing. */ - mad->timer_accurate = !Xing; - - int bytepos = -1; - if( Xing ) - { - /* We can speed up the seek using the XING tag. First, figure - * out what percentage the requested position falls in. */ - ASSERT( SampleRate != 0 ); - int ms = int( (iFrame * 1000LL) / SampleRate ); - ASSERT( mad->length != 0 ); - int percent = ms * 100 / mad->length; - if( percent < 100 ) - { - int jump = mad->xingtag.toc[percent]; - bytepos = mad->filesize * jump / 256; - } - else - bytepos = 2000000000; /* force EOF */ - - mad_timer_set( &mad->Timer, 0, percent * mad->length, 100000 ); - } - else - { - mad_timer_t desired; - mad_timer_set( &desired, 0, iFrame, SampleRate ); - - if( mad->tocmap.empty() ) - return 1; /* don't have any info */ - - /* Find the last entry <= iFrame that we actually have an entry for; - * this will get us as close as possible. */ - madlib_t::tocmap_t::iterator it = mad->tocmap.upper_bound( desired ); - if( it == mad->tocmap.begin() ) - return 1; /* don't have any info */ - --it; - - mad->Timer = it->first; - bytepos = it->second; - } - - if( bytepos != -1 ) - { - /* Seek backwards up to 4k. */ - const int seekpos = max( 0, bytepos - 1024*4 ); - seek_stream_to_byte( seekpos ); - - do - { - int ret = do_mad_frame_decode(); - if( ret <= 0 ) - return ret; /* it set the error */ - } while( get_this_frame_byte(mad) < bytepos ); - synth_output(); - } - - return 1; -} - -int RageSoundReader_MP3::SetPosition_hard( int iFrame ) -{ - mad_timer_t desired; - mad_timer_set( &desired, 0, iFrame, mad->Frame.header.samplerate ); - - /* This seek doesn't change the accuracy of our timer. */ - - /* If we're already exactly at the requested position, OK. */ - if( mad_timer_compare(mad->Timer, desired) == 0 ) - return 1; - - /* We always come in here with data synthed. Be careful not to synth the - * same frame twice. */ - bool synthed=true; - - /* If we're already past the requested position, rewind. */ - if(mad_timer_compare(mad->Timer, desired) > 0) - { - MADLIB_rewind(); - do_mad_frame_decode(); - synthed = false; - } - - /* Decode frames until the current frame contains the desired offset. */ - while(1) - { - /* If desired < next_frame_timer, this frame contains the position. Since we've - * already decoded the frame, synth it, too. */ - mad_timer_t next_frame_timer = mad->Timer; - mad_timer_add( &next_frame_timer, mad->framelength ); - - if( mad_timer_compare(desired, next_frame_timer) < 0 ) - { - if( !synthed ) - { - synth_output(); - } - else - { - mad->outleft += mad->outpos; - mad->outpos = 0; - } - - /* We just synthed data starting at mad->Timer, containing the desired offset. - * Skip (desired - mad->Timer) worth of frames in the output to line up. */ - mad_timer_t skip = desired; - mad_timer_sub( &skip, mad->Timer ); - - int samples = mad_timer_count( skip, (mad_units) SampleRate ); - - /* Skip 'samples' samples. */ - mad->outpos = samples * this->Channels; - mad->outleft -= samples * this->Channels; - return 1; - } - - /* Otherwise, if the desired time will be in the *next* decode, then synth - * this one, too. */ - mad_timer_t next_next_frame_timer = next_frame_timer; - mad_timer_add( &next_next_frame_timer, mad->framelength ); - - if( mad_timer_compare(desired, next_next_frame_timer) < 0 && !synthed ) - { - synth_output(); - synthed = true; - } - - int ret = do_mad_frame_decode(); - if( ret <= 0 ) - { - mad->outleft = mad->outpos = 0; - return ret; /* it set the error */ - } - - synthed = false; - } -} - -/* Do a seek based on the bitrate. */ -int RageSoundReader_MP3::SetPosition_estimate( int iFrame ) -{ - /* This doesn't leave us accurate. */ - mad->timer_accurate = 0; - - mad_timer_t seekamt; - mad_timer_set( &seekamt, 0, iFrame, mad->Frame.header.samplerate ); - { - /* We're going to skip ahead two samples below, so seek earlier than - * we were asked to. */ - mad_timer_t back_len = mad->framelength; - mad_timer_multiply(&back_len, -2); - mad_timer_add(&seekamt, back_len); - if( mad_timer_compare(seekamt, mad_timer_zero) < 0 ) - seekamt = mad_timer_zero; - } - - int seekpos = mad_timer_count( seekamt, MAD_UNITS_MILLISECONDS ) * (mad->bitrate / 8 / 1000); - seekpos += mad->header_bytes; - seek_stream_to_byte( seekpos ); - - /* We've jumped across the file, so the decoder is currently desynced. - * Don't use resync(); it's slow. Just decode a few frames. */ - for( int i = 0; i < 2; ++i ) - { - int ret = do_mad_frame_decode(); - if( ret <= 0 ) - return ret; - } - - /* Throw out one synth. */ - synth_output(); - mad->outleft = 0; - - /* Find out where we really seeked to. */ - int ms = (get_this_frame_byte(mad) - mad->header_bytes) / (mad->bitrate / 8 / 1000); - mad_timer_set(&mad->Timer, 0, ms, 1000); - - return 1; -} - -int RageSoundReader_MP3::SetPosition( int iFrame ) -{ - if( m_bAccurateSync ) - { - /* Seek using our own internal (accurate) TOC. */ - int ret = SetPosition_toc( iFrame, false ); - if( ret <= 0 ) - return ret; /* it set the error */ - - /* Align exactly. */ - return SetPosition_hard( iFrame ); - } - else - { - /* Rewinding is always fast and accurate, and SetPosition_estimate is bad at 0. */ - if( !iFrame ) - { - MADLIB_rewind(); - return 1; /* ok */ - } - - /* We can do a fast jump in VBR with Xing with more accuracy than without Xing. */ - if( mad->has_xing ) - return SetPosition_toc( iFrame, true ); - - /* Guess. This is only remotely accurate when we're not VBR, but also - * do it if we have no Xing tag. */ - return SetPosition_estimate( iFrame ); - } -} - -bool RageSoundReader_MP3::SetProperty( const RString &sProperty, float fValue ) -{ - if( sProperty == "AccurateSync" ) - { - m_bAccurateSync = (fValue > 0.001f); - return true; - } - - return RageSoundReader_FileReader::SetProperty( sProperty, fValue ); -} - -int RageSoundReader_MP3::GetNextSourceFrame() const -{ - int iFrame = mad_timer_count( mad->Timer, mad_units(mad->Frame.header.samplerate) ); - iFrame += mad->outpos / this->Channels; - return iFrame; -} - -int RageSoundReader_MP3::GetLengthInternal( bool fast ) -{ - if( mad->has_xing && mad->length != -1 ) - return mad->length; /* should be accurate */ - - /* Check to see if a frame in the middle of the file is the same - * bitrate as the first frame. If it is, assume the file is really CBR. */ - seek_stream_to_byte( mad->filesize / 2 ); - - /* XXX use mad_header_decode and check more than one frame */ - if(mad->length != -1 && - do_mad_frame_decode() && - mad->bitrate == (int) mad->Frame.header.bitrate) - { - return mad->length; - } - - if( !MADLIB_rewind() ) - return 0; - - /* Worst-case: vbr && !xing. We've made a guess at the length, but let's actually - * scan the size, since the guess is probably wrong. */ - if( fast ) - { - SetError("Can't estimate file length"); - return -1; - } - - MADLIB_rewind(); - while(1) - { - int ret = do_mad_frame_decode( true ); - if( ret == -1 ) - return -1; /* it set the error */ - if( ret == 0 ) /* EOF */ - break; - } - - /* mad->Timer is the timestamp of the current frame; find the timestamp of - * the very end. */ - mad_timer_t end = mad->Timer; - mad_timer_add( &end, mad->framelength ); - - /* Count milliseconds. */ - return mad_timer_count( end, MAD_UNITS_MILLISECONDS ); -} - -int RageSoundReader_MP3::GetLengthConst( bool fast ) const -{ - RageSoundReader_MP3 *pCopy = this->Copy(); - - int iLength = pCopy->GetLengthInternal( fast ); - - delete pCopy; - return iLength; -} - - - - - - -/* begin XING code ripped out of madplay */ - -/* - * NAME: xing->init() - * DESCRIPTION: initialize Xing structure - */ -void xing_init(struct xing *xing) -{ - xing->flags = 0; -} - -/* - * NAME: xing->parse() - * DESCRIPTION: parse a Xing VBR header - */ -int xing_parse(struct xing *xing, struct mad_bitptr ptr, unsigned int bitlen) -{ - const unsigned XING_MAGIC = (('X' << 24) | ('i' << 16) | ('n' << 8) | 'g'); - const unsigned INFO_MAGIC = (('I' << 24) | ('n' << 16) | ('f' << 8) | 'o'); - unsigned data; - if (bitlen < 64) - goto fail; - data = mad_bit_read(&ptr, 32); - - if( data == XING_MAGIC ) - xing->type = xing::XING; - else if( data == INFO_MAGIC ) - xing->type = xing::INFO; - else - goto fail; - - xing->flags = mad_bit_read(&ptr, 32); - bitlen -= 64; - - if (xing->flags & XING_FRAMES) - { - if (bitlen < 32) - goto fail; - - xing->frames = mad_bit_read(&ptr, 32); - bitlen -= 32; - } - - if (xing->flags & XING_BYTES) - { - if (bitlen < 32) - goto fail; - - xing->bytes = mad_bit_read(&ptr, 32); - bitlen -= 32; - } - - if (xing->flags & XING_TOC) - { - if (bitlen < 800) - goto fail; - - for (int i = 0; i < 100; ++i) - xing->toc[i] = (unsigned char) mad_bit_read(&ptr, 8); - - bitlen -= 800; - } - - if (xing->flags & XING_SCALE) - { - if (bitlen < 32) - goto fail; - - xing->scale = mad_bit_read(&ptr, 32); - bitlen -= 32; - } - - return 0; - -fail: - xing->flags = 0; - return -1; -} - -/* end XING code ripped out of madplay */ - -/* - * Copyright (c) 2003-2004 Glenn Maynard - * Copyright (c) 2000-2003 Robert Leslie (Xing code from madplay) - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - */ - +/* MAD is available from: http://www.underbit.com/products/mad/ */ + +#include "global.h" +#include "RageSoundReader_MP3.h" +#include "RageLog.h" +#include "RageUtil.h" + +#include +#include +#include + +#if defined(_WINDOWS) || defined(MACOSX) +#include "../extern/mad-0.15.1b/mad.h" +#ifdef _MSC_VER +#pragma comment(lib, "libmad.lib") +#endif //_MSC_VER +#else +#include +#endif // _WINDOWS + +// ID3 code from libid3: +enum tagtype { + TAGTYPE_NONE = 0, + TAGTYPE_ID3V1, + TAGTYPE_ID3V2, + TAGTYPE_ID3V2_FOOTER +}; + +static const int ID3_TAG_FLAG_FOOTERPRESENT = 0x10; + +static tagtype tagtype( const unsigned char *data, id3_length_t length ) +{ + if (length >= 3 && + data[0] == 'T' && data[1] == 'A' && data[2] == 'G') + return TAGTYPE_ID3V1; + + if (length >= 10 && + ((data[0] == 'I' && data[1] == 'D' && data[2] == '3') || + (data[0] == '3' && data[1] == 'D' && data[2] == 'I')) && + data[3] < 0xff && data[4] < 0xff && + data[6] < 0x80 && data[7] < 0x80 && data[8] < 0x80 && data[9] < 0x80) + return data[0] == 'I' ? TAGTYPE_ID3V2 : TAGTYPE_ID3V2_FOOTER; + + return TAGTYPE_NONE; +} + +static unsigned long id3_parse_uint( const unsigned char **ptr, unsigned int bytes ) +{ + unsigned long value = 0; + + ASSERT(bytes >= 1 && bytes <= 4); + + switch (bytes) + { + case 4: value = (value << 8) | *(*ptr)++; + case 3: value = (value << 8) | *(*ptr)++; + case 2: value = (value << 8) | *(*ptr)++; + case 1: value = (value << 8) | *(*ptr)++; + } + + return value; +} + +static unsigned long id3_parse_syncsafe( const unsigned char **ptr, unsigned int bytes ) +{ + unsigned long value = 0; + + ASSERT(bytes == 4 || bytes == 5); + + switch (bytes) + { + case 5: + value = (value << 4) | (*(*ptr)++ & 0x0f); + case 4: + value = (value << 7) | (*(*ptr)++ & 0x7f); + value = (value << 7) | (*(*ptr)++ & 0x7f); + value = (value << 7) | (*(*ptr)++ & 0x7f); + value = (value << 7) | (*(*ptr)++ & 0x7f); + } + + return value; +} + + +static void parse_header(const unsigned char **ptr, + unsigned int *version, int *flags, id3_length_t *size) +{ + *ptr += 3; + + *version = id3_parse_uint(ptr, 2); + *flags = id3_parse_uint(ptr, 1); + *size = id3_parse_syncsafe(ptr, 4); +} + +/* + * NAME: tag->query() + * DESCRIPTION: if a tag begins at the given location, return its size + */ +signed long id3_tag_query( const unsigned char *data, id3_length_t length ) +{ + unsigned int version; + int flags; + id3_length_t size; + + switch (tagtype(data, length)) + { + case TAGTYPE_ID3V1: + return 128; + + case TAGTYPE_ID3V2: + parse_header(&data, &version, &flags, &size); + + if (flags & ID3_TAG_FLAG_FOOTERPRESENT) + size += 10; + + return 10 + size; + + case TAGTYPE_ID3V2_FOOTER: + parse_header(&data, &version, &flags, &size); + return -(int)size - 10; + + case TAGTYPE_NONE: + break; + } + + return 0; +} + + + + +/* XING code ripped out of madplay (header) + * + * mad - MPEG audio decoder + * Copyright (C) 2000-2001 Robert Leslie + */ +struct xing +{ + long flags; /* valid fields (see below) */ + unsigned long frames; /* total number of frames */ + unsigned long bytes; /* total number of bytes */ + unsigned char toc[100]; /* 100-point seek table */ + long scale; /* ?? */ + enum { XING, INFO } type; +}; + +enum { + XING_FRAMES = 0x00000001L, + XING_BYTES = 0x00000002L, + XING_TOC = 0x00000004L, + XING_SCALE = 0x00000008L +}; + +void xing_init( struct xing *xing ); +int xing_parse( struct xing *xing, struct mad_bitptr ptr, unsigned int bitlen ); + +/* end XING header */ + + +/* a -= b */ +static void mad_timer_sub(mad_timer_t *a, mad_timer_t b) +{ + /* a = a - b -> a = a + -b */ + mad_timer_negate(&b); + mad_timer_add(a, b); +} + + + + + + +/* internal->decoder_private field */ +struct madlib_t +{ + madlib_t() + { + outpos = 0; + outleft = 0; + memset( inbuf, 0, sizeof(inbuf) ); + memset( outbuf, 0, sizeof(outbuf) ); + memset( &Stream, 0, sizeof(Stream) ); + memset( &Frame, 0, sizeof(Frame) ); + memset( &Synth, 0, sizeof(Synth) ); + Timer = mad_timer_zero; + timer_accurate = false; + inbuf_filepos = 0; + filesize = 0; + header_bytes = 0; + first_frame = 0; + has_xing = 0; + memset( &xingtag, 0, sizeof(xingtag) ); + length = 0; + framelength = mad_timer_zero; + bitrate = 0; + } + + uint8_t inbuf[16384]; + float outbuf[8192]; + int outpos; + unsigned outleft; + + struct mad_stream Stream; + struct mad_frame Frame; + struct mad_synth Synth; + /* Timestamp of the next frame. */ + mad_timer_t Timer; + + /* Whether Timer is trusted; this is false after doing a quick seek. */ + int timer_accurate; + + /* + * Frame index of each percentage of the file. This is constructed + * as we read the file, so we can quickly seek back later. + */ + struct mad_timer_compare_lt + { + bool operator() ( const mad_timer_t &timer1, const mad_timer_t &timer2 ) const + { + return mad_timer_compare( timer1, timer2 ) < 0; + } + }; + + typedef map tocmap_t; + tocmap_t tocmap; + + /* Position in the file of inbuf: */ + int inbuf_filepos; + + /* File size. */ + int filesize; + + /* Number of bytes of header data at the beginning; used for seeking. */ + int header_bytes; + + /* Whether we've decoded the first frame of real audio yet. If this + * is false, we're somewhere before the first frame; we might be in + * the middle of headers. */ + int first_frame; + + /* This data is filled in when the first frame is decoded. */ + int has_xing; /* whether xingtag is valid */ + struct xing xingtag; + + /* If has_xing is true, this is filled in based on the xing header. + * If it's false, this is filled in based on the size of the file + * and the first frame. */ + int length; + mad_timer_t framelength; + + /* If we have a Xing tag, this is the average bitrate; otherwise it's + * the bitrate of the first frame. */ + int bitrate; +}; + + + + + + + + +static float scale(mad_fixed_t sample) +{ + return float( double(sample) / (1<inbuf_filepos; + + /* If we have a frame, adjust. */ + if( mad->Stream.this_frame != nullptr ) + ret += mad->Stream.this_frame-mad->inbuf; + + return ret; +} + + +/* Called on the first frame decoded. Returns true if this frame + * should be ignored. */ +bool RageSoundReader_MP3::handle_first_frame() +{ + bool ret = false; + + /* Check for a XING tag. */ + xing_init( &mad->xingtag ); + if( xing_parse(&mad->xingtag, mad->Stream.anc_ptr, mad->Stream.anc_bitlen) == 0 ) + { + /* + * "Info" tags are written by some tools. They're just Xing tags, but for + * CBR files. + * + * However, DWI's decoder, BASS, doesn't understand this, and treats it as a + * corrupt frame, outputting a frame of silence. Let's ignore the tag, so + * it'll be treated as an invalid frame, so we match DWI sync. + * + * The information in it isn't very useful to us. The TOC is less accurate + * then computing it ourself (low resolution). The file length computation + * might be wrong, if the tag is incorrect, and we can compute that accurately + * ourself for CBR files. + */ + if( mad->xingtag.type == xing::INFO ) + return false; + + mad->header_bytes = max( mad->header_bytes, get_this_frame_byte(mad) ); + + mad->has_xing = true; + + mad_timer_t tm = mad->Frame.header.duration; + /* XXX: does this include the Xing header itself? */ + mad_timer_multiply( &tm, mad->xingtag.frames ); + mad->length = mad_timer_count( tm, MAD_UNITS_MILLISECONDS ); + + /* XXX: an id3v2 footer tag would throw this off a little. This also assumes + * the Xing tag is the last header; it always is, I think. */ + int bytes = mad->filesize - mad->header_bytes; + mad->bitrate = (int)(bytes * 8 / (mad->length/1000.f)); + + if( mad->xingtag.type == xing::XING ) + ret = 1; + } + + /* If there's no Xing tag, mad->length will be filled in by _open. */ + + return ret; +} + + +int RageSoundReader_MP3::fill_buffer() +{ + /* Need more data. */ + int inbytes = 0; + if( mad->Stream.next_frame != nullptr ) + { + /* Pull out remaining data from the last buffer. */ + inbytes = mad->Stream.bufend-mad->Stream.next_frame; + memmove( mad->inbuf, mad->Stream.next_frame, inbytes ); + mad->inbuf_filepos += mad->Stream.next_frame - mad->inbuf; + } + + const bool bWasAtEOF = m_pFile->AtEOF(); + + int rc = m_pFile->Read( mad->inbuf + inbytes, sizeof(mad->inbuf)-inbytes-MAD_BUFFER_GUARD ); + if( rc < 0 ) + { + SetError( m_pFile->GetError() ); + return -1; + } + + if ( m_pFile->AtEOF() && !bWasAtEOF ) + { + /* We just reached EOF. Append MAD_BUFFER_GUARD bytes of NULs to the + * buffer, to ensure that the last frame is flushed. */ + memset( mad->inbuf + inbytes + rc, 0, MAD_BUFFER_GUARD ); + rc += MAD_BUFFER_GUARD; + } + + if ( rc == 0 ) + return 0; + + inbytes += rc; + mad_stream_buffer( &mad->Stream, mad->inbuf, inbytes ); + return rc; +} + +void fill_frame_index_cache( madlib_t *mad ) +{ + int pos; + + /* Only update the frame cache if our timer is consistent. */ + if(!mad->timer_accurate) return; + + /* the frame we just decoded: */ + pos = get_this_frame_byte(mad); + + /* Fill in the TOC percent. */ + if( !mad->tocmap.empty() ) + { + /* Don't add an entry if one already exists near this time. */ + madlib_t::tocmap_t::iterator it = mad->tocmap.upper_bound( mad->Timer ); + if( it != mad->tocmap.begin() ) + { + --it; + int iDiffSeconds = mad->Timer.seconds - it->first.seconds; + if( iDiffSeconds < 5 ) + return; + } + } + + mad->tocmap[mad->Timer] = pos; +} + +/* Handle first-stage decoding: extracting the MP3 frame data. */ +int RageSoundReader_MP3::do_mad_frame_decode( bool headers_only ) +{ + int bytes_read = 0; + + while(1) + { + int ret; + + /* Always actually decode the first packet, so we cleanly parse Xing tags. */ + if( headers_only && !mad->first_frame ) + ret=mad_header_decode( &mad->Frame.header,&mad->Stream ); + else + ret=mad_frame_decode( &mad->Frame,&mad->Stream ); + + if( ret == -1 && (mad->Stream.error == MAD_ERROR_BUFLEN || mad->Stream.error == MAD_ERROR_BUFPTR) ) + { + if( bytes_read > 25000 ) + { + /* We've read this much without actually getting a frame; error. */ + SetError( "Can't find data" ); + return -1; + } + + ret = fill_buffer(); + if( ret <= 0 ) + return ret; + bytes_read += ret; + + continue; + } + + if( ret == -1 && mad->Stream.error == MAD_ERROR_LOSTSYNC ) + { + /* This might be an ID3V2 tag. */ + const int tagsize = id3_tag_query(mad->Stream.this_frame, + mad->Stream.bufend - mad->Stream.this_frame); + + if( tagsize ) + { + mad_stream_skip(&mad->Stream, tagsize); + + /* Don't count the tagsize against the max-read-per-call figure. */ + bytes_read -= tagsize; + + continue; + } + } + + if( ret == -1 && mad->Stream.error == MAD_ERROR_BADDATAPTR ) + { + /* + * Something's corrupt. One cause of this is cutting an MP3 in the middle + * without reencoding; the first two frames will reference data from previous + * frames that have been removed. The frame is valid--we can get a header from + * it, we just can't synth useful data. + * + * BASS pretends the bad frames are silent. Emulate that, for compatibility. + */ + ret = 0; /* pretend success */ + } + + if( !ret ) + { + /* OK. */ + if( mad->first_frame ) + { + /* We're at the beginning. Is this a Xing tag? */ + if(handle_first_frame()) + { + /* The first frame contained a header. Continue searching. */ + continue; + } + + /* We've decoded the first frame of data. + * + * We want mad->Timer to represent the timestamp of the first sample of the + * currently decoded frame. Don't increment mad->Timer on the first frame, + * or it'll be the time of the *next* frame. (All frames have the same + * duration.) */ + mad->first_frame = false; + mad->Timer = mad_timer_zero; + mad->header_bytes = get_this_frame_byte(mad); + } + else + { + mad_timer_add( &mad->Timer,mad->Frame.header.duration ); + } + + fill_frame_index_cache( mad ); + + return 1; + } + + if( mad->Stream.error == MAD_ERROR_BADCRC ) + { + /* XXX untested */ + mad_frame_mute(&mad->Frame); + mad_synth_mute(&mad->Synth); + + continue; + } + + if( !MAD_RECOVERABLE(mad->Stream.error) ) + { + /* We've received an unrecoverable error. */ + SetError( mad_stream_errorstr(&mad->Stream) ); + return -1; + } + } +} + + +void RageSoundReader_MP3::synth_output() +{ + mad->outleft = mad->outpos = 0; + + if( MAD_NCHANNELS(&mad->Frame.header) != this->Channels ) + { + /* This frame contains a different number of channels than the first. + * I've never actually seen this--it's just to prevent exploding if + * it happens--and we could continue on easily enough, so if it happens, + * just continue. */ + return; + } + + mad_synth_frame(&mad->Synth, &mad->Frame); + for(int i=0; i < mad->Synth.pcm.length; i++) + { + for(int chan = 0; chan < this->Channels; ++chan) + { + float Sample = scale(mad->Synth.pcm.samples[chan][i]); + mad->outbuf[mad->outleft] = Sample; + ++mad->outleft; + } + } +} + +/* Seek to a byte in the file. If you're going to put the file position + * back when you're done, and not going to read any data, you don't have + * to use this. */ +int RageSoundReader_MP3::seek_stream_to_byte( int byte ) +{ + if( m_pFile->Seek(byte) == -1 ) + { + SetError( strerror(errno) ); + return 0; + } + + mad_frame_mute(&mad->Frame); + mad_synth_mute(&mad->Synth); + mad_stream_finish(&mad->Stream); + mad_stream_init(&mad->Stream); + + mad->outleft = mad->outpos = 0; + + mad->inbuf_filepos = byte; + + /* If the position is <= the position of the first audio sample, then + * we're at the beginning. */ + if( !mad->tocmap.empty() ) + mad->first_frame = ( byte <= mad->tocmap.begin()->second ); + + return 1; +} + + +#if 0 +/* Call this after seeking the stream. We'll back up a bit and reread + * frames until we're back where we started, so the next read is aligned + * to a frame and synced. This must never leave the position ahead of where + * it way, since that can confuse the seek optimizations. */ +int RageSoundReader_MP3::resync() +{ + /* Save the timer; decoding will change it, and we need to put it back. */ + mad_timer_t orig = mad->Timer; + + /* Seek backwards up to 4k. */ + const int origpos = mad->inbuf_filepos; + const int seekpos = max( 0, origpos - 1024*4 ); + seek_stream_to_byte( seekpos ); + + /* Agh. This is annoying. We want to decode enough so that the next frame + * read will be the first frame after the current file pointer. If we just + * read until the file pointer is >= what it was, we've passed it already. + * So, read until it's >= what it was, counting the number of times we had + * to read; then back up again and read n-1 times. Gross. */ + int reads = 0; + do + { + if( do_mad_frame_decode() <= 0 ) /* XXX eof */ + return -1; /* it set the error */ + + reads++; + } while( get_this_frame_byte(mad) < origpos ); + + seek_stream_to_byte( seekpos ); + + reads--; + while( reads-- > 0 ) + { + if( do_mad_frame_decode() <= 0 ) /* XXX eof */ + return -1; /* it set the error */ + } + + /* Restore the timer. */ + mad->Timer = orig; + mad->outpos = mad->outleft = 0; + + return 1; +} +#endif + +RageSoundReader_MP3::RageSoundReader_MP3() +{ + mad = new madlib_t; + m_bAccurateSync = false; + + mad_stream_init( &mad->Stream ); + mad_frame_init( &mad->Frame ); + mad_synth_init( &mad->Synth ); + + mad_frame_mute( &mad->Frame ); + mad_timer_reset( &mad->Timer ); + mad->length = -1; + mad->inbuf_filepos = 0; + mad->header_bytes = 0; + mad->has_xing = false; + mad->timer_accurate = 1; + mad->bitrate = -1; + mad->first_frame = true; +} + +RageSoundReader_MP3::~RageSoundReader_MP3() +{ + mad_synth_finish( &mad->Synth ); + mad_frame_finish( &mad->Frame ); + mad_stream_finish( &mad->Stream ); + + delete mad; +} + +RageSoundReader_FileReader::OpenResult RageSoundReader_MP3::Open( RageFileBasic *pFile ) +{ + m_pFile = pFile; + + mad->filesize = m_pFile->GetFileSize(); + ASSERT( mad->filesize != -1 ); + + /* Make sure we can decode at least one frame. This will also fill in header info. */ + mad->outpos = 0; + + int ret = do_mad_frame_decode(); + switch(ret) + { + case 0: + SetError( "Failed to read any data at all" ); + return OPEN_UNKNOWN_FILE_FORMAT; + case -1: + SetError( GetError() + " (not an MP3 stream?)" ); + return OPEN_UNKNOWN_FILE_FORMAT; + } + + /* Store the bitrate of the frame we just got. */ + if( mad->bitrate == -1 ) /* might have been set by a Xing tag */ + mad->bitrate = mad->Frame.header.bitrate; + + SampleRate = mad->Frame.header.samplerate; + mad->framelength = mad->Frame.header.duration; + this->Channels = MAD_NCHANNELS( &mad->Frame.header ); + + /* Since we've already decoded a frame, just synth it instead of rewinding + * the stream. */ + synth_output(); + + if(mad->length == -1) + { + /* If vbr and !xing, this is just an estimate. */ + int bps = mad->bitrate / 8; + float secs = (float)(mad->filesize - mad->header_bytes) / bps; + mad->length = (int)(secs * 1000.f); + } + + return OPEN_OK; +} + + +RageSoundReader_MP3 *RageSoundReader_MP3::Copy() const +{ + RageSoundReader_MP3 *ret = new RageSoundReader_MP3; + + ret->m_pFile = m_pFile->Copy(); + ret->m_pFile->Seek( 0 ); + ret->m_bAccurateSync = m_bAccurateSync; + ret->mad->filesize = mad->filesize; + ret->mad->bitrate = mad->bitrate; + ret->SampleRate = SampleRate; + ret->mad->framelength = mad->framelength; + ret->Channels = Channels; + ret->mad->length = mad->length; + +// int n = ret->do_mad_frame_decode(); +// ASSERT( n > 0 ); +// ret->synth_output(); + + return ret; +} + +int RageSoundReader_MP3::Read( float *buf, int iFrames ) +{ + int iFramesWritten = 0; + + while( iFrames > 0 ) + { + if( mad->outleft > 0 ) + { + int iFramesToCopy = min( iFrames, int(mad->outleft / GetNumChannels()) ); + const int iSamplesToCopy = iFramesToCopy * GetNumChannels(); + const int iBytesToCopy = iSamplesToCopy * sizeof(float); + + memcpy( buf, mad->outbuf + mad->outpos, iBytesToCopy ); + + buf += iSamplesToCopy; + iFrames -= iFramesToCopy; + iFramesWritten += iFramesToCopy; + mad->outpos += iSamplesToCopy; + mad->outleft -= iSamplesToCopy; + continue; + } + + /* Decode more from the MP3 stream. */ + int ret = do_mad_frame_decode(); + if( ret == 0 ) + return END_OF_FILE; + if( ret == -1 ) + return ERROR; + + synth_output(); + } + + return iFramesWritten; +} + +bool RageSoundReader_MP3::MADLIB_rewind() +{ + m_pFile->Seek(0); + + mad_frame_mute(&mad->Frame); + mad_synth_mute(&mad->Synth); + mad_timer_reset(&mad->Timer); + mad->outpos = mad->outleft = 0; + + mad_stream_finish(&mad->Stream); + mad_stream_init(&mad->Stream); + mad_stream_buffer(&mad->Stream, NULL, 0); + mad->inbuf_filepos = 0; + + /* Be careful. We need to leave header_bytes alone, so if we try to SetPosition_estimate + * immediately after this, we still know the header size. However, we need to set first_frame + * to true, since the first frame is handled specially in do_mad_frame_decode; if we don't + * set it, then we'll be desynced by a frame after an accurate seek. */ +// mad->header_bytes = 0; + mad->first_frame = true; + mad->Stream.this_frame = NULL; + + return true; +} + +/* Methods of seeking: + * + * 1. We can jump based on a TOC. We potentially have two; the Xing TOC and our + * own index. The Xing TOC is only accurate to 1/256th of the file size, + * so it's unsuitable for precise seeks. Our own TOC is byte-accurate. + * (SetPosition_toc) + * + * 2. We can jump based on the bitrate. This is fast, but not accurate. + * (SetPosition_estimate) + * + * 3. We can seek from any position to any higher position by decoding headers. + * (SetPosition_hard) + * + * Both 1 and 2 will leave the position behind the actual requested position; + * combine them with 3 to catch up. Never do 3 alone in "fast" mode, since it's + * slow if it ends up seeking from the beginning of the file. Never do 2 in + * "precise" mode. + */ + +/* Returns actual position on success, 0 if past EOF, -1 on error. */ +int RageSoundReader_MP3::SetPosition_toc( int iFrame, bool Xing ) +{ + ASSERT( !Xing || mad->has_xing ); + ASSERT( mad->length != -1 ); + + /* This leaves our timer accurate if we're using our own TOC, and inaccurate + * if we're using Xing. */ + mad->timer_accurate = !Xing; + + int bytepos = -1; + if( Xing ) + { + /* We can speed up the seek using the XING tag. First, figure + * out what percentage the requested position falls in. */ + ASSERT( SampleRate != 0 ); + int ms = int( (iFrame * 1000LL) / SampleRate ); + ASSERT( mad->length != 0 ); + int percent = ms * 100 / mad->length; + if( percent < 100 ) + { + int jump = mad->xingtag.toc[percent]; + bytepos = mad->filesize * jump / 256; + } + else + bytepos = 2000000000; /* force EOF */ + + mad_timer_set( &mad->Timer, 0, percent * mad->length, 100000 ); + } + else + { + mad_timer_t desired; + mad_timer_set( &desired, 0, iFrame, SampleRate ); + + if( mad->tocmap.empty() ) + return 1; /* don't have any info */ + + /* Find the last entry <= iFrame that we actually have an entry for; + * this will get us as close as possible. */ + madlib_t::tocmap_t::iterator it = mad->tocmap.upper_bound( desired ); + if( it == mad->tocmap.begin() ) + return 1; /* don't have any info */ + --it; + + mad->Timer = it->first; + bytepos = it->second; + } + + if( bytepos != -1 ) + { + /* Seek backwards up to 4k. */ + const int seekpos = max( 0, bytepos - 1024*4 ); + seek_stream_to_byte( seekpos ); + + do + { + int ret = do_mad_frame_decode(); + if( ret <= 0 ) + return ret; /* it set the error */ + } while( get_this_frame_byte(mad) < bytepos ); + synth_output(); + } + + return 1; +} + +int RageSoundReader_MP3::SetPosition_hard( int iFrame ) +{ + mad_timer_t desired; + mad_timer_set( &desired, 0, iFrame, mad->Frame.header.samplerate ); + + /* This seek doesn't change the accuracy of our timer. */ + + /* If we're already exactly at the requested position, OK. */ + if( mad_timer_compare(mad->Timer, desired) == 0 ) + return 1; + + /* We always come in here with data synthed. Be careful not to synth the + * same frame twice. */ + bool synthed=true; + + /* If we're already past the requested position, rewind. */ + if(mad_timer_compare(mad->Timer, desired) > 0) + { + MADLIB_rewind(); + do_mad_frame_decode(); + synthed = false; + } + + /* Decode frames until the current frame contains the desired offset. */ + while(1) + { + /* If desired < next_frame_timer, this frame contains the position. Since we've + * already decoded the frame, synth it, too. */ + mad_timer_t next_frame_timer = mad->Timer; + mad_timer_add( &next_frame_timer, mad->framelength ); + + if( mad_timer_compare(desired, next_frame_timer) < 0 ) + { + if( !synthed ) + { + synth_output(); + } + else + { + mad->outleft += mad->outpos; + mad->outpos = 0; + } + + /* We just synthed data starting at mad->Timer, containing the desired offset. + * Skip (desired - mad->Timer) worth of frames in the output to line up. */ + mad_timer_t skip = desired; + mad_timer_sub( &skip, mad->Timer ); + + int samples = mad_timer_count( skip, (mad_units) SampleRate ); + + /* Skip 'samples' samples. */ + mad->outpos = samples * this->Channels; + mad->outleft -= samples * this->Channels; + return 1; + } + + /* Otherwise, if the desired time will be in the *next* decode, then synth + * this one, too. */ + mad_timer_t next_next_frame_timer = next_frame_timer; + mad_timer_add( &next_next_frame_timer, mad->framelength ); + + if( mad_timer_compare(desired, next_next_frame_timer) < 0 && !synthed ) + { + synth_output(); + synthed = true; + } + + int ret = do_mad_frame_decode(); + if( ret <= 0 ) + { + mad->outleft = mad->outpos = 0; + return ret; /* it set the error */ + } + + synthed = false; + } +} + +/* Do a seek based on the bitrate. */ +int RageSoundReader_MP3::SetPosition_estimate( int iFrame ) +{ + /* This doesn't leave us accurate. */ + mad->timer_accurate = 0; + + mad_timer_t seekamt; + mad_timer_set( &seekamt, 0, iFrame, mad->Frame.header.samplerate ); + { + /* We're going to skip ahead two samples below, so seek earlier than + * we were asked to. */ + mad_timer_t back_len = mad->framelength; + mad_timer_multiply(&back_len, -2); + mad_timer_add(&seekamt, back_len); + if( mad_timer_compare(seekamt, mad_timer_zero) < 0 ) + seekamt = mad_timer_zero; + } + + int seekpos = mad_timer_count( seekamt, MAD_UNITS_MILLISECONDS ) * (mad->bitrate / 8 / 1000); + seekpos += mad->header_bytes; + seek_stream_to_byte( seekpos ); + + /* We've jumped across the file, so the decoder is currently desynced. + * Don't use resync(); it's slow. Just decode a few frames. */ + for( int i = 0; i < 2; ++i ) + { + int ret = do_mad_frame_decode(); + if( ret <= 0 ) + return ret; + } + + /* Throw out one synth. */ + synth_output(); + mad->outleft = 0; + + /* Find out where we really seeked to. */ + int ms = (get_this_frame_byte(mad) - mad->header_bytes) / (mad->bitrate / 8 / 1000); + mad_timer_set(&mad->Timer, 0, ms, 1000); + + return 1; +} + +int RageSoundReader_MP3::SetPosition( int iFrame ) +{ + if( m_bAccurateSync ) + { + /* Seek using our own internal (accurate) TOC. */ + int ret = SetPosition_toc( iFrame, false ); + if( ret <= 0 ) + return ret; /* it set the error */ + + /* Align exactly. */ + return SetPosition_hard( iFrame ); + } + else + { + /* Rewinding is always fast and accurate, and SetPosition_estimate is bad at 0. */ + if( !iFrame ) + { + MADLIB_rewind(); + return 1; /* ok */ + } + + /* We can do a fast jump in VBR with Xing with more accuracy than without Xing. */ + if( mad->has_xing ) + return SetPosition_toc( iFrame, true ); + + /* Guess. This is only remotely accurate when we're not VBR, but also + * do it if we have no Xing tag. */ + return SetPosition_estimate( iFrame ); + } +} + +bool RageSoundReader_MP3::SetProperty( const RString &sProperty, float fValue ) +{ + if( sProperty == "AccurateSync" ) + { + m_bAccurateSync = (fValue > 0.001f); + return true; + } + + return RageSoundReader_FileReader::SetProperty( sProperty, fValue ); +} + +int RageSoundReader_MP3::GetNextSourceFrame() const +{ + int iFrame = mad_timer_count( mad->Timer, mad_units(mad->Frame.header.samplerate) ); + iFrame += mad->outpos / this->Channels; + return iFrame; +} + +int RageSoundReader_MP3::GetLengthInternal( bool fast ) +{ + if( mad->has_xing && mad->length != -1 ) + return mad->length; /* should be accurate */ + + /* Check to see if a frame in the middle of the file is the same + * bitrate as the first frame. If it is, assume the file is really CBR. */ + seek_stream_to_byte( mad->filesize / 2 ); + + /* XXX use mad_header_decode and check more than one frame */ + if(mad->length != -1 && + do_mad_frame_decode() && + mad->bitrate == (int) mad->Frame.header.bitrate) + { + return mad->length; + } + + if( !MADLIB_rewind() ) + return 0; + + /* Worst-case: vbr && !xing. We've made a guess at the length, but let's actually + * scan the size, since the guess is probably wrong. */ + if( fast ) + { + SetError("Can't estimate file length"); + return -1; + } + + MADLIB_rewind(); + while(1) + { + int ret = do_mad_frame_decode( true ); + if( ret == -1 ) + return -1; /* it set the error */ + if( ret == 0 ) /* EOF */ + break; + } + + /* mad->Timer is the timestamp of the current frame; find the timestamp of + * the very end. */ + mad_timer_t end = mad->Timer; + mad_timer_add( &end, mad->framelength ); + + /* Count milliseconds. */ + return mad_timer_count( end, MAD_UNITS_MILLISECONDS ); +} + +int RageSoundReader_MP3::GetLengthConst( bool fast ) const +{ + RageSoundReader_MP3 *pCopy = this->Copy(); + + int iLength = pCopy->GetLengthInternal( fast ); + + delete pCopy; + return iLength; +} + + + + + + +/* begin XING code ripped out of madplay */ + +/* + * NAME: xing->init() + * DESCRIPTION: initialize Xing structure + */ +void xing_init(struct xing *xing) +{ + xing->flags = 0; +} + +/* + * NAME: xing->parse() + * DESCRIPTION: parse a Xing VBR header + */ +int xing_parse(struct xing *xing, struct mad_bitptr ptr, unsigned int bitlen) +{ + const unsigned XING_MAGIC = (('X' << 24) | ('i' << 16) | ('n' << 8) | 'g'); + const unsigned INFO_MAGIC = (('I' << 24) | ('n' << 16) | ('f' << 8) | 'o'); + unsigned data; + if (bitlen < 64) + goto fail; + data = mad_bit_read(&ptr, 32); + + if( data == XING_MAGIC ) + xing->type = xing::XING; + else if( data == INFO_MAGIC ) + xing->type = xing::INFO; + else + goto fail; + + xing->flags = mad_bit_read(&ptr, 32); + bitlen -= 64; + + if (xing->flags & XING_FRAMES) + { + if (bitlen < 32) + goto fail; + + xing->frames = mad_bit_read(&ptr, 32); + bitlen -= 32; + } + + if (xing->flags & XING_BYTES) + { + if (bitlen < 32) + goto fail; + + xing->bytes = mad_bit_read(&ptr, 32); + bitlen -= 32; + } + + if (xing->flags & XING_TOC) + { + if (bitlen < 800) + goto fail; + + for (int i = 0; i < 100; ++i) + xing->toc[i] = (unsigned char) mad_bit_read(&ptr, 8); + + bitlen -= 800; + } + + if (xing->flags & XING_SCALE) + { + if (bitlen < 32) + goto fail; + + xing->scale = mad_bit_read(&ptr, 32); + bitlen -= 32; + } + + return 0; + +fail: + xing->flags = 0; + return -1; +} + +/* end XING code ripped out of madplay */ + +/* + * Copyright (c) 2003-2004 Glenn Maynard + * Copyright (c) 2000-2003 Robert Leslie (Xing code from madplay) + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + diff --git a/src/RageSoundReader_Preload.cpp b/src/RageSoundReader_Preload.cpp index ff7e659bf1..d11b395f9a 100644 --- a/src/RageSoundReader_Preload.cpp +++ b/src/RageSoundReader_Preload.cpp @@ -1,191 +1,191 @@ -/* This reader simply precaches all of the data from another reader. This - * reduces CPU usage for sounds that are played several times at once. */ - -#include "global.h" -#include "RageSoundReader_Preload.h" -#include "RageUtil.h" -#include "RageSoundUtil.h" -#include "Preference.h" - -/* If true, preloaded sounds are stored in 16-bit instead of floats. Most - * processing happens after preloading, and it's usually a waste to store high- - * resolution data for sound effects. */ -Preference g_bSoundPreload16bit( "SoundPreload16bit", true ); - -/* If a sound is smaller than this, we'll load it entirely into memory. */ -Preference g_iSoundPreloadMaxSamples( "SoundPreloadMaxSamples", 1024*1024 ); - -#define samplesize (m_bBufferIs16Bit? sizeof(int16_t):sizeof(float)) -#define framesize (samplesize * m_iChannels) - -bool RageSoundReader_Preload::PreloadSound( RageSoundReader *&pSound ) -{ - RageSoundReader_Preload *pPreload = new RageSoundReader_Preload; - if( !pPreload->Open(pSound) ) - { - /* Preload failed. It read some data, so we need to rewind the reader. */ - pSound->SetPosition( 0 ); - delete pPreload; - return false; - } - - pSound = pPreload; - return true; -} - -RageSoundReader_Preload::RageSoundReader_Preload(): - m_Buffer( new RString ), m_bBufferIs16Bit(false), - m_iPosition(0), m_iSampleRate(0), m_iChannels(0), m_fRate(0.0f) -{ - m_bBufferIs16Bit = g_bSoundPreload16bit.Get(); -} - -int RageSoundReader_Preload::GetTotalFrames() const -{ - return m_Buffer->size() / framesize; -} - -bool RageSoundReader_Preload::Open( RageSoundReader *pSource ) -{ - ASSERT( pSource != NULL ); - m_iSampleRate = pSource->GetSampleRate(); - m_iChannels = pSource->GetNumChannels(); - m_fRate = pSource->GetStreamToSourceRatio(); - - int iMaxSamples = g_iSoundPreloadMaxSamples.Get(); - - /* Check the length, and see if we think it'll fit in the buffer. */ - int iLen = pSource->GetLength_Fast(); - if( iLen != -1 ) - { - float fSecs = iLen / 1000.f; - - int iFrames = lrintf( fSecs * m_iSampleRate ); /* seconds -> frames */ - int iSamples = unsigned( iFrames * m_iChannels ); /* frames -> samples */ - if( iSamples > iMaxSamples ) - return false; /* Don't bother trying to preload it. */ - - int iBytes = unsigned( iSamples * samplesize ); /* samples -> bytes */ - m_Buffer.Get()->reserve( iBytes ); - } - - while(1) - { - /* If the rate changes, we won't preload it. */ - if( pSource->GetStreamToSourceRatio() != m_fRate ) - return false; /* Don't bother trying to preload it. */ - - float buffer[1024]; - int iCnt = pSource->Read( buffer, ARRAYLEN(buffer) / m_iChannels ); - - if( iCnt == END_OF_FILE ) - break; - if( iCnt < 0 ) - return false; - - /* Add the buffer. */ - if( m_bBufferIs16Bit ) - { - int16_t buffer16[1024]; - RageSoundUtil::ConvertFloatToNativeInt16( buffer, buffer16, iCnt*m_iChannels ); - m_Buffer.Get()->append( (char *) buffer16, (char *) (buffer16+iCnt*m_iChannels) ); - } - else - { - m_Buffer.Get()->append( (char *) buffer, (char *) (buffer+iCnt*m_iChannels) ); - } - - if( m_Buffer.Get()->size() > iMaxSamples * samplesize ) - return false; /* too big */ - } - - m_iPosition = 0; - delete pSource; - return true; -} - -int RageSoundReader_Preload::GetLength() const -{ - return int(float(GetTotalFrames()) * 1000.f / m_iSampleRate); -} - -int RageSoundReader_Preload::GetLength_Fast() const -{ - return GetLength(); -} - -int RageSoundReader_Preload::SetPosition( int iFrame ) -{ - m_iPosition = iFrame; - m_iPosition = lrintf(m_iPosition / m_fRate); - - if( m_iPosition >= int(m_Buffer->size() / framesize) ) - { - m_iPosition = m_Buffer->size() / framesize; - return 0; - } - - return 1; -} - -int RageSoundReader_Preload::GetNextSourceFrame() const -{ - return lrintf(m_iPosition * m_fRate); -} - -int RageSoundReader_Preload::Read( float *pBuffer, int iFrames ) -{ - const int iSizeFrames = m_Buffer->size() / framesize; - const int iFramesAvail = iSizeFrames - m_iPosition; - - iFrames = min( iFrames, iFramesAvail ); - if( iFrames == 0 ) - return END_OF_FILE; - if( m_bBufferIs16Bit ) - { - const int16_t *pIn = (const int16_t *) (m_Buffer->data() + (m_iPosition * framesize)); - RageSoundUtil::ConvertNativeInt16ToFloat( pIn, pBuffer, iFrames * m_iChannels ); - } - else - { - memcpy( pBuffer, m_Buffer->data() + (m_iPosition * framesize), iFrames * framesize ); - } - m_iPosition += iFrames; - - return iFrames; -} - -RageSoundReader_Preload *RageSoundReader_Preload::Copy() const -{ - return new RageSoundReader_Preload(*this); -} - -int RageSoundReader_Preload::GetReferenceCount() const -{ - return m_Buffer.GetReferenceCount(); -} - -/* - * Copyright (c) 2003 Glenn Maynard - * 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. - */ +/* This reader simply precaches all of the data from another reader. This + * reduces CPU usage for sounds that are played several times at once. */ + +#include "global.h" +#include "RageSoundReader_Preload.h" +#include "RageUtil.h" +#include "RageSoundUtil.h" +#include "Preference.h" + +/* If true, preloaded sounds are stored in 16-bit instead of floats. Most + * processing happens after preloading, and it's usually a waste to store high- + * resolution data for sound effects. */ +Preference g_bSoundPreload16bit( "SoundPreload16bit", true ); + +/* If a sound is smaller than this, we'll load it entirely into memory. */ +Preference g_iSoundPreloadMaxSamples( "SoundPreloadMaxSamples", 1024*1024 ); + +#define samplesize (m_bBufferIs16Bit? sizeof(int16_t):sizeof(float)) +#define framesize (samplesize * m_iChannels) + +bool RageSoundReader_Preload::PreloadSound( RageSoundReader *&pSound ) +{ + RageSoundReader_Preload *pPreload = new RageSoundReader_Preload; + if( !pPreload->Open(pSound) ) + { + /* Preload failed. It read some data, so we need to rewind the reader. */ + pSound->SetPosition( 0 ); + delete pPreload; + return false; + } + + pSound = pPreload; + return true; +} + +RageSoundReader_Preload::RageSoundReader_Preload(): + m_Buffer( new RString ), m_bBufferIs16Bit(false), + m_iPosition(0), m_iSampleRate(0), m_iChannels(0), m_fRate(0.0f) +{ + m_bBufferIs16Bit = g_bSoundPreload16bit.Get(); +} + +int RageSoundReader_Preload::GetTotalFrames() const +{ + return m_Buffer->size() / framesize; +} + +bool RageSoundReader_Preload::Open( RageSoundReader *pSource ) +{ + ASSERT( pSource != nullptr ); + m_iSampleRate = pSource->GetSampleRate(); + m_iChannels = pSource->GetNumChannels(); + m_fRate = pSource->GetStreamToSourceRatio(); + + int iMaxSamples = g_iSoundPreloadMaxSamples.Get(); + + /* Check the length, and see if we think it'll fit in the buffer. */ + int iLen = pSource->GetLength_Fast(); + if( iLen != -1 ) + { + float fSecs = iLen / 1000.f; + + int iFrames = lrintf( fSecs * m_iSampleRate ); /* seconds -> frames */ + int iSamples = unsigned( iFrames * m_iChannels ); /* frames -> samples */ + if( iSamples > iMaxSamples ) + return false; /* Don't bother trying to preload it. */ + + int iBytes = unsigned( iSamples * samplesize ); /* samples -> bytes */ + m_Buffer.Get()->reserve( iBytes ); + } + + while(1) + { + /* If the rate changes, we won't preload it. */ + if( pSource->GetStreamToSourceRatio() != m_fRate ) + return false; /* Don't bother trying to preload it. */ + + float buffer[1024]; + int iCnt = pSource->Read( buffer, ARRAYLEN(buffer) / m_iChannels ); + + if( iCnt == END_OF_FILE ) + break; + if( iCnt < 0 ) + return false; + + /* Add the buffer. */ + if( m_bBufferIs16Bit ) + { + int16_t buffer16[1024]; + RageSoundUtil::ConvertFloatToNativeInt16( buffer, buffer16, iCnt*m_iChannels ); + m_Buffer.Get()->append( (char *) buffer16, (char *) (buffer16+iCnt*m_iChannels) ); + } + else + { + m_Buffer.Get()->append( (char *) buffer, (char *) (buffer+iCnt*m_iChannels) ); + } + + if( m_Buffer.Get()->size() > iMaxSamples * samplesize ) + return false; /* too big */ + } + + m_iPosition = 0; + delete pSource; + return true; +} + +int RageSoundReader_Preload::GetLength() const +{ + return int(float(GetTotalFrames()) * 1000.f / m_iSampleRate); +} + +int RageSoundReader_Preload::GetLength_Fast() const +{ + return GetLength(); +} + +int RageSoundReader_Preload::SetPosition( int iFrame ) +{ + m_iPosition = iFrame; + m_iPosition = lrintf(m_iPosition / m_fRate); + + if( m_iPosition >= int(m_Buffer->size() / framesize) ) + { + m_iPosition = m_Buffer->size() / framesize; + return 0; + } + + return 1; +} + +int RageSoundReader_Preload::GetNextSourceFrame() const +{ + return lrintf(m_iPosition * m_fRate); +} + +int RageSoundReader_Preload::Read( float *pBuffer, int iFrames ) +{ + const int iSizeFrames = m_Buffer->size() / framesize; + const int iFramesAvail = iSizeFrames - m_iPosition; + + iFrames = min( iFrames, iFramesAvail ); + if( iFrames == 0 ) + return END_OF_FILE; + if( m_bBufferIs16Bit ) + { + const int16_t *pIn = (const int16_t *) (m_Buffer->data() + (m_iPosition * framesize)); + RageSoundUtil::ConvertNativeInt16ToFloat( pIn, pBuffer, iFrames * m_iChannels ); + } + else + { + memcpy( pBuffer, m_Buffer->data() + (m_iPosition * framesize), iFrames * framesize ); + } + m_iPosition += iFrames; + + return iFrames; +} + +RageSoundReader_Preload *RageSoundReader_Preload::Copy() const +{ + return new RageSoundReader_Preload(*this); +} + +int RageSoundReader_Preload::GetReferenceCount() const +{ + return m_Buffer.GetReferenceCount(); +} + +/* + * Copyright (c) 2003 Glenn Maynard + * 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. + */ diff --git a/src/RageSoundReader_Resample_Good.cpp b/src/RageSoundReader_Resample_Good.cpp index ecc176c108..4b90c97058 100644 --- a/src/RageSoundReader_Resample_Good.cpp +++ b/src/RageSoundReader_Resample_Good.cpp @@ -667,7 +667,7 @@ int RageSoundReader_Resample_Good::Read( float *pBuf, int iFrames ) { int iFramesNeeded = m_apResamplers[0]->NumInputsForOutputSamples(iFrames); float *pTmpBuf = (float *) alloca( iFramesNeeded * sizeof(float) * iChannels ); - ASSERT( pTmpBuf != NULL ); + ASSERT( pTmpBuf != nullptr ); int iFramesIn = m_pSource->Read( pTmpBuf, iFramesNeeded ); if( iFramesIn < 0 ) return iFramesIn; diff --git a/src/RageSoundReader_Vorbisfile.cpp b/src/RageSoundReader_Vorbisfile.cpp index 6b60ab34bc..b410d3df49 100644 --- a/src/RageSoundReader_Vorbisfile.cpp +++ b/src/RageSoundReader_Vorbisfile.cpp @@ -1,330 +1,330 @@ -#include "global.h" - -#include "RageUtil.h" -#include "RageSoundReader_Vorbisfile.h" -#include "RageLog.h" - -#if defined(INTEGER_VORBIS) -#include -#else -#include <../extern/vorbis/vorbis/vorbisfile.h> -#endif - -#if defined(_MSC_VER) -#pragma comment(lib, OGG_LIB_DIR "ogg_static.lib") -#pragma comment(lib, OGG_LIB_DIR "vorbis_static.lib") -#pragma comment(lib, OGG_LIB_DIR "vorbisfile_static.lib") -#endif // _MSC_VER - -#include -#include -#include "RageFile.h" -static size_t OggRageFile_read_func( void *ptr, size_t size, size_t nmemb, void *datasource ) -{ - RageFileBasic *f = (RageFileBasic *) datasource; - return f->Read( ptr, size, nmemb ); -} - -static int OggRageFile_seek_func( void *datasource, ogg_int64_t offset, int whence ) -{ - RageFileBasic *f = (RageFileBasic *) datasource; - return f->Seek( (int) offset, whence ); -} - -static int OggRageFile_close_func( void *datasource ) -{ - return 0; -} - -static long OggRageFile_tell_func( void *datasource ) -{ - RageFileBasic *f = (RageFileBasic *) datasource; - return f->Tell(); -} - -static RString ov_ssprintf( int err, const char *fmt, ...) -{ - va_list va; - va_start( va, fmt ); - RString s = vssprintf( fmt, va ); - va_end( va ); - - RString errstr; - switch( err ) - { - /* XXX: In the case of OV_EREAD, can we snoop at errno? */ - case OV_EREAD: errstr = "Read error"; break; - case OV_EFAULT: errstr = "Internal error"; break; - case OV_EIMPL: errstr = "Feature not implemented"; break; - case OV_EINVAL: errstr = "Invalid argument"; break; - case OV_ENOTVORBIS: errstr = "Not Vorbis data"; break; - case OV_EBADHEADER: errstr = "Invalid Vorbis bitstream header"; break; - case OV_EVERSION: errstr = "Vorbis version mismatch"; break; - case OV_ENOTAUDIO: errstr = "OV_ENOTAUDIO"; break; - case OV_EBADPACKET: errstr = "OV_EBADPACKET"; break; - case OV_EBADLINK: errstr = "Link corrupted"; break; - case OV_ENOSEEK: errstr = "Stream is not seekable"; break; - default: errstr = ssprintf( "unknown error %i", err ); break; - } - - return s + ssprintf( " (%s)", errstr.c_str() ); -} - -RageSoundReader_FileReader::OpenResult RageSoundReader_Vorbisfile::Open( RageFileBasic *pFile ) -{ - m_pFile = pFile; - vf = new OggVorbis_File; - memset( vf, 0, sizeof(*vf) ); - - ov_callbacks callbacks; - callbacks.read_func = OggRageFile_read_func; - callbacks.seek_func = OggRageFile_seek_func; - callbacks.close_func = OggRageFile_close_func; - callbacks.tell_func = OggRageFile_tell_func; - - int ret = ov_open_callbacks( pFile, vf, NULL, 0, callbacks ); - if( ret < 0 ) - { - SetError( ov_ssprintf(ret, "ov_open failed") ); - delete vf; - vf = NULL; - switch( ret ) - { - case OV_ENOTVORBIS: - return OPEN_UNKNOWN_FILE_FORMAT; - default: - return OPEN_FATAL_ERROR; - } - } - - eof = false; - read_offset = (int) ov_pcm_tell(vf); - - vorbis_info *vi = ov_info( vf, -1 ); - channels = vi->channels; - - return OPEN_OK; -} - -int RageSoundReader_Vorbisfile::GetLength() const -{ -#if defined(INTEGER_VORBIS) - int len = ov_time_total(vf, -1); -#else - int len = int(ov_time_total(vf, -1) * 1000); -#endif - if( len == OV_EINVAL ) - RageException::Throw( "RageSoundReader_Vorbisfile::GetLength: ov_time_total returned OV_EINVAL." ); - - return len; -} - -int RageSoundReader_Vorbisfile::SetPosition( int iFrame ) -{ - eof = false; - - const ogg_int64_t sample = ogg_int64_t(iFrame); - - int ret = ov_pcm_seek( vf, sample ); - if(ret < 0) - { - /* Returns OV_EINVAL on EOF. */ - if( ret == OV_EINVAL ) - { - eof = true; - return 0; - } - SetError( ov_ssprintf(ret, "ogg: SetPosition failed") ); - return -1; - } - read_offset = (int) ov_pcm_tell(vf); - - return 1; -} - -int RageSoundReader_Vorbisfile::Read( float *buf, int iFrames ) -{ - int frames_read = 0; - - while( iFrames && !eof ) - { - const int bytes_per_frame = sizeof(float)*channels; - - int iFramesRead = 0; - - { - int curofs = (int) ov_pcm_tell(vf); - if( curofs < read_offset ) - { - /* The timestamps moved backwards. Ignore it. This file probably - * won't sync correctly. */ - LOG->Trace( "p ahead %p %i < %i, we're ahead by %i", - this, curofs, read_offset, read_offset-curofs ); - read_offset = curofs; - } - else if( curofs > read_offset ) - { - /* Our offset doesn't match. We have a hole in the data, or corruption. - * If we're reading with accurate syncing, insert silence to line it up. - * That way, corruptions in the file won't casue desyncs. */ - - /* In bytes: */ - int iSilentFrames = curofs - read_offset; - iSilentFrames = min( iSilentFrames, (int) iFrames ); - int silence = iSilentFrames * bytes_per_frame; - CHECKPOINT_M( ssprintf("p %i,%i: %i frames of silence needed", curofs, read_offset, silence) ); - - memset( buf, 0, silence ); - iFramesRead = iSilentFrames; - } - } - - if( iFramesRead == 0 ) - { - int bstream; -#if defined(INTEGER_VORBIS) - int ret = ov_read( vf, (char *) buf, iFrames * channels * sizeof(int16_t), &bstream ); -#else // float vorbis decoder - float **pcm; - int ret = ov_read_float( vf, &pcm, iFrames, &bstream ); -#endif - - { - vorbis_info *vi = ov_info( vf, -1 ); - ASSERT( vi != NULL ); - - if( (unsigned) vi->channels != channels ) - RageException::Throw( "File \"%s\" changes channel count from %i to %i; not supported.", - filename.c_str(), channels, (int)vi->channels ); - } - - - if( ret == OV_HOLE ) - continue; - if( ret == OV_EBADLINK ) - { - SetError( ssprintf("Read: OV_EBADLINK") ); - return ERROR; - } - - if( ret == 0 ) - { - eof = true; - continue; - } - -#if defined(INTEGER_VORBIS) - if( ret > 0 ) - { - int iSamplesRead = ret / sizeof(int16_t); - iFramesRead = iSamplesRead / channels; - - /* Convert in reverse, so we can do it in-place. */ - const int16_t *pIn = (int16_t *) buf; - float *pOut = (float *) buf; - for( int i = iSamplesRead-1; i >= 0; --i ) - pOut[i] = pIn[i] / 32768.0f; - } -#else - if( ret > 0 ) - { - iFramesRead = ret; - - int iNumChannels = channels; - for( int iChannel = 0; iChannel < iNumChannels; ++iChannel ) - { - const float *pChannelIn = pcm[iChannel]; - float *pChannelOut = &buf[iChannel]; - for( int i = 0; i < iFramesRead; ++i ) - { - *pChannelOut = *pChannelIn; - ++pChannelIn; - pChannelOut += iNumChannels; - } - } - } -#endif - } - - read_offset += iFramesRead; - - buf += iFramesRead * channels; - frames_read += iFramesRead; - iFrames -= iFramesRead; - } - - if( !frames_read ) - return END_OF_FILE; - - return frames_read; -} - -int RageSoundReader_Vorbisfile::GetSampleRate() const -{ - ASSERT(vf != NULL); - - vorbis_info *vi = ov_info(vf, -1); - ASSERT(vi != NULL); - - return vi->rate; -} - -int RageSoundReader_Vorbisfile::GetNextSourceFrame() const -{ - ASSERT(vf != NULL); - - int iFrame = (int)ov_pcm_tell( vf ); - return iFrame; -} - -RageSoundReader_Vorbisfile::RageSoundReader_Vorbisfile() -{ - vf = NULL; -} - -RageSoundReader_Vorbisfile::~RageSoundReader_Vorbisfile() -{ - if(vf) - ov_clear(vf); - delete vf; -} - -RageSoundReader_Vorbisfile *RageSoundReader_Vorbisfile::Copy() const -{ - RageFileBasic *pFile = m_pFile->Copy(); - pFile->Seek(0); - RageSoundReader_Vorbisfile *ret = new RageSoundReader_Vorbisfile; - - /* If we were able to open the sound in the first place, we expect to - * be able to reopen it. */ - if( ret->Open(pFile) != OPEN_OK ) - FAIL_M( ssprintf("Copying sound failed: %s", ret->GetError().c_str()) ); - - return ret; -} - -/* - * Copyright (c) 2003 Glenn Maynard - * 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. - */ - +#include "global.h" + +#include "RageUtil.h" +#include "RageSoundReader_Vorbisfile.h" +#include "RageLog.h" + +#if defined(INTEGER_VORBIS) +#include +#else +#include <../extern/vorbis/vorbis/vorbisfile.h> +#endif + +#if defined(_MSC_VER) +#pragma comment(lib, OGG_LIB_DIR "ogg_static.lib") +#pragma comment(lib, OGG_LIB_DIR "vorbis_static.lib") +#pragma comment(lib, OGG_LIB_DIR "vorbisfile_static.lib") +#endif // _MSC_VER + +#include +#include +#include "RageFile.h" +static size_t OggRageFile_read_func( void *ptr, size_t size, size_t nmemb, void *datasource ) +{ + RageFileBasic *f = (RageFileBasic *) datasource; + return f->Read( ptr, size, nmemb ); +} + +static int OggRageFile_seek_func( void *datasource, ogg_int64_t offset, int whence ) +{ + RageFileBasic *f = (RageFileBasic *) datasource; + return f->Seek( (int) offset, whence ); +} + +static int OggRageFile_close_func( void *datasource ) +{ + return 0; +} + +static long OggRageFile_tell_func( void *datasource ) +{ + RageFileBasic *f = (RageFileBasic *) datasource; + return f->Tell(); +} + +static RString ov_ssprintf( int err, const char *fmt, ...) +{ + va_list va; + va_start( va, fmt ); + RString s = vssprintf( fmt, va ); + va_end( va ); + + RString errstr; + switch( err ) + { + /* XXX: In the case of OV_EREAD, can we snoop at errno? */ + case OV_EREAD: errstr = "Read error"; break; + case OV_EFAULT: errstr = "Internal error"; break; + case OV_EIMPL: errstr = "Feature not implemented"; break; + case OV_EINVAL: errstr = "Invalid argument"; break; + case OV_ENOTVORBIS: errstr = "Not Vorbis data"; break; + case OV_EBADHEADER: errstr = "Invalid Vorbis bitstream header"; break; + case OV_EVERSION: errstr = "Vorbis version mismatch"; break; + case OV_ENOTAUDIO: errstr = "OV_ENOTAUDIO"; break; + case OV_EBADPACKET: errstr = "OV_EBADPACKET"; break; + case OV_EBADLINK: errstr = "Link corrupted"; break; + case OV_ENOSEEK: errstr = "Stream is not seekable"; break; + default: errstr = ssprintf( "unknown error %i", err ); break; + } + + return s + ssprintf( " (%s)", errstr.c_str() ); +} + +RageSoundReader_FileReader::OpenResult RageSoundReader_Vorbisfile::Open( RageFileBasic *pFile ) +{ + m_pFile = pFile; + vf = new OggVorbis_File; + memset( vf, 0, sizeof(*vf) ); + + ov_callbacks callbacks; + callbacks.read_func = OggRageFile_read_func; + callbacks.seek_func = OggRageFile_seek_func; + callbacks.close_func = OggRageFile_close_func; + callbacks.tell_func = OggRageFile_tell_func; + + int ret = ov_open_callbacks( pFile, vf, NULL, 0, callbacks ); + if( ret < 0 ) + { + SetError( ov_ssprintf(ret, "ov_open failed") ); + delete vf; + vf = NULL; + switch( ret ) + { + case OV_ENOTVORBIS: + return OPEN_UNKNOWN_FILE_FORMAT; + default: + return OPEN_FATAL_ERROR; + } + } + + eof = false; + read_offset = (int) ov_pcm_tell(vf); + + vorbis_info *vi = ov_info( vf, -1 ); + channels = vi->channels; + + return OPEN_OK; +} + +int RageSoundReader_Vorbisfile::GetLength() const +{ +#if defined(INTEGER_VORBIS) + int len = ov_time_total(vf, -1); +#else + int len = int(ov_time_total(vf, -1) * 1000); +#endif + if( len == OV_EINVAL ) + RageException::Throw( "RageSoundReader_Vorbisfile::GetLength: ov_time_total returned OV_EINVAL." ); + + return len; +} + +int RageSoundReader_Vorbisfile::SetPosition( int iFrame ) +{ + eof = false; + + const ogg_int64_t sample = ogg_int64_t(iFrame); + + int ret = ov_pcm_seek( vf, sample ); + if(ret < 0) + { + /* Returns OV_EINVAL on EOF. */ + if( ret == OV_EINVAL ) + { + eof = true; + return 0; + } + SetError( ov_ssprintf(ret, "ogg: SetPosition failed") ); + return -1; + } + read_offset = (int) ov_pcm_tell(vf); + + return 1; +} + +int RageSoundReader_Vorbisfile::Read( float *buf, int iFrames ) +{ + int frames_read = 0; + + while( iFrames && !eof ) + { + const int bytes_per_frame = sizeof(float)*channels; + + int iFramesRead = 0; + + { + int curofs = (int) ov_pcm_tell(vf); + if( curofs < read_offset ) + { + /* The timestamps moved backwards. Ignore it. This file probably + * won't sync correctly. */ + LOG->Trace( "p ahead %p %i < %i, we're ahead by %i", + this, curofs, read_offset, read_offset-curofs ); + read_offset = curofs; + } + else if( curofs > read_offset ) + { + /* Our offset doesn't match. We have a hole in the data, or corruption. + * If we're reading with accurate syncing, insert silence to line it up. + * That way, corruptions in the file won't casue desyncs. */ + + /* In bytes: */ + int iSilentFrames = curofs - read_offset; + iSilentFrames = min( iSilentFrames, (int) iFrames ); + int silence = iSilentFrames * bytes_per_frame; + CHECKPOINT_M( ssprintf("p %i,%i: %i frames of silence needed", curofs, read_offset, silence) ); + + memset( buf, 0, silence ); + iFramesRead = iSilentFrames; + } + } + + if( iFramesRead == 0 ) + { + int bstream; +#if defined(INTEGER_VORBIS) + int ret = ov_read( vf, (char *) buf, iFrames * channels * sizeof(int16_t), &bstream ); +#else // float vorbis decoder + float **pcm; + int ret = ov_read_float( vf, &pcm, iFrames, &bstream ); +#endif + + { + vorbis_info *vi = ov_info( vf, -1 ); + ASSERT( vi != nullptr ); + + if( (unsigned) vi->channels != channels ) + RageException::Throw( "File \"%s\" changes channel count from %i to %i; not supported.", + filename.c_str(), channels, (int)vi->channels ); + } + + + if( ret == OV_HOLE ) + continue; + if( ret == OV_EBADLINK ) + { + SetError( ssprintf("Read: OV_EBADLINK") ); + return ERROR; + } + + if( ret == 0 ) + { + eof = true; + continue; + } + +#if defined(INTEGER_VORBIS) + if( ret > 0 ) + { + int iSamplesRead = ret / sizeof(int16_t); + iFramesRead = iSamplesRead / channels; + + /* Convert in reverse, so we can do it in-place. */ + const int16_t *pIn = (int16_t *) buf; + float *pOut = (float *) buf; + for( int i = iSamplesRead-1; i >= 0; --i ) + pOut[i] = pIn[i] / 32768.0f; + } +#else + if( ret > 0 ) + { + iFramesRead = ret; + + int iNumChannels = channels; + for( int iChannel = 0; iChannel < iNumChannels; ++iChannel ) + { + const float *pChannelIn = pcm[iChannel]; + float *pChannelOut = &buf[iChannel]; + for( int i = 0; i < iFramesRead; ++i ) + { + *pChannelOut = *pChannelIn; + ++pChannelIn; + pChannelOut += iNumChannels; + } + } + } +#endif + } + + read_offset += iFramesRead; + + buf += iFramesRead * channels; + frames_read += iFramesRead; + iFrames -= iFramesRead; + } + + if( !frames_read ) + return END_OF_FILE; + + return frames_read; +} + +int RageSoundReader_Vorbisfile::GetSampleRate() const +{ + ASSERT(vf != nullptr); + + vorbis_info *vi = ov_info(vf, -1); + ASSERT(vi != nullptr); + + return vi->rate; +} + +int RageSoundReader_Vorbisfile::GetNextSourceFrame() const +{ + ASSERT(vf != nullptr); + + int iFrame = (int)ov_pcm_tell( vf ); + return iFrame; +} + +RageSoundReader_Vorbisfile::RageSoundReader_Vorbisfile() +{ + vf = NULL; +} + +RageSoundReader_Vorbisfile::~RageSoundReader_Vorbisfile() +{ + if(vf) + ov_clear(vf); + delete vf; +} + +RageSoundReader_Vorbisfile *RageSoundReader_Vorbisfile::Copy() const +{ + RageFileBasic *pFile = m_pFile->Copy(); + pFile->Seek(0); + RageSoundReader_Vorbisfile *ret = new RageSoundReader_Vorbisfile; + + /* If we were able to open the sound in the first place, we expect to + * be able to reopen it. */ + if( ret->Open(pFile) != OPEN_OK ) + FAIL_M( ssprintf("Copying sound failed: %s", ret->GetError().c_str()) ); + + return ret; +} + +/* + * Copyright (c) 2003 Glenn Maynard + * 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. + */ + diff --git a/src/RageSoundReader_WAV.cpp b/src/RageSoundReader_WAV.cpp index d1b689f7f9..d8361d29a4 100644 --- a/src/RageSoundReader_WAV.cpp +++ b/src/RageSoundReader_WAV.cpp @@ -1,648 +1,648 @@ -/* - * Straightforward WAV reading. This only supports 8-bit and 16-bit PCM, - * 4-bit ADPCM with one or two channels. No other decompressors are planned: - * this format is only useful for fast uncompressed audio, and ADPCM is only - * supported to retain compatibility. - * - * http://www.saettler.com/RIFFNEW/RIFFNEW.htm - * http://www.kk.iij4u.or.jp/~kondo/wave/wavecomp.htm - * http://www.sonicspot.com/guide/wavefiles.html - */ - -#include "global.h" -#include "RageSoundReader_WAV.h" -#include "RageUtil.h" -#include "RageLog.h" -#include "RageFileBasic.h" - -namespace -{ - /* pBuf contains iSamples 8-bit samples; convert to 16-bit. pBuf must - * have enough storage to hold the resulting data. */ - void Convert8bitToFloat( void *pBuf, int iSamples ) - { - /* Convert in reverse, so we can do it in-place. */ - const uint8_t *pIn = (uint8_t *) pBuf; - float *pOut = (float *) pBuf; - for( int i = iSamples-1; i >= 0; --i ) - { - int iSample = pIn[i]; - iSample -= 128; /* 0..255 -> -128..127 */ - pOut[i] = iSample / 128.0f; - } - } - - /* Flip 16-bit samples if necessary. On little-endian systems, this will - * optimize out. */ - void ConvertLittleEndian16BitToFloat( void *pBuf, int iSamples ) - { - /* Convert in reverse, so we can do it in-place. */ - const int16_t *pIn = (int16_t *) pBuf; - float *pOut = (float *) pBuf; - for( int i = iSamples-1; i >= 0; --i ) - { - int16_t iSample = Swap16LE( pIn[i] ); - pOut[i] = iSample / 32768.0f; - } - } - - void ConvertLittleEndian24BitToFloat( void *pBuf, int iSamples ) - { - /* Convert in reverse, so we can do it in-place. */ - const unsigned char *pIn = (unsigned char *) pBuf; - float *pOut = (float *) pBuf; - pIn += iSamples * 3; - for( int i = iSamples-1; i >= 0; --i ) - { - pIn -= 3; - - int32_t iSample = - (int(pIn[0]) << 0) | - (int(pIn[1]) << 8) | - (int(pIn[2]) << 16); - - /* Sign-extend 24-bit to 32-bit: */ - if( iSample & 0x800000 ) - iSample |= 0xFF000000; - - pOut[i] = iSample / 8388608.0f; - } - } - - void ConvertLittleEndian32BitToFloat( void *pBuf, int iSamples ) - { - /* Convert in reverse, so we can do it in-place. */ - const int32_t *pIn = (int32_t *) pBuf; - float *pOut = (float *) pBuf; - for( int i = iSamples-1; i >= 0; --i ) - { - int32_t iSample = Swap32LE( pIn[i] ); - pOut[i] = iSample / 2147483648.0f; - } - } -}; - -struct WavReader -{ - WavReader( RageFileBasic &f, const RageSoundReader_WAV::WavData &data ): - m_File(f), m_WavData(data) { } - virtual ~WavReader() { } - virtual int Read( float *pBuf, int iFrames ) = 0; - virtual int GetLength() const = 0; - virtual bool Init() = 0; - virtual int SetPosition( int iFrame ) = 0; - virtual int GetNextSourceFrame() const = 0; - RString GetError() const { return m_sError; } - -protected: - RageFileBasic &m_File; - const RageSoundReader_WAV::WavData &m_WavData; - RString m_sError; -}; - -struct WavReaderPCM: public WavReader -{ - WavReaderPCM( RageFileBasic &f, const RageSoundReader_WAV::WavData &data ): - WavReader(f, data) { } - - bool Init() - { - if( QuantizeUp(m_WavData.m_iBitsPerSample, 8) < 8 || - QuantizeUp(m_WavData.m_iBitsPerSample, 8) > 32 ) - { - m_sError = ssprintf("Unsupported sample size %i", m_WavData.m_iBitsPerSample); - return false; - } - - if( m_WavData.m_iFormatTag == 3 && m_WavData.m_iBitsPerSample != 32 ) - { - m_sError = ssprintf( "Unsupported float sample size %i", m_WavData.m_iBitsPerSample ); - return false; - } - - m_File.Seek( m_WavData.m_iDataChunkPos ); - return true; - } - - int Read( float *buf, int iFrames ) - { - int iBytesPerSample = QuantizeUp(m_WavData.m_iBitsPerSample, 8) / 8; - int len = iFrames * m_WavData.m_iChannels; - len *= iBytesPerSample; - - const int iBytesLeftInDataChunk = m_WavData.m_iDataChunkSize - (m_File.Tell() - m_WavData.m_iDataChunkPos); - if( !iBytesLeftInDataChunk ) - return RageSoundReader::END_OF_FILE; - - len = min( len, iBytesLeftInDataChunk ); - int iGot = m_File.Read( buf, len ); - - int iGotSamples = iGot / iBytesPerSample; - if( m_WavData.m_iFormatTag == 1 ) - { - switch( iBytesPerSample ) - { - case 1: - Convert8bitToFloat( buf, iGotSamples ); - break; - case 2: - ConvertLittleEndian16BitToFloat( buf, iGotSamples ); - break; - case 3: - ConvertLittleEndian24BitToFloat( buf, iGotSamples ); - break; - case 4: - ConvertLittleEndian32BitToFloat( buf, iGotSamples ); - /* otherwise 3; already a float */ - break; - } - } - return iGotSamples / m_WavData.m_iChannels; - } - - int GetLength() const - { - const int iBytesPerSec = m_WavData.m_iSampleRate * m_WavData.m_iChannels * m_WavData.m_iBitsPerSample / 8; - int64_t iMS = (int64_t(m_WavData.m_iDataChunkSize) * 1000) / iBytesPerSec; - return (int) iMS; - } - - int SetPosition( int iFrame ) - { - int iByte = (int) (int64_t(iFrame) * m_WavData.m_iChannels * m_WavData.m_iBitsPerSample / 8); - if( iByte > m_WavData.m_iDataChunkSize ) - { - m_File.Seek( m_WavData.m_iDataChunkSize+m_WavData.m_iDataChunkPos ); - return 0; - } - - m_File.Seek( iByte+m_WavData.m_iDataChunkPos ); - return 1; - } - - // XXX: untested - int GetNextSourceFrame() const - { - int iByte = m_File.Tell() - m_WavData.m_iDataChunkPos; - int iFrame = iByte / (m_WavData.m_iChannels * m_WavData.m_iBitsPerSample / 8); - return iFrame; - } -}; - -struct WavReaderADPCM: public WavReader -{ -public: - vector m_iaCoef1, m_iaCoef2; - int16_t m_iFramesPerBlock; - float *m_pBuffer; - int m_iBufferAvail, m_iBufferUsed; - - WavReaderADPCM( RageFileBasic &f, const RageSoundReader_WAV::WavData &data ): - WavReader(f, data) - { - m_pBuffer = NULL; - } - - virtual ~WavReaderADPCM() - { - delete[] m_pBuffer; - } - - bool Init() - { - if( m_WavData.m_iBitsPerSample != 4 ) - { - m_sError = ssprintf( "Unsupported ADPCM sample size %i", m_WavData.m_iBitsPerSample ); - return false; - } - - m_File.Seek( m_WavData.m_iExtraFmtPos ); - - m_iFramesPerBlock = FileReading::read_16_le( m_File, m_sError ); - int16_t iNumCoef = FileReading::read_16_le( m_File, m_sError ); - m_iaCoef1.resize( iNumCoef ); - m_iaCoef2.resize( iNumCoef ); - for( int i = 0; i < iNumCoef; ++i ) - { - m_iaCoef1[i] = FileReading::read_16_le( m_File, m_sError ); - m_iaCoef2[i] = FileReading::read_16_le( m_File, m_sError ); - } - - if( m_sError.size() != 0 ) - return false; - - m_pBuffer = new float[m_iFramesPerBlock*m_WavData.m_iChannels]; - m_iBufferAvail = m_iBufferUsed = 0; - - m_File.Seek( m_WavData.m_iDataChunkPos ); - return true; - } - - void SetEOF() - { - m_iBufferUsed = m_iBufferAvail = 0; - m_File.Seek( m_WavData.m_iDataChunkSize+m_WavData.m_iDataChunkPos ); - } - - /* Return false on error, true on success (even if we hit EOF). */ - bool DecodeADPCMBlock() - { - ASSERT_M( m_iBufferUsed == m_iBufferAvail, ssprintf("%i", m_iBufferUsed) ); - - m_iBufferUsed = m_iBufferAvail = 0; - m_sError = ""; - - if( m_File.Tell() >= m_WavData.m_iDataChunkSize+m_WavData.m_iDataChunkPos || m_File.AtEOF() ) - return true; /* past the data chunk */ - - int8_t iPredictor[2]; - int16_t iDelta[2], iSamp1[2], iSamp2[2]; - for( int i = 0; i < m_WavData.m_iChannels; ++i ) - iPredictor[i] = FileReading::read_8( m_File, m_sError ); - for( int i = 0; i < m_WavData.m_iChannels; ++i ) - iDelta[i] = FileReading::read_16_le( m_File, m_sError ); - for( int i = 0; i < m_WavData.m_iChannels; ++i ) - iSamp1[i] = FileReading::read_16_le( m_File, m_sError ); - for( int i = 0; i < m_WavData.m_iChannels; ++i ) - iSamp2[i] = FileReading::read_16_le( m_File, m_sError ); - - if( m_sError.size() != 0 ) - return false; - - float *pBuffer = m_pBuffer; - int iCoef1[2], iCoef2[2]; - for( int i = 0; i < m_WavData.m_iChannels; ++i ) - { - if( iPredictor[i] >= (int) m_iaCoef1.size() ) - { - LOG->Trace( "%s: predictor out of range", m_File.GetDisplayPath().c_str() ); - - /* XXX: silence this block? */ - iPredictor[i] = 0; - } - - iCoef1[i] = m_iaCoef1[iPredictor[i]]; - iCoef2[i] = m_iaCoef2[iPredictor[i]]; - } - - /* We've read the block header; read the rest. Don't read past the end of the data chunk. */ - int iMaxSize = min( (int) m_WavData.m_iBlockAlign - 7 * m_WavData.m_iChannels, (m_WavData.m_iDataChunkSize+m_WavData.m_iDataChunkPos) - m_File.Tell() ); - - char *pBuf = (char *) alloca( iMaxSize ); - ASSERT( pBuf != NULL ); - - int iBlockSize = m_File.Read( pBuf, iMaxSize ); - if( iBlockSize == 0 ) - return true; - if( iBlockSize == -1 ) - { - m_sError = m_File.GetError(); - return false; - } - - for( int i = 0; i < m_WavData.m_iChannels; ++i ) - pBuffer[m_iBufferAvail++] = (int16_t)iSamp2[i] / 32768.0f; - for( int i = 0; i < m_WavData.m_iChannels; ++i ) - pBuffer[m_iBufferAvail++] = (int16_t)iSamp1[i] / 32768.0f; - - int8_t iBufSize = 0; - uint8_t iBuf = 0; - - bool bDone = false; - for( int i = 2; !bDone && i < m_iFramesPerBlock; ++i ) - { - for( int c = 0; !bDone && c < m_WavData.m_iChannels; ++c ) - { - if( iBufSize == 0 ) - { - if( !iBlockSize ) - { - bDone = true; - continue; - } - iBuf = *pBuf; - ++pBuf; - --iBlockSize; - iBufSize = 2; - } - - /* Store the nibble in signed char, so we get an arithmetic shift. */ - int8_t iErrorDelta = (int8_t)(iBuf) >> 4; - uint8_t iErrorDeltaUnsigned = iBuf >> 4; - iBuf <<= 4; - --iBufSize; - - int32_t iPredSample = (iSamp1[c] * iCoef1[c] + iSamp2[c] * iCoef2[c]) / (1<<8); - if( iPredSample < -32768 ) iPredSample = -32768; - if( iPredSample > 32767 ) iPredSample = 32767; - - int16_t iNewSample = (int16_t)iPredSample + (iDelta[c] * iErrorDelta); - pBuffer[m_iBufferAvail++] = iNewSample / 32768.0f; - - static const int aAdaptionTable[] = { - 230, 230, 230, 230, 307, 409, 512, 614, - 768, 614, 512, 409, 307, 230, 230, 230 - }; - iDelta[c] = int16_t( (iDelta[c] * aAdaptionTable[iErrorDeltaUnsigned]) / (1<<8) ); - iDelta[c] = max( (int16_t) 16, iDelta[c] ); - - iSamp2[c] = iSamp1[c]; - iSamp1[c] = iNewSample; - } - } - - return true; - } - - int Read( float *buf, int iFrames ) - { - int iGotFrames = 0; - - int iSample = 0; - - while( iGotFrames < (int) iFrames ) - { - if( m_iBufferUsed == m_iBufferAvail ) - { - if( !DecodeADPCMBlock() ) - return RageSoundReader::ERROR; - } - if( m_iBufferAvail == 0 ) - { - if( !iGotFrames ) - return RageSoundReader::END_OF_FILE; - else - return iGotFrames; - } - for( int c = 0; c < m_WavData.m_iChannels; c ++ ) - { - buf[iSample++] = m_pBuffer[m_iBufferUsed++]; - } - iGotFrames++; - } - return iGotFrames; - } - - int GetLength() const - { - const int iNumWholeBlocks = m_WavData.m_iDataChunkSize / m_WavData.m_iBlockAlign; - const int iExtraBytes = m_WavData.m_iDataChunkSize - (iNumWholeBlocks*m_WavData.m_iBlockAlign); - - int iFrames = iNumWholeBlocks * m_iFramesPerBlock; - - const int iBlockHeaderSize = 7 * m_WavData.m_iChannels; - if( iExtraBytes > iBlockHeaderSize ) - { - const int iExtraADPCMNibbles = max( 0, iExtraBytes-iBlockHeaderSize )*2; - const int iExtraADPCMFrames = iExtraADPCMNibbles/m_WavData.m_iChannels; - - iFrames += 2+iExtraADPCMFrames; - } - - int iMS = int((int64_t(iFrames)*1000)/m_WavData.m_iSampleRate); - return iMS; - } - - int SetPosition( int iFrame ) - { - const int iBlock = iFrame / m_iFramesPerBlock; - - m_iBufferUsed = m_iBufferAvail = 0; - - { - const int iByte = iBlock*m_WavData.m_iBlockAlign; - if( iByte >= m_WavData.m_iDataChunkSize ) - { - /* Past EOF. */ - SetEOF(); - return 0; - } - m_File.Seek( iByte+m_WavData.m_iDataChunkPos ); - } - - if( !DecodeADPCMBlock() ) - return -1; - - const int iRemainingFrames = iFrame - iBlock*m_iFramesPerBlock; - m_iBufferUsed = iRemainingFrames * m_WavData.m_iChannels; - if( m_iBufferUsed > m_iBufferAvail ) - { - SetEOF(); - return 0; - } - - return 1; - } - - // XXX: untested - int GetNextSourceFrame() const - { - int iByte = m_File.Tell() - m_WavData.m_iDataChunkPos; - int iBlock = iByte / m_WavData.m_iBlockAlign; - int iFrame = iBlock * m_iFramesPerBlock; - - int iBufferRemainingBytes = m_iBufferAvail - m_iBufferUsed; - int iBufferRemainingFrames = iBufferRemainingBytes / (m_WavData.m_iChannels * sizeof(int16_t)); - iFrame -= iBufferRemainingFrames; - - return iFrame; - } -}; - -RString ReadString( RageFileBasic &f, int iSize, RString &sError ) -{ - if( sError.size() != 0 ) - return RString(); - - RString sBuf; - char *pBuf = sBuf.GetBuffer( iSize ); - FileReading::ReadBytes( f, pBuf, iSize, sError ); - sBuf.ReleaseBuffer( iSize ); - return sBuf; -} - -#define FATAL_ERROR(s) \ -{ \ - if( sError.size() == 0 ) sError = (s); \ - SetError( sError ); \ - return OPEN_FATAL_ERROR; \ -} - -RageSoundReader_FileReader::OpenResult RageSoundReader_WAV::Open( RageFileBasic *pFile ) -{ - m_pFile = pFile; - - RString sError; - - /* RIFF header: */ - if( ReadString( *m_pFile, 4, sError ) != "RIFF" ) - { - SetError( "Not a WAV file" ); - return OPEN_UNKNOWN_FILE_FORMAT; - } - - FileReading::read_32_le( *m_pFile, sError ); /* file size */ - if( ReadString( *m_pFile, 4, sError ) != "WAVE" ) - { - SetError( "Not a WAV file" ); - return OPEN_UNKNOWN_FILE_FORMAT; - } - - bool bGotFormatChunk = false, bGotDataChunk = false; - while( !bGotFormatChunk || !bGotDataChunk ) - { - RString ChunkID = ReadString( *m_pFile, 4, sError ); - int32_t iChunkSize = FileReading::read_32_le( *m_pFile, sError ); - - if( sError.size() != 0 ) - { - SetError( sError ); - return OPEN_FATAL_ERROR; - } - - int iNextChunk = m_pFile->Tell() + iChunkSize; - /* Chunks are always word-aligned: */ - iNextChunk = (iNextChunk+1)&~1; - - if( ChunkID == "fmt " ) - { - if( bGotFormatChunk ) - LOG->Warn( "File %s has more than one fmt chunk", m_pFile->GetDisplayPath().c_str() ); - - m_WavData.m_iFormatTag = FileReading::read_16_le( *m_pFile, sError ); - m_WavData.m_iChannels = FileReading::read_16_le( *m_pFile, sError ); - m_WavData.m_iSampleRate = FileReading::read_32_le( *m_pFile, sError ); - FileReading::read_32_le( *m_pFile, sError ); /* BytesPerSec */ - m_WavData.m_iBlockAlign = FileReading::read_16_le( *m_pFile, sError ); - m_WavData.m_iBitsPerSample = FileReading::read_16_le( *m_pFile, sError ); - m_WavData.m_iExtraFmtBytes = FileReading::read_16_le( *m_pFile, sError ); - - if( m_WavData.m_iChannels < 1 || m_WavData.m_iChannels > 2 ) - FATAL_ERROR( ssprintf( "Unsupported channel count: %i", m_WavData.m_iChannels) ); - - if( m_WavData.m_iSampleRate < 4000 || m_WavData.m_iSampleRate > 100000 ) /* unlikely */ - FATAL_ERROR( ssprintf( "Invalid sample rate: %i", m_WavData.m_iSampleRate) ); - - m_WavData.m_iExtraFmtPos = m_pFile->Tell(); - - bGotFormatChunk = true; - } - - if( ChunkID == "data" ) - { - m_WavData.m_iDataChunkPos = m_pFile->Tell(); - m_WavData.m_iDataChunkSize = iChunkSize; - - int iFileSize = m_pFile->GetFileSize(); - int iMaxSize = iFileSize-m_WavData.m_iDataChunkPos; - if( iMaxSize < m_WavData.m_iDataChunkSize ) - { - LOG->Warn( "File %s truncated (%i < data chunk size %i)", m_pFile->GetDisplayPath().c_str(), - iMaxSize, m_WavData.m_iDataChunkSize ); - - m_WavData.m_iDataChunkSize = iMaxSize; - } - - bGotDataChunk = true; - } - m_pFile->Seek( iNextChunk ); - } - - if( sError.size() != 0 ) - { - SetError( sError ); - return OPEN_FATAL_ERROR; - } - - switch( m_WavData.m_iFormatTag ) - { - case 1: // PCM - case 3: // FLOAT - m_pImpl = new WavReaderPCM( *m_pFile, m_WavData ); - break; - case 2: // ADPCM - m_pImpl = new WavReaderADPCM( *m_pFile, m_WavData ); - break; - case 85: // MP3 - /* Return unknown, so other decoders will be tried. MAD can read MP3s embedded in WAVs. */ - return OPEN_UNKNOWN_FILE_FORMAT; - default: - FATAL_ERROR( ssprintf( "Unsupported data format %i", m_WavData.m_iFormatTag) ); - } - - if( !m_pImpl->Init() ) - { - SetError( m_pImpl->GetError() ); - return OPEN_FATAL_ERROR; - } - - return OPEN_OK; -} - -int RageSoundReader_WAV::GetLength() const -{ - ASSERT( m_pImpl != NULL ); - return m_pImpl->GetLength(); -} - -int RageSoundReader_WAV::SetPosition( int iFrame ) -{ - ASSERT( m_pImpl != NULL ); - return m_pImpl->SetPosition( iFrame ); -} - -int RageSoundReader_WAV::GetNextSourceFrame() const -{ - ASSERT( m_pImpl != NULL ); - return m_pImpl->GetNextSourceFrame(); -} - -int RageSoundReader_WAV::Read( float *pBuf, int iFrames ) -{ - ASSERT( m_pImpl != NULL ); - return m_pImpl->Read( pBuf, iFrames ); -} - -RageSoundReader_WAV::RageSoundReader_WAV() -{ - m_pImpl = NULL; -} - -RageSoundReader_WAV::~RageSoundReader_WAV() -{ - delete m_pImpl; -} - -RageSoundReader_WAV *RageSoundReader_WAV::Copy() const -{ - RageSoundReader_WAV *ret = new RageSoundReader_WAV; - RageFileBasic *pFile = m_pFile->Copy(); - pFile->Seek( 0 ); - ret->Open( pFile ); - return ret; -} - -/* - * (c) 2004 Glenn Maynard - * 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. - */ +/* + * Straightforward WAV reading. This only supports 8-bit and 16-bit PCM, + * 4-bit ADPCM with one or two channels. No other decompressors are planned: + * this format is only useful for fast uncompressed audio, and ADPCM is only + * supported to retain compatibility. + * + * http://www.saettler.com/RIFFNEW/RIFFNEW.htm + * http://www.kk.iij4u.or.jp/~kondo/wave/wavecomp.htm + * http://www.sonicspot.com/guide/wavefiles.html + */ + +#include "global.h" +#include "RageSoundReader_WAV.h" +#include "RageUtil.h" +#include "RageLog.h" +#include "RageFileBasic.h" + +namespace +{ + /* pBuf contains iSamples 8-bit samples; convert to 16-bit. pBuf must + * have enough storage to hold the resulting data. */ + void Convert8bitToFloat( void *pBuf, int iSamples ) + { + /* Convert in reverse, so we can do it in-place. */ + const uint8_t *pIn = (uint8_t *) pBuf; + float *pOut = (float *) pBuf; + for( int i = iSamples-1; i >= 0; --i ) + { + int iSample = pIn[i]; + iSample -= 128; /* 0..255 -> -128..127 */ + pOut[i] = iSample / 128.0f; + } + } + + /* Flip 16-bit samples if necessary. On little-endian systems, this will + * optimize out. */ + void ConvertLittleEndian16BitToFloat( void *pBuf, int iSamples ) + { + /* Convert in reverse, so we can do it in-place. */ + const int16_t *pIn = (int16_t *) pBuf; + float *pOut = (float *) pBuf; + for( int i = iSamples-1; i >= 0; --i ) + { + int16_t iSample = Swap16LE( pIn[i] ); + pOut[i] = iSample / 32768.0f; + } + } + + void ConvertLittleEndian24BitToFloat( void *pBuf, int iSamples ) + { + /* Convert in reverse, so we can do it in-place. */ + const unsigned char *pIn = (unsigned char *) pBuf; + float *pOut = (float *) pBuf; + pIn += iSamples * 3; + for( int i = iSamples-1; i >= 0; --i ) + { + pIn -= 3; + + int32_t iSample = + (int(pIn[0]) << 0) | + (int(pIn[1]) << 8) | + (int(pIn[2]) << 16); + + /* Sign-extend 24-bit to 32-bit: */ + if( iSample & 0x800000 ) + iSample |= 0xFF000000; + + pOut[i] = iSample / 8388608.0f; + } + } + + void ConvertLittleEndian32BitToFloat( void *pBuf, int iSamples ) + { + /* Convert in reverse, so we can do it in-place. */ + const int32_t *pIn = (int32_t *) pBuf; + float *pOut = (float *) pBuf; + for( int i = iSamples-1; i >= 0; --i ) + { + int32_t iSample = Swap32LE( pIn[i] ); + pOut[i] = iSample / 2147483648.0f; + } + } +}; + +struct WavReader +{ + WavReader( RageFileBasic &f, const RageSoundReader_WAV::WavData &data ): + m_File(f), m_WavData(data) { } + virtual ~WavReader() { } + virtual int Read( float *pBuf, int iFrames ) = 0; + virtual int GetLength() const = 0; + virtual bool Init() = 0; + virtual int SetPosition( int iFrame ) = 0; + virtual int GetNextSourceFrame() const = 0; + RString GetError() const { return m_sError; } + +protected: + RageFileBasic &m_File; + const RageSoundReader_WAV::WavData &m_WavData; + RString m_sError; +}; + +struct WavReaderPCM: public WavReader +{ + WavReaderPCM( RageFileBasic &f, const RageSoundReader_WAV::WavData &data ): + WavReader(f, data) { } + + bool Init() + { + if( QuantizeUp(m_WavData.m_iBitsPerSample, 8) < 8 || + QuantizeUp(m_WavData.m_iBitsPerSample, 8) > 32 ) + { + m_sError = ssprintf("Unsupported sample size %i", m_WavData.m_iBitsPerSample); + return false; + } + + if( m_WavData.m_iFormatTag == 3 && m_WavData.m_iBitsPerSample != 32 ) + { + m_sError = ssprintf( "Unsupported float sample size %i", m_WavData.m_iBitsPerSample ); + return false; + } + + m_File.Seek( m_WavData.m_iDataChunkPos ); + return true; + } + + int Read( float *buf, int iFrames ) + { + int iBytesPerSample = QuantizeUp(m_WavData.m_iBitsPerSample, 8) / 8; + int len = iFrames * m_WavData.m_iChannels; + len *= iBytesPerSample; + + const int iBytesLeftInDataChunk = m_WavData.m_iDataChunkSize - (m_File.Tell() - m_WavData.m_iDataChunkPos); + if( !iBytesLeftInDataChunk ) + return RageSoundReader::END_OF_FILE; + + len = min( len, iBytesLeftInDataChunk ); + int iGot = m_File.Read( buf, len ); + + int iGotSamples = iGot / iBytesPerSample; + if( m_WavData.m_iFormatTag == 1 ) + { + switch( iBytesPerSample ) + { + case 1: + Convert8bitToFloat( buf, iGotSamples ); + break; + case 2: + ConvertLittleEndian16BitToFloat( buf, iGotSamples ); + break; + case 3: + ConvertLittleEndian24BitToFloat( buf, iGotSamples ); + break; + case 4: + ConvertLittleEndian32BitToFloat( buf, iGotSamples ); + /* otherwise 3; already a float */ + break; + } + } + return iGotSamples / m_WavData.m_iChannels; + } + + int GetLength() const + { + const int iBytesPerSec = m_WavData.m_iSampleRate * m_WavData.m_iChannels * m_WavData.m_iBitsPerSample / 8; + int64_t iMS = (int64_t(m_WavData.m_iDataChunkSize) * 1000) / iBytesPerSec; + return (int) iMS; + } + + int SetPosition( int iFrame ) + { + int iByte = (int) (int64_t(iFrame) * m_WavData.m_iChannels * m_WavData.m_iBitsPerSample / 8); + if( iByte > m_WavData.m_iDataChunkSize ) + { + m_File.Seek( m_WavData.m_iDataChunkSize+m_WavData.m_iDataChunkPos ); + return 0; + } + + m_File.Seek( iByte+m_WavData.m_iDataChunkPos ); + return 1; + } + + // XXX: untested + int GetNextSourceFrame() const + { + int iByte = m_File.Tell() - m_WavData.m_iDataChunkPos; + int iFrame = iByte / (m_WavData.m_iChannels * m_WavData.m_iBitsPerSample / 8); + return iFrame; + } +}; + +struct WavReaderADPCM: public WavReader +{ +public: + vector m_iaCoef1, m_iaCoef2; + int16_t m_iFramesPerBlock; + float *m_pBuffer; + int m_iBufferAvail, m_iBufferUsed; + + WavReaderADPCM( RageFileBasic &f, const RageSoundReader_WAV::WavData &data ): + WavReader(f, data) + { + m_pBuffer = NULL; + } + + virtual ~WavReaderADPCM() + { + delete[] m_pBuffer; + } + + bool Init() + { + if( m_WavData.m_iBitsPerSample != 4 ) + { + m_sError = ssprintf( "Unsupported ADPCM sample size %i", m_WavData.m_iBitsPerSample ); + return false; + } + + m_File.Seek( m_WavData.m_iExtraFmtPos ); + + m_iFramesPerBlock = FileReading::read_16_le( m_File, m_sError ); + int16_t iNumCoef = FileReading::read_16_le( m_File, m_sError ); + m_iaCoef1.resize( iNumCoef ); + m_iaCoef2.resize( iNumCoef ); + for( int i = 0; i < iNumCoef; ++i ) + { + m_iaCoef1[i] = FileReading::read_16_le( m_File, m_sError ); + m_iaCoef2[i] = FileReading::read_16_le( m_File, m_sError ); + } + + if( m_sError.size() != 0 ) + return false; + + m_pBuffer = new float[m_iFramesPerBlock*m_WavData.m_iChannels]; + m_iBufferAvail = m_iBufferUsed = 0; + + m_File.Seek( m_WavData.m_iDataChunkPos ); + return true; + } + + void SetEOF() + { + m_iBufferUsed = m_iBufferAvail = 0; + m_File.Seek( m_WavData.m_iDataChunkSize+m_WavData.m_iDataChunkPos ); + } + + /* Return false on error, true on success (even if we hit EOF). */ + bool DecodeADPCMBlock() + { + ASSERT_M( m_iBufferUsed == m_iBufferAvail, ssprintf("%i", m_iBufferUsed) ); + + m_iBufferUsed = m_iBufferAvail = 0; + m_sError = ""; + + if( m_File.Tell() >= m_WavData.m_iDataChunkSize+m_WavData.m_iDataChunkPos || m_File.AtEOF() ) + return true; /* past the data chunk */ + + int8_t iPredictor[2]; + int16_t iDelta[2], iSamp1[2], iSamp2[2]; + for( int i = 0; i < m_WavData.m_iChannels; ++i ) + iPredictor[i] = FileReading::read_8( m_File, m_sError ); + for( int i = 0; i < m_WavData.m_iChannels; ++i ) + iDelta[i] = FileReading::read_16_le( m_File, m_sError ); + for( int i = 0; i < m_WavData.m_iChannels; ++i ) + iSamp1[i] = FileReading::read_16_le( m_File, m_sError ); + for( int i = 0; i < m_WavData.m_iChannels; ++i ) + iSamp2[i] = FileReading::read_16_le( m_File, m_sError ); + + if( m_sError.size() != 0 ) + return false; + + float *pBuffer = m_pBuffer; + int iCoef1[2], iCoef2[2]; + for( int i = 0; i < m_WavData.m_iChannels; ++i ) + { + if( iPredictor[i] >= (int) m_iaCoef1.size() ) + { + LOG->Trace( "%s: predictor out of range", m_File.GetDisplayPath().c_str() ); + + /* XXX: silence this block? */ + iPredictor[i] = 0; + } + + iCoef1[i] = m_iaCoef1[iPredictor[i]]; + iCoef2[i] = m_iaCoef2[iPredictor[i]]; + } + + /* We've read the block header; read the rest. Don't read past the end of the data chunk. */ + int iMaxSize = min( (int) m_WavData.m_iBlockAlign - 7 * m_WavData.m_iChannels, (m_WavData.m_iDataChunkSize+m_WavData.m_iDataChunkPos) - m_File.Tell() ); + + char *pBuf = (char *) alloca( iMaxSize ); + ASSERT( pBuf != nullptr ); + + int iBlockSize = m_File.Read( pBuf, iMaxSize ); + if( iBlockSize == 0 ) + return true; + if( iBlockSize == -1 ) + { + m_sError = m_File.GetError(); + return false; + } + + for( int i = 0; i < m_WavData.m_iChannels; ++i ) + pBuffer[m_iBufferAvail++] = (int16_t)iSamp2[i] / 32768.0f; + for( int i = 0; i < m_WavData.m_iChannels; ++i ) + pBuffer[m_iBufferAvail++] = (int16_t)iSamp1[i] / 32768.0f; + + int8_t iBufSize = 0; + uint8_t iBuf = 0; + + bool bDone = false; + for( int i = 2; !bDone && i < m_iFramesPerBlock; ++i ) + { + for( int c = 0; !bDone && c < m_WavData.m_iChannels; ++c ) + { + if( iBufSize == 0 ) + { + if( !iBlockSize ) + { + bDone = true; + continue; + } + iBuf = *pBuf; + ++pBuf; + --iBlockSize; + iBufSize = 2; + } + + /* Store the nibble in signed char, so we get an arithmetic shift. */ + int8_t iErrorDelta = (int8_t)(iBuf) >> 4; + uint8_t iErrorDeltaUnsigned = iBuf >> 4; + iBuf <<= 4; + --iBufSize; + + int32_t iPredSample = (iSamp1[c] * iCoef1[c] + iSamp2[c] * iCoef2[c]) / (1<<8); + if( iPredSample < -32768 ) iPredSample = -32768; + if( iPredSample > 32767 ) iPredSample = 32767; + + int16_t iNewSample = (int16_t)iPredSample + (iDelta[c] * iErrorDelta); + pBuffer[m_iBufferAvail++] = iNewSample / 32768.0f; + + static const int aAdaptionTable[] = { + 230, 230, 230, 230, 307, 409, 512, 614, + 768, 614, 512, 409, 307, 230, 230, 230 + }; + iDelta[c] = int16_t( (iDelta[c] * aAdaptionTable[iErrorDeltaUnsigned]) / (1<<8) ); + iDelta[c] = max( (int16_t) 16, iDelta[c] ); + + iSamp2[c] = iSamp1[c]; + iSamp1[c] = iNewSample; + } + } + + return true; + } + + int Read( float *buf, int iFrames ) + { + int iGotFrames = 0; + + int iSample = 0; + + while( iGotFrames < (int) iFrames ) + { + if( m_iBufferUsed == m_iBufferAvail ) + { + if( !DecodeADPCMBlock() ) + return RageSoundReader::ERROR; + } + if( m_iBufferAvail == 0 ) + { + if( !iGotFrames ) + return RageSoundReader::END_OF_FILE; + else + return iGotFrames; + } + for( int c = 0; c < m_WavData.m_iChannels; c ++ ) + { + buf[iSample++] = m_pBuffer[m_iBufferUsed++]; + } + iGotFrames++; + } + return iGotFrames; + } + + int GetLength() const + { + const int iNumWholeBlocks = m_WavData.m_iDataChunkSize / m_WavData.m_iBlockAlign; + const int iExtraBytes = m_WavData.m_iDataChunkSize - (iNumWholeBlocks*m_WavData.m_iBlockAlign); + + int iFrames = iNumWholeBlocks * m_iFramesPerBlock; + + const int iBlockHeaderSize = 7 * m_WavData.m_iChannels; + if( iExtraBytes > iBlockHeaderSize ) + { + const int iExtraADPCMNibbles = max( 0, iExtraBytes-iBlockHeaderSize )*2; + const int iExtraADPCMFrames = iExtraADPCMNibbles/m_WavData.m_iChannels; + + iFrames += 2+iExtraADPCMFrames; + } + + int iMS = int((int64_t(iFrames)*1000)/m_WavData.m_iSampleRate); + return iMS; + } + + int SetPosition( int iFrame ) + { + const int iBlock = iFrame / m_iFramesPerBlock; + + m_iBufferUsed = m_iBufferAvail = 0; + + { + const int iByte = iBlock*m_WavData.m_iBlockAlign; + if( iByte >= m_WavData.m_iDataChunkSize ) + { + /* Past EOF. */ + SetEOF(); + return 0; + } + m_File.Seek( iByte+m_WavData.m_iDataChunkPos ); + } + + if( !DecodeADPCMBlock() ) + return -1; + + const int iRemainingFrames = iFrame - iBlock*m_iFramesPerBlock; + m_iBufferUsed = iRemainingFrames * m_WavData.m_iChannels; + if( m_iBufferUsed > m_iBufferAvail ) + { + SetEOF(); + return 0; + } + + return 1; + } + + // XXX: untested + int GetNextSourceFrame() const + { + int iByte = m_File.Tell() - m_WavData.m_iDataChunkPos; + int iBlock = iByte / m_WavData.m_iBlockAlign; + int iFrame = iBlock * m_iFramesPerBlock; + + int iBufferRemainingBytes = m_iBufferAvail - m_iBufferUsed; + int iBufferRemainingFrames = iBufferRemainingBytes / (m_WavData.m_iChannels * sizeof(int16_t)); + iFrame -= iBufferRemainingFrames; + + return iFrame; + } +}; + +RString ReadString( RageFileBasic &f, int iSize, RString &sError ) +{ + if( sError.size() != 0 ) + return RString(); + + RString sBuf; + char *pBuf = sBuf.GetBuffer( iSize ); + FileReading::ReadBytes( f, pBuf, iSize, sError ); + sBuf.ReleaseBuffer( iSize ); + return sBuf; +} + +#define FATAL_ERROR(s) \ +{ \ + if( sError.size() == 0 ) sError = (s); \ + SetError( sError ); \ + return OPEN_FATAL_ERROR; \ +} + +RageSoundReader_FileReader::OpenResult RageSoundReader_WAV::Open( RageFileBasic *pFile ) +{ + m_pFile = pFile; + + RString sError; + + /* RIFF header: */ + if( ReadString( *m_pFile, 4, sError ) != "RIFF" ) + { + SetError( "Not a WAV file" ); + return OPEN_UNKNOWN_FILE_FORMAT; + } + + FileReading::read_32_le( *m_pFile, sError ); /* file size */ + if( ReadString( *m_pFile, 4, sError ) != "WAVE" ) + { + SetError( "Not a WAV file" ); + return OPEN_UNKNOWN_FILE_FORMAT; + } + + bool bGotFormatChunk = false, bGotDataChunk = false; + while( !bGotFormatChunk || !bGotDataChunk ) + { + RString ChunkID = ReadString( *m_pFile, 4, sError ); + int32_t iChunkSize = FileReading::read_32_le( *m_pFile, sError ); + + if( sError.size() != 0 ) + { + SetError( sError ); + return OPEN_FATAL_ERROR; + } + + int iNextChunk = m_pFile->Tell() + iChunkSize; + /* Chunks are always word-aligned: */ + iNextChunk = (iNextChunk+1)&~1; + + if( ChunkID == "fmt " ) + { + if( bGotFormatChunk ) + LOG->Warn( "File %s has more than one fmt chunk", m_pFile->GetDisplayPath().c_str() ); + + m_WavData.m_iFormatTag = FileReading::read_16_le( *m_pFile, sError ); + m_WavData.m_iChannels = FileReading::read_16_le( *m_pFile, sError ); + m_WavData.m_iSampleRate = FileReading::read_32_le( *m_pFile, sError ); + FileReading::read_32_le( *m_pFile, sError ); /* BytesPerSec */ + m_WavData.m_iBlockAlign = FileReading::read_16_le( *m_pFile, sError ); + m_WavData.m_iBitsPerSample = FileReading::read_16_le( *m_pFile, sError ); + m_WavData.m_iExtraFmtBytes = FileReading::read_16_le( *m_pFile, sError ); + + if( m_WavData.m_iChannels < 1 || m_WavData.m_iChannels > 2 ) + FATAL_ERROR( ssprintf( "Unsupported channel count: %i", m_WavData.m_iChannels) ); + + if( m_WavData.m_iSampleRate < 4000 || m_WavData.m_iSampleRate > 100000 ) /* unlikely */ + FATAL_ERROR( ssprintf( "Invalid sample rate: %i", m_WavData.m_iSampleRate) ); + + m_WavData.m_iExtraFmtPos = m_pFile->Tell(); + + bGotFormatChunk = true; + } + + if( ChunkID == "data" ) + { + m_WavData.m_iDataChunkPos = m_pFile->Tell(); + m_WavData.m_iDataChunkSize = iChunkSize; + + int iFileSize = m_pFile->GetFileSize(); + int iMaxSize = iFileSize-m_WavData.m_iDataChunkPos; + if( iMaxSize < m_WavData.m_iDataChunkSize ) + { + LOG->Warn( "File %s truncated (%i < data chunk size %i)", m_pFile->GetDisplayPath().c_str(), + iMaxSize, m_WavData.m_iDataChunkSize ); + + m_WavData.m_iDataChunkSize = iMaxSize; + } + + bGotDataChunk = true; + } + m_pFile->Seek( iNextChunk ); + } + + if( sError.size() != 0 ) + { + SetError( sError ); + return OPEN_FATAL_ERROR; + } + + switch( m_WavData.m_iFormatTag ) + { + case 1: // PCM + case 3: // FLOAT + m_pImpl = new WavReaderPCM( *m_pFile, m_WavData ); + break; + case 2: // ADPCM + m_pImpl = new WavReaderADPCM( *m_pFile, m_WavData ); + break; + case 85: // MP3 + /* Return unknown, so other decoders will be tried. MAD can read MP3s embedded in WAVs. */ + return OPEN_UNKNOWN_FILE_FORMAT; + default: + FATAL_ERROR( ssprintf( "Unsupported data format %i", m_WavData.m_iFormatTag) ); + } + + if( !m_pImpl->Init() ) + { + SetError( m_pImpl->GetError() ); + return OPEN_FATAL_ERROR; + } + + return OPEN_OK; +} + +int RageSoundReader_WAV::GetLength() const +{ + ASSERT( m_pImpl != nullptr ); + return m_pImpl->GetLength(); +} + +int RageSoundReader_WAV::SetPosition( int iFrame ) +{ + ASSERT( m_pImpl != nullptr ); + return m_pImpl->SetPosition( iFrame ); +} + +int RageSoundReader_WAV::GetNextSourceFrame() const +{ + ASSERT( m_pImpl != nullptr ); + return m_pImpl->GetNextSourceFrame(); +} + +int RageSoundReader_WAV::Read( float *pBuf, int iFrames ) +{ + ASSERT( m_pImpl != nullptr ); + return m_pImpl->Read( pBuf, iFrames ); +} + +RageSoundReader_WAV::RageSoundReader_WAV() +{ + m_pImpl = NULL; +} + +RageSoundReader_WAV::~RageSoundReader_WAV() +{ + delete m_pImpl; +} + +RageSoundReader_WAV *RageSoundReader_WAV::Copy() const +{ + RageSoundReader_WAV *ret = new RageSoundReader_WAV; + RageFileBasic *pFile = m_pFile->Copy(); + pFile->Seek( 0 ); + ret->Open( pFile ); + return ret; +} + +/* + * (c) 2004 Glenn Maynard + * 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. + */ diff --git a/src/RageSurface.cpp b/src/RageSurface.cpp index fd586849b9..4423c5acac 100644 --- a/src/RageSurface.cpp +++ b/src/RageSurface.cpp @@ -1,280 +1,280 @@ -#include "global.h" -#include "RageSurface.h" -#include "RageUtil.h" - -bool RageSurfaceColor::operator== ( const RageSurfaceColor &rhs ) const -{ - return rhs.r == r && rhs.g == g && rhs.b == b && rhs.a == a; -} - -int32_t RageSurfacePalette::FindColor( const RageSurfaceColor &color ) const -{ - for( int i = 0; i < ncolors; ++i ) - if( colors[i] == color ) - return i; - return -1; -} - -/* XXX: untested */ -int32_t RageSurfacePalette::FindClosestColor( const RageSurfaceColor &color ) const -{ - int iBest = -1; - int iBestDist = INT_MAX; - for( int i = 0; i < ncolors; ++i ) - { - if( colors[i] == color ) - return i; - - int iDist = abs( colors[i].r - color.r ) + - abs( colors[i].g - color.g ) + - abs( colors[i].b - color.b ) + - abs( colors[i].a - color.a ); - if( iDist < iBestDist ) - { - iBestDist = iDist ; - iBest = i; - } - } - - return iBest; -} - -RageSurfaceFormat::RageSurfaceFormat(): - Rmask(Mask[0]), Gmask(Mask[1]), Bmask(Mask[2]), Amask(Mask[3]), - Rshift(Shift[0]), Gshift(Shift[1]), Bshift(Shift[2]), Ashift(Shift[3]) -{ - palette = NULL; -} - -RageSurfaceFormat::RageSurfaceFormat( const RageSurfaceFormat &cpy ): - Rmask(Mask[0]), Gmask(Mask[1]), Bmask(Mask[2]), Amask(Mask[3]), - Rshift(Shift[0]), Gshift(Shift[1]), Bshift(Shift[2]), Ashift(Shift[3]) -{ - memcpy( this, &cpy, sizeof(RageSurfaceFormat) ); - if( palette ) - palette = new RageSurfacePalette( *palette ); -} - -RageSurfaceFormat::~RageSurfaceFormat() -{ - delete palette; -} - -void RageSurfaceFormat::GetRGB( uint32_t val, uint8_t *r, uint8_t *g, uint8_t *b ) const -{ - if( BytesPerPixel == 1 ) - { - ASSERT( palette != NULL ); - *r = palette->colors[val].r; - *g = palette->colors[val].g; - *b = palette->colors[val].b; - } else { - *r = int8_t( (val & Mask[0]) >> Shift[0] << Loss[0] ); - *g = int8_t( (val & Mask[1]) >> Shift[1] << Loss[1] ); - *b = int8_t( (val & Mask[2]) >> Shift[2] << Loss[2] ); - } -} - -bool RageSurfaceFormat::MapRGBA( uint8_t r, uint8_t g, uint8_t b, uint8_t a, uint32_t &val ) const -{ - if( BytesPerPixel == 1 ) - { - RageSurfaceColor c( r, g, b, a ); - int32_t n = palette->FindColor( c ); - if( n == -1 ) - return false; - val = (uint32_t) n; - } else { - val = - (r >> Loss[0] << Shift[0]) | - (g >> Loss[1] << Shift[1]) | - (b >> Loss[2] << Shift[2]) | - (a >> Loss[3] << Shift[3]); - } - return true; -} - -bool RageSurfaceFormat::operator== ( const RageSurfaceFormat &rhs ) const -{ - if( !Equivalent(rhs) ) - return false; - - if( BytesPerPixel == 1 ) - if( memcmp( palette, rhs.palette, sizeof(RageSurfaceFormat) ) ) - return false; - - return true; -} - -bool RageSurfaceFormat::Equivalent( const RageSurfaceFormat &rhs ) const -{ -#define COMP(a) if( a != rhs.a ) return false; - COMP( BytesPerPixel ); - COMP( Rmask ); - COMP( Gmask ); - COMP( Bmask ); - COMP( Amask ); - - return true; -} - -RageSurface::RageSurface() -{ - format = &fmt; - pixels = NULL; - pixels_owned = true; -} - -RageSurface::RageSurface( const RageSurface &cpy ) -{ - format = &fmt; - - w = cpy.w; - h = cpy.h; - pitch = cpy.pitch; - flags = cpy.flags; - pixels_owned = true; - if( cpy.pixels ) - { - pixels = new uint8_t[ pitch*h ]; - memcpy( pixels, cpy.pixels, pitch*h ); - } - else - pixels = NULL; -} - -RageSurface::~RageSurface() -{ - if( pixels_owned ) - delete [] pixels; -} - -static int GetShiftFromMask( uint32_t mask ) -{ - if( !mask ) - return 0; - - int iShift = 0; - while( (mask & 1) == 0 ) - { - mask >>= 1; - ++iShift; - } - return iShift; -} - -static int GetBitsFromMask( uint32_t mask ) -{ - if( !mask ) - return 0; - - mask >>= GetShiftFromMask(mask); - - int iBits = 0; - while( (mask & 1) == 1 ) - { - mask >>= 1; - ++iBits; - } - return iBits; -} - - -void SetupFormat( RageSurfaceFormat &fmt, - int width, int height, int BitsPerPixel, uint32_t Rmask, uint32_t Gmask, uint32_t Bmask, uint32_t Amask ) -{ - fmt.BitsPerPixel = BitsPerPixel; - fmt.BytesPerPixel = BitsPerPixel/8; - if( fmt.BytesPerPixel == 1 ) - { - ZERO( fmt.Mask ); - ZERO( fmt.Shift ); - - // Loss for paletted textures is zero; the actual palette entries are 8-bit. - ZERO( fmt.Loss ); - - fmt.palette = new RageSurfacePalette; - fmt.palette->ncolors = 256; - } - else - { - fmt.Mask[0] = Rmask; - fmt.Mask[1] = Gmask; - fmt.Mask[2] = Bmask; - fmt.Mask[3] = Amask; - - fmt.Shift[0] = GetShiftFromMask( Rmask ); - fmt.Shift[1] = GetShiftFromMask( Gmask ); - fmt.Shift[2] = GetShiftFromMask( Bmask ); - fmt.Shift[3] = GetShiftFromMask( Amask ); - - fmt.Loss[0] = (uint8_t) (8-GetBitsFromMask( Rmask )); - fmt.Loss[1] = (uint8_t) (8-GetBitsFromMask( Gmask )); - fmt.Loss[2] = (uint8_t) (8-GetBitsFromMask( Bmask )); - fmt.Loss[3] = (uint8_t) (8-GetBitsFromMask( Amask )); - } -} - -RageSurface *CreateSurface( int width, int height, int BitsPerPixel, uint32_t Rmask, uint32_t Gmask, uint32_t Bmask, uint32_t Amask ) -{ - RageSurface *pImg = new RageSurface; - - SetupFormat( pImg->fmt, width, height, BitsPerPixel, Rmask, Gmask, Bmask, Amask ); - - pImg->w = width; - pImg->h = height; - pImg->flags = 0; - pImg->pitch = width*BitsPerPixel/8; - pImg->pixels = new uint8_t[ pImg->pitch*height ]; - - /* - if( BitsPerPixel == 8 ) - { - pImg->fmt.palette = new RageSurfacePalette; - } - */ - - return pImg; -} - -RageSurface *CreateSurfaceFrom( int width, int height, int BitsPerPixel, uint32_t Rmask, uint32_t Gmask, uint32_t Bmask, uint32_t Amask, uint8_t *pPixels, uint32_t pitch ) -{ - RageSurface *pImg = new RageSurface; - - SetupFormat( pImg->fmt, width, height, BitsPerPixel, Rmask, Gmask, Bmask, Amask ); - - pImg->w = width; - pImg->h = height; - pImg->flags = 0; - pImg->pitch = pitch; - pImg->pixels = pPixels; - pImg->pixels_owned = false; - - return pImg; -} - -/* - * (c) 2001-2004 Glenn Maynard, 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. - */ - +#include "global.h" +#include "RageSurface.h" +#include "RageUtil.h" + +bool RageSurfaceColor::operator== ( const RageSurfaceColor &rhs ) const +{ + return rhs.r == r && rhs.g == g && rhs.b == b && rhs.a == a; +} + +int32_t RageSurfacePalette::FindColor( const RageSurfaceColor &color ) const +{ + for( int i = 0; i < ncolors; ++i ) + if( colors[i] == color ) + return i; + return -1; +} + +/* XXX: untested */ +int32_t RageSurfacePalette::FindClosestColor( const RageSurfaceColor &color ) const +{ + int iBest = -1; + int iBestDist = INT_MAX; + for( int i = 0; i < ncolors; ++i ) + { + if( colors[i] == color ) + return i; + + int iDist = abs( colors[i].r - color.r ) + + abs( colors[i].g - color.g ) + + abs( colors[i].b - color.b ) + + abs( colors[i].a - color.a ); + if( iDist < iBestDist ) + { + iBestDist = iDist ; + iBest = i; + } + } + + return iBest; +} + +RageSurfaceFormat::RageSurfaceFormat(): + Rmask(Mask[0]), Gmask(Mask[1]), Bmask(Mask[2]), Amask(Mask[3]), + Rshift(Shift[0]), Gshift(Shift[1]), Bshift(Shift[2]), Ashift(Shift[3]) +{ + palette = NULL; +} + +RageSurfaceFormat::RageSurfaceFormat( const RageSurfaceFormat &cpy ): + Rmask(Mask[0]), Gmask(Mask[1]), Bmask(Mask[2]), Amask(Mask[3]), + Rshift(Shift[0]), Gshift(Shift[1]), Bshift(Shift[2]), Ashift(Shift[3]) +{ + memcpy( this, &cpy, sizeof(RageSurfaceFormat) ); + if( palette ) + palette = new RageSurfacePalette( *palette ); +} + +RageSurfaceFormat::~RageSurfaceFormat() +{ + delete palette; +} + +void RageSurfaceFormat::GetRGB( uint32_t val, uint8_t *r, uint8_t *g, uint8_t *b ) const +{ + if( BytesPerPixel == 1 ) + { + ASSERT( palette != nullptr ); + *r = palette->colors[val].r; + *g = palette->colors[val].g; + *b = palette->colors[val].b; + } else { + *r = int8_t( (val & Mask[0]) >> Shift[0] << Loss[0] ); + *g = int8_t( (val & Mask[1]) >> Shift[1] << Loss[1] ); + *b = int8_t( (val & Mask[2]) >> Shift[2] << Loss[2] ); + } +} + +bool RageSurfaceFormat::MapRGBA( uint8_t r, uint8_t g, uint8_t b, uint8_t a, uint32_t &val ) const +{ + if( BytesPerPixel == 1 ) + { + RageSurfaceColor c( r, g, b, a ); + int32_t n = palette->FindColor( c ); + if( n == -1 ) + return false; + val = (uint32_t) n; + } else { + val = + (r >> Loss[0] << Shift[0]) | + (g >> Loss[1] << Shift[1]) | + (b >> Loss[2] << Shift[2]) | + (a >> Loss[3] << Shift[3]); + } + return true; +} + +bool RageSurfaceFormat::operator== ( const RageSurfaceFormat &rhs ) const +{ + if( !Equivalent(rhs) ) + return false; + + if( BytesPerPixel == 1 ) + if( memcmp( palette, rhs.palette, sizeof(RageSurfaceFormat) ) ) + return false; + + return true; +} + +bool RageSurfaceFormat::Equivalent( const RageSurfaceFormat &rhs ) const +{ +#define COMP(a) if( a != rhs.a ) return false; + COMP( BytesPerPixel ); + COMP( Rmask ); + COMP( Gmask ); + COMP( Bmask ); + COMP( Amask ); + + return true; +} + +RageSurface::RageSurface() +{ + format = &fmt; + pixels = NULL; + pixels_owned = true; +} + +RageSurface::RageSurface( const RageSurface &cpy ) +{ + format = &fmt; + + w = cpy.w; + h = cpy.h; + pitch = cpy.pitch; + flags = cpy.flags; + pixels_owned = true; + if( cpy.pixels ) + { + pixels = new uint8_t[ pitch*h ]; + memcpy( pixels, cpy.pixels, pitch*h ); + } + else + pixels = NULL; +} + +RageSurface::~RageSurface() +{ + if( pixels_owned ) + delete [] pixels; +} + +static int GetShiftFromMask( uint32_t mask ) +{ + if( !mask ) + return 0; + + int iShift = 0; + while( (mask & 1) == 0 ) + { + mask >>= 1; + ++iShift; + } + return iShift; +} + +static int GetBitsFromMask( uint32_t mask ) +{ + if( !mask ) + return 0; + + mask >>= GetShiftFromMask(mask); + + int iBits = 0; + while( (mask & 1) == 1 ) + { + mask >>= 1; + ++iBits; + } + return iBits; +} + + +void SetupFormat( RageSurfaceFormat &fmt, + int width, int height, int BitsPerPixel, uint32_t Rmask, uint32_t Gmask, uint32_t Bmask, uint32_t Amask ) +{ + fmt.BitsPerPixel = BitsPerPixel; + fmt.BytesPerPixel = BitsPerPixel/8; + if( fmt.BytesPerPixel == 1 ) + { + ZERO( fmt.Mask ); + ZERO( fmt.Shift ); + + // Loss for paletted textures is zero; the actual palette entries are 8-bit. + ZERO( fmt.Loss ); + + fmt.palette = new RageSurfacePalette; + fmt.palette->ncolors = 256; + } + else + { + fmt.Mask[0] = Rmask; + fmt.Mask[1] = Gmask; + fmt.Mask[2] = Bmask; + fmt.Mask[3] = Amask; + + fmt.Shift[0] = GetShiftFromMask( Rmask ); + fmt.Shift[1] = GetShiftFromMask( Gmask ); + fmt.Shift[2] = GetShiftFromMask( Bmask ); + fmt.Shift[3] = GetShiftFromMask( Amask ); + + fmt.Loss[0] = (uint8_t) (8-GetBitsFromMask( Rmask )); + fmt.Loss[1] = (uint8_t) (8-GetBitsFromMask( Gmask )); + fmt.Loss[2] = (uint8_t) (8-GetBitsFromMask( Bmask )); + fmt.Loss[3] = (uint8_t) (8-GetBitsFromMask( Amask )); + } +} + +RageSurface *CreateSurface( int width, int height, int BitsPerPixel, uint32_t Rmask, uint32_t Gmask, uint32_t Bmask, uint32_t Amask ) +{ + RageSurface *pImg = new RageSurface; + + SetupFormat( pImg->fmt, width, height, BitsPerPixel, Rmask, Gmask, Bmask, Amask ); + + pImg->w = width; + pImg->h = height; + pImg->flags = 0; + pImg->pitch = width*BitsPerPixel/8; + pImg->pixels = new uint8_t[ pImg->pitch*height ]; + + /* + if( BitsPerPixel == 8 ) + { + pImg->fmt.palette = new RageSurfacePalette; + } + */ + + return pImg; +} + +RageSurface *CreateSurfaceFrom( int width, int height, int BitsPerPixel, uint32_t Rmask, uint32_t Gmask, uint32_t Bmask, uint32_t Amask, uint8_t *pPixels, uint32_t pitch ) +{ + RageSurface *pImg = new RageSurface; + + SetupFormat( pImg->fmt, width, height, BitsPerPixel, Rmask, Gmask, Bmask, Amask ); + + pImg->w = width; + pImg->h = height; + pImg->flags = 0; + pImg->pitch = pitch; + pImg->pixels = pPixels; + pImg->pixels_owned = false; + + return pImg; +} + +/* + * (c) 2001-2004 Glenn Maynard, 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. + */ + diff --git a/src/RageSurfaceUtils.cpp b/src/RageSurfaceUtils.cpp index 539491bdf1..83bb1cbfdb 100644 --- a/src/RageSurfaceUtils.cpp +++ b/src/RageSurfaceUtils.cpp @@ -1,1072 +1,1072 @@ -#include "global.h" -#include "RageSurfaceUtils.h" -#include "RageSurface.h" -#include "RageUtil.h" -#include "RageLog.h" -#include "RageFile.h" - -uint32_t RageSurfaceUtils::decodepixel( const uint8_t *p, int bpp ) -{ - switch(bpp) - { - case 1: return *p; - case 2: return *(uint16_t *)p; - case 3: - if( BYTE_ORDER == BIG_ENDIAN ) - return p[0] << 16 | p[1] << 8 | p[2]; - else - return p[0] | p[1] << 8 | p[2] << 16; - - case 4: return *(uint32_t *)p; - default: return 0; // shouldn't happen, but avoids warnings - } -} - -void RageSurfaceUtils::encodepixel( uint8_t *p, int bpp, uint32_t pixel ) -{ - switch(bpp) - { - case 1: *p = uint8_t(pixel); break; - case 2: *(uint16_t *)p = uint16_t(pixel); break; - case 3: - if( BYTE_ORDER == BIG_ENDIAN ) - { - p[0] = uint8_t((pixel >> 16) & 0xff); - p[1] = uint8_t((pixel >> 8) & 0xff); - p[2] = uint8_t(pixel & 0xff); - } else { - p[0] = uint8_t(pixel & 0xff); - p[1] = uint8_t((pixel >> 8) & 0xff); - p[2] = uint8_t((pixel >> 16) & 0xff); - } - break; - case 4: *(uint32_t *)p = pixel; break; - } -} - -// Get and set colors without scaling to 0..255. -void RageSurfaceUtils::GetRawRGBAV( uint32_t pixel, const RageSurfaceFormat &fmt, uint8_t *v ) -{ - if( fmt.BytesPerPixel == 1 ) - { - v[0] = fmt.palette->colors[pixel].r; - v[1] = fmt.palette->colors[pixel].g; - v[2] = fmt.palette->colors[pixel].b; - v[3] = fmt.palette->colors[pixel].a; - } else { - v[0] = uint8_t((pixel & fmt.Rmask) >> fmt.Rshift); - v[1] = uint8_t((pixel & fmt.Gmask) >> fmt.Gshift); - v[2] = uint8_t((pixel & fmt.Bmask) >> fmt.Bshift); - v[3] = uint8_t((pixel & fmt.Amask) >> fmt.Ashift); - } -} - -void RageSurfaceUtils::GetRawRGBAV( const uint8_t *p, const RageSurfaceFormat &fmt, uint8_t *v ) -{ - uint32_t pixel = decodepixel( p, fmt.BytesPerPixel ); - GetRawRGBAV( pixel, fmt, v ); -} - -void RageSurfaceUtils::GetRGBAV( uint32_t pixel, const RageSurface *src, uint8_t *v ) -{ - GetRawRGBAV(pixel, src->fmt, v); - const RageSurfaceFormat *fmt = src->format; - for( int c = 0; c < 4; ++c ) - v[c] = v[c] << fmt->Loss[c]; - - // Correct for surfaces that don't have an alpha channel. - if( fmt->Loss[3] == 8 ) - v[3] = 255; -} - -void RageSurfaceUtils::GetRGBAV( const uint8_t *p, const RageSurface *src, uint8_t *v ) -{ - uint32_t pixel = decodepixel(p, src->format->BytesPerPixel); - if( src->format->BytesPerPixel == 1 ) // paletted - { - memcpy( v, &src->format->palette->colors[pixel], sizeof(RageSurfaceColor)); - } - else // RGBA - GetRGBAV(pixel, src, v); -} - - -// Inverse of GetRawRGBAV. -uint32_t RageSurfaceUtils::SetRawRGBAV( const RageSurfaceFormat *fmt, const uint8_t *v ) -{ - return v[0] << fmt->Rshift | - v[1] << fmt->Gshift | - v[2] << fmt->Bshift | - v[3] << fmt->Ashift; -} - -void RageSurfaceUtils::SetRawRGBAV( uint8_t *p, const RageSurface *src, const uint8_t *v ) -{ - uint32_t pixel = SetRawRGBAV(src->format, v); - encodepixel(p, src->format->BytesPerPixel, pixel); -} - -// Inverse of GetRGBAV. -uint32_t RageSurfaceUtils::SetRGBAV( const RageSurfaceFormat *fmt, const uint8_t *v ) -{ - return (v[0] >> fmt->Loss[0]) << fmt->Shift[0] | - (v[1] >> fmt->Loss[1]) << fmt->Shift[1] | - (v[2] >> fmt->Loss[2]) << fmt->Shift[2] | - (v[3] >> fmt->Loss[3]) << fmt->Shift[3]; -} - -void RageSurfaceUtils::SetRGBAV( uint8_t *p, const RageSurface *src, const uint8_t *v ) -{ - uint32_t pixel = SetRGBAV(src->format, v); - encodepixel(p, src->format->BytesPerPixel, pixel); -} - - -void RageSurfaceUtils::GetBitsPerChannel( const RageSurfaceFormat *fmt, uint32_t bits[4] ) -{ - // The actual bits stored in each color is 8-loss. - for( int c = 0; c < 4; ++c ) - bits[c] = 8 - fmt->Loss[c]; -} - -void RageSurfaceUtils::CopySurface( const RageSurface *src, RageSurface *dest ) -{ - // Copy the palette, if we have one. - if( src->format->BitsPerPixel == 8 && dest->format->BitsPerPixel == 8 ) - { - ASSERT( dest->fmt.palette != NULL ); - *dest->fmt.palette = *src->fmt.palette; - } - - Blit( src, dest, -1, -1 ); -} - -bool RageSurfaceUtils::ConvertSurface( const RageSurface *src, RageSurface *&dst, - int width, int height, int bpp, - uint32_t R, uint32_t G, uint32_t B, uint32_t A ) -{ - dst = CreateSurface( width, height, bpp, R, G, B, A ); - - // If the formats are the same, no conversion is needed. Ignore the palette. - if( width == src->w && height == src->h && src->format->Equivalent( *dst->format ) ) - { - delete dst; - dst = NULL; - return false; - } - - CopySurface( src, dst ); - return true; -} - -void RageSurfaceUtils::ConvertSurface(RageSurface *&image, - int width, int height, int bpp, - uint32_t R, uint32_t G, uint32_t B, uint32_t A) -{ - RageSurface *ret_image; - if( !ConvertSurface( image, ret_image, width, height, bpp, R, G, B, A ) ) - return; - - delete image; - image = ret_image; -} - - -// Local helper for FixHiddenAlpha. -static void FindAlphaRGB(const RageSurface *img, uint8_t &r, uint8_t &g, uint8_t &b, bool reverse) -{ - r = g = b = 0; - - // If we have no alpha, there's no alpha color. - if( img->format->BitsPerPixel > 8 && !img->format->Amask ) - return; - - // Eww. Sorry. Iterate front-to-back or in reverse. - for(int y = reverse? img->h-1:0; - reverse? (y >=0):(y < img->h); reverse? (--y):(++y)) - { - uint8_t *row = (uint8_t *)img->pixels + img->pitch*y; - if(reverse) - row += img->format->BytesPerPixel * (img->w-1); - - for(int x = 0; x < img->w; ++x) - { - uint32_t val = RageSurfaceUtils::decodepixel(row, img->format->BytesPerPixel); - if( img->format->BitsPerPixel == 8 ) - { - if( img->format->palette->colors[val].a ) - { - // This color isn't fully transparent, so grab it. - r = img->format->palette->colors[val].r; - g = img->format->palette->colors[val].g; - b = img->format->palette->colors[val].b; - return; - } - } - else - { - if( val & img->format->Amask ) - { - // This color isn't fully transparent, so grab it. - img->format->GetRGB( val, &r, &g, &b ); - return; - } - } - - if( reverse ) - row -= img->format->BytesPerPixel; - else - row += img->format->BytesPerPixel; - } - } - - // Huh? The image is completely transparent. - r = g = b = 0; -} - -/* Local helper for FixHiddenAlpha. Set the underlying RGB values of all pixels - * in img that are completely transparent. */ -static void SetAlphaRGB(const RageSurface *pImg, uint8_t r, uint8_t g, uint8_t b) -{ - // If it's a paletted surface, all we have to do is change the palette. - if( pImg->format->BitsPerPixel == 8 ) - { - for( int c = 0; c < pImg->format->palette->ncolors; ++c ) - { - if( pImg->format->palette->colors[c].a ) - continue; - pImg->format->palette->colors[c].r = r; - pImg->format->palette->colors[c].g = g; - pImg->format->palette->colors[c].b = b; - } - return; - } - - // If it's RGBA and there's no alpha channel, we have nothing to do. - if( pImg->format->BitsPerPixel > 8 && !pImg->format->Amask ) - return; - - uint32_t trans; - pImg->format->MapRGBA( r, g, b, 0, trans ); - for( int y = 0; y < pImg->h; ++y ) - { - uint8_t *row = pImg->pixels + pImg->pitch*y; - - for( int x = 0; x < pImg->w; ++x ) - { - uint32_t val = RageSurfaceUtils::decodepixel( row, pImg->format->BytesPerPixel ); - if( val != trans && !(val&pImg->format->Amask) ) - { - RageSurfaceUtils::encodepixel( row, pImg->format->BytesPerPixel, trans ); - } - - row += pImg->format->BytesPerPixel; - } - } -} - -/* When we scale up images (which we always do in high res), pixels - * that are completely transparent can be blended with opaque pixels, - * causing their RGB elements to show. This is visible in many textures - * as a pixel-wide border in the wrong color. This is tricky to fix. - * We need to set the RGB components of completely transparent pixels - * to a reasonable color. - * - * Most images have a single border color. For these, the transparent - * color is easy: search through the image top-bottom-left-right, - * find the first non-transparent pixel, and pull out its RGB. - * - * A few images don't. We can only make a guess here. After the above - * search, do the same in reverse (bottom-top-right-left). If the color - * we find is different, just set the border color to black. - */ -void RageSurfaceUtils::FixHiddenAlpha( RageSurface *pImg ) -{ - // If there are no alpha bits, there's nothing to fix. - if( pImg->format->BitsPerPixel != 8 && pImg->format->Amask == 0 ) - return; - - uint8_t r, g, b; - FindAlphaRGB( pImg, r, g, b, false ); - - uint8_t cr, cg, cb; // compare - FindAlphaRGB( pImg, cr, cg, cb, true ); - - if( cr != r || cg != g || cb != b ) - r = g = b = 0; - - SetAlphaRGB( pImg, r, g, b ); -} - -/* Scan the surface to see what level of alpha it uses. This can be used to - * find the best surface format for a texture; eg. a TRAIT_BOOL_TRANSPARENCY or - * TRAIT_NO_TRANSPARENCY surface can use RGB5A1 instead of RGBA4 for greater - * color resolution; a TRAIT_NO_TRANSPARENCY could also use R5G6B5. */ -int RageSurfaceUtils::FindSurfaceTraits( const RageSurface *img ) -{ - const int NEEDS_NO_ALPHA=0, NEEDS_BOOL_ALPHA=1, NEEDS_FULL_ALPHA=2; - int alpha_type = NEEDS_NO_ALPHA; - - uint32_t max_alpha; - if( img->format->BitsPerPixel == 8 ) - { - // Short circuit if we already know we have no transparency. - bool bHaveNonOpaque = false; - for( int c = 0; !bHaveNonOpaque && c < img->format->palette->ncolors; ++c ) - { - if( img->format->palette->colors[c].a != 0xFF ) - bHaveNonOpaque = true; - } - - if( !bHaveNonOpaque ) - return TRAIT_NO_TRANSPARENCY; - - max_alpha = 0xFF; - } - else - { - // Short circuit if we already know we have no transparency. - if( img->format->Amask == 0 ) - return TRAIT_NO_TRANSPARENCY; - - max_alpha = img->format->Amask; - } - - for(int y = 0; y < img->h; ++y) - { - uint8_t *row = (uint8_t *)img->pixels + img->pitch*y; - - for(int x = 0; x < img->w; ++x) - { - uint32_t val = decodepixel(row, img->format->BytesPerPixel); - - uint32_t alpha; - if( img->format->BitsPerPixel == 8 ) - alpha = img->format->palette->colors[val].a; - else - alpha = (val & img->format->Amask); - - if( alpha == 0 ) - alpha_type = max( alpha_type, NEEDS_BOOL_ALPHA ); - else if( alpha != max_alpha ) - alpha_type = max( alpha_type, NEEDS_FULL_ALPHA ); - - row += img->format->BytesPerPixel; - } - } - - int ret = 0; - switch( alpha_type ) - { - case NEEDS_NO_ALPHA: ret |= TRAIT_NO_TRANSPARENCY; break; - case NEEDS_BOOL_ALPHA: ret |= TRAIT_BOOL_TRANSPARENCY; break; - case NEEDS_FULL_ALPHA: break; - default: - FAIL_M(ssprintf("Invalid alpha type: %i", alpha_type)); - } - - return ret; -} - - -// Local helper for BlitTransform. -static inline void GetRawRGBAV_XY( const RageSurface *src, uint8_t *v, int x, int y ) -{ - const uint8_t *srcp = (const uint8_t *) src->pixels + (y * src->pitch); - const uint8_t *srcpx = srcp + (x * src->fmt.BytesPerPixel); - - RageSurfaceUtils::GetRawRGBAV( srcpx, src->fmt, v ); -} - -static inline float scale( float x, float l1, float h1, float l2, float h2 ) -{ - return ((x - l1) / (h1 - l1) * (h2 - l2) + l2); -} - -// Completely unoptimized. -void RageSurfaceUtils::BlitTransform( const RageSurface *src, RageSurface *dst, - const float fCoords[8] /* TL, BR, BL, TR */ ) -{ - ASSERT( src->format->BytesPerPixel == dst->format->BytesPerPixel ); - - const float Coords[8] = { - (fCoords[0] * (src->w)), (fCoords[1] * (src->h)), - (fCoords[2] * (src->w)), (fCoords[3] * (src->h)), - (fCoords[4] * (src->w)), (fCoords[5] * (src->h)), - (fCoords[6] * (src->w)), (fCoords[7] * (src->h)) - }; - - const int TL_X = 0, TL_Y = 1, BL_X = 2, BL_Y = 3, - BR_X = 4, BR_Y = 5, TR_X = 6, TR_Y = 7; - - for( int y = 0; y < dst->h; ++y ) - { - uint8_t *dstp = (uint8_t *) dst->pixels + (y * dst->pitch); /* line */ - uint8_t *dstpx = dstp; // pixel - - const float start_y = scale(float(y), 0, float(dst->h), Coords[TL_Y], Coords[BL_Y]); - const float end_y = scale(float(y), 0, float(dst->h), Coords[TR_Y], Coords[BR_Y]); - - const float start_x = scale(float(y), 0, float(dst->h), Coords[TL_X], Coords[BL_X]); - const float end_x = scale(float(y), 0, float(dst->h), Coords[TR_X], Coords[BR_X]); - - for( int x = 0; x < dst->w; ++x ) - { - const float src_xp = scale(float(x), 0, float(dst->w), start_x, end_x); - const float src_yp = scale(float(x), 0, float(dst->w), start_y, end_y); - - /* If the surface is two pixels wide, src_xp is 0..2. .5 indicates - * pixel[0]; 1 indicates 50% pixel[0], 50% pixel[1]; 1.5 indicates - * pixel[1]; 2 indicates 50% pixel[1], 50% pixel[2] (which is clamped - * to pixel[1]). */ - int src_x[2], src_y[2]; - src_x[0] = (int) truncf(src_xp - 0.5f); - src_x[1] = src_x[0] + 1; - - src_y[0] = (int) truncf(src_yp - 0.5f); - src_y[1] = src_y[0] + 1; - - // Emulate GL_REPEAT. - src_x[0] = clamp(src_x[0], 0, src->w); - src_x[1] = clamp(src_x[1], 0, src->w); - src_y[0] = clamp(src_y[0], 0, src->h); - src_y[1] = clamp(src_y[1], 0, src->h); - - // Decode our four pixels. - uint8_t v[4][4]; - GetRawRGBAV_XY(src, v[0], src_x[0], src_y[0]); - GetRawRGBAV_XY(src, v[1], src_x[0], src_y[1]); - GetRawRGBAV_XY(src, v[2], src_x[1], src_y[0]); - GetRawRGBAV_XY(src, v[3], src_x[1], src_y[1]); - - // Distance from the pixel chosen: - float weight_x = src_xp - (src_x[0] + 0.5f); - float weight_y = src_yp - (src_y[0] + 0.5f); - - // Filter: - uint8_t out[4] = { 0,0,0,0 }; - for(int i = 0; i < 4; ++i) - { - float sum = 0; - sum += v[0][i] * (1-weight_x) * (1-weight_y); - sum += v[1][i] * (1-weight_x) * (weight_y); - sum += v[2][i] * (weight_x) * (1-weight_y); - sum += v[3][i] * (weight_x) * (weight_y); - out[i] = (uint8_t) clamp( lrintf(sum), 0L, 255L ); - } - - // If the source has no alpha, set the destination to opaque. - if( src->format->Amask == 0 ) - out[3] = uint8_t( dst->format->Amask >> dst->format->Ashift ); - - SetRawRGBAV(dstpx, dst, out); - - dstpx += dst->format->BytesPerPixel; - } - } -} - - -/* Simplified: - * - * No source alpha. - * Palette -> palette blits assume the palette is identical (no mapping). - * No color key. - * No general blitting rects. */ - -static bool blit_same_type( const RageSurface *src_surf, const RageSurface *dst_surf, int width, int height ) -{ - if( src_surf->format->BytesPerPixel != dst_surf->format->BytesPerPixel || - src_surf->format->Rmask != dst_surf->format->Rmask || - src_surf->format->Gmask != dst_surf->format->Gmask || - src_surf->format->Bmask != dst_surf->format->Bmask || - src_surf->format->Amask != dst_surf->format->Amask ) - return false; - - const uint8_t *src = src_surf->pixels; - uint8_t *dst = dst_surf->pixels; - - // If possible, memcpy the whole thing. - if( src_surf->w == width && dst_surf->w == width && src_surf->pitch == dst_surf->pitch ) - { - memcpy( dst, src, height*src_surf->pitch ); - return true; - } - - // The rows don't line up, so memcpy row by row. - while( height-- ) - { - memcpy( dst, src, width*src_surf->format->BytesPerPixel ); - src += src_surf->pitch; - dst += dst_surf->pitch; - } - - return true; -} - -/* Rescaling blit with no ckey. This is used to update movies in - * D3D, so optimization is very important. */ -static bool blit_rgba_to_rgba( const RageSurface *src_surf, const RageSurface *dst_surf, int width, int height ) -{ - if( src_surf->format->BytesPerPixel == 1 || dst_surf->format->BytesPerPixel == 1 ) - return false; - - const uint8_t *src = src_surf->pixels; - uint8_t *dst = dst_surf->pixels; - - // Bytes to skip at the end of a line. - const int srcskip = src_surf->pitch - width*src_surf->format->BytesPerPixel; - const int dstskip = dst_surf->pitch - width*dst_surf->format->BytesPerPixel; - - const uint32_t *src_shifts = src_surf->format->Shift; - const uint32_t *dst_shifts = dst_surf->format->Shift; - const uint32_t *src_masks = src_surf->format->Mask; - const uint32_t *dst_masks = dst_surf->format->Mask; - - uint8_t lookup[4][256]; - for( int c = 0; c < 4; ++c ) - { - const uint32_t max_src_val = src_masks[c] >> src_shifts[c]; - const uint32_t max_dst_val = dst_masks[c] >> dst_shifts[c]; - ASSERT( max_src_val <= 0xFF ); - ASSERT( max_dst_val <= 0xFF ); - - if( src_masks[c] == 0 ) - { - /* The source is missing a channel. Alpha defaults to opaque, other - * channels default to 0. */ - if( c == 3 ) - lookup[c][0] = (uint8_t) max_dst_val; - else - lookup[c][0] = 0; - } else { - /* Calculate a color conversion table. There are a few ways we can do - * this (each list is the resulting table for 4->2 bit): - * - * SCALE( i, 0, max_src_val+1, 0, max_dst_val+1 ); - * { 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3 } - * SCALE( i, 0, max_src_val, 0, max_dst_val ); - * { 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 3 } - * lrintf( ((float) i / max_src_val) * max_dst_val ) - * { 0, 0, 0, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3 } - * - * We use the first for increasing resolution, since it gives the most even - * distribution. - * - * 2->4 bit: - * SCALE( i, 0, max_src_val+1, 0, max_dst_val+1 ); - * { 0, 4, 8, 12 } - * SCALE( i, 0, max_src_val, 0, max_dst_val ); - * { 0, 5, 10, 15 } - * lrintf( ((float) i / max_src_val) * max_dst_val ) - * { 0, 5, 10, 15 } - * - * The latter two are equivalent and give an even distribution; we use the - * second, since the first doesn't scale max_src_val to max_dst_val. - * - * Having separate formulas for increasing and decreasing resolution seems - * strange; what's wrong here? */ - if( max_src_val > max_dst_val ) - for( uint32_t i = 0; i <= max_src_val; ++i ) - lookup[c][i] = (uint8_t) SCALE( i, 0, max_src_val+1, 0, max_dst_val+1 ); - else - for( uint32_t i = 0; i <= max_src_val; ++i ) - lookup[c][i] = (uint8_t) SCALE( i, 0, max_src_val, 0, max_dst_val ); - } - } - - while( height-- ) - { - int x = 0; - while( x++ < width ) - { - unsigned int pixel = RageSurfaceUtils::decodepixel( src, src_surf->format->BytesPerPixel ); - - // Convert pixel to the destination format. - unsigned int opixel = 0; - for( int c = 0; c < 4; ++c ) - { - int lSrc = (pixel & src_masks[c]) >> src_shifts[c]; - opixel |= lookup[c][lSrc] << dst_shifts[c]; - } - - // Store it. - RageSurfaceUtils::encodepixel( dst, dst_surf->format->BytesPerPixel, opixel ); - - src += src_surf->format->BytesPerPixel; - dst += dst_surf->format->BytesPerPixel; - } - - src += srcskip; - dst += dstskip; - } - - return true; -} - -static bool blit_generic( const RageSurface *src_surf, const RageSurface *dst_surf, int width, int height ) -{ - if( src_surf->format->BytesPerPixel != 1 || dst_surf->format->BytesPerPixel == 1 ) - return false; - - const uint8_t *src = src_surf->pixels; - uint8_t *dst = dst_surf->pixels; - - // Bytes to skip at the end of a line. - const int srcskip = src_surf->pitch - width*src_surf->format->BytesPerPixel; - const int dstskip = dst_surf->pitch - width*dst_surf->format->BytesPerPixel; - - while( height-- ) - { - int x = 0; - while( x++ < width ) - { - unsigned int pixel = RageSurfaceUtils::decodepixel( src, src_surf->format->BytesPerPixel ); - - uint8_t colors[4]; - // Convert pixel to the destination RGBA. - colors[0] = src_surf->format->palette->colors[pixel].r; - colors[1] = src_surf->format->palette->colors[pixel].g; - colors[2] = src_surf->format->palette->colors[pixel].b; - colors[3] = src_surf->format->palette->colors[pixel].a; - pixel = RageSurfaceUtils::SetRGBAV(dst_surf->format, colors); - - // Store it. - RageSurfaceUtils::encodepixel( dst, dst_surf->format->BytesPerPixel, pixel ); - - src += src_surf->format->BytesPerPixel; - dst += dst_surf->format->BytesPerPixel; - } - - src += srcskip; - dst += dstskip; - } - - return true; -} - -// Blit src onto dst. -void RageSurfaceUtils::Blit( const RageSurface *src, RageSurface *dst, int width, int height ) -{ - if( width == -1 ) - width = src->w; - if( height == -1 ) - height = src->h; - width = min( src->w, dst->w ); - height = min( src->h, dst->h ); - - /* Try each blit until we find one that works; run them in order of efficiency, - * so we use the fastest blit possible. */ - do - { - // RGBA->RGBA with the same format, or PAL->PAL. Simple copy. - if( blit_same_type(src, dst, width, height) ) - break; - - // RGBA->RGBA with different formats. - if( blit_rgba_to_rgba(src, dst, width, height) ) - break; - - // PAL->RGBA. - if( blit_generic(src, dst, width, height) ) - break; - - FAIL_M("We don't do RGBA->PAL"); - } while(0); - - /* The destination surface may be larger than the source. For example, we may be - * blitting a 200x200 image onto a 256x256 surface for OpenGL. Normally, that extra - * space isn't actually used; we'll only render the image space. However, bilinear - * filtering will cause the lines of pixels at 201x... and ...x201 to be visible. We - * need to make sure those pixels make sense. - * - * Previously, we just cleared the image to transparent or the color key. This - * has two problems. First, we may not have space for a color key (an image with - * 256 non-transparent palette colors). Second, that's not completely correct; - * it'll force the outside border of the image to filter to transparent. If the image - * is being tiled with another image, that may leave seams. - * - * (In some cases, filtering to transparent is preferable, particularly when displaying - * a sprite in perspective. If you want that, add blank space to the image explicitly.) - * - * Copy the last column (200x... -> 201x...), then the last row (...x200 -> ...x201). */ - - CorrectBorderPixels( dst, width, height ); -} - -/* If only width x height of img is actually going to be used, and there's extra - * space on the surface, duplicate the last row and column to ensure that we don't - * pull in unexpected data when rendering with bilinear filtering. - * - * We do this if there's memory available, even if that space extends outside - * of the image (in the per-line padding or after the end). This way, surfaces - * can be padded to power-of-two dimensions by the image loaders, and if no other - * adjustments are needed, they can be passed directly to the renderer without - * doing any extra copies. */ -void RageSurfaceUtils::CorrectBorderPixels( RageSurface *img, int width, int height ) -{ - if( width*img->fmt.BytesPerPixel < img->pitch ) - { - // Duplicate the last column. - int offset = img->format->BytesPerPixel * (width-1); - uint8_t *p = (uint8_t *) img->pixels + offset; - - for( int y = 0; y < height; ++y ) - { - uint32_t pixel = decodepixel( p, img->format->BytesPerPixel ); - encodepixel( p+img->format->BytesPerPixel, img->format->BytesPerPixel, pixel ); - - p += img->pitch; - } - } - - if( height < img->h ) - { - // Duplicate the last row. - uint8_t *srcp = img->pixels; - srcp += img->pitch * (height-1); - memcpy( srcp + img->pitch, srcp, img->pitch ); - } -} - -struct SurfaceHeader -{ - int width, height, pitch; - int Rmask, Gmask, Bmask, Amask; - int bpp; -}; - -// Save and load RageSurfaces to disk, in a very fast, nonportable way. -bool RageSurfaceUtils::SaveSurface( const RageSurface *img, RString file ) -{ - RageFile f; - if( !f.Open( file, RageFile::WRITE ) ) - return false; - - SurfaceHeader h; - memset( &h, 0, sizeof(h) ); - - h.height = img->h; - h.width = img->w; - h.pitch = img->pitch; - h.Rmask = img->format->Rmask; - h.Gmask = img->format->Gmask; - h.Bmask = img->format->Bmask; - h.Amask = img->format->Amask; - h.bpp = img->format->BitsPerPixel; - - f.Write( &h, sizeof(h) ); - - if( h.bpp == 8 ) - { - f.Write( &img->format->palette->ncolors, sizeof(img->format->palette->ncolors) ); - f.Write( img->format->palette->colors, img->format->palette->ncolors * sizeof(RageSurfaceColor) ); - } - - f.Write( img->pixels, img->h * img->pitch ); - - return true; -} - -RageSurface *RageSurfaceUtils::LoadSurface( RString file ) -{ - RageFile f; - if( !f.Open( file ) ) - return NULL; - - SurfaceHeader h; - if( f.Read( &h, sizeof(h) ) != sizeof(h) ) - return NULL; - - RageSurfacePalette palette; - if( h.bpp == 8 ) - { - if( f.Read( &palette.ncolors, sizeof(palette.ncolors) ) != sizeof(palette.ncolors) ) - return NULL; - ASSERT_M( palette.ncolors <= 256, ssprintf("%i", palette.ncolors) ); - if( f.Read( palette.colors, palette.ncolors * sizeof(RageSurfaceColor) ) != int(palette.ncolors * sizeof(RageSurfaceColor)) ) - return NULL; - } - - // Create the surface. - RageSurface *img = CreateSurface( h.width, h.height, h.bpp, - h.Rmask, h.Gmask, h.Bmask, h.Amask ); - ASSERT( img != NULL ); - - /* If the pitch has changed, this surface is either corrupt, or was - * created with a different version whose CreateSurface() behavior - * was different. */ - if( h.pitch != img->pitch ) - { - LOG->Trace( "Error loading \"%s\": expected pitch %i, got %i (%ibpp, %i width)", - file.c_str(), h.pitch, img->pitch, h.bpp, h.width ); - delete img; - return NULL; - } - - if( f.Read( img->pixels, h.height * h.pitch ) != h.height * h.pitch ) - { - delete img; - return NULL; - } - - // Set the palette. - if( h.bpp == 8 ) - *img->fmt.palette = palette; - - return img; -} - -/* This converts an image to a special 8-bit paletted format. The palette is set up - * so that palette indexes look like regular, packed components. - * - * For example, an image with 8 bits of grayscale and 0 bits of alpha has a palette - * that looks like { 0,0,0,255 }, { 1,1,1,255 }, { 2,2,2,255 }, ... { 255,255,255,255 }. - * This results in index components that can be treated as grayscale values. - * - * An image with 2 bits of grayscale and 2 bits of alpha look like - * { 0,0,0,0 }, { 85,85,85,0 }, { 170,170,170,0 }, { 255,255,255,0 }, - * { 0,0,0,85 }, { 85,85,85,85 }, { 170,170,170,85 }, { 255,255,255,85 }, ... - * - * This results in index components that can be pulled apart like regular packed - * values: the first two bits of the index are the grayscale component, and the next - * two bits are the alpha component. - * - * This gives us a generic way to handle arbitrary 8-bit texture formats. */ -RageSurface *RageSurfaceUtils::PalettizeToGrayscale( const RageSurface *src_surf, int GrayBits, int AlphaBits ) -{ - AlphaBits = min( AlphaBits, 8-src_surf->format->Loss[3] ); - - const int TotalBits = GrayBits + AlphaBits; - ASSERT( TotalBits <= 8 ); - - RageSurface *dst_surf = CreateSurface(src_surf->w, src_surf->h, - 8, 0,0,0,0 ); - - // Set up the palette. - const int TotalColors = 1 << TotalBits; - const int Ivalues = 1 << GrayBits; // number of intensity values - const int Ishift = 0; // intensity shift - const int Imask = ((1 << GrayBits) - 1) << Ishift; // intensity mask - const int Iloss = 8-GrayBits; - - const int Avalues = 1 << AlphaBits; // number of alpha values - const int Ashift = GrayBits; // alpha shift - const int Amask = ((1 << AlphaBits) - 1) << Ashift; // alpha mask - const int Aloss = 8-AlphaBits; - - for( int index = 0; index < TotalColors; ++index ) - { - const int I = (index & Imask) >> Ishift; - const int A = (index & Amask) >> Ashift; - - int ScaledI; - if( Ivalues == 1 ) - ScaledI = 255; // if only one intensity value, always fullbright - else - ScaledI = clamp( lrintf(I * (255.0f / (Ivalues-1))), 0L, 255L ); - - int ScaledA; - if( Avalues == 1 ) - ScaledA = 255; // if only one alpha value, always opaque - else - ScaledA = clamp( lrintf(A * (255.0f / (Avalues-1))), 0L, 255L ); - - RageSurfaceColor c; - c.r = uint8_t(ScaledI); - c.g = uint8_t(ScaledI); - c.b = uint8_t(ScaledI); - c.a = uint8_t(ScaledA); - - dst_surf->fmt.palette->colors[index] = c; - } - - const uint8_t *src = src_surf->pixels; - uint8_t *dst = dst_surf->pixels; - - int height = src_surf->h; - int width = src_surf->w; - - // Bytes to skip at the end of a line. - const int srcskip = src_surf->pitch - width*src_surf->format->BytesPerPixel; - const int dstskip = dst_surf->pitch - width*dst_surf->format->BytesPerPixel; - - while( height-- ) - { - int x = 0; - while( x++ < width ) - { - unsigned int pixel = decodepixel( src, src_surf->format->BytesPerPixel ); - - uint8_t colors[4]; - GetRGBAV(pixel, src_surf, colors); - - int Ival = 0; - Ival += colors[0]; - Ival += colors[1]; - Ival += colors[2]; - Ival /= 3; - - pixel = (Ival >> Iloss) << Ishift | - (colors[3] >> Aloss) << Ashift; - - // Store it. - *dst = uint8_t(pixel); - - src += src_surf->format->BytesPerPixel; - dst += dst_surf->format->BytesPerPixel; - } - - src += srcskip; - dst += dstskip; - } - - return dst_surf; -} - - -RageSurface *RageSurfaceUtils::MakeDummySurface( int height, int width ) -{ - RageSurface *ret_image = CreateSurface( width, height, 8, 0,0,0,0 ); - - RageSurfaceColor pink( 0xFF, 0x10, 0xFF, 0xFF ); - ret_image->fmt.palette->colors[0] = pink; - - memset( ret_image->pixels, 0, ret_image->h*ret_image->pitch ); - - return ret_image; -} - -/* HACK: Some banners and textures have #F800F8 as the color key. - * Search the edge for it; if we find it, use that as the color key. */ -static bool ImageUsesOffHotPink( const RageSurface *img ) -{ - uint32_t OffHotPink; - if( !img->format->MapRGBA( 0xF8, 0, 0xF8, 0xFF, OffHotPink ) ) - return false; - - const uint8_t *p = img->pixels; - for( int x = 0; x < img->w; ++x ) - { - uint32_t val = RageSurfaceUtils::decodepixel( p, img->format->BytesPerPixel ); - if( val == OffHotPink ) - return true; - p += img->format->BytesPerPixel; - } - - p = img->pixels; - p += img->pitch * (img->h-1); - for( int i=0; i < img->w; i++ ) - { - uint32_t val = RageSurfaceUtils::decodepixel( p, img->format->BytesPerPixel ); - if( val == OffHotPink ) - return true; - p += img->format->BytesPerPixel; - } - return false; -} - -/* Set #FF00FF and #F800F8 to transparent. img may be reallocated if it has no - * alpha bits. */ -void RageSurfaceUtils::ApplyHotPinkColorKey( RageSurface *&img ) -{ - if( img->format->BitsPerPixel == 8 ) - { - uint32_t color; - if( img->format->MapRGBA( 0xF8, 0, 0xF8, 0xFF, color ) ) - img->format->palette->colors[ color ].a = 0; - if( img->format->MapRGBA( 0xFF, 0, 0xFF, 0xFF, color ) ) - img->format->palette->colors[ color ].a = 0; - return; - } - - // RGBA. Make sure we have alpha. - if( img->format->Amask == 0 ) - { - // We don't have any alpha. Try to enable it without copying. - /* XXX: need to scan the surface and make sure the new alpha bit is always 1 */ - /* - const int used_bits = img->format->Rmask | img->format->Gmask | img->format->Bmask; - - for( int i = 0; img->format->Amask == 0 && i < img->format->BitsPerPixel; ++i ) - { - if( (used_bits & (1<format->Amask = 1<format->Aloss = 7; - img->format->Ashift = (uint8_t) i; - } - } - */ - // If we didn't have any free bits, convert to make room. - if( img->format->Amask == 0 ) - ConvertSurface( img, img->w, img->h, - 32, 0xFF000000, 0x00FF0000, 0x0000FF00, 0x000000FF ); - } - - uint32_t HotPink; - - bool bHaveColorKey; - if( ImageUsesOffHotPink(img) ) - bHaveColorKey = img->format->MapRGBA( 0xF8, 0, 0xF8, 0xFF, HotPink ); - else - bHaveColorKey = img->format->MapRGBA( 0xFF, 0, 0xFF, 0xFF, HotPink ); - if( !bHaveColorKey ) - return; - - for( int y = 0; y < img->h; ++y ) - { - uint8_t *row = img->pixels + img->pitch*y; - - for( int x = 0; x < img->w; ++x ) - { - uint32_t val = decodepixel( row, img->format->BytesPerPixel ); - if( val == HotPink ) - encodepixel( row, img->format->BytesPerPixel, 0 ); - - row += img->format->BytesPerPixel; - } - } -} - -void RageSurfaceUtils::FlipVertically( RageSurface *img ) -{ - const int pitch = img->pitch; - const int bytes_per_row = img->format->BytesPerPixel * img->w; - char *row = new char[bytes_per_row]; - - for( int y=0; y < img->h/2; y++ ) - { - int y2 = img->h-1-y; - memcpy( row, img->pixels + pitch * y, bytes_per_row ); - memcpy( img->pixels + pitch * y, img->pixels + pitch * y2, bytes_per_row ); - memcpy( img->pixels + pitch * y2, row, bytes_per_row ); - } - - delete [] row; -} - -/* - * (c) 2001-2004 Glenn Maynard, 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. - */ +#include "global.h" +#include "RageSurfaceUtils.h" +#include "RageSurface.h" +#include "RageUtil.h" +#include "RageLog.h" +#include "RageFile.h" + +uint32_t RageSurfaceUtils::decodepixel( const uint8_t *p, int bpp ) +{ + switch(bpp) + { + case 1: return *p; + case 2: return *(uint16_t *)p; + case 3: + if( BYTE_ORDER == BIG_ENDIAN ) + return p[0] << 16 | p[1] << 8 | p[2]; + else + return p[0] | p[1] << 8 | p[2] << 16; + + case 4: return *(uint32_t *)p; + default: return 0; // shouldn't happen, but avoids warnings + } +} + +void RageSurfaceUtils::encodepixel( uint8_t *p, int bpp, uint32_t pixel ) +{ + switch(bpp) + { + case 1: *p = uint8_t(pixel); break; + case 2: *(uint16_t *)p = uint16_t(pixel); break; + case 3: + if( BYTE_ORDER == BIG_ENDIAN ) + { + p[0] = uint8_t((pixel >> 16) & 0xff); + p[1] = uint8_t((pixel >> 8) & 0xff); + p[2] = uint8_t(pixel & 0xff); + } else { + p[0] = uint8_t(pixel & 0xff); + p[1] = uint8_t((pixel >> 8) & 0xff); + p[2] = uint8_t((pixel >> 16) & 0xff); + } + break; + case 4: *(uint32_t *)p = pixel; break; + } +} + +// Get and set colors without scaling to 0..255. +void RageSurfaceUtils::GetRawRGBAV( uint32_t pixel, const RageSurfaceFormat &fmt, uint8_t *v ) +{ + if( fmt.BytesPerPixel == 1 ) + { + v[0] = fmt.palette->colors[pixel].r; + v[1] = fmt.palette->colors[pixel].g; + v[2] = fmt.palette->colors[pixel].b; + v[3] = fmt.palette->colors[pixel].a; + } else { + v[0] = uint8_t((pixel & fmt.Rmask) >> fmt.Rshift); + v[1] = uint8_t((pixel & fmt.Gmask) >> fmt.Gshift); + v[2] = uint8_t((pixel & fmt.Bmask) >> fmt.Bshift); + v[3] = uint8_t((pixel & fmt.Amask) >> fmt.Ashift); + } +} + +void RageSurfaceUtils::GetRawRGBAV( const uint8_t *p, const RageSurfaceFormat &fmt, uint8_t *v ) +{ + uint32_t pixel = decodepixel( p, fmt.BytesPerPixel ); + GetRawRGBAV( pixel, fmt, v ); +} + +void RageSurfaceUtils::GetRGBAV( uint32_t pixel, const RageSurface *src, uint8_t *v ) +{ + GetRawRGBAV(pixel, src->fmt, v); + const RageSurfaceFormat *fmt = src->format; + for( int c = 0; c < 4; ++c ) + v[c] = v[c] << fmt->Loss[c]; + + // Correct for surfaces that don't have an alpha channel. + if( fmt->Loss[3] == 8 ) + v[3] = 255; +} + +void RageSurfaceUtils::GetRGBAV( const uint8_t *p, const RageSurface *src, uint8_t *v ) +{ + uint32_t pixel = decodepixel(p, src->format->BytesPerPixel); + if( src->format->BytesPerPixel == 1 ) // paletted + { + memcpy( v, &src->format->palette->colors[pixel], sizeof(RageSurfaceColor)); + } + else // RGBA + GetRGBAV(pixel, src, v); +} + + +// Inverse of GetRawRGBAV. +uint32_t RageSurfaceUtils::SetRawRGBAV( const RageSurfaceFormat *fmt, const uint8_t *v ) +{ + return v[0] << fmt->Rshift | + v[1] << fmt->Gshift | + v[2] << fmt->Bshift | + v[3] << fmt->Ashift; +} + +void RageSurfaceUtils::SetRawRGBAV( uint8_t *p, const RageSurface *src, const uint8_t *v ) +{ + uint32_t pixel = SetRawRGBAV(src->format, v); + encodepixel(p, src->format->BytesPerPixel, pixel); +} + +// Inverse of GetRGBAV. +uint32_t RageSurfaceUtils::SetRGBAV( const RageSurfaceFormat *fmt, const uint8_t *v ) +{ + return (v[0] >> fmt->Loss[0]) << fmt->Shift[0] | + (v[1] >> fmt->Loss[1]) << fmt->Shift[1] | + (v[2] >> fmt->Loss[2]) << fmt->Shift[2] | + (v[3] >> fmt->Loss[3]) << fmt->Shift[3]; +} + +void RageSurfaceUtils::SetRGBAV( uint8_t *p, const RageSurface *src, const uint8_t *v ) +{ + uint32_t pixel = SetRGBAV(src->format, v); + encodepixel(p, src->format->BytesPerPixel, pixel); +} + + +void RageSurfaceUtils::GetBitsPerChannel( const RageSurfaceFormat *fmt, uint32_t bits[4] ) +{ + // The actual bits stored in each color is 8-loss. + for( int c = 0; c < 4; ++c ) + bits[c] = 8 - fmt->Loss[c]; +} + +void RageSurfaceUtils::CopySurface( const RageSurface *src, RageSurface *dest ) +{ + // Copy the palette, if we have one. + if( src->format->BitsPerPixel == 8 && dest->format->BitsPerPixel == 8 ) + { + ASSERT( dest->fmt.palette != nullptr ); + *dest->fmt.palette = *src->fmt.palette; + } + + Blit( src, dest, -1, -1 ); +} + +bool RageSurfaceUtils::ConvertSurface( const RageSurface *src, RageSurface *&dst, + int width, int height, int bpp, + uint32_t R, uint32_t G, uint32_t B, uint32_t A ) +{ + dst = CreateSurface( width, height, bpp, R, G, B, A ); + + // If the formats are the same, no conversion is needed. Ignore the palette. + if( width == src->w && height == src->h && src->format->Equivalent( *dst->format ) ) + { + delete dst; + dst = NULL; + return false; + } + + CopySurface( src, dst ); + return true; +} + +void RageSurfaceUtils::ConvertSurface(RageSurface *&image, + int width, int height, int bpp, + uint32_t R, uint32_t G, uint32_t B, uint32_t A) +{ + RageSurface *ret_image; + if( !ConvertSurface( image, ret_image, width, height, bpp, R, G, B, A ) ) + return; + + delete image; + image = ret_image; +} + + +// Local helper for FixHiddenAlpha. +static void FindAlphaRGB(const RageSurface *img, uint8_t &r, uint8_t &g, uint8_t &b, bool reverse) +{ + r = g = b = 0; + + // If we have no alpha, there's no alpha color. + if( img->format->BitsPerPixel > 8 && !img->format->Amask ) + return; + + // Eww. Sorry. Iterate front-to-back or in reverse. + for(int y = reverse? img->h-1:0; + reverse? (y >=0):(y < img->h); reverse? (--y):(++y)) + { + uint8_t *row = (uint8_t *)img->pixels + img->pitch*y; + if(reverse) + row += img->format->BytesPerPixel * (img->w-1); + + for(int x = 0; x < img->w; ++x) + { + uint32_t val = RageSurfaceUtils::decodepixel(row, img->format->BytesPerPixel); + if( img->format->BitsPerPixel == 8 ) + { + if( img->format->palette->colors[val].a ) + { + // This color isn't fully transparent, so grab it. + r = img->format->palette->colors[val].r; + g = img->format->palette->colors[val].g; + b = img->format->palette->colors[val].b; + return; + } + } + else + { + if( val & img->format->Amask ) + { + // This color isn't fully transparent, so grab it. + img->format->GetRGB( val, &r, &g, &b ); + return; + } + } + + if( reverse ) + row -= img->format->BytesPerPixel; + else + row += img->format->BytesPerPixel; + } + } + + // Huh? The image is completely transparent. + r = g = b = 0; +} + +/* Local helper for FixHiddenAlpha. Set the underlying RGB values of all pixels + * in img that are completely transparent. */ +static void SetAlphaRGB(const RageSurface *pImg, uint8_t r, uint8_t g, uint8_t b) +{ + // If it's a paletted surface, all we have to do is change the palette. + if( pImg->format->BitsPerPixel == 8 ) + { + for( int c = 0; c < pImg->format->palette->ncolors; ++c ) + { + if( pImg->format->palette->colors[c].a ) + continue; + pImg->format->palette->colors[c].r = r; + pImg->format->palette->colors[c].g = g; + pImg->format->palette->colors[c].b = b; + } + return; + } + + // If it's RGBA and there's no alpha channel, we have nothing to do. + if( pImg->format->BitsPerPixel > 8 && !pImg->format->Amask ) + return; + + uint32_t trans; + pImg->format->MapRGBA( r, g, b, 0, trans ); + for( int y = 0; y < pImg->h; ++y ) + { + uint8_t *row = pImg->pixels + pImg->pitch*y; + + for( int x = 0; x < pImg->w; ++x ) + { + uint32_t val = RageSurfaceUtils::decodepixel( row, pImg->format->BytesPerPixel ); + if( val != trans && !(val&pImg->format->Amask) ) + { + RageSurfaceUtils::encodepixel( row, pImg->format->BytesPerPixel, trans ); + } + + row += pImg->format->BytesPerPixel; + } + } +} + +/* When we scale up images (which we always do in high res), pixels + * that are completely transparent can be blended with opaque pixels, + * causing their RGB elements to show. This is visible in many textures + * as a pixel-wide border in the wrong color. This is tricky to fix. + * We need to set the RGB components of completely transparent pixels + * to a reasonable color. + * + * Most images have a single border color. For these, the transparent + * color is easy: search through the image top-bottom-left-right, + * find the first non-transparent pixel, and pull out its RGB. + * + * A few images don't. We can only make a guess here. After the above + * search, do the same in reverse (bottom-top-right-left). If the color + * we find is different, just set the border color to black. + */ +void RageSurfaceUtils::FixHiddenAlpha( RageSurface *pImg ) +{ + // If there are no alpha bits, there's nothing to fix. + if( pImg->format->BitsPerPixel != 8 && pImg->format->Amask == 0 ) + return; + + uint8_t r, g, b; + FindAlphaRGB( pImg, r, g, b, false ); + + uint8_t cr, cg, cb; // compare + FindAlphaRGB( pImg, cr, cg, cb, true ); + + if( cr != r || cg != g || cb != b ) + r = g = b = 0; + + SetAlphaRGB( pImg, r, g, b ); +} + +/* Scan the surface to see what level of alpha it uses. This can be used to + * find the best surface format for a texture; eg. a TRAIT_BOOL_TRANSPARENCY or + * TRAIT_NO_TRANSPARENCY surface can use RGB5A1 instead of RGBA4 for greater + * color resolution; a TRAIT_NO_TRANSPARENCY could also use R5G6B5. */ +int RageSurfaceUtils::FindSurfaceTraits( const RageSurface *img ) +{ + const int NEEDS_NO_ALPHA=0, NEEDS_BOOL_ALPHA=1, NEEDS_FULL_ALPHA=2; + int alpha_type = NEEDS_NO_ALPHA; + + uint32_t max_alpha; + if( img->format->BitsPerPixel == 8 ) + { + // Short circuit if we already know we have no transparency. + bool bHaveNonOpaque = false; + for( int c = 0; !bHaveNonOpaque && c < img->format->palette->ncolors; ++c ) + { + if( img->format->palette->colors[c].a != 0xFF ) + bHaveNonOpaque = true; + } + + if( !bHaveNonOpaque ) + return TRAIT_NO_TRANSPARENCY; + + max_alpha = 0xFF; + } + else + { + // Short circuit if we already know we have no transparency. + if( img->format->Amask == 0 ) + return TRAIT_NO_TRANSPARENCY; + + max_alpha = img->format->Amask; + } + + for(int y = 0; y < img->h; ++y) + { + uint8_t *row = (uint8_t *)img->pixels + img->pitch*y; + + for(int x = 0; x < img->w; ++x) + { + uint32_t val = decodepixel(row, img->format->BytesPerPixel); + + uint32_t alpha; + if( img->format->BitsPerPixel == 8 ) + alpha = img->format->palette->colors[val].a; + else + alpha = (val & img->format->Amask); + + if( alpha == 0 ) + alpha_type = max( alpha_type, NEEDS_BOOL_ALPHA ); + else if( alpha != max_alpha ) + alpha_type = max( alpha_type, NEEDS_FULL_ALPHA ); + + row += img->format->BytesPerPixel; + } + } + + int ret = 0; + switch( alpha_type ) + { + case NEEDS_NO_ALPHA: ret |= TRAIT_NO_TRANSPARENCY; break; + case NEEDS_BOOL_ALPHA: ret |= TRAIT_BOOL_TRANSPARENCY; break; + case NEEDS_FULL_ALPHA: break; + default: + FAIL_M(ssprintf("Invalid alpha type: %i", alpha_type)); + } + + return ret; +} + + +// Local helper for BlitTransform. +static inline void GetRawRGBAV_XY( const RageSurface *src, uint8_t *v, int x, int y ) +{ + const uint8_t *srcp = (const uint8_t *) src->pixels + (y * src->pitch); + const uint8_t *srcpx = srcp + (x * src->fmt.BytesPerPixel); + + RageSurfaceUtils::GetRawRGBAV( srcpx, src->fmt, v ); +} + +static inline float scale( float x, float l1, float h1, float l2, float h2 ) +{ + return ((x - l1) / (h1 - l1) * (h2 - l2) + l2); +} + +// Completely unoptimized. +void RageSurfaceUtils::BlitTransform( const RageSurface *src, RageSurface *dst, + const float fCoords[8] /* TL, BR, BL, TR */ ) +{ + ASSERT( src->format->BytesPerPixel == dst->format->BytesPerPixel ); + + const float Coords[8] = { + (fCoords[0] * (src->w)), (fCoords[1] * (src->h)), + (fCoords[2] * (src->w)), (fCoords[3] * (src->h)), + (fCoords[4] * (src->w)), (fCoords[5] * (src->h)), + (fCoords[6] * (src->w)), (fCoords[7] * (src->h)) + }; + + const int TL_X = 0, TL_Y = 1, BL_X = 2, BL_Y = 3, + BR_X = 4, BR_Y = 5, TR_X = 6, TR_Y = 7; + + for( int y = 0; y < dst->h; ++y ) + { + uint8_t *dstp = (uint8_t *) dst->pixels + (y * dst->pitch); /* line */ + uint8_t *dstpx = dstp; // pixel + + const float start_y = scale(float(y), 0, float(dst->h), Coords[TL_Y], Coords[BL_Y]); + const float end_y = scale(float(y), 0, float(dst->h), Coords[TR_Y], Coords[BR_Y]); + + const float start_x = scale(float(y), 0, float(dst->h), Coords[TL_X], Coords[BL_X]); + const float end_x = scale(float(y), 0, float(dst->h), Coords[TR_X], Coords[BR_X]); + + for( int x = 0; x < dst->w; ++x ) + { + const float src_xp = scale(float(x), 0, float(dst->w), start_x, end_x); + const float src_yp = scale(float(x), 0, float(dst->w), start_y, end_y); + + /* If the surface is two pixels wide, src_xp is 0..2. .5 indicates + * pixel[0]; 1 indicates 50% pixel[0], 50% pixel[1]; 1.5 indicates + * pixel[1]; 2 indicates 50% pixel[1], 50% pixel[2] (which is clamped + * to pixel[1]). */ + int src_x[2], src_y[2]; + src_x[0] = (int) truncf(src_xp - 0.5f); + src_x[1] = src_x[0] + 1; + + src_y[0] = (int) truncf(src_yp - 0.5f); + src_y[1] = src_y[0] + 1; + + // Emulate GL_REPEAT. + src_x[0] = clamp(src_x[0], 0, src->w); + src_x[1] = clamp(src_x[1], 0, src->w); + src_y[0] = clamp(src_y[0], 0, src->h); + src_y[1] = clamp(src_y[1], 0, src->h); + + // Decode our four pixels. + uint8_t v[4][4]; + GetRawRGBAV_XY(src, v[0], src_x[0], src_y[0]); + GetRawRGBAV_XY(src, v[1], src_x[0], src_y[1]); + GetRawRGBAV_XY(src, v[2], src_x[1], src_y[0]); + GetRawRGBAV_XY(src, v[3], src_x[1], src_y[1]); + + // Distance from the pixel chosen: + float weight_x = src_xp - (src_x[0] + 0.5f); + float weight_y = src_yp - (src_y[0] + 0.5f); + + // Filter: + uint8_t out[4] = { 0,0,0,0 }; + for(int i = 0; i < 4; ++i) + { + float sum = 0; + sum += v[0][i] * (1-weight_x) * (1-weight_y); + sum += v[1][i] * (1-weight_x) * (weight_y); + sum += v[2][i] * (weight_x) * (1-weight_y); + sum += v[3][i] * (weight_x) * (weight_y); + out[i] = (uint8_t) clamp( lrintf(sum), 0L, 255L ); + } + + // If the source has no alpha, set the destination to opaque. + if( src->format->Amask == 0 ) + out[3] = uint8_t( dst->format->Amask >> dst->format->Ashift ); + + SetRawRGBAV(dstpx, dst, out); + + dstpx += dst->format->BytesPerPixel; + } + } +} + + +/* Simplified: + * + * No source alpha. + * Palette -> palette blits assume the palette is identical (no mapping). + * No color key. + * No general blitting rects. */ + +static bool blit_same_type( const RageSurface *src_surf, const RageSurface *dst_surf, int width, int height ) +{ + if( src_surf->format->BytesPerPixel != dst_surf->format->BytesPerPixel || + src_surf->format->Rmask != dst_surf->format->Rmask || + src_surf->format->Gmask != dst_surf->format->Gmask || + src_surf->format->Bmask != dst_surf->format->Bmask || + src_surf->format->Amask != dst_surf->format->Amask ) + return false; + + const uint8_t *src = src_surf->pixels; + uint8_t *dst = dst_surf->pixels; + + // If possible, memcpy the whole thing. + if( src_surf->w == width && dst_surf->w == width && src_surf->pitch == dst_surf->pitch ) + { + memcpy( dst, src, height*src_surf->pitch ); + return true; + } + + // The rows don't line up, so memcpy row by row. + while( height-- ) + { + memcpy( dst, src, width*src_surf->format->BytesPerPixel ); + src += src_surf->pitch; + dst += dst_surf->pitch; + } + + return true; +} + +/* Rescaling blit with no ckey. This is used to update movies in + * D3D, so optimization is very important. */ +static bool blit_rgba_to_rgba( const RageSurface *src_surf, const RageSurface *dst_surf, int width, int height ) +{ + if( src_surf->format->BytesPerPixel == 1 || dst_surf->format->BytesPerPixel == 1 ) + return false; + + const uint8_t *src = src_surf->pixels; + uint8_t *dst = dst_surf->pixels; + + // Bytes to skip at the end of a line. + const int srcskip = src_surf->pitch - width*src_surf->format->BytesPerPixel; + const int dstskip = dst_surf->pitch - width*dst_surf->format->BytesPerPixel; + + const uint32_t *src_shifts = src_surf->format->Shift; + const uint32_t *dst_shifts = dst_surf->format->Shift; + const uint32_t *src_masks = src_surf->format->Mask; + const uint32_t *dst_masks = dst_surf->format->Mask; + + uint8_t lookup[4][256]; + for( int c = 0; c < 4; ++c ) + { + const uint32_t max_src_val = src_masks[c] >> src_shifts[c]; + const uint32_t max_dst_val = dst_masks[c] >> dst_shifts[c]; + ASSERT( max_src_val <= 0xFF ); + ASSERT( max_dst_val <= 0xFF ); + + if( src_masks[c] == 0 ) + { + /* The source is missing a channel. Alpha defaults to opaque, other + * channels default to 0. */ + if( c == 3 ) + lookup[c][0] = (uint8_t) max_dst_val; + else + lookup[c][0] = 0; + } else { + /* Calculate a color conversion table. There are a few ways we can do + * this (each list is the resulting table for 4->2 bit): + * + * SCALE( i, 0, max_src_val+1, 0, max_dst_val+1 ); + * { 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3 } + * SCALE( i, 0, max_src_val, 0, max_dst_val ); + * { 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 3 } + * lrintf( ((float) i / max_src_val) * max_dst_val ) + * { 0, 0, 0, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3 } + * + * We use the first for increasing resolution, since it gives the most even + * distribution. + * + * 2->4 bit: + * SCALE( i, 0, max_src_val+1, 0, max_dst_val+1 ); + * { 0, 4, 8, 12 } + * SCALE( i, 0, max_src_val, 0, max_dst_val ); + * { 0, 5, 10, 15 } + * lrintf( ((float) i / max_src_val) * max_dst_val ) + * { 0, 5, 10, 15 } + * + * The latter two are equivalent and give an even distribution; we use the + * second, since the first doesn't scale max_src_val to max_dst_val. + * + * Having separate formulas for increasing and decreasing resolution seems + * strange; what's wrong here? */ + if( max_src_val > max_dst_val ) + for( uint32_t i = 0; i <= max_src_val; ++i ) + lookup[c][i] = (uint8_t) SCALE( i, 0, max_src_val+1, 0, max_dst_val+1 ); + else + for( uint32_t i = 0; i <= max_src_val; ++i ) + lookup[c][i] = (uint8_t) SCALE( i, 0, max_src_val, 0, max_dst_val ); + } + } + + while( height-- ) + { + int x = 0; + while( x++ < width ) + { + unsigned int pixel = RageSurfaceUtils::decodepixel( src, src_surf->format->BytesPerPixel ); + + // Convert pixel to the destination format. + unsigned int opixel = 0; + for( int c = 0; c < 4; ++c ) + { + int lSrc = (pixel & src_masks[c]) >> src_shifts[c]; + opixel |= lookup[c][lSrc] << dst_shifts[c]; + } + + // Store it. + RageSurfaceUtils::encodepixel( dst, dst_surf->format->BytesPerPixel, opixel ); + + src += src_surf->format->BytesPerPixel; + dst += dst_surf->format->BytesPerPixel; + } + + src += srcskip; + dst += dstskip; + } + + return true; +} + +static bool blit_generic( const RageSurface *src_surf, const RageSurface *dst_surf, int width, int height ) +{ + if( src_surf->format->BytesPerPixel != 1 || dst_surf->format->BytesPerPixel == 1 ) + return false; + + const uint8_t *src = src_surf->pixels; + uint8_t *dst = dst_surf->pixels; + + // Bytes to skip at the end of a line. + const int srcskip = src_surf->pitch - width*src_surf->format->BytesPerPixel; + const int dstskip = dst_surf->pitch - width*dst_surf->format->BytesPerPixel; + + while( height-- ) + { + int x = 0; + while( x++ < width ) + { + unsigned int pixel = RageSurfaceUtils::decodepixel( src, src_surf->format->BytesPerPixel ); + + uint8_t colors[4]; + // Convert pixel to the destination RGBA. + colors[0] = src_surf->format->palette->colors[pixel].r; + colors[1] = src_surf->format->palette->colors[pixel].g; + colors[2] = src_surf->format->palette->colors[pixel].b; + colors[3] = src_surf->format->palette->colors[pixel].a; + pixel = RageSurfaceUtils::SetRGBAV(dst_surf->format, colors); + + // Store it. + RageSurfaceUtils::encodepixel( dst, dst_surf->format->BytesPerPixel, pixel ); + + src += src_surf->format->BytesPerPixel; + dst += dst_surf->format->BytesPerPixel; + } + + src += srcskip; + dst += dstskip; + } + + return true; +} + +// Blit src onto dst. +void RageSurfaceUtils::Blit( const RageSurface *src, RageSurface *dst, int width, int height ) +{ + if( width == -1 ) + width = src->w; + if( height == -1 ) + height = src->h; + width = min( src->w, dst->w ); + height = min( src->h, dst->h ); + + /* Try each blit until we find one that works; run them in order of efficiency, + * so we use the fastest blit possible. */ + do + { + // RGBA->RGBA with the same format, or PAL->PAL. Simple copy. + if( blit_same_type(src, dst, width, height) ) + break; + + // RGBA->RGBA with different formats. + if( blit_rgba_to_rgba(src, dst, width, height) ) + break; + + // PAL->RGBA. + if( blit_generic(src, dst, width, height) ) + break; + + FAIL_M("We don't do RGBA->PAL"); + } while(0); + + /* The destination surface may be larger than the source. For example, we may be + * blitting a 200x200 image onto a 256x256 surface for OpenGL. Normally, that extra + * space isn't actually used; we'll only render the image space. However, bilinear + * filtering will cause the lines of pixels at 201x... and ...x201 to be visible. We + * need to make sure those pixels make sense. + * + * Previously, we just cleared the image to transparent or the color key. This + * has two problems. First, we may not have space for a color key (an image with + * 256 non-transparent palette colors). Second, that's not completely correct; + * it'll force the outside border of the image to filter to transparent. If the image + * is being tiled with another image, that may leave seams. + * + * (In some cases, filtering to transparent is preferable, particularly when displaying + * a sprite in perspective. If you want that, add blank space to the image explicitly.) + * + * Copy the last column (200x... -> 201x...), then the last row (...x200 -> ...x201). */ + + CorrectBorderPixels( dst, width, height ); +} + +/* If only width x height of img is actually going to be used, and there's extra + * space on the surface, duplicate the last row and column to ensure that we don't + * pull in unexpected data when rendering with bilinear filtering. + * + * We do this if there's memory available, even if that space extends outside + * of the image (in the per-line padding or after the end). This way, surfaces + * can be padded to power-of-two dimensions by the image loaders, and if no other + * adjustments are needed, they can be passed directly to the renderer without + * doing any extra copies. */ +void RageSurfaceUtils::CorrectBorderPixels( RageSurface *img, int width, int height ) +{ + if( width*img->fmt.BytesPerPixel < img->pitch ) + { + // Duplicate the last column. + int offset = img->format->BytesPerPixel * (width-1); + uint8_t *p = (uint8_t *) img->pixels + offset; + + for( int y = 0; y < height; ++y ) + { + uint32_t pixel = decodepixel( p, img->format->BytesPerPixel ); + encodepixel( p+img->format->BytesPerPixel, img->format->BytesPerPixel, pixel ); + + p += img->pitch; + } + } + + if( height < img->h ) + { + // Duplicate the last row. + uint8_t *srcp = img->pixels; + srcp += img->pitch * (height-1); + memcpy( srcp + img->pitch, srcp, img->pitch ); + } +} + +struct SurfaceHeader +{ + int width, height, pitch; + int Rmask, Gmask, Bmask, Amask; + int bpp; +}; + +// Save and load RageSurfaces to disk, in a very fast, nonportable way. +bool RageSurfaceUtils::SaveSurface( const RageSurface *img, RString file ) +{ + RageFile f; + if( !f.Open( file, RageFile::WRITE ) ) + return false; + + SurfaceHeader h; + memset( &h, 0, sizeof(h) ); + + h.height = img->h; + h.width = img->w; + h.pitch = img->pitch; + h.Rmask = img->format->Rmask; + h.Gmask = img->format->Gmask; + h.Bmask = img->format->Bmask; + h.Amask = img->format->Amask; + h.bpp = img->format->BitsPerPixel; + + f.Write( &h, sizeof(h) ); + + if( h.bpp == 8 ) + { + f.Write( &img->format->palette->ncolors, sizeof(img->format->palette->ncolors) ); + f.Write( img->format->palette->colors, img->format->palette->ncolors * sizeof(RageSurfaceColor) ); + } + + f.Write( img->pixels, img->h * img->pitch ); + + return true; +} + +RageSurface *RageSurfaceUtils::LoadSurface( RString file ) +{ + RageFile f; + if( !f.Open( file ) ) + return NULL; + + SurfaceHeader h; + if( f.Read( &h, sizeof(h) ) != sizeof(h) ) + return NULL; + + RageSurfacePalette palette; + if( h.bpp == 8 ) + { + if( f.Read( &palette.ncolors, sizeof(palette.ncolors) ) != sizeof(palette.ncolors) ) + return NULL; + ASSERT_M( palette.ncolors <= 256, ssprintf("%i", palette.ncolors) ); + if( f.Read( palette.colors, palette.ncolors * sizeof(RageSurfaceColor) ) != int(palette.ncolors * sizeof(RageSurfaceColor)) ) + return NULL; + } + + // Create the surface. + RageSurface *img = CreateSurface( h.width, h.height, h.bpp, + h.Rmask, h.Gmask, h.Bmask, h.Amask ); + ASSERT( img != nullptr ); + + /* If the pitch has changed, this surface is either corrupt, or was + * created with a different version whose CreateSurface() behavior + * was different. */ + if( h.pitch != img->pitch ) + { + LOG->Trace( "Error loading \"%s\": expected pitch %i, got %i (%ibpp, %i width)", + file.c_str(), h.pitch, img->pitch, h.bpp, h.width ); + delete img; + return NULL; + } + + if( f.Read( img->pixels, h.height * h.pitch ) != h.height * h.pitch ) + { + delete img; + return NULL; + } + + // Set the palette. + if( h.bpp == 8 ) + *img->fmt.palette = palette; + + return img; +} + +/* This converts an image to a special 8-bit paletted format. The palette is set up + * so that palette indexes look like regular, packed components. + * + * For example, an image with 8 bits of grayscale and 0 bits of alpha has a palette + * that looks like { 0,0,0,255 }, { 1,1,1,255 }, { 2,2,2,255 }, ... { 255,255,255,255 }. + * This results in index components that can be treated as grayscale values. + * + * An image with 2 bits of grayscale and 2 bits of alpha look like + * { 0,0,0,0 }, { 85,85,85,0 }, { 170,170,170,0 }, { 255,255,255,0 }, + * { 0,0,0,85 }, { 85,85,85,85 }, { 170,170,170,85 }, { 255,255,255,85 }, ... + * + * This results in index components that can be pulled apart like regular packed + * values: the first two bits of the index are the grayscale component, and the next + * two bits are the alpha component. + * + * This gives us a generic way to handle arbitrary 8-bit texture formats. */ +RageSurface *RageSurfaceUtils::PalettizeToGrayscale( const RageSurface *src_surf, int GrayBits, int AlphaBits ) +{ + AlphaBits = min( AlphaBits, 8-src_surf->format->Loss[3] ); + + const int TotalBits = GrayBits + AlphaBits; + ASSERT( TotalBits <= 8 ); + + RageSurface *dst_surf = CreateSurface(src_surf->w, src_surf->h, + 8, 0,0,0,0 ); + + // Set up the palette. + const int TotalColors = 1 << TotalBits; + const int Ivalues = 1 << GrayBits; // number of intensity values + const int Ishift = 0; // intensity shift + const int Imask = ((1 << GrayBits) - 1) << Ishift; // intensity mask + const int Iloss = 8-GrayBits; + + const int Avalues = 1 << AlphaBits; // number of alpha values + const int Ashift = GrayBits; // alpha shift + const int Amask = ((1 << AlphaBits) - 1) << Ashift; // alpha mask + const int Aloss = 8-AlphaBits; + + for( int index = 0; index < TotalColors; ++index ) + { + const int I = (index & Imask) >> Ishift; + const int A = (index & Amask) >> Ashift; + + int ScaledI; + if( Ivalues == 1 ) + ScaledI = 255; // if only one intensity value, always fullbright + else + ScaledI = clamp( lrintf(I * (255.0f / (Ivalues-1))), 0L, 255L ); + + int ScaledA; + if( Avalues == 1 ) + ScaledA = 255; // if only one alpha value, always opaque + else + ScaledA = clamp( lrintf(A * (255.0f / (Avalues-1))), 0L, 255L ); + + RageSurfaceColor c; + c.r = uint8_t(ScaledI); + c.g = uint8_t(ScaledI); + c.b = uint8_t(ScaledI); + c.a = uint8_t(ScaledA); + + dst_surf->fmt.palette->colors[index] = c; + } + + const uint8_t *src = src_surf->pixels; + uint8_t *dst = dst_surf->pixels; + + int height = src_surf->h; + int width = src_surf->w; + + // Bytes to skip at the end of a line. + const int srcskip = src_surf->pitch - width*src_surf->format->BytesPerPixel; + const int dstskip = dst_surf->pitch - width*dst_surf->format->BytesPerPixel; + + while( height-- ) + { + int x = 0; + while( x++ < width ) + { + unsigned int pixel = decodepixel( src, src_surf->format->BytesPerPixel ); + + uint8_t colors[4]; + GetRGBAV(pixel, src_surf, colors); + + int Ival = 0; + Ival += colors[0]; + Ival += colors[1]; + Ival += colors[2]; + Ival /= 3; + + pixel = (Ival >> Iloss) << Ishift | + (colors[3] >> Aloss) << Ashift; + + // Store it. + *dst = uint8_t(pixel); + + src += src_surf->format->BytesPerPixel; + dst += dst_surf->format->BytesPerPixel; + } + + src += srcskip; + dst += dstskip; + } + + return dst_surf; +} + + +RageSurface *RageSurfaceUtils::MakeDummySurface( int height, int width ) +{ + RageSurface *ret_image = CreateSurface( width, height, 8, 0,0,0,0 ); + + RageSurfaceColor pink( 0xFF, 0x10, 0xFF, 0xFF ); + ret_image->fmt.palette->colors[0] = pink; + + memset( ret_image->pixels, 0, ret_image->h*ret_image->pitch ); + + return ret_image; +} + +/* HACK: Some banners and textures have #F800F8 as the color key. + * Search the edge for it; if we find it, use that as the color key. */ +static bool ImageUsesOffHotPink( const RageSurface *img ) +{ + uint32_t OffHotPink; + if( !img->format->MapRGBA( 0xF8, 0, 0xF8, 0xFF, OffHotPink ) ) + return false; + + const uint8_t *p = img->pixels; + for( int x = 0; x < img->w; ++x ) + { + uint32_t val = RageSurfaceUtils::decodepixel( p, img->format->BytesPerPixel ); + if( val == OffHotPink ) + return true; + p += img->format->BytesPerPixel; + } + + p = img->pixels; + p += img->pitch * (img->h-1); + for( int i=0; i < img->w; i++ ) + { + uint32_t val = RageSurfaceUtils::decodepixel( p, img->format->BytesPerPixel ); + if( val == OffHotPink ) + return true; + p += img->format->BytesPerPixel; + } + return false; +} + +/* Set #FF00FF and #F800F8 to transparent. img may be reallocated if it has no + * alpha bits. */ +void RageSurfaceUtils::ApplyHotPinkColorKey( RageSurface *&img ) +{ + if( img->format->BitsPerPixel == 8 ) + { + uint32_t color; + if( img->format->MapRGBA( 0xF8, 0, 0xF8, 0xFF, color ) ) + img->format->palette->colors[ color ].a = 0; + if( img->format->MapRGBA( 0xFF, 0, 0xFF, 0xFF, color ) ) + img->format->palette->colors[ color ].a = 0; + return; + } + + // RGBA. Make sure we have alpha. + if( img->format->Amask == 0 ) + { + // We don't have any alpha. Try to enable it without copying. + /* XXX: need to scan the surface and make sure the new alpha bit is always 1 */ + /* + const int used_bits = img->format->Rmask | img->format->Gmask | img->format->Bmask; + + for( int i = 0; img->format->Amask == 0 && i < img->format->BitsPerPixel; ++i ) + { + if( (used_bits & (1<format->Amask = 1<format->Aloss = 7; + img->format->Ashift = (uint8_t) i; + } + } + */ + // If we didn't have any free bits, convert to make room. + if( img->format->Amask == 0 ) + ConvertSurface( img, img->w, img->h, + 32, 0xFF000000, 0x00FF0000, 0x0000FF00, 0x000000FF ); + } + + uint32_t HotPink; + + bool bHaveColorKey; + if( ImageUsesOffHotPink(img) ) + bHaveColorKey = img->format->MapRGBA( 0xF8, 0, 0xF8, 0xFF, HotPink ); + else + bHaveColorKey = img->format->MapRGBA( 0xFF, 0, 0xFF, 0xFF, HotPink ); + if( !bHaveColorKey ) + return; + + for( int y = 0; y < img->h; ++y ) + { + uint8_t *row = img->pixels + img->pitch*y; + + for( int x = 0; x < img->w; ++x ) + { + uint32_t val = decodepixel( row, img->format->BytesPerPixel ); + if( val == HotPink ) + encodepixel( row, img->format->BytesPerPixel, 0 ); + + row += img->format->BytesPerPixel; + } + } +} + +void RageSurfaceUtils::FlipVertically( RageSurface *img ) +{ + const int pitch = img->pitch; + const int bytes_per_row = img->format->BytesPerPixel * img->w; + char *row = new char[bytes_per_row]; + + for( int y=0; y < img->h/2; y++ ) + { + int y2 = img->h-1-y; + memcpy( row, img->pixels + pitch * y, bytes_per_row ); + memcpy( img->pixels + pitch * y, img->pixels + pitch * y2, bytes_per_row ); + memcpy( img->pixels + pitch * y2, row, bytes_per_row ); + } + + delete [] row; +} + +/* + * (c) 2001-2004 Glenn Maynard, 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. + */ diff --git a/src/RageSurfaceUtils_Palettize.cpp b/src/RageSurfaceUtils_Palettize.cpp index 7957c0db43..3b1f1cf645 100644 --- a/src/RageSurfaceUtils_Palettize.cpp +++ b/src/RageSurfaceUtils_Palettize.cpp @@ -1,614 +1,614 @@ -/* from http://www.libpng.org/pub/png/apps/pngquant.html */ - -#include "global.h" -#include "RageSurfaceUtils_Palettize.h" -#include "RageSurface.h" -#include "RageSurfaceUtils.h" -#include "RageUtil.h" - -typedef uint8_t pixval; -typedef uint8_t apixel[4]; - -#define PAM_GETR(p) ((p)[0]) -#define PAM_GETG(p) ((p)[1]) -#define PAM_GETB(p) ((p)[2]) -#define PAM_GETA(p) ((p)[3]) -#define PAM_ASSIGN(p,red,grn,blu,alf) \ - do { (p)[0] = (red); (p)[1] = (grn); (p)[2] = (blu); (p)[3] = (alf); } while (0) -#define PAM_EQUAL(p,q) \ - ((p)[0] == (q)[0] && (p)[1] == (q)[1] && (p)[2] == (q)[2] && (p)[3] == (q)[3]) -#define PAM_DEPTH(p) \ - PAM_ASSIGN( (p), (uint8_t) table[PAM_GETR(p)], (uint8_t) table[PAM_GETG(p)], (uint8_t) table[PAM_GETB(p)], (uint8_t) table[PAM_GETA(p)] ) - -struct acolorhist_item -{ - apixel acolor; - int value; -}; - -typedef struct acolorhist_list_item *acolorhist_list; -struct acolorhist_list_item -{ - struct acolorhist_item ch; - acolorhist_list next; -}; - -static const unsigned int HASH_SIZE = 20023u; - -struct acolorhash_hash -{ - acolorhist_list hash[HASH_SIZE]; - acolorhash_hash() - { - ZERO( hash ); - } - - ~acolorhash_hash() - { - for( unsigned i = 0; i < HASH_SIZE; ++i ) - { - acolorhist_list achl, achlnext; - for ( achl = hash[i]; achl != NULL; achl = achlnext ) - { - achlnext = achl->next; - free( achl ); - } - } - } -}; - - - -#define MAXCOLORS 32767 -#define FS_SCALE 1024 /* Floyd-Steinberg scaling factor */ - -/* #define REP_AVERAGE_COLORS */ -#define REP_AVERAGE_PIXELS - - - -static acolorhist_item *mediancut( acolorhist_item *achv, int colors, int sum, int maxval, int newcolors ); - -static bool compare_index_0( const acolorhist_item &ch1, const acolorhist_item &ch2 ) -{ - return ch1.acolor[0] < ch2.acolor[0]; -} - -static bool compare_index_1( const acolorhist_item &ch1, const acolorhist_item &ch2 ) -{ - return ch1.acolor[1] < ch2.acolor[1]; -} - -static bool compare_index_2( const acolorhist_item &ch1, const acolorhist_item &ch2 ) -{ - return ch1.acolor[2] < ch2.acolor[2]; -} - -static bool compare_index_3( const acolorhist_item &ch1, const acolorhist_item &ch2 ) -{ - return ch1.acolor[3] < ch2.acolor[3]; -} - -static acolorhist_item *pam_computeacolorhist( const RageSurface *src, int maxacolors, int* acolorsP ); -static void pam_addtoacolorhash( acolorhash_hash &acht, const uint8_t acolorP[4], int value ); -static int pam_lookupacolor( const acolorhash_hash &acht, const uint8_t acolorP[4] ); -static void pam_freeacolorhist( acolorhist_item *achv ); - -struct pixerror_t -{ - int c[4]; -}; - -void RageSurfaceUtils::Palettize( RageSurface *&pImg, int iColors, bool bDither ) -{ - ASSERT( iColors != 0 ); - - acolorhist_item *acolormap=NULL; - int newcolors = 0; - - // "apixel", etc. make assumptions about byte order. - RageSurfaceUtils::ConvertSurface( pImg, pImg->w, pImg->h, 32, - Swap32BE(0xFF000000), Swap32BE(0x00FF0000), Swap32BE(0x0000FF00), Swap32BE(0x000000FF)); - - pixval maxval = 255; - - { - /* Attempt to make a histogram of the colors, unclustered. - * If at first we don't succeed, lower maxval to increase color - * coherence and try again. This will eventually terminate, with - * maxval at worst 15, since 32^3 is approximately MAXCOLORS. - * [GRR POSSIBLE BUG: what about 32^4 ?] */ - acolorhist_item *achv; - int colors; - while(1) - { - achv = pam_computeacolorhist( pImg, MAXCOLORS, &colors ); - if( achv != NULL ) - break; - pixval newmaxval = maxval / 2; - - int table[256]; - for( int c = 0; c <= maxval; ++c ) - table[c] = ( (uint8_t) c * newmaxval + maxval/2 ) / maxval; - - for( int row = 0; row < pImg->h; ++row ) - { - apixel *pP = (apixel *) (pImg->pixels+row*pImg->pitch); - for( int col = 0; col < pImg->w; ++col, ++pP ) - PAM_DEPTH( *pP ); - } - maxval = newmaxval; - } - newcolors = min( colors, iColors ); - - // Apply median-cut to histogram, making the new acolormap. - acolormap = mediancut( achv, colors, pImg->h * pImg->w, maxval, newcolors ); - pam_freeacolorhist( achv ); - } - - RageSurface *pRet = CreateSurface( pImg->w, pImg->h, 8, 0, 0, 0, 0 ); - pRet->format->palette->ncolors = newcolors; - - // Rescale the palette colors to a maxval of 255. - { - RageSurfacePalette *pal = pRet->format->palette; - for( int x = 0; x < pal->ncolors; ++x ) - { - // This is really just PAM_DEPTH() broken out for the palette. - pal->colors[x].r - = (PAM_GETR(acolormap[x].acolor)*255 + (maxval >> 1)) / maxval; - pal->colors[x].g - = (PAM_GETG(acolormap[x].acolor)*255 + (maxval >> 1)) / maxval; - pal->colors[x].b - = (PAM_GETB(acolormap[x].acolor)*255 + (maxval >> 1)) / maxval; - pal->colors[x].a - = (PAM_GETA(acolormap[x].acolor)*255 + (maxval >> 1)) / maxval; - } - } - - // Map the colors in the image to their closest match in the new colormap. - acolorhash_hash acht; - - bool fs_direction = 0; - pixerror_t *thiserr = NULL, *nexterr = NULL; - - if( bDither ) - { - // Initialize Floyd-Steinberg error vectors. - thiserr = new pixerror_t[pImg->w + 2]; - nexterr = new pixerror_t[pImg->w + 2]; - - memset( thiserr, 0, sizeof(pixerror_t) * (pImg->w + 2) ); - } - - for( int row = 0; row < pImg->h; ++row ) - { - if( bDither ) - memset( nexterr, 0, sizeof(pixerror_t) * (pImg->w + 2) ); - - int col, limitcol; - if( !fs_direction ) - { - col = 0; - limitcol = pImg->w; - } else { - col = pImg->w - 1; - limitcol = -1; - } - - const uint8_t *pIn = pImg->pixels + row*pImg->pitch; - uint8_t *pOut = pRet->pixels + row*pRet->pitch; - pIn += col * 4; - pOut += col; - - do - { - int32_t sc[4]; - uint8_t pixel[4] = { pIn[0], pIn[1], pIn[2], pIn[3] }; - if( bDither ) - { - // Use Floyd-Steinberg errors to adjust actual color. - for( int c = 0; c < 4; ++c ) - { - sc[c] = pixel[c] + thiserr[col + 1].c[c] / FS_SCALE; - sc[c] = clamp( sc[c], 0, (int32_t) maxval ); - } - - PAM_ASSIGN( pixel, (uint8_t)sc[0], (uint8_t)sc[1], (uint8_t)sc[2], (uint8_t)sc[3] ); - } - - // Check hash table to see if we have already matched this color. - int ind = pam_lookupacolor( acht, pixel ); - if( ind == -1 ) - { - // No; search acolormap for closest match. - static int square_table[512], *pSquareTable = NULL; - if( pSquareTable == NULL ) - { - pSquareTable = square_table+256; - for( int c = -256; c < 256; ++c ) - pSquareTable[c] = c*c; - } - - long dist = 2000000000; - for( int i = 0; i < newcolors; ++i ) - { - const uint8_t *colors2 = acolormap[i].acolor; - - int newdist = 0; - newdist += pSquareTable[ int(pixel[0]) - colors2[0] ]; - newdist += pSquareTable[ int(pixel[1]) - colors2[1] ]; - newdist += pSquareTable[ int(pixel[2]) - colors2[2] ]; - newdist += pSquareTable[ int(pixel[3]) - colors2[3] ]; - - if( newdist < dist ) - { - ind = i; - dist = newdist; - } - } - - pam_addtoacolorhash( acht, pixel, ind ); - } - - if( bDither ) - { - // Propagate Floyd-Steinberg error terms. - if( !fs_direction ) - { - for( int c = 0; c < 4; ++c ) - { - long err = (sc[c] - (long)acolormap[ind].acolor[c])*FS_SCALE; - thiserr[col + 2].c[c] += ( err * 7 ) / 16; - nexterr[col ].c[c] += ( err * 3 ) / 16; - nexterr[col + 1].c[c] += ( err * 5 ) / 16; - nexterr[col + 2].c[c] += ( err * 1 ) / 16; - } - } else { - for( int c = 0; c < 4; ++c ) - { - long err = (sc[c] - (long)acolormap[ind].acolor[c])*FS_SCALE; - thiserr[col ].c[c] += ( err * 7 ) / 16; - nexterr[col + 2].c[c] += ( err * 3 ) / 16; - nexterr[col + 1].c[c] += ( err * 5 ) / 16; - nexterr[col ].c[c] += ( err * 1 ) / 16; - } - } - } - - *pOut = (uint8_t) ind; - - if( !fs_direction ) - { - ++col; - pIn += 4; - ++pOut; - } else { - --col; - pIn -= 4; - --pOut; - } - } - while( col != limitcol ); - - if( bDither ) - { - swap( thiserr, nexterr ); - fs_direction = !fs_direction; - } - } - - delete [] thiserr; - delete [] nexterr; - - delete pImg; - pImg = pRet; -} - -/* Here is the fun part, the median-cut colormap generator. This is based - * on Paul Heckbert's paper, "Color Image Quantization for Frame Buffer - * Display," SIGGRAPH 1982 Proceedings, page 297. */ - -typedef struct box *box_vector; -struct box -{ - int ind; - int colors; - int sum; -}; - -static bool CompareBySumDescending( const box &b1, const box &b2 ) -{ - return b2.sum < b1.sum; -} - - -static acolorhist_item *mediancut( acolorhist_item *achv, int colors, int sum, int maxval, int newcolors ) -{ - acolorhist_item *acolormap; - box_vector bv; - int boxes; - - bv = (box_vector) malloc( sizeof(struct box) * newcolors ); - ASSERT( bv != NULL ); - acolormap = (acolorhist_item*) malloc( sizeof(struct acolorhist_item) * newcolors); - ASSERT( acolormap != NULL ); - - for ( int i = 0; i < newcolors; ++i ) - PAM_ASSIGN( acolormap[i].acolor, 0, 0, 0, 0 ); - - // Set up the initial box. - bv[0].ind = 0; - bv[0].colors = colors; - bv[0].sum = sum; - boxes = 1; - - // Main loop: split boxes until we have enough. - while( boxes < newcolors ) - { - int indx, clrs; - int sm; - int halfsum, lowersum; - - // Find the first splittable box. - int bi; - for( bi = 0; bi < boxes; ++bi ) - if ( bv[bi].colors >= 2 ) - break; - if( bi == boxes ) - break; // ran out of colors! - indx = bv[bi].ind; - clrs = bv[bi].colors; - sm = bv[bi].sum; - - /* Go through the box finding the minimum and maximum of each - * component - the boundaries of the box. */ - int mins[4], maxs[4]; - mins[0] = maxs[0] = achv[indx].acolor[0]; - mins[1] = maxs[1] = achv[indx].acolor[1]; - mins[2] = maxs[2] = achv[indx].acolor[2]; - mins[3] = maxs[3] = achv[indx].acolor[3]; - - for ( int i = 1; i < clrs; ++i ) - { - int v; - v = achv[indx + i].acolor[0]; - mins[0] = min( mins[0], v ); - maxs[0] = max( maxs[0], v ); - v = achv[indx + i].acolor[1]; - mins[1] = min( mins[1], v ); - maxs[1] = max( maxs[1], v ); - v = achv[indx + i].acolor[2]; - mins[2] = min( mins[2], v ); - maxs[2] = max( maxs[2], v ); - v = achv[indx + i].acolor[3]; - mins[3] = min( mins[3], v ); - maxs[3] = max( maxs[3], v ); - } - - // Find the largest dimension, and sort by that component. - { - int iMax = 0; - for( int i = 1; i < 3; ++i ) - if( maxs[i] - mins[i] > maxs[iMax] - mins[iMax] ) - iMax = i; - - switch( iMax ) - { - case 0: sort( &achv[indx], &achv[indx+clrs], compare_index_0 ); break; - case 1: sort( &achv[indx], &achv[indx+clrs], compare_index_1 ); break; - case 2: sort( &achv[indx], &achv[indx+clrs], compare_index_2 ); break; - case 3: sort( &achv[indx], &achv[indx+clrs], compare_index_3 ); break; - } - } - /* Now find the median based on the counts, so that about half the - * pixels (not colors, pixels) are in each subdivision. */ - lowersum = achv[indx].value; - halfsum = sm / 2; - int j; - for ( j = 1; j < clrs - 1; ++j ) - { - if ( lowersum >= halfsum ) - break; - lowersum += achv[indx + j].value; - } - - // Split the box, and sort to bring the biggest boxes to the top. - bv[bi].colors = j; - bv[bi].sum = lowersum; - bv[boxes].ind = indx + j; - bv[boxes].colors = clrs - j; - bv[boxes].sum = sm - lowersum; - ++boxes; - sort( &bv[0], &bv[boxes], CompareBySumDescending ); - } - - /* Ok, we've got enough boxes. Now choose a representative color for - * each box. There are a number of possible ways to make this choice. - * One would be to choose the center of the box; this ignores any structure - * within the boxes. Another method would be to average all the colors in - * the box - this is the method specified in Heckbert's paper. A third - * method is to average all the pixels in the box. You can switch which - * method is used by switching the commenting on the REP_ defines at - * the beginning of this source file. */ - for( int bi = 0; bi < boxes; ++bi ) - { -#ifdef REP_AVERAGE_COLORS - int indx = bv[bi].ind; - int clrs = bv[bi].colors; - long r = 0, g = 0, b = 0, a = 0; - - for ( i = 0; i < clrs; ++i ) - { - r += PAM_GETR( achv[indx + i].acolor ); - g += PAM_GETG( achv[indx + i].acolor ); - b += PAM_GETB( achv[indx + i].acolor ); - a += PAM_GETA( achv[indx + i].acolor ); - } - r = r / clrs; - g = g / clrs; - b = b / clrs; - a = a / clrs; - PAM_ASSIGN( acolormap[bi].acolor, r, g, b, a ); -#endif // REP_AVERAGE_COLORS -#ifdef REP_AVERAGE_PIXELS - int indx = bv[bi].ind; - int clrs = bv[bi].colors; - long r = 0, g = 0, b = 0, a = 0, lSum = 0; - - for ( int i = 0; i < clrs; ++i ) - { - r += PAM_GETR( achv[indx + i].acolor ) * achv[indx + i].value; - g += PAM_GETG( achv[indx + i].acolor ) * achv[indx + i].value; - b += PAM_GETB( achv[indx + i].acolor ) * achv[indx + i].value; - a += PAM_GETA( achv[indx + i].acolor ) * achv[indx + i].value; - lSum += achv[indx + i].value; - } - r = r / lSum; - r = min( r, (long) maxval ); - g = g / lSum; - g = min( g, (long) maxval ); - b = b / lSum; - b = min( b, (long) maxval ); - a = a / lSum; - a = min( a, (long) maxval ); - PAM_ASSIGN( acolormap[bi].acolor, (uint8_t)r, (uint8_t)g, (uint8_t)b, (uint8_t)a ); -#endif // REP_AVERAGE_PIXELS - } - - // All done. - return acolormap; -} - -/* - * libpam3.c - pam (portable alpha map) utility library part 3 - * - * Colormap routines. - * - * Copyright (C) 1989, 1991 by Jef Poskanzer. - * Copyright (C) 1997 by Greg Roelofs. - * - * Permission to use, copy, modify, and distribute this software and its - * documentation for any purpose and without fee is hereby granted, provided - * that the above copyright notice appear in all copies and that both that - * copyright notice and this permission notice appear in supporting - * documentation. This software is provided "as is" without express or - * implied warranty. - */ - -#define pam_hashapixel(p) ( ( (unsigned) PAM_GETR(p) * 33023 + \ - (unsigned) PAM_GETG(p) * 30013 + \ - (unsigned) PAM_GETB(p) * 27011 + \ - (unsigned) PAM_GETA(p) * 24007 ) \ - % (unsigned) HASH_SIZE ) - -static bool pam_computeacolorhash( const RageSurface *src, int maxacolors, int* acolorsP, acolorhash_hash &hash ) -{ - ASSERT( src->format->BytesPerPixel == 4 ); - - *acolorsP = 0; - - // Go through the entire image, building a hash table of colors. - for( int row = 0; row < src->h; ++row ) - { - const apixel *pP = (const apixel *) (src->pixels + row*src->pitch); - for( int col = 0; col < src->w; ++col, pP++ ) - { - int hashval = pam_hashapixel( *pP ); - acolorhist_list achl; - for ( achl = hash.hash[hashval]; achl != NULL; achl = achl->next ) - if ( PAM_EQUAL( achl->ch.acolor, *pP ) ) - break; - if ( achl != NULL ) - ++achl->ch.value; - else - { - if ( ++(*acolorsP) > maxacolors ) - return false; - achl = (acolorhist_list) malloc( sizeof(struct acolorhist_list_item) ); - ASSERT( achl != NULL ); - - memcpy( achl->ch.acolor, *pP, sizeof(apixel) ); - achl->ch.value = 1; - achl->next = hash.hash[hashval]; - hash.hash[hashval] = achl; - } - } - } - - return true; -} - -static acolorhist_item *pam_acolorhashtoacolorhist( const acolorhash_hash &acht, int maxacolors ) -{ - // Collate the hash table into a simple acolorhist array. - acolorhist_item *achv = (acolorhist_item*) malloc( maxacolors * sizeof(struct acolorhist_item) ); - ASSERT( achv != NULL ); - - // Loop through the hash table. - int j = 0; - for( unsigned i = 0; i < HASH_SIZE; ++i ) - { - for ( acolorhist_list achl = acht.hash[i]; achl != NULL; achl = achl->next ) - { - // Add the new entry. - achv[j] = achl->ch; - ++j; - } - } - - // All done. - return achv; -} - -static acolorhist_item *pam_computeacolorhist( const RageSurface *src, int maxacolors, int* acolorsP ) -{ - acolorhash_hash acht; - if ( !pam_computeacolorhash( src, maxacolors, acolorsP, acht ) ) - return NULL; - - acolorhist_item *achv = pam_acolorhashtoacolorhist( acht, *acolorsP ); - return achv; -} - -static void pam_addtoacolorhash( acolorhash_hash &acht, const uint8_t acolorP[4], int value ) -{ - acolorhist_list achl = (acolorhist_list) malloc( sizeof(struct acolorhist_list_item) ); - ASSERT( achl != NULL ); - - int hash = pam_hashapixel( acolorP ); - memcpy( achl->ch.acolor, acolorP, sizeof(apixel) ); - achl->ch.value = value; - achl->next = acht.hash[hash]; - acht.hash[hash] = achl; -} - - -static int pam_lookupacolor( const acolorhash_hash &acht, const uint8_t acolorP[4] ) -{ - const int hash = pam_hashapixel( acolorP ); - for ( acolorhist_list_item *achl = acht.hash[hash]; achl != NULL; achl = achl->next ) - if ( PAM_EQUAL( achl->ch.acolor, acolorP ) ) - return achl->ch.value; - - return -1; -} - - -static void pam_freeacolorhist( acolorhist_item *achv ) -{ - free( (char*) achv ); -} - -/* - * Copyright (C) 1989, 1991 by Jef Poskanzer. - * Copyright (C) 1997, 2000, 2002 by Greg Roelofs; based on an idea by - * Stefan Schneider. - * - * Permission to use, copy, modify, and distribute this software and its - * documentation for any purpose and without fee is hereby granted, provided - * that the above copyright notice appear in all copies and that both that - * copyright notice and this permission notice appear in supporting - * documentation. This software is provided "as is" without express or - * implied warranty. - */ +/* from http://www.libpng.org/pub/png/apps/pngquant.html */ + +#include "global.h" +#include "RageSurfaceUtils_Palettize.h" +#include "RageSurface.h" +#include "RageSurfaceUtils.h" +#include "RageUtil.h" + +typedef uint8_t pixval; +typedef uint8_t apixel[4]; + +#define PAM_GETR(p) ((p)[0]) +#define PAM_GETG(p) ((p)[1]) +#define PAM_GETB(p) ((p)[2]) +#define PAM_GETA(p) ((p)[3]) +#define PAM_ASSIGN(p,red,grn,blu,alf) \ + do { (p)[0] = (red); (p)[1] = (grn); (p)[2] = (blu); (p)[3] = (alf); } while (0) +#define PAM_EQUAL(p,q) \ + ((p)[0] == (q)[0] && (p)[1] == (q)[1] && (p)[2] == (q)[2] && (p)[3] == (q)[3]) +#define PAM_DEPTH(p) \ + PAM_ASSIGN( (p), (uint8_t) table[PAM_GETR(p)], (uint8_t) table[PAM_GETG(p)], (uint8_t) table[PAM_GETB(p)], (uint8_t) table[PAM_GETA(p)] ) + +struct acolorhist_item +{ + apixel acolor; + int value; +}; + +typedef struct acolorhist_list_item *acolorhist_list; +struct acolorhist_list_item +{ + struct acolorhist_item ch; + acolorhist_list next; +}; + +static const unsigned int HASH_SIZE = 20023u; + +struct acolorhash_hash +{ + acolorhist_list hash[HASH_SIZE]; + acolorhash_hash() + { + ZERO( hash ); + } + + ~acolorhash_hash() + { + for( unsigned i = 0; i < HASH_SIZE; ++i ) + { + acolorhist_list achl, achlnext; + for ( achl = hash[i]; achl != nullptr; achl = achlnext ) + { + achlnext = achl->next; + free( achl ); + } + } + } +}; + + + +#define MAXCOLORS 32767 +#define FS_SCALE 1024 /* Floyd-Steinberg scaling factor */ + +/* #define REP_AVERAGE_COLORS */ +#define REP_AVERAGE_PIXELS + + + +static acolorhist_item *mediancut( acolorhist_item *achv, int colors, int sum, int maxval, int newcolors ); + +static bool compare_index_0( const acolorhist_item &ch1, const acolorhist_item &ch2 ) +{ + return ch1.acolor[0] < ch2.acolor[0]; +} + +static bool compare_index_1( const acolorhist_item &ch1, const acolorhist_item &ch2 ) +{ + return ch1.acolor[1] < ch2.acolor[1]; +} + +static bool compare_index_2( const acolorhist_item &ch1, const acolorhist_item &ch2 ) +{ + return ch1.acolor[2] < ch2.acolor[2]; +} + +static bool compare_index_3( const acolorhist_item &ch1, const acolorhist_item &ch2 ) +{ + return ch1.acolor[3] < ch2.acolor[3]; +} + +static acolorhist_item *pam_computeacolorhist( const RageSurface *src, int maxacolors, int* acolorsP ); +static void pam_addtoacolorhash( acolorhash_hash &acht, const uint8_t acolorP[4], int value ); +static int pam_lookupacolor( const acolorhash_hash &acht, const uint8_t acolorP[4] ); +static void pam_freeacolorhist( acolorhist_item *achv ); + +struct pixerror_t +{ + int c[4]; +}; + +void RageSurfaceUtils::Palettize( RageSurface *&pImg, int iColors, bool bDither ) +{ + ASSERT( iColors != 0 ); + + acolorhist_item *acolormap=NULL; + int newcolors = 0; + + // "apixel", etc. make assumptions about byte order. + RageSurfaceUtils::ConvertSurface( pImg, pImg->w, pImg->h, 32, + Swap32BE(0xFF000000), Swap32BE(0x00FF0000), Swap32BE(0x0000FF00), Swap32BE(0x000000FF)); + + pixval maxval = 255; + + { + /* Attempt to make a histogram of the colors, unclustered. + * If at first we don't succeed, lower maxval to increase color + * coherence and try again. This will eventually terminate, with + * maxval at worst 15, since 32^3 is approximately MAXCOLORS. + * [GRR POSSIBLE BUG: what about 32^4 ?] */ + acolorhist_item *achv; + int colors; + while(1) + { + achv = pam_computeacolorhist( pImg, MAXCOLORS, &colors ); + if( achv != nullptr ) + break; + pixval newmaxval = maxval / 2; + + int table[256]; + for( int c = 0; c <= maxval; ++c ) + table[c] = ( (uint8_t) c * newmaxval + maxval/2 ) / maxval; + + for( int row = 0; row < pImg->h; ++row ) + { + apixel *pP = (apixel *) (pImg->pixels+row*pImg->pitch); + for( int col = 0; col < pImg->w; ++col, ++pP ) + PAM_DEPTH( *pP ); + } + maxval = newmaxval; + } + newcolors = min( colors, iColors ); + + // Apply median-cut to histogram, making the new acolormap. + acolormap = mediancut( achv, colors, pImg->h * pImg->w, maxval, newcolors ); + pam_freeacolorhist( achv ); + } + + RageSurface *pRet = CreateSurface( pImg->w, pImg->h, 8, 0, 0, 0, 0 ); + pRet->format->palette->ncolors = newcolors; + + // Rescale the palette colors to a maxval of 255. + { + RageSurfacePalette *pal = pRet->format->palette; + for( int x = 0; x < pal->ncolors; ++x ) + { + // This is really just PAM_DEPTH() broken out for the palette. + pal->colors[x].r + = (PAM_GETR(acolormap[x].acolor)*255 + (maxval >> 1)) / maxval; + pal->colors[x].g + = (PAM_GETG(acolormap[x].acolor)*255 + (maxval >> 1)) / maxval; + pal->colors[x].b + = (PAM_GETB(acolormap[x].acolor)*255 + (maxval >> 1)) / maxval; + pal->colors[x].a + = (PAM_GETA(acolormap[x].acolor)*255 + (maxval >> 1)) / maxval; + } + } + + // Map the colors in the image to their closest match in the new colormap. + acolorhash_hash acht; + + bool fs_direction = 0; + pixerror_t *thiserr = NULL, *nexterr = NULL; + + if( bDither ) + { + // Initialize Floyd-Steinberg error vectors. + thiserr = new pixerror_t[pImg->w + 2]; + nexterr = new pixerror_t[pImg->w + 2]; + + memset( thiserr, 0, sizeof(pixerror_t) * (pImg->w + 2) ); + } + + for( int row = 0; row < pImg->h; ++row ) + { + if( bDither ) + memset( nexterr, 0, sizeof(pixerror_t) * (pImg->w + 2) ); + + int col, limitcol; + if( !fs_direction ) + { + col = 0; + limitcol = pImg->w; + } else { + col = pImg->w - 1; + limitcol = -1; + } + + const uint8_t *pIn = pImg->pixels + row*pImg->pitch; + uint8_t *pOut = pRet->pixels + row*pRet->pitch; + pIn += col * 4; + pOut += col; + + do + { + int32_t sc[4]; + uint8_t pixel[4] = { pIn[0], pIn[1], pIn[2], pIn[3] }; + if( bDither ) + { + // Use Floyd-Steinberg errors to adjust actual color. + for( int c = 0; c < 4; ++c ) + { + sc[c] = pixel[c] + thiserr[col + 1].c[c] / FS_SCALE; + sc[c] = clamp( sc[c], 0, (int32_t) maxval ); + } + + PAM_ASSIGN( pixel, (uint8_t)sc[0], (uint8_t)sc[1], (uint8_t)sc[2], (uint8_t)sc[3] ); + } + + // Check hash table to see if we have already matched this color. + int ind = pam_lookupacolor( acht, pixel ); + if( ind == -1 ) + { + // No; search acolormap for closest match. + static int square_table[512], *pSquareTable = NULL; + if( pSquareTable == NULL ) + { + pSquareTable = square_table+256; + for( int c = -256; c < 256; ++c ) + pSquareTable[c] = c*c; + } + + long dist = 2000000000; + for( int i = 0; i < newcolors; ++i ) + { + const uint8_t *colors2 = acolormap[i].acolor; + + int newdist = 0; + newdist += pSquareTable[ int(pixel[0]) - colors2[0] ]; + newdist += pSquareTable[ int(pixel[1]) - colors2[1] ]; + newdist += pSquareTable[ int(pixel[2]) - colors2[2] ]; + newdist += pSquareTable[ int(pixel[3]) - colors2[3] ]; + + if( newdist < dist ) + { + ind = i; + dist = newdist; + } + } + + pam_addtoacolorhash( acht, pixel, ind ); + } + + if( bDither ) + { + // Propagate Floyd-Steinberg error terms. + if( !fs_direction ) + { + for( int c = 0; c < 4; ++c ) + { + long err = (sc[c] - (long)acolormap[ind].acolor[c])*FS_SCALE; + thiserr[col + 2].c[c] += ( err * 7 ) / 16; + nexterr[col ].c[c] += ( err * 3 ) / 16; + nexterr[col + 1].c[c] += ( err * 5 ) / 16; + nexterr[col + 2].c[c] += ( err * 1 ) / 16; + } + } else { + for( int c = 0; c < 4; ++c ) + { + long err = (sc[c] - (long)acolormap[ind].acolor[c])*FS_SCALE; + thiserr[col ].c[c] += ( err * 7 ) / 16; + nexterr[col + 2].c[c] += ( err * 3 ) / 16; + nexterr[col + 1].c[c] += ( err * 5 ) / 16; + nexterr[col ].c[c] += ( err * 1 ) / 16; + } + } + } + + *pOut = (uint8_t) ind; + + if( !fs_direction ) + { + ++col; + pIn += 4; + ++pOut; + } else { + --col; + pIn -= 4; + --pOut; + } + } + while( col != limitcol ); + + if( bDither ) + { + swap( thiserr, nexterr ); + fs_direction = !fs_direction; + } + } + + delete [] thiserr; + delete [] nexterr; + + delete pImg; + pImg = pRet; +} + +/* Here is the fun part, the median-cut colormap generator. This is based + * on Paul Heckbert's paper, "Color Image Quantization for Frame Buffer + * Display," SIGGRAPH 1982 Proceedings, page 297. */ + +typedef struct box *box_vector; +struct box +{ + int ind; + int colors; + int sum; +}; + +static bool CompareBySumDescending( const box &b1, const box &b2 ) +{ + return b2.sum < b1.sum; +} + + +static acolorhist_item *mediancut( acolorhist_item *achv, int colors, int sum, int maxval, int newcolors ) +{ + acolorhist_item *acolormap; + box_vector bv; + int boxes; + + bv = (box_vector) malloc( sizeof(struct box) * newcolors ); + ASSERT( bv != nullptr ); + acolormap = (acolorhist_item*) malloc( sizeof(struct acolorhist_item) * newcolors); + ASSERT( acolormap != nullptr ); + + for ( int i = 0; i < newcolors; ++i ) + PAM_ASSIGN( acolormap[i].acolor, 0, 0, 0, 0 ); + + // Set up the initial box. + bv[0].ind = 0; + bv[0].colors = colors; + bv[0].sum = sum; + boxes = 1; + + // Main loop: split boxes until we have enough. + while( boxes < newcolors ) + { + int indx, clrs; + int sm; + int halfsum, lowersum; + + // Find the first splittable box. + int bi; + for( bi = 0; bi < boxes; ++bi ) + if ( bv[bi].colors >= 2 ) + break; + if( bi == boxes ) + break; // ran out of colors! + indx = bv[bi].ind; + clrs = bv[bi].colors; + sm = bv[bi].sum; + + /* Go through the box finding the minimum and maximum of each + * component - the boundaries of the box. */ + int mins[4], maxs[4]; + mins[0] = maxs[0] = achv[indx].acolor[0]; + mins[1] = maxs[1] = achv[indx].acolor[1]; + mins[2] = maxs[2] = achv[indx].acolor[2]; + mins[3] = maxs[3] = achv[indx].acolor[3]; + + for ( int i = 1; i < clrs; ++i ) + { + int v; + v = achv[indx + i].acolor[0]; + mins[0] = min( mins[0], v ); + maxs[0] = max( maxs[0], v ); + v = achv[indx + i].acolor[1]; + mins[1] = min( mins[1], v ); + maxs[1] = max( maxs[1], v ); + v = achv[indx + i].acolor[2]; + mins[2] = min( mins[2], v ); + maxs[2] = max( maxs[2], v ); + v = achv[indx + i].acolor[3]; + mins[3] = min( mins[3], v ); + maxs[3] = max( maxs[3], v ); + } + + // Find the largest dimension, and sort by that component. + { + int iMax = 0; + for( int i = 1; i < 3; ++i ) + if( maxs[i] - mins[i] > maxs[iMax] - mins[iMax] ) + iMax = i; + + switch( iMax ) + { + case 0: sort( &achv[indx], &achv[indx+clrs], compare_index_0 ); break; + case 1: sort( &achv[indx], &achv[indx+clrs], compare_index_1 ); break; + case 2: sort( &achv[indx], &achv[indx+clrs], compare_index_2 ); break; + case 3: sort( &achv[indx], &achv[indx+clrs], compare_index_3 ); break; + } + } + /* Now find the median based on the counts, so that about half the + * pixels (not colors, pixels) are in each subdivision. */ + lowersum = achv[indx].value; + halfsum = sm / 2; + int j; + for ( j = 1; j < clrs - 1; ++j ) + { + if ( lowersum >= halfsum ) + break; + lowersum += achv[indx + j].value; + } + + // Split the box, and sort to bring the biggest boxes to the top. + bv[bi].colors = j; + bv[bi].sum = lowersum; + bv[boxes].ind = indx + j; + bv[boxes].colors = clrs - j; + bv[boxes].sum = sm - lowersum; + ++boxes; + sort( &bv[0], &bv[boxes], CompareBySumDescending ); + } + + /* Ok, we've got enough boxes. Now choose a representative color for + * each box. There are a number of possible ways to make this choice. + * One would be to choose the center of the box; this ignores any structure + * within the boxes. Another method would be to average all the colors in + * the box - this is the method specified in Heckbert's paper. A third + * method is to average all the pixels in the box. You can switch which + * method is used by switching the commenting on the REP_ defines at + * the beginning of this source file. */ + for( int bi = 0; bi < boxes; ++bi ) + { +#ifdef REP_AVERAGE_COLORS + int indx = bv[bi].ind; + int clrs = bv[bi].colors; + long r = 0, g = 0, b = 0, a = 0; + + for ( i = 0; i < clrs; ++i ) + { + r += PAM_GETR( achv[indx + i].acolor ); + g += PAM_GETG( achv[indx + i].acolor ); + b += PAM_GETB( achv[indx + i].acolor ); + a += PAM_GETA( achv[indx + i].acolor ); + } + r = r / clrs; + g = g / clrs; + b = b / clrs; + a = a / clrs; + PAM_ASSIGN( acolormap[bi].acolor, r, g, b, a ); +#endif // REP_AVERAGE_COLORS +#ifdef REP_AVERAGE_PIXELS + int indx = bv[bi].ind; + int clrs = bv[bi].colors; + long r = 0, g = 0, b = 0, a = 0, lSum = 0; + + for ( int i = 0; i < clrs; ++i ) + { + r += PAM_GETR( achv[indx + i].acolor ) * achv[indx + i].value; + g += PAM_GETG( achv[indx + i].acolor ) * achv[indx + i].value; + b += PAM_GETB( achv[indx + i].acolor ) * achv[indx + i].value; + a += PAM_GETA( achv[indx + i].acolor ) * achv[indx + i].value; + lSum += achv[indx + i].value; + } + r = r / lSum; + r = min( r, (long) maxval ); + g = g / lSum; + g = min( g, (long) maxval ); + b = b / lSum; + b = min( b, (long) maxval ); + a = a / lSum; + a = min( a, (long) maxval ); + PAM_ASSIGN( acolormap[bi].acolor, (uint8_t)r, (uint8_t)g, (uint8_t)b, (uint8_t)a ); +#endif // REP_AVERAGE_PIXELS + } + + // All done. + return acolormap; +} + +/* + * libpam3.c - pam (portable alpha map) utility library part 3 + * + * Colormap routines. + * + * Copyright (C) 1989, 1991 by Jef Poskanzer. + * Copyright (C) 1997 by Greg Roelofs. + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose and without fee is hereby granted, provided + * that the above copyright notice appear in all copies and that both that + * copyright notice and this permission notice appear in supporting + * documentation. This software is provided "as is" without express or + * implied warranty. + */ + +#define pam_hashapixel(p) ( ( (unsigned) PAM_GETR(p) * 33023 + \ + (unsigned) PAM_GETG(p) * 30013 + \ + (unsigned) PAM_GETB(p) * 27011 + \ + (unsigned) PAM_GETA(p) * 24007 ) \ + % (unsigned) HASH_SIZE ) + +static bool pam_computeacolorhash( const RageSurface *src, int maxacolors, int* acolorsP, acolorhash_hash &hash ) +{ + ASSERT( src->format->BytesPerPixel == 4 ); + + *acolorsP = 0; + + // Go through the entire image, building a hash table of colors. + for( int row = 0; row < src->h; ++row ) + { + const apixel *pP = (const apixel *) (src->pixels + row*src->pitch); + for( int col = 0; col < src->w; ++col, pP++ ) + { + int hashval = pam_hashapixel( *pP ); + acolorhist_list achl; + for ( achl = hash.hash[hashval]; achl != nullptr; achl = achl->next ) + if ( PAM_EQUAL( achl->ch.acolor, *pP ) ) + break; + if ( achl != nullptr ) + ++achl->ch.value; + else + { + if ( ++(*acolorsP) > maxacolors ) + return false; + achl = (acolorhist_list) malloc( sizeof(struct acolorhist_list_item) ); + ASSERT( achl != nullptr ); + + memcpy( achl->ch.acolor, *pP, sizeof(apixel) ); + achl->ch.value = 1; + achl->next = hash.hash[hashval]; + hash.hash[hashval] = achl; + } + } + } + + return true; +} + +static acolorhist_item *pam_acolorhashtoacolorhist( const acolorhash_hash &acht, int maxacolors ) +{ + // Collate the hash table into a simple acolorhist array. + acolorhist_item *achv = (acolorhist_item*) malloc( maxacolors * sizeof(struct acolorhist_item) ); + ASSERT( achv != nullptr ); + + // Loop through the hash table. + int j = 0; + for( unsigned i = 0; i < HASH_SIZE; ++i ) + { + for ( acolorhist_list achl = acht.hash[i]; achl != nullptr; achl = achl->next ) + { + // Add the new entry. + achv[j] = achl->ch; + ++j; + } + } + + // All done. + return achv; +} + +static acolorhist_item *pam_computeacolorhist( const RageSurface *src, int maxacolors, int* acolorsP ) +{ + acolorhash_hash acht; + if ( !pam_computeacolorhash( src, maxacolors, acolorsP, acht ) ) + return NULL; + + acolorhist_item *achv = pam_acolorhashtoacolorhist( acht, *acolorsP ); + return achv; +} + +static void pam_addtoacolorhash( acolorhash_hash &acht, const uint8_t acolorP[4], int value ) +{ + acolorhist_list achl = (acolorhist_list) malloc( sizeof(struct acolorhist_list_item) ); + ASSERT( achl != nullptr ); + + int hash = pam_hashapixel( acolorP ); + memcpy( achl->ch.acolor, acolorP, sizeof(apixel) ); + achl->ch.value = value; + achl->next = acht.hash[hash]; + acht.hash[hash] = achl; +} + + +static int pam_lookupacolor( const acolorhash_hash &acht, const uint8_t acolorP[4] ) +{ + const int hash = pam_hashapixel( acolorP ); + for ( acolorhist_list_item *achl = acht.hash[hash]; achl != nullptr; achl = achl->next ) + if ( PAM_EQUAL( achl->ch.acolor, acolorP ) ) + return achl->ch.value; + + return -1; +} + + +static void pam_freeacolorhist( acolorhist_item *achv ) +{ + free( (char*) achv ); +} + +/* + * Copyright (C) 1989, 1991 by Jef Poskanzer. + * Copyright (C) 1997, 2000, 2002 by Greg Roelofs; based on an idea by + * Stefan Schneider. + * + * Permission to use, copy, modify, and distribute this software and its + * documentation for any purpose and without fee is hereby granted, provided + * that the above copyright notice appear in all copies and that both that + * copyright notice and this permission notice appear in supporting + * documentation. This software is provided "as is" without express or + * implied warranty. + */ diff --git a/src/RageSurface_Load.cpp b/src/RageSurface_Load.cpp index ee2f69da02..c740a76cb9 100644 --- a/src/RageSurface_Load.cpp +++ b/src/RageSurface_Load.cpp @@ -1,144 +1,144 @@ -#include "global.h" -#include "RageSurface_Load.h" -#include "RageSurface_Load_PNG.h" -#include "RageSurface_Load_JPEG.h" -#include "RageSurface_Load_GIF.h" -#include "RageSurface_Load_BMP.h" -#include "RageUtil.h" -#include "RageFile.h" -#include "RageLog.h" -#include - - -static RageSurface *TryOpenFile( RString sPath, bool bHeaderOnly, RString &error, RString format, bool &bKeepTrying ) -{ - RageSurface *ret = NULL; - RageSurfaceUtils::OpenResult result; - if( !format.CompareNoCase("png") ) - result = RageSurface_Load_PNG( sPath, ret, bHeaderOnly, error ); - else if( !format.CompareNoCase("gif") ) - result = RageSurface_Load_GIF( sPath, ret, bHeaderOnly, error ); - else if( !format.CompareNoCase("jpg") || !format.CompareNoCase("jpeg") ) - result = RageSurface_Load_JPEG( sPath, ret, bHeaderOnly, error ); - else if( !format.CompareNoCase("bmp") ) - result = RageSurface_Load_BMP( sPath, ret, bHeaderOnly, error ); - else - { - error = "Unsupported format"; - bKeepTrying = true; - return NULL; - } - - if( result == RageSurfaceUtils::OPEN_OK ) - { - ASSERT( ret != NULL ); - return ret; - } - - LOG->Trace( "Format %s failed: %s", format.c_str(), error.c_str() ); - - /* - * The file failed to open, or failed to read. This indicates a problem that will - * affect all readers, so don't waste time trying more readers. (OPEN_IO_ERROR) - * - * Errors fall in two categories: - * OPEN_UNKNOWN_FILE_FORMAT: Data was successfully read from the file, but it's the - * wrong file format. The error message always looks like "unknown file format" or - * "Not Vorbis data"; ignore it so we always give a consistent error message, and - * continue trying other file formats. - * - * OPEN_FATAL_ERROR: Either the file was opened successfully and appears to be the - * correct format, but a fatal format-specific error was encountered that will probably - * not be fixed by using a different reader (for example, an Ogg file that doesn't - * actually contain any audio streams); or the file failed to open or read ("I/O - * error", "permission denied"), in which case all other readers will probably fail, - * too. The returned error is used, and no other formats will be tried. - */ - bKeepTrying = (result != RageSurfaceUtils::OPEN_FATAL_ERROR); - switch( result ) - { - case RageSurfaceUtils::OPEN_UNKNOWN_FILE_FORMAT: - bKeepTrying = true; - error = "Unknown file format"; - break; - - case RageSurfaceUtils::OPEN_FATAL_ERROR: - /* The file matched, but failed to load. We know it's this type of data; - * don't bother trying the other file types. */ - bKeepTrying = false; - break; - default: break; - } - - return NULL; -} - -RageSurface *RageSurfaceUtils::LoadFile( const RString &sPath, RString &error, bool bHeaderOnly ) -{ - { - RageFile TestOpen; - if( !TestOpen.Open( sPath ) ) - { - error = TestOpen.GetError(); - return NULL; - } - } - - set FileTypes; - FileTypes.insert("png"); - FileTypes.insert("jpg"); - FileTypes.insert("jpeg"); - FileTypes.insert("gif"); - FileTypes.insert("bmp"); - - RString format = GetExtension(sPath); - format.MakeLower(); - - bool bKeepTrying = true; - - /* If the extension matches a format, try that first. */ - if( FileTypes.find(format) != FileTypes.end() ) - { - RageSurface *ret = TryOpenFile( sPath, bHeaderOnly, error, format, bKeepTrying ); - if( ret ) - return ret; - FileTypes.erase( format ); - } - - for( set::iterator it = FileTypes.begin(); bKeepTrying && it != FileTypes.end(); ++it ) - { - RageSurface *ret = TryOpenFile( sPath, bHeaderOnly, error, *it, bKeepTrying ); - if( ret ) - { - LOG->UserLog( "Graphic file", sPath, "is really %s", it->c_str() ); - return ret; - } - } - - return NULL; -} - -/* - * (c) 2004 Glenn Maynard - * 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. - */ +#include "global.h" +#include "RageSurface_Load.h" +#include "RageSurface_Load_PNG.h" +#include "RageSurface_Load_JPEG.h" +#include "RageSurface_Load_GIF.h" +#include "RageSurface_Load_BMP.h" +#include "RageUtil.h" +#include "RageFile.h" +#include "RageLog.h" +#include + + +static RageSurface *TryOpenFile( RString sPath, bool bHeaderOnly, RString &error, RString format, bool &bKeepTrying ) +{ + RageSurface *ret = NULL; + RageSurfaceUtils::OpenResult result; + if( !format.CompareNoCase("png") ) + result = RageSurface_Load_PNG( sPath, ret, bHeaderOnly, error ); + else if( !format.CompareNoCase("gif") ) + result = RageSurface_Load_GIF( sPath, ret, bHeaderOnly, error ); + else if( !format.CompareNoCase("jpg") || !format.CompareNoCase("jpeg") ) + result = RageSurface_Load_JPEG( sPath, ret, bHeaderOnly, error ); + else if( !format.CompareNoCase("bmp") ) + result = RageSurface_Load_BMP( sPath, ret, bHeaderOnly, error ); + else + { + error = "Unsupported format"; + bKeepTrying = true; + return NULL; + } + + if( result == RageSurfaceUtils::OPEN_OK ) + { + ASSERT( ret != nullptr ); + return ret; + } + + LOG->Trace( "Format %s failed: %s", format.c_str(), error.c_str() ); + + /* + * The file failed to open, or failed to read. This indicates a problem that will + * affect all readers, so don't waste time trying more readers. (OPEN_IO_ERROR) + * + * Errors fall in two categories: + * OPEN_UNKNOWN_FILE_FORMAT: Data was successfully read from the file, but it's the + * wrong file format. The error message always looks like "unknown file format" or + * "Not Vorbis data"; ignore it so we always give a consistent error message, and + * continue trying other file formats. + * + * OPEN_FATAL_ERROR: Either the file was opened successfully and appears to be the + * correct format, but a fatal format-specific error was encountered that will probably + * not be fixed by using a different reader (for example, an Ogg file that doesn't + * actually contain any audio streams); or the file failed to open or read ("I/O + * error", "permission denied"), in which case all other readers will probably fail, + * too. The returned error is used, and no other formats will be tried. + */ + bKeepTrying = (result != RageSurfaceUtils::OPEN_FATAL_ERROR); + switch( result ) + { + case RageSurfaceUtils::OPEN_UNKNOWN_FILE_FORMAT: + bKeepTrying = true; + error = "Unknown file format"; + break; + + case RageSurfaceUtils::OPEN_FATAL_ERROR: + /* The file matched, but failed to load. We know it's this type of data; + * don't bother trying the other file types. */ + bKeepTrying = false; + break; + default: break; + } + + return NULL; +} + +RageSurface *RageSurfaceUtils::LoadFile( const RString &sPath, RString &error, bool bHeaderOnly ) +{ + { + RageFile TestOpen; + if( !TestOpen.Open( sPath ) ) + { + error = TestOpen.GetError(); + return NULL; + } + } + + set FileTypes; + FileTypes.insert("png"); + FileTypes.insert("jpg"); + FileTypes.insert("jpeg"); + FileTypes.insert("gif"); + FileTypes.insert("bmp"); + + RString format = GetExtension(sPath); + format.MakeLower(); + + bool bKeepTrying = true; + + /* If the extension matches a format, try that first. */ + if( FileTypes.find(format) != FileTypes.end() ) + { + RageSurface *ret = TryOpenFile( sPath, bHeaderOnly, error, format, bKeepTrying ); + if( ret ) + return ret; + FileTypes.erase( format ); + } + + for( set::iterator it = FileTypes.begin(); bKeepTrying && it != FileTypes.end(); ++it ) + { + RageSurface *ret = TryOpenFile( sPath, bHeaderOnly, error, *it, bKeepTrying ); + if( ret ) + { + LOG->UserLog( "Graphic file", sPath, "is really %s", it->c_str() ); + return ret; + } + } + + return NULL; +} + +/* + * (c) 2004 Glenn Maynard + * 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. + */ diff --git a/src/RageSurface_Load_BMP.cpp b/src/RageSurface_Load_BMP.cpp index 686beb9721..7534179470 100644 --- a/src/RageSurface_Load_BMP.cpp +++ b/src/RageSurface_Load_BMP.cpp @@ -1,236 +1,236 @@ -#include "global.h" -#include "RageSurface_Load_BMP.h" -#include "RageFile.h" -#include "RageUtil.h" -#include "RageLog.h" -#include "RageSurface.h" -using namespace FileReading; - -/* Tested with http://entropymine.com/jason/bmpsuite/. */ - -enum -{ - COMP_BI_RGB = 0, - COMP_BI_RLE4, /* unsupported */ - COMP_BI_RLE8, /* unsupported */ - COMP_BI_BITFIELDS -}; - - -/* When returning error, the first error encountered takes priority. */ -#define FATAL_ERROR(s) \ -{ \ - if( sError.size() == 0 ) sError = (s); \ - return RageSurfaceUtils::OPEN_FATAL_ERROR; \ -} - -static RageSurfaceUtils::OpenResult LoadBMP( RageFile &f, RageSurface *&img, RString &sError ) -{ - char magic[2]; - ReadBytes( f, magic, 2, sError ); - if( magic[0] != 'B' || magic[1] != 'M' ) - { - sError = "not a BMP"; - return RageSurfaceUtils::OPEN_UNKNOWN_FILE_FORMAT; - } - - img = NULL; - - read_u32_le( f, sError ); /* file size */ - read_u32_le( f, sError ); /* unused */ - uint32_t iDataOffset = read_u32_le( f, sError ); - uint32_t iHeaderSize = read_u32_le( f, sError ); - - uint32_t iWidth, iHeight, iPlanes, iBPP, iCompression = COMP_BI_RGB, iColors = 0; - if( iHeaderSize == 12 ) - { - /* OS/2 format */ - iWidth = read_u16_le( f, sError ); - iHeight = read_u16_le( f, sError ); - iPlanes = read_u16_le( f, sError ); - iBPP = read_u16_le( f, sError ); - } - else if( iHeaderSize == 40 ) - { - iWidth = read_u32_le( f, sError ); - iHeight = read_u32_le( f, sError ); - iPlanes = read_u16_le( f, sError ); - iBPP = read_u16_le( f, sError ); - iCompression = read_u32_le( f, sError ); - read_u32_le( f, sError ); /* bitmap size */ - read_u32_le( f, sError ); /* horiz resolution */ - read_u32_le( f, sError ); /* vert resolution */ - iColors = read_u32_le( f, sError ); - read_u32_le( f, sError ); /* "important" colors */ - } - else - FATAL_ERROR( ssprintf( "expected header size of 40, got %u", iHeaderSize ) ); - - if( iBPP <= 8 && iColors == 0 ) - iColors = 1 << iBPP; - if( iPlanes != 1 ) - FATAL_ERROR( ssprintf( "expected one plane, got %u", iPlanes ) ); - if( iBPP != 1 && iBPP != 4 && iBPP != 8 && iBPP != 16 && iBPP != 24 && iBPP != 32 ) - FATAL_ERROR( ssprintf( "unsupported bpp %u", iBPP ) ); - if( iCompression != COMP_BI_RGB && iCompression != COMP_BI_BITFIELDS ) - FATAL_ERROR( ssprintf( "unsupported compression %u", iCompression ) ); - - if( iCompression == COMP_BI_BITFIELDS && iBPP <= 8 ) - FATAL_ERROR( ssprintf( "BI_BITFIELDS unexpected with bpp %u", iBPP ) ); - - int iFileBPP = iBPP; - iBPP = max( iBPP, 8u ); - - int Rmask = 0, Gmask = 0, Bmask = 0, Amask = 0; - switch( iBPP ) - { - case 16: - Rmask = Swap16LE( 0x7C00 ); - Gmask = Swap16LE( 0x03E0 ); - Bmask = Swap16LE( 0x001F ); - break; - case 24: - Rmask = Swap24LE( 0xFF0000 ); - Gmask = Swap24LE( 0x00FF00 ); - Bmask = Swap24LE( 0x0000FF ); - break; - case 32: - Rmask = Swap32LE( 0x00FF0000 ); - Gmask = Swap32LE( 0x0000FF00 ); - Bmask = Swap32LE( 0x000000FF ); - break; - } - - if( iCompression == COMP_BI_BITFIELDS ) - { - Rmask = read_u32_le( f, sError ); - Gmask = read_u32_le( f, sError ); - Bmask = read_u32_le( f, sError ); - } - - /* Stop on error before we use any of the values we just read. */ - if( sError.size() != 0 ) - return RageSurfaceUtils::OPEN_FATAL_ERROR; - - img = CreateSurface( iWidth, iHeight, iBPP, Rmask, Gmask, Bmask, Amask ); - - if( iBPP == 8 ) - { - RageSurfaceColor Palette[256]; - ZERO( Palette ); - - if( iColors > 256 ) - FATAL_ERROR( ssprintf( "unexpected colors %i", iColors ) ); - - for( unsigned i = 0; i < iColors; ++i ) - { - Palette[i].b = read_8( f, sError ); - Palette[i].g = read_8( f, sError ); - Palette[i].r = read_8( f, sError ); - Palette[i].a = 0xFF; - /* Windows BMP palettes are padded to 32bpp. */ - if( iHeaderSize == 40 ) - read_8( f, sError ); - } - - memcpy( img->fmt.palette->colors, Palette, sizeof(Palette) ); - } - - /* Stop on error before we seek, so we don't return the wrong error message. */ - if( sError.size() != 0 ) - return RageSurfaceUtils::OPEN_FATAL_ERROR; - - int iFilePitch = iFileBPP * iWidth; // in bits - iFilePitch = (iFilePitch+7) / 8; // in bytes: round up - iFilePitch = (iFilePitch+3) & ~3; // round up a multiple of 4 - - { - int ret = f.Seek( iDataOffset ); - if( ret == -1 ) - FATAL_ERROR( f.GetError() ); - if( ret != (int) iDataOffset ) - FATAL_ERROR( "Unexpected end of file" ); - } - - for( int y = (int) iHeight-1; y >= 0; --y ) - { - uint8_t *pRow = img->pixels + img->pitch*y; - RString buf; - - f.Read( buf, iFilePitch ); - - /* Expand 1- and 4-bits to 8-bits. */ - if( iFileBPP == 1 ) - { - for( unsigned x = 0; x < iWidth; ++x ) - { - int iByteNo = x >> 3; - int iBitNo = 7-(x&7); - int iBit = 1 << iBitNo; - pRow[x] = !!(buf[iByteNo] & iBit); - } - } - else if( iFileBPP == 4 ) - { - for( unsigned x = 0; x < iWidth; ++x ) - { - if( (x & 1) == 0 ) - pRow[x] = buf[x/2] & 0x0F; - else - pRow[x] = (buf[x/2] >> 4) & 0x0F; - } - } - else - memcpy( pRow, buf.data(), img->pitch ); - } - - return sError.size() != 0? RageSurfaceUtils::OPEN_FATAL_ERROR: RageSurfaceUtils::OPEN_OK; -} - -RageSurfaceUtils::OpenResult RageSurface_Load_BMP( const RString &sPath, RageSurface *&img, bool bHeaderOnly, RString &error ) -{ - RageFile f; - - if( !f.Open( sPath ) ) - { - error = f.GetError(); - return RageSurfaceUtils::OPEN_FATAL_ERROR; - } - - RageSurfaceUtils::OpenResult ret; - img = NULL; - ret = LoadBMP( f, img, error ); - - if( ret != RageSurfaceUtils::OPEN_OK && img != NULL ) - { - delete img; - img = NULL; - } - - return ret; -} - -/* - * Copyright (c) 2004 Glenn Maynard - * 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. - */ +#include "global.h" +#include "RageSurface_Load_BMP.h" +#include "RageFile.h" +#include "RageUtil.h" +#include "RageLog.h" +#include "RageSurface.h" +using namespace FileReading; + +/* Tested with http://entropymine.com/jason/bmpsuite/. */ + +enum +{ + COMP_BI_RGB = 0, + COMP_BI_RLE4, /* unsupported */ + COMP_BI_RLE8, /* unsupported */ + COMP_BI_BITFIELDS +}; + + +/* When returning error, the first error encountered takes priority. */ +#define FATAL_ERROR(s) \ +{ \ + if( sError.size() == 0 ) sError = (s); \ + return RageSurfaceUtils::OPEN_FATAL_ERROR; \ +} + +static RageSurfaceUtils::OpenResult LoadBMP( RageFile &f, RageSurface *&img, RString &sError ) +{ + char magic[2]; + ReadBytes( f, magic, 2, sError ); + if( magic[0] != 'B' || magic[1] != 'M' ) + { + sError = "not a BMP"; + return RageSurfaceUtils::OPEN_UNKNOWN_FILE_FORMAT; + } + + img = NULL; + + read_u32_le( f, sError ); /* file size */ + read_u32_le( f, sError ); /* unused */ + uint32_t iDataOffset = read_u32_le( f, sError ); + uint32_t iHeaderSize = read_u32_le( f, sError ); + + uint32_t iWidth, iHeight, iPlanes, iBPP, iCompression = COMP_BI_RGB, iColors = 0; + if( iHeaderSize == 12 ) + { + /* OS/2 format */ + iWidth = read_u16_le( f, sError ); + iHeight = read_u16_le( f, sError ); + iPlanes = read_u16_le( f, sError ); + iBPP = read_u16_le( f, sError ); + } + else if( iHeaderSize == 40 ) + { + iWidth = read_u32_le( f, sError ); + iHeight = read_u32_le( f, sError ); + iPlanes = read_u16_le( f, sError ); + iBPP = read_u16_le( f, sError ); + iCompression = read_u32_le( f, sError ); + read_u32_le( f, sError ); /* bitmap size */ + read_u32_le( f, sError ); /* horiz resolution */ + read_u32_le( f, sError ); /* vert resolution */ + iColors = read_u32_le( f, sError ); + read_u32_le( f, sError ); /* "important" colors */ + } + else + FATAL_ERROR( ssprintf( "expected header size of 40, got %u", iHeaderSize ) ); + + if( iBPP <= 8 && iColors == 0 ) + iColors = 1 << iBPP; + if( iPlanes != 1 ) + FATAL_ERROR( ssprintf( "expected one plane, got %u", iPlanes ) ); + if( iBPP != 1 && iBPP != 4 && iBPP != 8 && iBPP != 16 && iBPP != 24 && iBPP != 32 ) + FATAL_ERROR( ssprintf( "unsupported bpp %u", iBPP ) ); + if( iCompression != COMP_BI_RGB && iCompression != COMP_BI_BITFIELDS ) + FATAL_ERROR( ssprintf( "unsupported compression %u", iCompression ) ); + + if( iCompression == COMP_BI_BITFIELDS && iBPP <= 8 ) + FATAL_ERROR( ssprintf( "BI_BITFIELDS unexpected with bpp %u", iBPP ) ); + + int iFileBPP = iBPP; + iBPP = max( iBPP, 8u ); + + int Rmask = 0, Gmask = 0, Bmask = 0, Amask = 0; + switch( iBPP ) + { + case 16: + Rmask = Swap16LE( 0x7C00 ); + Gmask = Swap16LE( 0x03E0 ); + Bmask = Swap16LE( 0x001F ); + break; + case 24: + Rmask = Swap24LE( 0xFF0000 ); + Gmask = Swap24LE( 0x00FF00 ); + Bmask = Swap24LE( 0x0000FF ); + break; + case 32: + Rmask = Swap32LE( 0x00FF0000 ); + Gmask = Swap32LE( 0x0000FF00 ); + Bmask = Swap32LE( 0x000000FF ); + break; + } + + if( iCompression == COMP_BI_BITFIELDS ) + { + Rmask = read_u32_le( f, sError ); + Gmask = read_u32_le( f, sError ); + Bmask = read_u32_le( f, sError ); + } + + /* Stop on error before we use any of the values we just read. */ + if( sError.size() != 0 ) + return RageSurfaceUtils::OPEN_FATAL_ERROR; + + img = CreateSurface( iWidth, iHeight, iBPP, Rmask, Gmask, Bmask, Amask ); + + if( iBPP == 8 ) + { + RageSurfaceColor Palette[256]; + ZERO( Palette ); + + if( iColors > 256 ) + FATAL_ERROR( ssprintf( "unexpected colors %i", iColors ) ); + + for( unsigned i = 0; i < iColors; ++i ) + { + Palette[i].b = read_8( f, sError ); + Palette[i].g = read_8( f, sError ); + Palette[i].r = read_8( f, sError ); + Palette[i].a = 0xFF; + /* Windows BMP palettes are padded to 32bpp. */ + if( iHeaderSize == 40 ) + read_8( f, sError ); + } + + memcpy( img->fmt.palette->colors, Palette, sizeof(Palette) ); + } + + /* Stop on error before we seek, so we don't return the wrong error message. */ + if( sError.size() != 0 ) + return RageSurfaceUtils::OPEN_FATAL_ERROR; + + int iFilePitch = iFileBPP * iWidth; // in bits + iFilePitch = (iFilePitch+7) / 8; // in bytes: round up + iFilePitch = (iFilePitch+3) & ~3; // round up a multiple of 4 + + { + int ret = f.Seek( iDataOffset ); + if( ret == -1 ) + FATAL_ERROR( f.GetError() ); + if( ret != (int) iDataOffset ) + FATAL_ERROR( "Unexpected end of file" ); + } + + for( int y = (int) iHeight-1; y >= 0; --y ) + { + uint8_t *pRow = img->pixels + img->pitch*y; + RString buf; + + f.Read( buf, iFilePitch ); + + /* Expand 1- and 4-bits to 8-bits. */ + if( iFileBPP == 1 ) + { + for( unsigned x = 0; x < iWidth; ++x ) + { + int iByteNo = x >> 3; + int iBitNo = 7-(x&7); + int iBit = 1 << iBitNo; + pRow[x] = !!(buf[iByteNo] & iBit); + } + } + else if( iFileBPP == 4 ) + { + for( unsigned x = 0; x < iWidth; ++x ) + { + if( (x & 1) == 0 ) + pRow[x] = buf[x/2] & 0x0F; + else + pRow[x] = (buf[x/2] >> 4) & 0x0F; + } + } + else + memcpy( pRow, buf.data(), img->pitch ); + } + + return sError.size() != 0? RageSurfaceUtils::OPEN_FATAL_ERROR: RageSurfaceUtils::OPEN_OK; +} + +RageSurfaceUtils::OpenResult RageSurface_Load_BMP( const RString &sPath, RageSurface *&img, bool bHeaderOnly, RString &error ) +{ + RageFile f; + + if( !f.Open( sPath ) ) + { + error = f.GetError(); + return RageSurfaceUtils::OPEN_FATAL_ERROR; + } + + RageSurfaceUtils::OpenResult ret; + img = NULL; + ret = LoadBMP( f, img, error ); + + if( ret != RageSurfaceUtils::OPEN_OK && img != nullptr ) + { + delete img; + img = NULL; + } + + return ret; +} + +/* + * Copyright (c) 2004 Glenn Maynard + * 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. + */ diff --git a/src/RageSurface_Load_PNG.cpp b/src/RageSurface_Load_PNG.cpp index a2f3b9a4ed..d5a9e3aae7 100644 --- a/src/RageSurface_Load_PNG.cpp +++ b/src/RageSurface_Load_PNG.cpp @@ -1,299 +1,299 @@ -#include "global.h" -#include "RageSurface_Load_PNG.h" -#include "RageUtil.h" -#include "RageLog.h" -#include "RageFile.h" -#include "RageSurface.h" - - -#if defined(_WINDOWS) -# include "png.h" -# if defined(_MSC_VER) -# pragma comment(lib, "libpng.lib") -# pragma warning(disable: 4611) /* interaction between '_setjmp' and C++ object destruction is non-portable */ -# endif // _MSC_VER -#else -# include <../extern/libpng/include/png.h> -#endif - -namespace -{ -void RageFile_png_read( png_struct *png, png_byte *p, png_size_t size ) -{ - CHECKPOINT; - RageFile *f = (RageFile *) png_get_io_ptr(png); - - int got = f->Read( p, size ); - if( got == -1 ) - { - /* png_error will call PNG_Error, which will longjmp. If we just pass - * GetError().c_str() to it, a temporary may be created; since control - * never returns here, it may never be destructed and we could leak. */ - static char error[256]; - strncpy( error, f->GetError(), sizeof(error) ); - error[sizeof(error)-1] = 0; - png_error( png, error ); - } - else if( got != (int) size ) - png_error( png, "Unexpected EOF" ); -} - -struct error_info -{ - char *err; - const char *fn; -}; - -void PNG_Error( png_struct *png, const char *error ) -{ - CHECKPOINT; - error_info *info = (error_info *) png_get_error_ptr(png); - strncpy( info->err, error, 1024 ); - info->err[1023] = 0; - LOG->Trace( "loading \"%s\": err: %s", info->fn, info->err ); - longjmp( png_jmpbuf(png), 1 ); -} - -void PNG_Warning( png_struct *png, const char *warning ) -{ - CHECKPOINT; - error_info *info = (error_info *) png_get_io_ptr(png); - LOG->Trace( "loading \"%s\": warning: %s", info->fn, warning ); -} - -/* Since libpng forces us to use longjmp (gross!), this function shouldn't create any C++ - * objects, and needs to watch out for memleaks. */ -static RageSurface *RageSurface_Load_PNG( RageFile *f, const char *fn, char errorbuf[1024], bool bHeaderOnly ) -{ - error_info error; - error.err = errorbuf; - error.fn = fn; - - png_struct *png = png_create_read_struct( PNG_LIBPNG_VER_STRING, &error, PNG_Error, PNG_Warning ); - - if( png == NULL ) - { - sprintf( errorbuf, "creating png_create_read_struct failed"); - return NULL; - } - - png_info *info_ptr = png_create_info_struct(png); - if( info_ptr == NULL ) - { - png_destroy_read_struct( &png, NULL, NULL ); - sprintf( errorbuf, "creating png_create_info_struct failed"); - return NULL; - } - - RageSurface *volatile img = NULL; - CHECKPOINT; - if( setjmp(png_jmpbuf(png) )) - { - png_destroy_read_struct( &png, &info_ptr, NULL ); - delete img; - return NULL; - } - CHECKPOINT; - - png_set_read_fn( png, f, RageFile_png_read ); - - png_read_info( png, info_ptr ); - - png_uint_32 width, height; - int bit_depth, color_type; - png_get_IHDR( png, info_ptr, &width, &height, &bit_depth, &color_type, NULL, NULL, NULL ); - - /* If bHeaderOnly is true, don't allocate the pixel storage space or decompress - * the image. Just return an empty surface with only the width and height set. */ - if( bHeaderOnly ) - { - CHECKPOINT; - img = CreateSurfaceFrom( width, height, 32, 0, 0, 0, 0, NULL, width*4 ); - png_destroy_read_struct( &png, &info_ptr, NULL ); - - return img; - } - - - CHECKPOINT; - png_set_strip_16(png); /* 16bit->8bit */ - png_set_packing( png ); /* 1,2,4 bit->8 bit */ - - /* Expand grayscale images to the full 8 bits from 1, 2, or 4 bits/pixel */ - if( color_type == PNG_COLOR_TYPE_GRAY && bit_depth < 8 ) - png_set_expand_gray_1_2_4_to_8( png ); - - /* These are set for type == PALETTE. */ - RageSurfaceColor colors[256]; - int iColorKey = -1; - - /* We import three types of files: paletted, RGBX and RGBA. The only difference - * between RGBX and RGBA is that RGBX won't set the alpha mask, so it's easier - * to tell later on that there's no alpha (without actually having to do a pixel scan). */ - enum { PALETTE, RGBX, RGBA } type; - switch( color_type ) - { - case PNG_COLOR_TYPE_GRAY: - /* Fake PNG_COLOR_TYPE_GRAY. */ - for( int i = 0; i < 256; ++i ) - { - colors[i].r = colors[i].g = colors[i].b = (int8_t) i; - colors[i].a = 0xFF; - } - - type = PALETTE; - break; - - case PNG_COLOR_TYPE_GRAY_ALPHA: - type = RGBA; - png_set_gray_to_rgb( png ); - break; - case PNG_COLOR_TYPE_PALETTE: - type = PALETTE; - break; - case PNG_COLOR_TYPE_RGB: - type = RGBX; - break; - case PNG_COLOR_TYPE_RGB_ALPHA: - type = RGBA; - break; - default: - FAIL_M(ssprintf( "%i", color_type) ); - } - - CHECKPOINT; - if( color_type == PNG_COLOR_TYPE_GRAY ) - { - png_color_16 *trans; - if( png_get_tRNS( png, info_ptr, NULL, NULL, &trans ) == PNG_INFO_tRNS ) - iColorKey = trans->gray; - } - else if( color_type == PNG_COLOR_TYPE_PALETTE ) - { - int num_palette; - png_color *palette; - int ret = png_get_PLTE( png, info_ptr, &palette, &num_palette ); - ASSERT( ret == PNG_INFO_PLTE ); - - png_byte *trans = NULL; - int num_trans = 0; - png_get_tRNS( png, info_ptr, &trans, &num_trans, NULL ); - - for( int i = 0; i < num_palette; ++i ) - { - colors[i].r = palette[i].red; - colors[i].g = palette[i].green; - colors[i].b = palette[i].blue; - colors[i].a = 0xFF; - if( i < num_trans ) - colors[i].a = trans[i]; - } - } - else - { - /* If we have RGB image and tRNS, it's a color key. Just convert it to RGBA. */ - if( png_get_valid(png, info_ptr, PNG_INFO_tRNS) ) - { - /* We don't care about RGB color keys; just convert them to alpha. */ - png_set_tRNS_to_alpha( png ); - type = RGBA; - } - - /* RGB->RGBX */ - png_set_filler( png, 0xff, PNG_FILLER_AFTER ); - } - - png_set_interlace_handling( png ); - - CHECKPOINT; - png_read_update_info( png, info_ptr ); - - switch( type ) - { - case PALETTE: - img = CreateSurface( width, height, 8, 0, 0, 0, 0 ); - memcpy( img->fmt.palette->colors, colors, 256*sizeof(RageSurfaceColor) ); - - if( iColorKey != -1 ) - img->format->palette->colors[ iColorKey ].a = 0; - - break; - case RGBX: - case RGBA: - img = CreateSurface( width, height, 32, - Swap32BE( 0xFF000000 ), - Swap32BE( 0x00FF0000 ), - Swap32BE( 0x0000FF00 ), - Swap32BE( type == RGBA? 0x000000FF:0x00000000 ) ); - break; - default: - FAIL_M(ssprintf( "%i", type) ); - } - ASSERT( img != NULL ); - - /* alloca to prevent memleaks if libpng longjmps us */ - png_byte **row_pointers = (png_byte **) alloca( sizeof(png_byte*) * height ); - CHECKPOINT_M( ssprintf("%p",row_pointers) ); - - for( unsigned y = 0; y < height; ++y ) - { - png_byte *p = (png_byte *) img->pixels; - row_pointers[y] = p + img->pitch*y; - } - - CHECKPOINT; - png_read_image( png, row_pointers ); - - CHECKPOINT; - png_read_end( png, info_ptr ); - png_destroy_read_struct( &png, &info_ptr, NULL ); - - return img; -} - -}; - -RageSurfaceUtils::OpenResult RageSurface_Load_PNG( const RString &sPath, RageSurface *&ret, bool bHeaderOnly, RString &error ) -{ - RageFile f; - if( !f.Open( sPath ) ) - { - error = f.GetError(); - return RageSurfaceUtils::OPEN_FATAL_ERROR; - } - - char errorbuf[1024]; - ret = RageSurface_Load_PNG( &f, sPath, errorbuf, bHeaderOnly ); - if( ret == NULL ) - { - error = errorbuf; - return RageSurfaceUtils::OPEN_UNKNOWN_FILE_FORMAT; // XXX - } - - return RageSurfaceUtils::OPEN_OK; -} - -/* - * (c) 2004 Glenn Maynard - * 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. - */ +#include "global.h" +#include "RageSurface_Load_PNG.h" +#include "RageUtil.h" +#include "RageLog.h" +#include "RageFile.h" +#include "RageSurface.h" + + +#if defined(_WINDOWS) +# include "png.h" +# if defined(_MSC_VER) +# pragma comment(lib, "libpng.lib") +# pragma warning(disable: 4611) /* interaction between '_setjmp' and C++ object destruction is non-portable */ +# endif // _MSC_VER +#else +# include <../extern/libpng/include/png.h> +#endif + +namespace +{ +void RageFile_png_read( png_struct *png, png_byte *p, png_size_t size ) +{ + CHECKPOINT; + RageFile *f = (RageFile *) png_get_io_ptr(png); + + int got = f->Read( p, size ); + if( got == -1 ) + { + /* png_error will call PNG_Error, which will longjmp. If we just pass + * GetError().c_str() to it, a temporary may be created; since control + * never returns here, it may never be destructed and we could leak. */ + static char error[256]; + strncpy( error, f->GetError(), sizeof(error) ); + error[sizeof(error)-1] = 0; + png_error( png, error ); + } + else if( got != (int) size ) + png_error( png, "Unexpected EOF" ); +} + +struct error_info +{ + char *err; + const char *fn; +}; + +void PNG_Error( png_struct *png, const char *error ) +{ + CHECKPOINT; + error_info *info = (error_info *) png_get_error_ptr(png); + strncpy( info->err, error, 1024 ); + info->err[1023] = 0; + LOG->Trace( "loading \"%s\": err: %s", info->fn, info->err ); + longjmp( png_jmpbuf(png), 1 ); +} + +void PNG_Warning( png_struct *png, const char *warning ) +{ + CHECKPOINT; + error_info *info = (error_info *) png_get_io_ptr(png); + LOG->Trace( "loading \"%s\": warning: %s", info->fn, warning ); +} + +/* Since libpng forces us to use longjmp (gross!), this function shouldn't create any C++ + * objects, and needs to watch out for memleaks. */ +static RageSurface *RageSurface_Load_PNG( RageFile *f, const char *fn, char errorbuf[1024], bool bHeaderOnly ) +{ + error_info error; + error.err = errorbuf; + error.fn = fn; + + png_struct *png = png_create_read_struct( PNG_LIBPNG_VER_STRING, &error, PNG_Error, PNG_Warning ); + + if( png == NULL ) + { + sprintf( errorbuf, "creating png_create_read_struct failed"); + return NULL; + } + + png_info *info_ptr = png_create_info_struct(png); + if( info_ptr == NULL ) + { + png_destroy_read_struct( &png, NULL, NULL ); + sprintf( errorbuf, "creating png_create_info_struct failed"); + return NULL; + } + + RageSurface *volatile img = NULL; + CHECKPOINT; + if( setjmp(png_jmpbuf(png) )) + { + png_destroy_read_struct( &png, &info_ptr, NULL ); + delete img; + return NULL; + } + CHECKPOINT; + + png_set_read_fn( png, f, RageFile_png_read ); + + png_read_info( png, info_ptr ); + + png_uint_32 width, height; + int bit_depth, color_type; + png_get_IHDR( png, info_ptr, &width, &height, &bit_depth, &color_type, NULL, NULL, NULL ); + + /* If bHeaderOnly is true, don't allocate the pixel storage space or decompress + * the image. Just return an empty surface with only the width and height set. */ + if( bHeaderOnly ) + { + CHECKPOINT; + img = CreateSurfaceFrom( width, height, 32, 0, 0, 0, 0, NULL, width*4 ); + png_destroy_read_struct( &png, &info_ptr, NULL ); + + return img; + } + + + CHECKPOINT; + png_set_strip_16(png); /* 16bit->8bit */ + png_set_packing( png ); /* 1,2,4 bit->8 bit */ + + /* Expand grayscale images to the full 8 bits from 1, 2, or 4 bits/pixel */ + if( color_type == PNG_COLOR_TYPE_GRAY && bit_depth < 8 ) + png_set_expand_gray_1_2_4_to_8( png ); + + /* These are set for type == PALETTE. */ + RageSurfaceColor colors[256]; + int iColorKey = -1; + + /* We import three types of files: paletted, RGBX and RGBA. The only difference + * between RGBX and RGBA is that RGBX won't set the alpha mask, so it's easier + * to tell later on that there's no alpha (without actually having to do a pixel scan). */ + enum { PALETTE, RGBX, RGBA } type; + switch( color_type ) + { + case PNG_COLOR_TYPE_GRAY: + /* Fake PNG_COLOR_TYPE_GRAY. */ + for( int i = 0; i < 256; ++i ) + { + colors[i].r = colors[i].g = colors[i].b = (int8_t) i; + colors[i].a = 0xFF; + } + + type = PALETTE; + break; + + case PNG_COLOR_TYPE_GRAY_ALPHA: + type = RGBA; + png_set_gray_to_rgb( png ); + break; + case PNG_COLOR_TYPE_PALETTE: + type = PALETTE; + break; + case PNG_COLOR_TYPE_RGB: + type = RGBX; + break; + case PNG_COLOR_TYPE_RGB_ALPHA: + type = RGBA; + break; + default: + FAIL_M(ssprintf( "%i", color_type) ); + } + + CHECKPOINT; + if( color_type == PNG_COLOR_TYPE_GRAY ) + { + png_color_16 *trans; + if( png_get_tRNS( png, info_ptr, NULL, NULL, &trans ) == PNG_INFO_tRNS ) + iColorKey = trans->gray; + } + else if( color_type == PNG_COLOR_TYPE_PALETTE ) + { + int num_palette; + png_color *palette; + int ret = png_get_PLTE( png, info_ptr, &palette, &num_palette ); + ASSERT( ret == PNG_INFO_PLTE ); + + png_byte *trans = NULL; + int num_trans = 0; + png_get_tRNS( png, info_ptr, &trans, &num_trans, NULL ); + + for( int i = 0; i < num_palette; ++i ) + { + colors[i].r = palette[i].red; + colors[i].g = palette[i].green; + colors[i].b = palette[i].blue; + colors[i].a = 0xFF; + if( i < num_trans ) + colors[i].a = trans[i]; + } + } + else + { + /* If we have RGB image and tRNS, it's a color key. Just convert it to RGBA. */ + if( png_get_valid(png, info_ptr, PNG_INFO_tRNS) ) + { + /* We don't care about RGB color keys; just convert them to alpha. */ + png_set_tRNS_to_alpha( png ); + type = RGBA; + } + + /* RGB->RGBX */ + png_set_filler( png, 0xff, PNG_FILLER_AFTER ); + } + + png_set_interlace_handling( png ); + + CHECKPOINT; + png_read_update_info( png, info_ptr ); + + switch( type ) + { + case PALETTE: + img = CreateSurface( width, height, 8, 0, 0, 0, 0 ); + memcpy( img->fmt.palette->colors, colors, 256*sizeof(RageSurfaceColor) ); + + if( iColorKey != -1 ) + img->format->palette->colors[ iColorKey ].a = 0; + + break; + case RGBX: + case RGBA: + img = CreateSurface( width, height, 32, + Swap32BE( 0xFF000000 ), + Swap32BE( 0x00FF0000 ), + Swap32BE( 0x0000FF00 ), + Swap32BE( type == RGBA? 0x000000FF:0x00000000 ) ); + break; + default: + FAIL_M(ssprintf( "%i", type) ); + } + ASSERT( img != nullptr ); + + /* alloca to prevent memleaks if libpng longjmps us */ + png_byte **row_pointers = (png_byte **) alloca( sizeof(png_byte*) * height ); + CHECKPOINT_M( ssprintf("%p",row_pointers) ); + + for( unsigned y = 0; y < height; ++y ) + { + png_byte *p = (png_byte *) img->pixels; + row_pointers[y] = p + img->pitch*y; + } + + CHECKPOINT; + png_read_image( png, row_pointers ); + + CHECKPOINT; + png_read_end( png, info_ptr ); + png_destroy_read_struct( &png, &info_ptr, NULL ); + + return img; +} + +}; + +RageSurfaceUtils::OpenResult RageSurface_Load_PNG( const RString &sPath, RageSurface *&ret, bool bHeaderOnly, RString &error ) +{ + RageFile f; + if( !f.Open( sPath ) ) + { + error = f.GetError(); + return RageSurfaceUtils::OPEN_FATAL_ERROR; + } + + char errorbuf[1024]; + ret = RageSurface_Load_PNG( &f, sPath, errorbuf, bHeaderOnly ); + if( ret == NULL ) + { + error = errorbuf; + return RageSurfaceUtils::OPEN_UNKNOWN_FILE_FORMAT; // XXX + } + + return RageSurfaceUtils::OPEN_OK; +} + +/* + * (c) 2004 Glenn Maynard + * 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. + */ diff --git a/src/RageTexturePreloader.cpp b/src/RageTexturePreloader.cpp index 772d5975cf..8472380b99 100644 --- a/src/RageTexturePreloader.cpp +++ b/src/RageTexturePreloader.cpp @@ -1,74 +1,74 @@ -/* - * Preemptively load textures before use, by loading it and keeping - * a reference to it. By putting a RageTexturePreloader inside the - * object doing the preloading, the preload will exist for the lifetime - * of that object. - */ - -#include "global.h" -#include "RageTexturePreloader.h" -#include "RageTextureManager.h" - -RageTexturePreloader &RageTexturePreloader::operator=( const RageTexturePreloader &rhs ) -{ - if( &rhs == this ) - return *this; - - UnloadAll(); - - for( unsigned i = 0; i < rhs.m_apTextures.size(); ++i ) - { - RageTexture *pTexture = TEXTUREMAN->CopyTexture( rhs.m_apTextures[i] ); - m_apTextures.push_back( pTexture ); - } - - return *this; -} - -void RageTexturePreloader::Load( const RageTextureID &ID ) -{ - ASSERT( TEXTUREMAN != NULL ); - - RageTexture *pTexture = TEXTUREMAN->LoadTexture( ID ); - m_apTextures.push_back( pTexture ); -} - -void RageTexturePreloader::UnloadAll() -{ - if( TEXTUREMAN == NULL ) - return; - - for( unsigned i = 0; i < m_apTextures.size(); ++i ) - TEXTUREMAN->UnloadTexture( m_apTextures[i] ); - m_apTextures.clear(); -} - -RageTexturePreloader::~RageTexturePreloader() -{ - UnloadAll(); -} - -/* - * (c) 2005 Glenn Maynard - * 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. - */ +/* + * Preemptively load textures before use, by loading it and keeping + * a reference to it. By putting a RageTexturePreloader inside the + * object doing the preloading, the preload will exist for the lifetime + * of that object. + */ + +#include "global.h" +#include "RageTexturePreloader.h" +#include "RageTextureManager.h" + +RageTexturePreloader &RageTexturePreloader::operator=( const RageTexturePreloader &rhs ) +{ + if( &rhs == this ) + return *this; + + UnloadAll(); + + for( unsigned i = 0; i < rhs.m_apTextures.size(); ++i ) + { + RageTexture *pTexture = TEXTUREMAN->CopyTexture( rhs.m_apTextures[i] ); + m_apTextures.push_back( pTexture ); + } + + return *this; +} + +void RageTexturePreloader::Load( const RageTextureID &ID ) +{ + ASSERT( TEXTUREMAN != nullptr ); + + RageTexture *pTexture = TEXTUREMAN->LoadTexture( ID ); + m_apTextures.push_back( pTexture ); +} + +void RageTexturePreloader::UnloadAll() +{ + if( TEXTUREMAN == NULL ) + return; + + for( unsigned i = 0; i < m_apTextures.size(); ++i ) + TEXTUREMAN->UnloadTexture( m_apTextures[i] ); + m_apTextures.clear(); +} + +RageTexturePreloader::~RageTexturePreloader() +{ + UnloadAll(); +} + +/* + * (c) 2005 Glenn Maynard + * 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. + */ diff --git a/src/RageThreads.cpp b/src/RageThreads.cpp index e3aeb6b946..f2e65a6567 100644 --- a/src/RageThreads.cpp +++ b/src/RageThreads.cpp @@ -94,12 +94,12 @@ void ThreadSlot::ThreadCheckpoint::Set( const char *szFile, int iLine, const cha m_szMessage = szMessage; /* Skip any path components. */ - if( m_szFile != NULL ) + if( m_szFile != nullptr ) { const char *p = strrchr( m_szFile, '/' ); if( p == NULL ) p = strrchr( m_szFile, '\\' ); - if( p != NULL && p[1] != '\0' ) + if( p != nullptr && p[1] != '\0' ) m_szFile = p+1; } @@ -215,7 +215,7 @@ RageThread::RageThread( const RageThread &cpy ): RageThread::~RageThread() { - if( m_pSlot != NULL ) + if( m_pSlot != nullptr ) Wait(); } @@ -310,8 +310,8 @@ bool RageThread::EnumThreadIDs( int n, uint64_t &iID ) int RageThread::Wait() { - ASSERT( m_pSlot != NULL ); - ASSERT( m_pSlot->m_pImpl != NULL ); + ASSERT( m_pSlot != nullptr ); + ASSERT( m_pSlot->m_pImpl != nullptr ); int ret = m_pSlot->m_pImpl->Wait(); LockMut( GetThreadSlotsLock() ); @@ -323,14 +323,14 @@ int RageThread::Wait() } void RageThread::Halt(bool Kill) { - ASSERT( m_pSlot != NULL ); - ASSERT( m_pSlot->m_pImpl != NULL ); + ASSERT( m_pSlot != nullptr ); + ASSERT( m_pSlot->m_pImpl != nullptr ); m_pSlot->m_pImpl->Halt(Kill); } void RageThread::Resume() { - ASSERT( m_pSlot != NULL ); - ASSERT( m_pSlot->m_pImpl != NULL ); + ASSERT( m_pSlot != nullptr ); + ASSERT( m_pSlot->m_pImpl != nullptr ); m_pSlot->m_pImpl->Resume(); } @@ -432,7 +432,7 @@ void Checkpoints::GetLogs( char *pBuf, int iSize, const char *delim ) strcat( pBuf, buf ); strcat( pBuf, delim ); - for( int line = 1; (buf = GetCheckpointLog(slotno, line)) != NULL; ++line ) + for( int line = 1; (buf = GetCheckpointLog(slotno, line)) != nullptr; ++line ) { strcat( pBuf, buf ); strcat( pBuf, delim ); @@ -704,7 +704,7 @@ bool RageEvent::Wait( RageTimer *pTimeout ) ASSERT( m_LockCnt == 0 ); /* A zero RageTimer also means no timeout. */ - if( pTimeout != NULL && pTimeout->IsZero() ) + if( pTimeout != nullptr && pTimeout->IsZero() ) pTimeout = NULL; bool bRet = m_pEvent->Wait( pTimeout ); diff --git a/src/RageThreads.h b/src/RageThreads.h index 1fc7c7f49f..1f32779c0e 100644 --- a/src/RageThreads.h +++ b/src/RageThreads.h @@ -1,225 +1,225 @@ -#ifndef RAGE_THREADS_H -#define RAGE_THREADS_H - -struct ThreadSlot; -class RageTimer; -/** @brief Thread, mutex, semaphore, and event classes. */ -class RageThread -{ -public: - RageThread(); - RageThread( const RageThread &cpy ); - ~RageThread(); - - void SetName( const RString &n ) { m_sName = n; } - RString GetName() const { return m_sName; } - void Create( int (*fn)(void *), void *data ); - - void Halt( bool Kill=false); - void Resume(); - - /* For crash handlers: kill or suspend all threads (except for - * the running one) immediately. */ - static void HaltAllThreads( bool Kill=false ); - - /* If HaltAllThreads was called (with Kill==false), resume. */ - static void ResumeAllThreads(); - - static uint64_t GetCurrentThreadID(); - - static const char *GetCurrentThreadName(); - static const char *GetThreadNameByID( uint64_t iID ); - static bool EnumThreadIDs( int n, uint64_t &iID ); - int Wait(); - bool IsCreated() const { return m_pSlot != NULL; } - - /* A system can define HAVE_TLS, indicating that it can compile thread_local - * code, but an individual environment may not actually have functional TLS. - * If this returns false, thread_local variables are considered undefined. */ - static bool GetSupportsTLS() { return s_bSystemSupportsTLS; } - static void SetSupportsTLS( bool b ) { s_bSystemSupportsTLS = b; } - - static bool GetIsShowingDialog() { return s_bIsShowingDialog; } - static void SetIsShowingDialog( bool b ) { s_bIsShowingDialog = b; } - static uint64_t GetInvalidThreadID(); - -private: - ThreadSlot *m_pSlot; - RString m_sName; - - static bool s_bSystemSupportsTLS; - static bool s_bIsShowingDialog; - - // Swallow up warnings. If they must be used, define them. - RageThread& operator=(const RageThread& rhs); -}; - -/** - * @brief Register a thread created outside of RageThread. - * - * This gives it a name for RageThread::GetCurrentThreadName, - * and allocates a slot for checkpoints. */ -class RageThreadRegister -{ -public: - RageThreadRegister( const RString &sName ); - ~RageThreadRegister(); - -private: - ThreadSlot *m_pSlot; - // Swallow up warnings. If they must be used, define them. - RageThreadRegister& operator=(const RageThreadRegister& rhs); - RageThreadRegister(const RageThreadRegister& rhs); -}; - -namespace Checkpoints -{ - void LogCheckpoints( bool yes=true ); - void SetCheckpoint( const char *file, int line, const char *message ); - void GetLogs( char *pBuf, int iSize, const char *delim ); -}; - -#define CHECKPOINT (Checkpoints::SetCheckpoint(__FILE__, __LINE__, NULL)) -#define CHECKPOINT_M(m) (Checkpoints::SetCheckpoint(__FILE__, __LINE__, m)) - -/* Mutex class that follows the behavior of Windows mutexes: if the same - * thread locks the same mutex twice, we just increase a refcount; a mutex - * is considered unlocked when the refcount reaches zero. This is more - * convenient, though much slower on some archs. (We don't have any tightly- - * coupled threads, so that's OK.) */ -class MutexImpl; -class RageMutex -{ -public: - RString GetName() const { return m_sName; } - void SetName( const RString &s ) { m_sName = s; } - virtual void Lock(); - virtual bool TryLock(); - virtual void Unlock(); - virtual bool IsLockedByThisThread() const; - - RageMutex( const RString &name ); - virtual ~RageMutex(); - -protected: - MutexImpl *m_pMutex; - RString m_sName; - - int m_UniqueID; - - uint64_t m_LockedBy; - int m_LockCnt; - - void MarkLockedMutex(); -private: - // Swallow up warnings. If they must be used, define them. - RageMutex& operator=(const RageMutex& rhs); - RageMutex(const RageMutex& rhs); -}; - -/** - * @brief Lock a mutex on construction, unlock it on destruction. - * - * Helps for functions with more than one return path. */ -class LockMutex -{ - RageMutex &mutex; - - const char *file; - int line; - float locked_at; - bool locked; - -public: - LockMutex(RageMutex &mut, const char *file, int line); - LockMutex(RageMutex &mut): mutex(mut), file(NULL), line(-1), locked_at(-1), locked(true) { mutex.Lock(); } - ~LockMutex(); - LockMutex(LockMutex &cpy): mutex(cpy.mutex), file(NULL), line(-1), locked_at(cpy.locked_at), locked(true) { mutex.Lock(); } - - /** - * @brief Unlock the mutex (before this would normally go out of scope). - * - * This can only be called once. */ - void Unlock(); -private: - // Swallow up warnings. If they must be used, define them. - LockMutex& operator=(const LockMutex& rhs); -}; - -#define LockMut(m) LockMutex UNIQUE_NAME(LocalLock) (m, __FILE__, __LINE__) - -class EventImpl; -class RageEvent: public RageMutex -{ -public: - RageEvent( RString name ); - ~RageEvent(); - - /* - * If pTimeout is non-NULL, the event will be automatically signalled at the given - * time. Note that implementing this timeout is optional; not all archs support it. - * If false is returned, the wait timed out (and the mutex is locked, as if the - * event had been signalled). - */ - bool Wait( RageTimer *pTimeout = NULL ); - void Signal(); - void Broadcast(); - bool WaitTimeoutSupported() const; - // Swallow up warnings. If they must be used, define them. - RageEvent& operator=(const RageEvent& rhs); - RageEvent(const RageEvent& rhs); - -private: - EventImpl *m_pEvent; -}; - -class SemaImpl; -class RageSemaphore -{ -public: - RageSemaphore( RString sName, int iInitialValue = 0 ); - ~RageSemaphore(); - - RString GetName() const { return m_sName; } - int GetValue() const; - void Post(); - void Wait( bool bFailOnTimeout=true ); - bool TryWait(); - -private: - SemaImpl *m_pSema; - RString m_sName; - - // Swallow up warnings. If they must be used, define them. - RageSemaphore& operator=(const RageSemaphore& rhs); - RageSemaphore(const RageSemaphore& rhs); -}; - -#endif - -/** - * @file - * @author Glenn Maynard (c) 2001-2004 - * @section LICENSE - * 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. - */ +#ifndef RAGE_THREADS_H +#define RAGE_THREADS_H + +struct ThreadSlot; +class RageTimer; +/** @brief Thread, mutex, semaphore, and event classes. */ +class RageThread +{ +public: + RageThread(); + RageThread( const RageThread &cpy ); + ~RageThread(); + + void SetName( const RString &n ) { m_sName = n; } + RString GetName() const { return m_sName; } + void Create( int (*fn)(void *), void *data ); + + void Halt( bool Kill=false); + void Resume(); + + /* For crash handlers: kill or suspend all threads (except for + * the running one) immediately. */ + static void HaltAllThreads( bool Kill=false ); + + /* If HaltAllThreads was called (with Kill==false), resume. */ + static void ResumeAllThreads(); + + static uint64_t GetCurrentThreadID(); + + static const char *GetCurrentThreadName(); + static const char *GetThreadNameByID( uint64_t iID ); + static bool EnumThreadIDs( int n, uint64_t &iID ); + int Wait(); + bool IsCreated() const { return m_pSlot != nullptr; } + + /* A system can define HAVE_TLS, indicating that it can compile thread_local + * code, but an individual environment may not actually have functional TLS. + * If this returns false, thread_local variables are considered undefined. */ + static bool GetSupportsTLS() { return s_bSystemSupportsTLS; } + static void SetSupportsTLS( bool b ) { s_bSystemSupportsTLS = b; } + + static bool GetIsShowingDialog() { return s_bIsShowingDialog; } + static void SetIsShowingDialog( bool b ) { s_bIsShowingDialog = b; } + static uint64_t GetInvalidThreadID(); + +private: + ThreadSlot *m_pSlot; + RString m_sName; + + static bool s_bSystemSupportsTLS; + static bool s_bIsShowingDialog; + + // Swallow up warnings. If they must be used, define them. + RageThread& operator=(const RageThread& rhs); +}; + +/** + * @brief Register a thread created outside of RageThread. + * + * This gives it a name for RageThread::GetCurrentThreadName, + * and allocates a slot for checkpoints. */ +class RageThreadRegister +{ +public: + RageThreadRegister( const RString &sName ); + ~RageThreadRegister(); + +private: + ThreadSlot *m_pSlot; + // Swallow up warnings. If they must be used, define them. + RageThreadRegister& operator=(const RageThreadRegister& rhs); + RageThreadRegister(const RageThreadRegister& rhs); +}; + +namespace Checkpoints +{ + void LogCheckpoints( bool yes=true ); + void SetCheckpoint( const char *file, int line, const char *message ); + void GetLogs( char *pBuf, int iSize, const char *delim ); +}; + +#define CHECKPOINT (Checkpoints::SetCheckpoint(__FILE__, __LINE__, NULL)) +#define CHECKPOINT_M(m) (Checkpoints::SetCheckpoint(__FILE__, __LINE__, m)) + +/* Mutex class that follows the behavior of Windows mutexes: if the same + * thread locks the same mutex twice, we just increase a refcount; a mutex + * is considered unlocked when the refcount reaches zero. This is more + * convenient, though much slower on some archs. (We don't have any tightly- + * coupled threads, so that's OK.) */ +class MutexImpl; +class RageMutex +{ +public: + RString GetName() const { return m_sName; } + void SetName( const RString &s ) { m_sName = s; } + virtual void Lock(); + virtual bool TryLock(); + virtual void Unlock(); + virtual bool IsLockedByThisThread() const; + + RageMutex( const RString &name ); + virtual ~RageMutex(); + +protected: + MutexImpl *m_pMutex; + RString m_sName; + + int m_UniqueID; + + uint64_t m_LockedBy; + int m_LockCnt; + + void MarkLockedMutex(); +private: + // Swallow up warnings. If they must be used, define them. + RageMutex& operator=(const RageMutex& rhs); + RageMutex(const RageMutex& rhs); +}; + +/** + * @brief Lock a mutex on construction, unlock it on destruction. + * + * Helps for functions with more than one return path. */ +class LockMutex +{ + RageMutex &mutex; + + const char *file; + int line; + float locked_at; + bool locked; + +public: + LockMutex(RageMutex &mut, const char *file, int line); + LockMutex(RageMutex &mut): mutex(mut), file(NULL), line(-1), locked_at(-1), locked(true) { mutex.Lock(); } + ~LockMutex(); + LockMutex(LockMutex &cpy): mutex(cpy.mutex), file(NULL), line(-1), locked_at(cpy.locked_at), locked(true) { mutex.Lock(); } + + /** + * @brief Unlock the mutex (before this would normally go out of scope). + * + * This can only be called once. */ + void Unlock(); +private: + // Swallow up warnings. If they must be used, define them. + LockMutex& operator=(const LockMutex& rhs); +}; + +#define LockMut(m) LockMutex UNIQUE_NAME(LocalLock) (m, __FILE__, __LINE__) + +class EventImpl; +class RageEvent: public RageMutex +{ +public: + RageEvent( RString name ); + ~RageEvent(); + + /* + * If pTimeout is non-NULL, the event will be automatically signalled at the given + * time. Note that implementing this timeout is optional; not all archs support it. + * If false is returned, the wait timed out (and the mutex is locked, as if the + * event had been signalled). + */ + bool Wait( RageTimer *pTimeout = NULL ); + void Signal(); + void Broadcast(); + bool WaitTimeoutSupported() const; + // Swallow up warnings. If they must be used, define them. + RageEvent& operator=(const RageEvent& rhs); + RageEvent(const RageEvent& rhs); + +private: + EventImpl *m_pEvent; +}; + +class SemaImpl; +class RageSemaphore +{ +public: + RageSemaphore( RString sName, int iInitialValue = 0 ); + ~RageSemaphore(); + + RString GetName() const { return m_sName; } + int GetValue() const; + void Post(); + void Wait( bool bFailOnTimeout=true ); + bool TryWait(); + +private: + SemaImpl *m_pSema; + RString m_sName; + + // Swallow up warnings. If they must be used, define them. + RageSemaphore& operator=(const RageSemaphore& rhs); + RageSemaphore(const RageSemaphore& rhs); +}; + +#endif + +/** + * @file + * @author Glenn Maynard (c) 2001-2004 + * @section LICENSE + * 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. + */ diff --git a/src/RageUtil.cpp b/src/RageUtil.cpp index 551529c5eb..6be59ebd13 100644 --- a/src/RageUtil.cpp +++ b/src/RageUtil.cpp @@ -1027,7 +1027,7 @@ bool GetCommandlineArgument( const RString &option, RString *argument, int iInde RString GetCwd() { char buf[PATH_MAX]; - bool ret = getcwd(buf, PATH_MAX) != NULL; + bool ret = getcwd(buf, PATH_MAX) != nullptr; ASSERT(ret); return buf; } @@ -2274,7 +2274,7 @@ bool FileCopy( RageFileBasic &in, RageFileBasic &out, RString &sError, bool *bRe if( in.Read(data, 1024*32) == -1 ) { sError = ssprintf( "read error: %s", in.GetError().c_str() ); - if( bReadError != NULL ) + if( bReadError != nullptr ) *bReadError = true; return false; } @@ -2284,7 +2284,7 @@ bool FileCopy( RageFileBasic &in, RageFileBasic &out, RString &sError, bool *bRe if( i == -1 ) { sError = ssprintf( "write error: %s", out.GetError().c_str() ); - if( bReadError != NULL ) + if( bReadError != nullptr ) *bReadError = false; return false; } @@ -2293,7 +2293,7 @@ bool FileCopy( RageFileBasic &in, RageFileBasic &out, RString &sError, bool *bRe if( out.Flush() == -1 ) { sError = ssprintf( "write error: %s", out.GetError().c_str() ); - if( bReadError != NULL ) + if( bReadError != nullptr ) *bReadError = false; return false; } diff --git a/src/RageUtil_AutoPtr.h b/src/RageUtil_AutoPtr.h index 1c009dcbb3..06113b54f8 100644 --- a/src/RageUtil_AutoPtr.h +++ b/src/RageUtil_AutoPtr.h @@ -1,219 +1,219 @@ -/* AutoPtrCopyOnWrite - Simple smart pointer template. */ - -#ifndef RAGE_UTIL_AUTO_PTR_H -#define RAGE_UTIL_AUTO_PTR_H - -/* - * This is a simple copy-on-write refcounted smart pointer. Once constructed, all read-only - * access to the object is made without extra copying. If you need read-write access, you - * can get a pointer with Get(), which will cause the object to deep-copy. (Don't free - * the resulting pointer.) - * - * Note that there are no non-const operator* or operator-> overloads, because that would - * cause all const access by code with non-const permissions to deep-copy. For example, - * - * AutoPtrCopyOnWrite a( new int(1) ); - * AutoPtrCopyOnWrite b( a ); - * printf( "%i\n", *a ); - * - * If we have a non-const operator*, this *a will use it (even though it only needs const - * access), and will copy the underlying object wastefully. g++ std::string has this behavior, - * which is why it's important to qualify strings as "const" when const access is desired, - * but that's brittle, so let's make all potential deep-copying explicit. - */ - -template -class AutoPtrCopyOnWrite -{ -public: - /* This constructor only exists to make us work with STL containers. */ - inline AutoPtrCopyOnWrite(): m_pPtr(NULL), m_iRefCount(new int(1)) - { - } - - explicit inline AutoPtrCopyOnWrite( T *p ): m_pPtr(p), m_iRefCount(new int(1)) - { - } - - inline AutoPtrCopyOnWrite( const AutoPtrCopyOnWrite &rhs ): - m_pPtr(rhs.m_pPtr), m_iRefCount(rhs.m_iRefCount) - { - ++(*m_iRefCount); - } - - void Swap( AutoPtrCopyOnWrite &rhs ) - { - swap( m_pPtr, rhs.m_pPtr ); - swap( m_iRefCount, rhs.m_iRefCount ); - } - - inline AutoPtrCopyOnWrite &operator=( const AutoPtrCopyOnWrite &rhs ) - { - AutoPtrCopyOnWrite obj( rhs ); - this->Swap( obj ); - return *this; - } - - ~AutoPtrCopyOnWrite() - { - --(*m_iRefCount); - if( *m_iRefCount == 0 ) - { - delete m_pPtr; - delete m_iRefCount; - } - } - - /* Get a non-const pointer. This will deep-copy the object if necessary. */ - T *Get() - { - if( *m_iRefCount > 1 ) - { - --*m_iRefCount; - m_pPtr = new T(*m_pPtr); - m_iRefCount = new int(1); - } - - return m_pPtr; - } - - int GetReferenceCount() const { return *m_iRefCount; } - - const T &operator *() const { return *m_pPtr; } - const T *operator ->() const { return m_pPtr; } - -private: - T *m_pPtr; - int *m_iRefCount; -}; - -template -inline void swap( AutoPtrCopyOnWrite &a, AutoPtrCopyOnWrite &b ) -{ - a.Swap(b); -} - -/* - * This smart pointer template is used to safely hide implementations from - * headers, to reduce dependencies. This is the same as declaring a pointer - * to a class, and allocating/deallocating it in the implementation: only - * the implementation needs to include that class. This makes copying - * and deletion automatic, so you don't need to include a copy ctor or - * remember to delete it. - * - * There's one subtlety: in order to copy or delete an object, we need its - * definition. This is intended to avoid pulling in the definition. So, - * we use a traits class to hide it. Use REGISTER_CLASS_TRAITS for each - * class used with this template. - * - * Concepts from http://www.gotw.ca/gotw/062.htm. - */ -template -struct HiddenPtrTraits -{ - static T *Copy( const T *pCopy ); - static void Delete( T *p ); -}; -#define REGISTER_CLASS_TRAITS(T, CopyExpr) \ - template<> T *HiddenPtrTraits::Copy( const T *pCopy ) { return CopyExpr; } \ - template<> void HiddenPtrTraits::Delete( T *p ) { delete p; } - -template -class HiddenPtr -{ -public: - const T& operator*() const { return *m_pPtr; } - const T* operator->() const { return m_pPtr; } - T& operator*() { return *m_pPtr; } - T* operator->() { return m_pPtr; } - - explicit HiddenPtr( T *p = NULL ): m_pPtr(p) {} - - HiddenPtr( const HiddenPtr &cpy ): m_pPtr(NULL) - { - if( cpy.m_pPtr != NULL ) - m_pPtr = HiddenPtrTraits::Copy( cpy.m_pPtr ); - } - -#if 0 // broken VC6 - template - HiddenPtr( const HiddenPtr &cpy ) - { - if( cpy.m_pPtr == NULL ) - m_pPtr = NULL; - else - m_pPtr = HiddenPtrTraits::Copy( cpy.m_pPtr ); - } -#endif - - ~HiddenPtr() - { - HiddenPtrTraits::Delete( m_pPtr ); - } - void Swap( HiddenPtr &rhs ) { swap( m_pPtr, rhs.m_pPtr ); } - - HiddenPtr &operator=( T *p ) - { - HiddenPtr t( p ); - Swap( t ); - return *this; - } - - HiddenPtr &operator=( const HiddenPtr &cpy ) - { - HiddenPtr t( cpy ); - Swap( t ); - return *this; - } - -#if 0 // broken VC6 - template - HiddenPtr &operator=( const HiddenPtr &cpy ) - { - HiddenPtr t( cpy ); - Swap( t ); - return *this; - } -#endif - -private: - T *m_pPtr; - -#if 0 // broken VC6 - template - friend class HiddenPtr; -#endif -}; - -template -inline void swap( HiddenPtr &a, HiddenPtr &b ) -{ - a.Swap(b); -} - -#endif - -/* - * (c) 2005 Glenn Maynard - * 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. - */ +/* AutoPtrCopyOnWrite - Simple smart pointer template. */ + +#ifndef RAGE_UTIL_AUTO_PTR_H +#define RAGE_UTIL_AUTO_PTR_H + +/* + * This is a simple copy-on-write refcounted smart pointer. Once constructed, all read-only + * access to the object is made without extra copying. If you need read-write access, you + * can get a pointer with Get(), which will cause the object to deep-copy. (Don't free + * the resulting pointer.) + * + * Note that there are no non-const operator* or operator-> overloads, because that would + * cause all const access by code with non-const permissions to deep-copy. For example, + * + * AutoPtrCopyOnWrite a( new int(1) ); + * AutoPtrCopyOnWrite b( a ); + * printf( "%i\n", *a ); + * + * If we have a non-const operator*, this *a will use it (even though it only needs const + * access), and will copy the underlying object wastefully. g++ std::string has this behavior, + * which is why it's important to qualify strings as "const" when const access is desired, + * but that's brittle, so let's make all potential deep-copying explicit. + */ + +template +class AutoPtrCopyOnWrite +{ +public: + /* This constructor only exists to make us work with STL containers. */ + inline AutoPtrCopyOnWrite(): m_pPtr(NULL), m_iRefCount(new int(1)) + { + } + + explicit inline AutoPtrCopyOnWrite( T *p ): m_pPtr(p), m_iRefCount(new int(1)) + { + } + + inline AutoPtrCopyOnWrite( const AutoPtrCopyOnWrite &rhs ): + m_pPtr(rhs.m_pPtr), m_iRefCount(rhs.m_iRefCount) + { + ++(*m_iRefCount); + } + + void Swap( AutoPtrCopyOnWrite &rhs ) + { + swap( m_pPtr, rhs.m_pPtr ); + swap( m_iRefCount, rhs.m_iRefCount ); + } + + inline AutoPtrCopyOnWrite &operator=( const AutoPtrCopyOnWrite &rhs ) + { + AutoPtrCopyOnWrite obj( rhs ); + this->Swap( obj ); + return *this; + } + + ~AutoPtrCopyOnWrite() + { + --(*m_iRefCount); + if( *m_iRefCount == 0 ) + { + delete m_pPtr; + delete m_iRefCount; + } + } + + /* Get a non-const pointer. This will deep-copy the object if necessary. */ + T *Get() + { + if( *m_iRefCount > 1 ) + { + --*m_iRefCount; + m_pPtr = new T(*m_pPtr); + m_iRefCount = new int(1); + } + + return m_pPtr; + } + + int GetReferenceCount() const { return *m_iRefCount; } + + const T &operator *() const { return *m_pPtr; } + const T *operator ->() const { return m_pPtr; } + +private: + T *m_pPtr; + int *m_iRefCount; +}; + +template +inline void swap( AutoPtrCopyOnWrite &a, AutoPtrCopyOnWrite &b ) +{ + a.Swap(b); +} + +/* + * This smart pointer template is used to safely hide implementations from + * headers, to reduce dependencies. This is the same as declaring a pointer + * to a class, and allocating/deallocating it in the implementation: only + * the implementation needs to include that class. This makes copying + * and deletion automatic, so you don't need to include a copy ctor or + * remember to delete it. + * + * There's one subtlety: in order to copy or delete an object, we need its + * definition. This is intended to avoid pulling in the definition. So, + * we use a traits class to hide it. Use REGISTER_CLASS_TRAITS for each + * class used with this template. + * + * Concepts from http://www.gotw.ca/gotw/062.htm. + */ +template +struct HiddenPtrTraits +{ + static T *Copy( const T *pCopy ); + static void Delete( T *p ); +}; +#define REGISTER_CLASS_TRAITS(T, CopyExpr) \ + template<> T *HiddenPtrTraits::Copy( const T *pCopy ) { return CopyExpr; } \ + template<> void HiddenPtrTraits::Delete( T *p ) { delete p; } + +template +class HiddenPtr +{ +public: + const T& operator*() const { return *m_pPtr; } + const T* operator->() const { return m_pPtr; } + T& operator*() { return *m_pPtr; } + T* operator->() { return m_pPtr; } + + explicit HiddenPtr( T *p = NULL ): m_pPtr(p) {} + + HiddenPtr( const HiddenPtr &cpy ): m_pPtr(NULL) + { + if( cpy.m_pPtr != nullptr ) + m_pPtr = HiddenPtrTraits::Copy( cpy.m_pPtr ); + } + +#if 0 // broken VC6 + template + HiddenPtr( const HiddenPtr &cpy ) + { + if( cpy.m_pPtr == NULL ) + m_pPtr = NULL; + else + m_pPtr = HiddenPtrTraits::Copy( cpy.m_pPtr ); + } +#endif + + ~HiddenPtr() + { + HiddenPtrTraits::Delete( m_pPtr ); + } + void Swap( HiddenPtr &rhs ) { swap( m_pPtr, rhs.m_pPtr ); } + + HiddenPtr &operator=( T *p ) + { + HiddenPtr t( p ); + Swap( t ); + return *this; + } + + HiddenPtr &operator=( const HiddenPtr &cpy ) + { + HiddenPtr t( cpy ); + Swap( t ); + return *this; + } + +#if 0 // broken VC6 + template + HiddenPtr &operator=( const HiddenPtr &cpy ) + { + HiddenPtr t( cpy ); + Swap( t ); + return *this; + } +#endif + +private: + T *m_pPtr; + +#if 0 // broken VC6 + template + friend class HiddenPtr; +#endif +}; + +template +inline void swap( HiddenPtr &a, HiddenPtr &b ) +{ + a.Swap(b); +} + +#endif + +/* + * (c) 2005 Glenn Maynard + * 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. + */ diff --git a/src/RageUtil_CachedObject.h b/src/RageUtil_CachedObject.h index 1fee1a6397..84488c2864 100644 --- a/src/RageUtil_CachedObject.h +++ b/src/RageUtil_CachedObject.h @@ -31,7 +31,7 @@ public: ~CachedObject() { - if( m_pObject != NULL ) + if( m_pObject != nullptr ) ClearCacheSpecific( m_pObject ); } @@ -145,7 +145,7 @@ public: CachedObjectHelpers::Lock(); m_pCache = p; m_bCacheIsSet = true; - if( p != NULL ) + if( p != nullptr ) p->m_CachedObject.m_pObject = p; CachedObjectHelpers::Unlock(); } diff --git a/src/RageUtil_FileDB.cpp b/src/RageUtil_FileDB.cpp index 4997fe560a..ccfe50a0bf 100644 --- a/src/RageUtil_FileDB.cpp +++ b/src/RageUtil_FileDB.cpp @@ -1,648 +1,648 @@ -#include "global.h" - -#include "RageUtil_FileDB.h" -#include "RageUtil.h" -#include "RageLog.h" - -/* Search for "beginning*containing*ending". */ -void FileSet::GetFilesMatching( const RString &sBeginning_, const RString &sContaining_, const RString &sEnding_, vector &asOut, bool bOnlyDirs ) const -{ - /* "files" is a case-insensitive mapping, by filename. Use lower_bound to figure - * out where to start. */ - RString sBeginning = sBeginning_; - sBeginning.MakeLower(); - RString sContaining = sContaining_; - sContaining.MakeLower(); - RString sEnding = sEnding_; - sEnding.MakeLower(); - - set::const_iterator i = files.lower_bound( File(sBeginning) ); - for( ; i != files.end(); ++i ) - { - const File &f = *i; - - if( bOnlyDirs && !f.dir ) - continue; - - const RString &sPath = f.lname; - - /* Check sBeginning. Once we hit a filename that no longer matches sBeginning, - * we're past all possible matches in the sort, so stop. */ - if( sBeginning.size() > sPath.size() ) - break; /* can't start with it */ - if( sPath.compare(0, sBeginning.size(), sBeginning) ) - break; /* doesn't start with it */ - - /* Position the end starts on: */ - int end_pos = int(sPath.size())-int(sEnding.size()); - - /* Check end. */ - if( end_pos < 0 ) - continue; /* can't end with it */ - if( sPath.compare(end_pos, string::npos, sEnding) ) - continue; /* doesn't end with it */ - - /* Check sContaining. Do this last, since it's the slowest (substring - * search instead of string match). */ - if( !sContaining.empty() ) - { - size_t pos = sPath.find( sContaining, sBeginning.size() ); - if( pos == sPath.npos ) - continue; /* doesn't contain it */ - if( pos + sContaining.size() > unsigned(end_pos) ) - continue; /* found it but it overlaps with the end */ - } - - asOut.push_back( f.name ); - } -} - -void FileSet::GetFilesEqualTo( const RString &sStr, vector &asOut, bool bOnlyDirs ) const -{ - set::const_iterator i = files.find( File(sStr) ); - if( i == files.end() ) - return; - - if( bOnlyDirs && !i->dir ) - return; - - asOut.push_back( i->name ); -} - -RageFileManager::FileType FileSet::GetFileType( const RString &sPath ) const -{ - set::const_iterator i = files.find( File(sPath) ); - if( i == files.end() ) - return RageFileManager::TYPE_NONE; - - return i->dir? RageFileManager::TYPE_DIR:RageFileManager::TYPE_FILE; -} - -int FileSet::GetFileSize( const RString &sPath ) const -{ - set::const_iterator i = files.find( File(sPath) ); - if( i == files.end() ) - return -1; - return i->size; -} - -int FileSet::GetFileHash( const RString &sPath ) const -{ - set::const_iterator i = files.find( File(sPath) ); - if( i == files.end() ) - return -1; - return i->hash + i->size; -} - -/* - * Given "foo/bar/baz/" or "foo/bar/baz", return "foo/bar/" and "baz". - * "foo" -> "", "foo" - */ -static void SplitPath( RString sPath, RString &sDir, RString &sName ) -{ - CollapsePath( sPath ); - if( sPath.Right(1) == "/" ) - sPath.erase( sPath.size()-1 ); - - size_t iSep = sPath.find_last_of( '/' ); - if( iSep == RString::npos ) - { - sDir = ""; - sName = sPath; - } - else - { - sDir = sPath.substr( 0, iSep+1 ); - sName = sPath.substr( iSep+1 ); - } -} - - -RageFileManager::FileType FilenameDB::GetFileType( const RString &sPath ) -{ - ASSERT( !m_Mutex.IsLockedByThisThread() ); - - RString sDir, sName; - SplitPath( sPath, sDir, sName ); - - if( sName == "/" ) - return RageFileManager::TYPE_DIR; - - const FileSet *fs = GetFileSet( sDir ); - RageFileManager::FileType ret = fs->GetFileType( sName ); - m_Mutex.Unlock(); /* locked by GetFileSet */ - return ret; -} - - -int FilenameDB::GetFileSize( const RString &sPath ) -{ - ASSERT( !m_Mutex.IsLockedByThisThread() ); - - RString sDir, sName; - SplitPath( sPath, sDir, sName ); - - const FileSet *fs = GetFileSet( sDir ); - int ret = fs->GetFileSize( sName ); - m_Mutex.Unlock(); /* locked by GetFileSet */ - return ret; -} - -int FilenameDB::GetFileHash( const RString &sPath ) -{ - ASSERT( !m_Mutex.IsLockedByThisThread() ); - - RString sDir, sName; - SplitPath( sPath, sDir, sName ); - - const FileSet *fs = GetFileSet( sDir ); - int ret = fs->GetFileHash( sName ); - m_Mutex.Unlock(); /* locked by GetFileSet */ - return ret; -} - -/* path should be fully collapsed, so we can operate in-place: no . or .. */ -bool FilenameDB::ResolvePath( RString &sPath ) -{ - if( sPath == "/" || sPath == "" ) - return true; - - /* Split path into components. */ - int iBegin = 0, iSize = -1; - - /* Resolve each component. */ - RString ret = ""; - const FileSet *fs = NULL; - - static const RString slash("/"); - for(;;) - { - split( sPath, slash, iBegin, iSize, true ); - if( iBegin == (int) sPath.size() ) - break; - - if( fs == NULL ) - fs = GetFileSet( ret ); - else - m_Mutex.Lock(); /* for access to fs */ - - RString p = sPath.substr( iBegin, iSize ); - ASSERT_M( p.size() != 1 || p[0] != '.', sPath ); // no . - ASSERT_M( p.size() != 2 || p[0] != '.' || p[1] != '.', sPath ); // no .. - set::const_iterator it = fs->files.find( File(p) ); - - /* If there were no matches, the path isn't found. */ - if( it == fs->files.end() ) - { - m_Mutex.Unlock(); /* locked by GetFileSet */ - return false; - } - - ret += "/" + it->name; - - fs = it->dirp; - - m_Mutex.Unlock(); /* locked by GetFileSet */ - } - - if( sPath.size() && sPath[sPath.size()-1] == '/' ) - sPath = ret + "/"; - else - sPath = ret; - return true; -} - -void FilenameDB::GetFilesMatching( const RString &sDir, const RString &sBeginning, const RString &sContaining, const RString &sEnding, vector &asOut, bool bOnlyDirs ) -{ - ASSERT( !m_Mutex.IsLockedByThisThread() ); - - const FileSet *fs = GetFileSet( sDir ); - fs->GetFilesMatching( sBeginning, sContaining, sEnding, asOut, bOnlyDirs ); - m_Mutex.Unlock(); /* locked by GetFileSet */ -} - -void FilenameDB::GetFilesEqualTo( const RString &sDir, const RString &sFile, vector &asOut, bool bOnlyDirs ) -{ - ASSERT( !m_Mutex.IsLockedByThisThread() ); - - const FileSet *fs = GetFileSet( sDir ); - fs->GetFilesEqualTo( sFile, asOut, bOnlyDirs ); - m_Mutex.Unlock(); /* locked by GetFileSet */ -} - - -void FilenameDB::GetFilesSimpleMatch( const RString &sDir, const RString &sMask, vector &asOut, bool bOnlyDirs ) -{ - /* Does this contain a wildcard? */ - size_t first_pos = sMask.find_first_of( '*' ); - if( first_pos == sMask.npos ) - { - /* No; just do a regular search. */ - GetFilesEqualTo( sDir, sMask, asOut, bOnlyDirs ); - return; - } - size_t second_pos = sMask.find_first_of( '*', first_pos+1 ); - if( second_pos == sMask.npos ) - { - /* Only one *: "A*B". */ - /* XXX: "_blank.png*.png" shouldn't match the file "_blank.png". */ - GetFilesMatching( sDir, sMask.substr(0, first_pos), RString(), sMask.substr(first_pos+1), asOut, bOnlyDirs ); - return; - } - - /* Two *s: "A*B*C". */ - GetFilesMatching( sDir, - sMask.substr(0, first_pos), - sMask.substr(first_pos+1, second_pos-first_pos-1), - sMask.substr(second_pos+1), asOut, bOnlyDirs ); -} - -/* - * Get the FileSet for dir; if create is true, create the FileSet if necessary. - * - * We want to unlock the object while we populate FileSets, so m_Mutex should not - * be locked when this is called. It will be locked on return; the caller must - * unlock it. - */ -FileSet *FilenameDB::GetFileSet( const RString &sDir_, bool bCreate ) -{ - RString sDir = sDir_; - - /* Creating can take a long time; don't hold the lock if we might do that. */ - if( bCreate && m_Mutex.IsLockedByThisThread() && LOG ) - LOG->Warn( "FilenameDB::GetFileSet: m_Mutex was locked" ); - - /* Normalize the path. */ - sDir.Replace("\\", "/"); /* foo\bar -> foo/bar */ - sDir.Replace("//", "/"); /* foo//bar -> foo/bar */ - - if( sDir == "" ) - sDir = "/"; - - RString sLower = sDir; - sLower.MakeLower(); - - m_Mutex.Lock(); - - for(;;) - { - /* Look for the directory. */ - map::iterator i = dirs.find( sLower ); - if( !bCreate ) - { - if( i == dirs.end() ) - return NULL; - return i->second; - } - - /* We're allowed to create. If the directory wasn't found, break out and - * create it. */ - if( i == dirs.end() ) - break; - - /* This directory already exists. If it's still being filled in by another - * thread, wait for it. */ - FileSet *pFileSet = i->second; - if( !pFileSet->m_bFilled ) - { - m_Mutex.Wait(); - - /* Beware: when we unlock m_Mutex to wait for it to finish filling, - * we give up our claim to dirs, so i may be invalid. Start over - * and re-search. */ - continue; - } - - if( ExpireSeconds == -1 || pFileSet->age.PeekDeltaTime() < ExpireSeconds ) - { - /* Found it, and it hasn't expired. */ - return pFileSet; - } - - /* It's expired. Delete the old entry. */ - this->DelFileSet( i ); - break; - } - - /* Create the FileSet and insert it. Set it to !m_bFilled, so if other threads - * happen to try to use this directory before we finish filling it, they'll wait. */ - FileSet *pRet = new FileSet; - pRet->m_bFilled = false; - dirs[sLower] = pRet; - - /* Unlock while we populate the directory. This way, reads to other directories - * won't block if this takes a while. */ - m_Mutex.Unlock(); - ASSERT( !m_Mutex.IsLockedByThisThread() ); - PopulateFileSet( *pRet, sDir ); - - /* If this isn't the root directory, we want to set the dirp pointer of our parent - * to the newly-created directory. Find the pointer we need to set. Be careful of - * order of operations, here: since we just unlocked, any this->dirs searches we did - * previously are no longer valid. */ - FileSet **pParentDirp = NULL; - if( sDir != "/" ) - { - RString sParent = Dirname( sDir ); - if( sParent == "./" ) - sParent = ""; - - /* This also re-locks m_Mutex for us. */ - FileSet *pParent = GetFileSet( sParent ); - if( pParent != NULL ) - { - set::iterator it = pParent->files.find( File(Basename(sDir)) ); - if( it != pParent->files.end() ) - pParentDirp = const_cast(&it->dirp); - } - } - else - { - m_Mutex.Lock(); - } - - if( pParentDirp != NULL ) - *pParentDirp = pRet; - - pRet->age.Touch(); - pRet->m_bFilled = true; - - /* Signal the event, to wake up any other threads that might be waiting for this - * directory. Leave the mutex locked; those threads will wake up when the current - * operation completes. */ - m_Mutex.Broadcast(); - - return pRet; -} - -/* Add the file or directory "sPath". sPath is a directory if it ends with - * a slash. */ -void FilenameDB::AddFile( const RString &sPath_, int iSize, int iHash, void *pPriv ) -{ - RString sPath(sPath_); - - if( sPath == "" || sPath == "/" ) - return; - - if( sPath[0] != '/' ) - sPath = "/" + sPath; - - vector asParts; - split( sPath, "/", asParts, false ); - - vector::const_iterator begin = asParts.begin(); - vector::const_iterator end = asParts.end(); - - bool IsDir = true; - if( sPath[sPath.size()-1] != '/' ) - IsDir = false; - else - --end; - - /* Skip the leading slash. */ - ++begin; - - do - { - /* Combine all but the last part. */ - RString dir = "/" + join( "/", begin, end-1 ); - if( dir != "/" ) - dir += "/"; - const RString &fn = *(end-1); - FileSet *fs = GetFileSet( dir ); - ASSERT( m_Mutex.IsLockedByThisThread() ); - - // const_cast to cast away the constness that is only needed for the name - File &f = const_cast(*fs->files.insert( fn ).first); - f.dir = IsDir; - if( !IsDir ) - { - f.size = iSize; - f.hash = iHash; - f.priv = pPriv; - } - m_Mutex.Unlock(); /* locked by GetFileSet */ - IsDir = true; - - --end; - } while( begin != end ); -} - -/* Remove the given FileSet, and all dirp pointers to it. This means the cache has - * expired, not that the directory is necessarily gone; don't actually delete the file - * from the parent. */ -void FilenameDB::DelFileSet( map::iterator dir ) -{ - /* If this isn't locked, dir may not be valid. */ - ASSERT( m_Mutex.IsLockedByThisThread() ); - - if( dir == dirs.end() ) - return; - - FileSet *fs = dir->second; - - /* Remove any stale dirp pointers. */ - for( map::iterator it = dirs.begin(); it != dirs.end(); ++it ) - { - FileSet *Clean = it->second; - for( set::iterator f = Clean->files.begin(); f != Clean->files.end(); ++f ) - { - File &ff = (File &) *f; - if( ff.dirp == fs ) - ff.dirp = NULL; - } - } - - delete fs; - dirs.erase( dir ); -} - -void FilenameDB::DelFile( const RString &sPath ) -{ - LockMut(m_Mutex); - RString lower = sPath; - lower.MakeLower(); - - map::iterator fsi = dirs.find( lower ); - DelFileSet( fsi ); - - /* Delete sPath from its parent. */ - RString Dir, Name; - SplitPath(sPath, Dir, Name); - FileSet *Parent = GetFileSet( Dir, false ); - if( Parent ) - Parent->files.erase( Name ); - - m_Mutex.Unlock(); /* locked by GetFileSet */ -} - -void FilenameDB::FlushDirCache( const RString & /* sDir */ ) -{ - FileSet *pFileSet = NULL; - m_Mutex.Lock(); - - for(;;) - { - if( dirs.empty() ) - break; - - /* Grab the first entry. Take it out of the list while we hold the - * lock, to guarantee that we own it. */ - pFileSet = dirs.begin()->second; - - dirs.erase( dirs.begin() ); - - /* If it's being filled, we don't really own it until it's finished being - * filled, so wait. */ - while( !pFileSet->m_bFilled ) - m_Mutex.Wait(); - delete pFileSet; - } - -#if 0 - /* XXX: This is tricky, we want to flush all of the subdirectories of - * sDir, but once we unlock the mutex, we basically have to start over. - * It's just an optimization though, so it can wait. */ - { - if( it != dirs.end() ) - { - pFileSet = it->second; - dirs.erase( it ); - while( !pFileSet->m_bFilled ) - m_Mutex.Wait(); - delete pFileSet; - - if( sDir != "/" ) - { - RString sParent = Dirname( sDir ); - if( sParent == "./" ) - sParent = ""; - sParent.MakeLower(); - it = dirs.find( sParent ); - if( it != dirs.end() ) - { - FileSet *pParent = it->second; - set::iterator fileit = pParent->files.find( File(Basename(sDir)) ); - if( fileit != pParent->files.end() ) - fileit->dirp = NULL; - } - } - } - else - { - LOG->Warn( "Trying to flush an unknown directory %s.", sDir.c_str() ); - } -#endif - m_Mutex.Unlock(); -} - -const File *FilenameDB::GetFile( const RString &sPath ) -{ - if( m_Mutex.IsLockedByThisThread() && LOG ) - LOG->Warn( "FilenameDB::GetFile: m_Mutex was locked" ); - - RString Dir, Name; - SplitPath(sPath, Dir, Name); - FileSet *fs = GetFileSet( Dir ); - - set::iterator it; - it = fs->files.find( File(Name) ); - if( it == fs->files.end() ) - return NULL; - - return &*it; -} - -void *FilenameDB::GetFilePriv( const RString &path ) -{ - ASSERT( !m_Mutex.IsLockedByThisThread() ); - - const File *pFile = GetFile( path ); - void *pRet = NULL; - if( pFile != NULL ) - pRet = pFile->priv; - - m_Mutex.Unlock(); /* locked by GetFileSet */ - return pRet; -} - - - -void FilenameDB::GetDirListing( const RString &sPath_, vector &asAddTo, bool bOnlyDirs, bool bReturnPathToo ) -{ - RString sPath = sPath_; -// LOG->Trace( "GetDirListing( %s )", sPath.c_str() ); - - ASSERT( !sPath.empty() ); - - /* Strip off the last path element and use it as a mask. */ - size_t pos = sPath.find_last_of( '/' ); - RString fn; - if( pos == sPath.npos ) - { - fn = sPath; - sPath = ""; - } - else - { - fn = sPath.substr(pos+1); - sPath = sPath.substr(0, pos+1); - } - - /* If the last element was empty, use "*". */ - if( fn.size() == 0 ) - fn = "*"; - - unsigned iStart = asAddTo.size(); - GetFilesSimpleMatch( sPath, fn, asAddTo, bOnlyDirs ); - - if( bReturnPathToo && iStart < asAddTo.size() ) - { - while( iStart < asAddTo.size() ) - { - asAddTo[iStart].insert( 0, sPath ); - iStart++; - } - } -} - -/* Get a complete copy of a FileSet. This isn't very efficient, since it's a deep - * copy, but allows retrieving a copy from elsewhere without having to worry about - * our locking semantics. */ -void FilenameDB::GetFileSetCopy( const RString &sDir, FileSet &out ) -{ - FileSet *pFileSet = GetFileSet( sDir ); - out = *pFileSet; - m_Mutex.Unlock(); /* locked by GetFileSet */ -} - -void FilenameDB::CacheFile( const RString &sPath ) -{ - LOG->Warn( "Slow cache due to: %s", sPath.c_str() ); - FlushDirCache( Dirname(sPath) ); -} - -/* - * Copyright (c) 2003-2004 Glenn Maynard - * 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. - */ +#include "global.h" + +#include "RageUtil_FileDB.h" +#include "RageUtil.h" +#include "RageLog.h" + +/* Search for "beginning*containing*ending". */ +void FileSet::GetFilesMatching( const RString &sBeginning_, const RString &sContaining_, const RString &sEnding_, vector &asOut, bool bOnlyDirs ) const +{ + /* "files" is a case-insensitive mapping, by filename. Use lower_bound to figure + * out where to start. */ + RString sBeginning = sBeginning_; + sBeginning.MakeLower(); + RString sContaining = sContaining_; + sContaining.MakeLower(); + RString sEnding = sEnding_; + sEnding.MakeLower(); + + set::const_iterator i = files.lower_bound( File(sBeginning) ); + for( ; i != files.end(); ++i ) + { + const File &f = *i; + + if( bOnlyDirs && !f.dir ) + continue; + + const RString &sPath = f.lname; + + /* Check sBeginning. Once we hit a filename that no longer matches sBeginning, + * we're past all possible matches in the sort, so stop. */ + if( sBeginning.size() > sPath.size() ) + break; /* can't start with it */ + if( sPath.compare(0, sBeginning.size(), sBeginning) ) + break; /* doesn't start with it */ + + /* Position the end starts on: */ + int end_pos = int(sPath.size())-int(sEnding.size()); + + /* Check end. */ + if( end_pos < 0 ) + continue; /* can't end with it */ + if( sPath.compare(end_pos, string::npos, sEnding) ) + continue; /* doesn't end with it */ + + /* Check sContaining. Do this last, since it's the slowest (substring + * search instead of string match). */ + if( !sContaining.empty() ) + { + size_t pos = sPath.find( sContaining, sBeginning.size() ); + if( pos == sPath.npos ) + continue; /* doesn't contain it */ + if( pos + sContaining.size() > unsigned(end_pos) ) + continue; /* found it but it overlaps with the end */ + } + + asOut.push_back( f.name ); + } +} + +void FileSet::GetFilesEqualTo( const RString &sStr, vector &asOut, bool bOnlyDirs ) const +{ + set::const_iterator i = files.find( File(sStr) ); + if( i == files.end() ) + return; + + if( bOnlyDirs && !i->dir ) + return; + + asOut.push_back( i->name ); +} + +RageFileManager::FileType FileSet::GetFileType( const RString &sPath ) const +{ + set::const_iterator i = files.find( File(sPath) ); + if( i == files.end() ) + return RageFileManager::TYPE_NONE; + + return i->dir? RageFileManager::TYPE_DIR:RageFileManager::TYPE_FILE; +} + +int FileSet::GetFileSize( const RString &sPath ) const +{ + set::const_iterator i = files.find( File(sPath) ); + if( i == files.end() ) + return -1; + return i->size; +} + +int FileSet::GetFileHash( const RString &sPath ) const +{ + set::const_iterator i = files.find( File(sPath) ); + if( i == files.end() ) + return -1; + return i->hash + i->size; +} + +/* + * Given "foo/bar/baz/" or "foo/bar/baz", return "foo/bar/" and "baz". + * "foo" -> "", "foo" + */ +static void SplitPath( RString sPath, RString &sDir, RString &sName ) +{ + CollapsePath( sPath ); + if( sPath.Right(1) == "/" ) + sPath.erase( sPath.size()-1 ); + + size_t iSep = sPath.find_last_of( '/' ); + if( iSep == RString::npos ) + { + sDir = ""; + sName = sPath; + } + else + { + sDir = sPath.substr( 0, iSep+1 ); + sName = sPath.substr( iSep+1 ); + } +} + + +RageFileManager::FileType FilenameDB::GetFileType( const RString &sPath ) +{ + ASSERT( !m_Mutex.IsLockedByThisThread() ); + + RString sDir, sName; + SplitPath( sPath, sDir, sName ); + + if( sName == "/" ) + return RageFileManager::TYPE_DIR; + + const FileSet *fs = GetFileSet( sDir ); + RageFileManager::FileType ret = fs->GetFileType( sName ); + m_Mutex.Unlock(); /* locked by GetFileSet */ + return ret; +} + + +int FilenameDB::GetFileSize( const RString &sPath ) +{ + ASSERT( !m_Mutex.IsLockedByThisThread() ); + + RString sDir, sName; + SplitPath( sPath, sDir, sName ); + + const FileSet *fs = GetFileSet( sDir ); + int ret = fs->GetFileSize( sName ); + m_Mutex.Unlock(); /* locked by GetFileSet */ + return ret; +} + +int FilenameDB::GetFileHash( const RString &sPath ) +{ + ASSERT( !m_Mutex.IsLockedByThisThread() ); + + RString sDir, sName; + SplitPath( sPath, sDir, sName ); + + const FileSet *fs = GetFileSet( sDir ); + int ret = fs->GetFileHash( sName ); + m_Mutex.Unlock(); /* locked by GetFileSet */ + return ret; +} + +/* path should be fully collapsed, so we can operate in-place: no . or .. */ +bool FilenameDB::ResolvePath( RString &sPath ) +{ + if( sPath == "/" || sPath == "" ) + return true; + + /* Split path into components. */ + int iBegin = 0, iSize = -1; + + /* Resolve each component. */ + RString ret = ""; + const FileSet *fs = NULL; + + static const RString slash("/"); + for(;;) + { + split( sPath, slash, iBegin, iSize, true ); + if( iBegin == (int) sPath.size() ) + break; + + if( fs == NULL ) + fs = GetFileSet( ret ); + else + m_Mutex.Lock(); /* for access to fs */ + + RString p = sPath.substr( iBegin, iSize ); + ASSERT_M( p.size() != 1 || p[0] != '.', sPath ); // no . + ASSERT_M( p.size() != 2 || p[0] != '.' || p[1] != '.', sPath ); // no .. + set::const_iterator it = fs->files.find( File(p) ); + + /* If there were no matches, the path isn't found. */ + if( it == fs->files.end() ) + { + m_Mutex.Unlock(); /* locked by GetFileSet */ + return false; + } + + ret += "/" + it->name; + + fs = it->dirp; + + m_Mutex.Unlock(); /* locked by GetFileSet */ + } + + if( sPath.size() && sPath[sPath.size()-1] == '/' ) + sPath = ret + "/"; + else + sPath = ret; + return true; +} + +void FilenameDB::GetFilesMatching( const RString &sDir, const RString &sBeginning, const RString &sContaining, const RString &sEnding, vector &asOut, bool bOnlyDirs ) +{ + ASSERT( !m_Mutex.IsLockedByThisThread() ); + + const FileSet *fs = GetFileSet( sDir ); + fs->GetFilesMatching( sBeginning, sContaining, sEnding, asOut, bOnlyDirs ); + m_Mutex.Unlock(); /* locked by GetFileSet */ +} + +void FilenameDB::GetFilesEqualTo( const RString &sDir, const RString &sFile, vector &asOut, bool bOnlyDirs ) +{ + ASSERT( !m_Mutex.IsLockedByThisThread() ); + + const FileSet *fs = GetFileSet( sDir ); + fs->GetFilesEqualTo( sFile, asOut, bOnlyDirs ); + m_Mutex.Unlock(); /* locked by GetFileSet */ +} + + +void FilenameDB::GetFilesSimpleMatch( const RString &sDir, const RString &sMask, vector &asOut, bool bOnlyDirs ) +{ + /* Does this contain a wildcard? */ + size_t first_pos = sMask.find_first_of( '*' ); + if( first_pos == sMask.npos ) + { + /* No; just do a regular search. */ + GetFilesEqualTo( sDir, sMask, asOut, bOnlyDirs ); + return; + } + size_t second_pos = sMask.find_first_of( '*', first_pos+1 ); + if( second_pos == sMask.npos ) + { + /* Only one *: "A*B". */ + /* XXX: "_blank.png*.png" shouldn't match the file "_blank.png". */ + GetFilesMatching( sDir, sMask.substr(0, first_pos), RString(), sMask.substr(first_pos+1), asOut, bOnlyDirs ); + return; + } + + /* Two *s: "A*B*C". */ + GetFilesMatching( sDir, + sMask.substr(0, first_pos), + sMask.substr(first_pos+1, second_pos-first_pos-1), + sMask.substr(second_pos+1), asOut, bOnlyDirs ); +} + +/* + * Get the FileSet for dir; if create is true, create the FileSet if necessary. + * + * We want to unlock the object while we populate FileSets, so m_Mutex should not + * be locked when this is called. It will be locked on return; the caller must + * unlock it. + */ +FileSet *FilenameDB::GetFileSet( const RString &sDir_, bool bCreate ) +{ + RString sDir = sDir_; + + /* Creating can take a long time; don't hold the lock if we might do that. */ + if( bCreate && m_Mutex.IsLockedByThisThread() && LOG ) + LOG->Warn( "FilenameDB::GetFileSet: m_Mutex was locked" ); + + /* Normalize the path. */ + sDir.Replace("\\", "/"); /* foo\bar -> foo/bar */ + sDir.Replace("//", "/"); /* foo//bar -> foo/bar */ + + if( sDir == "" ) + sDir = "/"; + + RString sLower = sDir; + sLower.MakeLower(); + + m_Mutex.Lock(); + + for(;;) + { + /* Look for the directory. */ + map::iterator i = dirs.find( sLower ); + if( !bCreate ) + { + if( i == dirs.end() ) + return NULL; + return i->second; + } + + /* We're allowed to create. If the directory wasn't found, break out and + * create it. */ + if( i == dirs.end() ) + break; + + /* This directory already exists. If it's still being filled in by another + * thread, wait for it. */ + FileSet *pFileSet = i->second; + if( !pFileSet->m_bFilled ) + { + m_Mutex.Wait(); + + /* Beware: when we unlock m_Mutex to wait for it to finish filling, + * we give up our claim to dirs, so i may be invalid. Start over + * and re-search. */ + continue; + } + + if( ExpireSeconds == -1 || pFileSet->age.PeekDeltaTime() < ExpireSeconds ) + { + /* Found it, and it hasn't expired. */ + return pFileSet; + } + + /* It's expired. Delete the old entry. */ + this->DelFileSet( i ); + break; + } + + /* Create the FileSet and insert it. Set it to !m_bFilled, so if other threads + * happen to try to use this directory before we finish filling it, they'll wait. */ + FileSet *pRet = new FileSet; + pRet->m_bFilled = false; + dirs[sLower] = pRet; + + /* Unlock while we populate the directory. This way, reads to other directories + * won't block if this takes a while. */ + m_Mutex.Unlock(); + ASSERT( !m_Mutex.IsLockedByThisThread() ); + PopulateFileSet( *pRet, sDir ); + + /* If this isn't the root directory, we want to set the dirp pointer of our parent + * to the newly-created directory. Find the pointer we need to set. Be careful of + * order of operations, here: since we just unlocked, any this->dirs searches we did + * previously are no longer valid. */ + FileSet **pParentDirp = NULL; + if( sDir != "/" ) + { + RString sParent = Dirname( sDir ); + if( sParent == "./" ) + sParent = ""; + + /* This also re-locks m_Mutex for us. */ + FileSet *pParent = GetFileSet( sParent ); + if( pParent != nullptr ) + { + set::iterator it = pParent->files.find( File(Basename(sDir)) ); + if( it != pParent->files.end() ) + pParentDirp = const_cast(&it->dirp); + } + } + else + { + m_Mutex.Lock(); + } + + if( pParentDirp != nullptr ) + *pParentDirp = pRet; + + pRet->age.Touch(); + pRet->m_bFilled = true; + + /* Signal the event, to wake up any other threads that might be waiting for this + * directory. Leave the mutex locked; those threads will wake up when the current + * operation completes. */ + m_Mutex.Broadcast(); + + return pRet; +} + +/* Add the file or directory "sPath". sPath is a directory if it ends with + * a slash. */ +void FilenameDB::AddFile( const RString &sPath_, int iSize, int iHash, void *pPriv ) +{ + RString sPath(sPath_); + + if( sPath == "" || sPath == "/" ) + return; + + if( sPath[0] != '/' ) + sPath = "/" + sPath; + + vector asParts; + split( sPath, "/", asParts, false ); + + vector::const_iterator begin = asParts.begin(); + vector::const_iterator end = asParts.end(); + + bool IsDir = true; + if( sPath[sPath.size()-1] != '/' ) + IsDir = false; + else + --end; + + /* Skip the leading slash. */ + ++begin; + + do + { + /* Combine all but the last part. */ + RString dir = "/" + join( "/", begin, end-1 ); + if( dir != "/" ) + dir += "/"; + const RString &fn = *(end-1); + FileSet *fs = GetFileSet( dir ); + ASSERT( m_Mutex.IsLockedByThisThread() ); + + // const_cast to cast away the constness that is only needed for the name + File &f = const_cast(*fs->files.insert( fn ).first); + f.dir = IsDir; + if( !IsDir ) + { + f.size = iSize; + f.hash = iHash; + f.priv = pPriv; + } + m_Mutex.Unlock(); /* locked by GetFileSet */ + IsDir = true; + + --end; + } while( begin != end ); +} + +/* Remove the given FileSet, and all dirp pointers to it. This means the cache has + * expired, not that the directory is necessarily gone; don't actually delete the file + * from the parent. */ +void FilenameDB::DelFileSet( map::iterator dir ) +{ + /* If this isn't locked, dir may not be valid. */ + ASSERT( m_Mutex.IsLockedByThisThread() ); + + if( dir == dirs.end() ) + return; + + FileSet *fs = dir->second; + + /* Remove any stale dirp pointers. */ + for( map::iterator it = dirs.begin(); it != dirs.end(); ++it ) + { + FileSet *Clean = it->second; + for( set::iterator f = Clean->files.begin(); f != Clean->files.end(); ++f ) + { + File &ff = (File &) *f; + if( ff.dirp == fs ) + ff.dirp = NULL; + } + } + + delete fs; + dirs.erase( dir ); +} + +void FilenameDB::DelFile( const RString &sPath ) +{ + LockMut(m_Mutex); + RString lower = sPath; + lower.MakeLower(); + + map::iterator fsi = dirs.find( lower ); + DelFileSet( fsi ); + + /* Delete sPath from its parent. */ + RString Dir, Name; + SplitPath(sPath, Dir, Name); + FileSet *Parent = GetFileSet( Dir, false ); + if( Parent ) + Parent->files.erase( Name ); + + m_Mutex.Unlock(); /* locked by GetFileSet */ +} + +void FilenameDB::FlushDirCache( const RString & /* sDir */ ) +{ + FileSet *pFileSet = NULL; + m_Mutex.Lock(); + + for(;;) + { + if( dirs.empty() ) + break; + + /* Grab the first entry. Take it out of the list while we hold the + * lock, to guarantee that we own it. */ + pFileSet = dirs.begin()->second; + + dirs.erase( dirs.begin() ); + + /* If it's being filled, we don't really own it until it's finished being + * filled, so wait. */ + while( !pFileSet->m_bFilled ) + m_Mutex.Wait(); + delete pFileSet; + } + +#if 0 + /* XXX: This is tricky, we want to flush all of the subdirectories of + * sDir, but once we unlock the mutex, we basically have to start over. + * It's just an optimization though, so it can wait. */ + { + if( it != dirs.end() ) + { + pFileSet = it->second; + dirs.erase( it ); + while( !pFileSet->m_bFilled ) + m_Mutex.Wait(); + delete pFileSet; + + if( sDir != "/" ) + { + RString sParent = Dirname( sDir ); + if( sParent == "./" ) + sParent = ""; + sParent.MakeLower(); + it = dirs.find( sParent ); + if( it != dirs.end() ) + { + FileSet *pParent = it->second; + set::iterator fileit = pParent->files.find( File(Basename(sDir)) ); + if( fileit != pParent->files.end() ) + fileit->dirp = NULL; + } + } + } + else + { + LOG->Warn( "Trying to flush an unknown directory %s.", sDir.c_str() ); + } +#endif + m_Mutex.Unlock(); +} + +const File *FilenameDB::GetFile( const RString &sPath ) +{ + if( m_Mutex.IsLockedByThisThread() && LOG ) + LOG->Warn( "FilenameDB::GetFile: m_Mutex was locked" ); + + RString Dir, Name; + SplitPath(sPath, Dir, Name); + FileSet *fs = GetFileSet( Dir ); + + set::iterator it; + it = fs->files.find( File(Name) ); + if( it == fs->files.end() ) + return NULL; + + return &*it; +} + +void *FilenameDB::GetFilePriv( const RString &path ) +{ + ASSERT( !m_Mutex.IsLockedByThisThread() ); + + const File *pFile = GetFile( path ); + void *pRet = NULL; + if( pFile != nullptr ) + pRet = pFile->priv; + + m_Mutex.Unlock(); /* locked by GetFileSet */ + return pRet; +} + + + +void FilenameDB::GetDirListing( const RString &sPath_, vector &asAddTo, bool bOnlyDirs, bool bReturnPathToo ) +{ + RString sPath = sPath_; +// LOG->Trace( "GetDirListing( %s )", sPath.c_str() ); + + ASSERT( !sPath.empty() ); + + /* Strip off the last path element and use it as a mask. */ + size_t pos = sPath.find_last_of( '/' ); + RString fn; + if( pos == sPath.npos ) + { + fn = sPath; + sPath = ""; + } + else + { + fn = sPath.substr(pos+1); + sPath = sPath.substr(0, pos+1); + } + + /* If the last element was empty, use "*". */ + if( fn.size() == 0 ) + fn = "*"; + + unsigned iStart = asAddTo.size(); + GetFilesSimpleMatch( sPath, fn, asAddTo, bOnlyDirs ); + + if( bReturnPathToo && iStart < asAddTo.size() ) + { + while( iStart < asAddTo.size() ) + { + asAddTo[iStart].insert( 0, sPath ); + iStart++; + } + } +} + +/* Get a complete copy of a FileSet. This isn't very efficient, since it's a deep + * copy, but allows retrieving a copy from elsewhere without having to worry about + * our locking semantics. */ +void FilenameDB::GetFileSetCopy( const RString &sDir, FileSet &out ) +{ + FileSet *pFileSet = GetFileSet( sDir ); + out = *pFileSet; + m_Mutex.Unlock(); /* locked by GetFileSet */ +} + +void FilenameDB::CacheFile( const RString &sPath ) +{ + LOG->Warn( "Slow cache due to: %s", sPath.c_str() ); + FlushDirCache( Dirname(sPath) ); +} + +/* + * Copyright (c) 2003-2004 Glenn Maynard + * 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. + */ diff --git a/src/RoomWheel.cpp b/src/RoomWheel.cpp index acb792a167..d9e68a39a9 100644 --- a/src/RoomWheel.cpp +++ b/src/RoomWheel.cpp @@ -180,7 +180,7 @@ void RoomWheel::Move( int n ) if( n == 0 && m_iSelection >= m_offset ) { const RoomWheelItemData* data = GetItem( m_iSelection-m_offset ); - if( data != NULL ) + if( data != nullptr ) SCREENMAN->PostMessageToTopScreen( SM_RoomInfoDeploy, 0 ); } else diff --git a/src/ScoreKeeperNormal.cpp b/src/ScoreKeeperNormal.cpp index 2ff68dd11b..37cbdf20a1 100644 --- a/src/ScoreKeeperNormal.cpp +++ b/src/ScoreKeeperNormal.cpp @@ -104,9 +104,9 @@ void ScoreKeeperNormal::Load( for( unsigned i=0; iGetNoteData( ndTemp ); diff --git a/src/ScoreKeeperRave.cpp b/src/ScoreKeeperRave.cpp index 30c9dc7b10..a78fb8c42d 100644 --- a/src/ScoreKeeperRave.cpp +++ b/src/ScoreKeeperRave.cpp @@ -1,233 +1,233 @@ -#include "global.h" -#include "ScoreKeeperRave.h" -#include "ThemeManager.h" -#include "RageUtil.h" -#include "GameState.h" -#include "Character.h" -#include "ScreenManager.h" -#include "PrefsManager.h" -#include "ThemeMetric.h" -#include "PlayerState.h" -#include "NoteTypes.h" - -ThemeMetric ATTACK_DURATION_SECONDS ("ScoreKeeperRave","AttackDurationSeconds"); - -static const float g_fSuperMeterPercentChangeInit[] = -{ - +0.02f, // SE_CheckpointHit - +0.05f, // SE_W1 - +0.04f, // SE_W2 - +0.02f, // SE_W3 - +0.00f, // SE_W4 - +0.00f, // SE_W5 - -0.20f, // SE_Miss - -0.40f, // SE_HitMine - -0.02f, // SE_CheckpointMiss - +0.04f, // SE_Held - -0.20f, // SE_LetGo -}; -COMPILE_ASSERT( ARRAYLEN(g_fSuperMeterPercentChangeInit) == NUM_ScoreEvent ); - -static void SuperMeterPercentChangeInit( size_t /*ScoreEvent*/ i, RString &sNameOut, float &defaultValueOut ) -{ - sNameOut = "SuperMeterPercentChange" + ScoreEventToString( (ScoreEvent)i ); - defaultValueOut = g_fSuperMeterPercentChangeInit[i]; -} - -static Preference1D g_fSuperMeterPercentChange( SuperMeterPercentChangeInit, NUM_ScoreEvent ); - -ScoreKeeperRave::ScoreKeeperRave( PlayerState *pPlayerState, PlayerStageStats *pPlayerStageStats ) : - ScoreKeeper(pPlayerState, pPlayerStageStats) -{ -} - -void ScoreKeeperRave::HandleTapScore( const TapNote &tn ) -{ - TapNoteScore score = tn.result.tns; - float fPercentToMove = 0; - - if( score == TNS_HitMine ) - fPercentToMove = g_fSuperMeterPercentChange[SE_HitMine]; - - AddSuperMeterDelta( fPercentToMove ); -} - -#define CROSSED( val ) (fOld < val && fNew >= val) -#define CROSSED_ATTACK_LEVEL( level ) CROSSED(1.f/NUM_ATTACK_LEVELS*(level+1)) -void ScoreKeeperRave::HandleTapRowScore( const NoteData &nd, int iRow ) -{ - TapNoteScore scoreOfLastTap; - int iNumTapsInRow; - float fPercentToMove = 0.0f; - - GetScoreOfLastTapInRow( nd, iRow, scoreOfLastTap, iNumTapsInRow ); - if( iNumTapsInRow <= 0 ) - return; - switch( scoreOfLastTap ) - { - DEFAULT_FAIL( scoreOfLastTap ); - case TNS_W1: fPercentToMove = g_fSuperMeterPercentChange[SE_W1]; break; - case TNS_W2: fPercentToMove = g_fSuperMeterPercentChange[SE_W2]; break; - case TNS_W3: fPercentToMove = g_fSuperMeterPercentChange[SE_W3]; break; - case TNS_W4: fPercentToMove = g_fSuperMeterPercentChange[SE_W4]; break; - case TNS_W5: fPercentToMove = g_fSuperMeterPercentChange[SE_W5]; break; - case TNS_Miss: fPercentToMove = g_fSuperMeterPercentChange[SE_Miss]; break; - } - AddSuperMeterDelta( fPercentToMove ); -} - -void ScoreKeeperRave::HandleHoldScore( const TapNote &tn ) -{ - // todo: should hit mine be handled in HandleTapRow score instead? -aj - TapNoteScore tapScore = tn.result.tns; - float fPercentToMove = 0.0f; - switch( tapScore ) - { - case TNS_HitMine: - fPercentToMove = g_fSuperMeterPercentChange[SE_HitMine]; - break; - default: break; - } - - // Playing with this code enabled seems to feel "wrong", but I'm leaving it - // in for player feedback. -aj - HoldNoteScore holdScore = tn.HoldResult.hns; - switch( holdScore ) - { - case HNS_Held: fPercentToMove = g_fSuperMeterPercentChange[SE_Held]; break; - case HNS_LetGo: fPercentToMove = g_fSuperMeterPercentChange[SE_LetGo]; break; - default: break; - } - AddSuperMeterDelta( fPercentToMove ); -} - -extern ThemeMetric PENALIZE_TAP_SCORE_NONE; -void ScoreKeeperRave::HandleTapScoreNone() -{ - if( PENALIZE_TAP_SCORE_NONE ) - { - float fPercentToMove = g_fSuperMeterPercentChange[SE_Miss]; - AddSuperMeterDelta( fPercentToMove ); - } -} - -void ScoreKeeperRave::AddSuperMeterDelta( float fUnscaledPercentChange ) -{ - if( PREFSMAN->m_bMercifulDrain && fUnscaledPercentChange<0 ) - { - float fSuperPercentage = m_pPlayerState->m_fSuperMeter / NUM_ATTACK_LEVELS; - fUnscaledPercentChange *= SCALE( fSuperPercentage, 0.f, 1.f, 0.5f, 1.f); - } - - // more mercy: Grow super meter slower or faster depending on life. - if( PREFSMAN->m_bMercifulSuperMeter ) - { - float fLifePercentage = 0; - switch( m_pPlayerState->m_PlayerNumber ) - { - case PLAYER_1: fLifePercentage = GAMESTATE->m_fTugLifePercentP1; break; - case PLAYER_2: fLifePercentage = 1 - GAMESTATE->m_fTugLifePercentP1; break; - default: - FAIL_M(ssprintf("Invalid player number: %i", m_pPlayerState->m_PlayerNumber)); - } - CLAMP( fLifePercentage, 0.f, 1.f ); - if( fUnscaledPercentChange > 0 ) - fUnscaledPercentChange *= SCALE( fLifePercentage, 0.f, 1.f, 1.7f, 0.3f); - else // fUnscaledPercentChange <= 0 - fUnscaledPercentChange /= SCALE( fLifePercentage, 0.f, 1.f, 1.7f, 0.3f); - } - - // mercy: drop super meter faster if at a higher level - if( fUnscaledPercentChange < 0 ) - fUnscaledPercentChange *= SCALE( m_pPlayerState->m_fSuperMeter, 0.f, 1.f, 0.01f, 1.f ); - - AttackLevel oldAL = (AttackLevel)(int)m_pPlayerState->m_fSuperMeter; - - float fPercentToMove = fUnscaledPercentChange; - m_pPlayerState->m_fSuperMeter += fPercentToMove * m_pPlayerState->m_fSuperMeterGrowthScale; - CLAMP( m_pPlayerState->m_fSuperMeter, 0.f, NUM_ATTACK_LEVELS ); - - AttackLevel newAL = (AttackLevel)(int)m_pPlayerState->m_fSuperMeter; - - if( newAL > oldAL ) - { - LaunchAttack( oldAL ); - if( newAL == NUM_ATTACK_LEVELS ) // hit upper bounds of meter - m_pPlayerState->m_fSuperMeter -= 1.f; - } - - // mercy: if losing remove attacks on life drain - if( fUnscaledPercentChange < 0 ) - { - bool bWinning; - switch( m_pPlayerState->m_PlayerNumber ) - { - case PLAYER_1: bWinning = GAMESTATE->m_fTugLifePercentP1 > 0.5f; break; - case PLAYER_2: bWinning = GAMESTATE->m_fTugLifePercentP1 < 0.5f; break; - default: - bWinning = false; - FAIL_M(ssprintf("Invalid player number: %i", m_pPlayerState->m_PlayerNumber)); - } - if( !bWinning ) - m_pPlayerState->EndActiveAttacks(); - } -} - -void ScoreKeeperRave::LaunchAttack( AttackLevel al ) -{ - PlayerNumber pn = m_pPlayerState->m_PlayerNumber; - - RString* asAttacks = GAMESTATE->m_pCurCharacters[pn]->m_sAttacks[al]; // [NUM_ATTACKS_PER_LEVEL] - RString sAttackToGive; - - if (GAMESTATE->m_pCurCharacters[pn] != NULL) - sAttackToGive = asAttacks[ RandomInt(NUM_ATTACKS_PER_LEVEL) ]; - else - { - // "If you add any noteskins here, you need to make sure they're cached, too." -? - // Noteskins probably won't work here anymore. -aj - RString DefaultAttacks[8] = { "1.5x", "2.0x", "0.5x", "reverse", "sudden", "boost", "brake", "wave" }; - sAttackToGive = DefaultAttacks[ RandomInt(8) ]; - } - - PlayerNumber pnToAttack = OPPOSITE_PLAYER[pn]; - PlayerState *pPlayerStateToAttack = GAMESTATE->m_pPlayerState[pnToAttack]; - - Attack a; - a.level = al; - a.fSecsRemaining = ATTACK_DURATION_SECONDS; - a.sModifiers = sAttackToGive; - - // remove current attack (if any) - pPlayerStateToAttack->RemoveActiveAttacks(); - - // apply new attack - pPlayerStateToAttack->LaunchAttack( a ); - -// SCREENMAN->SystemMessage( ssprintf( "attacking %d with %s", pnToAttack, sAttackToGive.c_str() ) ); -} - -/* - * (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. - */ +#include "global.h" +#include "ScoreKeeperRave.h" +#include "ThemeManager.h" +#include "RageUtil.h" +#include "GameState.h" +#include "Character.h" +#include "ScreenManager.h" +#include "PrefsManager.h" +#include "ThemeMetric.h" +#include "PlayerState.h" +#include "NoteTypes.h" + +ThemeMetric ATTACK_DURATION_SECONDS ("ScoreKeeperRave","AttackDurationSeconds"); + +static const float g_fSuperMeterPercentChangeInit[] = +{ + +0.02f, // SE_CheckpointHit + +0.05f, // SE_W1 + +0.04f, // SE_W2 + +0.02f, // SE_W3 + +0.00f, // SE_W4 + +0.00f, // SE_W5 + -0.20f, // SE_Miss + -0.40f, // SE_HitMine + -0.02f, // SE_CheckpointMiss + +0.04f, // SE_Held + -0.20f, // SE_LetGo +}; +COMPILE_ASSERT( ARRAYLEN(g_fSuperMeterPercentChangeInit) == NUM_ScoreEvent ); + +static void SuperMeterPercentChangeInit( size_t /*ScoreEvent*/ i, RString &sNameOut, float &defaultValueOut ) +{ + sNameOut = "SuperMeterPercentChange" + ScoreEventToString( (ScoreEvent)i ); + defaultValueOut = g_fSuperMeterPercentChangeInit[i]; +} + +static Preference1D g_fSuperMeterPercentChange( SuperMeterPercentChangeInit, NUM_ScoreEvent ); + +ScoreKeeperRave::ScoreKeeperRave( PlayerState *pPlayerState, PlayerStageStats *pPlayerStageStats ) : + ScoreKeeper(pPlayerState, pPlayerStageStats) +{ +} + +void ScoreKeeperRave::HandleTapScore( const TapNote &tn ) +{ + TapNoteScore score = tn.result.tns; + float fPercentToMove = 0; + + if( score == TNS_HitMine ) + fPercentToMove = g_fSuperMeterPercentChange[SE_HitMine]; + + AddSuperMeterDelta( fPercentToMove ); +} + +#define CROSSED( val ) (fOld < val && fNew >= val) +#define CROSSED_ATTACK_LEVEL( level ) CROSSED(1.f/NUM_ATTACK_LEVELS*(level+1)) +void ScoreKeeperRave::HandleTapRowScore( const NoteData &nd, int iRow ) +{ + TapNoteScore scoreOfLastTap; + int iNumTapsInRow; + float fPercentToMove = 0.0f; + + GetScoreOfLastTapInRow( nd, iRow, scoreOfLastTap, iNumTapsInRow ); + if( iNumTapsInRow <= 0 ) + return; + switch( scoreOfLastTap ) + { + DEFAULT_FAIL( scoreOfLastTap ); + case TNS_W1: fPercentToMove = g_fSuperMeterPercentChange[SE_W1]; break; + case TNS_W2: fPercentToMove = g_fSuperMeterPercentChange[SE_W2]; break; + case TNS_W3: fPercentToMove = g_fSuperMeterPercentChange[SE_W3]; break; + case TNS_W4: fPercentToMove = g_fSuperMeterPercentChange[SE_W4]; break; + case TNS_W5: fPercentToMove = g_fSuperMeterPercentChange[SE_W5]; break; + case TNS_Miss: fPercentToMove = g_fSuperMeterPercentChange[SE_Miss]; break; + } + AddSuperMeterDelta( fPercentToMove ); +} + +void ScoreKeeperRave::HandleHoldScore( const TapNote &tn ) +{ + // todo: should hit mine be handled in HandleTapRow score instead? -aj + TapNoteScore tapScore = tn.result.tns; + float fPercentToMove = 0.0f; + switch( tapScore ) + { + case TNS_HitMine: + fPercentToMove = g_fSuperMeterPercentChange[SE_HitMine]; + break; + default: break; + } + + // Playing with this code enabled seems to feel "wrong", but I'm leaving it + // in for player feedback. -aj + HoldNoteScore holdScore = tn.HoldResult.hns; + switch( holdScore ) + { + case HNS_Held: fPercentToMove = g_fSuperMeterPercentChange[SE_Held]; break; + case HNS_LetGo: fPercentToMove = g_fSuperMeterPercentChange[SE_LetGo]; break; + default: break; + } + AddSuperMeterDelta( fPercentToMove ); +} + +extern ThemeMetric PENALIZE_TAP_SCORE_NONE; +void ScoreKeeperRave::HandleTapScoreNone() +{ + if( PENALIZE_TAP_SCORE_NONE ) + { + float fPercentToMove = g_fSuperMeterPercentChange[SE_Miss]; + AddSuperMeterDelta( fPercentToMove ); + } +} + +void ScoreKeeperRave::AddSuperMeterDelta( float fUnscaledPercentChange ) +{ + if( PREFSMAN->m_bMercifulDrain && fUnscaledPercentChange<0 ) + { + float fSuperPercentage = m_pPlayerState->m_fSuperMeter / NUM_ATTACK_LEVELS; + fUnscaledPercentChange *= SCALE( fSuperPercentage, 0.f, 1.f, 0.5f, 1.f); + } + + // more mercy: Grow super meter slower or faster depending on life. + if( PREFSMAN->m_bMercifulSuperMeter ) + { + float fLifePercentage = 0; + switch( m_pPlayerState->m_PlayerNumber ) + { + case PLAYER_1: fLifePercentage = GAMESTATE->m_fTugLifePercentP1; break; + case PLAYER_2: fLifePercentage = 1 - GAMESTATE->m_fTugLifePercentP1; break; + default: + FAIL_M(ssprintf("Invalid player number: %i", m_pPlayerState->m_PlayerNumber)); + } + CLAMP( fLifePercentage, 0.f, 1.f ); + if( fUnscaledPercentChange > 0 ) + fUnscaledPercentChange *= SCALE( fLifePercentage, 0.f, 1.f, 1.7f, 0.3f); + else // fUnscaledPercentChange <= 0 + fUnscaledPercentChange /= SCALE( fLifePercentage, 0.f, 1.f, 1.7f, 0.3f); + } + + // mercy: drop super meter faster if at a higher level + if( fUnscaledPercentChange < 0 ) + fUnscaledPercentChange *= SCALE( m_pPlayerState->m_fSuperMeter, 0.f, 1.f, 0.01f, 1.f ); + + AttackLevel oldAL = (AttackLevel)(int)m_pPlayerState->m_fSuperMeter; + + float fPercentToMove = fUnscaledPercentChange; + m_pPlayerState->m_fSuperMeter += fPercentToMove * m_pPlayerState->m_fSuperMeterGrowthScale; + CLAMP( m_pPlayerState->m_fSuperMeter, 0.f, NUM_ATTACK_LEVELS ); + + AttackLevel newAL = (AttackLevel)(int)m_pPlayerState->m_fSuperMeter; + + if( newAL > oldAL ) + { + LaunchAttack( oldAL ); + if( newAL == NUM_ATTACK_LEVELS ) // hit upper bounds of meter + m_pPlayerState->m_fSuperMeter -= 1.f; + } + + // mercy: if losing remove attacks on life drain + if( fUnscaledPercentChange < 0 ) + { + bool bWinning; + switch( m_pPlayerState->m_PlayerNumber ) + { + case PLAYER_1: bWinning = GAMESTATE->m_fTugLifePercentP1 > 0.5f; break; + case PLAYER_2: bWinning = GAMESTATE->m_fTugLifePercentP1 < 0.5f; break; + default: + bWinning = false; + FAIL_M(ssprintf("Invalid player number: %i", m_pPlayerState->m_PlayerNumber)); + } + if( !bWinning ) + m_pPlayerState->EndActiveAttacks(); + } +} + +void ScoreKeeperRave::LaunchAttack( AttackLevel al ) +{ + PlayerNumber pn = m_pPlayerState->m_PlayerNumber; + + RString* asAttacks = GAMESTATE->m_pCurCharacters[pn]->m_sAttacks[al]; // [NUM_ATTACKS_PER_LEVEL] + RString sAttackToGive; + + if (GAMESTATE->m_pCurCharacters[pn] != nullptr) + sAttackToGive = asAttacks[ RandomInt(NUM_ATTACKS_PER_LEVEL) ]; + else + { + // "If you add any noteskins here, you need to make sure they're cached, too." -? + // Noteskins probably won't work here anymore. -aj + RString DefaultAttacks[8] = { "1.5x", "2.0x", "0.5x", "reverse", "sudden", "boost", "brake", "wave" }; + sAttackToGive = DefaultAttacks[ RandomInt(8) ]; + } + + PlayerNumber pnToAttack = OPPOSITE_PLAYER[pn]; + PlayerState *pPlayerStateToAttack = GAMESTATE->m_pPlayerState[pnToAttack]; + + Attack a; + a.level = al; + a.fSecsRemaining = ATTACK_DURATION_SECONDS; + a.sModifiers = sAttackToGive; + + // remove current attack (if any) + pPlayerStateToAttack->RemoveActiveAttacks(); + + // apply new attack + pPlayerStateToAttack->LaunchAttack( a ); + +// SCREENMAN->SystemMessage( ssprintf( "attacking %d with %s", pnToAttack, sAttackToGive.c_str() ) ); +} + +/* + * (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. + */ diff --git a/src/ScreenEdit.cpp b/src/ScreenEdit.cpp index b06a38006a..f6837fb53f 100644 --- a/src/ScreenEdit.cpp +++ b/src/ScreenEdit.cpp @@ -1102,8 +1102,8 @@ void ScreenEdit::Init() SubscribeToMessage( "Judgment" ); - ASSERT( GAMESTATE->m_pCurSong != NULL ); - ASSERT( GAMESTATE->m_pCurSteps[PLAYER_1] != NULL ); + ASSERT( GAMESTATE->m_pCurSong != nullptr ); + ASSERT( GAMESTATE->m_pCurSteps[PLAYER_1] != nullptr ); EDIT_MODE.Load( m_sName, "EditMode" ); ScreenWithMenuElements::Init(); @@ -2112,7 +2112,7 @@ bool ScreenEdit::InputEdit( const InputEventPlus &input, EditButton EditB ) // save current steps Steps* pSteps = GAMESTATE->m_pCurSteps[PLAYER_1]; - ASSERT( pSteps != NULL ); + ASSERT( pSteps != nullptr ); pSteps->SetNoteData( m_NoteDataEdit ); // Get all Steps of this StepsType @@ -3087,7 +3087,7 @@ void ScreenEdit::TransitionEditState( EditState em ) /* FirstBeat affects backgrounds, so commit changes to memory (not to disk) * and recalc it. */ Steps* pSteps = GAMESTATE->m_pCurSteps[PLAYER_1]; - ASSERT( pSteps != NULL ); + ASSERT( pSteps != nullptr ); pSteps->SetNoteData( m_NoteDataEdit ); m_pSong->ReCalculateRadarValuesAndLastSecond(); @@ -3493,7 +3493,7 @@ void ScreenEdit::HandleScreenMessage( const ScreenMessage SM ) GAMESTATE->m_pCurCourse.Set( pCourse ); GAMESTATE->m_iEditCourseEntryIndex.Set( iCourseEntryIndex ); - ASSERT( GAMESTATE->m_pCurCourse != NULL ); + ASSERT( GAMESTATE->m_pCurCourse != nullptr ); } } else if (SM == SM_BackFromKeysoundTrack) @@ -5520,8 +5520,8 @@ void ScreenEdit::SetupCourseAttacks() void ScreenEdit::CopyToLastSave() { - ASSERT( GAMESTATE->m_pCurSong != NULL ); - ASSERT( GAMESTATE->m_pCurSteps[PLAYER_1] != NULL ); + ASSERT( GAMESTATE->m_pCurSong != nullptr ); + ASSERT( GAMESTATE->m_pCurSteps[PLAYER_1] != nullptr ); m_SongLastSave = *GAMESTATE->m_pCurSong; m_vStepsLastSave.clear(); const vector &vSteps = GAMESTATE->m_pCurSong->GetStepsByStepsType( GAMESTATE->m_pCurSteps[PLAYER_1]->m_StepsType ); @@ -5543,7 +5543,7 @@ void ScreenEdit::CopyFromLastSave() void ScreenEdit::RevertFromDisk() { - ASSERT( GAMESTATE->m_pCurSteps[PLAYER_1] != NULL ); + ASSERT( GAMESTATE->m_pCurSteps[PLAYER_1] != nullptr ); StepsID id; id.FromSteps( GAMESTATE->m_pCurSteps[PLAYER_1] ); ASSERT( id.IsValid() ); diff --git a/src/ScreenEditMenu.cpp b/src/ScreenEditMenu.cpp index 23111bd833..2b234f09f4 100644 --- a/src/ScreenEditMenu.cpp +++ b/src/ScreenEditMenu.cpp @@ -220,7 +220,7 @@ bool ScreenEditMenu::MenuStart( const InputEventPlus & ) { case EditMenuAction_Delete: { - ASSERT( pSteps != NULL ); + ASSERT( pSteps != nullptr ); if( pSteps->IsAutogen() ) { SCREENMAN->PlayInvalidSound(); @@ -238,7 +238,7 @@ bool ScreenEditMenu::MenuStart( const InputEventPlus & ) case EditMenuAction_Practice: break; case EditMenuAction_Delete: - ASSERT( pSteps != NULL ); + ASSERT( pSteps != nullptr ); ScreenPrompt::Prompt( SM_None, STEPS_WILL_BE_LOST.GetValue() + "\n\n" + CONTINUE_WITH_DELETE.GetValue(), PROMPT_YES_NO, ANSWER_NO ); break; @@ -298,7 +298,7 @@ bool ScreenEditMenu::MenuStart( const InputEventPlus & ) case EditMenuAction_Practice: { // Prepare for ScreenEdit - ASSERT( pSteps != NULL ); + ASSERT( pSteps != nullptr ); bool bPromptToNameSteps = (action == EditMenuAction_Create && dc == Difficulty_Edit); if( bPromptToNameSteps ) { diff --git a/src/ScreenGameplay.cpp b/src/ScreenGameplay.cpp index 6a45af059e..35a52c1b80 100644 --- a/src/ScreenGameplay.cpp +++ b/src/ScreenGameplay.cpp @@ -428,7 +428,7 @@ void ScreenGameplay::Init() GAMESTATE->m_pCurSteps[p].Set( GAMESTATE->m_pCurSteps[ GAMESTATE->GetFirstHumanPlayer() ] ); FOREACH_EnabledPlayer(p) - ASSERT( GAMESTATE->m_pCurSteps[p].Get() != NULL ); + ASSERT( GAMESTATE->m_pCurSteps[p].Get() != nullptr ); } /* Increment the course play count. */ @@ -814,28 +814,28 @@ void ScreenGameplay::InitSongQueues() if( GAMESTATE->IsCourseMode() ) { Course* pCourse = GAMESTATE->m_pCurCourse; - ASSERT( pCourse != NULL ); + ASSERT( pCourse != nullptr ); m_apSongsQueue.clear(); PlayerNumber pnMaster = GAMESTATE->GetMasterPlayerNumber(); Trail *pTrail = GAMESTATE->m_pCurTrail[pnMaster]; - ASSERT( pTrail != NULL ); + ASSERT( pTrail != nullptr ); for (TrailEntry const &e : pTrail->m_vEntries) { - ASSERT( e.pSong != NULL ); + ASSERT( e.pSong != nullptr ); m_apSongsQueue.push_back( e.pSong ); } FOREACH_EnabledPlayerInfo( m_vPlayerInfo, pi ) { Trail *lTrail = GAMESTATE->m_pCurTrail[ pi->GetStepsAndTrailIndex() ]; - ASSERT( lTrail != NULL ); + ASSERT( lTrail != nullptr ); pi->m_vpStepsQueue.clear(); pi->m_asModifiersQueue.clear(); for (TrailEntry const &e : lTrail->m_vEntries) { - ASSERT( e.pSteps != NULL ); + ASSERT( e.pSteps != nullptr ); pi->m_vpStepsQueue.push_back( e.pSteps ); AttackArray a; e.GetAttackArray( a ); @@ -1094,7 +1094,7 @@ void ScreenGameplay::LoadNextSong() Steps* pSteps = GAMESTATE->m_pCurSteps[ pi->GetStepsAndTrailIndex() ]; ++pi->GetPlayerStageStats()->m_iStepsPlayed; - ASSERT( GAMESTATE->m_pCurSteps[ pi->GetStepsAndTrailIndex() ] != NULL ); + ASSERT( GAMESTATE->m_pCurSteps[ pi->GetStepsAndTrailIndex() ] != nullptr ); if( pi->m_ptextStepsDescription ) pi->m_ptextStepsDescription->SetText( pSteps->GetDescription() ); @@ -1285,10 +1285,10 @@ void ScreenGameplay::LoadLights() // First, check if the song has explicit lights m_CabinetLightsNoteData.Init(); - ASSERT( GAMESTATE->m_pCurSong != NULL ); + ASSERT( GAMESTATE->m_pCurSong != nullptr ); const Steps *pSteps = SongUtil::GetClosestNotes( GAMESTATE->m_pCurSong, StepsType_lights_cabinet, Difficulty_Medium ); - if( pSteps != NULL ) + if( pSteps != nullptr ) { pSteps->GetNoteData( m_CabinetLightsNoteData ); return; @@ -1751,7 +1751,7 @@ void ScreenGameplay::Update( float fDeltaTime ) DancingCharacters *pCharacter = NULL; if( m_pSongBackground ) pCharacter = m_pSongBackground->GetDancingCharacters(); - if( pCharacter != NULL ) + if( pCharacter != nullptr ) { TapNoteScore tns = pi->m_pPlayer->GetLastTapNoteScore(); diff --git a/src/ScreenGameplayLesson.cpp b/src/ScreenGameplayLesson.cpp index e7e2faa1d1..636436678c 100644 --- a/src/ScreenGameplayLesson.cpp +++ b/src/ScreenGameplayLesson.cpp @@ -15,8 +15,8 @@ ScreenGameplayLesson::ScreenGameplayLesson() void ScreenGameplayLesson::Init() { - ASSERT( GAMESTATE->GetCurrentStyle() != NULL ); - ASSERT( GAMESTATE->m_pCurSong != NULL ); + ASSERT( GAMESTATE->GetCurrentStyle() != nullptr ); + ASSERT( GAMESTATE->m_pCurSong != nullptr ); /* Now that we've set up, init the base class. */ ScreenGameplayNormal::Init(); diff --git a/src/ScreenHighScores.cpp b/src/ScreenHighScores.cpp index 25a2d98f9f..d6130a5a88 100644 --- a/src/ScreenHighScores.cpp +++ b/src/ScreenHighScores.cpp @@ -116,9 +116,9 @@ void ScoreScroller::ConfigureActor( Actor *pActor, int iItem ) const ScoreRowItemData &data = m_vScoreRowItemData[iItem]; Message msg("Set"); - if( data.m_pSong != NULL ) + if( data.m_pSong != nullptr ) msg.SetParam( "Song", data.m_pSong ); - if( data.m_pCourse != NULL ) + if( data.m_pCourse != nullptr ) msg.SetParam( "Course", data.m_pCourse ); @@ -133,7 +133,7 @@ void ScoreScroller::ConfigureActor( Actor *pActor, int iItem ) Difficulty dc = iter.first; StepsType st = iter.second; - if( data.m_pSong != NULL ) + if( data.m_pSong != nullptr ) { const Song* pSong = data.m_pSong; Steps *pSteps = SongUtil::GetStepsByDifficulty( pSong, st, dc, false ); @@ -141,7 +141,7 @@ void ScoreScroller::ConfigureActor( Actor *pActor, int iItem ) pSteps = NULL; LuaHelpers::Push( L, pSteps ); } - else if( data.m_pCourse != NULL ) + else if( data.m_pCourse != nullptr ) { const Course* pCourse = data.m_pCourse; Trail *pTrail = pCourse->GetTrail( st, dc ); diff --git a/src/ScreenHowToPlay.cpp b/src/ScreenHowToPlay.cpp index 2daf79084e..37d61ea05b 100644 --- a/src/ScreenHowToPlay.cpp +++ b/src/ScreenHowToPlay.cpp @@ -149,7 +149,7 @@ void ScreenHowToPlay::Init() const Style* pStyle = GAMESTATE->GetCurrentStyle(); Steps *pSteps = SongUtil::GetClosestNotes( &m_Song, pStyle->m_StepsType, Difficulty_Beginner ); - ASSERT_M( pSteps != NULL, ssprintf("No playable steps of StepsType '%s' for ScreenHowToPlay", StringConversion::ToString(pStyle->m_StepsType).c_str()) ); + ASSERT_M( pSteps != nullptr, ssprintf("No playable steps of StepsType '%s' for ScreenHowToPlay", StringConversion::ToString(pStyle->m_StepsType).c_str()) ); m_Song.m_SongTiming.TidyUpData( false ); pSteps->m_Timing.TidyUpData( true ); @@ -248,7 +248,7 @@ void ScreenHowToPlay::Step() void ScreenHowToPlay::Update( float fDelta ) { - if( GAMESTATE->m_pCurSong != NULL ) + if( GAMESTATE->m_pCurSong != nullptr ) { RageTimer tm; GAMESTATE->UpdateSongPosition( m_fFakeSecondsIntoSong, GAMESTATE->m_pCurSong->m_SongTiming, tm, true ); diff --git a/src/ScreenJukebox.cpp b/src/ScreenJukebox.cpp index fef90f90c5..615cc694a7 100644 --- a/src/ScreenJukebox.cpp +++ b/src/ScreenJukebox.cpp @@ -34,7 +34,7 @@ void ScreenJukebox::SetSong() /* Check to see if there is a theme course. If there is a course that has * the exact same name as the theme, then we pick a song from this course. */ Course *pCourse = SONGMAN->GetCourseFromName( THEME->GetCurThemeName() ); - if( pCourse != NULL ) + if( pCourse != nullptr ) for ( unsigned i = 0; i < pCourse->m_vEntries.size(); i++ ) if( pCourse->m_vEntries[i].IsFixedSong() ) vSongs.push_back( pCourse->m_vEntries[i].songID.ToSong() ); @@ -73,7 +73,7 @@ void ScreenJukebox::SetSong() Song* pSong = vSongs[RandomInt(vSongs.size())]; - ASSERT( pSong != NULL ); + ASSERT( pSong != nullptr ); if( !pSong->HasMusic() ) continue; // skip if( !pSong->NormallyDisplayed() ) @@ -152,7 +152,7 @@ void ScreenJukebox::SetSong() FOREACH_PlayerNumber( p ) { GAMESTATE->m_pCurTrail[p].Set( lCourse->GetTrail( GAMESTATE->GetCurrentStyle()->m_StepsType ) ); - ASSERT( GAMESTATE->m_pCurTrail[p] != NULL ); + ASSERT( GAMESTATE->m_pCurTrail[p] != nullptr ); } } } @@ -172,7 +172,7 @@ ScreenJukebox::ScreenJukebox() void ScreenJukebox::Init() { // ScreenJukeboxMenu must set this - ASSERT( GAMESTATE->GetCurrentStyle() != NULL ); + ASSERT( GAMESTATE->GetCurrentStyle() != nullptr ); GAMESTATE->m_PlayMode.Set( PLAY_MODE_REGULAR ); SetSong(); diff --git a/src/ScreenManager.cpp b/src/ScreenManager.cpp index 084c673bc2..17cafd1370 100644 --- a/src/ScreenManager.cpp +++ b/src/ScreenManager.cpp @@ -770,14 +770,14 @@ void ScreenManager::PopAllScreens() void ScreenManager::PostMessageToTopScreen( ScreenMessage SM, float fDelay ) { Screen* pTopScreen = GetTopScreen(); - if( pTopScreen != NULL ) + if( pTopScreen != nullptr ) pTopScreen->PostScreenMessage( SM, fDelay ); } void ScreenManager::SendMessageToTopScreen( ScreenMessage SM ) { Screen* pTopScreen = GetTopScreen(); - if( pTopScreen != NULL ) + if( pTopScreen != nullptr ) pTopScreen->HandleScreenMessage( SM ); } @@ -858,7 +858,7 @@ public: static int GetTopScreen( T* p, lua_State *L ) { Actor *pScreen = p->GetTopScreen(); - if( pScreen != NULL ) + if( pScreen != nullptr ) pScreen->PushSelf(L); else lua_pushnil( L ); diff --git a/src/ScreenMiniMenu.cpp b/src/ScreenMiniMenu.cpp index 56bbc23327..bd93036e33 100644 --- a/src/ScreenMiniMenu.cpp +++ b/src/ScreenMiniMenu.cpp @@ -59,7 +59,7 @@ void ScreenMiniMenu::Init() void ScreenMiniMenu::BeginScreen() { - ASSERT( g_pMenuDef != NULL ); + ASSERT( g_pMenuDef != nullptr ); LoadMenu( g_pMenuDef ); m_SMSendOnOK = g_SendOnOK; diff --git a/src/ScreenNetRoom.cpp b/src/ScreenNetRoom.cpp index 792d9ed7af..b18b84ed31 100644 --- a/src/ScreenNetRoom.cpp +++ b/src/ScreenNetRoom.cpp @@ -1,343 +1,343 @@ -#include "global.h" - -#if !defined(WITHOUT_NETWORKING) -#include "ScreenNetRoom.h" -#include "ScreenManager.h" -#include "NetworkSyncManager.h" -#include "GameState.h" -#include "ThemeManager.h" -#include "ScreenTextEntry.h" -#include "WheelItemBase.h" -#include "InputEventPlus.h" -#include "LocalizedString.h" - -AutoScreenMessage( SM_SMOnlinePack ); -AutoScreenMessage( SM_BackFromRoomName ); -AutoScreenMessage( SM_BackFromRoomDesc ); -AutoScreenMessage( SM_BackFromRoomPass ); -AutoScreenMessage( SM_BackFromReqPass ); -AutoScreenMessage( SM_RoomInfoRetract ); -AutoScreenMessage( SM_RoomInfoDeploy ); - -static LocalizedString ENTER_ROOM_DESCRIPTION ("ScreenNetRoom","Enter a description for the room:"); -static LocalizedString ENTER_ROOM_PASSWORD ("ScreenNetRoom","Enter a password for the room (blank, no password):"); -static LocalizedString ENTER_ROOM_REQPASSWORD ("ScreenNetRoom","Enter Room's Password:"); - -REGISTER_SCREEN_CLASS( ScreenNetRoom ); - -void ScreenNetRoom::Init() -{ - GAMESTATE->FinishStage(); - - ScreenNetSelectBase::Init(); - - m_soundChangeSel.Load( THEME->GetPathS("ScreenNetRoom","change sel") ); - - m_iRoomPlace = 0; - - m_RoomWheel.SetName( "RoomWheel" ); - m_RoomWheel.Load( "RoomWheel" ); - m_RoomWheel.BeginScreen(); - LOAD_ALL_COMMANDS_AND_SET_XY_AND_ON_COMMAND( m_RoomWheel ); - this->AddChild( &m_RoomWheel ); - - // Since the room info display does not start active, and it is activated by - // code elsewhere, it should not be put on screen to begin with. - m_roomInfo.SetName( "RoomInfoDisplay" ); - m_roomInfo.Load( "RoomInfoDisplay" ); - m_roomInfo.SetDrawOrder( 1 ); - this->AddChild( &m_roomInfo ); - - this->SortByDrawOrder(); - NSMAN->ReportNSSOnOff( 7 ); -} - -bool ScreenNetRoom::Input( const InputEventPlus &input ) -{ - if( (input.MenuI == GAME_BUTTON_LEFT || input.MenuI == GAME_BUTTON_RIGHT) && input.type == IET_RELEASE ) - m_RoomWheel.Move( 0 ); - - return ScreenNetSelectBase::Input( input ); -} - -void ScreenNetRoom::HandleScreenMessage( const ScreenMessage SM ) -{ - if( SM == SM_GoToPrevScreen ) - { - SCREENMAN->SetNewScreen( THEME->GetMetric (m_sName, "PrevScreen") ); - } - else if( SM == SM_GoToNextScreen ) - { - SCREENMAN->SetNewScreen( THEME->GetMetric (m_sName, "NextScreen") ); - } - else if( SM == SM_BackFromReqPass ) - { - if ( !ScreenTextEntry::s_bCancelledLast ) - { - NSMAN->m_SMOnlinePacket.ClearPacket(); - NSMAN->m_SMOnlinePacket.Write1( 1 ); - NSMAN->m_SMOnlinePacket.Write1( 1 ); //Type (enter a room) - NSMAN->m_SMOnlinePacket.WriteNT( m_sLastPickedRoom ); - NSMAN->m_SMOnlinePacket.WriteNT( ScreenTextEntry::s_sLastAnswer ); - NSMAN->SendSMOnline( ); - } - } - else if( SM == SM_SMOnlinePack ) - { - switch( NSMAN->m_SMOnlinePacket.Read1() ) - { - case 1: - switch ( NSMAN->m_SMOnlinePacket.Read1() ) - { - case 0: //Room title Change - { - RString title, subtitle; - title = NSMAN->m_SMOnlinePacket.ReadNT(); - subtitle = NSMAN->m_SMOnlinePacket.ReadNT(); - - Message msg( MessageIDToString(Message_UpdateScreenHeader) ); - msg.SetParam( "Header", title ); - msg.SetParam( "Subheader", subtitle ); - MESSAGEMAN->Broadcast( msg ); - - if ( NSMAN->m_SMOnlinePacket.Read1() != 0 ) - { - RString SMOnlineSelectScreen = THEME->GetMetric( m_sName, "MusicSelectScreen" ); - SCREENMAN->SetNewScreen( SMOnlineSelectScreen ); - } - } - case 1: //Rooms list change - { - int numRooms = NSMAN->m_SMOnlinePacket.Read1(); - m_Rooms.clear(); - for( int i=0; im_SMOnlinePacket.ReadNT() ); - tmpRoomData.SetDescription( NSMAN->m_SMOnlinePacket.ReadNT() ); - m_Rooms.push_back( tmpRoomData ); - } - //Abide by protocol and read room status - for( int i=0; im_SMOnlinePacket.Read1() ); - - for( int i=0; im_SMOnlinePacket.Read1() ); - - if( m_iRoomPlace<0 ) - m_iRoomPlace=0; - if( m_iRoomPlace >= (int) m_Rooms.size() ) - m_iRoomPlace=m_Rooms.size()-1; - UpdateRoomsList(); - } - } - break; - case 3: - RoomInfo info; - info.songTitle = NSMAN->m_SMOnlinePacket.ReadNT(); - info.songSubTitle = NSMAN->m_SMOnlinePacket.ReadNT(); - info.songArtist = NSMAN->m_SMOnlinePacket.ReadNT(); - info.numPlayers = NSMAN->m_SMOnlinePacket.Read1(); - info.maxPlayers = NSMAN->m_SMOnlinePacket.Read1(); - info.players.resize( info.numPlayers ); - for( int i = 0; i < info.numPlayers; ++i ) - info.players[i] = NSMAN->m_SMOnlinePacket.ReadNT(); - - m_roomInfo.SetRoomInfo( info ); - break; - } - } - else if ( SM == SM_BackFromRoomName ) - { - if ( !ScreenTextEntry::s_bCancelledLast ) - { - m_newRoomName = ScreenTextEntry::s_sLastAnswer; - ScreenTextEntry::TextEntry( SM_BackFromRoomDesc, ENTER_ROOM_DESCRIPTION, "", 255 ); - } - } - else if( SM == SM_BackFromRoomDesc ) - { - if ( !ScreenTextEntry::s_bCancelledLast ) - { - m_newRoomDesc = ScreenTextEntry::s_sLastAnswer; - ScreenTextEntry::TextEntry( SM_BackFromRoomPass, ENTER_ROOM_PASSWORD, "", 255 ); - } - } - else if( SM == SM_BackFromRoomPass ) - { - if ( !ScreenTextEntry::s_bCancelledLast ) - { - m_newRoomPass = ScreenTextEntry::s_sLastAnswer; - CreateNewRoom( m_newRoomName, m_newRoomDesc, m_newRoomPass); - } - } - else if ( SM == SM_RoomInfoRetract ) - { - m_roomInfo.RetractInfoBox(); - } - else if ( SM == SM_RoomInfoDeploy ) - { - int i = m_RoomWheel.GetCurrentIndex() - m_RoomWheel.GetPerminateOffset(); - const RoomWheelItemData* data = m_RoomWheel.GetItem(i); - if( data != NULL ) - m_roomInfo.SetRoom( data ); - } - - ScreenNetSelectBase::HandleScreenMessage( SM ); -} - -void ScreenNetRoom::TweenOffScreen() -{ - NSMAN->ReportNSSOnOff( 6 ); -} - -bool ScreenNetRoom::MenuStart( const InputEventPlus &input ) -{ - m_RoomWheel.Select(); - RoomWheelItemData* rwd = dynamic_cast( m_RoomWheel.LastSelected() ); - if( rwd ) - { - if ( rwd->m_iFlags % 2 ) - { - m_sLastPickedRoom = rwd->m_sText; - ScreenTextEntry::TextEntry( SM_BackFromReqPass, ENTER_ROOM_REQPASSWORD, "", 255 ); - } - else - { - NSMAN->m_SMOnlinePacket.ClearPacket(); - NSMAN->m_SMOnlinePacket.Write1( 1 ); - NSMAN->m_SMOnlinePacket.Write1( 1 ); //Type (enter a room) - NSMAN->m_SMOnlinePacket.WriteNT( rwd->m_sText ); - NSMAN->SendSMOnline( ); - } - } - ScreenNetSelectBase::MenuStart( input ); - return true; -} - -bool ScreenNetRoom::MenuBack( const InputEventPlus &input ) -{ - TweenOffScreen(); - - Cancel( SM_GoToPrevScreen ); - - ScreenNetSelectBase::MenuBack( input ); - return true; -} - -bool ScreenNetRoom::MenuLeft( const InputEventPlus &input ) -{ - bool bHandled = false; - if( input.type == IET_FIRST_PRESS ) - { - m_RoomWheel.Move( -1 ); - bHandled = true; - } - - return ScreenNetSelectBase::MenuLeft( input ) || bHandled; -} - -bool ScreenNetRoom::MenuRight( const InputEventPlus &input ) -{ - bool bHandled = false; - if( input.type == IET_FIRST_PRESS ) - { - m_RoomWheel.Move( 1 ); - bHandled = true; - } - - return ScreenNetSelectBase::MenuRight( input ) || bHandled; -} - -void ScreenNetRoom::UpdateRoomsList() -{ - int difference = 0; - RoomWheelItemData* itemData = NULL; - - difference = m_RoomWheel.GetNumItems() - m_Rooms.size(); - - if( !m_RoomWheel.IsEmpty() ) - { - if( difference > 0 ) - for( int x = 0; x < difference; ++x ) - m_RoomWheel.RemoveItem( m_RoomWheel.GetNumItems() - 1 ); - else - { - difference = abs( difference ); - for( int x = 0; x < difference; ++x ) - m_RoomWheel.AddItem( new RoomWheelItemData(WheelItemDataType_Generic, "", "", RageColor(1,1,1,1)) ); - } - } - else - { - for ( unsigned int x = 0; x < m_Rooms.size(); ++x) - m_RoomWheel.AddItem( new RoomWheelItemData(WheelItemDataType_Generic, "", "", RageColor(1,1,1,1)) ); - } - - for( unsigned int i = 0; i < m_Rooms.size(); ++i ) - { - itemData = m_RoomWheel.GetItem( i ); - - itemData->m_sText = m_Rooms[i].Name(); - itemData->m_sDesc = m_Rooms[i].Description(); - itemData->m_iFlags = m_Rooms[i].GetFlags(); - - switch( m_Rooms[i].State() ) - { - case 0: - itemData->m_color = THEME->GetMetricC( m_sName, "OpenRoomColor"); - break; - case 2: - itemData->m_color = THEME->GetMetricC( m_sName, "InGameRoomColor"); - break; - default: - itemData->m_color = THEME->GetMetricC( m_sName, "OpenRoomColor"); - break; - } - - if ( m_Rooms[i].GetFlags() % 2 ) - itemData->m_color = THEME->GetMetricC( m_sName, "PasswdRoomColor"); - } - - m_RoomWheel.RebuildWheelItems(); -} - -void ScreenNetRoom::CreateNewRoom( const RString& rName, const RString& rDesc, const RString& rPass ) -{ - NSMAN->m_SMOnlinePacket.ClearPacket(); - NSMAN->m_SMOnlinePacket.Write1( (uint8_t)2 ); // Create room command - NSMAN->m_SMOnlinePacket.Write1( 1 ); // Type game room - NSMAN->m_SMOnlinePacket.WriteNT( rName ); - NSMAN->m_SMOnlinePacket.WriteNT( rDesc ); - if ( !rPass.empty() ) - NSMAN->m_SMOnlinePacket.WriteNT( rPass ); - NSMAN->SendSMOnline( ); -} - -#endif - -/* - * (c) 2004 Charles Lohr, Josh Allen - * (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. - */ +#include "global.h" + +#if !defined(WITHOUT_NETWORKING) +#include "ScreenNetRoom.h" +#include "ScreenManager.h" +#include "NetworkSyncManager.h" +#include "GameState.h" +#include "ThemeManager.h" +#include "ScreenTextEntry.h" +#include "WheelItemBase.h" +#include "InputEventPlus.h" +#include "LocalizedString.h" + +AutoScreenMessage( SM_SMOnlinePack ); +AutoScreenMessage( SM_BackFromRoomName ); +AutoScreenMessage( SM_BackFromRoomDesc ); +AutoScreenMessage( SM_BackFromRoomPass ); +AutoScreenMessage( SM_BackFromReqPass ); +AutoScreenMessage( SM_RoomInfoRetract ); +AutoScreenMessage( SM_RoomInfoDeploy ); + +static LocalizedString ENTER_ROOM_DESCRIPTION ("ScreenNetRoom","Enter a description for the room:"); +static LocalizedString ENTER_ROOM_PASSWORD ("ScreenNetRoom","Enter a password for the room (blank, no password):"); +static LocalizedString ENTER_ROOM_REQPASSWORD ("ScreenNetRoom","Enter Room's Password:"); + +REGISTER_SCREEN_CLASS( ScreenNetRoom ); + +void ScreenNetRoom::Init() +{ + GAMESTATE->FinishStage(); + + ScreenNetSelectBase::Init(); + + m_soundChangeSel.Load( THEME->GetPathS("ScreenNetRoom","change sel") ); + + m_iRoomPlace = 0; + + m_RoomWheel.SetName( "RoomWheel" ); + m_RoomWheel.Load( "RoomWheel" ); + m_RoomWheel.BeginScreen(); + LOAD_ALL_COMMANDS_AND_SET_XY_AND_ON_COMMAND( m_RoomWheel ); + this->AddChild( &m_RoomWheel ); + + // Since the room info display does not start active, and it is activated by + // code elsewhere, it should not be put on screen to begin with. + m_roomInfo.SetName( "RoomInfoDisplay" ); + m_roomInfo.Load( "RoomInfoDisplay" ); + m_roomInfo.SetDrawOrder( 1 ); + this->AddChild( &m_roomInfo ); + + this->SortByDrawOrder(); + NSMAN->ReportNSSOnOff( 7 ); +} + +bool ScreenNetRoom::Input( const InputEventPlus &input ) +{ + if( (input.MenuI == GAME_BUTTON_LEFT || input.MenuI == GAME_BUTTON_RIGHT) && input.type == IET_RELEASE ) + m_RoomWheel.Move( 0 ); + + return ScreenNetSelectBase::Input( input ); +} + +void ScreenNetRoom::HandleScreenMessage( const ScreenMessage SM ) +{ + if( SM == SM_GoToPrevScreen ) + { + SCREENMAN->SetNewScreen( THEME->GetMetric (m_sName, "PrevScreen") ); + } + else if( SM == SM_GoToNextScreen ) + { + SCREENMAN->SetNewScreen( THEME->GetMetric (m_sName, "NextScreen") ); + } + else if( SM == SM_BackFromReqPass ) + { + if ( !ScreenTextEntry::s_bCancelledLast ) + { + NSMAN->m_SMOnlinePacket.ClearPacket(); + NSMAN->m_SMOnlinePacket.Write1( 1 ); + NSMAN->m_SMOnlinePacket.Write1( 1 ); //Type (enter a room) + NSMAN->m_SMOnlinePacket.WriteNT( m_sLastPickedRoom ); + NSMAN->m_SMOnlinePacket.WriteNT( ScreenTextEntry::s_sLastAnswer ); + NSMAN->SendSMOnline( ); + } + } + else if( SM == SM_SMOnlinePack ) + { + switch( NSMAN->m_SMOnlinePacket.Read1() ) + { + case 1: + switch ( NSMAN->m_SMOnlinePacket.Read1() ) + { + case 0: //Room title Change + { + RString title, subtitle; + title = NSMAN->m_SMOnlinePacket.ReadNT(); + subtitle = NSMAN->m_SMOnlinePacket.ReadNT(); + + Message msg( MessageIDToString(Message_UpdateScreenHeader) ); + msg.SetParam( "Header", title ); + msg.SetParam( "Subheader", subtitle ); + MESSAGEMAN->Broadcast( msg ); + + if ( NSMAN->m_SMOnlinePacket.Read1() != 0 ) + { + RString SMOnlineSelectScreen = THEME->GetMetric( m_sName, "MusicSelectScreen" ); + SCREENMAN->SetNewScreen( SMOnlineSelectScreen ); + } + } + case 1: //Rooms list change + { + int numRooms = NSMAN->m_SMOnlinePacket.Read1(); + m_Rooms.clear(); + for( int i=0; im_SMOnlinePacket.ReadNT() ); + tmpRoomData.SetDescription( NSMAN->m_SMOnlinePacket.ReadNT() ); + m_Rooms.push_back( tmpRoomData ); + } + //Abide by protocol and read room status + for( int i=0; im_SMOnlinePacket.Read1() ); + + for( int i=0; im_SMOnlinePacket.Read1() ); + + if( m_iRoomPlace<0 ) + m_iRoomPlace=0; + if( m_iRoomPlace >= (int) m_Rooms.size() ) + m_iRoomPlace=m_Rooms.size()-1; + UpdateRoomsList(); + } + } + break; + case 3: + RoomInfo info; + info.songTitle = NSMAN->m_SMOnlinePacket.ReadNT(); + info.songSubTitle = NSMAN->m_SMOnlinePacket.ReadNT(); + info.songArtist = NSMAN->m_SMOnlinePacket.ReadNT(); + info.numPlayers = NSMAN->m_SMOnlinePacket.Read1(); + info.maxPlayers = NSMAN->m_SMOnlinePacket.Read1(); + info.players.resize( info.numPlayers ); + for( int i = 0; i < info.numPlayers; ++i ) + info.players[i] = NSMAN->m_SMOnlinePacket.ReadNT(); + + m_roomInfo.SetRoomInfo( info ); + break; + } + } + else if ( SM == SM_BackFromRoomName ) + { + if ( !ScreenTextEntry::s_bCancelledLast ) + { + m_newRoomName = ScreenTextEntry::s_sLastAnswer; + ScreenTextEntry::TextEntry( SM_BackFromRoomDesc, ENTER_ROOM_DESCRIPTION, "", 255 ); + } + } + else if( SM == SM_BackFromRoomDesc ) + { + if ( !ScreenTextEntry::s_bCancelledLast ) + { + m_newRoomDesc = ScreenTextEntry::s_sLastAnswer; + ScreenTextEntry::TextEntry( SM_BackFromRoomPass, ENTER_ROOM_PASSWORD, "", 255 ); + } + } + else if( SM == SM_BackFromRoomPass ) + { + if ( !ScreenTextEntry::s_bCancelledLast ) + { + m_newRoomPass = ScreenTextEntry::s_sLastAnswer; + CreateNewRoom( m_newRoomName, m_newRoomDesc, m_newRoomPass); + } + } + else if ( SM == SM_RoomInfoRetract ) + { + m_roomInfo.RetractInfoBox(); + } + else if ( SM == SM_RoomInfoDeploy ) + { + int i = m_RoomWheel.GetCurrentIndex() - m_RoomWheel.GetPerminateOffset(); + const RoomWheelItemData* data = m_RoomWheel.GetItem(i); + if( data != nullptr ) + m_roomInfo.SetRoom( data ); + } + + ScreenNetSelectBase::HandleScreenMessage( SM ); +} + +void ScreenNetRoom::TweenOffScreen() +{ + NSMAN->ReportNSSOnOff( 6 ); +} + +bool ScreenNetRoom::MenuStart( const InputEventPlus &input ) +{ + m_RoomWheel.Select(); + RoomWheelItemData* rwd = dynamic_cast( m_RoomWheel.LastSelected() ); + if( rwd ) + { + if ( rwd->m_iFlags % 2 ) + { + m_sLastPickedRoom = rwd->m_sText; + ScreenTextEntry::TextEntry( SM_BackFromReqPass, ENTER_ROOM_REQPASSWORD, "", 255 ); + } + else + { + NSMAN->m_SMOnlinePacket.ClearPacket(); + NSMAN->m_SMOnlinePacket.Write1( 1 ); + NSMAN->m_SMOnlinePacket.Write1( 1 ); //Type (enter a room) + NSMAN->m_SMOnlinePacket.WriteNT( rwd->m_sText ); + NSMAN->SendSMOnline( ); + } + } + ScreenNetSelectBase::MenuStart( input ); + return true; +} + +bool ScreenNetRoom::MenuBack( const InputEventPlus &input ) +{ + TweenOffScreen(); + + Cancel( SM_GoToPrevScreen ); + + ScreenNetSelectBase::MenuBack( input ); + return true; +} + +bool ScreenNetRoom::MenuLeft( const InputEventPlus &input ) +{ + bool bHandled = false; + if( input.type == IET_FIRST_PRESS ) + { + m_RoomWheel.Move( -1 ); + bHandled = true; + } + + return ScreenNetSelectBase::MenuLeft( input ) || bHandled; +} + +bool ScreenNetRoom::MenuRight( const InputEventPlus &input ) +{ + bool bHandled = false; + if( input.type == IET_FIRST_PRESS ) + { + m_RoomWheel.Move( 1 ); + bHandled = true; + } + + return ScreenNetSelectBase::MenuRight( input ) || bHandled; +} + +void ScreenNetRoom::UpdateRoomsList() +{ + int difference = 0; + RoomWheelItemData* itemData = NULL; + + difference = m_RoomWheel.GetNumItems() - m_Rooms.size(); + + if( !m_RoomWheel.IsEmpty() ) + { + if( difference > 0 ) + for( int x = 0; x < difference; ++x ) + m_RoomWheel.RemoveItem( m_RoomWheel.GetNumItems() - 1 ); + else + { + difference = abs( difference ); + for( int x = 0; x < difference; ++x ) + m_RoomWheel.AddItem( new RoomWheelItemData(WheelItemDataType_Generic, "", "", RageColor(1,1,1,1)) ); + } + } + else + { + for ( unsigned int x = 0; x < m_Rooms.size(); ++x) + m_RoomWheel.AddItem( new RoomWheelItemData(WheelItemDataType_Generic, "", "", RageColor(1,1,1,1)) ); + } + + for( unsigned int i = 0; i < m_Rooms.size(); ++i ) + { + itemData = m_RoomWheel.GetItem( i ); + + itemData->m_sText = m_Rooms[i].Name(); + itemData->m_sDesc = m_Rooms[i].Description(); + itemData->m_iFlags = m_Rooms[i].GetFlags(); + + switch( m_Rooms[i].State() ) + { + case 0: + itemData->m_color = THEME->GetMetricC( m_sName, "OpenRoomColor"); + break; + case 2: + itemData->m_color = THEME->GetMetricC( m_sName, "InGameRoomColor"); + break; + default: + itemData->m_color = THEME->GetMetricC( m_sName, "OpenRoomColor"); + break; + } + + if ( m_Rooms[i].GetFlags() % 2 ) + itemData->m_color = THEME->GetMetricC( m_sName, "PasswdRoomColor"); + } + + m_RoomWheel.RebuildWheelItems(); +} + +void ScreenNetRoom::CreateNewRoom( const RString& rName, const RString& rDesc, const RString& rPass ) +{ + NSMAN->m_SMOnlinePacket.ClearPacket(); + NSMAN->m_SMOnlinePacket.Write1( (uint8_t)2 ); // Create room command + NSMAN->m_SMOnlinePacket.Write1( 1 ); // Type game room + NSMAN->m_SMOnlinePacket.WriteNT( rName ); + NSMAN->m_SMOnlinePacket.WriteNT( rDesc ); + if ( !rPass.empty() ) + NSMAN->m_SMOnlinePacket.WriteNT( rPass ); + NSMAN->SendSMOnline( ); +} + +#endif + +/* + * (c) 2004 Charles Lohr, Josh Allen + * (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. + */ diff --git a/src/ScreenNetSelectBase.cpp b/src/ScreenNetSelectBase.cpp index 509f3dc38c..cbc674c5fb 100644 --- a/src/ScreenNetSelectBase.cpp +++ b/src/ScreenNetSelectBase.cpp @@ -1,460 +1,460 @@ -#include "global.h" - -#if !defined(WITHOUT_NETWORKING) -#include "ScreenNetSelectBase.h" -#include "ScreenManager.h" -#include "ThemeManager.h" -#include "RageTimer.h" -#include "ActorUtil.h" -#include "Actor.h" -#include "GameSoundManager.h" -#include "MenuTimer.h" -#include "NetworkSyncManager.h" -#include "RageUtil.h" -#include "GameState.h" -#include "InputEventPlus.h" -#include "RageInput.h" -#include "Font.h" -#include "RageDisplay.h" - -#define CHAT_TEXT_OUTPUT_WIDTH THEME->GetMetricF(m_sName,"ChatTextOutputWidth") -#define CHAT_TEXT_INPUT_WIDTH THEME->GetMetricF(m_sName,"ChatTextInputWidth") -#define SHOW_CHAT_LINES THEME->GetMetricI(m_sName,"ChatOutputLines") - -#define USERS_X THEME->GetMetricF(m_sName,"UsersX") -#define USERS_Y THEME->GetMetricF(m_sName,"UsersY") -#define USER_SPACING_X THEME->GetMetricF(m_sName,"UserSpacingX") -#define USER_ADD_Y THEME->GetMetricF(m_sName,"UserLine2Y") - -AutoScreenMessage( SM_AddToChat ); -AutoScreenMessage( SM_UsersUpdate ); -AutoScreenMessage( SM_SMOnlinePack ); - -REGISTER_SCREEN_CLASS( ScreenNetSelectBase ); - -void ScreenNetSelectBase::Init() -{ - ScreenWithMenuElements::Init(); - - // Chat boxes - m_sprChatInputBox.Load( THEME->GetPathG( m_sName, "ChatInputBox" ) ); - m_sprChatInputBox->SetName( "ChatInputBox" ); - LOAD_ALL_COMMANDS_AND_SET_XY_AND_ON_COMMAND( m_sprChatInputBox ); - this->AddChild( m_sprChatInputBox ); - - m_sprChatOutputBox.Load( THEME->GetPathG( m_sName, "ChatOutputBox" ) ); - m_sprChatOutputBox->SetName( "ChatOutputBox" ); - LOAD_ALL_COMMANDS_AND_SET_XY_AND_ON_COMMAND( m_sprChatOutputBox ); - this->AddChild( m_sprChatOutputBox ); - - m_textChatInput.LoadFromFont( THEME->GetPathF(m_sName,"chat") ); - m_textChatInput.SetName( "ChatInput" ); - m_textChatInput.SetWrapWidthPixels( (int)(CHAT_TEXT_INPUT_WIDTH) ); - LOAD_ALL_COMMANDS_AND_SET_XY_AND_ON_COMMAND( m_textChatInput ); - this->AddChild( &m_textChatInput ); - - m_textChatOutput.LoadFromFont( THEME->GetPathF(m_sName,"chat") ); - m_textChatOutput.SetName( "ChatOutput" ); - m_textChatOutput.SetWrapWidthPixels( (int)(CHAT_TEXT_OUTPUT_WIDTH) ); - LOAD_ALL_COMMANDS_AND_SET_XY_AND_ON_COMMAND( m_textChatOutput ); - this->AddChild( &m_textChatOutput ); - - m_textChatOutput.SetText( NSMAN->m_sChatText ); - m_textChatOutput.SetMaxLines( SHOW_CHAT_LINES, 1 ); - - //Display users list - UpdateUsers(); - - return; -} - -bool ScreenNetSelectBase::Input( const InputEventPlus &input ) -{ - if( m_In.IsTransitioning() || m_Out.IsTransitioning() ) - return false; - - if( input.type != IET_FIRST_PRESS && input.type != IET_REPEAT ) - return false; - - bool bHoldingCtrl = - INPUTFILTER->IsBeingPressed(DeviceInput(DEVICE_KEYBOARD, KEY_LCTRL)) || - INPUTFILTER->IsBeingPressed(DeviceInput(DEVICE_KEYBOARD, KEY_RCTRL)) || - (!NSMAN->useSMserver); // If we are disconnected, assume no chatting. - - switch( input.DeviceI.button ) - { - case KEY_ENTER: - case KEY_KP_ENTER: - if (!bHoldingCtrl) - { - if ( m_sTextInput != "" ) - NSMAN->SendChat( m_sTextInput ); - m_sTextInput=""; - UpdateTextInput(); - return true; - } - break; - case KEY_BACK: - if(!m_sTextInput.empty()) - m_sTextInput = m_sTextInput.erase( m_sTextInput.size()-1 ); - UpdateTextInput(); - break; - default: - wchar_t c; - c = INPUTMAN->DeviceInputToChar(input.DeviceI, true); - - if( (c >= ' ') && (!bHoldingCtrl) ) - { - m_sTextInput += WStringToRString(wstring()+c); - UpdateTextInput(); - } - - // Tricky: If both players are playing, allow the 2 button through to - // the keymapper. (who? -aj) - // What purpose does this serve? -aj - if( c == '2' && GAMESTATE->IsPlayerEnabled( PLAYER_2 ) && GAMESTATE->IsPlayerEnabled( PLAYER_1 ) ) - break; - - if( c >= ' ' ) - return true; - break; - } - return Screen::Input( input ); -} - -void ScreenNetSelectBase::HandleScreenMessage( const ScreenMessage SM ) -{ - if( SM == SM_GoToNextScreen ) - SOUND->StopMusic(); - else if( SM == SM_AddToChat ) - { - m_textChatOutput.SetText( NSMAN->m_sChatText ); - m_textChatOutput.SetMaxLines( SHOW_CHAT_LINES, 1 ); - } - else if( SM == SM_UsersUpdate ) - { - UpdateUsers(); - } - - ScreenWithMenuElements::HandleScreenMessage( SM ); -} - -void ScreenNetSelectBase::TweenOffScreen() -{ - OFF_COMMAND( m_sprChatInputBox ); - OFF_COMMAND( m_sprChatOutputBox ); - OFF_COMMAND( m_textChatInput ); - OFF_COMMAND( m_textChatOutput ); - - for( unsigned i=0; iRemoveChild( &m_textUsers[i] ); - - m_textUsers.clear(); - - m_textUsers.resize( NSMAN->m_ActivePlayer.size() ); - - for( unsigned i=0; i < NSMAN->m_ActivePlayer.size(); i++) - { - m_textUsers[i].LoadFromFont( THEME->GetPathF(m_sName,"chat") ); - m_textUsers[i].SetHorizAlign( align_center ); - m_textUsers[i].SetVertAlign( align_top ); - m_textUsers[i].SetShadowLength( 0 ); - m_textUsers[i].SetName( "Users" ); - - tX += USER_SPACING_X; - - if ( (i % 2) == 1) - tY = USER_ADD_Y + USERS_Y; - else - tY = USERS_Y; - m_textUsers[i].SetXY( tX, tY ); - - ActorUtil::LoadAllCommands( m_textUsers[i], m_sName ); - ActorUtil::OnCommand( m_textUsers[i] ); - - m_textUsers[i].SetText( NSMAN->m_PlayerNames[NSMAN->m_ActivePlayer[i]] ); - m_textUsers[i].RunCommands( THEME->GetMetricA( m_sName, - ssprintf("Users%dCommand", NSMAN->m_PlayerStatus[NSMAN->m_ActivePlayer[i]] ) ) ); - - this->AddChild( &m_textUsers[i] ); - } -} - -/** ColorBitmapText ***********************************************************/ -void ColorBitmapText::SetText( const RString& _sText, const RString& _sAlternateText, int iWrapWidthPixels ) -{ - ASSERT( m_pFont != NULL ); - - RString sNewText = StringWillUseAlternate(_sText,_sAlternateText) ? _sAlternateText : _sText; - - if( iWrapWidthPixels == -1 ) // wrap not specified - iWrapWidthPixels = m_iWrapWidthPixels; - - if( m_sText == sNewText && iWrapWidthPixels==m_iWrapWidthPixels ) - return; - m_sText = sNewText; - m_iWrapWidthPixels = iWrapWidthPixels; - - // Set up the first color. - m_vColors.clear(); - ColorChange change; - change.c = RageColor (1, 1, 1, 1); - change.l = 0; - m_vColors.push_back( change ); - - m_wTextLines.clear(); - - RString sCurrentLine = ""; - int iLineWidth = 0; - - RString sCurrentWord = ""; - int iWordWidth = 0; - int iGlyphsSoFar = 0; - - for( unsigned i = 0; i < m_sText.length(); i++ ) - { - int iCharsLeft = m_sText.length() - i - 1; - - // First: Check for the special (color) case. - - if( m_sText.length() > 8 && i < m_sText.length() - 9 ) - { - RString FirstThree = m_sText.substr( i, 3 ); - if( FirstThree.CompareNoCase("|c0") == 0 && iCharsLeft > 8 ) - { - ColorChange cChange; - unsigned int r, g, b; - sscanf( m_sText.substr( i, 9 ).c_str(), "|%*c0%2x%2x%2x", &r, &g, &b ); - cChange.c = RageColor( r/255.f, g/255.f, b/255.f, 1.f ); - cChange.l = iGlyphsSoFar; - if( iGlyphsSoFar == 0 ) - m_vColors[0] = cChange; - else - m_vColors.push_back( cChange ); - i+=8; - continue; - } - } - - char curChar = m_sText[i]; - int iCharLen = m_pFont->GetLineWidthInSourcePixels( wstring(1, curChar) ); - - switch( curChar ) - { - case ' ': - if( /* iLineWidth == 0 &&*/ iWordWidth == 0 ) - break; - sCurrentLine += sCurrentWord + " "; - iLineWidth += iWordWidth + iCharLen; - sCurrentWord = ""; - iWordWidth = 0; - iGlyphsSoFar++; - break; - case '\n': - if( iLineWidth + iWordWidth > iWrapWidthPixels ) - { - SimpleAddLine( sCurrentLine, iLineWidth ); - if( iWordWidth > 0 ) - iLineWidth = iWordWidth + //Add the width of a space - m_pFont->GetLineWidthInSourcePixels( L" " ); - sCurrentLine = sCurrentWord + " "; - iWordWidth = 0; - sCurrentWord = ""; - iGlyphsSoFar++; - } - else - { - SimpleAddLine( sCurrentLine + sCurrentWord, iLineWidth + iWordWidth ); - sCurrentLine = ""; iLineWidth = 0; - sCurrentWord = ""; iWordWidth = 0; - } - break; - default: - if( iWordWidth + iCharLen > iWrapWidthPixels && iLineWidth == 0 ) - { - SimpleAddLine( sCurrentWord, iWordWidth ); - sCurrentWord = curChar; iWordWidth = iCharLen; - } - else if( iWordWidth + iLineWidth + iCharLen > iWrapWidthPixels ) - { - SimpleAddLine( sCurrentLine, iLineWidth ); - sCurrentLine = ""; - iLineWidth = 0; - sCurrentWord += curChar; - iWordWidth += iCharLen; - } - else - { - sCurrentWord += curChar; - iWordWidth += iCharLen; - } - iGlyphsSoFar++; - break; - } - } - - if( iWordWidth > 0 ) - { - sCurrentLine += sCurrentWord; - iLineWidth += iWordWidth; - } - - if( iLineWidth > 0 ) - SimpleAddLine( sCurrentLine, iLineWidth ); - - BuildChars(); - UpdateBaseZoom(); -} - -void ColorBitmapText::SimpleAddLine( const RString &sAddition, const int iWidthPixels) -{ - m_wTextLines.push_back( RStringToWstring( sAddition ) ); - m_iLineWidths.push_back( iWidthPixels ); -} - -void ColorBitmapText::DrawPrimitives( ) -{ - Actor::SetGlobalRenderStates(); // set Actor-specified render states - DISPLAY->SetTextureMode( TextureUnit_1, TextureMode_Modulate ); - - /* Draw if we're not fully transparent or the zbuffer is enabled */ - if( m_pTempState->diffuse[0].a != 0 ) - { - // render the shadow - if( m_fShadowLengthX != 0 || m_fShadowLengthY != 0 ) - { - DISPLAY->PushMatrix(); - DISPLAY->TranslateWorld( m_fShadowLengthX, m_fShadowLengthY, 0 ); // shift by 5 units - RageColor c = m_ShadowColor; - c.a *= m_pTempState->diffuse[0].a; - for( unsigned i=0; iPopMatrix(); - } - - // render the diffuse pass - int loc = 0, cur = 0; - RageColor c = m_pTempState->diffuse[0]; - - for( unsigned i=0; i m_vColors[cur].l ) - { - c = m_vColors[cur].c; - cur++; - } - } - for( unsigned j=0; j<4; j++ ) - m_aVertices[i+j].c = c; - } - - DrawChars( false ); - } - - // render the glow pass - if( m_pTempState->glow.a > 0.0001f ) - { - DISPLAY->SetTextureMode( TextureUnit_1, TextureMode_Glow ); - - for( unsigned i=0; iglow; - DrawChars( false ); - } -} - -void ColorBitmapText::SetMaxLines( int iNumLines, int iDirection ) -{ - iNumLines = max( 0, iNumLines ); - iNumLines = min( (int)m_wTextLines.size(), iNumLines ); - if( iDirection == 0 ) - { - // Crop all bottom lines - m_wTextLines.resize( iNumLines ); - m_iLineWidths.resize( iNumLines ); - } - else - { - // Because colors are relative to the beginning, we have to crop them back - unsigned shift = 0; - - for( unsigned i = 0; i < m_wTextLines.size() - iNumLines; i++ ) - shift += m_wTextLines[i].length(); - - // When we're cutting out text, we need to maintain the last - // color, so our text at the top doesn't become colorless. - RageColor LastColor; - - for( unsigned i = 0; i < m_vColors.size(); i++ ) - { - m_vColors[i].l -= shift; - if( m_vColors[i].l < 0 ) - { - LastColor = m_vColors[i].c; - m_vColors.erase( m_vColors.begin() + i ); - i--; - } - } - - // If we already have a color set for the first char - // do not override it. - if( m_vColors.size() > 0 && m_vColors[0].l > 0 ) - { - ColorChange tmp; - tmp.c = LastColor; - tmp.l = 0; - m_vColors.insert( m_vColors.begin(), tmp ); - } - - m_wTextLines.erase( m_wTextLines.begin(), m_wTextLines.end() - iNumLines ); - m_iLineWidths.erase( m_iLineWidths.begin(), m_iLineWidths.end() - iNumLines ); - } - BuildChars(); -} - - -#endif - -/* - * (c) 2004 Charles Lohr - * All rights reserved. - * Elements from ScreenTextEntry - * - * 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. - */ +#include "global.h" + +#if !defined(WITHOUT_NETWORKING) +#include "ScreenNetSelectBase.h" +#include "ScreenManager.h" +#include "ThemeManager.h" +#include "RageTimer.h" +#include "ActorUtil.h" +#include "Actor.h" +#include "GameSoundManager.h" +#include "MenuTimer.h" +#include "NetworkSyncManager.h" +#include "RageUtil.h" +#include "GameState.h" +#include "InputEventPlus.h" +#include "RageInput.h" +#include "Font.h" +#include "RageDisplay.h" + +#define CHAT_TEXT_OUTPUT_WIDTH THEME->GetMetricF(m_sName,"ChatTextOutputWidth") +#define CHAT_TEXT_INPUT_WIDTH THEME->GetMetricF(m_sName,"ChatTextInputWidth") +#define SHOW_CHAT_LINES THEME->GetMetricI(m_sName,"ChatOutputLines") + +#define USERS_X THEME->GetMetricF(m_sName,"UsersX") +#define USERS_Y THEME->GetMetricF(m_sName,"UsersY") +#define USER_SPACING_X THEME->GetMetricF(m_sName,"UserSpacingX") +#define USER_ADD_Y THEME->GetMetricF(m_sName,"UserLine2Y") + +AutoScreenMessage( SM_AddToChat ); +AutoScreenMessage( SM_UsersUpdate ); +AutoScreenMessage( SM_SMOnlinePack ); + +REGISTER_SCREEN_CLASS( ScreenNetSelectBase ); + +void ScreenNetSelectBase::Init() +{ + ScreenWithMenuElements::Init(); + + // Chat boxes + m_sprChatInputBox.Load( THEME->GetPathG( m_sName, "ChatInputBox" ) ); + m_sprChatInputBox->SetName( "ChatInputBox" ); + LOAD_ALL_COMMANDS_AND_SET_XY_AND_ON_COMMAND( m_sprChatInputBox ); + this->AddChild( m_sprChatInputBox ); + + m_sprChatOutputBox.Load( THEME->GetPathG( m_sName, "ChatOutputBox" ) ); + m_sprChatOutputBox->SetName( "ChatOutputBox" ); + LOAD_ALL_COMMANDS_AND_SET_XY_AND_ON_COMMAND( m_sprChatOutputBox ); + this->AddChild( m_sprChatOutputBox ); + + m_textChatInput.LoadFromFont( THEME->GetPathF(m_sName,"chat") ); + m_textChatInput.SetName( "ChatInput" ); + m_textChatInput.SetWrapWidthPixels( (int)(CHAT_TEXT_INPUT_WIDTH) ); + LOAD_ALL_COMMANDS_AND_SET_XY_AND_ON_COMMAND( m_textChatInput ); + this->AddChild( &m_textChatInput ); + + m_textChatOutput.LoadFromFont( THEME->GetPathF(m_sName,"chat") ); + m_textChatOutput.SetName( "ChatOutput" ); + m_textChatOutput.SetWrapWidthPixels( (int)(CHAT_TEXT_OUTPUT_WIDTH) ); + LOAD_ALL_COMMANDS_AND_SET_XY_AND_ON_COMMAND( m_textChatOutput ); + this->AddChild( &m_textChatOutput ); + + m_textChatOutput.SetText( NSMAN->m_sChatText ); + m_textChatOutput.SetMaxLines( SHOW_CHAT_LINES, 1 ); + + //Display users list + UpdateUsers(); + + return; +} + +bool ScreenNetSelectBase::Input( const InputEventPlus &input ) +{ + if( m_In.IsTransitioning() || m_Out.IsTransitioning() ) + return false; + + if( input.type != IET_FIRST_PRESS && input.type != IET_REPEAT ) + return false; + + bool bHoldingCtrl = + INPUTFILTER->IsBeingPressed(DeviceInput(DEVICE_KEYBOARD, KEY_LCTRL)) || + INPUTFILTER->IsBeingPressed(DeviceInput(DEVICE_KEYBOARD, KEY_RCTRL)) || + (!NSMAN->useSMserver); // If we are disconnected, assume no chatting. + + switch( input.DeviceI.button ) + { + case KEY_ENTER: + case KEY_KP_ENTER: + if (!bHoldingCtrl) + { + if ( m_sTextInput != "" ) + NSMAN->SendChat( m_sTextInput ); + m_sTextInput=""; + UpdateTextInput(); + return true; + } + break; + case KEY_BACK: + if(!m_sTextInput.empty()) + m_sTextInput = m_sTextInput.erase( m_sTextInput.size()-1 ); + UpdateTextInput(); + break; + default: + wchar_t c; + c = INPUTMAN->DeviceInputToChar(input.DeviceI, true); + + if( (c >= ' ') && (!bHoldingCtrl) ) + { + m_sTextInput += WStringToRString(wstring()+c); + UpdateTextInput(); + } + + // Tricky: If both players are playing, allow the 2 button through to + // the keymapper. (who? -aj) + // What purpose does this serve? -aj + if( c == '2' && GAMESTATE->IsPlayerEnabled( PLAYER_2 ) && GAMESTATE->IsPlayerEnabled( PLAYER_1 ) ) + break; + + if( c >= ' ' ) + return true; + break; + } + return Screen::Input( input ); +} + +void ScreenNetSelectBase::HandleScreenMessage( const ScreenMessage SM ) +{ + if( SM == SM_GoToNextScreen ) + SOUND->StopMusic(); + else if( SM == SM_AddToChat ) + { + m_textChatOutput.SetText( NSMAN->m_sChatText ); + m_textChatOutput.SetMaxLines( SHOW_CHAT_LINES, 1 ); + } + else if( SM == SM_UsersUpdate ) + { + UpdateUsers(); + } + + ScreenWithMenuElements::HandleScreenMessage( SM ); +} + +void ScreenNetSelectBase::TweenOffScreen() +{ + OFF_COMMAND( m_sprChatInputBox ); + OFF_COMMAND( m_sprChatOutputBox ); + OFF_COMMAND( m_textChatInput ); + OFF_COMMAND( m_textChatOutput ); + + for( unsigned i=0; iRemoveChild( &m_textUsers[i] ); + + m_textUsers.clear(); + + m_textUsers.resize( NSMAN->m_ActivePlayer.size() ); + + for( unsigned i=0; i < NSMAN->m_ActivePlayer.size(); i++) + { + m_textUsers[i].LoadFromFont( THEME->GetPathF(m_sName,"chat") ); + m_textUsers[i].SetHorizAlign( align_center ); + m_textUsers[i].SetVertAlign( align_top ); + m_textUsers[i].SetShadowLength( 0 ); + m_textUsers[i].SetName( "Users" ); + + tX += USER_SPACING_X; + + if ( (i % 2) == 1) + tY = USER_ADD_Y + USERS_Y; + else + tY = USERS_Y; + m_textUsers[i].SetXY( tX, tY ); + + ActorUtil::LoadAllCommands( m_textUsers[i], m_sName ); + ActorUtil::OnCommand( m_textUsers[i] ); + + m_textUsers[i].SetText( NSMAN->m_PlayerNames[NSMAN->m_ActivePlayer[i]] ); + m_textUsers[i].RunCommands( THEME->GetMetricA( m_sName, + ssprintf("Users%dCommand", NSMAN->m_PlayerStatus[NSMAN->m_ActivePlayer[i]] ) ) ); + + this->AddChild( &m_textUsers[i] ); + } +} + +/** ColorBitmapText ***********************************************************/ +void ColorBitmapText::SetText( const RString& _sText, const RString& _sAlternateText, int iWrapWidthPixels ) +{ + ASSERT( m_pFont != nullptr ); + + RString sNewText = StringWillUseAlternate(_sText,_sAlternateText) ? _sAlternateText : _sText; + + if( iWrapWidthPixels == -1 ) // wrap not specified + iWrapWidthPixels = m_iWrapWidthPixels; + + if( m_sText == sNewText && iWrapWidthPixels==m_iWrapWidthPixels ) + return; + m_sText = sNewText; + m_iWrapWidthPixels = iWrapWidthPixels; + + // Set up the first color. + m_vColors.clear(); + ColorChange change; + change.c = RageColor (1, 1, 1, 1); + change.l = 0; + m_vColors.push_back( change ); + + m_wTextLines.clear(); + + RString sCurrentLine = ""; + int iLineWidth = 0; + + RString sCurrentWord = ""; + int iWordWidth = 0; + int iGlyphsSoFar = 0; + + for( unsigned i = 0; i < m_sText.length(); i++ ) + { + int iCharsLeft = m_sText.length() - i - 1; + + // First: Check for the special (color) case. + + if( m_sText.length() > 8 && i < m_sText.length() - 9 ) + { + RString FirstThree = m_sText.substr( i, 3 ); + if( FirstThree.CompareNoCase("|c0") == 0 && iCharsLeft > 8 ) + { + ColorChange cChange; + unsigned int r, g, b; + sscanf( m_sText.substr( i, 9 ).c_str(), "|%*c0%2x%2x%2x", &r, &g, &b ); + cChange.c = RageColor( r/255.f, g/255.f, b/255.f, 1.f ); + cChange.l = iGlyphsSoFar; + if( iGlyphsSoFar == 0 ) + m_vColors[0] = cChange; + else + m_vColors.push_back( cChange ); + i+=8; + continue; + } + } + + char curChar = m_sText[i]; + int iCharLen = m_pFont->GetLineWidthInSourcePixels( wstring(1, curChar) ); + + switch( curChar ) + { + case ' ': + if( /* iLineWidth == 0 &&*/ iWordWidth == 0 ) + break; + sCurrentLine += sCurrentWord + " "; + iLineWidth += iWordWidth + iCharLen; + sCurrentWord = ""; + iWordWidth = 0; + iGlyphsSoFar++; + break; + case '\n': + if( iLineWidth + iWordWidth > iWrapWidthPixels ) + { + SimpleAddLine( sCurrentLine, iLineWidth ); + if( iWordWidth > 0 ) + iLineWidth = iWordWidth + //Add the width of a space + m_pFont->GetLineWidthInSourcePixels( L" " ); + sCurrentLine = sCurrentWord + " "; + iWordWidth = 0; + sCurrentWord = ""; + iGlyphsSoFar++; + } + else + { + SimpleAddLine( sCurrentLine + sCurrentWord, iLineWidth + iWordWidth ); + sCurrentLine = ""; iLineWidth = 0; + sCurrentWord = ""; iWordWidth = 0; + } + break; + default: + if( iWordWidth + iCharLen > iWrapWidthPixels && iLineWidth == 0 ) + { + SimpleAddLine( sCurrentWord, iWordWidth ); + sCurrentWord = curChar; iWordWidth = iCharLen; + } + else if( iWordWidth + iLineWidth + iCharLen > iWrapWidthPixels ) + { + SimpleAddLine( sCurrentLine, iLineWidth ); + sCurrentLine = ""; + iLineWidth = 0; + sCurrentWord += curChar; + iWordWidth += iCharLen; + } + else + { + sCurrentWord += curChar; + iWordWidth += iCharLen; + } + iGlyphsSoFar++; + break; + } + } + + if( iWordWidth > 0 ) + { + sCurrentLine += sCurrentWord; + iLineWidth += iWordWidth; + } + + if( iLineWidth > 0 ) + SimpleAddLine( sCurrentLine, iLineWidth ); + + BuildChars(); + UpdateBaseZoom(); +} + +void ColorBitmapText::SimpleAddLine( const RString &sAddition, const int iWidthPixels) +{ + m_wTextLines.push_back( RStringToWstring( sAddition ) ); + m_iLineWidths.push_back( iWidthPixels ); +} + +void ColorBitmapText::DrawPrimitives( ) +{ + Actor::SetGlobalRenderStates(); // set Actor-specified render states + DISPLAY->SetTextureMode( TextureUnit_1, TextureMode_Modulate ); + + /* Draw if we're not fully transparent or the zbuffer is enabled */ + if( m_pTempState->diffuse[0].a != 0 ) + { + // render the shadow + if( m_fShadowLengthX != 0 || m_fShadowLengthY != 0 ) + { + DISPLAY->PushMatrix(); + DISPLAY->TranslateWorld( m_fShadowLengthX, m_fShadowLengthY, 0 ); // shift by 5 units + RageColor c = m_ShadowColor; + c.a *= m_pTempState->diffuse[0].a; + for( unsigned i=0; iPopMatrix(); + } + + // render the diffuse pass + int loc = 0, cur = 0; + RageColor c = m_pTempState->diffuse[0]; + + for( unsigned i=0; i m_vColors[cur].l ) + { + c = m_vColors[cur].c; + cur++; + } + } + for( unsigned j=0; j<4; j++ ) + m_aVertices[i+j].c = c; + } + + DrawChars( false ); + } + + // render the glow pass + if( m_pTempState->glow.a > 0.0001f ) + { + DISPLAY->SetTextureMode( TextureUnit_1, TextureMode_Glow ); + + for( unsigned i=0; iglow; + DrawChars( false ); + } +} + +void ColorBitmapText::SetMaxLines( int iNumLines, int iDirection ) +{ + iNumLines = max( 0, iNumLines ); + iNumLines = min( (int)m_wTextLines.size(), iNumLines ); + if( iDirection == 0 ) + { + // Crop all bottom lines + m_wTextLines.resize( iNumLines ); + m_iLineWidths.resize( iNumLines ); + } + else + { + // Because colors are relative to the beginning, we have to crop them back + unsigned shift = 0; + + for( unsigned i = 0; i < m_wTextLines.size() - iNumLines; i++ ) + shift += m_wTextLines[i].length(); + + // When we're cutting out text, we need to maintain the last + // color, so our text at the top doesn't become colorless. + RageColor LastColor; + + for( unsigned i = 0; i < m_vColors.size(); i++ ) + { + m_vColors[i].l -= shift; + if( m_vColors[i].l < 0 ) + { + LastColor = m_vColors[i].c; + m_vColors.erase( m_vColors.begin() + i ); + i--; + } + } + + // If we already have a color set for the first char + // do not override it. + if( m_vColors.size() > 0 && m_vColors[0].l > 0 ) + { + ColorChange tmp; + tmp.c = LastColor; + tmp.l = 0; + m_vColors.insert( m_vColors.begin(), tmp ); + } + + m_wTextLines.erase( m_wTextLines.begin(), m_wTextLines.end() - iNumLines ); + m_iLineWidths.erase( m_iLineWidths.begin(), m_iLineWidths.end() - iNumLines ); + } + BuildChars(); +} + + +#endif + +/* + * (c) 2004 Charles Lohr + * All rights reserved. + * Elements from ScreenTextEntry + * + * 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. + */ diff --git a/src/ScreenNetSelectMusic.cpp b/src/ScreenNetSelectMusic.cpp index 0ea1c3c2d5..3f51b9262c 100644 --- a/src/ScreenNetSelectMusic.cpp +++ b/src/ScreenNetSelectMusic.cpp @@ -1,598 +1,598 @@ -#include "global.h" - -#if !defined(WITHOUT_NETWORKING) -#include "ScreenNetSelectMusic.h" -#include "ScreenManager.h" -#include "GameSoundManager.h" -#include "GameConstantsAndTypes.h" -#include "ThemeManager.h" -#include "GameState.h" -#include "Style.h" -#include "Steps.h" -#include "RageTimer.h" -#include "ActorUtil.h" -#include "AnnouncerManager.h" -#include "MenuTimer.h" -#include "NetworkSyncManager.h" -#include "StepsUtil.h" -#include "RageUtil.h" -#include "MusicWheel.h" -#include "InputMapper.h" -#include "RageLog.h" -#include "Song.h" -#include "InputEventPlus.h" -#include "SongUtil.h" -#include "RageInput.h" -#include "SongManager.h" -#include "CodeDetector.h" - -AutoScreenMessage( SM_NoSongs ); -AutoScreenMessage( SM_ChangeSong ); -AutoScreenMessage( SM_SMOnlinePack ); -AutoScreenMessage( SM_SetWheelSong ); -AutoScreenMessage( SM_RefreshWheelLocation ); -AutoScreenMessage( SM_SongChanged ); -AutoScreenMessage( SM_UsersUpdate ); -AutoScreenMessage( SM_BackFromPlayerOptions ); - -REGISTER_SCREEN_CLASS( ScreenNetSelectMusic ); - -void ScreenNetSelectMusic::Init() -{ - // Finish any previous stage. It's OK to call this when we haven't played a stage yet. - GAMESTATE->FinishStage(); - - ScreenNetSelectBase::Init(); - - SAMPLE_MUSIC_PREVIEW_MODE.Load( m_sName, "SampleMusicPreviewMode" ); - MUSIC_WHEEL_TYPE.Load( m_sName, "MusicWheelType" ); - PLAYER_OPTIONS_SCREEN.Load( m_sName, "PlayerOptionsScreen" ); - - FOREACH_EnabledPlayer (p) - { - m_DC[p] = GAMESTATE->m_PreferredDifficulty[p]; - - m_StepsDisplays[p].SetName( ssprintf("StepsDisplayP%d",p+1) ); - m_StepsDisplays[p].Load( "StepsDisplayNet", NULL ); - LOAD_ALL_COMMANDS_AND_SET_XY( m_StepsDisplays[p] ); - this->AddChild( &m_StepsDisplays[p] ); - } - - m_MusicWheel.SetName( "MusicWheel" ); - m_MusicWheel.Load( MUSIC_WHEEL_TYPE ); - LOAD_ALL_COMMANDS_AND_SET_XY( m_MusicWheel ); - m_MusicWheel.BeginScreen(); - ON_COMMAND( m_MusicWheel ); - this->AddChild( &m_MusicWheel ); - this->MoveToHead( &m_MusicWheel ); - - // todo: handle me theme-side -aj - FOREACH_EnabledPlayer( p ) - { - m_ModIconRow[p].SetName( ssprintf("ModIconsP%d",p+1) ); - m_ModIconRow[p].Load( "ModIconRowSelectMusic", p ); - m_ModIconRow[p].SetFromGameState(); - LOAD_ALL_COMMANDS_AND_SET_XY( m_ModIconRow[p] ); - this->AddChild( &m_ModIconRow[p] ); - } - - // Load SFX and music - m_soundChangeOpt.Load( THEME->GetPathS(m_sName,"change opt") ); - m_soundChangeSel.Load( THEME->GetPathS(m_sName,"change sel") ); - m_sSectionMusicPath = THEME->GetPathS(m_sName,"section music"); - m_sRouletteMusicPath = THEME->GetPathS(m_sName,"roulette music"); - m_sRandomMusicPath = THEME->GetPathS(m_sName,"random music"); - - NSMAN->ReportNSSOnOff(1); - NSMAN->ReportPlayerOptions(); - - m_bInitialSelect = false; - m_bAllowInput = false; -} - -bool ScreenNetSelectMusic::Input( const InputEventPlus &input ) -{ - if( !m_bAllowInput || IsTransitioning() ) - return false; - - if( input.type == IET_RELEASE ) - { - m_MusicWheel.Move(0); - return true; - } - - if( input.type != IET_FIRST_PRESS && input.type != IET_REPEAT ) - return false; - - bool bHoldingCtrl = - INPUTFILTER->IsBeingPressed(DeviceInput(DEVICE_KEYBOARD, KEY_LCTRL)) || - INPUTFILTER->IsBeingPressed(DeviceInput(DEVICE_KEYBOARD, KEY_RCTRL)) || - (!NSMAN->useSMserver); // If we are disconnected, assume no chatting - - wchar_t c = INPUTMAN->DeviceInputToChar(input.DeviceI,false); - MakeUpper( &c, 1 ); - - // Ctrl+[A-Z] to go to that letter of the alphabet - bool handled = false; - if( bHoldingCtrl && ( c >= 'A' ) && ( c <= 'Z' ) ) - { - SortOrder so = GAMESTATE->m_SortOrder; - if( ( so != SORT_TITLE ) && ( so != SORT_ARTIST ) ) - { - so = SORT_TITLE; - - GAMESTATE->m_PreferredSortOrder = so; - GAMESTATE->m_SortOrder.Set( so ); - // Odd, changing the sort order requires us to call SetOpenSection more than once - m_MusicWheel.ChangeSort( so ); - m_MusicWheel.SetOpenSection( ssprintf("%c", c ) ); - } - m_MusicWheel.SelectSection( ssprintf("%c", c ) ); - m_MusicWheel.ChangeSort( so ); - m_MusicWheel.SetOpenSection( ssprintf("%c", c ) ); - m_MusicWheel.Move(+1); - handled = true; - } - - return ScreenNetSelectBase::Input( input ) || handled; -} - -void ScreenNetSelectMusic::HandleScreenMessage( const ScreenMessage SM ) -{ - if( SM == SM_GoToPrevScreen ) - { - SCREENMAN->SetNewScreen( THEME->GetMetric (m_sName, "PrevScreen") ); - } - else if( SM == SM_GoToNextScreen ) - { - SOUND->StopMusic(); - SCREENMAN->SetNewScreen( THEME->GetMetric (m_sName, "NextScreen") ); - } - else if( SM == SM_UsersUpdate ) - { - m_MusicWheel.Move( 0 ); - } - else if( SM == SM_NoSongs ) - { - SCREENMAN->SetNewScreen( THEME->GetMetric (m_sName, "NoSongsScreen") ); - } - else if( SM == SM_ChangeSong ) - { - // First check to see if this song is already selected. This is so that if - // you have multiple copies of the "same" song you can chose which copy to play. - Song* CurSong = m_MusicWheel.GetSelectedSong(); - - if(CurSong != NULL ) - - if( ( !CurSong->GetTranslitArtist().CompareNoCase( NSMAN->m_sArtist ) ) && - ( !CurSong->GetTranslitMainTitle().CompareNoCase( NSMAN->m_sMainTitle ) ) && - ( !CurSong->GetTranslitSubTitle().CompareNoCase( NSMAN->m_sSubTitle ) ) ) - { - switch ( NSMAN->m_iSelectMode ) - { - case 0: - case 1: - NSMAN->m_iSelectMode = 0; - NSMAN->SelectUserSong(); - break; - case 2: // Proper starting of song - case 3: // Blind starting of song - StartSelectedSong(); - goto done; - } - } - - vector AllSongs = SONGMAN->GetAllSongs(); - unsigned i; - for( i=0; i < AllSongs.size(); i++ ) - { - m_cSong = AllSongs[i]; - if( ( !m_cSong->GetTranslitArtist().CompareNoCase( NSMAN->m_sArtist ) ) && - ( !m_cSong->GetTranslitMainTitle().CompareNoCase( NSMAN->m_sMainTitle ) ) && - ( !m_cSong->GetTranslitSubTitle().CompareNoCase( NSMAN->m_sSubTitle ) ) ) - break; - } - - bool haveSong = i != AllSongs.size(); - - switch (NSMAN->m_iSelectMode) - { - case 3: - StartSelectedSong(); - break; - case 2: // We need to do cmd 1 as well here - if(haveSong) - { - if(!m_MusicWheel.SelectSong( m_cSong ) ) - { - m_MusicWheel.ChangeSort( SORT_GROUP ); - m_MusicWheel.FinishTweening(); - SCREENMAN->PostMessageToTopScreen( SM_SetWheelSong, 0.710f ); - } - m_MusicWheel.Select(); - m_MusicWheel.Move(-1); - m_MusicWheel.Move(1); - StartSelectedSong(); - m_MusicWheel.Select(); - } - break; - case 1: // Scroll to song as well - if(haveSong) - { - if(!m_MusicWheel.SelectSong( m_cSong ) ) - { - //m_MusicWheel.ChangeSort( SORT_GROUP ); - //m_MusicWheel.FinishTweening(); - //SCREENMAN->PostMessageToTopScreen( SM_SetWheelSong, 0.710f ); - m_MusicWheel.ChangeSort( SORT_GROUP ); - m_MusicWheel.SetOpenSection( "" ); - } - m_MusicWheel.SelectSong( m_cSong ); - m_MusicWheel.Select(); - m_MusicWheel.Move(-1); - m_MusicWheel.Move(1); - m_MusicWheel.Select(); - } - // don't break here - case 0: // See if client has song - if(haveSong) - NSMAN->m_iSelectMode = 0; - else - NSMAN->m_iSelectMode = 1; - NSMAN->SelectUserSong(); - } - } - else if( SM == SM_SetWheelSong ) // After we've done the sort on wheel, select song. - { - m_MusicWheel.SelectSong( m_cSong ); - } - else if( SM == SM_RefreshWheelLocation ) - { - m_MusicWheel.Select(); - m_MusicWheel.Move(-1); - m_MusicWheel.Move(1); - m_MusicWheel.Select(); - m_bAllowInput = true; - } - else if( SM == SM_BackFromPlayerOptions ) - { - // XXX HACK: This will cause ScreenSelectOptions to go back here. - NSMAN->ReportNSSOnOff(1); - GAMESTATE->m_EditMode = EditMode_Invalid; - NSMAN->ReportPlayerOptions(); - - // Update changes - FOREACH_EnabledPlayer(p) - m_ModIconRow[p].SetFromGameState(); - } - else if( SM == SM_SongChanged ) - { - GAMESTATE->m_pCurSong.Set( m_MusicWheel.GetSelectedSong() ); - MusicChanged(); - } - else if( SM == SM_SMOnlinePack ) - { - if( NSMAN->m_SMOnlinePacket.Read1() == 1 ) - { - switch ( NSMAN->m_SMOnlinePacket.Read1() ) - { - case 0: // Room title Change - { - RString titleSub; - titleSub = NSMAN->m_SMOnlinePacket.ReadNT() + "\n"; - titleSub += NSMAN->m_SMOnlinePacket.ReadNT(); - if( NSMAN->m_SMOnlinePacket.Read1() != 1 ) - { - RString SMOnlineSelectScreen = THEME->GetMetric( m_sName, "RoomSelectScreen" ); - SCREENMAN->SetNewScreen( SMOnlineSelectScreen ); - } - } - } - } - } - -done: - // Must be at end, as so it is last resort for SMOnline packets. - // If it doesn't know what to do, then it'll just remove them. - ScreenNetSelectBase::HandleScreenMessage( SM ); -} - -bool ScreenNetSelectMusic::LeftAndRightPressed( const PlayerNumber pn ) -{ - return INPUTMAPPER->IsBeingPressed( GAME_BUTTON_LEFT, pn ) - && INPUTMAPPER->IsBeingPressed( GAME_BUTTON_RIGHT, pn ); -} - -bool ScreenNetSelectMusic::MenuLeft( const InputEventPlus &input ) -{ - PlayerNumber pn = input.pn; - - if( LeftAndRightPressed(pn) ) - m_MusicWheel.ChangeSort( SORT_MODE_MENU ); - else - m_MusicWheel.Move( -1 ); - return true; -} - -bool ScreenNetSelectMusic::MenuRight( const InputEventPlus &input ) -{ - PlayerNumber pn = input.pn; - - if( LeftAndRightPressed(pn) ) - m_MusicWheel.ChangeSort( SORT_MODE_MENU ); - else - m_MusicWheel.Move( +1 ); - return true; -} - -bool ScreenNetSelectMusic::MenuUp( const InputEventPlus &input ) -{ - NSMAN->ReportNSSOnOff(3); - GAMESTATE->m_EditMode = EditMode_Full; - SCREENMAN->AddNewScreenToTop( PLAYER_OPTIONS_SCREEN, SM_BackFromPlayerOptions ); - return true; -} - -bool ScreenNetSelectMusic::MenuDown( const InputEventPlus &input ) -{ - /* Tricky: If we have a player on player 2, and there is only player 2, - * allow them to use player 1's controls to change their difficulty. */ - /* Why? Nothing else allows that. (-who?) */ - // I agree, that's a stupid idea -aj - - PlayerNumber pn = input.pn; - if( GAMESTATE->IsPlayerEnabled( PLAYER_2 ) && - !GAMESTATE->IsPlayerEnabled( PLAYER_1 ) ) - pn = PLAYER_2; - - if( GAMESTATE->m_pCurSong == NULL ) - return false; - StepsType st = GAMESTATE->GetCurrentStyle()->m_StepsType; - vector MultiSteps; - MultiSteps = GAMESTATE->m_pCurSong->GetStepsByStepsType( st ); - if(MultiSteps.size() == 0) - m_DC[pn] = NUM_Difficulty; - else - { - int i; - - bool dcs[NUM_Difficulty]; - - for( i=0; iGetDifficulty()] = true; - - for( i=0; i m_DC[pn]) ) - { - m_DC[pn] = (Difficulty)i; - break; - } - } - // If failed to go up, loop - if( i == NUM_Difficulty ) - { - for(i = 0;im_PreferredDifficulty[pn].Set( m_DC[pn] ); - return true; -} - -bool ScreenNetSelectMusic::MenuStart( const InputEventPlus &input ) -{ - bool bResult = m_MusicWheel.Select(); - - if( !bResult ) - return true; - - if( m_MusicWheel.GetSelectedType() != WheelItemDataType_Song ) - return true; - - Song * pSong = m_MusicWheel.GetSelectedSong(); - - if( pSong == NULL ) - return false; - - GAMESTATE->m_pCurSong.Set( pSong ); - - if( NSMAN->useSMserver ) - { - NSMAN->m_sArtist = pSong->GetTranslitArtist(); - NSMAN->m_sMainTitle = pSong->GetTranslitMainTitle(); - NSMAN->m_sSubTitle = pSong->GetTranslitSubTitle(); - NSMAN->m_iSelectMode = 2; // Command for user selecting song - NSMAN->SelectUserSong (); - } - else - StartSelectedSong(); - return true; -} - -bool ScreenNetSelectMusic::MenuBack( const InputEventPlus &input ) -{ - SOUND->StopMusic(); - TweenOffScreen(); - - Cancel( SM_GoToPrevScreen ); - return true; -} - -void ScreenNetSelectMusic::TweenOffScreen() -{ - ScreenNetSelectBase::TweenOffScreen(); - - OFF_COMMAND( m_MusicWheel ); - - FOREACH_EnabledPlayer (pn) - { - OFF_COMMAND( m_StepsDisplays[pn] ); - //OFF_COMMAND( m_DifficultyIcon[pn] ); - OFF_COMMAND( m_ModIconRow[pn] ); - } - - OFF_COMMAND( m_MusicWheel ); - - NSMAN->ReportNSSOnOff(0); -} - -void ScreenNetSelectMusic::StartSelectedSong() -{ - Song * pSong = m_MusicWheel.GetSelectedSong(); - GAMESTATE->m_pCurSong.Set( pSong ); - StepsType st = GAMESTATE->GetCurrentStyle()->m_StepsType; //StepsType_dance_single; - FOREACH_EnabledPlayer (pn) - { - GAMESTATE->m_PreferredDifficulty[pn].Set( m_DC[pn] ); - Steps *pSteps = SongUtil::GetStepsByDifficulty(pSong, st, m_DC[pn]); - GAMESTATE->m_pCurSteps[pn].Set( pSteps ); - } - - GAMESTATE->m_PreferredSortOrder = GAMESTATE->m_SortOrder; - GAMESTATE->m_pPreferredSong = pSong; - - // force event mode - GAMESTATE->m_bTemporaryEventMode = true; - - TweenOffScreen(); - StartTransitioningScreen( SM_GoToNextScreen ); -} - -void ScreenNetSelectMusic::UpdateDifficulties( PlayerNumber pn ) -{ - if( GAMESTATE->m_pCurSong == NULL ) - { - m_StepsDisplays[pn].SetFromStepsTypeAndMeterAndDifficultyAndCourseType( StepsType_Invalid, 0, Difficulty_Beginner, CourseType_Invalid ); - //m_DifficultyIcon[pn].SetFromSteps( pn, NULL ); // It will blank it out - return; - } - - StepsType st = GAMESTATE->GetCurrentStyle()->m_StepsType; - - Steps * pSteps = SongUtil::GetStepsByDifficulty( GAMESTATE->m_pCurSong, st, m_DC[pn] ); - GAMESTATE->m_pCurSteps[pn].Set( pSteps ); - - if( ( m_DC[pn] < NUM_Difficulty ) && ( m_DC[pn] >= Difficulty_Beginner ) ) - m_StepsDisplays[pn].SetFromSteps( pSteps ); - else - m_StepsDisplays[pn].SetFromStepsTypeAndMeterAndDifficultyAndCourseType( StepsType_Invalid, 0, Difficulty_Beginner, CourseType_Invalid ); -} - -void ScreenNetSelectMusic::MusicChanged() -{ - if( GAMESTATE->m_pCurSong == NULL ) - { - FOREACH_EnabledPlayer (pn) - UpdateDifficulties( pn ); - - SOUND->StopMusic(); - // todo: handle playing section music correctly. -aj - // SOUND->PlayMusic( m_sSectionMusicPath, NULL, true, 0, -1 ); - return; - } - - FOREACH_EnabledPlayer (pn) - { - m_DC[pn] = GAMESTATE->m_PreferredDifficulty[pn]; - StepsType st = GAMESTATE->GetCurrentStyle()->m_StepsType; - vector MultiSteps; - MultiSteps = GAMESTATE->m_pCurSong->GetStepsByStepsType( st ); - if(MultiSteps.size() == 0) - m_DC[pn] = NUM_Difficulty; - else - { - int i; - Difficulty Target = Difficulty_Easy; - - bool dcs[NUM_Difficulty]; - - for( i=0; iGetDifficulty()] = true; - - for( i=0; i= m_DC[pn] ) - { - m_DC[pn] = (Difficulty)i; - break; - } - } - - if( i == NUM_Difficulty ) - m_DC[pn] = Target; - } - UpdateDifficulties( pn ); - } - - // Copied from ScreenSelectMusic - // TODO: Update me! -aj - SOUND->StopMusic(); - if( GAMESTATE->m_pCurSong->HasMusic() ) - { - // don't play the same sound over and over - if(SOUND->GetMusicPath().CompareNoCase(GAMESTATE->m_pCurSong->GetMusicPath())) - { - SOUND->StopMusic(); - SOUND->PlayMusic( - GAMESTATE->m_pCurSong->GetMusicPath(), - NULL, - true, - GAMESTATE->m_pCurSong->m_fMusicSampleStartSeconds, - GAMESTATE->m_pCurSong->m_fMusicSampleLengthSeconds ); - } - } -} - -void ScreenNetSelectMusic::Update( float fDeltaTime ) -{ - if(!m_bInitialSelect) - { - m_bInitialSelect = true; - SCREENMAN->PostMessageToTopScreen( SM_RefreshWheelLocation, 1.0f ); - } - ScreenNetSelectBase::Update( fDeltaTime ); -} - -#endif - -/* - * (c) 2004-2005 Charles Lohr - * 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. - */ +#include "global.h" + +#if !defined(WITHOUT_NETWORKING) +#include "ScreenNetSelectMusic.h" +#include "ScreenManager.h" +#include "GameSoundManager.h" +#include "GameConstantsAndTypes.h" +#include "ThemeManager.h" +#include "GameState.h" +#include "Style.h" +#include "Steps.h" +#include "RageTimer.h" +#include "ActorUtil.h" +#include "AnnouncerManager.h" +#include "MenuTimer.h" +#include "NetworkSyncManager.h" +#include "StepsUtil.h" +#include "RageUtil.h" +#include "MusicWheel.h" +#include "InputMapper.h" +#include "RageLog.h" +#include "Song.h" +#include "InputEventPlus.h" +#include "SongUtil.h" +#include "RageInput.h" +#include "SongManager.h" +#include "CodeDetector.h" + +AutoScreenMessage( SM_NoSongs ); +AutoScreenMessage( SM_ChangeSong ); +AutoScreenMessage( SM_SMOnlinePack ); +AutoScreenMessage( SM_SetWheelSong ); +AutoScreenMessage( SM_RefreshWheelLocation ); +AutoScreenMessage( SM_SongChanged ); +AutoScreenMessage( SM_UsersUpdate ); +AutoScreenMessage( SM_BackFromPlayerOptions ); + +REGISTER_SCREEN_CLASS( ScreenNetSelectMusic ); + +void ScreenNetSelectMusic::Init() +{ + // Finish any previous stage. It's OK to call this when we haven't played a stage yet. + GAMESTATE->FinishStage(); + + ScreenNetSelectBase::Init(); + + SAMPLE_MUSIC_PREVIEW_MODE.Load( m_sName, "SampleMusicPreviewMode" ); + MUSIC_WHEEL_TYPE.Load( m_sName, "MusicWheelType" ); + PLAYER_OPTIONS_SCREEN.Load( m_sName, "PlayerOptionsScreen" ); + + FOREACH_EnabledPlayer (p) + { + m_DC[p] = GAMESTATE->m_PreferredDifficulty[p]; + + m_StepsDisplays[p].SetName( ssprintf("StepsDisplayP%d",p+1) ); + m_StepsDisplays[p].Load( "StepsDisplayNet", NULL ); + LOAD_ALL_COMMANDS_AND_SET_XY( m_StepsDisplays[p] ); + this->AddChild( &m_StepsDisplays[p] ); + } + + m_MusicWheel.SetName( "MusicWheel" ); + m_MusicWheel.Load( MUSIC_WHEEL_TYPE ); + LOAD_ALL_COMMANDS_AND_SET_XY( m_MusicWheel ); + m_MusicWheel.BeginScreen(); + ON_COMMAND( m_MusicWheel ); + this->AddChild( &m_MusicWheel ); + this->MoveToHead( &m_MusicWheel ); + + // todo: handle me theme-side -aj + FOREACH_EnabledPlayer( p ) + { + m_ModIconRow[p].SetName( ssprintf("ModIconsP%d",p+1) ); + m_ModIconRow[p].Load( "ModIconRowSelectMusic", p ); + m_ModIconRow[p].SetFromGameState(); + LOAD_ALL_COMMANDS_AND_SET_XY( m_ModIconRow[p] ); + this->AddChild( &m_ModIconRow[p] ); + } + + // Load SFX and music + m_soundChangeOpt.Load( THEME->GetPathS(m_sName,"change opt") ); + m_soundChangeSel.Load( THEME->GetPathS(m_sName,"change sel") ); + m_sSectionMusicPath = THEME->GetPathS(m_sName,"section music"); + m_sRouletteMusicPath = THEME->GetPathS(m_sName,"roulette music"); + m_sRandomMusicPath = THEME->GetPathS(m_sName,"random music"); + + NSMAN->ReportNSSOnOff(1); + NSMAN->ReportPlayerOptions(); + + m_bInitialSelect = false; + m_bAllowInput = false; +} + +bool ScreenNetSelectMusic::Input( const InputEventPlus &input ) +{ + if( !m_bAllowInput || IsTransitioning() ) + return false; + + if( input.type == IET_RELEASE ) + { + m_MusicWheel.Move(0); + return true; + } + + if( input.type != IET_FIRST_PRESS && input.type != IET_REPEAT ) + return false; + + bool bHoldingCtrl = + INPUTFILTER->IsBeingPressed(DeviceInput(DEVICE_KEYBOARD, KEY_LCTRL)) || + INPUTFILTER->IsBeingPressed(DeviceInput(DEVICE_KEYBOARD, KEY_RCTRL)) || + (!NSMAN->useSMserver); // If we are disconnected, assume no chatting + + wchar_t c = INPUTMAN->DeviceInputToChar(input.DeviceI,false); + MakeUpper( &c, 1 ); + + // Ctrl+[A-Z] to go to that letter of the alphabet + bool handled = false; + if( bHoldingCtrl && ( c >= 'A' ) && ( c <= 'Z' ) ) + { + SortOrder so = GAMESTATE->m_SortOrder; + if( ( so != SORT_TITLE ) && ( so != SORT_ARTIST ) ) + { + so = SORT_TITLE; + + GAMESTATE->m_PreferredSortOrder = so; + GAMESTATE->m_SortOrder.Set( so ); + // Odd, changing the sort order requires us to call SetOpenSection more than once + m_MusicWheel.ChangeSort( so ); + m_MusicWheel.SetOpenSection( ssprintf("%c", c ) ); + } + m_MusicWheel.SelectSection( ssprintf("%c", c ) ); + m_MusicWheel.ChangeSort( so ); + m_MusicWheel.SetOpenSection( ssprintf("%c", c ) ); + m_MusicWheel.Move(+1); + handled = true; + } + + return ScreenNetSelectBase::Input( input ) || handled; +} + +void ScreenNetSelectMusic::HandleScreenMessage( const ScreenMessage SM ) +{ + if( SM == SM_GoToPrevScreen ) + { + SCREENMAN->SetNewScreen( THEME->GetMetric (m_sName, "PrevScreen") ); + } + else if( SM == SM_GoToNextScreen ) + { + SOUND->StopMusic(); + SCREENMAN->SetNewScreen( THEME->GetMetric (m_sName, "NextScreen") ); + } + else if( SM == SM_UsersUpdate ) + { + m_MusicWheel.Move( 0 ); + } + else if( SM == SM_NoSongs ) + { + SCREENMAN->SetNewScreen( THEME->GetMetric (m_sName, "NoSongsScreen") ); + } + else if( SM == SM_ChangeSong ) + { + // First check to see if this song is already selected. This is so that if + // you have multiple copies of the "same" song you can chose which copy to play. + Song* CurSong = m_MusicWheel.GetSelectedSong(); + + if(CurSong != nullptr ) + + if( ( !CurSong->GetTranslitArtist().CompareNoCase( NSMAN->m_sArtist ) ) && + ( !CurSong->GetTranslitMainTitle().CompareNoCase( NSMAN->m_sMainTitle ) ) && + ( !CurSong->GetTranslitSubTitle().CompareNoCase( NSMAN->m_sSubTitle ) ) ) + { + switch ( NSMAN->m_iSelectMode ) + { + case 0: + case 1: + NSMAN->m_iSelectMode = 0; + NSMAN->SelectUserSong(); + break; + case 2: // Proper starting of song + case 3: // Blind starting of song + StartSelectedSong(); + goto done; + } + } + + vector AllSongs = SONGMAN->GetAllSongs(); + unsigned i; + for( i=0; i < AllSongs.size(); i++ ) + { + m_cSong = AllSongs[i]; + if( ( !m_cSong->GetTranslitArtist().CompareNoCase( NSMAN->m_sArtist ) ) && + ( !m_cSong->GetTranslitMainTitle().CompareNoCase( NSMAN->m_sMainTitle ) ) && + ( !m_cSong->GetTranslitSubTitle().CompareNoCase( NSMAN->m_sSubTitle ) ) ) + break; + } + + bool haveSong = i != AllSongs.size(); + + switch (NSMAN->m_iSelectMode) + { + case 3: + StartSelectedSong(); + break; + case 2: // We need to do cmd 1 as well here + if(haveSong) + { + if(!m_MusicWheel.SelectSong( m_cSong ) ) + { + m_MusicWheel.ChangeSort( SORT_GROUP ); + m_MusicWheel.FinishTweening(); + SCREENMAN->PostMessageToTopScreen( SM_SetWheelSong, 0.710f ); + } + m_MusicWheel.Select(); + m_MusicWheel.Move(-1); + m_MusicWheel.Move(1); + StartSelectedSong(); + m_MusicWheel.Select(); + } + break; + case 1: // Scroll to song as well + if(haveSong) + { + if(!m_MusicWheel.SelectSong( m_cSong ) ) + { + //m_MusicWheel.ChangeSort( SORT_GROUP ); + //m_MusicWheel.FinishTweening(); + //SCREENMAN->PostMessageToTopScreen( SM_SetWheelSong, 0.710f ); + m_MusicWheel.ChangeSort( SORT_GROUP ); + m_MusicWheel.SetOpenSection( "" ); + } + m_MusicWheel.SelectSong( m_cSong ); + m_MusicWheel.Select(); + m_MusicWheel.Move(-1); + m_MusicWheel.Move(1); + m_MusicWheel.Select(); + } + // don't break here + case 0: // See if client has song + if(haveSong) + NSMAN->m_iSelectMode = 0; + else + NSMAN->m_iSelectMode = 1; + NSMAN->SelectUserSong(); + } + } + else if( SM == SM_SetWheelSong ) // After we've done the sort on wheel, select song. + { + m_MusicWheel.SelectSong( m_cSong ); + } + else if( SM == SM_RefreshWheelLocation ) + { + m_MusicWheel.Select(); + m_MusicWheel.Move(-1); + m_MusicWheel.Move(1); + m_MusicWheel.Select(); + m_bAllowInput = true; + } + else if( SM == SM_BackFromPlayerOptions ) + { + // XXX HACK: This will cause ScreenSelectOptions to go back here. + NSMAN->ReportNSSOnOff(1); + GAMESTATE->m_EditMode = EditMode_Invalid; + NSMAN->ReportPlayerOptions(); + + // Update changes + FOREACH_EnabledPlayer(p) + m_ModIconRow[p].SetFromGameState(); + } + else if( SM == SM_SongChanged ) + { + GAMESTATE->m_pCurSong.Set( m_MusicWheel.GetSelectedSong() ); + MusicChanged(); + } + else if( SM == SM_SMOnlinePack ) + { + if( NSMAN->m_SMOnlinePacket.Read1() == 1 ) + { + switch ( NSMAN->m_SMOnlinePacket.Read1() ) + { + case 0: // Room title Change + { + RString titleSub; + titleSub = NSMAN->m_SMOnlinePacket.ReadNT() + "\n"; + titleSub += NSMAN->m_SMOnlinePacket.ReadNT(); + if( NSMAN->m_SMOnlinePacket.Read1() != 1 ) + { + RString SMOnlineSelectScreen = THEME->GetMetric( m_sName, "RoomSelectScreen" ); + SCREENMAN->SetNewScreen( SMOnlineSelectScreen ); + } + } + } + } + } + +done: + // Must be at end, as so it is last resort for SMOnline packets. + // If it doesn't know what to do, then it'll just remove them. + ScreenNetSelectBase::HandleScreenMessage( SM ); +} + +bool ScreenNetSelectMusic::LeftAndRightPressed( const PlayerNumber pn ) +{ + return INPUTMAPPER->IsBeingPressed( GAME_BUTTON_LEFT, pn ) + && INPUTMAPPER->IsBeingPressed( GAME_BUTTON_RIGHT, pn ); +} + +bool ScreenNetSelectMusic::MenuLeft( const InputEventPlus &input ) +{ + PlayerNumber pn = input.pn; + + if( LeftAndRightPressed(pn) ) + m_MusicWheel.ChangeSort( SORT_MODE_MENU ); + else + m_MusicWheel.Move( -1 ); + return true; +} + +bool ScreenNetSelectMusic::MenuRight( const InputEventPlus &input ) +{ + PlayerNumber pn = input.pn; + + if( LeftAndRightPressed(pn) ) + m_MusicWheel.ChangeSort( SORT_MODE_MENU ); + else + m_MusicWheel.Move( +1 ); + return true; +} + +bool ScreenNetSelectMusic::MenuUp( const InputEventPlus &input ) +{ + NSMAN->ReportNSSOnOff(3); + GAMESTATE->m_EditMode = EditMode_Full; + SCREENMAN->AddNewScreenToTop( PLAYER_OPTIONS_SCREEN, SM_BackFromPlayerOptions ); + return true; +} + +bool ScreenNetSelectMusic::MenuDown( const InputEventPlus &input ) +{ + /* Tricky: If we have a player on player 2, and there is only player 2, + * allow them to use player 1's controls to change their difficulty. */ + /* Why? Nothing else allows that. (-who?) */ + // I agree, that's a stupid idea -aj + + PlayerNumber pn = input.pn; + if( GAMESTATE->IsPlayerEnabled( PLAYER_2 ) && + !GAMESTATE->IsPlayerEnabled( PLAYER_1 ) ) + pn = PLAYER_2; + + if( GAMESTATE->m_pCurSong == NULL ) + return false; + StepsType st = GAMESTATE->GetCurrentStyle()->m_StepsType; + vector MultiSteps; + MultiSteps = GAMESTATE->m_pCurSong->GetStepsByStepsType( st ); + if(MultiSteps.size() == 0) + m_DC[pn] = NUM_Difficulty; + else + { + int i; + + bool dcs[NUM_Difficulty]; + + for( i=0; iGetDifficulty()] = true; + + for( i=0; i m_DC[pn]) ) + { + m_DC[pn] = (Difficulty)i; + break; + } + } + // If failed to go up, loop + if( i == NUM_Difficulty ) + { + for(i = 0;im_PreferredDifficulty[pn].Set( m_DC[pn] ); + return true; +} + +bool ScreenNetSelectMusic::MenuStart( const InputEventPlus &input ) +{ + bool bResult = m_MusicWheel.Select(); + + if( !bResult ) + return true; + + if( m_MusicWheel.GetSelectedType() != WheelItemDataType_Song ) + return true; + + Song * pSong = m_MusicWheel.GetSelectedSong(); + + if( pSong == NULL ) + return false; + + GAMESTATE->m_pCurSong.Set( pSong ); + + if( NSMAN->useSMserver ) + { + NSMAN->m_sArtist = pSong->GetTranslitArtist(); + NSMAN->m_sMainTitle = pSong->GetTranslitMainTitle(); + NSMAN->m_sSubTitle = pSong->GetTranslitSubTitle(); + NSMAN->m_iSelectMode = 2; // Command for user selecting song + NSMAN->SelectUserSong (); + } + else + StartSelectedSong(); + return true; +} + +bool ScreenNetSelectMusic::MenuBack( const InputEventPlus &input ) +{ + SOUND->StopMusic(); + TweenOffScreen(); + + Cancel( SM_GoToPrevScreen ); + return true; +} + +void ScreenNetSelectMusic::TweenOffScreen() +{ + ScreenNetSelectBase::TweenOffScreen(); + + OFF_COMMAND( m_MusicWheel ); + + FOREACH_EnabledPlayer (pn) + { + OFF_COMMAND( m_StepsDisplays[pn] ); + //OFF_COMMAND( m_DifficultyIcon[pn] ); + OFF_COMMAND( m_ModIconRow[pn] ); + } + + OFF_COMMAND( m_MusicWheel ); + + NSMAN->ReportNSSOnOff(0); +} + +void ScreenNetSelectMusic::StartSelectedSong() +{ + Song * pSong = m_MusicWheel.GetSelectedSong(); + GAMESTATE->m_pCurSong.Set( pSong ); + StepsType st = GAMESTATE->GetCurrentStyle()->m_StepsType; //StepsType_dance_single; + FOREACH_EnabledPlayer (pn) + { + GAMESTATE->m_PreferredDifficulty[pn].Set( m_DC[pn] ); + Steps *pSteps = SongUtil::GetStepsByDifficulty(pSong, st, m_DC[pn]); + GAMESTATE->m_pCurSteps[pn].Set( pSteps ); + } + + GAMESTATE->m_PreferredSortOrder = GAMESTATE->m_SortOrder; + GAMESTATE->m_pPreferredSong = pSong; + + // force event mode + GAMESTATE->m_bTemporaryEventMode = true; + + TweenOffScreen(); + StartTransitioningScreen( SM_GoToNextScreen ); +} + +void ScreenNetSelectMusic::UpdateDifficulties( PlayerNumber pn ) +{ + if( GAMESTATE->m_pCurSong == NULL ) + { + m_StepsDisplays[pn].SetFromStepsTypeAndMeterAndDifficultyAndCourseType( StepsType_Invalid, 0, Difficulty_Beginner, CourseType_Invalid ); + //m_DifficultyIcon[pn].SetFromSteps( pn, NULL ); // It will blank it out + return; + } + + StepsType st = GAMESTATE->GetCurrentStyle()->m_StepsType; + + Steps * pSteps = SongUtil::GetStepsByDifficulty( GAMESTATE->m_pCurSong, st, m_DC[pn] ); + GAMESTATE->m_pCurSteps[pn].Set( pSteps ); + + if( ( m_DC[pn] < NUM_Difficulty ) && ( m_DC[pn] >= Difficulty_Beginner ) ) + m_StepsDisplays[pn].SetFromSteps( pSteps ); + else + m_StepsDisplays[pn].SetFromStepsTypeAndMeterAndDifficultyAndCourseType( StepsType_Invalid, 0, Difficulty_Beginner, CourseType_Invalid ); +} + +void ScreenNetSelectMusic::MusicChanged() +{ + if( GAMESTATE->m_pCurSong == NULL ) + { + FOREACH_EnabledPlayer (pn) + UpdateDifficulties( pn ); + + SOUND->StopMusic(); + // todo: handle playing section music correctly. -aj + // SOUND->PlayMusic( m_sSectionMusicPath, NULL, true, 0, -1 ); + return; + } + + FOREACH_EnabledPlayer (pn) + { + m_DC[pn] = GAMESTATE->m_PreferredDifficulty[pn]; + StepsType st = GAMESTATE->GetCurrentStyle()->m_StepsType; + vector MultiSteps; + MultiSteps = GAMESTATE->m_pCurSong->GetStepsByStepsType( st ); + if(MultiSteps.size() == 0) + m_DC[pn] = NUM_Difficulty; + else + { + int i; + Difficulty Target = Difficulty_Easy; + + bool dcs[NUM_Difficulty]; + + for( i=0; iGetDifficulty()] = true; + + for( i=0; i= m_DC[pn] ) + { + m_DC[pn] = (Difficulty)i; + break; + } + } + + if( i == NUM_Difficulty ) + m_DC[pn] = Target; + } + UpdateDifficulties( pn ); + } + + // Copied from ScreenSelectMusic + // TODO: Update me! -aj + SOUND->StopMusic(); + if( GAMESTATE->m_pCurSong->HasMusic() ) + { + // don't play the same sound over and over + if(SOUND->GetMusicPath().CompareNoCase(GAMESTATE->m_pCurSong->GetMusicPath())) + { + SOUND->StopMusic(); + SOUND->PlayMusic( + GAMESTATE->m_pCurSong->GetMusicPath(), + NULL, + true, + GAMESTATE->m_pCurSong->m_fMusicSampleStartSeconds, + GAMESTATE->m_pCurSong->m_fMusicSampleLengthSeconds ); + } + } +} + +void ScreenNetSelectMusic::Update( float fDeltaTime ) +{ + if(!m_bInitialSelect) + { + m_bInitialSelect = true; + SCREENMAN->PostMessageToTopScreen( SM_RefreshWheelLocation, 1.0f ); + } + ScreenNetSelectBase::Update( fDeltaTime ); +} + +#endif + +/* + * (c) 2004-2005 Charles Lohr + * 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. + */ diff --git a/src/ScreenOptionsEditCourse.cpp b/src/ScreenOptionsEditCourse.cpp index 2070e4523b..601a44d54c 100644 --- a/src/ScreenOptionsEditCourse.cpp +++ b/src/ScreenOptionsEditCourse.cpp @@ -335,7 +335,7 @@ void ScreenOptionsEditCourse::ExportOptions( int iRow, const vectorGetStepsForEntry( iEntryIndex ); - ASSERT_M( pSteps != NULL, "No Steps for this Song!" ); + ASSERT_M( pSteps != nullptr, "No Steps for this Song!" ); CourseEntry ce; ce.songID.FromSong( pSong ); ce.stepsCriteria.m_difficulty = pSteps->GetDifficulty(); @@ -395,7 +395,7 @@ void ScreenOptionsEditCourse::SetCurrentSong() if( index != 0 ) pSong = m_vpSongs[ index - 1 ]; } - if ( pSong != NULL ) + if ( pSong != nullptr ) { GAMESTATE->m_pCurSong.Set( pSong ); } @@ -412,7 +412,7 @@ void ScreenOptionsEditCourse::SetCurrentSteps() OptionRow &row = *m_pRows[ EntryIndexAndRowTypeToRow(iEntryIndex, RowType_Steps) ]; int iStepsIndex = row.GetOneSharedSelection(); const EditCourseOptionRowHandlerSteps *pHand = dynamic_cast( row.GetHandler() ); - ASSERT( pHand != NULL ); + ASSERT( pHand != nullptr ); Steps *pSteps = pHand->GetSteps( iStepsIndex ); GAMESTATE->m_pCurSteps[PLAYER_1].Set( pSteps ); } diff --git a/src/ScreenOptionsEditProfile.cpp b/src/ScreenOptionsEditProfile.cpp index 98e42580fb..178a28edc6 100644 --- a/src/ScreenOptionsEditProfile.cpp +++ b/src/ScreenOptionsEditProfile.cpp @@ -30,7 +30,7 @@ void ScreenOptionsEditProfile::BeginScreen() vector vHands; Profile *pProfile = PROFILEMAN->GetLocalProfile( GAMESTATE->m_sEditLocalProfileID ); - ASSERT( pProfile != NULL ); + ASSERT( pProfile != nullptr ); { vHands.push_back( OptionRowHandlerUtil::MakeNull() ); @@ -64,7 +64,7 @@ ScreenOptionsEditProfile::~ScreenOptionsEditProfile() void ScreenOptionsEditProfile::ImportOptions( int iRow, const vector &vpns ) { Profile *pProfile = PROFILEMAN->GetLocalProfile( GAMESTATE->m_sEditLocalProfileID ); - ASSERT( pProfile != NULL ); + ASSERT( pProfile != nullptr ); OptionRow &row = *m_pRows[iRow]; switch( iRow ) @@ -78,7 +78,7 @@ void ScreenOptionsEditProfile::ImportOptions( int iRow, const vector &vpns ) { Profile *pProfile = PROFILEMAN->GetLocalProfile( GAMESTATE->m_sEditLocalProfileID ); - ASSERT( pProfile != NULL ); + ASSERT( pProfile != nullptr ); OptionRow &row = *m_pRows[iRow]; int iIndex = row.GetOneSharedSelection( true ); RString sValue; diff --git a/src/ScreenOptionsManageEditSteps.cpp b/src/ScreenOptionsManageEditSteps.cpp index 0f3c69928e..2c2a7ecbe6 100644 --- a/src/ScreenOptionsManageEditSteps.cpp +++ b/src/ScreenOptionsManageEditSteps.cpp @@ -140,7 +140,7 @@ void ScreenOptionsManageEditSteps::HandleScreenMessage( const ScreenMessage SM ) else // a Steps { Steps *pSteps = GAMESTATE->m_pCurSteps[PLAYER_1]; - ASSERT( pSteps != NULL ); + ASSERT( pSteps != nullptr ); const Style *pStyle = GAMEMAN->GetEditorStyleForStepsType( pSteps->m_StepsType ); GAMESTATE->SetCurrentStyle( pStyle ); // do base behavior diff --git a/src/ScreenOptionsManageProfiles.cpp b/src/ScreenOptionsManageProfiles.cpp index 2a3a285321..fcbee7bc8b 100644 --- a/src/ScreenOptionsManageProfiles.cpp +++ b/src/ScreenOptionsManageProfiles.cpp @@ -60,7 +60,7 @@ static bool ValidateLocalProfileName( const RString &sAnswer, RString &sErrorOut } Profile *pProfile = PROFILEMAN->GetLocalProfile( GAMESTATE->m_sEditLocalProfileID ); - if( pProfile != NULL && sAnswer == pProfile->m_sDisplayName ) + if( pProfile != nullptr && sAnswer == pProfile->m_sDisplayName ) return true; // unchanged vector vsProfileNames; @@ -111,7 +111,7 @@ void ScreenOptionsManageProfiles::BeginScreen() for (RString const &s : m_vsLocalProfileID) { Profile *pProfile = PROFILEMAN->GetLocalProfile( s ); - ASSERT( pProfile != NULL ); + ASSERT( pProfile != nullptr ); RString sCommand = ssprintf( "gamecommand;screen,ScreenOptionsEditProfile;profileid,%s;name,dummy", s.c_str() ); OptionRowHandler *pHand = OptionRowHandlerUtil::Make( ParseCommands(sCommand) ); @@ -261,7 +261,7 @@ void ScreenOptionsManageProfiles::HandleScreenMessage( const ScreenMessage SM ) if( !ScreenMiniMenu::s_bCancelled ) { Profile *pProfile = PROFILEMAN->GetLocalProfile( GAMESTATE->m_sEditLocalProfileID ); - ASSERT( pProfile != NULL ); + ASSERT( pProfile != nullptr ); switch( ScreenMiniMenu::s_iLastRowCode ) { diff --git a/src/ScreenOptionsMasterPrefs.cpp b/src/ScreenOptionsMasterPrefs.cpp index c02f73769c..4fe1b1fd74 100644 --- a/src/ScreenOptionsMasterPrefs.cpp +++ b/src/ScreenOptionsMasterPrefs.cpp @@ -101,9 +101,9 @@ static void MoveMap( int &sel, IPreference &opt, bool ToSel, const T *mapping, u template static void MoveMap( int &sel, const ConfOption *pConfOption, bool ToSel, const T *mapping, unsigned cnt ) { - ASSERT( pConfOption != NULL ); + ASSERT( pConfOption != nullptr ); IPreference *pPref = IPreference::GetPreferenceByName( pConfOption->m_sPrefName ); - ASSERT_M( pPref != NULL, pConfOption->m_sPrefName ); + ASSERT_M( pPref != nullptr, pConfOption->m_sPrefName ); MoveMap( sel, *pPref, ToSel, mapping, cnt ); } @@ -112,7 +112,7 @@ template static void MovePref( int &iSel, bool bToSel, const ConfOption *pConfOption ) { IPreference *pPref = IPreference::GetPreferenceByName( pConfOption->m_sPrefName ); - ASSERT_M( pPref != NULL, pConfOption->m_sPrefName ); + ASSERT_M( pPref != nullptr, pConfOption->m_sPrefName ); if( bToSel ) { @@ -132,7 +132,7 @@ template <> void MovePref( int &iSel, bool bToSel, const ConfOption *pConfOption ) { IPreference *pPref = IPreference::GetPreferenceByName( pConfOption->m_sPrefName ); - ASSERT_M( pPref != NULL, pConfOption->m_sPrefName ); + ASSERT_M( pPref != nullptr, pConfOption->m_sPrefName ); if( bToSel ) { @@ -790,7 +790,7 @@ ConfOption *ConfOption::Find( RString name ) void ConfOption::UpdateAvailableOptions() { - if( MakeOptionsListCB != NULL ) + if( MakeOptionsListCB != nullptr ) { names.clear(); MakeOptionsListCB( names ); diff --git a/src/ScreenSelectMaster.cpp b/src/ScreenSelectMaster.cpp index f576c3a60c..15fc03ab76 100644 --- a/src/ScreenSelectMaster.cpp +++ b/src/ScreenSelectMaster.cpp @@ -775,7 +775,7 @@ float ScreenSelectMaster::DoMenuStart( PlayerNumber pn ) } if( SHOW_CURSOR ) { - if(m_sprCursor[pn] != NULL) + if(m_sprCursor[pn] != nullptr) { m_sprCursor[pn]->PlayCommand( "Choose" ); fSecs = max( fSecs, m_sprCursor[pn]->GetTweenTimeLeft() ); diff --git a/src/ScreenSelectMusic.cpp b/src/ScreenSelectMusic.cpp index bd83bc2875..b74b04b799 100644 --- a/src/ScreenSelectMusic.cpp +++ b/src/ScreenSelectMusic.cpp @@ -1190,7 +1190,7 @@ bool ScreenSelectMusic::MenuStart( const InputEventPlus &input ) return false; // a song was selected - if( m_MusicWheel.GetSelectedSong() != NULL ) + if( m_MusicWheel.GetSelectedSong() != nullptr ) { if(TWO_PART_CONFIRMS_ONLY && SAMPLE_MUSIC_PREVIEW_MODE == SampleMusicPreviewMode_StartToPreview) { @@ -1235,12 +1235,12 @@ bool ScreenSelectMusic::MenuStart( const InputEventPlus &input ) if( GAMESTATE->IsCourseMode() ) GAMESTATE->m_PlayMode.Set( PLAY_MODE_REGULAR ); } - else if( m_MusicWheel.GetSelectedCourse() != NULL ) + else if( m_MusicWheel.GetSelectedCourse() != nullptr ) { SOUND->PlayOnceFromAnnouncer( "select course comment general" ); Course *pCourse = m_MusicWheel.GetSelectedCourse(); - ASSERT( pCourse != NULL ); + ASSERT( pCourse != nullptr ); GAMESTATE->m_PlayMode.Set( pCourse->GetPlayMode() ); // apply #LIVES @@ -1430,12 +1430,12 @@ bool ScreenSelectMusic::MenuStart( const InputEventPlus &input ) PlayerNumber pn = GAMESTATE->GetMasterPlayerNumber(); if( GAMESTATE->IsCourseMode() ) { - ASSERT( GAMESTATE->m_pCurTrail[pn] != NULL ); + ASSERT( GAMESTATE->m_pCurTrail[pn] != nullptr ); stCurrent = GAMESTATE->m_pCurTrail[pn]->m_StepsType; } else { - ASSERT( GAMESTATE->m_pCurSteps[pn] != NULL ); + ASSERT( GAMESTATE->m_pCurSteps[pn] != nullptr ); stCurrent = GAMESTATE->m_pCurSteps[pn]->m_StepsType; } vector vst; diff --git a/src/ScreenServiceAction.cpp b/src/ScreenServiceAction.cpp index 6155bf6cc5..eb55486b5b 100644 --- a/src/ScreenServiceAction.cpp +++ b/src/ScreenServiceAction.cpp @@ -400,7 +400,7 @@ void ScreenServiceAction::BeginScreen() else if( s == "SyncEditsMachineToMemoryCard" ) pfn = SyncEditsMachineToMemoryCard; else if( s == "ResetPreferences" ) pfn = ResetPreferences; - ASSERT_M( pfn != NULL, s ); + ASSERT_M( pfn != nullptr, s ); RString sResult = pfn(); vsResults.push_back( sResult ); diff --git a/src/ScreenSyncOverlay.cpp b/src/ScreenSyncOverlay.cpp index f0e93f398c..185e80cb85 100644 --- a/src/ScreenSyncOverlay.cpp +++ b/src/ScreenSyncOverlay.cpp @@ -124,7 +124,7 @@ void ScreenSyncOverlay::UpdateText() FAIL_M(ssprintf("Invalid autosync type: %i", type)); } - if( GAMESTATE->m_pCurSong != NULL && !GAMESTATE->IsCourseMode() ) // sync controls available + if( GAMESTATE->m_pCurSong != nullptr && !GAMESTATE->IsCourseMode() ) // sync controls available { AdjustSync::GetSyncChangeTextGlobal( vs ); AdjustSync::GetSyncChangeTextSong( vs ); @@ -224,7 +224,7 @@ bool ScreenSyncOverlay::Input( const InputEventPlus &input ) } default: break; } - if( GAMESTATE->m_pCurSong != NULL ) + if( GAMESTATE->m_pCurSong != nullptr ) { TimingData &sTiming = GAMESTATE->m_pCurSong->m_SongTiming; BPMSegment * seg = sTiming.GetBPMSegmentAtBeat( GAMESTATE->m_Position.m_fSongBeat ); @@ -276,7 +276,7 @@ bool ScreenSyncOverlay::Input( const InputEventPlus &input ) case ChangeSongOffset: { - if( GAMESTATE->m_pCurSong != NULL ) + if( GAMESTATE->m_pCurSong != nullptr ) { GAMESTATE->m_pCurSong->m_SongTiming.m_fBeat0OffsetInSeconds += fDelta; const vector& vpSteps = GAMESTATE->m_pCurSong->GetAllSteps(); diff --git a/src/ScreenTextEntry.cpp b/src/ScreenTextEntry.cpp index 49e75bef12..9646ffb109 100644 --- a/src/ScreenTextEntry.cpp +++ b/src/ScreenTextEntry.cpp @@ -1,793 +1,793 @@ -#include "global.h" -#include "ScreenTextEntry.h" -#include "RageUtil.h" -#include "Preference.h" -#include "ScreenManager.h" -#include "ThemeManager.h" -#include "FontCharAliases.h" -#include "ScreenDimensions.h" -#include "ScreenPrompt.h" -#include "ActorUtil.h" -#include "InputEventPlus.h" -#include "RageInput.h" -#include "LocalizedString.h" -#include "RageLog.h" -#include "LuaBinding.h" - -static const char* g_szKeys[NUM_KeyboardRow][KEYS_PER_ROW] = -{ - {"A","B","C","D","E","F","G","H","I","J","K","L","M"}, - {"N","O","P","Q","R","S","T","U","V","W","X","Y","Z"}, - {"a","b","c","d","e","f","g","h","i","j","k","l","m"}, - {"n","o","p","q","r","s","t","u","v","w","x","y","z"}, - {"0","1","2","3","4","5","6","7","8","9","", "", "" }, - {"!","@","#","$","%","^","&","(",")","[","]","{","}"}, - {"+","-","=","_",",",".","'","\"",":","", "", "", ""}, - {"","","Space","","","Backspace","","","Cancel","","","Done",""}, -}; - -RString ScreenTextEntry::s_sLastAnswer = ""; - -// Settings: -namespace -{ - RString g_sQuestion; - RString g_sInitialAnswer; - int g_iMaxInputLength; - bool(*g_pValidate)(const RString &sAnswer,RString &sErrorOut); - void(*g_pOnOK)(const RString &sAnswer); - void(*g_pOnCancel)(); - bool g_bPassword; - bool (*g_pValidateAppend)(const RString &sAnswerBeforeChar, RString &sAppend); - RString (*g_pFormatAnswerForDisplay)(const RString &sAnswer); - - // Lua bridge - LuaReference g_ValidateFunc; - LuaReference g_OnOKFunc; - LuaReference g_OnCancelFunc; - LuaReference g_ValidateAppendFunc; - LuaReference g_FormatAnswerForDisplayFunc; -}; - -void ScreenTextEntry::SetTextEntrySettings( - RString sQuestion, - RString sInitialAnswer, - int iMaxInputLength, - bool(*Validate)(const RString &sAnswer,RString &sErrorOut), - void(*OnOK)(const RString &sAnswer), - void(*OnCancel)(), - bool bPassword, - bool (*ValidateAppend)(const RString &sAnswerBeforeChar, RString &sAppend), - RString (*FormatAnswerForDisplay)(const RString &sAnswer) - ) -{ - g_sQuestion = sQuestion; - g_sInitialAnswer = sInitialAnswer; - g_iMaxInputLength = iMaxInputLength; - g_pValidate = Validate; - g_pOnOK = OnOK; - g_pOnCancel = OnCancel; - g_pValidateAppend = ValidateAppend; - g_pFormatAnswerForDisplay = FormatAnswerForDisplay; -} - -void ScreenTextEntry::TextEntry( - ScreenMessage smSendOnPop, - RString sQuestion, - RString sInitialAnswer, - int iMaxInputLength, - bool(*Validate)(const RString &sAnswer,RString &sErrorOut), - void(*OnOK)(const RString &sAnswer), - void(*OnCancel)(), - bool bPassword, - bool (*ValidateAppend)(const RString &sAnswerBeforeChar, RString &sAppend), - RString (*FormatAnswerForDisplay)(const RString &sAnswer) - ) -{ - g_sQuestion = sQuestion; - g_sInitialAnswer = sInitialAnswer; - g_iMaxInputLength = iMaxInputLength; - g_pValidate = Validate; - g_pOnOK = OnOK; - g_pOnCancel = OnCancel; - g_bPassword = bPassword; - g_pValidateAppend = ValidateAppend; - g_pFormatAnswerForDisplay = FormatAnswerForDisplay; - - SCREENMAN->AddNewScreenToTop( "ScreenTextEntry", smSendOnPop ); -} - -static LocalizedString INVALID_FLOAT( "ScreenTextEntry", "\"%s\" is an invalid floating point value." ); -bool ScreenTextEntry::FloatValidate( const RString &sAnswer, RString &sErrorOut ) -{ - float f; - if( StringToFloat(sAnswer, f) ) - return true; - sErrorOut = ssprintf( INVALID_FLOAT.GetValue(), sAnswer.c_str() ); - return false; -} - -bool ScreenTextEntry::s_bCancelledLast = false; - -/* Handle UTF-8. Right now, we need to at least be able to backspace a whole - * UTF-8 character. Better would be to operate in wchar_t. - * - * XXX: Don't allow internal-use codepoints (above 0xFFFF); those are subject to - * change and shouldn't be written to disk. */ -REGISTER_SCREEN_CLASS( ScreenTextEntry ); -REGISTER_SCREEN_CLASS( ScreenTextEntryVisual ); - -void ScreenTextEntry::Init() -{ - ScreenWithMenuElements::Init(); - - m_textQuestion.LoadFromFont( THEME->GetPathF(m_sName,"question") ); - m_textQuestion.SetName( "Question" ); - LOAD_ALL_COMMANDS( m_textQuestion ); - this->AddChild( &m_textQuestion ); - - m_textAnswer.LoadFromFont( THEME->GetPathF(m_sName,"answer") ); - m_textAnswer.SetName( "Answer" ); - LOAD_ALL_COMMANDS( m_textAnswer ); - this->AddChild( &m_textAnswer ); - - m_bShowAnswerCaret = false; - //m_iCaretLocation = 0; - - m_sndType.Load( THEME->GetPathS(m_sName,"type"), true ); - m_sndBackspace.Load( THEME->GetPathS(m_sName,"backspace"), true ); -} - -void ScreenTextEntry::BeginScreen() -{ - m_sAnswer = RStringToWstring( g_sInitialAnswer ); - - ScreenWithMenuElements::BeginScreen(); - - m_textQuestion.SetText( g_sQuestion ); - SET_XY( m_textQuestion ); - SET_XY( m_textAnswer ); - - UpdateAnswerText(); -} - -static LocalizedString ANSWER_CARET ( "ScreenTextEntry", "AnswerCaret" ); -static LocalizedString ANSWER_BLANK ( "ScreenTextEntry", "AnswerBlank" ); -void ScreenTextEntry::UpdateAnswerText() -{ - RString s; - if( g_bPassword ) - s = RString( m_sAnswer.size(), '*' ); - else - s = WStringToRString(m_sAnswer); - - bool bAnswerFull = (int) s.length() >= g_iMaxInputLength; - - if( g_pFormatAnswerForDisplay ) - s = g_pFormatAnswerForDisplay( s ); - - // Handle caret drawing - //m_iCaretLocation = s.length() - if( m_bShowAnswerCaret && !bAnswerFull ) - s += ANSWER_CARET; // was '_' - else - { - s += ANSWER_BLANK; // was " " - } - - FontCharAliases::ReplaceMarkers( s ); - m_textAnswer.SetText( s ); -} - -void ScreenTextEntry::Update( float fDelta ) -{ - ScreenWithMenuElements::Update( fDelta ); - - if( m_timerToggleCursor.PeekDeltaTime() > 0.25f ) - { - m_timerToggleCursor.Touch(); - m_bShowAnswerCaret = !m_bShowAnswerCaret; - UpdateAnswerText(); - } -} - -bool ScreenTextEntry::Input( const InputEventPlus &input ) -{ - if( IsTransitioning() ) - return false; - - bool bHandled = false; - if( input.DeviceI == DeviceInput(DEVICE_KEYBOARD, KEY_BACK) ) - { - switch( input.type ) - { - case IET_FIRST_PRESS: - case IET_REPEAT: - BackspaceInAnswer(); - bHandled = true; - default: - break; - } - } - else if( input.type == IET_FIRST_PRESS ) - { - wchar_t c = INPUTMAN->DeviceInputToChar(input.DeviceI,true); - if( c >= L' ' ) - { - // todo: handle caps lock -aj - TryAppendToAnswer( WStringToRString(wstring()+c) ); - - TextEnteredDirectly(); - bHandled = true; - } - } - - return ScreenWithMenuElements::Input( input ) || bHandled; -} - -void ScreenTextEntry::TryAppendToAnswer( RString s ) -{ - { - wstring sNewAnswer = m_sAnswer+RStringToWstring(s); - if( (int)sNewAnswer.length() > g_iMaxInputLength ) - { - SCREENMAN->PlayInvalidSound(); - return; - } - } - - if( g_pValidateAppend && !g_pValidateAppend( WStringToRString(m_sAnswer), s ) ) - { - SCREENMAN->PlayInvalidSound(); - return; - } - - wstring sNewAnswer = m_sAnswer+RStringToWstring(s); - m_sAnswer = sNewAnswer; - m_sndType.Play(); - UpdateAnswerText(); -} - -void ScreenTextEntry::BackspaceInAnswer() -{ - if( m_sAnswer.empty() ) - { - SCREENMAN->PlayInvalidSound(); - return; - } - m_sAnswer.erase( m_sAnswer.end()-1 ); - m_sndBackspace.Play(); - UpdateAnswerText(); -} - -bool ScreenTextEntry::MenuStart( const InputEventPlus &input ) -{ - // HACK: Only allow the screen to end on the Enter key.-aj - if( input.DeviceI == DeviceInput(DEVICE_KEYBOARD, KEY_ENTER) && input.type==IET_FIRST_PRESS ) - { - End( false ); - return true; - } - return false; -} - -void ScreenTextEntry::End( bool bCancelled ) -{ - if( bCancelled ) - { - if( g_pOnCancel ) - g_pOnCancel(); - - Cancel( SM_GoToNextScreen ); - //TweenOffScreen(); - } - else - { - RString sAnswer = WStringToRString(m_sAnswer); - RString sError; - if( g_pValidate != NULL ) - { - bool bValidAnswer = g_pValidate( sAnswer, sError ); - if( !bValidAnswer ) - { - ScreenPrompt::Prompt( SM_None, sError ); - return; // don't end this screen. - } - } - - if( g_pOnOK ) - { - RString ret = WStringToRString(m_sAnswer); - FontCharAliases::ReplaceMarkers(ret); - g_pOnOK( ret ); - } - - StartTransitioningScreen( SM_GoToNextScreen ); - SCREENMAN->PlayStartSound(); - } - - s_bCancelledLast = bCancelled; - s_sLastAnswer = bCancelled ? RString("") : WStringToRString(m_sAnswer); -} - -bool ScreenTextEntry::MenuBack( const InputEventPlus &input ) -{ - if( input.type != IET_FIRST_PRESS ) - return false; - End( true ); - return true; -} - -void ScreenTextEntry::TextEntrySettings::FromStack( lua_State *L ) -{ - if( lua_type(L, 1) != LUA_TTABLE ) - { - LOG->Trace("not a table"); - return; - } - - lua_pushvalue( L, 1 ); - const int iTab = lua_gettop( L ); - - // Get ScreenMessage - lua_getfield( L, iTab, "SendOnPop" ); - const char *pStr = lua_tostring( L, -1 ); - if( pStr == NULL ) - smSendOnPop = SM_None; - else - smSendOnPop = ScreenMessageHelpers::ToScreenMessage( pStr ); - lua_settop( L, iTab ); - - // Get Question - lua_getfield( L, iTab, "Question" ); - pStr = lua_tostring( L, -1 ); - if( pStr == NULL ) - RageException::Throw( "\"Question\" entry is not a string." ); - sQuestion = pStr; - lua_settop( L, iTab ); - - // Get Initial Answer - lua_getfield( L, iTab, "InitialAnswer" ); - pStr = lua_tostring( L, -1 ); - if( pStr == NULL ) - pStr = ""; - sInitialAnswer = pStr; - lua_settop( L, iTab ); - - // Get Max Input Length - lua_getfield( L, iTab, "MaxInputLength" ); - iMaxInputLength = lua_tointeger( L, -1 ); - lua_settop( L, iTab ); - - // Get Password - lua_getfield( L, iTab, "Password" ); - bPassword = !!lua_toboolean( L, -1 ); - lua_settop( L, iTab ); - - // and now the hard part, the functions. - // Validate - lua_getfield( L, iTab, "Validate" ); - if( !lua_isfunction( L, -1 ) && !lua_isnil( L, -1 ) ) - RageException::Throw( "\"Validate\" is not a function." ); - Validate.SetFromStack( L ); - lua_settop( L, iTab ); - - // OnOK - lua_getfield( L, iTab, "OnOK" ); - if( !lua_isfunction( L, -1 ) && !lua_isnil( L, -1 ) ) - RageException::Throw( "\"OnOK\" is not a function." ); - OnOK.SetFromStack( L ); - lua_settop( L, iTab ); - - // OnCancel - lua_getfield( L, iTab, "OnCancel" ); - if( !lua_isfunction( L, -1 ) && !lua_isnil( L, -1 ) ) - RageException::Throw( "\"OnCancel\" is not a function." ); - OnCancel.SetFromStack( L ); - lua_settop( L, iTab ); - - // ValidateAppend - lua_getfield( L, iTab, "ValidateAppend" ); - if( !lua_isfunction( L, -1 ) && !lua_isnil( L, -1 ) ) - RageException::Throw( "\"ValidateAppend\" is not a function." ); - ValidateAppend.SetFromStack( L ); - lua_settop( L, iTab ); - - // FormatAnswerForDisplay - lua_getfield( L, iTab, "FormatAnswerForDisplay" ); - if( !lua_isfunction( L, -1 ) && !lua_isnil( L, -1 ) ) - RageException::Throw( "\"FormatAnswerForDisplay\" is not a function." ); - FormatAnswerForDisplay.SetFromStack( L ); - lua_settop( L, iTab ); -} - -// Lua bridges -static bool ValidateFromLua( const RString &sAnswer, RString &sErrorOut ) -{ - Lua *L = LUA->Get(); - - if( g_ValidateFunc.IsNil() ) - { - LUA->Release(L); - return true; - } - - g_ValidateFunc.PushSelf( L ); - - // Argument 1 (answer): - lua_pushstring( L, sAnswer ); - - // Argument 2 (error out): - lua_pushstring( L, sErrorOut ); - - lua_call( L, 2, 2 ); // call function with 2 arguments and 2 results - - if( !lua_isstring(L, -1) ) - RageException::Throw( "\"Validate\" did not return a string." ); - - if( !lua_isboolean(L, -2) ) - RageException::Throw( "\"Validate\" did not return a boolean." ); - - RString sErrorFromLua; - LuaHelpers::Pop( L, sErrorFromLua ); - if( !sErrorFromLua.empty() ) - sErrorOut = sErrorFromLua; - - bool bValidate; - LuaHelpers::Pop( L, bValidate ); - - LUA->Release(L); - return bValidate; -} - -static void OnOKFromLua( const RString &sAnswer ) -{ - Lua *L = LUA->Get(); - - if( g_OnOKFunc.IsNil() ) - { - LUA->Release(L); - return; - } - - g_OnOKFunc.PushSelf( L ); - // Argument 1 (answer): - lua_pushstring( L, sAnswer ); - lua_call( L, 1, 0 ); // call function with 1 argument and 0 results - - LUA->Release(L); -} - -static void OnCancelFromLua() -{ - Lua *L = LUA->Get(); - - if( g_OnCancelFunc.IsNil() ) - { - LUA->Release(L); - return; - } - - g_OnCancelFunc.PushSelf( L ); - lua_call( L, 0, 0 ); // call function with 0 arguments and 0 results - - LUA->Release(L); -} - -static bool ValidateAppendFromLua( const RString &sAnswerBeforeChar, RString &sAppend ) -{ - Lua *L = LUA->Get(); - - if( g_ValidateAppendFunc.IsNil() ) - { - LUA->Release(L); - return true; - } - - g_ValidateAppendFunc.PushSelf( L ); - - // Argument 1 (AnswerBeforeChar): - lua_pushstring( L, sAnswerBeforeChar ); - - // Argument 2 (Append): - lua_pushstring( L, sAppend ); - lua_call( L, 2, 1 ); // call function with 2 arguments and 1 result - - if( !lua_isboolean(L, -1) ) - RageException::Throw( "\"ValidateAppend\" did not return a boolean." ); - - bool bAppend; - LuaHelpers::Pop( L, bAppend ); - - LUA->Release(L); - return bAppend; -} - -static RString FormatAnswerForDisplayFromLua( const RString &sAnswer ) -{ - Lua *L = LUA->Get(); - - if( g_FormatAnswerForDisplayFunc.IsNil() ) - { - LUA->Release(L); - return sAnswer; - } - - g_FormatAnswerForDisplayFunc.PushSelf( L ); - // Argument 1 (Answer): - lua_pushstring( L, sAnswer ); - lua_call( L, 1, 1 ); // call function with 1 argument and 1 result - - if( !lua_isstring(L, -1) ) - RageException::Throw( "\"FormatAnswerForDisplay\" did not return a string." ); - - RString sAnswerFromLua; - LuaHelpers::Pop( L, sAnswerFromLua ); - - LUA->Release(L); - return sAnswerFromLua; -} - -void ScreenTextEntry::LoadFromTextEntrySettings( const TextEntrySettings &settings ) -{ - g_ValidateFunc = settings.Validate; - g_OnOKFunc = settings.OnOK; - g_OnCancelFunc = settings.OnCancel; - g_ValidateAppendFunc = settings.ValidateAppend; - g_FormatAnswerForDisplayFunc = settings.FormatAnswerForDisplay; - - // set functions - SetTextEntrySettings( - settings.sQuestion, - settings.sInitialAnswer, - settings.iMaxInputLength, - ValidateFromLua, // Validate - OnOKFromLua, // OnOK - OnCancelFromLua, // OnCancel - settings.bPassword, - ValidateAppendFromLua, // ValidateAppend - FormatAnswerForDisplayFromLua // FormatAnswerForDisplay - ); - - // Hack: reload screen with new info - BeginScreen(); -} - -/** @brief Allow Lua to have access to the ScreenTextEntry. */ -class LunaScreenTextEntry: public Luna -{ -public: - static int Load( T* p, lua_State *L ) - { - ScreenTextEntry::TextEntrySettings settings; - settings.FromStack( L ); - p->LoadFromTextEntrySettings(settings); - return 0; - } - - LunaScreenTextEntry() - { - ADD_METHOD( Load ); - } -}; -LUA_REGISTER_DERIVED_CLASS( ScreenTextEntry, ScreenWithMenuElements ) -// lua end - -// begin ScreenTextEntryVisual -void ScreenTextEntryVisual::Init() -{ - ROW_START_X.Load( m_sName, "RowStartX" ); - ROW_START_Y.Load( m_sName, "RowStartY" ); - ROW_END_X.Load( m_sName, "RowEndX" ); - ROW_END_Y.Load( m_sName, "RowEndY" ); - - ScreenTextEntry::Init(); - - m_sprCursor.Load( THEME->GetPathG(m_sName,"cursor") ); - m_sprCursor->SetName( "Cursor" ); - LOAD_ALL_COMMANDS( m_sprCursor ); - this->AddChild( m_sprCursor ); - - // Init keyboard - { - BitmapText text; - text.LoadFromFont( THEME->GetPathF(m_sName,"keyboard") ); - text.SetName( "Keys" ); - ActorUtil::LoadAllCommands( text, m_sName ); - text.PlayCommand( "Init" ); - - FOREACH_KeyboardRow( r ) - { - for( int x=0; xAddChild( pbt ); - - RString s = g_szKeys[r][x]; - if( !s.empty() && r == KEYBOARD_ROW_SPECIAL ) - s = THEME->GetString( m_sName, s ); - pbt->SetText( s ); - } - } - } - - m_sndChange.Load( THEME->GetPathS(m_sName,"change"), true ); -} - -ScreenTextEntryVisual::~ScreenTextEntryVisual() -{ - FOREACH_KeyboardRow( r ) - for( int x=0; xSetXY( bt.GetX(), bt.GetY() ); - m_sprCursor->PlayCommand( m_iFocusY == KEYBOARD_ROW_SPECIAL ? "SpecialKey" : "RegularKey" ); -} - -void ScreenTextEntryVisual::TextEnteredDirectly() -{ - // If the user enters text with a keyboard, jump to DONE, so enter ends the screen. - m_iFocusY = KEYBOARD_ROW_SPECIAL; - m_iFocusX = DONE; - - PositionCursor(); -} - -void ScreenTextEntryVisual::MoveX( int iDir ) -{ - RString sKey; - do - { - m_iFocusX += iDir; - wrap( m_iFocusX, KEYS_PER_ROW ); - - sKey = g_szKeys[m_iFocusY][m_iFocusX]; - } - while( sKey == "" ); - - m_sndChange.Play(); - PositionCursor(); -} - -void ScreenTextEntryVisual::MoveY( int iDir ) -{ - RString sKey; - do - { - m_iFocusY = enum_add2( m_iFocusY, +iDir ); - wrap( *ConvertValue(&m_iFocusY), NUM_KeyboardRow ); - - // HACK: Round to nearest option so that we always stop - // on KEYBOARD_ROW_SPECIAL. - if( m_iFocusY == KEYBOARD_ROW_SPECIAL ) - { - for( int i=0; true; i++ ) - { - sKey = g_szKeys[m_iFocusY][m_iFocusX]; - if( sKey != "" ) - break; - - // UGLY: Probe one space to the left before looking to the right - m_iFocusX += (i==0) ? -1 : +1; - wrap( m_iFocusX, KEYS_PER_ROW ); - } - } - - sKey = g_szKeys[m_iFocusY][m_iFocusX]; - } - while( sKey == "" ); - - m_sndChange.Play(); - PositionCursor(); -} - -bool ScreenTextEntryVisual::MenuLeft( const InputEventPlus &input ) -{ - if( input.type != IET_FIRST_PRESS ) - return false; - MoveX(-1); - return true; -} -bool ScreenTextEntryVisual::MenuRight( const InputEventPlus &input ) -{ - if( input.type != IET_FIRST_PRESS ) - return false; - MoveX(+1); - return true; -} -bool ScreenTextEntryVisual::MenuUp( const InputEventPlus &input ) -{ - if( input.type != IET_FIRST_PRESS ) - return false; - MoveY(-1); - return true; -} -bool ScreenTextEntryVisual::MenuDown( const InputEventPlus &input ) -{ - if( input.type != IET_FIRST_PRESS ) - return false; - MoveY(+1); - return true; -} - -bool ScreenTextEntryVisual::MenuStart( const InputEventPlus &input ) -{ - if( input.type != IET_FIRST_PRESS ) - return false; - if( m_iFocusY == KEYBOARD_ROW_SPECIAL ) - { - switch( m_iFocusX ) - { - case SPACEBAR: - TryAppendToAnswer( " " ); - break; - case BACKSPACE: - BackspaceInAnswer(); - break; - case CANCEL: - End( true ); - break; - case DONE: - End( false ); - break; - default: - return false; - } - } - else - { - TryAppendToAnswer( g_szKeys[m_iFocusY][m_iFocusX] ); - } - return true; -} - -/* - * (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. - */ +#include "global.h" +#include "ScreenTextEntry.h" +#include "RageUtil.h" +#include "Preference.h" +#include "ScreenManager.h" +#include "ThemeManager.h" +#include "FontCharAliases.h" +#include "ScreenDimensions.h" +#include "ScreenPrompt.h" +#include "ActorUtil.h" +#include "InputEventPlus.h" +#include "RageInput.h" +#include "LocalizedString.h" +#include "RageLog.h" +#include "LuaBinding.h" + +static const char* g_szKeys[NUM_KeyboardRow][KEYS_PER_ROW] = +{ + {"A","B","C","D","E","F","G","H","I","J","K","L","M"}, + {"N","O","P","Q","R","S","T","U","V","W","X","Y","Z"}, + {"a","b","c","d","e","f","g","h","i","j","k","l","m"}, + {"n","o","p","q","r","s","t","u","v","w","x","y","z"}, + {"0","1","2","3","4","5","6","7","8","9","", "", "" }, + {"!","@","#","$","%","^","&","(",")","[","]","{","}"}, + {"+","-","=","_",",",".","'","\"",":","", "", "", ""}, + {"","","Space","","","Backspace","","","Cancel","","","Done",""}, +}; + +RString ScreenTextEntry::s_sLastAnswer = ""; + +// Settings: +namespace +{ + RString g_sQuestion; + RString g_sInitialAnswer; + int g_iMaxInputLength; + bool(*g_pValidate)(const RString &sAnswer,RString &sErrorOut); + void(*g_pOnOK)(const RString &sAnswer); + void(*g_pOnCancel)(); + bool g_bPassword; + bool (*g_pValidateAppend)(const RString &sAnswerBeforeChar, RString &sAppend); + RString (*g_pFormatAnswerForDisplay)(const RString &sAnswer); + + // Lua bridge + LuaReference g_ValidateFunc; + LuaReference g_OnOKFunc; + LuaReference g_OnCancelFunc; + LuaReference g_ValidateAppendFunc; + LuaReference g_FormatAnswerForDisplayFunc; +}; + +void ScreenTextEntry::SetTextEntrySettings( + RString sQuestion, + RString sInitialAnswer, + int iMaxInputLength, + bool(*Validate)(const RString &sAnswer,RString &sErrorOut), + void(*OnOK)(const RString &sAnswer), + void(*OnCancel)(), + bool bPassword, + bool (*ValidateAppend)(const RString &sAnswerBeforeChar, RString &sAppend), + RString (*FormatAnswerForDisplay)(const RString &sAnswer) + ) +{ + g_sQuestion = sQuestion; + g_sInitialAnswer = sInitialAnswer; + g_iMaxInputLength = iMaxInputLength; + g_pValidate = Validate; + g_pOnOK = OnOK; + g_pOnCancel = OnCancel; + g_pValidateAppend = ValidateAppend; + g_pFormatAnswerForDisplay = FormatAnswerForDisplay; +} + +void ScreenTextEntry::TextEntry( + ScreenMessage smSendOnPop, + RString sQuestion, + RString sInitialAnswer, + int iMaxInputLength, + bool(*Validate)(const RString &sAnswer,RString &sErrorOut), + void(*OnOK)(const RString &sAnswer), + void(*OnCancel)(), + bool bPassword, + bool (*ValidateAppend)(const RString &sAnswerBeforeChar, RString &sAppend), + RString (*FormatAnswerForDisplay)(const RString &sAnswer) + ) +{ + g_sQuestion = sQuestion; + g_sInitialAnswer = sInitialAnswer; + g_iMaxInputLength = iMaxInputLength; + g_pValidate = Validate; + g_pOnOK = OnOK; + g_pOnCancel = OnCancel; + g_bPassword = bPassword; + g_pValidateAppend = ValidateAppend; + g_pFormatAnswerForDisplay = FormatAnswerForDisplay; + + SCREENMAN->AddNewScreenToTop( "ScreenTextEntry", smSendOnPop ); +} + +static LocalizedString INVALID_FLOAT( "ScreenTextEntry", "\"%s\" is an invalid floating point value." ); +bool ScreenTextEntry::FloatValidate( const RString &sAnswer, RString &sErrorOut ) +{ + float f; + if( StringToFloat(sAnswer, f) ) + return true; + sErrorOut = ssprintf( INVALID_FLOAT.GetValue(), sAnswer.c_str() ); + return false; +} + +bool ScreenTextEntry::s_bCancelledLast = false; + +/* Handle UTF-8. Right now, we need to at least be able to backspace a whole + * UTF-8 character. Better would be to operate in wchar_t. + * + * XXX: Don't allow internal-use codepoints (above 0xFFFF); those are subject to + * change and shouldn't be written to disk. */ +REGISTER_SCREEN_CLASS( ScreenTextEntry ); +REGISTER_SCREEN_CLASS( ScreenTextEntryVisual ); + +void ScreenTextEntry::Init() +{ + ScreenWithMenuElements::Init(); + + m_textQuestion.LoadFromFont( THEME->GetPathF(m_sName,"question") ); + m_textQuestion.SetName( "Question" ); + LOAD_ALL_COMMANDS( m_textQuestion ); + this->AddChild( &m_textQuestion ); + + m_textAnswer.LoadFromFont( THEME->GetPathF(m_sName,"answer") ); + m_textAnswer.SetName( "Answer" ); + LOAD_ALL_COMMANDS( m_textAnswer ); + this->AddChild( &m_textAnswer ); + + m_bShowAnswerCaret = false; + //m_iCaretLocation = 0; + + m_sndType.Load( THEME->GetPathS(m_sName,"type"), true ); + m_sndBackspace.Load( THEME->GetPathS(m_sName,"backspace"), true ); +} + +void ScreenTextEntry::BeginScreen() +{ + m_sAnswer = RStringToWstring( g_sInitialAnswer ); + + ScreenWithMenuElements::BeginScreen(); + + m_textQuestion.SetText( g_sQuestion ); + SET_XY( m_textQuestion ); + SET_XY( m_textAnswer ); + + UpdateAnswerText(); +} + +static LocalizedString ANSWER_CARET ( "ScreenTextEntry", "AnswerCaret" ); +static LocalizedString ANSWER_BLANK ( "ScreenTextEntry", "AnswerBlank" ); +void ScreenTextEntry::UpdateAnswerText() +{ + RString s; + if( g_bPassword ) + s = RString( m_sAnswer.size(), '*' ); + else + s = WStringToRString(m_sAnswer); + + bool bAnswerFull = (int) s.length() >= g_iMaxInputLength; + + if( g_pFormatAnswerForDisplay ) + s = g_pFormatAnswerForDisplay( s ); + + // Handle caret drawing + //m_iCaretLocation = s.length() + if( m_bShowAnswerCaret && !bAnswerFull ) + s += ANSWER_CARET; // was '_' + else + { + s += ANSWER_BLANK; // was " " + } + + FontCharAliases::ReplaceMarkers( s ); + m_textAnswer.SetText( s ); +} + +void ScreenTextEntry::Update( float fDelta ) +{ + ScreenWithMenuElements::Update( fDelta ); + + if( m_timerToggleCursor.PeekDeltaTime() > 0.25f ) + { + m_timerToggleCursor.Touch(); + m_bShowAnswerCaret = !m_bShowAnswerCaret; + UpdateAnswerText(); + } +} + +bool ScreenTextEntry::Input( const InputEventPlus &input ) +{ + if( IsTransitioning() ) + return false; + + bool bHandled = false; + if( input.DeviceI == DeviceInput(DEVICE_KEYBOARD, KEY_BACK) ) + { + switch( input.type ) + { + case IET_FIRST_PRESS: + case IET_REPEAT: + BackspaceInAnswer(); + bHandled = true; + default: + break; + } + } + else if( input.type == IET_FIRST_PRESS ) + { + wchar_t c = INPUTMAN->DeviceInputToChar(input.DeviceI,true); + if( c >= L' ' ) + { + // todo: handle caps lock -aj + TryAppendToAnswer( WStringToRString(wstring()+c) ); + + TextEnteredDirectly(); + bHandled = true; + } + } + + return ScreenWithMenuElements::Input( input ) || bHandled; +} + +void ScreenTextEntry::TryAppendToAnswer( RString s ) +{ + { + wstring sNewAnswer = m_sAnswer+RStringToWstring(s); + if( (int)sNewAnswer.length() > g_iMaxInputLength ) + { + SCREENMAN->PlayInvalidSound(); + return; + } + } + + if( g_pValidateAppend && !g_pValidateAppend( WStringToRString(m_sAnswer), s ) ) + { + SCREENMAN->PlayInvalidSound(); + return; + } + + wstring sNewAnswer = m_sAnswer+RStringToWstring(s); + m_sAnswer = sNewAnswer; + m_sndType.Play(); + UpdateAnswerText(); +} + +void ScreenTextEntry::BackspaceInAnswer() +{ + if( m_sAnswer.empty() ) + { + SCREENMAN->PlayInvalidSound(); + return; + } + m_sAnswer.erase( m_sAnswer.end()-1 ); + m_sndBackspace.Play(); + UpdateAnswerText(); +} + +bool ScreenTextEntry::MenuStart( const InputEventPlus &input ) +{ + // HACK: Only allow the screen to end on the Enter key.-aj + if( input.DeviceI == DeviceInput(DEVICE_KEYBOARD, KEY_ENTER) && input.type==IET_FIRST_PRESS ) + { + End( false ); + return true; + } + return false; +} + +void ScreenTextEntry::End( bool bCancelled ) +{ + if( bCancelled ) + { + if( g_pOnCancel ) + g_pOnCancel(); + + Cancel( SM_GoToNextScreen ); + //TweenOffScreen(); + } + else + { + RString sAnswer = WStringToRString(m_sAnswer); + RString sError; + if( g_pValidate != nullptr ) + { + bool bValidAnswer = g_pValidate( sAnswer, sError ); + if( !bValidAnswer ) + { + ScreenPrompt::Prompt( SM_None, sError ); + return; // don't end this screen. + } + } + + if( g_pOnOK ) + { + RString ret = WStringToRString(m_sAnswer); + FontCharAliases::ReplaceMarkers(ret); + g_pOnOK( ret ); + } + + StartTransitioningScreen( SM_GoToNextScreen ); + SCREENMAN->PlayStartSound(); + } + + s_bCancelledLast = bCancelled; + s_sLastAnswer = bCancelled ? RString("") : WStringToRString(m_sAnswer); +} + +bool ScreenTextEntry::MenuBack( const InputEventPlus &input ) +{ + if( input.type != IET_FIRST_PRESS ) + return false; + End( true ); + return true; +} + +void ScreenTextEntry::TextEntrySettings::FromStack( lua_State *L ) +{ + if( lua_type(L, 1) != LUA_TTABLE ) + { + LOG->Trace("not a table"); + return; + } + + lua_pushvalue( L, 1 ); + const int iTab = lua_gettop( L ); + + // Get ScreenMessage + lua_getfield( L, iTab, "SendOnPop" ); + const char *pStr = lua_tostring( L, -1 ); + if( pStr == NULL ) + smSendOnPop = SM_None; + else + smSendOnPop = ScreenMessageHelpers::ToScreenMessage( pStr ); + lua_settop( L, iTab ); + + // Get Question + lua_getfield( L, iTab, "Question" ); + pStr = lua_tostring( L, -1 ); + if( pStr == NULL ) + RageException::Throw( "\"Question\" entry is not a string." ); + sQuestion = pStr; + lua_settop( L, iTab ); + + // Get Initial Answer + lua_getfield( L, iTab, "InitialAnswer" ); + pStr = lua_tostring( L, -1 ); + if( pStr == NULL ) + pStr = ""; + sInitialAnswer = pStr; + lua_settop( L, iTab ); + + // Get Max Input Length + lua_getfield( L, iTab, "MaxInputLength" ); + iMaxInputLength = lua_tointeger( L, -1 ); + lua_settop( L, iTab ); + + // Get Password + lua_getfield( L, iTab, "Password" ); + bPassword = !!lua_toboolean( L, -1 ); + lua_settop( L, iTab ); + + // and now the hard part, the functions. + // Validate + lua_getfield( L, iTab, "Validate" ); + if( !lua_isfunction( L, -1 ) && !lua_isnil( L, -1 ) ) + RageException::Throw( "\"Validate\" is not a function." ); + Validate.SetFromStack( L ); + lua_settop( L, iTab ); + + // OnOK + lua_getfield( L, iTab, "OnOK" ); + if( !lua_isfunction( L, -1 ) && !lua_isnil( L, -1 ) ) + RageException::Throw( "\"OnOK\" is not a function." ); + OnOK.SetFromStack( L ); + lua_settop( L, iTab ); + + // OnCancel + lua_getfield( L, iTab, "OnCancel" ); + if( !lua_isfunction( L, -1 ) && !lua_isnil( L, -1 ) ) + RageException::Throw( "\"OnCancel\" is not a function." ); + OnCancel.SetFromStack( L ); + lua_settop( L, iTab ); + + // ValidateAppend + lua_getfield( L, iTab, "ValidateAppend" ); + if( !lua_isfunction( L, -1 ) && !lua_isnil( L, -1 ) ) + RageException::Throw( "\"ValidateAppend\" is not a function." ); + ValidateAppend.SetFromStack( L ); + lua_settop( L, iTab ); + + // FormatAnswerForDisplay + lua_getfield( L, iTab, "FormatAnswerForDisplay" ); + if( !lua_isfunction( L, -1 ) && !lua_isnil( L, -1 ) ) + RageException::Throw( "\"FormatAnswerForDisplay\" is not a function." ); + FormatAnswerForDisplay.SetFromStack( L ); + lua_settop( L, iTab ); +} + +// Lua bridges +static bool ValidateFromLua( const RString &sAnswer, RString &sErrorOut ) +{ + Lua *L = LUA->Get(); + + if( g_ValidateFunc.IsNil() ) + { + LUA->Release(L); + return true; + } + + g_ValidateFunc.PushSelf( L ); + + // Argument 1 (answer): + lua_pushstring( L, sAnswer ); + + // Argument 2 (error out): + lua_pushstring( L, sErrorOut ); + + lua_call( L, 2, 2 ); // call function with 2 arguments and 2 results + + if( !lua_isstring(L, -1) ) + RageException::Throw( "\"Validate\" did not return a string." ); + + if( !lua_isboolean(L, -2) ) + RageException::Throw( "\"Validate\" did not return a boolean." ); + + RString sErrorFromLua; + LuaHelpers::Pop( L, sErrorFromLua ); + if( !sErrorFromLua.empty() ) + sErrorOut = sErrorFromLua; + + bool bValidate; + LuaHelpers::Pop( L, bValidate ); + + LUA->Release(L); + return bValidate; +} + +static void OnOKFromLua( const RString &sAnswer ) +{ + Lua *L = LUA->Get(); + + if( g_OnOKFunc.IsNil() ) + { + LUA->Release(L); + return; + } + + g_OnOKFunc.PushSelf( L ); + // Argument 1 (answer): + lua_pushstring( L, sAnswer ); + lua_call( L, 1, 0 ); // call function with 1 argument and 0 results + + LUA->Release(L); +} + +static void OnCancelFromLua() +{ + Lua *L = LUA->Get(); + + if( g_OnCancelFunc.IsNil() ) + { + LUA->Release(L); + return; + } + + g_OnCancelFunc.PushSelf( L ); + lua_call( L, 0, 0 ); // call function with 0 arguments and 0 results + + LUA->Release(L); +} + +static bool ValidateAppendFromLua( const RString &sAnswerBeforeChar, RString &sAppend ) +{ + Lua *L = LUA->Get(); + + if( g_ValidateAppendFunc.IsNil() ) + { + LUA->Release(L); + return true; + } + + g_ValidateAppendFunc.PushSelf( L ); + + // Argument 1 (AnswerBeforeChar): + lua_pushstring( L, sAnswerBeforeChar ); + + // Argument 2 (Append): + lua_pushstring( L, sAppend ); + lua_call( L, 2, 1 ); // call function with 2 arguments and 1 result + + if( !lua_isboolean(L, -1) ) + RageException::Throw( "\"ValidateAppend\" did not return a boolean." ); + + bool bAppend; + LuaHelpers::Pop( L, bAppend ); + + LUA->Release(L); + return bAppend; +} + +static RString FormatAnswerForDisplayFromLua( const RString &sAnswer ) +{ + Lua *L = LUA->Get(); + + if( g_FormatAnswerForDisplayFunc.IsNil() ) + { + LUA->Release(L); + return sAnswer; + } + + g_FormatAnswerForDisplayFunc.PushSelf( L ); + // Argument 1 (Answer): + lua_pushstring( L, sAnswer ); + lua_call( L, 1, 1 ); // call function with 1 argument and 1 result + + if( !lua_isstring(L, -1) ) + RageException::Throw( "\"FormatAnswerForDisplay\" did not return a string." ); + + RString sAnswerFromLua; + LuaHelpers::Pop( L, sAnswerFromLua ); + + LUA->Release(L); + return sAnswerFromLua; +} + +void ScreenTextEntry::LoadFromTextEntrySettings( const TextEntrySettings &settings ) +{ + g_ValidateFunc = settings.Validate; + g_OnOKFunc = settings.OnOK; + g_OnCancelFunc = settings.OnCancel; + g_ValidateAppendFunc = settings.ValidateAppend; + g_FormatAnswerForDisplayFunc = settings.FormatAnswerForDisplay; + + // set functions + SetTextEntrySettings( + settings.sQuestion, + settings.sInitialAnswer, + settings.iMaxInputLength, + ValidateFromLua, // Validate + OnOKFromLua, // OnOK + OnCancelFromLua, // OnCancel + settings.bPassword, + ValidateAppendFromLua, // ValidateAppend + FormatAnswerForDisplayFromLua // FormatAnswerForDisplay + ); + + // Hack: reload screen with new info + BeginScreen(); +} + +/** @brief Allow Lua to have access to the ScreenTextEntry. */ +class LunaScreenTextEntry: public Luna +{ +public: + static int Load( T* p, lua_State *L ) + { + ScreenTextEntry::TextEntrySettings settings; + settings.FromStack( L ); + p->LoadFromTextEntrySettings(settings); + return 0; + } + + LunaScreenTextEntry() + { + ADD_METHOD( Load ); + } +}; +LUA_REGISTER_DERIVED_CLASS( ScreenTextEntry, ScreenWithMenuElements ) +// lua end + +// begin ScreenTextEntryVisual +void ScreenTextEntryVisual::Init() +{ + ROW_START_X.Load( m_sName, "RowStartX" ); + ROW_START_Y.Load( m_sName, "RowStartY" ); + ROW_END_X.Load( m_sName, "RowEndX" ); + ROW_END_Y.Load( m_sName, "RowEndY" ); + + ScreenTextEntry::Init(); + + m_sprCursor.Load( THEME->GetPathG(m_sName,"cursor") ); + m_sprCursor->SetName( "Cursor" ); + LOAD_ALL_COMMANDS( m_sprCursor ); + this->AddChild( m_sprCursor ); + + // Init keyboard + { + BitmapText text; + text.LoadFromFont( THEME->GetPathF(m_sName,"keyboard") ); + text.SetName( "Keys" ); + ActorUtil::LoadAllCommands( text, m_sName ); + text.PlayCommand( "Init" ); + + FOREACH_KeyboardRow( r ) + { + for( int x=0; xAddChild( pbt ); + + RString s = g_szKeys[r][x]; + if( !s.empty() && r == KEYBOARD_ROW_SPECIAL ) + s = THEME->GetString( m_sName, s ); + pbt->SetText( s ); + } + } + } + + m_sndChange.Load( THEME->GetPathS(m_sName,"change"), true ); +} + +ScreenTextEntryVisual::~ScreenTextEntryVisual() +{ + FOREACH_KeyboardRow( r ) + for( int x=0; xSetXY( bt.GetX(), bt.GetY() ); + m_sprCursor->PlayCommand( m_iFocusY == KEYBOARD_ROW_SPECIAL ? "SpecialKey" : "RegularKey" ); +} + +void ScreenTextEntryVisual::TextEnteredDirectly() +{ + // If the user enters text with a keyboard, jump to DONE, so enter ends the screen. + m_iFocusY = KEYBOARD_ROW_SPECIAL; + m_iFocusX = DONE; + + PositionCursor(); +} + +void ScreenTextEntryVisual::MoveX( int iDir ) +{ + RString sKey; + do + { + m_iFocusX += iDir; + wrap( m_iFocusX, KEYS_PER_ROW ); + + sKey = g_szKeys[m_iFocusY][m_iFocusX]; + } + while( sKey == "" ); + + m_sndChange.Play(); + PositionCursor(); +} + +void ScreenTextEntryVisual::MoveY( int iDir ) +{ + RString sKey; + do + { + m_iFocusY = enum_add2( m_iFocusY, +iDir ); + wrap( *ConvertValue(&m_iFocusY), NUM_KeyboardRow ); + + // HACK: Round to nearest option so that we always stop + // on KEYBOARD_ROW_SPECIAL. + if( m_iFocusY == KEYBOARD_ROW_SPECIAL ) + { + for( int i=0; true; i++ ) + { + sKey = g_szKeys[m_iFocusY][m_iFocusX]; + if( sKey != "" ) + break; + + // UGLY: Probe one space to the left before looking to the right + m_iFocusX += (i==0) ? -1 : +1; + wrap( m_iFocusX, KEYS_PER_ROW ); + } + } + + sKey = g_szKeys[m_iFocusY][m_iFocusX]; + } + while( sKey == "" ); + + m_sndChange.Play(); + PositionCursor(); +} + +bool ScreenTextEntryVisual::MenuLeft( const InputEventPlus &input ) +{ + if( input.type != IET_FIRST_PRESS ) + return false; + MoveX(-1); + return true; +} +bool ScreenTextEntryVisual::MenuRight( const InputEventPlus &input ) +{ + if( input.type != IET_FIRST_PRESS ) + return false; + MoveX(+1); + return true; +} +bool ScreenTextEntryVisual::MenuUp( const InputEventPlus &input ) +{ + if( input.type != IET_FIRST_PRESS ) + return false; + MoveY(-1); + return true; +} +bool ScreenTextEntryVisual::MenuDown( const InputEventPlus &input ) +{ + if( input.type != IET_FIRST_PRESS ) + return false; + MoveY(+1); + return true; +} + +bool ScreenTextEntryVisual::MenuStart( const InputEventPlus &input ) +{ + if( input.type != IET_FIRST_PRESS ) + return false; + if( m_iFocusY == KEYBOARD_ROW_SPECIAL ) + { + switch( m_iFocusX ) + { + case SPACEBAR: + TryAppendToAnswer( " " ); + break; + case BACKSPACE: + BackspaceInAnswer(); + break; + case CANCEL: + End( true ); + break; + case DONE: + End( false ); + break; + default: + return false; + } + } + else + { + TryAppendToAnswer( g_szKeys[m_iFocusY][m_iFocusX] ); + } + return true; +} + +/* + * (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. + */ diff --git a/src/ScreenUnlockStatus.cpp b/src/ScreenUnlockStatus.cpp index 0aa9e6a776..9fc6ea8df2 100644 --- a/src/ScreenUnlockStatus.cpp +++ b/src/ScreenUnlockStatus.cpp @@ -1,359 +1,359 @@ -#include "global.h" -#include "PrefsManager.h" -#include "ScreenUnlockStatus.h" -#include "ThemeManager.h" -#include "GameState.h" -#include "RageLog.h" -#include "UnlockManager.h" -#include "SongManager.h" -#include "ActorUtil.h" -#include "Song.h" -#include "Course.h" - -#define UNLOCK_TEXT_SCROLL_X THEME->GetMetricF("ScreenUnlockStatus","UnlockTextScrollX"); -#define UNLOCK_TEXT_SCROLL_START_Y THEME->GetMetricF("ScreenUnlockStatus","UnlockTextScrollStartY") -#define UNLOCK_TEXT_SCROLL_END_Y THEME->GetMetricF("ScreenUnlockStatus","UnlockTextScrollEndY") -#define UNLOCK_TEXT_SCROLL_ZOOM THEME->GetMetricF("ScreenUnlockStatus","UnlockTextScrollZoom") -#define UNLOCK_TEXT_SCROLL_ROWS THEME->GetMetricF("ScreenUnlockStatus","UnlockTextScrollRows") -#define UNLOCK_TEXT_SCROLL_MAX_WIDTH THEME->GetMetricF("ScreenUnlockStatus","UnlockTextScrollMaxWidth") -#define UNLOCK_TEXT_SCROLL_ICON_X THEME->GetMetricF("ScreenUnlockStatus","UnlockTextScrollIconX") -#define UNLOCK_TEXT_SCROLL_ICON_SIZE THEME->GetMetricF("ScreenUnlockStatus","UnlockTextScrollIconSize") -#define UNLOCK_TEXT_SCROLL THEME->GetMetricI("ScreenUnlockStatus","UnlockTextScroll") -#define TYPE_TO_DISPLAY THEME->GetMetric ("ScreenUnlockStatus","TypeOfPointsToDisplay") -#define ICON_COMMAND THEME->GetMetricA("ScreenUnlockStatus","UnlockIconCommand") -#define TIME_TO_DISPLAY THEME->GetMetricF("ScreenUnlockStatus","TimeToDisplay") -#define POINTS_ZOOM THEME->GetMetricF("ScreenUnlockStatus","PointsZoom") - -REGISTER_SCREEN_CLASS( ScreenUnlockStatus ); - -void ScreenUnlockStatus::Init() -{ - ScreenAttract::Init(); - - unsigned iNumUnlocks = UNLOCKMAN->m_UnlockEntries.size(); - - if( !PREFSMAN->m_bUseUnlockSystem || iNumUnlocks == 0 ) - { - this->PostScreenMessage( SM_GoToNextScreen, 0 ); - return; - } - - unsigned NumUnlocks = UNLOCKMAN->m_UnlockEntries.size(); - - PointsUntilNextUnlock.LoadFromFont( THEME->GetPathF("Common","normal") ); - PointsUntilNextUnlock.SetHorizAlign( align_left ); - - apActorCommands IconCommand = ICON_COMMAND; - for( unsigned i=1; i <= NumUnlocks; i++ ) - { - // get pertaining UnlockEntry - const UnlockEntry &entry = UNLOCKMAN->m_UnlockEntries[i-1]; - const Song *pSong = entry.m_Song.ToSong(); - - if( pSong == NULL) - continue; - - Sprite* pSpr = new Sprite; - - // new unlock graphic - pSpr->Load( THEME->GetPathG("ScreenUnlockStatus",ssprintf("%04d icon", i)) ); - - // set graphic location - pSpr->SetName( ssprintf("Unlock%04d",i) ); - LOAD_ALL_COMMANDS_AND_SET_XY( pSpr ); - - pSpr->RunCommands(IconCommand); - Unlocks.push_back(pSpr); - - if ( !entry.IsLocked() ) - this->AddChild(Unlocks[Unlocks.size() - 1]); - } - - // scrolling text - if (UNLOCK_TEXT_SCROLL != 0) - { - float ScrollingTextX = UNLOCK_TEXT_SCROLL_X; - float ScrollingTextStartY = UNLOCK_TEXT_SCROLL_START_Y; - float ScrollingTextEndY = UNLOCK_TEXT_SCROLL_END_Y; - float ScrollingTextZoom = UNLOCK_TEXT_SCROLL_ZOOM; - float ScrollingTextRows = UNLOCK_TEXT_SCROLL_ROWS; - float MaxWidth = UNLOCK_TEXT_SCROLL_MAX_WIDTH; - - float SecondsToScroll = TIME_TO_DISPLAY; - - if (SecondsToScroll > 2) SecondsToScroll--; - - float SECS_PER_CYCLE = 0; - - if (UNLOCK_TEXT_SCROLL != 3) - SECS_PER_CYCLE = (float)SecondsToScroll/(ScrollingTextRows + NumUnlocks); - else - SECS_PER_CYCLE = (float)SecondsToScroll/(ScrollingTextRows * 3 + NumUnlocks + 4); - - for(unsigned i = 1; i <= NumUnlocks; i++) - { - const UnlockEntry &entry = UNLOCKMAN->m_UnlockEntries[i-1]; - - BitmapText* text = new BitmapText; - - text->LoadFromFont( THEME->GetPathF("ScreenUnlockStatus","text") ); - text->SetHorizAlign( align_left ); - text->SetZoom(ScrollingTextZoom); - - switch( entry.m_Type ) - { - case UnlockRewardType_Song: - { - const Song *pSong = entry.m_Song.ToSong(); - ASSERT( pSong != NULL ); - - RString title = pSong->GetDisplayMainTitle(); - RString subtitle = pSong->GetDisplaySubTitle(); - if( subtitle != "" ) - title = title + "\n" + subtitle; - text->SetMaxWidth( MaxWidth ); - text->SetText( title ); - } - break; - case UnlockRewardType_Course: - { - const Course *pCourse = entry.m_Course.ToCourse(); - ASSERT( pCourse != NULL ); - - text->SetMaxWidth( MaxWidth ); - text->SetText( pCourse->GetDisplayFullTitle() ); - text->SetDiffuse( RageColor(0,1,0,1) ); - } - break; - default: - text->SetText( "" ); - text->SetDiffuse( RageColor(0.5f,0,0,1) ); - break; - } - - if( entry.IsLocked() ) - { - text->SetText("???"); - text->SetZoomX(1); - } - else - { - // unlocked. change color - const Song *pSong = entry.m_Song.ToSong(); - RageColor color = RageColor(1,1,1,1); - if( pSong ) - color = SONGMAN->GetSongGroupColor(pSong->m_sGroupName); - text->SetGlobalDiffuseColor(color); - } - - text->SetXY( ScrollingTextX, ScrollingTextStartY ); - - if (UNLOCK_TEXT_SCROLL == 3 && UNLOCK_TEXT_SCROLL_ROWS + i > NumUnlocks) - { // special command for last unlocks when scrolling is in effect - float TargetRow = -0.5f + i + UNLOCK_TEXT_SCROLL_ROWS - NumUnlocks; - float StopOffPoint = ScrollingTextEndY - TargetRow / UNLOCK_TEXT_SCROLL_ROWS * (ScrollingTextEndY - ScrollingTextStartY); - float FirstCycleTime = (UNLOCK_TEXT_SCROLL_ROWS - TargetRow) * SECS_PER_CYCLE; - float SecondCycleTime = (6 + TargetRow) * SECS_PER_CYCLE - FirstCycleTime; - //LOG->Trace("Target Row: %f", TargetRow); - //LOG->Trace("command for icon %d: %s", i, ssprintf("diffusealpha,0;sleep,%f;diffusealpha,1;linear,%f;y,%f;linear,%f;y,%f;linear,0.1;diffusealpha,0", SECS_PER_CYCLE * (i - 1), FirstCycleTime, StopOffPoint, SecondCycleTime * 2, ScrollingTextEndY).c_str() ); - RString sCommand = ssprintf("diffusealpha,0;sleep,%f;diffusealpha,1;linear,%f;y,%f;linear,%f;y,%f;linear,0.1;diffusealpha,0", SECS_PER_CYCLE * (i - 1), FirstCycleTime, StopOffPoint, SecondCycleTime, ScrollingTextEndY); - text->RunCommands( ActorUtil::ParseActorCommands(sCommand) ); - } - else - { - RString sCommand = ssprintf("diffusealpha,0;sleep,%f;diffusealpha,1;linear,%f;y,%f;linear,0.1;diffusealpha,0", SECS_PER_CYCLE * (i - 1), SECS_PER_CYCLE * (ScrollingTextRows), ScrollingTextEndY); - text->RunCommands( ActorUtil::ParseActorCommands(sCommand) ); - } - - item.push_back(text); - - if (UNLOCK_TEXT_SCROLL >= 2) - { - Sprite* IconCount = new Sprite; - - // new unlock graphic - IconCount->Load( THEME->GetPathG("ScreenUnlockStatus",ssprintf("%04d icon", i)) ); - - // set graphic location - IconCount->SetXY( UNLOCK_TEXT_SCROLL_ICON_X, ScrollingTextStartY); - - IconCount->SetHeight(UNLOCK_TEXT_SCROLL_ICON_SIZE); - IconCount->SetWidth(UNLOCK_TEXT_SCROLL_ICON_SIZE); - - if (UNLOCK_TEXT_SCROLL == 3 && UNLOCK_TEXT_SCROLL_ROWS + i > NumUnlocks) - { - float TargetRow = -0.5f + i + UNLOCK_TEXT_SCROLL_ROWS - NumUnlocks; - float StopOffPoint = ScrollingTextEndY - TargetRow / UNLOCK_TEXT_SCROLL_ROWS * (ScrollingTextEndY - ScrollingTextStartY); - float FirstCycleTime = (UNLOCK_TEXT_SCROLL_ROWS - TargetRow) * SECS_PER_CYCLE; - float SecondCycleTime = (6 + TargetRow) * SECS_PER_CYCLE - FirstCycleTime; - //LOG->Trace("Target Row: %f", TargetRow); - //LOG->Trace("command for icon %d: %s", i, ssprintf("diffusealpha,0;sleep,%f;diffusealpha,1;linear,%f;y,%f;linear,%f;y,%f;linear,0.1;diffusealpha,0", SECS_PER_CYCLE * (i - 1), FirstCycleTime, StopOffPoint, SecondCycleTime * 2, ScrollingTextEndY).c_str() ); - RString sCommand = ssprintf("diffusealpha,0;sleep,%f;diffusealpha,1;linear,%f;y,%f;linear,%f;y,%f;linear,0.1;diffusealpha,0", SECS_PER_CYCLE * (i - 1), FirstCycleTime, StopOffPoint, SecondCycleTime, ScrollingTextEndY); - IconCount->RunCommands( ActorUtil::ParseActorCommands(sCommand) ); - } - else - { - RString sCommand = ssprintf("diffusealpha,0;sleep,%f;diffusealpha,1;linear,%f;y,%f;linear,0.1;diffusealpha,0", SECS_PER_CYCLE * (i - 1), SECS_PER_CYCLE * (ScrollingTextRows), ScrollingTextEndY); - IconCount->RunCommands( ActorUtil::ParseActorCommands(sCommand) ); - } - - ItemIcons.push_back(IconCount); - - //LOG->Trace("Added unlock text %d", i); - - if (UNLOCK_TEXT_SCROLL == 3) - { - if ( !entry.IsLocked() ) - LastUnlocks.push_back(i); - } - } - } - } - - if (UNLOCK_TEXT_SCROLL == 3) - { - float ScrollingTextX = UNLOCK_TEXT_SCROLL_X; - float ScrollingTextStartY = UNLOCK_TEXT_SCROLL_START_Y; - float ScrollingTextEndY = UNLOCK_TEXT_SCROLL_END_Y; - float ScrollingTextRows = UNLOCK_TEXT_SCROLL_ROWS; - float MaxWidth = UNLOCK_TEXT_SCROLL_MAX_WIDTH; - float SecondsToScroll = TIME_TO_DISPLAY - 1; - float SECS_PER_CYCLE = (float)SecondsToScroll/(ScrollingTextRows * 3 + NumUnlocks + 4); - - for(unsigned i=1; i <= UNLOCK_TEXT_SCROLL_ROWS; i++) - { - if (i > LastUnlocks.size()) - continue; - - unsigned NextIcon = LastUnlocks[LastUnlocks.size() - i]; - - const UnlockEntry &entry = UNLOCKMAN->m_UnlockEntries[NextIcon-1]; - const Song *pSong = entry.m_Song.ToSong(); - if( pSong == NULL ) - continue; - - BitmapText* NewText = new BitmapText; - - NewText->LoadFromFont( THEME->GetPathF("ScreenUnlockStatus","text") ); - NewText->SetHorizAlign( align_left ); - - RString title = pSong->GetDisplayMainTitle(); - RString subtitle = pSong->GetDisplaySubTitle(); - - if( subtitle != "" ) - title = title + "\n" + subtitle; - NewText->SetZoom(UNLOCK_TEXT_SCROLL_ZOOM); - NewText->SetMaxWidth( MaxWidth ); - NewText->SetText( title ); - - RageColor color = SONGMAN->GetSongGroupColor(pSong->m_sGroupName); - NewText->SetGlobalDiffuseColor(color); - - NewText->SetXY(ScrollingTextX, ScrollingTextStartY); - { - RString sCommand = ssprintf("diffusealpha,0;sleep,%f;diffusealpha,1;linear,%f;y,%f;", SECS_PER_CYCLE * (NumUnlocks + 2 * i - 2), SECS_PER_CYCLE * ((ScrollingTextRows - i) * 2 + 1 ), (ScrollingTextStartY + (ScrollingTextEndY - ScrollingTextStartY) * (ScrollingTextRows - i + 0.5) / ScrollingTextRows )); - NewText->RunCommands( ActorUtil::ParseActorCommands(sCommand) ); - } - - // new unlock graphic - Sprite* NewIcon = new Sprite; - NewIcon->Load( THEME->GetPathG("ScreenUnlockStatus",ssprintf("%04d icon", NextIcon)) ); - NewIcon->SetXY( UNLOCK_TEXT_SCROLL_ICON_X, ScrollingTextStartY); - NewIcon->SetHeight(UNLOCK_TEXT_SCROLL_ICON_SIZE); - NewIcon->SetWidth(UNLOCK_TEXT_SCROLL_ICON_SIZE); - { - RString sCommand = ssprintf("diffusealpha,0;sleep,%f;diffusealpha,1;linear,%f;y,%f;", SECS_PER_CYCLE * (NumUnlocks + 2 * i - 2), SECS_PER_CYCLE * ((ScrollingTextRows - i) * 2 + 1 ), (ScrollingTextStartY + (ScrollingTextEndY - ScrollingTextStartY) * (ScrollingTextRows - i + 0.5) / ScrollingTextRows )); - NewIcon->RunCommands( ActorUtil::ParseActorCommands(sCommand) ); - } - - ItemIcons.push_back(NewIcon); - item.push_back(NewText); - } - } - - // NOTE: the following two loops require the iterator to - // be ints because if you decrement an unsigned when it - // equals zero, you get the maximum value of an unsigned, - // which is still greater than 0. By typecasting it as - // an integer, you can achieve -1, which exits the loop. - - for(int i = item.size() - 1; (int)i >= 0; i--) - this->AddChild(item[i]); - - for(int i = ItemIcons.size() - 1; (int)i >= 0; i--) - this->AddChild(ItemIcons[i]); - - PointsUntilNextUnlock.SetName( "PointsDisplay" ); - - RString PointDisplay = TYPE_TO_DISPLAY; - if (PointDisplay == "DP" || PointDisplay == "Dance") - { - RString sDP = ssprintf( "%d", (int)UNLOCKMAN->PointsUntilNextUnlock(UnlockRequirement_DancePoints) ); - PointsUntilNextUnlock.SetText( sDP ); - } - else if (PointDisplay == "AP" || PointDisplay == "Arcade") - { - RString sAP = ssprintf( "%d", (int)UNLOCKMAN->PointsUntilNextUnlock(UnlockRequirement_ArcadePoints) ); - PointsUntilNextUnlock.SetText( sAP ); - } - else if (PointDisplay == "SP" || PointDisplay == "Song") - { - RString sSP = ssprintf( "%d", (int)UNLOCKMAN->PointsUntilNextUnlock(UnlockRequirement_SongPoints) ); - PointsUntilNextUnlock.SetText( sSP ); - } - - PointsUntilNextUnlock.SetZoom( POINTS_ZOOM ); - LOAD_ALL_COMMANDS_AND_SET_XY( PointsUntilNextUnlock ); - this->AddChild( &PointsUntilNextUnlock ); - - this->ClearMessageQueue( SM_BeginFadingOut ); // ignore ScreenAttract's SecsToShow - - this->PostScreenMessage( SM_BeginFadingOut, TIME_TO_DISPLAY ); -} - -ScreenUnlockStatus::~ScreenUnlockStatus() -{ - while (Unlocks.size() > 0) - { - Sprite* entry = Unlocks[Unlocks.size()-1]; - SAFE_DELETE(entry); - Unlocks.pop_back(); - } - while (item.size() > 0) - { - BitmapText* entry = item[item.size()-1]; - SAFE_DELETE(entry); - item.pop_back(); - } - while (ItemIcons.size() > 0) - { - Sprite* entry = ItemIcons[ItemIcons.size()-1]; - SAFE_DELETE(entry); - ItemIcons.pop_back(); - } -} - -/* - * (c) 2003 Andrew Wong - * 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. - */ +#include "global.h" +#include "PrefsManager.h" +#include "ScreenUnlockStatus.h" +#include "ThemeManager.h" +#include "GameState.h" +#include "RageLog.h" +#include "UnlockManager.h" +#include "SongManager.h" +#include "ActorUtil.h" +#include "Song.h" +#include "Course.h" + +#define UNLOCK_TEXT_SCROLL_X THEME->GetMetricF("ScreenUnlockStatus","UnlockTextScrollX"); +#define UNLOCK_TEXT_SCROLL_START_Y THEME->GetMetricF("ScreenUnlockStatus","UnlockTextScrollStartY") +#define UNLOCK_TEXT_SCROLL_END_Y THEME->GetMetricF("ScreenUnlockStatus","UnlockTextScrollEndY") +#define UNLOCK_TEXT_SCROLL_ZOOM THEME->GetMetricF("ScreenUnlockStatus","UnlockTextScrollZoom") +#define UNLOCK_TEXT_SCROLL_ROWS THEME->GetMetricF("ScreenUnlockStatus","UnlockTextScrollRows") +#define UNLOCK_TEXT_SCROLL_MAX_WIDTH THEME->GetMetricF("ScreenUnlockStatus","UnlockTextScrollMaxWidth") +#define UNLOCK_TEXT_SCROLL_ICON_X THEME->GetMetricF("ScreenUnlockStatus","UnlockTextScrollIconX") +#define UNLOCK_TEXT_SCROLL_ICON_SIZE THEME->GetMetricF("ScreenUnlockStatus","UnlockTextScrollIconSize") +#define UNLOCK_TEXT_SCROLL THEME->GetMetricI("ScreenUnlockStatus","UnlockTextScroll") +#define TYPE_TO_DISPLAY THEME->GetMetric ("ScreenUnlockStatus","TypeOfPointsToDisplay") +#define ICON_COMMAND THEME->GetMetricA("ScreenUnlockStatus","UnlockIconCommand") +#define TIME_TO_DISPLAY THEME->GetMetricF("ScreenUnlockStatus","TimeToDisplay") +#define POINTS_ZOOM THEME->GetMetricF("ScreenUnlockStatus","PointsZoom") + +REGISTER_SCREEN_CLASS( ScreenUnlockStatus ); + +void ScreenUnlockStatus::Init() +{ + ScreenAttract::Init(); + + unsigned iNumUnlocks = UNLOCKMAN->m_UnlockEntries.size(); + + if( !PREFSMAN->m_bUseUnlockSystem || iNumUnlocks == 0 ) + { + this->PostScreenMessage( SM_GoToNextScreen, 0 ); + return; + } + + unsigned NumUnlocks = UNLOCKMAN->m_UnlockEntries.size(); + + PointsUntilNextUnlock.LoadFromFont( THEME->GetPathF("Common","normal") ); + PointsUntilNextUnlock.SetHorizAlign( align_left ); + + apActorCommands IconCommand = ICON_COMMAND; + for( unsigned i=1; i <= NumUnlocks; i++ ) + { + // get pertaining UnlockEntry + const UnlockEntry &entry = UNLOCKMAN->m_UnlockEntries[i-1]; + const Song *pSong = entry.m_Song.ToSong(); + + if( pSong == NULL) + continue; + + Sprite* pSpr = new Sprite; + + // new unlock graphic + pSpr->Load( THEME->GetPathG("ScreenUnlockStatus",ssprintf("%04d icon", i)) ); + + // set graphic location + pSpr->SetName( ssprintf("Unlock%04d",i) ); + LOAD_ALL_COMMANDS_AND_SET_XY( pSpr ); + + pSpr->RunCommands(IconCommand); + Unlocks.push_back(pSpr); + + if ( !entry.IsLocked() ) + this->AddChild(Unlocks[Unlocks.size() - 1]); + } + + // scrolling text + if (UNLOCK_TEXT_SCROLL != 0) + { + float ScrollingTextX = UNLOCK_TEXT_SCROLL_X; + float ScrollingTextStartY = UNLOCK_TEXT_SCROLL_START_Y; + float ScrollingTextEndY = UNLOCK_TEXT_SCROLL_END_Y; + float ScrollingTextZoom = UNLOCK_TEXT_SCROLL_ZOOM; + float ScrollingTextRows = UNLOCK_TEXT_SCROLL_ROWS; + float MaxWidth = UNLOCK_TEXT_SCROLL_MAX_WIDTH; + + float SecondsToScroll = TIME_TO_DISPLAY; + + if (SecondsToScroll > 2) SecondsToScroll--; + + float SECS_PER_CYCLE = 0; + + if (UNLOCK_TEXT_SCROLL != 3) + SECS_PER_CYCLE = (float)SecondsToScroll/(ScrollingTextRows + NumUnlocks); + else + SECS_PER_CYCLE = (float)SecondsToScroll/(ScrollingTextRows * 3 + NumUnlocks + 4); + + for(unsigned i = 1; i <= NumUnlocks; i++) + { + const UnlockEntry &entry = UNLOCKMAN->m_UnlockEntries[i-1]; + + BitmapText* text = new BitmapText; + + text->LoadFromFont( THEME->GetPathF("ScreenUnlockStatus","text") ); + text->SetHorizAlign( align_left ); + text->SetZoom(ScrollingTextZoom); + + switch( entry.m_Type ) + { + case UnlockRewardType_Song: + { + const Song *pSong = entry.m_Song.ToSong(); + ASSERT( pSong != nullptr ); + + RString title = pSong->GetDisplayMainTitle(); + RString subtitle = pSong->GetDisplaySubTitle(); + if( subtitle != "" ) + title = title + "\n" + subtitle; + text->SetMaxWidth( MaxWidth ); + text->SetText( title ); + } + break; + case UnlockRewardType_Course: + { + const Course *pCourse = entry.m_Course.ToCourse(); + ASSERT( pCourse != nullptr ); + + text->SetMaxWidth( MaxWidth ); + text->SetText( pCourse->GetDisplayFullTitle() ); + text->SetDiffuse( RageColor(0,1,0,1) ); + } + break; + default: + text->SetText( "" ); + text->SetDiffuse( RageColor(0.5f,0,0,1) ); + break; + } + + if( entry.IsLocked() ) + { + text->SetText("???"); + text->SetZoomX(1); + } + else + { + // unlocked. change color + const Song *pSong = entry.m_Song.ToSong(); + RageColor color = RageColor(1,1,1,1); + if( pSong ) + color = SONGMAN->GetSongGroupColor(pSong->m_sGroupName); + text->SetGlobalDiffuseColor(color); + } + + text->SetXY( ScrollingTextX, ScrollingTextStartY ); + + if (UNLOCK_TEXT_SCROLL == 3 && UNLOCK_TEXT_SCROLL_ROWS + i > NumUnlocks) + { // special command for last unlocks when scrolling is in effect + float TargetRow = -0.5f + i + UNLOCK_TEXT_SCROLL_ROWS - NumUnlocks; + float StopOffPoint = ScrollingTextEndY - TargetRow / UNLOCK_TEXT_SCROLL_ROWS * (ScrollingTextEndY - ScrollingTextStartY); + float FirstCycleTime = (UNLOCK_TEXT_SCROLL_ROWS - TargetRow) * SECS_PER_CYCLE; + float SecondCycleTime = (6 + TargetRow) * SECS_PER_CYCLE - FirstCycleTime; + //LOG->Trace("Target Row: %f", TargetRow); + //LOG->Trace("command for icon %d: %s", i, ssprintf("diffusealpha,0;sleep,%f;diffusealpha,1;linear,%f;y,%f;linear,%f;y,%f;linear,0.1;diffusealpha,0", SECS_PER_CYCLE * (i - 1), FirstCycleTime, StopOffPoint, SecondCycleTime * 2, ScrollingTextEndY).c_str() ); + RString sCommand = ssprintf("diffusealpha,0;sleep,%f;diffusealpha,1;linear,%f;y,%f;linear,%f;y,%f;linear,0.1;diffusealpha,0", SECS_PER_CYCLE * (i - 1), FirstCycleTime, StopOffPoint, SecondCycleTime, ScrollingTextEndY); + text->RunCommands( ActorUtil::ParseActorCommands(sCommand) ); + } + else + { + RString sCommand = ssprintf("diffusealpha,0;sleep,%f;diffusealpha,1;linear,%f;y,%f;linear,0.1;diffusealpha,0", SECS_PER_CYCLE * (i - 1), SECS_PER_CYCLE * (ScrollingTextRows), ScrollingTextEndY); + text->RunCommands( ActorUtil::ParseActorCommands(sCommand) ); + } + + item.push_back(text); + + if (UNLOCK_TEXT_SCROLL >= 2) + { + Sprite* IconCount = new Sprite; + + // new unlock graphic + IconCount->Load( THEME->GetPathG("ScreenUnlockStatus",ssprintf("%04d icon", i)) ); + + // set graphic location + IconCount->SetXY( UNLOCK_TEXT_SCROLL_ICON_X, ScrollingTextStartY); + + IconCount->SetHeight(UNLOCK_TEXT_SCROLL_ICON_SIZE); + IconCount->SetWidth(UNLOCK_TEXT_SCROLL_ICON_SIZE); + + if (UNLOCK_TEXT_SCROLL == 3 && UNLOCK_TEXT_SCROLL_ROWS + i > NumUnlocks) + { + float TargetRow = -0.5f + i + UNLOCK_TEXT_SCROLL_ROWS - NumUnlocks; + float StopOffPoint = ScrollingTextEndY - TargetRow / UNLOCK_TEXT_SCROLL_ROWS * (ScrollingTextEndY - ScrollingTextStartY); + float FirstCycleTime = (UNLOCK_TEXT_SCROLL_ROWS - TargetRow) * SECS_PER_CYCLE; + float SecondCycleTime = (6 + TargetRow) * SECS_PER_CYCLE - FirstCycleTime; + //LOG->Trace("Target Row: %f", TargetRow); + //LOG->Trace("command for icon %d: %s", i, ssprintf("diffusealpha,0;sleep,%f;diffusealpha,1;linear,%f;y,%f;linear,%f;y,%f;linear,0.1;diffusealpha,0", SECS_PER_CYCLE * (i - 1), FirstCycleTime, StopOffPoint, SecondCycleTime * 2, ScrollingTextEndY).c_str() ); + RString sCommand = ssprintf("diffusealpha,0;sleep,%f;diffusealpha,1;linear,%f;y,%f;linear,%f;y,%f;linear,0.1;diffusealpha,0", SECS_PER_CYCLE * (i - 1), FirstCycleTime, StopOffPoint, SecondCycleTime, ScrollingTextEndY); + IconCount->RunCommands( ActorUtil::ParseActorCommands(sCommand) ); + } + else + { + RString sCommand = ssprintf("diffusealpha,0;sleep,%f;diffusealpha,1;linear,%f;y,%f;linear,0.1;diffusealpha,0", SECS_PER_CYCLE * (i - 1), SECS_PER_CYCLE * (ScrollingTextRows), ScrollingTextEndY); + IconCount->RunCommands( ActorUtil::ParseActorCommands(sCommand) ); + } + + ItemIcons.push_back(IconCount); + + //LOG->Trace("Added unlock text %d", i); + + if (UNLOCK_TEXT_SCROLL == 3) + { + if ( !entry.IsLocked() ) + LastUnlocks.push_back(i); + } + } + } + } + + if (UNLOCK_TEXT_SCROLL == 3) + { + float ScrollingTextX = UNLOCK_TEXT_SCROLL_X; + float ScrollingTextStartY = UNLOCK_TEXT_SCROLL_START_Y; + float ScrollingTextEndY = UNLOCK_TEXT_SCROLL_END_Y; + float ScrollingTextRows = UNLOCK_TEXT_SCROLL_ROWS; + float MaxWidth = UNLOCK_TEXT_SCROLL_MAX_WIDTH; + float SecondsToScroll = TIME_TO_DISPLAY - 1; + float SECS_PER_CYCLE = (float)SecondsToScroll/(ScrollingTextRows * 3 + NumUnlocks + 4); + + for(unsigned i=1; i <= UNLOCK_TEXT_SCROLL_ROWS; i++) + { + if (i > LastUnlocks.size()) + continue; + + unsigned NextIcon = LastUnlocks[LastUnlocks.size() - i]; + + const UnlockEntry &entry = UNLOCKMAN->m_UnlockEntries[NextIcon-1]; + const Song *pSong = entry.m_Song.ToSong(); + if( pSong == NULL ) + continue; + + BitmapText* NewText = new BitmapText; + + NewText->LoadFromFont( THEME->GetPathF("ScreenUnlockStatus","text") ); + NewText->SetHorizAlign( align_left ); + + RString title = pSong->GetDisplayMainTitle(); + RString subtitle = pSong->GetDisplaySubTitle(); + + if( subtitle != "" ) + title = title + "\n" + subtitle; + NewText->SetZoom(UNLOCK_TEXT_SCROLL_ZOOM); + NewText->SetMaxWidth( MaxWidth ); + NewText->SetText( title ); + + RageColor color = SONGMAN->GetSongGroupColor(pSong->m_sGroupName); + NewText->SetGlobalDiffuseColor(color); + + NewText->SetXY(ScrollingTextX, ScrollingTextStartY); + { + RString sCommand = ssprintf("diffusealpha,0;sleep,%f;diffusealpha,1;linear,%f;y,%f;", SECS_PER_CYCLE * (NumUnlocks + 2 * i - 2), SECS_PER_CYCLE * ((ScrollingTextRows - i) * 2 + 1 ), (ScrollingTextStartY + (ScrollingTextEndY - ScrollingTextStartY) * (ScrollingTextRows - i + 0.5) / ScrollingTextRows )); + NewText->RunCommands( ActorUtil::ParseActorCommands(sCommand) ); + } + + // new unlock graphic + Sprite* NewIcon = new Sprite; + NewIcon->Load( THEME->GetPathG("ScreenUnlockStatus",ssprintf("%04d icon", NextIcon)) ); + NewIcon->SetXY( UNLOCK_TEXT_SCROLL_ICON_X, ScrollingTextStartY); + NewIcon->SetHeight(UNLOCK_TEXT_SCROLL_ICON_SIZE); + NewIcon->SetWidth(UNLOCK_TEXT_SCROLL_ICON_SIZE); + { + RString sCommand = ssprintf("diffusealpha,0;sleep,%f;diffusealpha,1;linear,%f;y,%f;", SECS_PER_CYCLE * (NumUnlocks + 2 * i - 2), SECS_PER_CYCLE * ((ScrollingTextRows - i) * 2 + 1 ), (ScrollingTextStartY + (ScrollingTextEndY - ScrollingTextStartY) * (ScrollingTextRows - i + 0.5) / ScrollingTextRows )); + NewIcon->RunCommands( ActorUtil::ParseActorCommands(sCommand) ); + } + + ItemIcons.push_back(NewIcon); + item.push_back(NewText); + } + } + + // NOTE: the following two loops require the iterator to + // be ints because if you decrement an unsigned when it + // equals zero, you get the maximum value of an unsigned, + // which is still greater than 0. By typecasting it as + // an integer, you can achieve -1, which exits the loop. + + for(int i = item.size() - 1; (int)i >= 0; i--) + this->AddChild(item[i]); + + for(int i = ItemIcons.size() - 1; (int)i >= 0; i--) + this->AddChild(ItemIcons[i]); + + PointsUntilNextUnlock.SetName( "PointsDisplay" ); + + RString PointDisplay = TYPE_TO_DISPLAY; + if (PointDisplay == "DP" || PointDisplay == "Dance") + { + RString sDP = ssprintf( "%d", (int)UNLOCKMAN->PointsUntilNextUnlock(UnlockRequirement_DancePoints) ); + PointsUntilNextUnlock.SetText( sDP ); + } + else if (PointDisplay == "AP" || PointDisplay == "Arcade") + { + RString sAP = ssprintf( "%d", (int)UNLOCKMAN->PointsUntilNextUnlock(UnlockRequirement_ArcadePoints) ); + PointsUntilNextUnlock.SetText( sAP ); + } + else if (PointDisplay == "SP" || PointDisplay == "Song") + { + RString sSP = ssprintf( "%d", (int)UNLOCKMAN->PointsUntilNextUnlock(UnlockRequirement_SongPoints) ); + PointsUntilNextUnlock.SetText( sSP ); + } + + PointsUntilNextUnlock.SetZoom( POINTS_ZOOM ); + LOAD_ALL_COMMANDS_AND_SET_XY( PointsUntilNextUnlock ); + this->AddChild( &PointsUntilNextUnlock ); + + this->ClearMessageQueue( SM_BeginFadingOut ); // ignore ScreenAttract's SecsToShow + + this->PostScreenMessage( SM_BeginFadingOut, TIME_TO_DISPLAY ); +} + +ScreenUnlockStatus::~ScreenUnlockStatus() +{ + while (Unlocks.size() > 0) + { + Sprite* entry = Unlocks[Unlocks.size()-1]; + SAFE_DELETE(entry); + Unlocks.pop_back(); + } + while (item.size() > 0) + { + BitmapText* entry = item[item.size()-1]; + SAFE_DELETE(entry); + item.pop_back(); + } + while (ItemIcons.size() > 0) + { + Sprite* entry = ItemIcons[ItemIcons.size()-1]; + SAFE_DELETE(entry); + ItemIcons.pop_back(); + } +} + +/* + * (c) 2003 Andrew Wong + * 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. + */ diff --git a/src/ScreenWithMenuElements.cpp b/src/ScreenWithMenuElements.cpp index 2c3e1b6cfe..2067258b85 100644 --- a/src/ScreenWithMenuElements.cpp +++ b/src/ScreenWithMenuElements.cpp @@ -156,7 +156,7 @@ ScreenWithMenuElements::~ScreenWithMenuElements() SAFE_DELETE( m_MenuTimer ); FOREACH_PlayerNumber( p ) { - if( m_MemoryCardDisplay[p] != NULL ) + if( m_MemoryCardDisplay[p] != nullptr ) SAFE_DELETE( m_MemoryCardDisplay[p] ); } for (Actor *actor : m_vDecorations) diff --git a/src/Song.cpp b/src/Song.cpp index b1d50c43d8..58ca0c586d 100644 --- a/src/Song.cpp +++ b/src/Song.cpp @@ -508,7 +508,7 @@ void Song::TidyUpData( bool fromCache, bool /* duringCache */ ) // Don't use this file. m_sMusicFile = ""; } - else if ( Sample != NULL ) + else if ( Sample != nullptr ) { m_fMusicLengthSeconds = Sample->GetLength() / 1000.0f; delete Sample; @@ -919,12 +919,12 @@ bool Song::SongCompleteForStyle( const Style *st ) const bool Song::HasStepsType( StepsType st ) const { - return SongUtil::GetOneSteps( this, st ) != NULL; + return SongUtil::GetOneSteps( this, st ) != nullptr; } bool Song::HasStepsTypeAndDifficulty( StepsType st, Difficulty dc ) const { - return SongUtil::GetOneSteps( this, st, dc ) != NULL; + return SongUtil::GetOneSteps( this, st, dc ) != nullptr; } void Song::Save() @@ -1526,7 +1526,7 @@ void Song::FreeAllLoadedFromProfile( ProfileSlot slot, const set *setInU continue; if( slot != ProfileSlot_Invalid && pSteps->GetLoadedFromProfileSlot() != slot ) continue; - if( setInUse != NULL && setInUse->find(pSteps) != setInUse->end() ) + if( setInUse != nullptr && setInUse->find(pSteps) != setInUse->end() ) continue; apToRemove.push_back( pSteps ); } diff --git a/src/SongManager.cpp b/src/SongManager.cpp index 242a97d504..e6a050710a 100644 --- a/src/SongManager.cpp +++ b/src/SongManager.cpp @@ -479,7 +479,7 @@ RageColor SongManager::GetSongGroupColor( const RString &sSongGroup ) const RageColor SongManager::GetSongColor( const Song* pSong ) const { - ASSERT( pSong != NULL ); + ASSERT( pSong != nullptr ); // protected by royal freem corporation. any modification/removal of // this code will result in prosecution. @@ -1224,7 +1224,7 @@ void SongManager::GetExtraStageInfo( bool bExtra2, const Style *sd, Song*& pSong } } - if( pExtra2Song == NULL && pExtra1Song != NULL ) + if( pExtra2Song == NULL && pExtra1Song != nullptr ) { pExtra2Song = pExtra1Song; pExtra2Notes = pExtra1Notes; @@ -1442,7 +1442,7 @@ void SongManager::UpdateShuffled() void SongManager::UpdatePreferredSort(RString sPreferredSongs, RString sPreferredCourses) { - ASSERT( UNLOCKMAN != NULL ); + ASSERT( UNLOCKMAN != nullptr ); { m_vPreferredSongSort.clear(); @@ -1544,7 +1544,7 @@ void SongManager::UpdatePreferredSort(RString sPreferredSongs, RString sPreferre for (PreferredSortSection const &i : m_vPreferredSongSort) for (Song const *j : i.vpSongs) { - ASSERT( j != NULL ); + ASSERT( j != nullptr ); } } @@ -1619,7 +1619,7 @@ void SongManager::UpdatePreferredSort(RString sPreferredSongs, RString sPreferre for (CoursePointerVector const &i : m_vPreferredCourseSort) for (Course *j : i) { - ASSERT( j != NULL ); + ASSERT( j != nullptr ); } } } diff --git a/src/SongUtil.cpp b/src/SongUtil.cpp index 3b7ce6584a..0cc612a736 100644 --- a/src/SongUtil.cpp +++ b/src/SongUtil.cpp @@ -477,7 +477,7 @@ void SongUtil::SortSongPointerArrayByGrades( vector &vpSongsInOut, bool b int iCounts[NUM_Grade]; const Profile *pProfile = PROFILEMAN->GetMachineProfile(); - ASSERT( pProfile != NULL ); + ASSERT( pProfile != nullptr ); pProfile->GetGrades( pSong, GAMESTATE->GetCurrentStyle()->m_StepsType, iCounts ); RString foo; @@ -554,7 +554,7 @@ void SongUtil::SortSongPointerArrayByNumPlays( vector &vpSongsInOut, Prof void SongUtil::SortSongPointerArrayByNumPlays( vector &vpSongsInOut, const Profile* pProfile, bool bDescending ) { - ASSERT( pProfile != NULL ); + ASSERT( pProfile != nullptr ); for(unsigned i = 0; i < vpSongsInOut.size(); ++i) g_mapSongSortVal[vpSongsInOut[i]] = ssprintf("%9i", pProfile->GetSongNumTimesPlayed(vpSongsInOut[i])); stable_sort( vpSongsInOut.begin(), vpSongsInOut.end(), bDescending ? CompareSongPointersBySortValueDescending : CompareSongPointersBySortValueAscending ); @@ -802,7 +802,7 @@ bool SongUtil::ValidateCurrentEditStepsDescription( const RString &sAnswer, RStr } static const RString sInvalidChars = "\\/:*?\"<>|"; - if( strpbrk(sAnswer, sInvalidChars) != NULL ) + if( strpbrk(sAnswer, sInvalidChars) != nullptr ) { sErrorOut = ssprintf( EDIT_NAME_CANNOT_CONTAIN.GetValue(), sInvalidChars.c_str() ); return false; @@ -852,7 +852,7 @@ bool SongUtil::ValidateCurrentStepsChartName(const RString &answer, RString &err if (answer.empty()) return true; static const RString sInvalidChars = "\\/:*?\"<>|"; - if( strpbrk(answer, sInvalidChars) != NULL ) + if( strpbrk(answer, sInvalidChars) != nullptr ) { error = ssprintf( CHART_NAME_CANNOT_CONTAIN.GetValue(), sInvalidChars.c_str() ); return false; @@ -886,7 +886,7 @@ bool SongUtil::ValidateCurrentStepsCredit( const RString &sAnswer, RString &sErr // Borrow from EditDescription testing. Perhaps this should be abstracted? -Wolfman2000 static const RString sInvalidChars = "\\/:*?\"<>|"; - if( strpbrk(sAnswer, sInvalidChars) != NULL ) + if( strpbrk(sAnswer, sInvalidChars) != nullptr ) { sErrorOut = ssprintf( AUTHOR_NAME_CANNOT_CONTAIN.GetValue(), sInvalidChars.c_str() ); return false; @@ -1119,7 +1119,7 @@ RString SongID::ToString() const bool SongID::IsValid() const { - return ToSong() != NULL; + return ToSong() != nullptr; } // lua start diff --git a/src/Sprite.cpp b/src/Sprite.cpp index c02f4f5daa..c71894d5d5 100644 --- a/src/Sprite.cpp +++ b/src/Sprite.cpp @@ -57,7 +57,7 @@ Sprite::Sprite( const Sprite &cpy ): CPY( m_fTexCoordVelocityY ); #undef CPY - if( cpy.m_pTexture != NULL ) + if( cpy.m_pTexture != nullptr ) m_pTexture = TEXTUREMAN->CopyTexture( cpy.m_pTexture ); else m_pTexture = NULL; @@ -152,7 +152,7 @@ void Sprite::LoadFromNode( const XNode* pNode ) vector aStates; const XNode *pFrames = pNode->GetChild( "Frames" ); - if( pFrames != NULL ) + if( pFrames != nullptr ) { /* All attributes are optional. If Frame is omitted, use the previous state's * frame (or 0 if the first). @@ -178,7 +178,7 @@ void Sprite::LoadFromNode( const XNode* pNode ) newState.rect = *m_pTexture->GetTextureCoordRect( iFrameIndex ); const XNode *pPoints[2] = { pFrame->GetChild( "1" ), pFrame->GetChild( "2" ) }; - if( pPoints[0] != NULL && pPoints[1] != NULL ) + if( pPoints[0] != nullptr && pPoints[1] != nullptr ) { RectF r = newState.rect; @@ -232,7 +232,7 @@ void Sprite::LoadFromNode( const XNode* pNode ) void Sprite::UnloadTexture() { - if( m_pTexture != NULL ) // If there was a previous bitmap... + if( m_pTexture != nullptr ) // If there was a previous bitmap... { TEXTUREMAN->UnloadTexture( m_pTexture ); // Unload it. m_pTexture = NULL; @@ -273,7 +273,7 @@ void Sprite::EnableAnimation( bool bEnable ) void Sprite::SetTexture( RageTexture *pTexture ) { - ASSERT( pTexture != NULL ); + ASSERT( pTexture != nullptr ); if( m_pTexture != pTexture ) { @@ -1052,7 +1052,7 @@ public: static int GetTexture( T* p, lua_State *L ) { RageTexture *pTexture = p->GetTexture(); - if( pTexture != NULL ) + if( pTexture != nullptr ) pTexture->PushSelf(L); else lua_pushnil( L ); diff --git a/src/StageStats.cpp b/src/StageStats.cpp index 7e70457e6c..c7f85794a3 100644 --- a/src/StageStats.cpp +++ b/src/StageStats.cpp @@ -44,9 +44,9 @@ void StageStats::AssertValid( PlayerNumber pn ) const CHECKPOINT_M( m_vpPlayedSongs[0]->GetTranslitFullTitle() ); ASSERT( m_player[pn].m_iStepsPlayed > 0 ); ASSERT( m_player[pn].m_vpPossibleSteps.size() != 0 ); - ASSERT( m_player[pn].m_vpPossibleSteps[0] != NULL ); + ASSERT( m_player[pn].m_vpPossibleSteps[0] != nullptr ); ASSERT_M( m_playMode < NUM_PlayMode, ssprintf("playmode %i", m_playMode) ); - ASSERT( m_pStyle != NULL ); + ASSERT( m_pStyle != nullptr ); ASSERT_M( m_player[pn].m_vpPossibleSteps[0]->GetDifficulty() < NUM_Difficulty, ssprintf("Invalid Difficulty %i", m_player[pn].m_vpPossibleSteps[0]->GetDifficulty()) ); ASSERT_M( (int) m_vpPlayedSongs.size() == m_player[pn].m_iStepsPlayed, ssprintf("%i Songs Played != %i Steps Played for player %i", (int)m_vpPlayedSongs.size(), (int)m_player[pn].m_iStepsPlayed, pn) ); ASSERT_M( m_vpPossibleSongs.size() == m_player[pn].m_vpPossibleSteps.size(), ssprintf("%i Possible Songs != %i Possible Steps for player %i", (int)m_vpPossibleSongs.size(), (int)m_player[pn].m_vpPossibleSteps.size(), pn) ); @@ -59,9 +59,9 @@ void StageStats::AssertValid( MultiPlayer pn ) const if( m_vpPlayedSongs[0] ) CHECKPOINT_M( m_vpPlayedSongs[0]->GetTranslitFullTitle() ); ASSERT( m_multiPlayer[pn].m_vpPossibleSteps.size() != 0 ); - ASSERT( m_multiPlayer[pn].m_vpPossibleSteps[0] != NULL ); + ASSERT( m_multiPlayer[pn].m_vpPossibleSteps[0] != nullptr ); ASSERT_M( m_playMode < NUM_PlayMode, ssprintf("playmode %i", m_playMode) ); - ASSERT( m_pStyle != NULL ); + ASSERT( m_pStyle != nullptr ); ASSERT_M( m_player[pn].m_vpPossibleSteps[0]->GetDifficulty() < NUM_Difficulty, ssprintf("difficulty %i", m_player[pn].m_vpPossibleSteps[0]->GetDifficulty()) ); ASSERT( (int) m_vpPlayedSongs.size() == m_player[pn].m_iStepsPlayed ); ASSERT( m_vpPossibleSongs.size() == m_player[pn].m_vpPossibleSteps.size() ); @@ -239,14 +239,14 @@ void StageStats::FinalizeScores( bool bSummary ) { // Save this stage to recent scores Course* pCourse = GAMESTATE->m_pCurCourse; - ASSERT( pCourse != NULL ); + ASSERT( pCourse != nullptr ); Trail* pTrail = GAMESTATE->m_pCurTrail[p]; PROFILEMAN->AddCourseScore( pCourse, pTrail, p, hs, m_player[p].m_iPersonalHighScoreIndex, m_player[p].m_iMachineHighScoreIndex ); } else { - ASSERT( pSteps != NULL ); + ASSERT( pSteps != nullptr ); PROFILEMAN->AddStepsScore( pSong, pSteps, p, hs, m_player[p].m_iPersonalHighScoreIndex, m_player[p].m_iMachineHighScoreIndex ); } @@ -272,9 +272,9 @@ void StageStats::FinalizeScores( bool bSummary ) else if( GAMESTATE->IsCourseMode() ) { Course* pCourse = GAMESTATE->m_pCurCourse; - ASSERT( pCourse != NULL ); + ASSERT( pCourse != nullptr ); Trail *pTrail = GAMESTATE->m_pCurTrail[p]; - ASSERT( pTrail != NULL ); + ASSERT( pTrail != nullptr ); pHSL = &pProfile->GetCourseHighScoreList( pCourse, pTrail ); } else diff --git a/src/StepMania.cpp b/src/StepMania.cpp index 6995a2cc86..7659250928 100644 --- a/src/StepMania.cpp +++ b/src/StepMania.cpp @@ -1,1523 +1,1523 @@ -#include "global.h" - -#include "StepMania.h" - -// Rage global classes -#include "RageLog.h" -#include "RageTextureManager.h" -#include "RageSoundManager.h" -#include "GameSoundManager.h" -#include "RageInput.h" -#include "RageTimer.h" -#include "RageMath.h" -#include "RageDisplay.h" -#include "RageThreads.h" -#include "LocalizedString.h" - -#include "arch/ArchHooks/ArchHooks.h" -#include "arch/LoadingWindow/LoadingWindow.h" -#include "arch/Dialog/Dialog.h" -#include - -#include "ProductInfo.h" - -#include "Screen.h" -#include "InputEventPlus.h" -#include "ScreenDimensions.h" -#include "CodeDetector.h" -#include "CommonMetrics.h" -#include "Game.h" -#include "RageSurface.h" -#include "RageSurface_Load.h" -#include "CommandLineActions.h" - -#if !defined(SUPPORT_OPENGL) && !defined(SUPPORT_D3D) -#define SUPPORT_OPENGL -#endif - -// StepMania global classes -#include "ThemeManager.h" -#include "NoteSkinManager.h" -#include "PrefsManager.h" -#include "SongManager.h" -#include "CharacterManager.h" -#include "GameState.h" -#include "AnnouncerManager.h" -#include "ProfileManager.h" -#include "MemoryCardManager.h" -#include "ScreenManager.h" -#include "LuaManager.h" -#include "GameManager.h" -#include "FontManager.h" -#include "InputFilter.h" -#include "InputMapper.h" -#include "InputQueue.h" -#include "SongCacheIndex.h" -#include "BannerCache.h" -//#include "BackgroundCache.h" -#include "UnlockManager.h" -#include "RageFileManager.h" -#include "LightsManager.h" -#include "ModelManager.h" -#include "CryptManager.h" -#include "NetworkSyncManager.h" -#include "MessageManager.h" -#include "StatsManager.h" -#include "GameLoop.h" -#include "SpecialFiles.h" -#include "Profile.h" - -#if defined(WIN32) -#include -#endif - -void ShutdownGame(); -bool HandleGlobalInputs( const InputEventPlus &input ); -void HandleInputEvents(float fDeltaTime); - -static Preference g_bAllowMultipleInstances( "AllowMultipleInstances", false ); - -void StepMania::GetPreferredVideoModeParams( VideoModeParams ¶msOut ) -{ - // resolution handling code that probably needs fixing - int iWidth = PREFSMAN->m_iDisplayWidth; - if( PREFSMAN->m_bWindowed ) - { - //float fRatio = PREFSMAN->m_iDisplayHeight; - //iWidth = PREFSMAN->m_iDisplayHeight * fRatio; - iWidth = static_cast(ceilf(PREFSMAN->m_iDisplayHeight * PREFSMAN->m_fDisplayAspectRatio)); - } - - paramsOut = VideoModeParams( - PREFSMAN->m_bWindowed, - iWidth, - PREFSMAN->m_iDisplayHeight, - PREFSMAN->m_iDisplayColorDepth, - PREFSMAN->m_iRefreshRate, - PREFSMAN->m_bVsync, - PREFSMAN->m_bInterlaced, - PREFSMAN->m_bSmoothLines, - PREFSMAN->m_bTrilinearFiltering, - PREFSMAN->m_bAnisotropicFiltering, - CommonMetrics::WINDOW_TITLE, - THEME->GetPathG("Common","window icon"), - PREFSMAN->m_bPAL, - PREFSMAN->m_fDisplayAspectRatio - ); -} - -static LocalizedString COLOR ("StepMania","color"); -static LocalizedString TEXTURE ("StepMania","texture"); -static LocalizedString WINDOWED ("StepMania","Windowed"); -static LocalizedString FULLSCREEN ("StepMania","Fullscreen"); -static LocalizedString ANNOUNCER_ ("StepMania","Announcer"); -static LocalizedString VSYNC ("StepMania","Vsync"); -static LocalizedString NO_VSYNC ("StepMania","NoVsync"); -static LocalizedString SMOOTH_LINES ("StepMania","SmoothLines"); -static LocalizedString NO_SMOOTH_LINES ("StepMania","NoSmoothLines"); - -static RString GetActualGraphicOptionsString() -{ - const VideoModeParams ¶ms = DISPLAY->GetActualVideoModeParams(); - RString sFormat = "%s %s %dx%d %d "+COLOR.GetValue()+" %d "+TEXTURE.GetValue()+" %dHz %s %s"; - RString sLog = ssprintf( sFormat, - DISPLAY->GetApiDescription().c_str(), - (params.windowed? WINDOWED : FULLSCREEN).GetValue().c_str(), - (int)params.width, - (int)params.height, - (int)params.bpp, - (int)PREFSMAN->m_iTextureColorDepth, - (int)params.rate, - (params.vsync? VSYNC : NO_VSYNC).GetValue().c_str(), - (PREFSMAN->m_bSmoothLines? SMOOTH_LINES : NO_SMOOTH_LINES).GetValue().c_str() ); - return sLog; -} - -static void StoreActualGraphicOptions() -{ - /* Store the settings that RageDisplay was actually able to use so that - * we don't go through the process of auto-detecting a usable video mode - * every time. */ - const VideoModeParams ¶ms = DISPLAY->GetActualVideoModeParams(); - PREFSMAN->m_bWindowed.Set( params.windowed ); - - /* If we're windowed, we may have tweaked the width based on the aspect ratio. - * Don't save this new value over the preferred value. */ - if( !PREFSMAN->m_bWindowed ) - { - PREFSMAN->m_iDisplayWidth .Set( params.width ); - PREFSMAN->m_iDisplayHeight .Set( params.height ); - } - PREFSMAN->m_iDisplayColorDepth .Set( params.bpp ); - if( PREFSMAN->m_iRefreshRate != REFRESH_DEFAULT ) - PREFSMAN->m_iRefreshRate.Set( params.rate ); - PREFSMAN->m_bVsync .Set( params.vsync ); - - Dialog::SetWindowed( params.windowed ); -} - -static RageDisplay *CreateDisplay(); - -bool StepMania::GetHighResolutionTextures() -{ - switch( PREFSMAN->m_HighResolutionTextures ) - { - default: - case HighResolutionTextures_Auto: - { - int height = PREFSMAN->m_iDisplayHeight; - return height > 480; - } - case HighResolutionTextures_ForceOn: - return true; - case HighResolutionTextures_ForceOff: - return false; - } -} - -static void StartDisplay() -{ - if( DISPLAY != NULL ) - return; // already started - - DISPLAY = CreateDisplay(); - - DISPLAY->ChangeCentering( - PREFSMAN->m_iCenterImageTranslateX, - PREFSMAN->m_iCenterImageTranslateY, - PREFSMAN->m_fCenterImageAddWidth, - PREFSMAN->m_fCenterImageAddHeight ); - - TEXTUREMAN = new RageTextureManager; - TEXTUREMAN->SetPrefs( - RageTextureManagerPrefs( - PREFSMAN->m_iTextureColorDepth, - PREFSMAN->m_iMovieColorDepth, - PREFSMAN->m_bDelayedTextureDelete, - PREFSMAN->m_iMaxTextureResolution, - StepMania::GetHighResolutionTextures(), - PREFSMAN->m_bForceMipMaps - ) - ); - - MODELMAN = new ModelManager; - MODELMAN->SetPrefs( - ModelManagerPrefs( - PREFSMAN->m_bDelayedModelDelete - ) - ); -} - -void StepMania::ApplyGraphicOptions() -{ - bool bNeedReload = false; - - VideoModeParams params; - GetPreferredVideoModeParams( params ); - RString sError = DISPLAY->SetVideoMode( params, bNeedReload ); - if( sError != "" ) - RageException::Throw( "%s", sError.c_str() ); - - DISPLAY->ChangeCentering( - PREFSMAN->m_iCenterImageTranslateX, - PREFSMAN->m_iCenterImageTranslateY, - PREFSMAN->m_fCenterImageAddWidth, - PREFSMAN->m_fCenterImageAddHeight ); - - bNeedReload |= TEXTUREMAN->SetPrefs( - RageTextureManagerPrefs( - PREFSMAN->m_iTextureColorDepth, - PREFSMAN->m_iMovieColorDepth, - PREFSMAN->m_bDelayedTextureDelete, - PREFSMAN->m_iMaxTextureResolution, - StepMania::GetHighResolutionTextures(), - PREFSMAN->m_bForceMipMaps - ) - ); - - bNeedReload |= MODELMAN->SetPrefs( - ModelManagerPrefs( - PREFSMAN->m_bDelayedModelDelete - ) - ); - - if( bNeedReload ) - TEXTUREMAN->ReloadAll(); - - StoreActualGraphicOptions(); - if( SCREENMAN ) - SCREENMAN->SystemMessage( GetActualGraphicOptionsString() ); - - // Give the input handlers a chance to re-open devices as necessary. - INPUTMAN->WindowReset(); -} - -static bool CheckVideoDefaultSettings(); - -void StepMania::ResetPreferences() -{ - PREFSMAN->ResetToFactoryDefaults(); - SOUNDMAN->SetMixVolume(); - CheckVideoDefaultSettings(); - ApplyGraphicOptions(); -} - -/* Shutdown all global singletons. Note that this may be called partway through - * initialization, due to an object failing to initialize, in which case some of - * these may still be NULL. */ -void ShutdownGame() -{ - /* First, tell SOUNDMAN that we're shutting down. This signals sound drivers to - * stop sounds, which we want to do before any threads that may have started sounds - * are closed; this prevents annoying DirectSound glitches and delays. */ - if( SOUNDMAN ) - SOUNDMAN->Shutdown(); - - SAFE_DELETE( SCREENMAN ); - SAFE_DELETE( STATSMAN ); - SAFE_DELETE( MESSAGEMAN ); - SAFE_DELETE( NSMAN ); - /* Delete INPUTMAN before the other INPUTFILTER handlers, or an input - * driver may try to send a message to INPUTFILTER after we delete it. */ - SAFE_DELETE( INPUTMAN ); - SAFE_DELETE( INPUTQUEUE ); - SAFE_DELETE( INPUTMAPPER ); - SAFE_DELETE( INPUTFILTER ); - SAFE_DELETE( MODELMAN ); - SAFE_DELETE( PROFILEMAN ); // PROFILEMAN needs the songs still loaded - SAFE_DELETE( CHARMAN ); - SAFE_DELETE( UNLOCKMAN ); - SAFE_DELETE( CRYPTMAN ); - SAFE_DELETE( MEMCARDMAN ); - SAFE_DELETE( SONGMAN ); - SAFE_DELETE( BANNERCACHE ); - //SAFE_DELETE( BACKGROUNDCACHE ); - SAFE_DELETE( SONGINDEX ); - SAFE_DELETE( SOUND ); // uses GAMESTATE, PREFSMAN - SAFE_DELETE( PREFSMAN ); - SAFE_DELETE( GAMESTATE ); - SAFE_DELETE( GAMEMAN ); - SAFE_DELETE( NOTESKIN ); - SAFE_DELETE( THEME ); - SAFE_DELETE( ANNOUNCER ); - SAFE_DELETE( LIGHTSMAN ); - SAFE_DELETE( SOUNDMAN ); - SAFE_DELETE( FONT ); - SAFE_DELETE( TEXTUREMAN ); - SAFE_DELETE( DISPLAY ); - Dialog::Shutdown(); - SAFE_DELETE( LOG ); - SAFE_DELETE( FILEMAN ); - SAFE_DELETE( LUA ); - SAFE_DELETE( HOOKS ); -} - -static void HandleException( const RString &sError ) -{ - if( g_bAutoRestart ) - HOOKS->RestartProgram(); - - // Shut down first, so we exit graphics mode before trying to open a dialog. - ShutdownGame(); - - // Throw up a pretty error dialog. - Dialog::Error( sError ); - Dialog::Shutdown(); // Shut it back down. -} - -void StepMania::ResetGame() -{ - GAMESTATE->Reset(); - - if( !THEME->DoesThemeExist( THEME->GetCurThemeName() ) ) - { - RString sGameName = GAMESTATE->GetCurrentGame()->m_szName; - if( !THEME->DoesThemeExist(sGameName) ) - sGameName = PREFSMAN->m_sDefaultTheme; // was previously "default" -aj - THEME->SwitchThemeAndLanguage( sGameName, THEME->GetCurLanguage(), PREFSMAN->m_bPseudoLocalize ); - TEXTUREMAN->DoDelayedDelete(); - } - - PREFSMAN->SavePrefsToDisk(); -} - -ThemeMetric INITIAL_SCREEN ("Common","InitialScreen"); -RString StepMania::GetInitialScreen() -{ - if( PREFSMAN->m_sTestInitialScreen.Get() != "" ) - return PREFSMAN->m_sTestInitialScreen; - return INITIAL_SCREEN.GetValue(); -} -ThemeMetric SELECT_MUSIC_SCREEN ("Common","SelectMusicScreen"); -RString StepMania::GetSelectMusicScreen() -{ - return SELECT_MUSIC_SCREEN.GetValue(); -} - -#if defined(WIN32) -static Preference g_iLastSeenMemory( "LastSeenMemory", 0 ); -#endif - -static void AdjustForChangedSystemCapabilities() -{ -#if defined(WIN32) - // Has the amount of memory changed? - MEMORYSTATUS mem; - GlobalMemoryStatus(&mem); - - const int Memory = mem.dwTotalPhys / (1024*1024); - - if( g_iLastSeenMemory == Memory ) - return; - - LOG->Trace( "Memory changed from %i to %i; settings changed", g_iLastSeenMemory.Get(), Memory ); - g_iLastSeenMemory.Set( Memory ); - - // is this assumption outdated? -aj - /* Let's consider 128-meg systems low-memory, and 256-meg systems high-memory. - * Cut off at 192. This is pretty conservative; many 128-meg systems can - * deal with higher memory profile settings, but some can't. - * - * Actually, Windows lops off a meg or two; cut off a little lower to treat - * 192-meg systems as high-memory. */ - const bool HighMemory = (Memory >= 190); - const bool LowMemory = (Memory < 100); // 64 and 96-meg systems - - /* Two memory-consuming features that we can disable are texture caching and - * preloaded banners. Texture caching can use a lot of memory; disable it for - * low-memory systems. */ - PREFSMAN->m_bDelayedTextureDelete.Set( HighMemory ); - - /* Preloaded banners takes about 9k per song. Although it's smaller than the - * actual song data, it still adds up with a lot of songs. - * Disable it for 64-meg systems. */ - PREFSMAN->m_BannerCache.Set( LowMemory ? BNCACHE_OFF:BNCACHE_LOW_RES_PRELOAD ); - // might wanna do this for backgrounds, too... -aj - //PREFSMAN->m_BackgroundCache.Set( LowMemory ? BGCACHE_OFF:BGCACHE_LOW_RES_PRELOAD ); - - PREFSMAN->SavePrefsToDisk(); -#endif -} - -#if defined(WIN32) -#include "RageDisplay_D3D.h" -#include "archutils/Win32/VideoDriverInfo.h" -#endif - -#if defined(SUPPORT_OPENGL) -#include "RageDisplay_OGL.h" -#endif - -#if defined(SUPPORT_GLES2) -#include "RageDisplay_GLES2.h" -#endif - -#include "RageDisplay_Null.h" - - -struct VideoCardDefaults -{ - RString sDriverRegex; - RString sVideoRenderers; - int iWidth; - int iHeight; - int iDisplayColor; - int iTextureColor; - int iMovieColor; - int iTextureSize; - bool bSmoothLines; - - VideoCardDefaults() {} - VideoCardDefaults( - RString sDriverRegex_, - RString sVideoRenderers_, - int iWidth_, - int iHeight_, - int iDisplayColor_, - int iTextureColor_, - int iMovieColor_, - int iTextureSize_, - bool bSmoothLines_ - ) - { - sDriverRegex = sDriverRegex_; - sVideoRenderers = sVideoRenderers_; - iWidth = iWidth_; - iHeight = iHeight_; - iDisplayColor = iDisplayColor_; - iTextureColor = iTextureColor_; - iMovieColor = iMovieColor_; - iTextureSize = iTextureSize_; - bSmoothLines = bSmoothLines_; - } -} const g_VideoCardDefaults[] = -{ - VideoCardDefaults( - "Voodoo *5", - "d3d,opengl", // received 3 reports of opengl crashing. -Chris - 640,480, - 32,32,32, - 2048, - true // accelerated - ), - VideoCardDefaults( - "Voodoo|3dfx", // all other Voodoos: some drivers don't identify which one - "d3d,opengl", - 640,480, - 16,16,16, - 256, - false // broken, causes black screen - ), - VideoCardDefaults( - "Radeon.* 7|Wonder 7500|ArcadeVGA", // Radeon 7xxx, RADEON Mobility 7500 - "d3d,opengl", // movie texture performance is terrible in OpenGL, but fine in D3D. - 640,480, - 16,16,16, - 2048, - true // accelerated - ), - VideoCardDefaults( - "GeForce|Radeon|Wonder 9|Quadro", - "opengl,d3d", - 640,480, - 32,32,32, // 32 bit textures are faster to load - 2048, - true // hardware accelerated - ), - VideoCardDefaults( - "TNT|Vanta|M64", - "opengl,d3d", - 640,480, - 16,16,16, // Athlon 1.2+TNT demonstration w/ movies: 70fps w/ 32bit textures, 86fps w/ 16bit textures - 2048, - true // hardware accelerated - ), - VideoCardDefaults( - "G200|G250|G400", - "d3d,opengl", - 640,480, - 16,16,16, - 2048, - false // broken, causes black screen - ), - VideoCardDefaults( - "Savage", - "d3d", - // OpenGL is unusable on my Savage IV with even the latest drivers. - // It draws 30 frames of gibberish then crashes. This happens even with - // simple NeHe demos. -Chris - 640,480, - 16,16,16, - 2048, - false - ), - VideoCardDefaults( - "XPERT@PLAY|IIC|RAGE PRO|RAGE LT PRO", // Rage Pro chip, Rage IIC chip - "d3d", - // OpenGL is not hardware accelerated, despite the fact that the - // drivers come with an ICD. Also, the WinXP driver performance - // is terrible and supports only 640. The ATI driver is usable. - // -Chris - 320,240, // lower resolution for 60fps. In-box WinXP driver doesn't support 400x300. - 16,16,16, - 256, - false - ), - VideoCardDefaults( - "RAGE MOBILITY-M1", - "d3d,opengl", // Vertex alpha is broken in OpenGL, but not D3D. -Chris - 400,300, // lower resolution for 60fps - 16,16,16, - 256, - false - ), - VideoCardDefaults( - "Mobility M3", // ATI Rage Mobility 128 (AKA "M3") - "d3d,opengl", // bad movie texture performance in opengl - 640,480, - 16,16,16, - 1024, - false - ), - VideoCardDefaults( - "Intel.*82810|Intel.*82815", - "opengl,d3d",// OpenGL is 50%+ faster than D3D w/ latest Intel drivers. -Chris - 512,384, // lower resolution for 60fps - 16,16,16, - 512, - false - ), - VideoCardDefaults( - "Intel*Extreme Graphics", - "d3d", // OpenGL blue screens w/ XP drivers from 6-21-2002 - 640,480, - 16,16,16, // slow at 32bpp - 1024, - false - ), - VideoCardDefaults( - "Intel.*", /* fallback: all unknown Intel cards to D3D, since Intel is notoriously bad at OpenGL */ - "d3d,opengl", - 640,480, - 16,16,16, - 2048, - false - ), - VideoCardDefaults( - // Cards that have problems with OpenGL: - // ASSERT fail somewhere in RageDisplay_OpenGL "Trident Video Accelerator CyberBlade" - // bug 764499: ASSERT fail after glDeleteTextures for "SiS 650_651_740" - // bug 764830: ASSERT fail after glDeleteTextures for "VIA Tech VT8361/VT8601 Graphics Controller" - // bug 791950: AV in glsis630!DrvSwapBuffers for "SiS 630/730" - "Trident Video Accelerator CyberBlade|VIA.*VT|SiS 6*", - "d3d,opengl", - 640,480, - 16,16,16, - 2048, - false - ), - VideoCardDefaults( - /* Unconfirmed texture problems on this; let's try D3D, since it's - * a VIA/S3 chipset. */ - "VIA/S3G KM400/KN400", - "d3d,opengl", - 640,480, - 16,16,16, - 2048, - false - ), - VideoCardDefaults( - "OpenGL", // This matches all drivers in Mac and Linux. -Chris - "opengl", - 640,480, - 16,16,16, - 2048, - true // Right now, they've got to have NVidia or ATi Cards anyway.. - ), - VideoCardDefaults( - // Default graphics settings used for all cards that don't match above. - // This must be the very last entry! - "", - "opengl,d3d", - 640,480, - 32,32,32, - 2048, - false // AA is slow on some cards, so let's selectively enable HW accelerated cards. - ), -}; - - -static RString GetVideoDriverName() -{ -#if defined(_WINDOWS) - return GetPrimaryVideoDriverName(); -#else - return "OpenGL"; -#endif -} - -bool CheckVideoDefaultSettings() -{ - // Video card changed since last run - RString sVideoDriver = GetVideoDriverName(); - - LOG->Trace( "Last seen video driver: %s", PREFSMAN->m_sLastSeenVideoDriver.Get().c_str() ); - - VideoCardDefaults defaults; - - unsigned i; - for( i=0; iTrace( "Card matches '%s'.", sDriverRegex.size()? sDriverRegex.c_str():"(unknown card)" ); - break; - } - } - if (i >= ARRAYLEN(g_VideoCardDefaults)) - { - FAIL_M("Failed to match video driver"); - } - - bool bSetDefaultVideoParams = false; - if( PREFSMAN->m_sVideoRenderers.Get() == "" ) - { - bSetDefaultVideoParams = true; - LOG->Trace( "Applying defaults for %s.", sVideoDriver.c_str() ); - } - else if( PREFSMAN->m_sLastSeenVideoDriver.Get() != sVideoDriver ) - { - bSetDefaultVideoParams = true; - LOG->Trace( "Video card has changed from %s to %s. Applying new defaults.", PREFSMAN->m_sLastSeenVideoDriver.Get().c_str(), sVideoDriver.c_str() ); - } - - if( bSetDefaultVideoParams ) - { - PREFSMAN->m_sVideoRenderers.Set( defaults.sVideoRenderers ); - PREFSMAN->m_iDisplayWidth.Set( defaults.iWidth ); - PREFSMAN->m_iDisplayHeight.Set( defaults.iHeight ); - PREFSMAN->m_iDisplayColorDepth.Set( defaults.iDisplayColor ); - PREFSMAN->m_iTextureColorDepth.Set( defaults.iTextureColor ); - PREFSMAN->m_iMovieColorDepth.Set( defaults.iMovieColor ); - PREFSMAN->m_iMaxTextureResolution.Set( defaults.iTextureSize ); - PREFSMAN->m_bSmoothLines.Set( defaults.bSmoothLines ); - // this only worked when we started in fullscreen by default. -aj - //PREFSMAN->m_fDisplayAspectRatio.Set( HOOKS->GetDisplayAspectRatio() ); - // now that we start in windowed mode, use the new default aspect ratio. - PREFSMAN->m_fDisplayAspectRatio.Set( PREFSMAN->m_fDisplayAspectRatio ); - - // Update last seen video card - PREFSMAN->m_sLastSeenVideoDriver.Set( GetVideoDriverName() ); - } - else if( PREFSMAN->m_sVideoRenderers.Get().CompareNoCase(defaults.sVideoRenderers) ) - { - LOG->Warn("Video renderer list has been changed from '%s' to '%s'", - defaults.sVideoRenderers.c_str(), PREFSMAN->m_sVideoRenderers.Get().c_str() ); - } - - LOG->Info( "Video renderers: '%s'", PREFSMAN->m_sVideoRenderers.Get().c_str() ); - return bSetDefaultVideoParams; -} - -static LocalizedString ERROR_INITIALIZING_CARD ( "StepMania", "There was an error while initializing your video card." ); -static LocalizedString ERROR_DONT_FILE_BUG ( "StepMania", "Please do not file this error as a bug! Use the web page below to troubleshoot this problem." ); -static LocalizedString ERROR_VIDEO_DRIVER ( "StepMania", "Video Driver: %s" ); -static LocalizedString ERROR_NO_VIDEO_RENDERERS ( "StepMania", "No video renderers attempted." ); -static LocalizedString ERROR_INITIALIZING ( "StepMania", "Initializing %s..." ); -static LocalizedString ERROR_UNKNOWN_VIDEO_RENDERER ( "StepMania", "Unknown video renderer value: %s" ); - -RageDisplay *CreateDisplay() -{ - /* We never want to bother users with having to decide which API to use. - * - * Some cards simply are too troublesome with OpenGL to ever use it, eg. Voodoos. - * If D3D8 isn't installed on those, complain and refuse to run (by default). - * For others, always use OpenGL. Allow forcing to D3D as an advanced option. - * - * If we're missing acceleration when we load D3D8 due to a card being in the - * D3D list, it means we need drivers and that they do exist. - * - * If we try to load OpenGL and we're missing acceleration, it may mean: - * 1. We're missing drivers, and they just need upgrading. - * 2. The card doesn't have drivers, and it should be using D3D8. - * In other words, it needs an entry in this table. - * 3. The card doesn't have drivers for either. (Sorry, no S3 868s.) - * Can't play. - * In this case, fail to load; don't silently fall back on D3D. We don't want - * people unknowingly using D3D8 with old drivers (and reporting obscure bugs - * due to driver problems). We'll probably get bug reports for all three types. - * #2 is the only case that's actually a bug. - * - * Actually, right now we're falling back. I'm not sure which behavior is better. - */ - - //bool bAppliedDefaults = CheckVideoDefaultSettings(); - CheckVideoDefaultSettings(); - - VideoModeParams params; - StepMania::GetPreferredVideoModeParams( params ); - - RString error = ERROR_INITIALIZING_CARD.GetValue()+"\n\n"+ - ERROR_DONT_FILE_BUG.GetValue()+"\n\n" - VIDEO_TROUBLESHOOTING_URL "\n\n"+ - ssprintf(ERROR_VIDEO_DRIVER.GetValue(), GetVideoDriverName().c_str())+"\n\n"; - - vector asRenderers; - split( PREFSMAN->m_sVideoRenderers, ",", asRenderers, true ); - - if( asRenderers.empty() ) - RageException::Throw( "%s", ERROR_NO_VIDEO_RENDERERS.GetValue().c_str() ); - - RageDisplay *pRet = NULL; - for( unsigned i=0; iInit( params, PREFSMAN->m_bAllowUnacceleratedRenderer ); - if( !sError.empty() ) - { - error += ssprintf(ERROR_INITIALIZING.GetValue(), sRenderer.c_str())+"\n" + sError; - SAFE_DELETE( pRet ); - error += "\n\n\n"; - continue; - } - - break; // the display is ready to go - } - - if( pRet == NULL) - RageException::Throw( "%s", error.c_str() ); - - return pRet; -} - -static void SwitchToLastPlayedGame() -{ - const Game *pGame = GAMEMAN->StringToGame( PREFSMAN->GetCurrentGame() ); - - // If the active game type isn't actually available, revert to the default. - if( pGame == NULL ) - pGame = GAMEMAN->GetDefaultGame(); - - if( !GAMEMAN->IsGameEnabled( pGame ) && pGame != GAMEMAN->GetDefaultGame() ) - { - pGame = GAMEMAN->GetDefaultGame(); - LOG->Warn( "Default NoteSkin for \"%s\" missing, reverting to \"%s\"", - pGame->m_szName, GAMEMAN->GetDefaultGame()->m_szName ); - } - - ASSERT( GAMEMAN->IsGameEnabled(pGame) ); - - StepMania::ChangeCurrentGame( pGame ); -} - -void StepMania::ChangeCurrentGame( const Game* g ) -{ - ASSERT( g != NULL ); - ASSERT( GAMESTATE != NULL ); - ASSERT( ANNOUNCER != NULL ); - ASSERT( THEME != NULL ); - - GAMESTATE->SetCurGame( g ); - - RString sAnnouncer = PREFSMAN->m_sAnnouncer; - RString sTheme = PREFSMAN->m_sTheme; - RString sLanguage = PREFSMAN->m_sLanguage; - - if( sAnnouncer.empty() ) - sAnnouncer = GAMESTATE->GetCurrentGame()->m_szName; - if( sTheme.empty() ) - sTheme = GAMESTATE->GetCurrentGame()->m_szName; - - // process theme and language command line arguments; - // these change the preferences in order for transparent loading -aj - RString argTheme; - if( GetCommandlineArgument( "theme",&argTheme) && argTheme != sTheme ) - { - sTheme = argTheme; - // set theme in preferences too for correct behavior -aj - PREFSMAN->m_sTheme.Set(sTheme); - } - - RString argLanguage; - if( GetCommandlineArgument( "language",&argLanguage) ) - { - sLanguage = argLanguage; - // set language in preferences too for correct behavior -aj - PREFSMAN->m_sLanguage.Set(sLanguage); - } - - // it's OK to call these functions with names that don't exist. - ANNOUNCER->SwitchAnnouncer( sAnnouncer ); - THEME->SwitchThemeAndLanguage( sTheme, sLanguage, PREFSMAN->m_bPseudoLocalize ); - - // Set the input scheme for the new game, and load keymaps. - if( INPUTMAPPER ) - { - INPUTMAPPER->SetInputScheme( &g->m_InputScheme ); - INPUTMAPPER->ReadMappingsFromDisk(); - } -} - -static void MountTreeOfZips( const RString &dir ) -{ - vector dirs; - dirs.push_back( dir ); - - while( dirs.size() ) - { - RString path = dirs.back(); - dirs.pop_back(); - - if( !IsADirectory(path) ) - continue; - - vector zips; - GetDirListing( path + "/*.zip", zips, false, true ); - GetDirListing( path + "/*.smzip", zips, false, true ); - - for( unsigned i = 0; i < zips.size(); ++i ) - { - if( !IsAFile(zips[i]) ) - continue; - - LOG->Trace( "VFS: found %s", zips[i].c_str() ); - FILEMAN->Mount( "zip", zips[i], "/" ); - } - - GetDirListing( path + "/*", dirs, true, true ); - } -} - -#if defined(HAVE_VERSION_INFO) -extern unsigned long version_num; -extern const char *const version_date; -extern const char *const version_time; -#endif - -static void WriteLogHeader() -{ - LOG->Info( PRODUCT_ID_VER ); - -#if defined(HAVE_VERSION_INFO) - LOG->Info( "Compiled %s @ %s (build %lu)", version_date, version_time, version_num ); -#endif - - // this code should only be enabled in distributed builds - //LOG->Info("sm-ssc is Copyright �2009 the spinal shark collective, all rights reserved. Commercial use of this binary is prohibited by law and will be prosecuted to the fullest extent of the law."); - // end limited code - - time_t cur_time; - time(&cur_time); - struct tm now; - localtime_r( &cur_time, &now ); - - LOG->Info( "Log starting %.4d-%.2d-%.2d %.2d:%.2d:%.2d", - 1900+now.tm_year, now.tm_mon+1, now.tm_mday, now.tm_hour, now.tm_min, now.tm_sec ); - LOG->Trace( " " ); - - if( g_argc > 1 ) - { - RString args; - for( int i = 1; i < g_argc; ++i ) - { - if( i>1 ) - args += " "; - - // surround all params with some marker, as they might have whitespace. - // using [[ and ]], as they are not likely to be in the params. - args += ssprintf( "[[%s]]", g_argv[i] ); - } - LOG->Info( "Command line args (count=%d): %s", (g_argc - 1), args.c_str()); - } -} - -static void ApplyLogPreferences() -{ - LOG->SetShowLogOutput( PREFSMAN->m_bShowLogOutput ); - LOG->SetLogToDisk( PREFSMAN->m_bLogToDisk ); - LOG->SetInfoToDisk( true ); - LOG->SetUserLogToDisk( true ); - LOG->SetFlushing( PREFSMAN->m_bForceLogFlush ); - Checkpoints::LogCheckpoints( PREFSMAN->m_bLogCheckpoints ); -} - -static LocalizedString COULDNT_OPEN_LOADING_WINDOW( "LoadingWindow", "Couldn't open any loading windows." ); - -int main(int argc, char* argv[]) -{ - RageThreadRegister thread( "Main thread" ); - RageException::SetCleanupHandler( HandleException ); - - SetCommandlineArguments( argc, argv ); - - // Set up arch hooks first. This may set up crash handling. - HOOKS = ArchHooks::Create(); - HOOKS->Init(); - - LUA = new LuaManager; - - // Almost everything uses this to read and write files. Load this early. - FILEMAN = new RageFileManager( argv[0] ); - FILEMAN->MountInitialFilesystems(); - - bool bPortable = DoesFileExist("Portable.ini"); - if( !bPortable ) - FILEMAN->MountUserFilesystems(); - - // Set this up next. Do this early, since it's needed for RageException::Throw. - LOG = new RageLog; - - // Whew--we should be able to crash safely now! - - // load preferences and mount any alternative trees. - PREFSMAN = new PrefsManager; - - /* Allow HOOKS to check for multiple instances. We need to do this after PREFS is initialized, - * so ArchHooks can use a preference to turn this off. We want to do this before ApplyLogPreferences, - * so if we exit because of another instance, we don't try to clobber its log. We also want to - * do this before opening the loading window, so if we give focus away, we don't flash the window. */ - if(!g_bAllowMultipleInstances.Get() && HOOKS->CheckForMultipleInstances(argc, argv)) - { - ShutdownGame(); - return 0; - } - - ApplyLogPreferences(); - - WriteLogHeader(); - - // Set up alternative filesystem trees. - if( PREFSMAN->m_sAdditionalFolders.Get() != "" ) - { - vector dirs; - split( PREFSMAN->m_sAdditionalFolders, ",", dirs, true ); - for( unsigned i=0; i < dirs.size(); i++) - FILEMAN->Mount( "dir", dirs[i], "/" ); - } - if( PREFSMAN->m_sAdditionalSongFolders.Get() != "" ) - { - vector dirs; - split( PREFSMAN->m_sAdditionalSongFolders, ",", dirs, true ); - for( unsigned i=0; i < dirs.size(); i++) - FILEMAN->Mount( "dir", dirs[i], "/AdditionalSongs" ); - } - if( PREFSMAN->m_sAdditionalCourseFolders.Get() != "" ) - { - vector dirs; - split( PREFSMAN->m_sAdditionalCourseFolders, ",", dirs, true ); - for( unsigned i=0; i < dirs.size(); i++) - FILEMAN->Mount( "dir", dirs[i], "/AdditionalCourses" ); - } - - MountTreeOfZips( SpecialFiles::PACKAGES_DIR ); - MountTreeOfZips( SpecialFiles::USER_PACKAGES_DIR ); - - /* One of the above filesystems might contain files that affect preferences - * (e.g. Data/Static.ini). Re-read preferences. */ - PREFSMAN->ReadPrefsFromDisk(); - ApplyLogPreferences(); - - // This needs PREFSMAN. - Dialog::Init(); - - // Create game objects - - GAMESTATE = new GameState; - - // This requires PREFSMAN, for PREFSMAN->m_bShowLoadingWindow. - LoadingWindow *pLoadingWindow = LoadingWindow::Create(); - if(pLoadingWindow == NULL) - RageException::Throw("%s", COULDNT_OPEN_LOADING_WINDOW.GetValue().c_str()); - - srand( time(NULL) ); // seed number generator - - /* Do this early, so we have debugging output if anything else fails. LOG and - * Dialog must be set up first. It shouldn't take long, but it might take a - * little time; do this after the LoadingWindow is shown, since we don't want - * that to appear delayed. */ - HOOKS->DumpDebugInfo(); - -#if defined(HAVE_TLS) - LOG->Info( "TLS is %savailable", RageThread::GetSupportsTLS()? "":"not " ); -#endif - - AdjustForChangedSystemCapabilities(); - - GAMEMAN = new GameManager; - THEME = new ThemeManager; - ANNOUNCER = new AnnouncerManager; - NOTESKIN = new NoteSkinManager; - - // Switch to the last used game type, and set up the theme and announcer. - SwitchToLastPlayedGame(); - - CommandLineActions::Handle(pLoadingWindow); - - // Aldo: Check for updates here! - if( /* PREFSMAN->m_bUpdateCheckEnable (do this later) */ 0 ) - { - // TODO - Aldo_MX: Use PREFSMAN->m_iUpdateCheckIntervalSeconds & PREFSMAN->m_iUpdateCheckLastCheckedSecond - unsigned long current_version = NetworkSyncManager::GetCurrentSMBuild( pLoadingWindow ); - if( current_version ) - { - if( current_version > version_num ) - { - switch( Dialog::YesNo( "A new version of " PRODUCT_ID " is available. Do you want to download it?", "UpdateCheck" ) ) - { - case Dialog::yes: - //PREFSMAN->SavePrefsToDisk(); - // TODO: GoToURL for Linux - if( !HOOKS->GoToURL( SM_DOWNLOAD_URL ) ) - { - Dialog::Error( "Please go to the following URL to download the latest version of " PRODUCT_ID ":\n\n" SM_DOWNLOAD_URL, "UpdateCheckConfirm" ); - } - ShutdownGame(); - return 0; - case Dialog::no: - break; - default: - FAIL_M("Invalid response to Yes/No dialog"); - } - } - else if( version_num < current_version ) - { - LOG->Info( "The current version is more recent than the public one, double check you downloaded it from " SM_DOWNLOAD_URL ); - } - } - else - { - LOG->Info( "Unable to check for updates. The server might be offline." ); - } - } - - if( GetCommandlineArgument("dopefish") ) - GAMESTATE->m_bDopefish = true; - - { - /* Now that THEME is loaded, load the icon and splash for the current - * theme into the loading window. */ - RString sError; - RageSurface *pSurface = RageSurfaceUtils::LoadFile( THEME->GetPathG( "Common", "window icon" ), sError ); - if( pSurface != NULL ) - pLoadingWindow->SetIcon( pSurface ); - delete pSurface; - pSurface = RageSurfaceUtils::LoadFile( THEME->GetPathG("Common","splash"), sError ); - if( pSurface != NULL ) - pLoadingWindow->SetSplash( pSurface ); - delete pSurface; - } - - if( PREFSMAN->m_iSoundWriteAhead ) - LOG->Info( "Sound writeahead has been overridden to %i", PREFSMAN->m_iSoundWriteAhead.Get() ); - - SOUNDMAN = new RageSoundManager; - SOUNDMAN->Init(); - SOUNDMAN->SetMixVolume(); - SOUND = new GameSoundManager; - LIGHTSMAN = new LightsManager; - INPUTFILTER = new InputFilter; - INPUTMAPPER = new InputMapper; - - StepMania::ChangeCurrentGame( GAMESTATE->GetCurrentGame() ); - - INPUTQUEUE = new InputQueue; - SONGINDEX = new SongCacheIndex; - BANNERCACHE = new BannerCache; - //BACKGROUNDCACHE = new BackgroundCache; - - // depends on SONGINDEX: - SONGMAN = new SongManager; - SONGMAN->InitAll( pLoadingWindow ); // this takes a long time - CRYPTMAN = new CryptManager; // need to do this before ProfileMan - if( PREFSMAN->m_bSignProfileData ) - CRYPTMAN->GenerateGlobalKeys(); - MEMCARDMAN = new MemoryCardManager; - CHARMAN = new CharacterManager; - PROFILEMAN = new ProfileManager; - PROFILEMAN->Init(); // must load after SONGMAN - UNLOCKMAN = new UnlockManager; - SONGMAN->UpdatePopular(); - SONGMAN->UpdatePreferredSort(); - NSMAN = new NetworkSyncManager( pLoadingWindow ); - MESSAGEMAN = new MessageManager; - STATSMAN = new StatsManager; - - // Initialize which courses are ranking courses here. - SONGMAN->UpdateRankingCourses(); - - SAFE_DELETE( pLoadingWindow ); // destroy this before init'ing Display - - /* If the user has tried to quit during the loading, do it before creating - * the main window. This prevents going to full screen just to quit. */ - if( ArchHooks::UserQuit() ) - { - ShutdownGame(); - return 0; - } - - StartDisplay(); - - StoreActualGraphicOptions(); - LOG->Info( "%s", GetActualGraphicOptionsString().c_str() ); - - SONGMAN->PreloadSongImages(); - - /* Input handlers can have dependences on the video system so - * INPUTMAN must be initialized after DISPLAY. */ - INPUTMAN = new RageInput; - - // These things depend on the TextureManager, so do them after! - FONT = new FontManager; - SCREENMAN = new ScreenManager; - - StepMania::ResetGame(); - - /* Now that GAMESTATE is reset, tell SCREENMAN to update the theme (load - * overlay screens and global sounds), and load the initial screen. */ - SCREENMAN->ThemeChanged(); - SCREENMAN->SetNewScreen( StepMania::GetInitialScreen() ); - - // Do this after ThemeChanged so that we can show a system message - RString sMessage; - if( INPUTMAPPER->CheckForChangedInputDevicesAndRemap(sMessage) ) - SCREENMAN->SystemMessage( sMessage ); - - CodeDetector::RefreshCacheItems(); - - if( GetCommandlineArgument("netip") ) - NSMAN->DisplayStartupStatus(); // If we're using networking show what happened - - // Run the main loop. - GameLoop::RunGameLoop(); - - PREFSMAN->SavePrefsToDisk(); - - ShutdownGame(); - - return 0; -} - -RString StepMania::SaveScreenshot( RString sDir, bool bSaveCompressed, bool bMakeSignature, int iIndex ) -{ - /* As of sm-ssc v1.0 rc2, screenshots are no longer named by an arbitrary - * index. This was causing naming issues for some unknown reason, so we have - * changed the screenshot names to a non-blocking format: date and time. - * As before, we ignore the extension. -aj */ - RString sFileNameNoExtension = DateTime::GetNowDateTime().GetString(); - // replace space with underscore. - sFileNameNoExtension.Replace(" ","_"); - // colons are illegal in filenames. - sFileNameNoExtension.Replace(":",""); - - // Save the screenshot. If writing lossy to a memcard, use - // SAVE_LOSSY_LOW_QUAL, so we don't eat up lots of space. - RageDisplay::GraphicsFileFormat fmt; - if( bSaveCompressed && MEMCARDMAN->PathIsMemCard(sDir) ) - fmt = RageDisplay::SAVE_LOSSY_LOW_QUAL; - else if( bSaveCompressed ) - fmt = RageDisplay::SAVE_LOSSY_HIGH_QUAL; - else - fmt = RageDisplay::SAVE_LOSSLESS_SENSIBLE; - - RString sFileName = sFileNameNoExtension + "." + (bSaveCompressed ? "jpg" : "png"); - RString sPath = sDir+sFileName; - bool bResult = DISPLAY->SaveScreenshot( sPath, fmt ); - if( !bResult ) - { - SCREENMAN->PlayInvalidSound(); - return RString(); - } - - SCREENMAN->PlayScreenshotSound(); - - if( PREFSMAN->m_bSignProfileData && bMakeSignature ) - CryptManager::SignFileToFile( sPath ); - - return sFileName; -} - -/* Returns true if the key has been handled and should be discarded, false if - * the key should be sent on to screens. */ -static LocalizedString SERVICE_SWITCH_PRESSED ( "StepMania", "Service switch pressed" ); -static LocalizedString RELOADED_METRICS( "ThemeManager", "Reloaded metrics" ); -static LocalizedString RELOADED_METRICS_AND_TEXTURES( "ThemeManager", "Reloaded metrics and textures" ); -static LocalizedString RELOADED_SCRIPTS( "ThemeManager", "Reloaded scripts" ); -static LocalizedString RELOADED_OVERLAY_SCREENS( "ThemeManager", "Reloaded overlay screens" ); -bool HandleGlobalInputs( const InputEventPlus &input ) -{ - // None of the globals keys act on types other than FIRST_PRESS - if( input.type != IET_FIRST_PRESS ) - return false; - - switch( input.MenuI ) - { - case GAME_BUTTON_OPERATOR: - /* Global operator key, to get quick access to the options menu. Don't - * do this if we're on a "system menu", which includes the editor - * (to prevent quitting without storing changes). */ - if( SCREENMAN->AllowOperatorMenuButton() ) - { - SCREENMAN->SystemMessage( SERVICE_SWITCH_PRESSED ); - SCREENMAN->PopAllScreens(); - GAMESTATE->Reset(); - SCREENMAN->SetNewScreen( CommonMetrics::OPERATOR_MENU_SCREEN ); - } - return true; - - default: break; - } - - /* Re-added for StepMania 3.9 theming veterans, plus it's just faster than - * the debug menu. The Shift button only reloads the metrics, unlike in 3.9 - * (where it saved machine profile). -aj */ - bool bIsShiftHeld = INPUTFILTER->IsBeingPressed( DeviceInput(DEVICE_KEYBOARD, KEY_LSHIFT), &input.InputList) || - INPUTFILTER->IsBeingPressed( DeviceInput(DEVICE_KEYBOARD, KEY_RSHIFT), &input.InputList); - bool bIsCtrlHeld = INPUTFILTER->IsBeingPressed( DeviceInput(DEVICE_KEYBOARD, KEY_LCTRL), &input.InputList) || - INPUTFILTER->IsBeingPressed( DeviceInput(DEVICE_KEYBOARD, KEY_RCTRL), &input.InputList); - if( input.DeviceI == DeviceInput(DEVICE_KEYBOARD, KEY_F2) ) - { - if( bIsShiftHeld && !bIsCtrlHeld ) - { - // Shift+F2: refresh metrics,noteskin cache and CodeDetector cache only - THEME->ReloadMetrics(); - NOTESKIN->RefreshNoteSkinData( GAMESTATE->m_pCurGame ); - CodeDetector::RefreshCacheItems(); - SCREENMAN->SystemMessage( RELOADED_METRICS ); - } - else if( bIsCtrlHeld && !bIsShiftHeld ) - { - // Ctrl+F2: reload scripts only - THEME->UpdateLuaGlobals(); - SCREENMAN->SystemMessage( RELOADED_SCRIPTS ); - } - else if( bIsCtrlHeld && bIsShiftHeld ) - { - // Shift+Ctrl+F2: reload overlay screens (and metrics, since themers - // are likely going to do this after changing metrics.) - THEME->ReloadMetrics(); - SCREENMAN->ReloadOverlayScreens(); - SCREENMAN->SystemMessage( RELOADED_OVERLAY_SCREENS ); - } - else - { - // F2 alone: refresh metrics, textures, noteskins, codedetector cache - THEME->ReloadMetrics(); - TEXTUREMAN->ReloadAll(); - NOTESKIN->RefreshNoteSkinData( GAMESTATE->m_pCurGame ); - CodeDetector::RefreshCacheItems(); - SCREENMAN->SystemMessage( RELOADED_METRICS_AND_TEXTURES ); - } - - return true; - } - - if( input.DeviceI == DeviceInput(DEVICE_KEYBOARD, KEY_PAUSE) ) - { - Message msg("ToggleConsoleDisplay"); - MESSAGEMAN->Broadcast( msg ); - return true; - } - -#if !defined(MACOSX) - if( input.DeviceI == DeviceInput(DEVICE_KEYBOARD, KEY_F4) ) - { - if( INPUTFILTER->IsBeingPressed( DeviceInput(DEVICE_KEYBOARD, KEY_RALT), &input.InputList) || - INPUTFILTER->IsBeingPressed( DeviceInput(DEVICE_KEYBOARD, KEY_LALT), &input.InputList) ) - { - // pressed Alt+F4 - ArchHooks::SetUserQuit(); - return true; - } - } -#else - if( input.DeviceI == DeviceInput(DEVICE_KEYBOARD, KEY_Cq) && - (INPUTFILTER->IsBeingPressed( DeviceInput(DEVICE_KEYBOARD, KEY_LMETA), &input.InputList ) || - INPUTFILTER->IsBeingPressed( DeviceInput(DEVICE_KEYBOARD, KEY_RMETA), &input.InputList )) ) - { - /* The user quit is handled by the menu item so we don't need to set it - * here; however, we do want to return that it has been handled since - * this will happen first. */ - return true; - } -#endif - - bool bDoScreenshot = -#if defined(MACOSX) - // Notebooks don't have F13. Use cmd-F12 as well. - input.DeviceI == DeviceInput( DEVICE_KEYBOARD, KEY_PRTSC ) || - input.DeviceI == DeviceInput( DEVICE_KEYBOARD, KEY_F13 ) || - ( input.DeviceI == DeviceInput(DEVICE_KEYBOARD, KEY_F12) && - (INPUTFILTER->IsBeingPressed(DeviceInput(DEVICE_KEYBOARD, KEY_LMETA), &input.InputList) || - INPUTFILTER->IsBeingPressed(DeviceInput(DEVICE_KEYBOARD, KEY_RMETA), &input.InputList)) ); -#else - /* The default Windows message handler will capture the desktop window upon - * pressing PrntScrn, or will capture the foreground with focus upon pressing - * Alt+PrntScrn. Windows will do this whether or not we save a screenshot - * ourself by dumping the frame buffer. */ - // "if pressing PrintScreen and not pressing Alt" - input.DeviceI == DeviceInput(DEVICE_KEYBOARD, KEY_PRTSC) && - !INPUTFILTER->IsBeingPressed(DeviceInput(DEVICE_KEYBOARD, KEY_LALT), &input.InputList) && - !INPUTFILTER->IsBeingPressed(DeviceInput(DEVICE_KEYBOARD, KEY_RALT), &input.InputList); -#endif - if( bDoScreenshot ) - { - // If holding Shift save uncompressed, else save compressed - bool bHoldingShift = ( INPUTFILTER->IsBeingPressed( DeviceInput(DEVICE_KEYBOARD, KEY_LSHIFT) ) - || INPUTFILTER->IsBeingPressed( DeviceInput(DEVICE_KEYBOARD, KEY_RSHIFT) ) ); - bool bSaveCompressed = !bHoldingShift; - RageTimer timer; - StepMania::SaveScreenshot( "Screenshots/", bSaveCompressed, false, -1 ); - LOG->Trace( "Screenshot took %f seconds.", timer.GetDeltaTime() ); - return true; // handled - } - - if( input.DeviceI == DeviceInput(DEVICE_KEYBOARD, KEY_ENTER) && - (INPUTFILTER->IsBeingPressed(DeviceInput(DEVICE_KEYBOARD, KEY_RALT), &input.InputList) || - INPUTFILTER->IsBeingPressed(DeviceInput(DEVICE_KEYBOARD, KEY_LALT), &input.InputList)) ) - { - // alt-enter - /* In OS X, this is a menu item and will be handled as such. This will - * happen first and then the lower priority GUI thread will happen second, - * causing the window to toggle twice. Another solution would be to put - * a timer in ArchHooks::SetToggleWindowed() and just not set the bool - * it if it's been less than, say, half a second. */ -#if !defined(MACOSX) - ArchHooks::SetToggleWindowed(); -#endif - return true; - } - - return false; -} - -void HandleInputEvents(float fDeltaTime) -{ - INPUTFILTER->Update( fDeltaTime ); - - /* Hack: If the topmost screen hasn't been updated yet, don't process input, - * since we must not send inputs to a screen that hasn't at least had one - * update yet. (The first Update should be the very first thing a screen gets.) - * We'll process it next time. Call Update above, so the inputs are - * read and timestamped. */ - if( SCREENMAN->GetTopScreen()->IsFirstUpdate() ) - return; - - vector ieArray; - INPUTFILTER->GetInputEvents( ieArray ); - - // If we don't have focus, discard input. - if( !HOOKS->AppHasFocus() ) - return; - - for( unsigned i=0; iIsBeingPressed( DeviceInput(DEVICE_KEYBOARD,KEY_LSHIFT) ) ) - input.DeviceI.device = (InputDevice)(input.DeviceI.device + 1); - if( INPUTFILTER->IsBeingPressed( DeviceInput(DEVICE_KEYBOARD,KEY_LCTRL) ) ) - input.DeviceI.device = (InputDevice)(input.DeviceI.device + 2); - if( INPUTFILTER->IsBeingPressed( DeviceInput(DEVICE_KEYBOARD,KEY_LALT) ) ) - input.DeviceI.device = (InputDevice)(input.DeviceI.device + 4); - if( INPUTFILTER->IsBeingPressed( DeviceInput(DEVICE_KEYBOARD,KEY_RALT) ) ) - input.DeviceI.device = (InputDevice)(input.DeviceI.device + 8); - if( INPUTFILTER->IsBeingPressed( DeviceInput(DEVICE_KEYBOARD,KEY_RCTRL) ) ) - input.DeviceI.device = (InputDevice)(input.DeviceI.device + 16); - } - */ - - INPUTMAPPER->DeviceToGame( input.DeviceI, input.GameI ); - - input.mp = MultiPlayer_Invalid; - - { - // Translate input to the appropriate MultiPlayer. Assume that all - // joystick devices are mapped the same as the master player. - if( input.DeviceI.IsJoystick() ) - { - DeviceInput diTemp = input.DeviceI; - diTemp.device = DEVICE_JOY1; - GameInput gi; - - //LOG->Trace( "device %d, %d", diTemp.device, diTemp.button ); - if( INPUTMAPPER->DeviceToGame(diTemp, gi) ) - { - if( GAMESTATE->m_bMultiplayer ) - { - input.GameI = gi; - //LOG->Trace( "game %d %d", input.GameI.controller, input.GameI.button ); - } - - input.mp = InputMapper::InputDeviceToMultiPlayer( input.DeviceI.device ); - //LOG->Trace( "multiplayer %d", input.mp ); - ASSERT( input.mp >= 0 && input.mp < NUM_MultiPlayer ); - } - } - } - - if( input.GameI.IsValid() ) - { - input.MenuI = INPUTMAPPER->GameButtonToMenuButton( input.GameI.button ); - input.pn = INPUTMAPPER->ControllerToPlayerNumber( input.GameI.controller ); - } - - INPUTQUEUE->RememberInput( input ); - - // When a GameButton is pressed, stop repeating other keys on the same controller. - if( input.type == IET_FIRST_PRESS && input.MenuI != GameButton_Invalid ) - { - FOREACH_ENUM( GameButton, m ) - { - if( input.MenuI != m ) - INPUTMAPPER->RepeatStopKey( m, input.pn ); - } - } - - if( HandleGlobalInputs(input) ) - continue; // skip - - // check back in event mode - if( GAMESTATE->IsEventMode() && - CodeDetector::EnteredCode(input.GameI.controller,CODE_BACK_IN_EVENT_MODE) ) - { - input.pn = PLAYER_1; - input.MenuI = GAME_BUTTON_BACK; - } - - SCREENMAN->Input( input ); - } - - if( ArchHooks::GetAndClearToggleWindowed() ) - { - PREFSMAN->m_bWindowed.Set( !PREFSMAN->m_bWindowed ); - StepMania::ApplyGraphicOptions(); - } -} - -/* - * (c) 2001-2004 Chris Danford, Glenn Maynard - * 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. - */ +#include "global.h" + +#include "StepMania.h" + +// Rage global classes +#include "RageLog.h" +#include "RageTextureManager.h" +#include "RageSoundManager.h" +#include "GameSoundManager.h" +#include "RageInput.h" +#include "RageTimer.h" +#include "RageMath.h" +#include "RageDisplay.h" +#include "RageThreads.h" +#include "LocalizedString.h" + +#include "arch/ArchHooks/ArchHooks.h" +#include "arch/LoadingWindow/LoadingWindow.h" +#include "arch/Dialog/Dialog.h" +#include + +#include "ProductInfo.h" + +#include "Screen.h" +#include "InputEventPlus.h" +#include "ScreenDimensions.h" +#include "CodeDetector.h" +#include "CommonMetrics.h" +#include "Game.h" +#include "RageSurface.h" +#include "RageSurface_Load.h" +#include "CommandLineActions.h" + +#if !defined(SUPPORT_OPENGL) && !defined(SUPPORT_D3D) +#define SUPPORT_OPENGL +#endif + +// StepMania global classes +#include "ThemeManager.h" +#include "NoteSkinManager.h" +#include "PrefsManager.h" +#include "SongManager.h" +#include "CharacterManager.h" +#include "GameState.h" +#include "AnnouncerManager.h" +#include "ProfileManager.h" +#include "MemoryCardManager.h" +#include "ScreenManager.h" +#include "LuaManager.h" +#include "GameManager.h" +#include "FontManager.h" +#include "InputFilter.h" +#include "InputMapper.h" +#include "InputQueue.h" +#include "SongCacheIndex.h" +#include "BannerCache.h" +//#include "BackgroundCache.h" +#include "UnlockManager.h" +#include "RageFileManager.h" +#include "LightsManager.h" +#include "ModelManager.h" +#include "CryptManager.h" +#include "NetworkSyncManager.h" +#include "MessageManager.h" +#include "StatsManager.h" +#include "GameLoop.h" +#include "SpecialFiles.h" +#include "Profile.h" + +#if defined(WIN32) +#include +#endif + +void ShutdownGame(); +bool HandleGlobalInputs( const InputEventPlus &input ); +void HandleInputEvents(float fDeltaTime); + +static Preference g_bAllowMultipleInstances( "AllowMultipleInstances", false ); + +void StepMania::GetPreferredVideoModeParams( VideoModeParams ¶msOut ) +{ + // resolution handling code that probably needs fixing + int iWidth = PREFSMAN->m_iDisplayWidth; + if( PREFSMAN->m_bWindowed ) + { + //float fRatio = PREFSMAN->m_iDisplayHeight; + //iWidth = PREFSMAN->m_iDisplayHeight * fRatio; + iWidth = static_cast(ceilf(PREFSMAN->m_iDisplayHeight * PREFSMAN->m_fDisplayAspectRatio)); + } + + paramsOut = VideoModeParams( + PREFSMAN->m_bWindowed, + iWidth, + PREFSMAN->m_iDisplayHeight, + PREFSMAN->m_iDisplayColorDepth, + PREFSMAN->m_iRefreshRate, + PREFSMAN->m_bVsync, + PREFSMAN->m_bInterlaced, + PREFSMAN->m_bSmoothLines, + PREFSMAN->m_bTrilinearFiltering, + PREFSMAN->m_bAnisotropicFiltering, + CommonMetrics::WINDOW_TITLE, + THEME->GetPathG("Common","window icon"), + PREFSMAN->m_bPAL, + PREFSMAN->m_fDisplayAspectRatio + ); +} + +static LocalizedString COLOR ("StepMania","color"); +static LocalizedString TEXTURE ("StepMania","texture"); +static LocalizedString WINDOWED ("StepMania","Windowed"); +static LocalizedString FULLSCREEN ("StepMania","Fullscreen"); +static LocalizedString ANNOUNCER_ ("StepMania","Announcer"); +static LocalizedString VSYNC ("StepMania","Vsync"); +static LocalizedString NO_VSYNC ("StepMania","NoVsync"); +static LocalizedString SMOOTH_LINES ("StepMania","SmoothLines"); +static LocalizedString NO_SMOOTH_LINES ("StepMania","NoSmoothLines"); + +static RString GetActualGraphicOptionsString() +{ + const VideoModeParams ¶ms = DISPLAY->GetActualVideoModeParams(); + RString sFormat = "%s %s %dx%d %d "+COLOR.GetValue()+" %d "+TEXTURE.GetValue()+" %dHz %s %s"; + RString sLog = ssprintf( sFormat, + DISPLAY->GetApiDescription().c_str(), + (params.windowed? WINDOWED : FULLSCREEN).GetValue().c_str(), + (int)params.width, + (int)params.height, + (int)params.bpp, + (int)PREFSMAN->m_iTextureColorDepth, + (int)params.rate, + (params.vsync? VSYNC : NO_VSYNC).GetValue().c_str(), + (PREFSMAN->m_bSmoothLines? SMOOTH_LINES : NO_SMOOTH_LINES).GetValue().c_str() ); + return sLog; +} + +static void StoreActualGraphicOptions() +{ + /* Store the settings that RageDisplay was actually able to use so that + * we don't go through the process of auto-detecting a usable video mode + * every time. */ + const VideoModeParams ¶ms = DISPLAY->GetActualVideoModeParams(); + PREFSMAN->m_bWindowed.Set( params.windowed ); + + /* If we're windowed, we may have tweaked the width based on the aspect ratio. + * Don't save this new value over the preferred value. */ + if( !PREFSMAN->m_bWindowed ) + { + PREFSMAN->m_iDisplayWidth .Set( params.width ); + PREFSMAN->m_iDisplayHeight .Set( params.height ); + } + PREFSMAN->m_iDisplayColorDepth .Set( params.bpp ); + if( PREFSMAN->m_iRefreshRate != REFRESH_DEFAULT ) + PREFSMAN->m_iRefreshRate.Set( params.rate ); + PREFSMAN->m_bVsync .Set( params.vsync ); + + Dialog::SetWindowed( params.windowed ); +} + +static RageDisplay *CreateDisplay(); + +bool StepMania::GetHighResolutionTextures() +{ + switch( PREFSMAN->m_HighResolutionTextures ) + { + default: + case HighResolutionTextures_Auto: + { + int height = PREFSMAN->m_iDisplayHeight; + return height > 480; + } + case HighResolutionTextures_ForceOn: + return true; + case HighResolutionTextures_ForceOff: + return false; + } +} + +static void StartDisplay() +{ + if( DISPLAY != nullptr ) + return; // already started + + DISPLAY = CreateDisplay(); + + DISPLAY->ChangeCentering( + PREFSMAN->m_iCenterImageTranslateX, + PREFSMAN->m_iCenterImageTranslateY, + PREFSMAN->m_fCenterImageAddWidth, + PREFSMAN->m_fCenterImageAddHeight ); + + TEXTUREMAN = new RageTextureManager; + TEXTUREMAN->SetPrefs( + RageTextureManagerPrefs( + PREFSMAN->m_iTextureColorDepth, + PREFSMAN->m_iMovieColorDepth, + PREFSMAN->m_bDelayedTextureDelete, + PREFSMAN->m_iMaxTextureResolution, + StepMania::GetHighResolutionTextures(), + PREFSMAN->m_bForceMipMaps + ) + ); + + MODELMAN = new ModelManager; + MODELMAN->SetPrefs( + ModelManagerPrefs( + PREFSMAN->m_bDelayedModelDelete + ) + ); +} + +void StepMania::ApplyGraphicOptions() +{ + bool bNeedReload = false; + + VideoModeParams params; + GetPreferredVideoModeParams( params ); + RString sError = DISPLAY->SetVideoMode( params, bNeedReload ); + if( sError != "" ) + RageException::Throw( "%s", sError.c_str() ); + + DISPLAY->ChangeCentering( + PREFSMAN->m_iCenterImageTranslateX, + PREFSMAN->m_iCenterImageTranslateY, + PREFSMAN->m_fCenterImageAddWidth, + PREFSMAN->m_fCenterImageAddHeight ); + + bNeedReload |= TEXTUREMAN->SetPrefs( + RageTextureManagerPrefs( + PREFSMAN->m_iTextureColorDepth, + PREFSMAN->m_iMovieColorDepth, + PREFSMAN->m_bDelayedTextureDelete, + PREFSMAN->m_iMaxTextureResolution, + StepMania::GetHighResolutionTextures(), + PREFSMAN->m_bForceMipMaps + ) + ); + + bNeedReload |= MODELMAN->SetPrefs( + ModelManagerPrefs( + PREFSMAN->m_bDelayedModelDelete + ) + ); + + if( bNeedReload ) + TEXTUREMAN->ReloadAll(); + + StoreActualGraphicOptions(); + if( SCREENMAN ) + SCREENMAN->SystemMessage( GetActualGraphicOptionsString() ); + + // Give the input handlers a chance to re-open devices as necessary. + INPUTMAN->WindowReset(); +} + +static bool CheckVideoDefaultSettings(); + +void StepMania::ResetPreferences() +{ + PREFSMAN->ResetToFactoryDefaults(); + SOUNDMAN->SetMixVolume(); + CheckVideoDefaultSettings(); + ApplyGraphicOptions(); +} + +/* Shutdown all global singletons. Note that this may be called partway through + * initialization, due to an object failing to initialize, in which case some of + * these may still be NULL. */ +void ShutdownGame() +{ + /* First, tell SOUNDMAN that we're shutting down. This signals sound drivers to + * stop sounds, which we want to do before any threads that may have started sounds + * are closed; this prevents annoying DirectSound glitches and delays. */ + if( SOUNDMAN ) + SOUNDMAN->Shutdown(); + + SAFE_DELETE( SCREENMAN ); + SAFE_DELETE( STATSMAN ); + SAFE_DELETE( MESSAGEMAN ); + SAFE_DELETE( NSMAN ); + /* Delete INPUTMAN before the other INPUTFILTER handlers, or an input + * driver may try to send a message to INPUTFILTER after we delete it. */ + SAFE_DELETE( INPUTMAN ); + SAFE_DELETE( INPUTQUEUE ); + SAFE_DELETE( INPUTMAPPER ); + SAFE_DELETE( INPUTFILTER ); + SAFE_DELETE( MODELMAN ); + SAFE_DELETE( PROFILEMAN ); // PROFILEMAN needs the songs still loaded + SAFE_DELETE( CHARMAN ); + SAFE_DELETE( UNLOCKMAN ); + SAFE_DELETE( CRYPTMAN ); + SAFE_DELETE( MEMCARDMAN ); + SAFE_DELETE( SONGMAN ); + SAFE_DELETE( BANNERCACHE ); + //SAFE_DELETE( BACKGROUNDCACHE ); + SAFE_DELETE( SONGINDEX ); + SAFE_DELETE( SOUND ); // uses GAMESTATE, PREFSMAN + SAFE_DELETE( PREFSMAN ); + SAFE_DELETE( GAMESTATE ); + SAFE_DELETE( GAMEMAN ); + SAFE_DELETE( NOTESKIN ); + SAFE_DELETE( THEME ); + SAFE_DELETE( ANNOUNCER ); + SAFE_DELETE( LIGHTSMAN ); + SAFE_DELETE( SOUNDMAN ); + SAFE_DELETE( FONT ); + SAFE_DELETE( TEXTUREMAN ); + SAFE_DELETE( DISPLAY ); + Dialog::Shutdown(); + SAFE_DELETE( LOG ); + SAFE_DELETE( FILEMAN ); + SAFE_DELETE( LUA ); + SAFE_DELETE( HOOKS ); +} + +static void HandleException( const RString &sError ) +{ + if( g_bAutoRestart ) + HOOKS->RestartProgram(); + + // Shut down first, so we exit graphics mode before trying to open a dialog. + ShutdownGame(); + + // Throw up a pretty error dialog. + Dialog::Error( sError ); + Dialog::Shutdown(); // Shut it back down. +} + +void StepMania::ResetGame() +{ + GAMESTATE->Reset(); + + if( !THEME->DoesThemeExist( THEME->GetCurThemeName() ) ) + { + RString sGameName = GAMESTATE->GetCurrentGame()->m_szName; + if( !THEME->DoesThemeExist(sGameName) ) + sGameName = PREFSMAN->m_sDefaultTheme; // was previously "default" -aj + THEME->SwitchThemeAndLanguage( sGameName, THEME->GetCurLanguage(), PREFSMAN->m_bPseudoLocalize ); + TEXTUREMAN->DoDelayedDelete(); + } + + PREFSMAN->SavePrefsToDisk(); +} + +ThemeMetric INITIAL_SCREEN ("Common","InitialScreen"); +RString StepMania::GetInitialScreen() +{ + if( PREFSMAN->m_sTestInitialScreen.Get() != "" ) + return PREFSMAN->m_sTestInitialScreen; + return INITIAL_SCREEN.GetValue(); +} +ThemeMetric SELECT_MUSIC_SCREEN ("Common","SelectMusicScreen"); +RString StepMania::GetSelectMusicScreen() +{ + return SELECT_MUSIC_SCREEN.GetValue(); +} + +#if defined(WIN32) +static Preference g_iLastSeenMemory( "LastSeenMemory", 0 ); +#endif + +static void AdjustForChangedSystemCapabilities() +{ +#if defined(WIN32) + // Has the amount of memory changed? + MEMORYSTATUS mem; + GlobalMemoryStatus(&mem); + + const int Memory = mem.dwTotalPhys / (1024*1024); + + if( g_iLastSeenMemory == Memory ) + return; + + LOG->Trace( "Memory changed from %i to %i; settings changed", g_iLastSeenMemory.Get(), Memory ); + g_iLastSeenMemory.Set( Memory ); + + // is this assumption outdated? -aj + /* Let's consider 128-meg systems low-memory, and 256-meg systems high-memory. + * Cut off at 192. This is pretty conservative; many 128-meg systems can + * deal with higher memory profile settings, but some can't. + * + * Actually, Windows lops off a meg or two; cut off a little lower to treat + * 192-meg systems as high-memory. */ + const bool HighMemory = (Memory >= 190); + const bool LowMemory = (Memory < 100); // 64 and 96-meg systems + + /* Two memory-consuming features that we can disable are texture caching and + * preloaded banners. Texture caching can use a lot of memory; disable it for + * low-memory systems. */ + PREFSMAN->m_bDelayedTextureDelete.Set( HighMemory ); + + /* Preloaded banners takes about 9k per song. Although it's smaller than the + * actual song data, it still adds up with a lot of songs. + * Disable it for 64-meg systems. */ + PREFSMAN->m_BannerCache.Set( LowMemory ? BNCACHE_OFF:BNCACHE_LOW_RES_PRELOAD ); + // might wanna do this for backgrounds, too... -aj + //PREFSMAN->m_BackgroundCache.Set( LowMemory ? BGCACHE_OFF:BGCACHE_LOW_RES_PRELOAD ); + + PREFSMAN->SavePrefsToDisk(); +#endif +} + +#if defined(WIN32) +#include "RageDisplay_D3D.h" +#include "archutils/Win32/VideoDriverInfo.h" +#endif + +#if defined(SUPPORT_OPENGL) +#include "RageDisplay_OGL.h" +#endif + +#if defined(SUPPORT_GLES2) +#include "RageDisplay_GLES2.h" +#endif + +#include "RageDisplay_Null.h" + + +struct VideoCardDefaults +{ + RString sDriverRegex; + RString sVideoRenderers; + int iWidth; + int iHeight; + int iDisplayColor; + int iTextureColor; + int iMovieColor; + int iTextureSize; + bool bSmoothLines; + + VideoCardDefaults() {} + VideoCardDefaults( + RString sDriverRegex_, + RString sVideoRenderers_, + int iWidth_, + int iHeight_, + int iDisplayColor_, + int iTextureColor_, + int iMovieColor_, + int iTextureSize_, + bool bSmoothLines_ + ) + { + sDriverRegex = sDriverRegex_; + sVideoRenderers = sVideoRenderers_; + iWidth = iWidth_; + iHeight = iHeight_; + iDisplayColor = iDisplayColor_; + iTextureColor = iTextureColor_; + iMovieColor = iMovieColor_; + iTextureSize = iTextureSize_; + bSmoothLines = bSmoothLines_; + } +} const g_VideoCardDefaults[] = +{ + VideoCardDefaults( + "Voodoo *5", + "d3d,opengl", // received 3 reports of opengl crashing. -Chris + 640,480, + 32,32,32, + 2048, + true // accelerated + ), + VideoCardDefaults( + "Voodoo|3dfx", // all other Voodoos: some drivers don't identify which one + "d3d,opengl", + 640,480, + 16,16,16, + 256, + false // broken, causes black screen + ), + VideoCardDefaults( + "Radeon.* 7|Wonder 7500|ArcadeVGA", // Radeon 7xxx, RADEON Mobility 7500 + "d3d,opengl", // movie texture performance is terrible in OpenGL, but fine in D3D. + 640,480, + 16,16,16, + 2048, + true // accelerated + ), + VideoCardDefaults( + "GeForce|Radeon|Wonder 9|Quadro", + "opengl,d3d", + 640,480, + 32,32,32, // 32 bit textures are faster to load + 2048, + true // hardware accelerated + ), + VideoCardDefaults( + "TNT|Vanta|M64", + "opengl,d3d", + 640,480, + 16,16,16, // Athlon 1.2+TNT demonstration w/ movies: 70fps w/ 32bit textures, 86fps w/ 16bit textures + 2048, + true // hardware accelerated + ), + VideoCardDefaults( + "G200|G250|G400", + "d3d,opengl", + 640,480, + 16,16,16, + 2048, + false // broken, causes black screen + ), + VideoCardDefaults( + "Savage", + "d3d", + // OpenGL is unusable on my Savage IV with even the latest drivers. + // It draws 30 frames of gibberish then crashes. This happens even with + // simple NeHe demos. -Chris + 640,480, + 16,16,16, + 2048, + false + ), + VideoCardDefaults( + "XPERT@PLAY|IIC|RAGE PRO|RAGE LT PRO", // Rage Pro chip, Rage IIC chip + "d3d", + // OpenGL is not hardware accelerated, despite the fact that the + // drivers come with an ICD. Also, the WinXP driver performance + // is terrible and supports only 640. The ATI driver is usable. + // -Chris + 320,240, // lower resolution for 60fps. In-box WinXP driver doesn't support 400x300. + 16,16,16, + 256, + false + ), + VideoCardDefaults( + "RAGE MOBILITY-M1", + "d3d,opengl", // Vertex alpha is broken in OpenGL, but not D3D. -Chris + 400,300, // lower resolution for 60fps + 16,16,16, + 256, + false + ), + VideoCardDefaults( + "Mobility M3", // ATI Rage Mobility 128 (AKA "M3") + "d3d,opengl", // bad movie texture performance in opengl + 640,480, + 16,16,16, + 1024, + false + ), + VideoCardDefaults( + "Intel.*82810|Intel.*82815", + "opengl,d3d",// OpenGL is 50%+ faster than D3D w/ latest Intel drivers. -Chris + 512,384, // lower resolution for 60fps + 16,16,16, + 512, + false + ), + VideoCardDefaults( + "Intel*Extreme Graphics", + "d3d", // OpenGL blue screens w/ XP drivers from 6-21-2002 + 640,480, + 16,16,16, // slow at 32bpp + 1024, + false + ), + VideoCardDefaults( + "Intel.*", /* fallback: all unknown Intel cards to D3D, since Intel is notoriously bad at OpenGL */ + "d3d,opengl", + 640,480, + 16,16,16, + 2048, + false + ), + VideoCardDefaults( + // Cards that have problems with OpenGL: + // ASSERT fail somewhere in RageDisplay_OpenGL "Trident Video Accelerator CyberBlade" + // bug 764499: ASSERT fail after glDeleteTextures for "SiS 650_651_740" + // bug 764830: ASSERT fail after glDeleteTextures for "VIA Tech VT8361/VT8601 Graphics Controller" + // bug 791950: AV in glsis630!DrvSwapBuffers for "SiS 630/730" + "Trident Video Accelerator CyberBlade|VIA.*VT|SiS 6*", + "d3d,opengl", + 640,480, + 16,16,16, + 2048, + false + ), + VideoCardDefaults( + /* Unconfirmed texture problems on this; let's try D3D, since it's + * a VIA/S3 chipset. */ + "VIA/S3G KM400/KN400", + "d3d,opengl", + 640,480, + 16,16,16, + 2048, + false + ), + VideoCardDefaults( + "OpenGL", // This matches all drivers in Mac and Linux. -Chris + "opengl", + 640,480, + 16,16,16, + 2048, + true // Right now, they've got to have NVidia or ATi Cards anyway.. + ), + VideoCardDefaults( + // Default graphics settings used for all cards that don't match above. + // This must be the very last entry! + "", + "opengl,d3d", + 640,480, + 32,32,32, + 2048, + false // AA is slow on some cards, so let's selectively enable HW accelerated cards. + ), +}; + + +static RString GetVideoDriverName() +{ +#if defined(_WINDOWS) + return GetPrimaryVideoDriverName(); +#else + return "OpenGL"; +#endif +} + +bool CheckVideoDefaultSettings() +{ + // Video card changed since last run + RString sVideoDriver = GetVideoDriverName(); + + LOG->Trace( "Last seen video driver: %s", PREFSMAN->m_sLastSeenVideoDriver.Get().c_str() ); + + VideoCardDefaults defaults; + + unsigned i; + for( i=0; iTrace( "Card matches '%s'.", sDriverRegex.size()? sDriverRegex.c_str():"(unknown card)" ); + break; + } + } + if (i >= ARRAYLEN(g_VideoCardDefaults)) + { + FAIL_M("Failed to match video driver"); + } + + bool bSetDefaultVideoParams = false; + if( PREFSMAN->m_sVideoRenderers.Get() == "" ) + { + bSetDefaultVideoParams = true; + LOG->Trace( "Applying defaults for %s.", sVideoDriver.c_str() ); + } + else if( PREFSMAN->m_sLastSeenVideoDriver.Get() != sVideoDriver ) + { + bSetDefaultVideoParams = true; + LOG->Trace( "Video card has changed from %s to %s. Applying new defaults.", PREFSMAN->m_sLastSeenVideoDriver.Get().c_str(), sVideoDriver.c_str() ); + } + + if( bSetDefaultVideoParams ) + { + PREFSMAN->m_sVideoRenderers.Set( defaults.sVideoRenderers ); + PREFSMAN->m_iDisplayWidth.Set( defaults.iWidth ); + PREFSMAN->m_iDisplayHeight.Set( defaults.iHeight ); + PREFSMAN->m_iDisplayColorDepth.Set( defaults.iDisplayColor ); + PREFSMAN->m_iTextureColorDepth.Set( defaults.iTextureColor ); + PREFSMAN->m_iMovieColorDepth.Set( defaults.iMovieColor ); + PREFSMAN->m_iMaxTextureResolution.Set( defaults.iTextureSize ); + PREFSMAN->m_bSmoothLines.Set( defaults.bSmoothLines ); + // this only worked when we started in fullscreen by default. -aj + //PREFSMAN->m_fDisplayAspectRatio.Set( HOOKS->GetDisplayAspectRatio() ); + // now that we start in windowed mode, use the new default aspect ratio. + PREFSMAN->m_fDisplayAspectRatio.Set( PREFSMAN->m_fDisplayAspectRatio ); + + // Update last seen video card + PREFSMAN->m_sLastSeenVideoDriver.Set( GetVideoDriverName() ); + } + else if( PREFSMAN->m_sVideoRenderers.Get().CompareNoCase(defaults.sVideoRenderers) ) + { + LOG->Warn("Video renderer list has been changed from '%s' to '%s'", + defaults.sVideoRenderers.c_str(), PREFSMAN->m_sVideoRenderers.Get().c_str() ); + } + + LOG->Info( "Video renderers: '%s'", PREFSMAN->m_sVideoRenderers.Get().c_str() ); + return bSetDefaultVideoParams; +} + +static LocalizedString ERROR_INITIALIZING_CARD ( "StepMania", "There was an error while initializing your video card." ); +static LocalizedString ERROR_DONT_FILE_BUG ( "StepMania", "Please do not file this error as a bug! Use the web page below to troubleshoot this problem." ); +static LocalizedString ERROR_VIDEO_DRIVER ( "StepMania", "Video Driver: %s" ); +static LocalizedString ERROR_NO_VIDEO_RENDERERS ( "StepMania", "No video renderers attempted." ); +static LocalizedString ERROR_INITIALIZING ( "StepMania", "Initializing %s..." ); +static LocalizedString ERROR_UNKNOWN_VIDEO_RENDERER ( "StepMania", "Unknown video renderer value: %s" ); + +RageDisplay *CreateDisplay() +{ + /* We never want to bother users with having to decide which API to use. + * + * Some cards simply are too troublesome with OpenGL to ever use it, eg. Voodoos. + * If D3D8 isn't installed on those, complain and refuse to run (by default). + * For others, always use OpenGL. Allow forcing to D3D as an advanced option. + * + * If we're missing acceleration when we load D3D8 due to a card being in the + * D3D list, it means we need drivers and that they do exist. + * + * If we try to load OpenGL and we're missing acceleration, it may mean: + * 1. We're missing drivers, and they just need upgrading. + * 2. The card doesn't have drivers, and it should be using D3D8. + * In other words, it needs an entry in this table. + * 3. The card doesn't have drivers for either. (Sorry, no S3 868s.) + * Can't play. + * In this case, fail to load; don't silently fall back on D3D. We don't want + * people unknowingly using D3D8 with old drivers (and reporting obscure bugs + * due to driver problems). We'll probably get bug reports for all three types. + * #2 is the only case that's actually a bug. + * + * Actually, right now we're falling back. I'm not sure which behavior is better. + */ + + //bool bAppliedDefaults = CheckVideoDefaultSettings(); + CheckVideoDefaultSettings(); + + VideoModeParams params; + StepMania::GetPreferredVideoModeParams( params ); + + RString error = ERROR_INITIALIZING_CARD.GetValue()+"\n\n"+ + ERROR_DONT_FILE_BUG.GetValue()+"\n\n" + VIDEO_TROUBLESHOOTING_URL "\n\n"+ + ssprintf(ERROR_VIDEO_DRIVER.GetValue(), GetVideoDriverName().c_str())+"\n\n"; + + vector asRenderers; + split( PREFSMAN->m_sVideoRenderers, ",", asRenderers, true ); + + if( asRenderers.empty() ) + RageException::Throw( "%s", ERROR_NO_VIDEO_RENDERERS.GetValue().c_str() ); + + RageDisplay *pRet = NULL; + for( unsigned i=0; iInit( params, PREFSMAN->m_bAllowUnacceleratedRenderer ); + if( !sError.empty() ) + { + error += ssprintf(ERROR_INITIALIZING.GetValue(), sRenderer.c_str())+"\n" + sError; + SAFE_DELETE( pRet ); + error += "\n\n\n"; + continue; + } + + break; // the display is ready to go + } + + if( pRet == NULL) + RageException::Throw( "%s", error.c_str() ); + + return pRet; +} + +static void SwitchToLastPlayedGame() +{ + const Game *pGame = GAMEMAN->StringToGame( PREFSMAN->GetCurrentGame() ); + + // If the active game type isn't actually available, revert to the default. + if( pGame == NULL ) + pGame = GAMEMAN->GetDefaultGame(); + + if( !GAMEMAN->IsGameEnabled( pGame ) && pGame != GAMEMAN->GetDefaultGame() ) + { + pGame = GAMEMAN->GetDefaultGame(); + LOG->Warn( "Default NoteSkin for \"%s\" missing, reverting to \"%s\"", + pGame->m_szName, GAMEMAN->GetDefaultGame()->m_szName ); + } + + ASSERT( GAMEMAN->IsGameEnabled(pGame) ); + + StepMania::ChangeCurrentGame( pGame ); +} + +void StepMania::ChangeCurrentGame( const Game* g ) +{ + ASSERT( g != nullptr ); + ASSERT( GAMESTATE != nullptr ); + ASSERT( ANNOUNCER != nullptr ); + ASSERT( THEME != nullptr ); + + GAMESTATE->SetCurGame( g ); + + RString sAnnouncer = PREFSMAN->m_sAnnouncer; + RString sTheme = PREFSMAN->m_sTheme; + RString sLanguage = PREFSMAN->m_sLanguage; + + if( sAnnouncer.empty() ) + sAnnouncer = GAMESTATE->GetCurrentGame()->m_szName; + if( sTheme.empty() ) + sTheme = GAMESTATE->GetCurrentGame()->m_szName; + + // process theme and language command line arguments; + // these change the preferences in order for transparent loading -aj + RString argTheme; + if( GetCommandlineArgument( "theme",&argTheme) && argTheme != sTheme ) + { + sTheme = argTheme; + // set theme in preferences too for correct behavior -aj + PREFSMAN->m_sTheme.Set(sTheme); + } + + RString argLanguage; + if( GetCommandlineArgument( "language",&argLanguage) ) + { + sLanguage = argLanguage; + // set language in preferences too for correct behavior -aj + PREFSMAN->m_sLanguage.Set(sLanguage); + } + + // it's OK to call these functions with names that don't exist. + ANNOUNCER->SwitchAnnouncer( sAnnouncer ); + THEME->SwitchThemeAndLanguage( sTheme, sLanguage, PREFSMAN->m_bPseudoLocalize ); + + // Set the input scheme for the new game, and load keymaps. + if( INPUTMAPPER ) + { + INPUTMAPPER->SetInputScheme( &g->m_InputScheme ); + INPUTMAPPER->ReadMappingsFromDisk(); + } +} + +static void MountTreeOfZips( const RString &dir ) +{ + vector dirs; + dirs.push_back( dir ); + + while( dirs.size() ) + { + RString path = dirs.back(); + dirs.pop_back(); + + if( !IsADirectory(path) ) + continue; + + vector zips; + GetDirListing( path + "/*.zip", zips, false, true ); + GetDirListing( path + "/*.smzip", zips, false, true ); + + for( unsigned i = 0; i < zips.size(); ++i ) + { + if( !IsAFile(zips[i]) ) + continue; + + LOG->Trace( "VFS: found %s", zips[i].c_str() ); + FILEMAN->Mount( "zip", zips[i], "/" ); + } + + GetDirListing( path + "/*", dirs, true, true ); + } +} + +#if defined(HAVE_VERSION_INFO) +extern unsigned long version_num; +extern const char *const version_date; +extern const char *const version_time; +#endif + +static void WriteLogHeader() +{ + LOG->Info( PRODUCT_ID_VER ); + +#if defined(HAVE_VERSION_INFO) + LOG->Info( "Compiled %s @ %s (build %lu)", version_date, version_time, version_num ); +#endif + + // this code should only be enabled in distributed builds + //LOG->Info("sm-ssc is Copyright �2009 the spinal shark collective, all rights reserved. Commercial use of this binary is prohibited by law and will be prosecuted to the fullest extent of the law."); + // end limited code + + time_t cur_time; + time(&cur_time); + struct tm now; + localtime_r( &cur_time, &now ); + + LOG->Info( "Log starting %.4d-%.2d-%.2d %.2d:%.2d:%.2d", + 1900+now.tm_year, now.tm_mon+1, now.tm_mday, now.tm_hour, now.tm_min, now.tm_sec ); + LOG->Trace( " " ); + + if( g_argc > 1 ) + { + RString args; + for( int i = 1; i < g_argc; ++i ) + { + if( i>1 ) + args += " "; + + // surround all params with some marker, as they might have whitespace. + // using [[ and ]], as they are not likely to be in the params. + args += ssprintf( "[[%s]]", g_argv[i] ); + } + LOG->Info( "Command line args (count=%d): %s", (g_argc - 1), args.c_str()); + } +} + +static void ApplyLogPreferences() +{ + LOG->SetShowLogOutput( PREFSMAN->m_bShowLogOutput ); + LOG->SetLogToDisk( PREFSMAN->m_bLogToDisk ); + LOG->SetInfoToDisk( true ); + LOG->SetUserLogToDisk( true ); + LOG->SetFlushing( PREFSMAN->m_bForceLogFlush ); + Checkpoints::LogCheckpoints( PREFSMAN->m_bLogCheckpoints ); +} + +static LocalizedString COULDNT_OPEN_LOADING_WINDOW( "LoadingWindow", "Couldn't open any loading windows." ); + +int main(int argc, char* argv[]) +{ + RageThreadRegister thread( "Main thread" ); + RageException::SetCleanupHandler( HandleException ); + + SetCommandlineArguments( argc, argv ); + + // Set up arch hooks first. This may set up crash handling. + HOOKS = ArchHooks::Create(); + HOOKS->Init(); + + LUA = new LuaManager; + + // Almost everything uses this to read and write files. Load this early. + FILEMAN = new RageFileManager( argv[0] ); + FILEMAN->MountInitialFilesystems(); + + bool bPortable = DoesFileExist("Portable.ini"); + if( !bPortable ) + FILEMAN->MountUserFilesystems(); + + // Set this up next. Do this early, since it's needed for RageException::Throw. + LOG = new RageLog; + + // Whew--we should be able to crash safely now! + + // load preferences and mount any alternative trees. + PREFSMAN = new PrefsManager; + + /* Allow HOOKS to check for multiple instances. We need to do this after PREFS is initialized, + * so ArchHooks can use a preference to turn this off. We want to do this before ApplyLogPreferences, + * so if we exit because of another instance, we don't try to clobber its log. We also want to + * do this before opening the loading window, so if we give focus away, we don't flash the window. */ + if(!g_bAllowMultipleInstances.Get() && HOOKS->CheckForMultipleInstances(argc, argv)) + { + ShutdownGame(); + return 0; + } + + ApplyLogPreferences(); + + WriteLogHeader(); + + // Set up alternative filesystem trees. + if( PREFSMAN->m_sAdditionalFolders.Get() != "" ) + { + vector dirs; + split( PREFSMAN->m_sAdditionalFolders, ",", dirs, true ); + for( unsigned i=0; i < dirs.size(); i++) + FILEMAN->Mount( "dir", dirs[i], "/" ); + } + if( PREFSMAN->m_sAdditionalSongFolders.Get() != "" ) + { + vector dirs; + split( PREFSMAN->m_sAdditionalSongFolders, ",", dirs, true ); + for( unsigned i=0; i < dirs.size(); i++) + FILEMAN->Mount( "dir", dirs[i], "/AdditionalSongs" ); + } + if( PREFSMAN->m_sAdditionalCourseFolders.Get() != "" ) + { + vector dirs; + split( PREFSMAN->m_sAdditionalCourseFolders, ",", dirs, true ); + for( unsigned i=0; i < dirs.size(); i++) + FILEMAN->Mount( "dir", dirs[i], "/AdditionalCourses" ); + } + + MountTreeOfZips( SpecialFiles::PACKAGES_DIR ); + MountTreeOfZips( SpecialFiles::USER_PACKAGES_DIR ); + + /* One of the above filesystems might contain files that affect preferences + * (e.g. Data/Static.ini). Re-read preferences. */ + PREFSMAN->ReadPrefsFromDisk(); + ApplyLogPreferences(); + + // This needs PREFSMAN. + Dialog::Init(); + + // Create game objects + + GAMESTATE = new GameState; + + // This requires PREFSMAN, for PREFSMAN->m_bShowLoadingWindow. + LoadingWindow *pLoadingWindow = LoadingWindow::Create(); + if(pLoadingWindow == NULL) + RageException::Throw("%s", COULDNT_OPEN_LOADING_WINDOW.GetValue().c_str()); + + srand( time(NULL) ); // seed number generator + + /* Do this early, so we have debugging output if anything else fails. LOG and + * Dialog must be set up first. It shouldn't take long, but it might take a + * little time; do this after the LoadingWindow is shown, since we don't want + * that to appear delayed. */ + HOOKS->DumpDebugInfo(); + +#if defined(HAVE_TLS) + LOG->Info( "TLS is %savailable", RageThread::GetSupportsTLS()? "":"not " ); +#endif + + AdjustForChangedSystemCapabilities(); + + GAMEMAN = new GameManager; + THEME = new ThemeManager; + ANNOUNCER = new AnnouncerManager; + NOTESKIN = new NoteSkinManager; + + // Switch to the last used game type, and set up the theme and announcer. + SwitchToLastPlayedGame(); + + CommandLineActions::Handle(pLoadingWindow); + + // Aldo: Check for updates here! + if( /* PREFSMAN->m_bUpdateCheckEnable (do this later) */ 0 ) + { + // TODO - Aldo_MX: Use PREFSMAN->m_iUpdateCheckIntervalSeconds & PREFSMAN->m_iUpdateCheckLastCheckedSecond + unsigned long current_version = NetworkSyncManager::GetCurrentSMBuild( pLoadingWindow ); + if( current_version ) + { + if( current_version > version_num ) + { + switch( Dialog::YesNo( "A new version of " PRODUCT_ID " is available. Do you want to download it?", "UpdateCheck" ) ) + { + case Dialog::yes: + //PREFSMAN->SavePrefsToDisk(); + // TODO: GoToURL for Linux + if( !HOOKS->GoToURL( SM_DOWNLOAD_URL ) ) + { + Dialog::Error( "Please go to the following URL to download the latest version of " PRODUCT_ID ":\n\n" SM_DOWNLOAD_URL, "UpdateCheckConfirm" ); + } + ShutdownGame(); + return 0; + case Dialog::no: + break; + default: + FAIL_M("Invalid response to Yes/No dialog"); + } + } + else if( version_num < current_version ) + { + LOG->Info( "The current version is more recent than the public one, double check you downloaded it from " SM_DOWNLOAD_URL ); + } + } + else + { + LOG->Info( "Unable to check for updates. The server might be offline." ); + } + } + + if( GetCommandlineArgument("dopefish") ) + GAMESTATE->m_bDopefish = true; + + { + /* Now that THEME is loaded, load the icon and splash for the current + * theme into the loading window. */ + RString sError; + RageSurface *pSurface = RageSurfaceUtils::LoadFile( THEME->GetPathG( "Common", "window icon" ), sError ); + if( pSurface != nullptr ) + pLoadingWindow->SetIcon( pSurface ); + delete pSurface; + pSurface = RageSurfaceUtils::LoadFile( THEME->GetPathG("Common","splash"), sError ); + if( pSurface != nullptr ) + pLoadingWindow->SetSplash( pSurface ); + delete pSurface; + } + + if( PREFSMAN->m_iSoundWriteAhead ) + LOG->Info( "Sound writeahead has been overridden to %i", PREFSMAN->m_iSoundWriteAhead.Get() ); + + SOUNDMAN = new RageSoundManager; + SOUNDMAN->Init(); + SOUNDMAN->SetMixVolume(); + SOUND = new GameSoundManager; + LIGHTSMAN = new LightsManager; + INPUTFILTER = new InputFilter; + INPUTMAPPER = new InputMapper; + + StepMania::ChangeCurrentGame( GAMESTATE->GetCurrentGame() ); + + INPUTQUEUE = new InputQueue; + SONGINDEX = new SongCacheIndex; + BANNERCACHE = new BannerCache; + //BACKGROUNDCACHE = new BackgroundCache; + + // depends on SONGINDEX: + SONGMAN = new SongManager; + SONGMAN->InitAll( pLoadingWindow ); // this takes a long time + CRYPTMAN = new CryptManager; // need to do this before ProfileMan + if( PREFSMAN->m_bSignProfileData ) + CRYPTMAN->GenerateGlobalKeys(); + MEMCARDMAN = new MemoryCardManager; + CHARMAN = new CharacterManager; + PROFILEMAN = new ProfileManager; + PROFILEMAN->Init(); // must load after SONGMAN + UNLOCKMAN = new UnlockManager; + SONGMAN->UpdatePopular(); + SONGMAN->UpdatePreferredSort(); + NSMAN = new NetworkSyncManager( pLoadingWindow ); + MESSAGEMAN = new MessageManager; + STATSMAN = new StatsManager; + + // Initialize which courses are ranking courses here. + SONGMAN->UpdateRankingCourses(); + + SAFE_DELETE( pLoadingWindow ); // destroy this before init'ing Display + + /* If the user has tried to quit during the loading, do it before creating + * the main window. This prevents going to full screen just to quit. */ + if( ArchHooks::UserQuit() ) + { + ShutdownGame(); + return 0; + } + + StartDisplay(); + + StoreActualGraphicOptions(); + LOG->Info( "%s", GetActualGraphicOptionsString().c_str() ); + + SONGMAN->PreloadSongImages(); + + /* Input handlers can have dependences on the video system so + * INPUTMAN must be initialized after DISPLAY. */ + INPUTMAN = new RageInput; + + // These things depend on the TextureManager, so do them after! + FONT = new FontManager; + SCREENMAN = new ScreenManager; + + StepMania::ResetGame(); + + /* Now that GAMESTATE is reset, tell SCREENMAN to update the theme (load + * overlay screens and global sounds), and load the initial screen. */ + SCREENMAN->ThemeChanged(); + SCREENMAN->SetNewScreen( StepMania::GetInitialScreen() ); + + // Do this after ThemeChanged so that we can show a system message + RString sMessage; + if( INPUTMAPPER->CheckForChangedInputDevicesAndRemap(sMessage) ) + SCREENMAN->SystemMessage( sMessage ); + + CodeDetector::RefreshCacheItems(); + + if( GetCommandlineArgument("netip") ) + NSMAN->DisplayStartupStatus(); // If we're using networking show what happened + + // Run the main loop. + GameLoop::RunGameLoop(); + + PREFSMAN->SavePrefsToDisk(); + + ShutdownGame(); + + return 0; +} + +RString StepMania::SaveScreenshot( RString sDir, bool bSaveCompressed, bool bMakeSignature, int iIndex ) +{ + /* As of sm-ssc v1.0 rc2, screenshots are no longer named by an arbitrary + * index. This was causing naming issues for some unknown reason, so we have + * changed the screenshot names to a non-blocking format: date and time. + * As before, we ignore the extension. -aj */ + RString sFileNameNoExtension = DateTime::GetNowDateTime().GetString(); + // replace space with underscore. + sFileNameNoExtension.Replace(" ","_"); + // colons are illegal in filenames. + sFileNameNoExtension.Replace(":",""); + + // Save the screenshot. If writing lossy to a memcard, use + // SAVE_LOSSY_LOW_QUAL, so we don't eat up lots of space. + RageDisplay::GraphicsFileFormat fmt; + if( bSaveCompressed && MEMCARDMAN->PathIsMemCard(sDir) ) + fmt = RageDisplay::SAVE_LOSSY_LOW_QUAL; + else if( bSaveCompressed ) + fmt = RageDisplay::SAVE_LOSSY_HIGH_QUAL; + else + fmt = RageDisplay::SAVE_LOSSLESS_SENSIBLE; + + RString sFileName = sFileNameNoExtension + "." + (bSaveCompressed ? "jpg" : "png"); + RString sPath = sDir+sFileName; + bool bResult = DISPLAY->SaveScreenshot( sPath, fmt ); + if( !bResult ) + { + SCREENMAN->PlayInvalidSound(); + return RString(); + } + + SCREENMAN->PlayScreenshotSound(); + + if( PREFSMAN->m_bSignProfileData && bMakeSignature ) + CryptManager::SignFileToFile( sPath ); + + return sFileName; +} + +/* Returns true if the key has been handled and should be discarded, false if + * the key should be sent on to screens. */ +static LocalizedString SERVICE_SWITCH_PRESSED ( "StepMania", "Service switch pressed" ); +static LocalizedString RELOADED_METRICS( "ThemeManager", "Reloaded metrics" ); +static LocalizedString RELOADED_METRICS_AND_TEXTURES( "ThemeManager", "Reloaded metrics and textures" ); +static LocalizedString RELOADED_SCRIPTS( "ThemeManager", "Reloaded scripts" ); +static LocalizedString RELOADED_OVERLAY_SCREENS( "ThemeManager", "Reloaded overlay screens" ); +bool HandleGlobalInputs( const InputEventPlus &input ) +{ + // None of the globals keys act on types other than FIRST_PRESS + if( input.type != IET_FIRST_PRESS ) + return false; + + switch( input.MenuI ) + { + case GAME_BUTTON_OPERATOR: + /* Global operator key, to get quick access to the options menu. Don't + * do this if we're on a "system menu", which includes the editor + * (to prevent quitting without storing changes). */ + if( SCREENMAN->AllowOperatorMenuButton() ) + { + SCREENMAN->SystemMessage( SERVICE_SWITCH_PRESSED ); + SCREENMAN->PopAllScreens(); + GAMESTATE->Reset(); + SCREENMAN->SetNewScreen( CommonMetrics::OPERATOR_MENU_SCREEN ); + } + return true; + + default: break; + } + + /* Re-added for StepMania 3.9 theming veterans, plus it's just faster than + * the debug menu. The Shift button only reloads the metrics, unlike in 3.9 + * (where it saved machine profile). -aj */ + bool bIsShiftHeld = INPUTFILTER->IsBeingPressed( DeviceInput(DEVICE_KEYBOARD, KEY_LSHIFT), &input.InputList) || + INPUTFILTER->IsBeingPressed( DeviceInput(DEVICE_KEYBOARD, KEY_RSHIFT), &input.InputList); + bool bIsCtrlHeld = INPUTFILTER->IsBeingPressed( DeviceInput(DEVICE_KEYBOARD, KEY_LCTRL), &input.InputList) || + INPUTFILTER->IsBeingPressed( DeviceInput(DEVICE_KEYBOARD, KEY_RCTRL), &input.InputList); + if( input.DeviceI == DeviceInput(DEVICE_KEYBOARD, KEY_F2) ) + { + if( bIsShiftHeld && !bIsCtrlHeld ) + { + // Shift+F2: refresh metrics,noteskin cache and CodeDetector cache only + THEME->ReloadMetrics(); + NOTESKIN->RefreshNoteSkinData( GAMESTATE->m_pCurGame ); + CodeDetector::RefreshCacheItems(); + SCREENMAN->SystemMessage( RELOADED_METRICS ); + } + else if( bIsCtrlHeld && !bIsShiftHeld ) + { + // Ctrl+F2: reload scripts only + THEME->UpdateLuaGlobals(); + SCREENMAN->SystemMessage( RELOADED_SCRIPTS ); + } + else if( bIsCtrlHeld && bIsShiftHeld ) + { + // Shift+Ctrl+F2: reload overlay screens (and metrics, since themers + // are likely going to do this after changing metrics.) + THEME->ReloadMetrics(); + SCREENMAN->ReloadOverlayScreens(); + SCREENMAN->SystemMessage( RELOADED_OVERLAY_SCREENS ); + } + else + { + // F2 alone: refresh metrics, textures, noteskins, codedetector cache + THEME->ReloadMetrics(); + TEXTUREMAN->ReloadAll(); + NOTESKIN->RefreshNoteSkinData( GAMESTATE->m_pCurGame ); + CodeDetector::RefreshCacheItems(); + SCREENMAN->SystemMessage( RELOADED_METRICS_AND_TEXTURES ); + } + + return true; + } + + if( input.DeviceI == DeviceInput(DEVICE_KEYBOARD, KEY_PAUSE) ) + { + Message msg("ToggleConsoleDisplay"); + MESSAGEMAN->Broadcast( msg ); + return true; + } + +#if !defined(MACOSX) + if( input.DeviceI == DeviceInput(DEVICE_KEYBOARD, KEY_F4) ) + { + if( INPUTFILTER->IsBeingPressed( DeviceInput(DEVICE_KEYBOARD, KEY_RALT), &input.InputList) || + INPUTFILTER->IsBeingPressed( DeviceInput(DEVICE_KEYBOARD, KEY_LALT), &input.InputList) ) + { + // pressed Alt+F4 + ArchHooks::SetUserQuit(); + return true; + } + } +#else + if( input.DeviceI == DeviceInput(DEVICE_KEYBOARD, KEY_Cq) && + (INPUTFILTER->IsBeingPressed( DeviceInput(DEVICE_KEYBOARD, KEY_LMETA), &input.InputList ) || + INPUTFILTER->IsBeingPressed( DeviceInput(DEVICE_KEYBOARD, KEY_RMETA), &input.InputList )) ) + { + /* The user quit is handled by the menu item so we don't need to set it + * here; however, we do want to return that it has been handled since + * this will happen first. */ + return true; + } +#endif + + bool bDoScreenshot = +#if defined(MACOSX) + // Notebooks don't have F13. Use cmd-F12 as well. + input.DeviceI == DeviceInput( DEVICE_KEYBOARD, KEY_PRTSC ) || + input.DeviceI == DeviceInput( DEVICE_KEYBOARD, KEY_F13 ) || + ( input.DeviceI == DeviceInput(DEVICE_KEYBOARD, KEY_F12) && + (INPUTFILTER->IsBeingPressed(DeviceInput(DEVICE_KEYBOARD, KEY_LMETA), &input.InputList) || + INPUTFILTER->IsBeingPressed(DeviceInput(DEVICE_KEYBOARD, KEY_RMETA), &input.InputList)) ); +#else + /* The default Windows message handler will capture the desktop window upon + * pressing PrntScrn, or will capture the foreground with focus upon pressing + * Alt+PrntScrn. Windows will do this whether or not we save a screenshot + * ourself by dumping the frame buffer. */ + // "if pressing PrintScreen and not pressing Alt" + input.DeviceI == DeviceInput(DEVICE_KEYBOARD, KEY_PRTSC) && + !INPUTFILTER->IsBeingPressed(DeviceInput(DEVICE_KEYBOARD, KEY_LALT), &input.InputList) && + !INPUTFILTER->IsBeingPressed(DeviceInput(DEVICE_KEYBOARD, KEY_RALT), &input.InputList); +#endif + if( bDoScreenshot ) + { + // If holding Shift save uncompressed, else save compressed + bool bHoldingShift = ( INPUTFILTER->IsBeingPressed( DeviceInput(DEVICE_KEYBOARD, KEY_LSHIFT) ) + || INPUTFILTER->IsBeingPressed( DeviceInput(DEVICE_KEYBOARD, KEY_RSHIFT) ) ); + bool bSaveCompressed = !bHoldingShift; + RageTimer timer; + StepMania::SaveScreenshot( "Screenshots/", bSaveCompressed, false, -1 ); + LOG->Trace( "Screenshot took %f seconds.", timer.GetDeltaTime() ); + return true; // handled + } + + if( input.DeviceI == DeviceInput(DEVICE_KEYBOARD, KEY_ENTER) && + (INPUTFILTER->IsBeingPressed(DeviceInput(DEVICE_KEYBOARD, KEY_RALT), &input.InputList) || + INPUTFILTER->IsBeingPressed(DeviceInput(DEVICE_KEYBOARD, KEY_LALT), &input.InputList)) ) + { + // alt-enter + /* In OS X, this is a menu item and will be handled as such. This will + * happen first and then the lower priority GUI thread will happen second, + * causing the window to toggle twice. Another solution would be to put + * a timer in ArchHooks::SetToggleWindowed() and just not set the bool + * it if it's been less than, say, half a second. */ +#if !defined(MACOSX) + ArchHooks::SetToggleWindowed(); +#endif + return true; + } + + return false; +} + +void HandleInputEvents(float fDeltaTime) +{ + INPUTFILTER->Update( fDeltaTime ); + + /* Hack: If the topmost screen hasn't been updated yet, don't process input, + * since we must not send inputs to a screen that hasn't at least had one + * update yet. (The first Update should be the very first thing a screen gets.) + * We'll process it next time. Call Update above, so the inputs are + * read and timestamped. */ + if( SCREENMAN->GetTopScreen()->IsFirstUpdate() ) + return; + + vector ieArray; + INPUTFILTER->GetInputEvents( ieArray ); + + // If we don't have focus, discard input. + if( !HOOKS->AppHasFocus() ) + return; + + for( unsigned i=0; iIsBeingPressed( DeviceInput(DEVICE_KEYBOARD,KEY_LSHIFT) ) ) + input.DeviceI.device = (InputDevice)(input.DeviceI.device + 1); + if( INPUTFILTER->IsBeingPressed( DeviceInput(DEVICE_KEYBOARD,KEY_LCTRL) ) ) + input.DeviceI.device = (InputDevice)(input.DeviceI.device + 2); + if( INPUTFILTER->IsBeingPressed( DeviceInput(DEVICE_KEYBOARD,KEY_LALT) ) ) + input.DeviceI.device = (InputDevice)(input.DeviceI.device + 4); + if( INPUTFILTER->IsBeingPressed( DeviceInput(DEVICE_KEYBOARD,KEY_RALT) ) ) + input.DeviceI.device = (InputDevice)(input.DeviceI.device + 8); + if( INPUTFILTER->IsBeingPressed( DeviceInput(DEVICE_KEYBOARD,KEY_RCTRL) ) ) + input.DeviceI.device = (InputDevice)(input.DeviceI.device + 16); + } + */ + + INPUTMAPPER->DeviceToGame( input.DeviceI, input.GameI ); + + input.mp = MultiPlayer_Invalid; + + { + // Translate input to the appropriate MultiPlayer. Assume that all + // joystick devices are mapped the same as the master player. + if( input.DeviceI.IsJoystick() ) + { + DeviceInput diTemp = input.DeviceI; + diTemp.device = DEVICE_JOY1; + GameInput gi; + + //LOG->Trace( "device %d, %d", diTemp.device, diTemp.button ); + if( INPUTMAPPER->DeviceToGame(diTemp, gi) ) + { + if( GAMESTATE->m_bMultiplayer ) + { + input.GameI = gi; + //LOG->Trace( "game %d %d", input.GameI.controller, input.GameI.button ); + } + + input.mp = InputMapper::InputDeviceToMultiPlayer( input.DeviceI.device ); + //LOG->Trace( "multiplayer %d", input.mp ); + ASSERT( input.mp >= 0 && input.mp < NUM_MultiPlayer ); + } + } + } + + if( input.GameI.IsValid() ) + { + input.MenuI = INPUTMAPPER->GameButtonToMenuButton( input.GameI.button ); + input.pn = INPUTMAPPER->ControllerToPlayerNumber( input.GameI.controller ); + } + + INPUTQUEUE->RememberInput( input ); + + // When a GameButton is pressed, stop repeating other keys on the same controller. + if( input.type == IET_FIRST_PRESS && input.MenuI != GameButton_Invalid ) + { + FOREACH_ENUM( GameButton, m ) + { + if( input.MenuI != m ) + INPUTMAPPER->RepeatStopKey( m, input.pn ); + } + } + + if( HandleGlobalInputs(input) ) + continue; // skip + + // check back in event mode + if( GAMESTATE->IsEventMode() && + CodeDetector::EnteredCode(input.GameI.controller,CODE_BACK_IN_EVENT_MODE) ) + { + input.pn = PLAYER_1; + input.MenuI = GAME_BUTTON_BACK; + } + + SCREENMAN->Input( input ); + } + + if( ArchHooks::GetAndClearToggleWindowed() ) + { + PREFSMAN->m_bWindowed.Set( !PREFSMAN->m_bWindowed ); + StepMania::ApplyGraphicOptions(); + } +} + +/* + * (c) 2001-2004 Chris Danford, Glenn Maynard + * 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. + */ diff --git a/src/Steps.cpp b/src/Steps.cpp index 40c9258665..685477b749 100644 --- a/src/Steps.cpp +++ b/src/Steps.cpp @@ -1,700 +1,700 @@ -/* This stores a single note pattern for a song. - * - * We can have too much data to keep everything decompressed as NoteData, so most - * songs are kept in memory compressed as SMData until requested. NoteData is normally - * not requested casually during gameplay; we can move through screens, the music - * wheel, etc. without touching any NoteData. - * - * To save more memory, if data is cached on disk, read it from disk on demand. Not - * all Steps will have an associated file for this purpose. (Profile edits don't do - * this yet.) - * - * Data can be on disk (always compressed), compressed in memory, and uncompressed in - * memory. */ -#include "global.h" -#include "Steps.h" -#include "StepsUtil.h" -#include "GameState.h" -#include "Song.h" -#include "RageUtil.h" -#include "RageLog.h" -#include "NoteData.h" -#include "GameManager.h" -#include "SongManager.h" -#include "NoteDataUtil.h" -#include "NotesLoaderSSC.h" -#include "NotesLoaderSM.h" -#include "NotesLoaderSMA.h" -#include "NotesLoaderDWI.h" -#include "NotesLoaderKSF.h" -#include "NotesLoaderBMS.h" -#include - -/* register DisplayBPM with StringConversion */ -#include "EnumHelper.h" - -static const char *DisplayBPMNames[] = -{ - "Actual", - "Specified", - "Random", -}; -XToString( DisplayBPM ); -LuaXType( DisplayBPM ); - -Steps::Steps(Song *song): m_StepsType(StepsType_Invalid), m_pSong(song), - parent(NULL), m_pNoteData(new NoteData), m_bNoteDataIsFilled(false), - m_sNoteDataCompressed(""), m_sFilename(""), m_bSavedToDisk(false), - m_LoadedFromProfile(ProfileSlot_Invalid), m_iHash(0), - m_sDescription(""), m_sChartStyle(""), - m_Difficulty(Difficulty_Invalid), m_iMeter(0), - m_bAreCachedRadarValuesJustLoaded(false), - m_sCredit(""), displayBPMType(DISPLAY_BPM_ACTUAL), - specifiedBPMMin(0), specifiedBPMMax(0) {} - -Steps::~Steps() -{ -} - -void Steps::GetDisplayBpms( DisplayBpms &AddTo ) const -{ - if( this->GetDisplayBPM() == DISPLAY_BPM_SPECIFIED ) - { - AddTo.Add( this->GetMinBPM() ); - AddTo.Add( this->GetMaxBPM() ); - } - else - { - float fMinBPM, fMaxBPM; - this->GetTimingData()->GetActualBPM( fMinBPM, fMaxBPM ); - AddTo.Add( fMinBPM ); - AddTo.Add( fMaxBPM ); - } -} - -bool Steps::HasAttacks() const -{ - return !this->m_Attacks.empty(); -} - -unsigned Steps::GetHash() const -{ - if( parent ) - return parent->GetHash(); - if( m_iHash ) - return m_iHash; - if( m_sNoteDataCompressed.empty() ) - { - if( !m_bNoteDataIsFilled ) - return 0; // No data, no hash. - NoteDataUtil::GetSMNoteDataString( *m_pNoteData, m_sNoteDataCompressed ); - } - m_iHash = GetHashForString( m_sNoteDataCompressed ); - return m_iHash; -} - -bool Steps::IsNoteDataEmpty() const -{ - return this->m_sNoteDataCompressed.empty(); -} - -bool Steps::GetNoteDataFromSimfile() -{ - // Replace the line below with the Steps' cache file. - RString stepFile = this->GetFilename(); - RString extension = GetExtension(stepFile); - extension.MakeLower(); // must do this because the code is expecting lowercase - - if (extension.empty() || extension == "ssc") // remember cache files. - { - SSCLoader loader; - if ( ! loader.LoadNoteDataFromSimfile(stepFile, *this) ) - { - /* - HACK: 7/20/12 -- see bugzilla #740 - users who edit songs using the ever popular .sm file - that remove or tamper with the .ssc file later on - complain of blank steps in the editor after reloading. - Despite the blank steps being well justified since - the cache files contain only the SSC step file, - give the user some leeway and search for a .sm replacement - */ - SMLoader backup_loader; - RString transformedStepFile = stepFile; - transformedStepFile.Replace(".ssc", ".sm"); - - return backup_loader.LoadNoteDataFromSimfile(transformedStepFile, *this); - } - else - { - return true; - } - } - else if (extension == "sm") - { - SMLoader loader; - return loader.LoadNoteDataFromSimfile(stepFile, *this); - } - else if (extension == "sma") - { - SMALoader loader; - return loader.LoadNoteDataFromSimfile(stepFile, *this); - } - else if (extension == "dwi") - { - return DWILoader::LoadNoteDataFromSimfile(stepFile, *this); - } - else if (extension == "ksf") - { - return KSFLoader::LoadNoteDataFromSimfile(stepFile, *this); - } - else if (extension == "bms" || extension == "bml" || extension == "bme" || extension == "pms") - { - return BMSLoader::LoadNoteDataFromSimfile(stepFile, *this); - } - return false; -} - -void Steps::SetNoteData( const NoteData& noteDataNew ) -{ - ASSERT( noteDataNew.GetNumTracks() == GAMEMAN->GetStepsTypeInfo(m_StepsType).iNumTracks ); - - DeAutogen( false ); - - *m_pNoteData = noteDataNew; - m_bNoteDataIsFilled = true; - - m_sNoteDataCompressed = RString(); - m_iHash = 0; -} - -void Steps::GetNoteData( NoteData& noteDataOut ) const -{ - Decompress(); - - if( m_bNoteDataIsFilled ) - { - noteDataOut = *m_pNoteData; - } - else - { - noteDataOut.ClearAll(); - noteDataOut.SetNumTracks( GAMEMAN->GetStepsTypeInfo(m_StepsType).iNumTracks ); - } -} - -NoteData Steps::GetNoteData() const -{ - NoteData tmp; - this->GetNoteData( tmp ); - return tmp; -} - -void Steps::SetSMNoteData( const RString ¬es_comp_ ) -{ - m_pNoteData->Init(); - m_bNoteDataIsFilled = false; - - m_sNoteDataCompressed = notes_comp_; - m_iHash = 0; -} - -/* XXX: this function should pull data from m_sFilename, like Decompress() */ -void Steps::GetSMNoteData( RString ¬es_comp_out ) const -{ - if( m_sNoteDataCompressed.empty() ) - { - if( !m_bNoteDataIsFilled ) - { - /* no data is no data */ - notes_comp_out = ""; - return; - } - - NoteDataUtil::GetSMNoteDataString( *m_pNoteData, m_sNoteDataCompressed ); - } - - notes_comp_out = m_sNoteDataCompressed; -} - -float Steps::PredictMeter() const -{ - float pMeter = 0.775f; - - const float RadarCoeffs[NUM_RadarCategory] = - { - 10.1f, 5.27f,-0.905f, -1.10f, 2.86f, - 0,0,0,0,0,0,0,0 - }; - const RadarValues &rv = GetRadarValues( PLAYER_1 ); - for( int r = 0; r < NUM_RadarCategory; ++r ) - pMeter += rv[r] * RadarCoeffs[r]; - - const float DifficultyCoeffs[NUM_Difficulty] = - { - -0.877f, -0.877f, 0, 0.722f, 0.722f, 0 - }; - pMeter += DifficultyCoeffs[this->GetDifficulty()]; - - // Init non-radar values - const float SV = rv[RadarCategory_Stream] * rv[RadarCategory_Voltage]; - const float ChaosSquare = rv[RadarCategory_Chaos] * rv[RadarCategory_Chaos]; - pMeter += -6.35f * SV; - pMeter += -2.58f * ChaosSquare; - if (pMeter < 1) pMeter = 1; - return pMeter; -} - -void Steps::TidyUpData() -{ - if( m_StepsType == StepsType_Invalid ) - m_StepsType = StepsType_dance_single; - - if( GetDifficulty() == Difficulty_Invalid ) - SetDifficulty( StringToDifficulty(GetDescription()) ); - - if( GetDifficulty() == Difficulty_Invalid ) - { - if( GetMeter() == 1 ) SetDifficulty( Difficulty_Beginner ); - else if( GetMeter() <= 3 ) SetDifficulty( Difficulty_Easy ); - else if( GetMeter() <= 6 ) SetDifficulty( Difficulty_Medium ); - else SetDifficulty( Difficulty_Hard ); - } - - if( GetMeter() < 1) // meter is invalid - SetMeter( int(PredictMeter()) ); -} - -void Steps::CalculateRadarValues( float fMusicLengthSeconds ) -{ - // If we're autogen, don't calculate values. GetRadarValues will take from our parent. - if( parent != NULL ) - return; - - if( m_bAreCachedRadarValuesJustLoaded ) - { - m_bAreCachedRadarValuesJustLoaded = false; - return; - } - - // Do write radar values, and leave it up to the reading app whether they want to trust - // the cached values without recalculating them. - /* - // If we're an edit, leave the RadarValues invalid. - if( IsAnEdit() ) - return; - */ - - NoteData tempNoteData; - this->GetNoteData( tempNoteData ); - - FOREACH_PlayerNumber( pn ) - m_CachedRadarValues[pn].Zero(); - - GAMESTATE->SetProcessedTimingData(this->GetTimingData()); - if( tempNoteData.IsComposite() ) - { - vector vParts; - - NoteDataUtil::SplitCompositeNoteData( tempNoteData, vParts ); - for( size_t pn = 0; pn < min(vParts.size(), size_t(NUM_PLAYERS)); ++pn ) - NoteDataUtil::CalculateRadarValues( vParts[pn], fMusicLengthSeconds, m_CachedRadarValues[pn] ); - } - else if (GAMEMAN->GetStepsTypeInfo(this->m_StepsType).m_StepsTypeCategory == StepsTypeCategory_Couple) - { - NoteData p1 = tempNoteData; - // XXX: Assumption that couple will always have an even number of notes. - const int tracks = tempNoteData.GetNumTracks() / 2; - p1.SetNumTracks(tracks); - NoteDataUtil::CalculateRadarValues(p1, - fMusicLengthSeconds, - m_CachedRadarValues[PLAYER_1]); - // at this point, p2 is tempNoteData. - NoteDataUtil::ShiftTracks(tempNoteData, tracks); - tempNoteData.SetNumTracks(tracks); - NoteDataUtil::CalculateRadarValues(tempNoteData, - fMusicLengthSeconds, - m_CachedRadarValues[PLAYER_2]); - } - else - { - NoteDataUtil::CalculateRadarValues( tempNoteData, fMusicLengthSeconds, m_CachedRadarValues[0] ); - fill_n( m_CachedRadarValues + 1, NUM_PLAYERS-1, m_CachedRadarValues[0] ); - } - GAMESTATE->SetProcessedTimingData(NULL); -} - -void Steps::Decompress() const -{ - const_cast(this)->Decompress(); -} - -void Steps::Decompress() -{ - if( m_bNoteDataIsFilled ) - return; // already decompressed - - if( parent ) - { - // get autogen m_pNoteData - NoteData notedata; - parent->GetNoteData( notedata ); - - m_bNoteDataIsFilled = true; - - int iNewTracks = GAMEMAN->GetStepsTypeInfo(m_StepsType).iNumTracks; - - if( this->m_StepsType == StepsType_lights_cabinet ) - { - NoteDataUtil::LoadTransformedLights( notedata, *m_pNoteData, iNewTracks ); - } - else - { - NoteDataUtil::LoadTransformedSlidingWindow( notedata, *m_pNoteData, iNewTracks ); - - NoteDataUtil::RemoveStretch( *m_pNoteData, m_StepsType ); - } - return; - } - - if( !m_sFilename.empty() && m_sNoteDataCompressed.empty() ) - { - // We have NoteData on disk and not in memory. Load it. - if (!this->GetNoteDataFromSimfile()) - { - LOG->Warn("Couldn't load the %s chart's NoteData from \"%s\"", - DifficultyToString(m_Difficulty).c_str(), m_sFilename.c_str()); - return; - } - - this->GetSMNoteData( m_sNoteDataCompressed ); - } - - if( m_sNoteDataCompressed.empty() ) - { - /* there is no data, do nothing */ - } - else - { - // load from compressed - bool bComposite = GAMEMAN->GetStepsTypeInfo(m_StepsType).m_StepsTypeCategory == StepsTypeCategory_Routine; - m_bNoteDataIsFilled = true; - m_pNoteData->SetNumTracks( GAMEMAN->GetStepsTypeInfo(m_StepsType).iNumTracks ); - - NoteDataUtil::LoadFromSMNoteDataString( *m_pNoteData, m_sNoteDataCompressed, bComposite ); - } -} - -void Steps::Compress() const -{ - // Always leave lights data uncompressed. - if( this->m_StepsType == StepsType_lights_cabinet && m_bNoteDataIsFilled ) - { - m_sNoteDataCompressed = RString(); - return; - } - - // Don't compress data in the editor: it's still in use. - if (GAMESTATE->m_bInStepEditor) - { - return; - } - - if( !m_sFilename.empty() && m_LoadedFromProfile == ProfileSlot_Invalid ) - { - /* We have a file on disk; clear all data in memory. - * Data on profiles can't be accessed normally (need to mount and time-out - * the device), and when we start a game and load edits, we want to be - * sure that it'll be available if the user picks it and pulls the device. - * Also, Decompress() doesn't know how to load .edits. */ - m_pNoteData->Init(); - m_bNoteDataIsFilled = false; - - /* Be careful; 'x = ""', m_sNoteDataCompressed.clear() and m_sNoteDataCompressed.reserve(0) - * don't always free the allocated memory. */ - m_sNoteDataCompressed = RString(); - return; - } - - // We have no file on disk. Compress the data, if necessary. - if( m_sNoteDataCompressed.empty() ) - { - if( !m_bNoteDataIsFilled ) - return; /* no data is no data */ - NoteDataUtil::GetSMNoteDataString( *m_pNoteData, m_sNoteDataCompressed ); - } - - m_pNoteData->Init(); - m_bNoteDataIsFilled = false; -} - -/* Copy our parent's data. This is done when we're being changed from autogen - * to normal. (needed?) */ -void Steps::DeAutogen( bool bCopyNoteData ) -{ - if( !parent ) - return; // OK - - if( bCopyNoteData ) - Decompress(); // fills in m_pNoteData with sliding window transform - - m_sDescription = Real()->m_sDescription; - m_sChartStyle = Real()->m_sChartStyle; - m_Difficulty = Real()->m_Difficulty; - m_iMeter = Real()->m_iMeter; - copy( Real()->m_CachedRadarValues, Real()->m_CachedRadarValues + NUM_PLAYERS, m_CachedRadarValues ); - m_sCredit = Real()->m_sCredit; - parent = NULL; - - if( bCopyNoteData ) - Compress(); -} - -void Steps::AutogenFrom( const Steps *parent_, StepsType ntTo ) -{ - parent = parent_; - m_StepsType = ntTo; - m_Timing = parent->m_Timing; -} - -void Steps::CopyFrom( Steps* pSource, StepsType ntTo, float fMusicLengthSeconds ) // pSource does not have to be of the same StepsType -{ - m_StepsType = ntTo; - NoteData noteData; - pSource->GetNoteData( noteData ); - noteData.SetNumTracks( GAMEMAN->GetStepsTypeInfo(ntTo).iNumTracks ); - parent = NULL; - m_Timing = pSource->m_Timing; - this->m_pSong = pSource->m_pSong; - this->m_Attacks = pSource->m_Attacks; - this->m_sAttackString = pSource->m_sAttackString; - this->SetNoteData( noteData ); - this->SetDescription( pSource->GetDescription() ); - this->SetDifficulty( pSource->GetDifficulty() ); - this->SetMeter( pSource->GetMeter() ); - this->CalculateRadarValues( fMusicLengthSeconds ); -} - -void Steps::CreateBlank( StepsType ntTo ) -{ - m_StepsType = ntTo; - NoteData noteData; - noteData.SetNumTracks( GAMEMAN->GetStepsTypeInfo(ntTo).iNumTracks ); - this->SetNoteData( noteData ); -} - -void Steps::SetDifficultyAndDescription( Difficulty dc, RString sDescription ) -{ - DeAutogen(); - m_Difficulty = dc; - m_sDescription = sDescription; - if( GetDifficulty() == Difficulty_Edit ) - MakeValidEditDescription( m_sDescription ); -} - -void Steps::SetCredit( RString sCredit ) -{ - DeAutogen(); - m_sCredit = sCredit; -} - -void Steps::SetChartStyle( RString sChartStyle ) -{ - DeAutogen(); - m_sChartStyle = sChartStyle; -} - -bool Steps::MakeValidEditDescription( RString &sPreferredDescription ) -{ - if( int(sPreferredDescription.size()) > MAX_STEPS_DESCRIPTION_LENGTH ) - { - sPreferredDescription = sPreferredDescription.Left( MAX_STEPS_DESCRIPTION_LENGTH ); - return true; - } - return false; -} - -void Steps::SetMeter( int meter ) -{ - DeAutogen(); - m_iMeter = meter; -} - -const TimingData *Steps::GetTimingData() const -{ - return m_Timing.empty() ? &m_pSong->m_SongTiming : &m_Timing; -} - -bool Steps::HasSignificantTimingChanges() const -{ - const TimingData *timing = GetTimingData(); - if( timing->HasStops() || timing->HasDelays() || timing->HasWarps() || - timing->HasSpeedChanges() || timing->HasScrollChanges() ) - return true; - - if( timing->HasBpmChanges() ) - { - // check to see if these changes are significant. - if( (GetMaxBPM() - GetMinBPM()) > 3.000f ) - return true; - } - - return false; -} - -void Steps::SetCachedRadarValues( const RadarValues v[NUM_PLAYERS] ) -{ - DeAutogen(); - copy( v, v + NUM_PLAYERS, m_CachedRadarValues ); - m_bAreCachedRadarValuesJustLoaded = true; -} - -// lua start -#include "LuaBinding.h" -/** @brief Allow Lua to have access to the Steps. */ -class LunaSteps: public Luna -{ -public: - DEFINE_METHOD( GetStepsType, m_StepsType ) - DEFINE_METHOD( GetDifficulty, GetDifficulty() ) - DEFINE_METHOD( GetDescription, GetDescription() ) - DEFINE_METHOD( GetChartStyle, GetChartStyle() ) - DEFINE_METHOD( GetAuthorCredit, GetCredit() ) - DEFINE_METHOD( GetMeter, GetMeter() ) - DEFINE_METHOD( GetFilename, GetFilename() ) - DEFINE_METHOD( IsAutogen, IsAutogen() ) - DEFINE_METHOD( IsAnEdit, IsAnEdit() ) - DEFINE_METHOD( IsAPlayerEdit, IsAPlayerEdit() ) - - static int HasSignificantTimingChanges( T* p, lua_State *L ) - { - lua_pushboolean(L, p->HasSignificantTimingChanges()); - return 1; - } - static int HasAttacks( T* p, lua_State *L ) - { - lua_pushboolean(L, p->HasAttacks()); - return 1; - } - static int GetRadarValues( T* p, lua_State *L ) - { - PlayerNumber pn = Enum::Check(L, 1); - RadarValues &rv = const_cast(p->GetRadarValues(pn)); - rv.PushSelf(L); - return 1; - } - static int GetTimingData( T* p, lua_State *L ) - { - p->GetTimingData()->PushSelf(L); - return 1; - } - static int GetHash( T* p, lua_State *L ) { lua_pushnumber( L, p->GetHash() ); return 1; } - // untested - /* - static int GetSMNoteData( T* p, lua_State *L ) - { - RString out; - p->GetSMNoteData( out ); - lua_pushstring( L, out ); - return 1; - } - */ - static int GetChartName(T *p, lua_State *L) - { - lua_pushstring(L, p->GetChartName()); - return 1; - } - static int GetDisplayBpms( T* p, lua_State *L ) - { - DisplayBpms temp; - p->GetDisplayBpms(temp); - float fMin = temp.GetMin(); - float fMax = temp.GetMax(); - vector fBPMs; - fBPMs.push_back( fMin ); - fBPMs.push_back( fMax ); - LuaHelpers::CreateTableFromArray(fBPMs, L); - return 1; - } - static int IsDisplayBpmSecret( T* p, lua_State *L ) - { - DisplayBpms temp; - p->GetDisplayBpms(temp); - lua_pushboolean( L, temp.IsSecret() ); - return 1; - } - static int IsDisplayBpmConstant( T* p, lua_State *L ) - { - DisplayBpms temp; - p->GetDisplayBpms(temp); - lua_pushboolean( L, temp.BpmIsConstant() ); - return 1; - } - static int IsDisplayBpmRandom( T* p, lua_State *L ) - { - lua_pushboolean( L, p->GetDisplayBPM() == DISPLAY_BPM_RANDOM ); - return 1; - } - DEFINE_METHOD( PredictMeter, PredictMeter() ) - static int GetDisplayBPMType( T* p, lua_State *L ) - { - LuaHelpers::Push( L, p->GetDisplayBPM() ); - return 1; - } - - LunaSteps() - { - ADD_METHOD( GetAuthorCredit ); - ADD_METHOD( GetChartStyle ); - ADD_METHOD( GetDescription ); - ADD_METHOD( GetDifficulty ); - ADD_METHOD( GetFilename ); - ADD_METHOD( GetHash ); - ADD_METHOD( GetMeter ); - ADD_METHOD( HasSignificantTimingChanges ); - ADD_METHOD( HasAttacks ); - ADD_METHOD( GetRadarValues ); - ADD_METHOD( GetTimingData ); - ADD_METHOD( GetChartName ); - //ADD_METHOD( GetSMNoteData ); - ADD_METHOD( GetStepsType ); - ADD_METHOD( IsAnEdit ); - ADD_METHOD( IsAutogen ); - ADD_METHOD( IsAPlayerEdit ); - ADD_METHOD( GetDisplayBpms ); - ADD_METHOD( IsDisplayBpmSecret ); - ADD_METHOD( IsDisplayBpmConstant ); - ADD_METHOD( IsDisplayBpmRandom ); - ADD_METHOD( PredictMeter ); - ADD_METHOD( GetDisplayBPMType ); - } -}; - -LUA_REGISTER_CLASS( Steps ) -// lua end - - -/* - * (c) 2001-2004 Chris Danford, Glenn Maynard, David Wilson - * 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. - */ +/* This stores a single note pattern for a song. + * + * We can have too much data to keep everything decompressed as NoteData, so most + * songs are kept in memory compressed as SMData until requested. NoteData is normally + * not requested casually during gameplay; we can move through screens, the music + * wheel, etc. without touching any NoteData. + * + * To save more memory, if data is cached on disk, read it from disk on demand. Not + * all Steps will have an associated file for this purpose. (Profile edits don't do + * this yet.) + * + * Data can be on disk (always compressed), compressed in memory, and uncompressed in + * memory. */ +#include "global.h" +#include "Steps.h" +#include "StepsUtil.h" +#include "GameState.h" +#include "Song.h" +#include "RageUtil.h" +#include "RageLog.h" +#include "NoteData.h" +#include "GameManager.h" +#include "SongManager.h" +#include "NoteDataUtil.h" +#include "NotesLoaderSSC.h" +#include "NotesLoaderSM.h" +#include "NotesLoaderSMA.h" +#include "NotesLoaderDWI.h" +#include "NotesLoaderKSF.h" +#include "NotesLoaderBMS.h" +#include + +/* register DisplayBPM with StringConversion */ +#include "EnumHelper.h" + +static const char *DisplayBPMNames[] = +{ + "Actual", + "Specified", + "Random", +}; +XToString( DisplayBPM ); +LuaXType( DisplayBPM ); + +Steps::Steps(Song *song): m_StepsType(StepsType_Invalid), m_pSong(song), + parent(NULL), m_pNoteData(new NoteData), m_bNoteDataIsFilled(false), + m_sNoteDataCompressed(""), m_sFilename(""), m_bSavedToDisk(false), + m_LoadedFromProfile(ProfileSlot_Invalid), m_iHash(0), + m_sDescription(""), m_sChartStyle(""), + m_Difficulty(Difficulty_Invalid), m_iMeter(0), + m_bAreCachedRadarValuesJustLoaded(false), + m_sCredit(""), displayBPMType(DISPLAY_BPM_ACTUAL), + specifiedBPMMin(0), specifiedBPMMax(0) {} + +Steps::~Steps() +{ +} + +void Steps::GetDisplayBpms( DisplayBpms &AddTo ) const +{ + if( this->GetDisplayBPM() == DISPLAY_BPM_SPECIFIED ) + { + AddTo.Add( this->GetMinBPM() ); + AddTo.Add( this->GetMaxBPM() ); + } + else + { + float fMinBPM, fMaxBPM; + this->GetTimingData()->GetActualBPM( fMinBPM, fMaxBPM ); + AddTo.Add( fMinBPM ); + AddTo.Add( fMaxBPM ); + } +} + +bool Steps::HasAttacks() const +{ + return !this->m_Attacks.empty(); +} + +unsigned Steps::GetHash() const +{ + if( parent ) + return parent->GetHash(); + if( m_iHash ) + return m_iHash; + if( m_sNoteDataCompressed.empty() ) + { + if( !m_bNoteDataIsFilled ) + return 0; // No data, no hash. + NoteDataUtil::GetSMNoteDataString( *m_pNoteData, m_sNoteDataCompressed ); + } + m_iHash = GetHashForString( m_sNoteDataCompressed ); + return m_iHash; +} + +bool Steps::IsNoteDataEmpty() const +{ + return this->m_sNoteDataCompressed.empty(); +} + +bool Steps::GetNoteDataFromSimfile() +{ + // Replace the line below with the Steps' cache file. + RString stepFile = this->GetFilename(); + RString extension = GetExtension(stepFile); + extension.MakeLower(); // must do this because the code is expecting lowercase + + if (extension.empty() || extension == "ssc") // remember cache files. + { + SSCLoader loader; + if ( ! loader.LoadNoteDataFromSimfile(stepFile, *this) ) + { + /* + HACK: 7/20/12 -- see bugzilla #740 + users who edit songs using the ever popular .sm file + that remove or tamper with the .ssc file later on + complain of blank steps in the editor after reloading. + Despite the blank steps being well justified since + the cache files contain only the SSC step file, + give the user some leeway and search for a .sm replacement + */ + SMLoader backup_loader; + RString transformedStepFile = stepFile; + transformedStepFile.Replace(".ssc", ".sm"); + + return backup_loader.LoadNoteDataFromSimfile(transformedStepFile, *this); + } + else + { + return true; + } + } + else if (extension == "sm") + { + SMLoader loader; + return loader.LoadNoteDataFromSimfile(stepFile, *this); + } + else if (extension == "sma") + { + SMALoader loader; + return loader.LoadNoteDataFromSimfile(stepFile, *this); + } + else if (extension == "dwi") + { + return DWILoader::LoadNoteDataFromSimfile(stepFile, *this); + } + else if (extension == "ksf") + { + return KSFLoader::LoadNoteDataFromSimfile(stepFile, *this); + } + else if (extension == "bms" || extension == "bml" || extension == "bme" || extension == "pms") + { + return BMSLoader::LoadNoteDataFromSimfile(stepFile, *this); + } + return false; +} + +void Steps::SetNoteData( const NoteData& noteDataNew ) +{ + ASSERT( noteDataNew.GetNumTracks() == GAMEMAN->GetStepsTypeInfo(m_StepsType).iNumTracks ); + + DeAutogen( false ); + + *m_pNoteData = noteDataNew; + m_bNoteDataIsFilled = true; + + m_sNoteDataCompressed = RString(); + m_iHash = 0; +} + +void Steps::GetNoteData( NoteData& noteDataOut ) const +{ + Decompress(); + + if( m_bNoteDataIsFilled ) + { + noteDataOut = *m_pNoteData; + } + else + { + noteDataOut.ClearAll(); + noteDataOut.SetNumTracks( GAMEMAN->GetStepsTypeInfo(m_StepsType).iNumTracks ); + } +} + +NoteData Steps::GetNoteData() const +{ + NoteData tmp; + this->GetNoteData( tmp ); + return tmp; +} + +void Steps::SetSMNoteData( const RString ¬es_comp_ ) +{ + m_pNoteData->Init(); + m_bNoteDataIsFilled = false; + + m_sNoteDataCompressed = notes_comp_; + m_iHash = 0; +} + +/* XXX: this function should pull data from m_sFilename, like Decompress() */ +void Steps::GetSMNoteData( RString ¬es_comp_out ) const +{ + if( m_sNoteDataCompressed.empty() ) + { + if( !m_bNoteDataIsFilled ) + { + /* no data is no data */ + notes_comp_out = ""; + return; + } + + NoteDataUtil::GetSMNoteDataString( *m_pNoteData, m_sNoteDataCompressed ); + } + + notes_comp_out = m_sNoteDataCompressed; +} + +float Steps::PredictMeter() const +{ + float pMeter = 0.775f; + + const float RadarCoeffs[NUM_RadarCategory] = + { + 10.1f, 5.27f,-0.905f, -1.10f, 2.86f, + 0,0,0,0,0,0,0,0 + }; + const RadarValues &rv = GetRadarValues( PLAYER_1 ); + for( int r = 0; r < NUM_RadarCategory; ++r ) + pMeter += rv[r] * RadarCoeffs[r]; + + const float DifficultyCoeffs[NUM_Difficulty] = + { + -0.877f, -0.877f, 0, 0.722f, 0.722f, 0 + }; + pMeter += DifficultyCoeffs[this->GetDifficulty()]; + + // Init non-radar values + const float SV = rv[RadarCategory_Stream] * rv[RadarCategory_Voltage]; + const float ChaosSquare = rv[RadarCategory_Chaos] * rv[RadarCategory_Chaos]; + pMeter += -6.35f * SV; + pMeter += -2.58f * ChaosSquare; + if (pMeter < 1) pMeter = 1; + return pMeter; +} + +void Steps::TidyUpData() +{ + if( m_StepsType == StepsType_Invalid ) + m_StepsType = StepsType_dance_single; + + if( GetDifficulty() == Difficulty_Invalid ) + SetDifficulty( StringToDifficulty(GetDescription()) ); + + if( GetDifficulty() == Difficulty_Invalid ) + { + if( GetMeter() == 1 ) SetDifficulty( Difficulty_Beginner ); + else if( GetMeter() <= 3 ) SetDifficulty( Difficulty_Easy ); + else if( GetMeter() <= 6 ) SetDifficulty( Difficulty_Medium ); + else SetDifficulty( Difficulty_Hard ); + } + + if( GetMeter() < 1) // meter is invalid + SetMeter( int(PredictMeter()) ); +} + +void Steps::CalculateRadarValues( float fMusicLengthSeconds ) +{ + // If we're autogen, don't calculate values. GetRadarValues will take from our parent. + if( parent != nullptr ) + return; + + if( m_bAreCachedRadarValuesJustLoaded ) + { + m_bAreCachedRadarValuesJustLoaded = false; + return; + } + + // Do write radar values, and leave it up to the reading app whether they want to trust + // the cached values without recalculating them. + /* + // If we're an edit, leave the RadarValues invalid. + if( IsAnEdit() ) + return; + */ + + NoteData tempNoteData; + this->GetNoteData( tempNoteData ); + + FOREACH_PlayerNumber( pn ) + m_CachedRadarValues[pn].Zero(); + + GAMESTATE->SetProcessedTimingData(this->GetTimingData()); + if( tempNoteData.IsComposite() ) + { + vector vParts; + + NoteDataUtil::SplitCompositeNoteData( tempNoteData, vParts ); + for( size_t pn = 0; pn < min(vParts.size(), size_t(NUM_PLAYERS)); ++pn ) + NoteDataUtil::CalculateRadarValues( vParts[pn], fMusicLengthSeconds, m_CachedRadarValues[pn] ); + } + else if (GAMEMAN->GetStepsTypeInfo(this->m_StepsType).m_StepsTypeCategory == StepsTypeCategory_Couple) + { + NoteData p1 = tempNoteData; + // XXX: Assumption that couple will always have an even number of notes. + const int tracks = tempNoteData.GetNumTracks() / 2; + p1.SetNumTracks(tracks); + NoteDataUtil::CalculateRadarValues(p1, + fMusicLengthSeconds, + m_CachedRadarValues[PLAYER_1]); + // at this point, p2 is tempNoteData. + NoteDataUtil::ShiftTracks(tempNoteData, tracks); + tempNoteData.SetNumTracks(tracks); + NoteDataUtil::CalculateRadarValues(tempNoteData, + fMusicLengthSeconds, + m_CachedRadarValues[PLAYER_2]); + } + else + { + NoteDataUtil::CalculateRadarValues( tempNoteData, fMusicLengthSeconds, m_CachedRadarValues[0] ); + fill_n( m_CachedRadarValues + 1, NUM_PLAYERS-1, m_CachedRadarValues[0] ); + } + GAMESTATE->SetProcessedTimingData(NULL); +} + +void Steps::Decompress() const +{ + const_cast(this)->Decompress(); +} + +void Steps::Decompress() +{ + if( m_bNoteDataIsFilled ) + return; // already decompressed + + if( parent ) + { + // get autogen m_pNoteData + NoteData notedata; + parent->GetNoteData( notedata ); + + m_bNoteDataIsFilled = true; + + int iNewTracks = GAMEMAN->GetStepsTypeInfo(m_StepsType).iNumTracks; + + if( this->m_StepsType == StepsType_lights_cabinet ) + { + NoteDataUtil::LoadTransformedLights( notedata, *m_pNoteData, iNewTracks ); + } + else + { + NoteDataUtil::LoadTransformedSlidingWindow( notedata, *m_pNoteData, iNewTracks ); + + NoteDataUtil::RemoveStretch( *m_pNoteData, m_StepsType ); + } + return; + } + + if( !m_sFilename.empty() && m_sNoteDataCompressed.empty() ) + { + // We have NoteData on disk and not in memory. Load it. + if (!this->GetNoteDataFromSimfile()) + { + LOG->Warn("Couldn't load the %s chart's NoteData from \"%s\"", + DifficultyToString(m_Difficulty).c_str(), m_sFilename.c_str()); + return; + } + + this->GetSMNoteData( m_sNoteDataCompressed ); + } + + if( m_sNoteDataCompressed.empty() ) + { + /* there is no data, do nothing */ + } + else + { + // load from compressed + bool bComposite = GAMEMAN->GetStepsTypeInfo(m_StepsType).m_StepsTypeCategory == StepsTypeCategory_Routine; + m_bNoteDataIsFilled = true; + m_pNoteData->SetNumTracks( GAMEMAN->GetStepsTypeInfo(m_StepsType).iNumTracks ); + + NoteDataUtil::LoadFromSMNoteDataString( *m_pNoteData, m_sNoteDataCompressed, bComposite ); + } +} + +void Steps::Compress() const +{ + // Always leave lights data uncompressed. + if( this->m_StepsType == StepsType_lights_cabinet && m_bNoteDataIsFilled ) + { + m_sNoteDataCompressed = RString(); + return; + } + + // Don't compress data in the editor: it's still in use. + if (GAMESTATE->m_bInStepEditor) + { + return; + } + + if( !m_sFilename.empty() && m_LoadedFromProfile == ProfileSlot_Invalid ) + { + /* We have a file on disk; clear all data in memory. + * Data on profiles can't be accessed normally (need to mount and time-out + * the device), and when we start a game and load edits, we want to be + * sure that it'll be available if the user picks it and pulls the device. + * Also, Decompress() doesn't know how to load .edits. */ + m_pNoteData->Init(); + m_bNoteDataIsFilled = false; + + /* Be careful; 'x = ""', m_sNoteDataCompressed.clear() and m_sNoteDataCompressed.reserve(0) + * don't always free the allocated memory. */ + m_sNoteDataCompressed = RString(); + return; + } + + // We have no file on disk. Compress the data, if necessary. + if( m_sNoteDataCompressed.empty() ) + { + if( !m_bNoteDataIsFilled ) + return; /* no data is no data */ + NoteDataUtil::GetSMNoteDataString( *m_pNoteData, m_sNoteDataCompressed ); + } + + m_pNoteData->Init(); + m_bNoteDataIsFilled = false; +} + +/* Copy our parent's data. This is done when we're being changed from autogen + * to normal. (needed?) */ +void Steps::DeAutogen( bool bCopyNoteData ) +{ + if( !parent ) + return; // OK + + if( bCopyNoteData ) + Decompress(); // fills in m_pNoteData with sliding window transform + + m_sDescription = Real()->m_sDescription; + m_sChartStyle = Real()->m_sChartStyle; + m_Difficulty = Real()->m_Difficulty; + m_iMeter = Real()->m_iMeter; + copy( Real()->m_CachedRadarValues, Real()->m_CachedRadarValues + NUM_PLAYERS, m_CachedRadarValues ); + m_sCredit = Real()->m_sCredit; + parent = NULL; + + if( bCopyNoteData ) + Compress(); +} + +void Steps::AutogenFrom( const Steps *parent_, StepsType ntTo ) +{ + parent = parent_; + m_StepsType = ntTo; + m_Timing = parent->m_Timing; +} + +void Steps::CopyFrom( Steps* pSource, StepsType ntTo, float fMusicLengthSeconds ) // pSource does not have to be of the same StepsType +{ + m_StepsType = ntTo; + NoteData noteData; + pSource->GetNoteData( noteData ); + noteData.SetNumTracks( GAMEMAN->GetStepsTypeInfo(ntTo).iNumTracks ); + parent = NULL; + m_Timing = pSource->m_Timing; + this->m_pSong = pSource->m_pSong; + this->m_Attacks = pSource->m_Attacks; + this->m_sAttackString = pSource->m_sAttackString; + this->SetNoteData( noteData ); + this->SetDescription( pSource->GetDescription() ); + this->SetDifficulty( pSource->GetDifficulty() ); + this->SetMeter( pSource->GetMeter() ); + this->CalculateRadarValues( fMusicLengthSeconds ); +} + +void Steps::CreateBlank( StepsType ntTo ) +{ + m_StepsType = ntTo; + NoteData noteData; + noteData.SetNumTracks( GAMEMAN->GetStepsTypeInfo(ntTo).iNumTracks ); + this->SetNoteData( noteData ); +} + +void Steps::SetDifficultyAndDescription( Difficulty dc, RString sDescription ) +{ + DeAutogen(); + m_Difficulty = dc; + m_sDescription = sDescription; + if( GetDifficulty() == Difficulty_Edit ) + MakeValidEditDescription( m_sDescription ); +} + +void Steps::SetCredit( RString sCredit ) +{ + DeAutogen(); + m_sCredit = sCredit; +} + +void Steps::SetChartStyle( RString sChartStyle ) +{ + DeAutogen(); + m_sChartStyle = sChartStyle; +} + +bool Steps::MakeValidEditDescription( RString &sPreferredDescription ) +{ + if( int(sPreferredDescription.size()) > MAX_STEPS_DESCRIPTION_LENGTH ) + { + sPreferredDescription = sPreferredDescription.Left( MAX_STEPS_DESCRIPTION_LENGTH ); + return true; + } + return false; +} + +void Steps::SetMeter( int meter ) +{ + DeAutogen(); + m_iMeter = meter; +} + +const TimingData *Steps::GetTimingData() const +{ + return m_Timing.empty() ? &m_pSong->m_SongTiming : &m_Timing; +} + +bool Steps::HasSignificantTimingChanges() const +{ + const TimingData *timing = GetTimingData(); + if( timing->HasStops() || timing->HasDelays() || timing->HasWarps() || + timing->HasSpeedChanges() || timing->HasScrollChanges() ) + return true; + + if( timing->HasBpmChanges() ) + { + // check to see if these changes are significant. + if( (GetMaxBPM() - GetMinBPM()) > 3.000f ) + return true; + } + + return false; +} + +void Steps::SetCachedRadarValues( const RadarValues v[NUM_PLAYERS] ) +{ + DeAutogen(); + copy( v, v + NUM_PLAYERS, m_CachedRadarValues ); + m_bAreCachedRadarValuesJustLoaded = true; +} + +// lua start +#include "LuaBinding.h" +/** @brief Allow Lua to have access to the Steps. */ +class LunaSteps: public Luna +{ +public: + DEFINE_METHOD( GetStepsType, m_StepsType ) + DEFINE_METHOD( GetDifficulty, GetDifficulty() ) + DEFINE_METHOD( GetDescription, GetDescription() ) + DEFINE_METHOD( GetChartStyle, GetChartStyle() ) + DEFINE_METHOD( GetAuthorCredit, GetCredit() ) + DEFINE_METHOD( GetMeter, GetMeter() ) + DEFINE_METHOD( GetFilename, GetFilename() ) + DEFINE_METHOD( IsAutogen, IsAutogen() ) + DEFINE_METHOD( IsAnEdit, IsAnEdit() ) + DEFINE_METHOD( IsAPlayerEdit, IsAPlayerEdit() ) + + static int HasSignificantTimingChanges( T* p, lua_State *L ) + { + lua_pushboolean(L, p->HasSignificantTimingChanges()); + return 1; + } + static int HasAttacks( T* p, lua_State *L ) + { + lua_pushboolean(L, p->HasAttacks()); + return 1; + } + static int GetRadarValues( T* p, lua_State *L ) + { + PlayerNumber pn = Enum::Check(L, 1); + RadarValues &rv = const_cast(p->GetRadarValues(pn)); + rv.PushSelf(L); + return 1; + } + static int GetTimingData( T* p, lua_State *L ) + { + p->GetTimingData()->PushSelf(L); + return 1; + } + static int GetHash( T* p, lua_State *L ) { lua_pushnumber( L, p->GetHash() ); return 1; } + // untested + /* + static int GetSMNoteData( T* p, lua_State *L ) + { + RString out; + p->GetSMNoteData( out ); + lua_pushstring( L, out ); + return 1; + } + */ + static int GetChartName(T *p, lua_State *L) + { + lua_pushstring(L, p->GetChartName()); + return 1; + } + static int GetDisplayBpms( T* p, lua_State *L ) + { + DisplayBpms temp; + p->GetDisplayBpms(temp); + float fMin = temp.GetMin(); + float fMax = temp.GetMax(); + vector fBPMs; + fBPMs.push_back( fMin ); + fBPMs.push_back( fMax ); + LuaHelpers::CreateTableFromArray(fBPMs, L); + return 1; + } + static int IsDisplayBpmSecret( T* p, lua_State *L ) + { + DisplayBpms temp; + p->GetDisplayBpms(temp); + lua_pushboolean( L, temp.IsSecret() ); + return 1; + } + static int IsDisplayBpmConstant( T* p, lua_State *L ) + { + DisplayBpms temp; + p->GetDisplayBpms(temp); + lua_pushboolean( L, temp.BpmIsConstant() ); + return 1; + } + static int IsDisplayBpmRandom( T* p, lua_State *L ) + { + lua_pushboolean( L, p->GetDisplayBPM() == DISPLAY_BPM_RANDOM ); + return 1; + } + DEFINE_METHOD( PredictMeter, PredictMeter() ) + static int GetDisplayBPMType( T* p, lua_State *L ) + { + LuaHelpers::Push( L, p->GetDisplayBPM() ); + return 1; + } + + LunaSteps() + { + ADD_METHOD( GetAuthorCredit ); + ADD_METHOD( GetChartStyle ); + ADD_METHOD( GetDescription ); + ADD_METHOD( GetDifficulty ); + ADD_METHOD( GetFilename ); + ADD_METHOD( GetHash ); + ADD_METHOD( GetMeter ); + ADD_METHOD( HasSignificantTimingChanges ); + ADD_METHOD( HasAttacks ); + ADD_METHOD( GetRadarValues ); + ADD_METHOD( GetTimingData ); + ADD_METHOD( GetChartName ); + //ADD_METHOD( GetSMNoteData ); + ADD_METHOD( GetStepsType ); + ADD_METHOD( IsAnEdit ); + ADD_METHOD( IsAutogen ); + ADD_METHOD( IsAPlayerEdit ); + ADD_METHOD( GetDisplayBpms ); + ADD_METHOD( IsDisplayBpmSecret ); + ADD_METHOD( IsDisplayBpmConstant ); + ADD_METHOD( IsDisplayBpmRandom ); + ADD_METHOD( PredictMeter ); + ADD_METHOD( GetDisplayBPMType ); + } +}; + +LUA_REGISTER_CLASS( Steps ) +// lua end + + +/* + * (c) 2001-2004 Chris Danford, Glenn Maynard, David Wilson + * 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. + */ diff --git a/src/Steps.h b/src/Steps.h index 178a0ab725..232bb9a139 100644 --- a/src/Steps.h +++ b/src/Steps.h @@ -1,279 +1,279 @@ -#ifndef STEPS_H -#define STEPS_H - -#include "Attack.h" -#include "GameConstantsAndTypes.h" -#include "PlayerNumber.h" -#include "Grade.h" -#include "RadarValues.h" -#include "Difficulty.h" -#include "RageUtil_AutoPtr.h" -#include "RageUtil_CachedObject.h" -#include "TimingData.h" - -class Profile; -class NoteData; -struct lua_State; - -/** - * @brief Enforce a limit on the number of chars for the description. - * - * In In The Groove, this limit was 12: we do not need such a limit now. - */ -const int MAX_STEPS_DESCRIPTION_LENGTH = 255; - -/** @brief The different ways of displaying the BPM. */ -enum DisplayBPM -{ - DISPLAY_BPM_ACTUAL, /**< Display the song's actual BPM. */ - DISPLAY_BPM_SPECIFIED, /**< Display a specified value or values. */ - DISPLAY_BPM_RANDOM, /**< Display a random selection of BPMs. */ - NUM_DisplayBPM, - DisplayBPM_Invalid -}; -const RString& DisplayBPMToString( DisplayBPM dbpm ); -LuaDeclareType( DisplayBPM ); - -/** - * @brief Holds note information for a Song. - * - * A Song may have one or more Notes. */ -class Steps -{ -public: - /** @brief Set up the Steps with initial values. */ - Steps( Song* song ); - /** @brief Destroy the Steps that are no longer needed. */ - ~Steps(); - - // initializers - void AutogenFrom( const Steps *parent, StepsType ntTo ); - void CopyFrom( Steps* pSource, StepsType ntTo, float fMusicLengthSeconds ); - void CreateBlank( StepsType ntTo ); - - void Compress() const; - void Decompress() const; - void Decompress(); - /** - * @brief Determine if these steps were created by the autogenerator. - * @return true if they were, false otherwise. - */ - bool IsAutogen() const { return parent != NULL; } - - /** - * @brief Determine if this set of Steps is an edit. - * - * Edits have a special value of difficulty to make it easy to determine. - * @return true if this is an edit, false otherwise. - */ - bool IsAnEdit() const { return m_Difficulty == Difficulty_Edit; } - /** - * @brief Determine if this set of Steps is a player edit. - * - * Player edits also have to be loaded from a player's profile slot, not the machine. - * @return true if this is a player edit, false otherwise. */ - bool IsAPlayerEdit() const { return IsAnEdit() && GetLoadedFromProfileSlot() < ProfileSlot_Machine; } - /** - * @brief Determine if these steps were loaded from a player's profile. - * @return true if they were from a player profile, false otherwise. - */ - bool WasLoadedFromProfile() const { return m_LoadedFromProfile != ProfileSlot_Invalid; } - ProfileSlot GetLoadedFromProfileSlot() const { return m_LoadedFromProfile; } - /** - * @brief Retrieve the description used for this edit. - * @return the description used for this edit. - */ - RString GetDescription() const { return Real()->m_sDescription; } - /** - * @brief Retrieve the ChartStyle used for this chart. - * @return the description used for this chart. - */ - RString GetChartStyle() const { return Real()->m_sChartStyle; } - /** - * @brief Retrieve the difficulty used for this edit. - * @return the difficulty used for this edit. - */ - Difficulty GetDifficulty() const { return Real()->m_Difficulty; } - /** - * @brief Retrieve the meter used for this edit. - * @return the meter used for this edit. - */ - int GetMeter() const { return Real()->m_iMeter; } - const RadarValues& GetRadarValues( PlayerNumber pn ) const { return Real()->m_CachedRadarValues[pn]; } - /** - * @brief Retrieve the author credit used for this edit. - * @return the author credit used for this edit. - */ - RString GetCredit() const { return Real()->m_sCredit; } - - /** @brief The list of attacks. */ - AttackArray m_Attacks; - /** @brief The stringified list of attacks. */ - vector m_sAttackString; - - RString GetChartName() const { return parent ? Real()->GetChartName() : this->chartName; } - void SetChartName(const RString name) { this->chartName = name; } - void SetFilename( RString fn ) { m_sFilename = fn; } - RString GetFilename() const { return m_sFilename; } - void SetSavedToDisk( bool b ) { DeAutogen(); m_bSavedToDisk = b; } - bool GetSavedToDisk() const { return Real()->m_bSavedToDisk; } - void SetDifficulty( Difficulty dc ) { SetDifficultyAndDescription( dc, GetDescription() ); } - void SetDescription( RString sDescription ) { SetDifficultyAndDescription( this->GetDifficulty(), sDescription ); } - void SetDifficultyAndDescription( Difficulty dc, RString sDescription ); - void SetCredit( RString sCredit ); - void SetChartStyle( RString sChartStyle ); - static bool MakeValidEditDescription( RString &sPreferredDescription ); // return true if was modified - - void SetLoadedFromProfile( ProfileSlot slot ) { m_LoadedFromProfile = slot; } - void SetMeter( int meter ); - void SetCachedRadarValues( const RadarValues v[NUM_PLAYERS] ); - float PredictMeter() const; - - unsigned GetHash() const; - void GetNoteData( NoteData& noteDataOut ) const; - NoteData GetNoteData() const; - void SetNoteData( const NoteData& noteDataNew ); - void SetSMNoteData( const RString ¬es_comp ); - void GetSMNoteData( RString ¬es_comp_out ) const; - - /** - * @brief Retrieve the NoteData from the original source. - * @return true if successful, false for failure. */ - bool GetNoteDataFromSimfile(); - - /** - * @brief Determine if we are missing any note data. - * - * This takes advantage of the fact that we usually compress our data. - * @return true if our notedata is empty, false otherwise. */ - bool IsNoteDataEmpty() const; - - void TidyUpData(); - void CalculateRadarValues( float fMusicLengthSeconds ); - - /** - * @brief The TimingData used by the Steps. - * - * This is required to allow Split Timing. */ - TimingData m_Timing; - - /** - * @brief Retrieves the appropriate timing data for the Steps. Falls - * back on the Song if needed. */ - const TimingData *GetTimingData() const; - TimingData *GetTimingData() { return const_cast( static_cast( this )->GetTimingData() ); }; - - /** - * @brief Determine if the Steps have any major timing changes during gameplay. - * @return true if it does, or false otherwise. */ - bool HasSignificantTimingChanges() const; - - /** - * @brief Determine if the Steps have any attacks. - * @return true if it does, or false otherwise. */ - bool HasAttacks() const; - - // Lua - void PushSelf( lua_State *L ); - - StepsType m_StepsType; - /** @brief The Song these Steps are associated with */ - Song *m_pSong; - - CachedObject m_CachedObject; - - void SetDisplayBPM(const DisplayBPM type) { this->displayBPMType = type; } - DisplayBPM GetDisplayBPM() const { return this->displayBPMType; } - void SetMinBPM(const float f) { this->specifiedBPMMin = f; } - float GetMinBPM() const { return this->specifiedBPMMin; } - void SetMaxBPM(const float f) { this->specifiedBPMMax = f; } - float GetMaxBPM() const { return this->specifiedBPMMax; } - void GetDisplayBpms( DisplayBpms &addTo) const; - - RString GetAttackString() const - { - return join(":", this->m_sAttackString); - } - -private: - inline const Steps *Real() const { return parent ? parent : this; } - void DeAutogen( bool bCopyNoteData = true ); /* If this Steps is autogenerated, make it a real Steps. */ - - /** - * @brief Identify this Steps' parent. - * - * If this Steps is autogenerated, this will point to the autogen - * source. If this is true, m_sNoteDataCompressed will always be empty. */ - const Steps *parent; - - /* We can have one or both of these; if we have both, they're always identical. - * Call Compress() to force us to only have m_sNoteDataCompressed; otherwise, creation of - * these is transparent. */ - mutable HiddenPtr m_pNoteData; - mutable bool m_bNoteDataIsFilled; - mutable RString m_sNoteDataCompressed; - - /** @brief The name of the file where these steps are stored. */ - RString m_sFilename; - /** @brief true if these Steps were loaded from or saved to disk. */ - bool m_bSavedToDisk; - /** @brief What profile was used? This is ProfileSlot_Invalid if not from a profile. */ - ProfileSlot m_LoadedFromProfile; - - /* These values are pulled from the autogen source first, if there is one. */ - /** @brief The hash of the steps. This is used only for Edit Steps. */ - mutable unsigned m_iHash; - /** @brief The name of the edit, or some other useful description. - This used to also contain the step author's name. */ - RString m_sDescription; - /** @brief The style of the chart. (e.g. "Pad", "Keyboard") */ - RString m_sChartStyle; - /** @brief The difficulty that these steps are assigned to. */ - Difficulty m_Difficulty; - /** @brief The numeric difficulty of the Steps, ranging from MIN_METER to MAX_METER. */ - int m_iMeter; - /** @brief The radar values used for each player. */ - RadarValues m_CachedRadarValues[NUM_PLAYERS]; - bool m_bAreCachedRadarValuesJustLoaded; - /** @brief The name of the person who created the Steps. */ - RString m_sCredit; - /** @brief The name of the chart. */ - RString chartName; - /** @brief How is the BPM displayed for this chart? */ - DisplayBPM displayBPMType; - /** @brief What is the minimum specified BPM? */ - float specifiedBPMMin; - /** - * @brief What is the maximum specified BPM? - * If this is a range, then min should not be equal to max. */ - float specifiedBPMMax; -}; - -#endif - -/** - * @file - * @author Chris Danford, Glenn Maynard (c) 2001-2004 - * @section LICENSE - * 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. - */ +#ifndef STEPS_H +#define STEPS_H + +#include "Attack.h" +#include "GameConstantsAndTypes.h" +#include "PlayerNumber.h" +#include "Grade.h" +#include "RadarValues.h" +#include "Difficulty.h" +#include "RageUtil_AutoPtr.h" +#include "RageUtil_CachedObject.h" +#include "TimingData.h" + +class Profile; +class NoteData; +struct lua_State; + +/** + * @brief Enforce a limit on the number of chars for the description. + * + * In In The Groove, this limit was 12: we do not need such a limit now. + */ +const int MAX_STEPS_DESCRIPTION_LENGTH = 255; + +/** @brief The different ways of displaying the BPM. */ +enum DisplayBPM +{ + DISPLAY_BPM_ACTUAL, /**< Display the song's actual BPM. */ + DISPLAY_BPM_SPECIFIED, /**< Display a specified value or values. */ + DISPLAY_BPM_RANDOM, /**< Display a random selection of BPMs. */ + NUM_DisplayBPM, + DisplayBPM_Invalid +}; +const RString& DisplayBPMToString( DisplayBPM dbpm ); +LuaDeclareType( DisplayBPM ); + +/** + * @brief Holds note information for a Song. + * + * A Song may have one or more Notes. */ +class Steps +{ +public: + /** @brief Set up the Steps with initial values. */ + Steps( Song* song ); + /** @brief Destroy the Steps that are no longer needed. */ + ~Steps(); + + // initializers + void AutogenFrom( const Steps *parent, StepsType ntTo ); + void CopyFrom( Steps* pSource, StepsType ntTo, float fMusicLengthSeconds ); + void CreateBlank( StepsType ntTo ); + + void Compress() const; + void Decompress() const; + void Decompress(); + /** + * @brief Determine if these steps were created by the autogenerator. + * @return true if they were, false otherwise. + */ + bool IsAutogen() const { return parent != nullptr; } + + /** + * @brief Determine if this set of Steps is an edit. + * + * Edits have a special value of difficulty to make it easy to determine. + * @return true if this is an edit, false otherwise. + */ + bool IsAnEdit() const { return m_Difficulty == Difficulty_Edit; } + /** + * @brief Determine if this set of Steps is a player edit. + * + * Player edits also have to be loaded from a player's profile slot, not the machine. + * @return true if this is a player edit, false otherwise. */ + bool IsAPlayerEdit() const { return IsAnEdit() && GetLoadedFromProfileSlot() < ProfileSlot_Machine; } + /** + * @brief Determine if these steps were loaded from a player's profile. + * @return true if they were from a player profile, false otherwise. + */ + bool WasLoadedFromProfile() const { return m_LoadedFromProfile != ProfileSlot_Invalid; } + ProfileSlot GetLoadedFromProfileSlot() const { return m_LoadedFromProfile; } + /** + * @brief Retrieve the description used for this edit. + * @return the description used for this edit. + */ + RString GetDescription() const { return Real()->m_sDescription; } + /** + * @brief Retrieve the ChartStyle used for this chart. + * @return the description used for this chart. + */ + RString GetChartStyle() const { return Real()->m_sChartStyle; } + /** + * @brief Retrieve the difficulty used for this edit. + * @return the difficulty used for this edit. + */ + Difficulty GetDifficulty() const { return Real()->m_Difficulty; } + /** + * @brief Retrieve the meter used for this edit. + * @return the meter used for this edit. + */ + int GetMeter() const { return Real()->m_iMeter; } + const RadarValues& GetRadarValues( PlayerNumber pn ) const { return Real()->m_CachedRadarValues[pn]; } + /** + * @brief Retrieve the author credit used for this edit. + * @return the author credit used for this edit. + */ + RString GetCredit() const { return Real()->m_sCredit; } + + /** @brief The list of attacks. */ + AttackArray m_Attacks; + /** @brief The stringified list of attacks. */ + vector m_sAttackString; + + RString GetChartName() const { return parent ? Real()->GetChartName() : this->chartName; } + void SetChartName(const RString name) { this->chartName = name; } + void SetFilename( RString fn ) { m_sFilename = fn; } + RString GetFilename() const { return m_sFilename; } + void SetSavedToDisk( bool b ) { DeAutogen(); m_bSavedToDisk = b; } + bool GetSavedToDisk() const { return Real()->m_bSavedToDisk; } + void SetDifficulty( Difficulty dc ) { SetDifficultyAndDescription( dc, GetDescription() ); } + void SetDescription( RString sDescription ) { SetDifficultyAndDescription( this->GetDifficulty(), sDescription ); } + void SetDifficultyAndDescription( Difficulty dc, RString sDescription ); + void SetCredit( RString sCredit ); + void SetChartStyle( RString sChartStyle ); + static bool MakeValidEditDescription( RString &sPreferredDescription ); // return true if was modified + + void SetLoadedFromProfile( ProfileSlot slot ) { m_LoadedFromProfile = slot; } + void SetMeter( int meter ); + void SetCachedRadarValues( const RadarValues v[NUM_PLAYERS] ); + float PredictMeter() const; + + unsigned GetHash() const; + void GetNoteData( NoteData& noteDataOut ) const; + NoteData GetNoteData() const; + void SetNoteData( const NoteData& noteDataNew ); + void SetSMNoteData( const RString ¬es_comp ); + void GetSMNoteData( RString ¬es_comp_out ) const; + + /** + * @brief Retrieve the NoteData from the original source. + * @return true if successful, false for failure. */ + bool GetNoteDataFromSimfile(); + + /** + * @brief Determine if we are missing any note data. + * + * This takes advantage of the fact that we usually compress our data. + * @return true if our notedata is empty, false otherwise. */ + bool IsNoteDataEmpty() const; + + void TidyUpData(); + void CalculateRadarValues( float fMusicLengthSeconds ); + + /** + * @brief The TimingData used by the Steps. + * + * This is required to allow Split Timing. */ + TimingData m_Timing; + + /** + * @brief Retrieves the appropriate timing data for the Steps. Falls + * back on the Song if needed. */ + const TimingData *GetTimingData() const; + TimingData *GetTimingData() { return const_cast( static_cast( this )->GetTimingData() ); }; + + /** + * @brief Determine if the Steps have any major timing changes during gameplay. + * @return true if it does, or false otherwise. */ + bool HasSignificantTimingChanges() const; + + /** + * @brief Determine if the Steps have any attacks. + * @return true if it does, or false otherwise. */ + bool HasAttacks() const; + + // Lua + void PushSelf( lua_State *L ); + + StepsType m_StepsType; + /** @brief The Song these Steps are associated with */ + Song *m_pSong; + + CachedObject m_CachedObject; + + void SetDisplayBPM(const DisplayBPM type) { this->displayBPMType = type; } + DisplayBPM GetDisplayBPM() const { return this->displayBPMType; } + void SetMinBPM(const float f) { this->specifiedBPMMin = f; } + float GetMinBPM() const { return this->specifiedBPMMin; } + void SetMaxBPM(const float f) { this->specifiedBPMMax = f; } + float GetMaxBPM() const { return this->specifiedBPMMax; } + void GetDisplayBpms( DisplayBpms &addTo) const; + + RString GetAttackString() const + { + return join(":", this->m_sAttackString); + } + +private: + inline const Steps *Real() const { return parent ? parent : this; } + void DeAutogen( bool bCopyNoteData = true ); /* If this Steps is autogenerated, make it a real Steps. */ + + /** + * @brief Identify this Steps' parent. + * + * If this Steps is autogenerated, this will point to the autogen + * source. If this is true, m_sNoteDataCompressed will always be empty. */ + const Steps *parent; + + /* We can have one or both of these; if we have both, they're always identical. + * Call Compress() to force us to only have m_sNoteDataCompressed; otherwise, creation of + * these is transparent. */ + mutable HiddenPtr m_pNoteData; + mutable bool m_bNoteDataIsFilled; + mutable RString m_sNoteDataCompressed; + + /** @brief The name of the file where these steps are stored. */ + RString m_sFilename; + /** @brief true if these Steps were loaded from or saved to disk. */ + bool m_bSavedToDisk; + /** @brief What profile was used? This is ProfileSlot_Invalid if not from a profile. */ + ProfileSlot m_LoadedFromProfile; + + /* These values are pulled from the autogen source first, if there is one. */ + /** @brief The hash of the steps. This is used only for Edit Steps. */ + mutable unsigned m_iHash; + /** @brief The name of the edit, or some other useful description. + This used to also contain the step author's name. */ + RString m_sDescription; + /** @brief The style of the chart. (e.g. "Pad", "Keyboard") */ + RString m_sChartStyle; + /** @brief The difficulty that these steps are assigned to. */ + Difficulty m_Difficulty; + /** @brief The numeric difficulty of the Steps, ranging from MIN_METER to MAX_METER. */ + int m_iMeter; + /** @brief The radar values used for each player. */ + RadarValues m_CachedRadarValues[NUM_PLAYERS]; + bool m_bAreCachedRadarValuesJustLoaded; + /** @brief The name of the person who created the Steps. */ + RString m_sCredit; + /** @brief The name of the chart. */ + RString chartName; + /** @brief How is the BPM displayed for this chart? */ + DisplayBPM displayBPMType; + /** @brief What is the minimum specified BPM? */ + float specifiedBPMMin; + /** + * @brief What is the maximum specified BPM? + * If this is a range, then min should not be equal to max. */ + float specifiedBPMMax; +}; + +#endif + +/** + * @file + * @author Chris Danford, Glenn Maynard (c) 2001-2004 + * @section LICENSE + * 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. + */ diff --git a/src/StepsUtil.cpp b/src/StepsUtil.cpp index f9c4ee8676..7414d8c2f2 100644 --- a/src/StepsUtil.cpp +++ b/src/StepsUtil.cpp @@ -123,7 +123,7 @@ void StepsUtil::SortStepsPointerArrayByNumPlays( vector &vStepsPointers, } } - ASSERT( pProfile != NULL ); + ASSERT( pProfile != nullptr ); for(unsigned i = 0; i < vStepsPointers.size(); ++i) { Steps* pSteps = vStepsPointers[i]; diff --git a/src/Style.cpp b/src/Style.cpp index 74e5ca5add..b0383379c1 100644 --- a/src/Style.cpp +++ b/src/Style.cpp @@ -1,165 +1,165 @@ -#include "global.h" - -/* - * Styles define a set of columns for each player, and information about those - * columns, like what Instruments are used play those columns and what track - * to use to populate the column's notes. - * A "track" is the term used to descibe a particular vertical sting of note - * in NoteData. - * A "column" is the term used to describe the vertical string of notes that - * a player sees on the screen while they're playing. Column notes are - * picked from a track, but columns and tracks don't have a 1-to-1 - * correspondance. For example, dance-versus has 8 columns but only 4 tracks - * because two players place from the same set of 4 tracks. - */ - -#include "Style.h" -#include "GameState.h" -#include "RageLog.h" -#include "RageUtil.h" -#include "InputMapper.h" -#include "NoteData.h" -#include - -bool Style::GetUsesCenteredArrows() const -{ - switch( m_StyleType ) - { - case StyleType_OnePlayerTwoSides: - case StyleType_TwoPlayersSharedSides: - return true; - default: - return false; - } -} - -void Style::GetTransformedNoteDataForStyle( PlayerNumber pn, const NoteData& original, NoteData& noteDataOut ) const -{ - ASSERT( pn >=0 && pn <= NUM_PLAYERS ); - - int iNewToOriginalTrack[MAX_COLS_PER_PLAYER]; - for( int col=0; colGetInputScheme()->m_iButtonsPerController; - for( GameButton gb=GAME_BUTTON_NEXT; gb < iButtonsPerController; gb=(GameButton)(gb+1) ) - { - int iThisInputCol = m_iInputColumn[gc][gb-GAME_BUTTON_NEXT]; - if( iThisInputCol == END_MAPPING ) - break; - - if( iThisInputCol == iCol ) - return GameInput( gc, gb ); - } - } - - FAIL_M( ssprintf("Invalid column number %i for player %i in the style %s", iCol, pn, GAMESTATE->GetCurrentStyle()->m_szName) ); -}; - -int Style::GameInputToColumn( const GameInput &GameI ) const -{ - if( GameI.button < GAME_BUTTON_NEXT ) - return Column_Invalid; - int iColumnIndex = GameI.button - GAME_BUTTON_NEXT; - if( m_iInputColumn[GameI.controller][iColumnIndex] == NO_MAPPING ) - return Column_Invalid; - - for( int i = 0; i <= iColumnIndex; ++i ) - if( m_iInputColumn[GameI.controller][i] == END_MAPPING ) - return Column_Invalid; - - return m_iInputColumn[GameI.controller][iColumnIndex]; -} - - -void Style::GetMinAndMaxColX( PlayerNumber pn, float& fMixXOut, float& fMaxXOut ) const -{ - ASSERT( pn != PLAYER_INVALID ); - - fMixXOut = FLT_MAX; - fMaxXOut = FLT_MIN; - for( int i=0; iGetInputScheme()->GetGameButtonName(GI.button); -} - -// Lua bindings -#include "LuaBinding.h" - -/** @brief Allow Lua to have access to the Style. */ -class LunaStyle: public Luna