#include "global.h" #include "DifficultyList.h" #include "GameState.h" #include "Song.h" #include "Steps.h" #include "Style.h" #include "StepsDisplay.h" #include "StepsUtil.h" #include "CommonMetrics.h" #include "Foreach.h" #include "SongUtil.h" #include "XmlFile.h" /** @brief Specifies the max number of charts available for a song. * * This includes autogenned charts. */ #define MAX_METERS (NUM_Difficulty * NUM_StepsType) + MAX_EDITS_PER_SONG REGISTER_ACTOR_CLASS( StepsDisplayList ); StepsDisplayList::StepsDisplayList() { m_bShown = true; FOREACH_ENUM( PlayerNumber, pn ) { SubscribeToMessage( (MessageID)(Message_CurrentStepsP1Changed+pn) ); SubscribeToMessage( (MessageID)(Message_CurrentTrailP1Changed+pn) ); } } StepsDisplayList::~StepsDisplayList() { } void StepsDisplayList::LoadFromNode( const XNode* pNode ) { ActorFrame::LoadFromNode( pNode ); ASSERT_M( !m_sName.empty(), "StepsDisplayList must have a Name" ); ITEMS_SPACING_Y.Load( m_sName, "ItemsSpacingY" ); NUM_SHOWN_ITEMS.Load( m_sName, "NumShownItems" ); CAPITALIZE_DIFFICULTY_NAMES.Load( m_sName, "CapitalizeDifficultyNames" ); MOVE_COMMAND.Load( m_sName, "MoveCommand" ); m_Lines.resize( MAX_METERS ); m_CurSong = NULL; FOREACH_ENUM( PlayerNumber, pn ) { const XNode *pChild = pNode->GetChild( ssprintf("CursorP%i",pn+1) ); if( pChild == NULL ) RageException::Throw( "%s: StepsDisplayList: missing the node \"CursorP%d\"", ActorUtil::GetWhere(pNode).c_str(), pn+1 ); m_Cursors[pn].LoadActorFromNode( pChild, this ); /* Hack: we need to tween cursors both up to down (cursor motion) and visible to * invisible (fading). Cursor motion needs to stoptweening, so multiple motions * don't queue and look unresponsive. However, that stoptweening interrupts fading, * resulting in the cursor remaining invisible or partially invisible. So, do them * in separate tweening stacks. This means the Cursor command can't change diffuse * colors; I think we do need a diffuse color stack ... */ pChild = pNode->GetChild( ssprintf("CursorP%iFrame",pn+1) ); if( pChild == NULL ) RageException::Throw( "%s: StepsDisplayList: missing the node \"CursorP%dFrame\"", ActorUtil::GetWhere(pNode).c_str(), pn+1 ); m_CursorFrames[pn].LoadFromNode( pChild ); m_CursorFrames[pn].AddChild( m_Cursors[pn] ); this->AddChild( &m_CursorFrames[pn] ); } for( unsigned m = 0; m < m_Lines.size(); ++m ) { // todo: Use Row1, Row2 for names? also m_sName+"Row" -aj m_Lines[m].m_Meter.SetName( "Row" ); m_Lines[m].m_Meter.Load( "StepsDisplayListRow", NULL ); this->AddChild( &m_Lines[m].m_Meter ); } UpdatePositions(); PositionItems(); } int StepsDisplayList::GetCurrentRowIndex( PlayerNumber pn ) const { Difficulty ClosestDifficulty = GAMESTATE->GetClosestShownDifficulty(pn); for( unsigned i=0; im_pCurSteps[pn] == NULL ) { if( row.m_dc == ClosestDifficulty ) return i; } else { if( GAMESTATE->m_pCurSteps[pn].Get() == row.m_Steps ) return i; } } return 0; } // Update m_fY and m_bHidden[]. void StepsDisplayList::UpdatePositions() { int iCurrentRow[NUM_PLAYERS]; FOREACH_HumanPlayer( p ) iCurrentRow[p] = GetCurrentRowIndex( p ); const int total = NUM_SHOWN_ITEMS; const int halfsize = total / 2; int first_start, first_end, second_start, second_end; // Choices for each player. If only one player is active, it's the same for both. int P1Choice = GAMESTATE->IsHumanPlayer(PLAYER_1)? iCurrentRow[PLAYER_1]: GAMESTATE->IsHumanPlayer(PLAYER_2)? iCurrentRow[PLAYER_2]: 0; int P2Choice = GAMESTATE->IsHumanPlayer(PLAYER_2)? iCurrentRow[PLAYER_2]: GAMESTATE->IsHumanPlayer(PLAYER_1)? iCurrentRow[PLAYER_1]: 0; vector &Rows = m_Rows; const bool BothPlayersActivated = GAMESTATE->IsHumanPlayer(PLAYER_1) && GAMESTATE->IsHumanPlayer(PLAYER_2); if( !BothPlayersActivated ) { // Simply center the cursor. first_start = max( P1Choice - halfsize, 0 ); first_end = first_start + total; second_start = second_end = first_end; } else { // First half: const int earliest = min( P1Choice, P2Choice ); first_start = max( earliest - halfsize/2, 0 ); first_end = first_start + halfsize; // Second half: const int latest = max( P1Choice, P2Choice ); second_start = max( latest - halfsize/2, 0 ); // Don't overlap. second_start = max( second_start, first_end ); second_end = second_start + halfsize; } first_end = min( first_end, (int) Rows.size() ); second_end = min( second_end, (int) Rows.size() ); /* If less than total (and Rows.size()) are displayed, fill in the empty * space intelligently. */ while(1) { const int sum = (first_end - first_start) + (second_end - second_start); if( sum >= (int) Rows.size() || sum >= total) break; // nothing more to display, or no room /* First priority: expand the top of the second half until it meets * the first half. */ if( second_start > first_end ) second_start--; // Otherwise, expand either end. else if( first_start > 0 ) first_start--; else if( second_end < (int) Rows.size() ) second_end++; else FAIL_M("Do we have room to grow, or don't we?"); } int pos = 0; for( int i=0; i<(int) Rows.size(); i++ ) // foreach row { float ItemPosition; if( i < first_start ) ItemPosition = -0.5f; else if( i < first_end ) ItemPosition = (float) pos++; else if( i < second_start ) ItemPosition = halfsize - 0.5f; else if( i < second_end ) ItemPosition = (float) pos++; else ItemPosition = (float) total - 0.5f; Row &row = Rows[i]; float fY = ITEMS_SPACING_Y*ItemPosition; row.m_fY = fY; row.m_bHidden = i < first_start || (i >= first_end && i < second_start) || i >= second_end; } } void StepsDisplayList::PositionItems() { for( int i = 0; i < MAX_METERS; ++i ) { bool bUnused = ( i >= (int)m_Rows.size() ); m_Lines[i].m_Meter.SetVisible( !bUnused ); } for( int m = 0; m < (int)m_Rows.size(); ++m ) { Row &row = m_Rows[m]; bool bHidden = row.m_bHidden; if( !m_bShown ) bHidden = true; const float fDiffuseAlpha = bHidden? 0.0f:1.0f; if( m_Lines[m].m_Meter.GetDestY() != row.m_fY || m_Lines[m].m_Meter.DestTweenState().diffuse[0][3] != fDiffuseAlpha ) { m_Lines[m].m_Meter.RunCommands( MOVE_COMMAND.GetValue() ); m_Lines[m].m_Meter.RunCommandsOnChildren( MOVE_COMMAND.GetValue() ); } m_Lines[m].m_Meter.SetY( row.m_fY ); } for( int m=0; m < MAX_METERS; ++m ) { bool bHidden = true; if( m_bShown && m < (int)m_Rows.size() ) bHidden = m_Rows[m].m_bHidden; float fDiffuseAlpha = bHidden?0.0f:1.0f; m_Lines[m].m_Meter.SetDiffuseAlpha( fDiffuseAlpha ); } FOREACH_HumanPlayer( pn ) { int iCurrentRow = GetCurrentRowIndex( pn ); float fY = 0; if( iCurrentRow < (int) m_Rows.size() ) fY = m_Rows[iCurrentRow].m_fY; m_CursorFrames[pn].PlayCommand( "Change" ); m_CursorFrames[pn].SetY( fY ); } } void StepsDisplayList::SetFromGameState() { const Song *pSong = GAMESTATE->m_pCurSong; unsigned i = 0; if( pSong == NULL ) { // FIXME: This clamps to between the min and the max difficulty, but // it really should round to the nearest difficulty that's in // DIFFICULTIES_TO_SHOW. const vector& difficulties = CommonMetrics::DIFFICULTIES_TO_SHOW.GetValue(); m_Rows.resize( difficulties.size() ); FOREACH_CONST( Difficulty, difficulties, d ) { m_Rows[i].m_dc = *d; m_Lines[i].m_Meter.SetFromStepsTypeAndMeterAndDifficultyAndCourseType( GAMESTATE->m_pCurStyle->m_StepsType, 0, *d, CourseType_Invalid ); ++i; } } else { vector vpSteps; SongUtil::GetPlayableSteps( pSong, vpSteps ); // Should match the sort in ScreenSelectMusic::AfterMusicChange. m_Rows.resize( vpSteps.size() ); FOREACH_CONST( Steps*, vpSteps, s ) { //LOG->Trace(ssprintf("setting steps for row %i",i)); m_Rows[i].m_Steps = *s; m_Lines[i].m_Meter.SetFromSteps( *s ); ++i; } } while( i < MAX_METERS ) m_Lines[i++].m_Meter.Unset(); UpdatePositions(); PositionItems(); for( int m = 0; m < MAX_METERS; ++m ) m_Lines[m].m_Meter.FinishTweening(); } void StepsDisplayList::HideRows() { for( unsigned m = 0; m < m_Rows.size(); ++m ) { Line &l = m_Lines[m]; l.m_Meter.FinishTweening(); l.m_Meter.SetDiffuseAlpha(0); } } void StepsDisplayList::TweenOnScreen() { FOREACH_HumanPlayer( pn ) ON_COMMAND( m_Cursors[pn] ); for( int m = 0; m < MAX_METERS; ++m ) ON_COMMAND( m_Lines[m].m_Meter ); this->SetHibernate( 0.5f ); m_bShown = true; for( unsigned m = 0; m < m_Rows.size(); ++m ) { Line &l = m_Lines[m]; l.m_Meter.FinishTweening(); } HideRows(); PositionItems(); FOREACH_HumanPlayer( pn ) COMMAND( m_Cursors[pn], "TweenOn" ); } void StepsDisplayList::TweenOffScreen() { } void StepsDisplayList::Show() { m_bShown = true; SetFromGameState(); HideRows(); PositionItems(); FOREACH_HumanPlayer( pn ) COMMAND( m_Cursors[pn], "Show" ); } void StepsDisplayList::Hide() { m_bShown = false; PositionItems(); FOREACH_HumanPlayer( pn ) COMMAND( m_Cursors[pn], "Hide" ); } void StepsDisplayList::HandleMessage( const Message &msg ) { FOREACH_ENUM( PlayerNumber, pn ) { if( msg.GetName() == MessageIDToString((MessageID)(Message_CurrentStepsP1Changed+pn)) || msg.GetName() == MessageIDToString((MessageID)(Message_CurrentTrailP1Changed+pn)) ) SetFromGameState(); } ActorFrame::HandleMessage(msg); } // lua start #include "LuaBinding.h" /** @brief Allow Lua to have access to the StepsDisplayList. */ class LunaStepsDisplayList: public Luna { public: static int setfromgamestate( T* p, lua_State *L ) { p->SetFromGameState(); return 0; } LunaStepsDisplayList() { ADD_METHOD( setfromgamestate ); } }; LUA_REGISTER_DERIVED_CLASS( StepsDisplayList, ActorFrame ) // lua end /* * (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. */