#include "global.h" /* ----------------------------------------------------------------------------- Class: NoteDataUtil Desc: See header. Copyright (c) 2001-2002 by the person(s) listed below. All rights reserved. Chris Danford ----------------------------------------------------------------------------- */ #include "NoteDataUtil.h" #include "RageUtil.h" #include "RageLog.h" #include "PlayerOptions.h" #include "song.h" #include "GameState.h" NoteType NoteDataUtil::GetSmallestNoteTypeForMeasure( const NoteData &n, int iMeasureIndex ) { const int iMeasureStartIndex = iMeasureIndex * ROWS_PER_MEASURE; const int iMeasureLastIndex = (iMeasureIndex+1) * ROWS_PER_MEASURE - 1; // probe to find the smallest note type NoteType nt; for( nt=(NoteType)0; nt= 'a' && sMeasureLine[c] <= 'z' ) { t = sMeasureLine[c]; } else { /* Invalid data. We don't want to assert, since there might * simply be invalid data in an .SM, and we don't want to die * due to invalid data. We should probably check for this when * we load SM data for the first time ... */ // ASSERT(0); t = TAP_EMPTY; } break; } out.SetTapNote(c, iIndex, t); } } } out.Convert2sAnd3sToHoldNotes(); } { // // Load attack data // CStringArray asLines; split( sSMAttackData, ",", asLines, true ); for( unsigned i=0; i::const_iterator iter = in_.GetAttackMap().begin(); iter != in_.GetAttackMap().end(); iter++ ) { TapNote tn = iter->first; Attack attack = iter->second; attack.sModifier.Replace( ',', '.' ); // comma is the map item separator asLines.push_back( ssprintf("%c=%s=%f,\n", tn, attack.sModifier.c_str(), attack.fSecsRemaining) ); } attacks_out = join( ",", asLines ); } } float NoteDataUtil::GetRadarValue( const NoteData &in, RadarCategory rv, float fSongSeconds ) { switch( rv ) { case RADAR_STREAM: return GetStreamRadarValue( in, fSongSeconds ); case RADAR_VOLTAGE: return GetVoltageRadarValue( in, fSongSeconds ); case RADAR_AIR: return GetAirRadarValue( in, fSongSeconds ); case RADAR_FREEZE: return GetFreezeRadarValue( in, fSongSeconds ); case RADAR_CHAOS: return GetChaosRadarValue( in, fSongSeconds ); case RADAR_NUM_TAPS_AND_HOLDS: return (float) in.GetNumRowsWithTapOrHoldHead(); case RADAR_NUM_JUMPS: return (float) in.GetNumDoubles(); // should hands also count as a jump? case RADAR_NUM_HOLDS: return (float) in.GetNumHoldNotes(); case RADAR_NUM_MINES: return (float) in.GetNumMines(); case RADAR_NUM_HANDS: return (float) in.GetNumHands(); default: ASSERT(0); return 0; } } float NoteDataUtil::GetStreamRadarValue( const NoteData &in, float fSongSeconds ) { if( !fSongSeconds ) return 0.0f; // density of steps int iNumNotes = in.GetNumTapNotes() + in.GetNumHoldNotes(); float fNotesPerSecond = iNumNotes/fSongSeconds; float fReturn = fNotesPerSecond / 7; return min( fReturn, 1.0f ); } float NoteDataUtil::GetVoltageRadarValue( const NoteData &in, float fSongSeconds ) { if( !fSongSeconds ) return 0.0f; const float fLastBeat = in.GetLastBeat(); const float fAvgBPS = fLastBeat / fSongSeconds; // peak density of steps float fMaxDensitySoFar = 0; const int BEAT_WINDOW = 8; for( int i=0; i<=int(fLastBeat); i+=BEAT_WINDOW ) { int iNumNotesThisWindow = in.GetNumTapNotes((float)i,(float)i+BEAT_WINDOW) + in.GetNumHoldNotes((float)i,(float)i+BEAT_WINDOW); float fDensityThisWindow = iNumNotesThisWindow/(float)BEAT_WINDOW; fMaxDensitySoFar = max( fMaxDensitySoFar, fDensityThisWindow ); } float fReturn = fMaxDensitySoFar*fAvgBPS/10; return min( fReturn, 1.0f ); } float NoteDataUtil::GetAirRadarValue( const NoteData &in, float fSongSeconds ) { if( !fSongSeconds ) return 0.0f; // number of doubles int iNumDoubles = in.GetNumDoubles(); float fReturn = iNumDoubles / fSongSeconds; return min( fReturn, 1.0f ); } float NoteDataUtil::GetFreezeRadarValue( const NoteData &in, float fSongSeconds ) { if( !fSongSeconds ) return 0.0f; // number of hold steps float fReturn = in.GetNumHoldNotes() / fSongSeconds; return min( fReturn, 1.0f ); } float NoteDataUtil::GetChaosRadarValue( const NoteData &in, float fSongSeconds ) { if( !fSongSeconds ) return 0.0f; // count number of triplets or 16ths int iNumChaosNotes = 0; int rows = in.GetLastRow(); for( int r=0; r<=rows; r++ ) { if( !in.IsRowEmpty(r) && GetNoteType(r) >= NOTE_TYPE_12TH ) iNumChaosNotes++; } float fReturn = iNumChaosNotes / fSongSeconds * 0.5f; return min( fReturn, 1.0f ); } void NoteDataUtil::RemoveHoldNotes(NoteData &in, float fStartBeat, float fEndBeat) { int iStartIndex = BeatToNoteRow( fStartBeat ); int iEndIndex = BeatToNoteRow( fEndBeat ); // turn all the HoldNotes into TapNotes for( int i=in.GetNumHoldNotes()-1; i>=0; i-- ) // iterate backwards so we can delete { const HoldNote hn = in.GetHoldNote( i ); if( !hn.RangeOverlaps(iStartIndex,iEndIndex) ) continue; // skip in.RemoveHoldNote( i ); in.SetTapNote( hn.iTrack, hn.iStartRow, TAP_TAP ); } } void NoteDataUtil::RemoveSimultaneousNotes(NoteData &in, int iMaxSimultaneous, float fStartBeat, float fEndBeat) { int iStartIndex = BeatToNoteRow( fStartBeat ); int iEndIndex = BeatToNoteRow( fEndBeat ); // turn all the HoldNotes into TapNotes for( int r=iStartIndex; r<=iEndIndex; r++ ) // iterate backwards so we can delete { if( in.IsRowEmpty(r) ) continue; // skip vector viTracksHeld; in.GetTracksHeldAtRow( r, viTracksHeld ); // remove the first tap note or the first hold note that starts on this row int iTotalTracksPressed = in.GetNumTracksWithTap(r) + viTracksHeld.size(); int iTracksToRemove = max( 0, iTotalTracksPressed - iMaxSimultaneous ); for( int t=0; iTracksToRemove>0 && tm_iRoundSeed; reshuffle: RandomGen rnd( iShuffleSeed ); vector aiTracksLeftToMap; for( t=0; t=0; i-- ) if( fmodf(in.GetHoldNote(i).GetStartBeat(),1) != 0 ) // doesn't start on a beat in.RemoveHoldNote( i ); } void NoteDataUtil::Wide( NoteData &in, float fStartBeat, float fEndBeat ) { // Make all all quarter notes into jumps. in.ConvertHoldNotesTo4s(); /* Start on an even beat. */ fStartBeat = froundf( fStartBeat, 2 ); const int first_row = BeatToNoteRow( fStartBeat ); const int last_row = min( BeatToNoteRow(fEndBeat), in.GetLastRow() ); for( int i=first_row; i 0 ) { bSpaceAroundIsEmpty = false; break; } if( !bSpaceAroundIsEmpty ) continue; // skip // add a note determinitsitcally int iBeat = (int)roundf( NoteRowToBeat(i) ); int iTrackOfNote = in.GetFirstTrackWithTap(i); int iTrackToAdd = iTrackOfNote + (iBeat%5)-2; // won't be more than 2 tracks away from the existing note CLAMP( iTrackToAdd, 0, in.GetNumTracks()-1 ); if( iTrackToAdd == iTrackOfNote ) iTrackToAdd++; CLAMP( iTrackToAdd, 0, in.GetNumTracks()-1 ); if( iTrackToAdd == iTrackOfNote ) iTrackToAdd--; CLAMP( iTrackToAdd, 0, in.GetNumTracks()-1 ); if( in.GetTapNote(iTrackToAdd, i) != TAP_EMPTY ) { iTrackToAdd = (iTrackToAdd+1) % in.GetNumTracks(); } in.SetTapNote(iTrackToAdd, i, TAP_ADDITION); } in.Convert4sToHoldNotes(); } void NoteDataUtil::Big( NoteData &in, float fStartBeat, float fEndBeat ) { InsertIntelligentTaps(in,1.0f,0.5f,1.0f,false,fStartBeat,fEndBeat); // add 8ths between 4ths } void NoteDataUtil::Quick( NoteData &in, float fStartBeat, float fEndBeat ) { InsertIntelligentTaps(in,0.5f,0.25f,1.0f,false,fStartBeat,fEndBeat); // add 16ths between 8ths } // Due to popular request by people annoyed with the "new" implementation of Quick, we now have // this BMR-izer for your steps. Use with caution. void NoteDataUtil::BMRize( NoteData &in, float fStartBeat, float fEndBeat ) { InsertIntelligentTaps(in,1.0f,0.5f,1.0f,false,fStartBeat,fEndBeat); // add 8ths between 4ths InsertIntelligentTaps(in,0.5f,0.25f,0.5f,false,fStartBeat,fEndBeat); // then add 16ths between ALL 8ths // ...net result: OUCH. :-) } void NoteDataUtil::Skippy( NoteData &in, float fStartBeat, float fEndBeat ) { InsertIntelligentTaps(in,1.0f,0.75f,1.0f,true,fStartBeat,fEndBeat); // add 16ths between 4ths } void NoteDataUtil::InsertIntelligentTaps( NoteData &in, float fWindowSizeBeats, float fInsertOffsetBeats, float fWindowStrideBeats, bool bSkippy, float fStartBeat, float fEndBeat ) { ASSERT( fInsertOffsetBeats <= fWindowSizeBeats ); ASSERT( fWindowSizeBeats <= fWindowStrideBeats ); in.ConvertHoldNotesTo4s(); /* Start on a multiple of fBeatInterval. */ fStartBeat = froundf( fStartBeat, fWindowStrideBeats ); // Insert a beat in the middle of every fBeatInterval. const int first_row = BeatToNoteRow( fStartBeat ); const int last_row = min( BeatToNoteRow(fEndBeat), in.GetLastRow() ); const int rows_per_window = BeatToNoteRow( fWindowSizeBeats ); const int rows_per_stride = BeatToNoteRow( fWindowStrideBeats ); const int insert_row_offset = BeatToNoteRow( fInsertOffsetBeats ); for( int i=first_row; i= 2 ) { iTrackOfNoteToAdd = min(iTrackOfNoteEarlier,iTrackOfNoteLater)+1; goto done_looking_for_track_to_add; } if( min(iTrackOfNoteEarlier,iTrackOfNoteLater)-1 >= 0 ) { iTrackOfNoteToAdd = min(iTrackOfNoteEarlier,iTrackOfNoteLater)-1; goto done_looking_for_track_to_add; } if( max(iTrackOfNoteEarlier,iTrackOfNoteLater)+1 < in.GetNumTracks() ) { iTrackOfNoteToAdd = max(iTrackOfNoteEarlier,iTrackOfNoteLater)+1; goto done_looking_for_track_to_add; } done_looking_for_track_to_add: in.SetTapNote(iTrackOfNoteToAdd, iRowToAdd, TAP_ADDITION); } in.Convert4sToHoldNotes(); } void NoteDataUtil::AddMines( NoteData &in, float fStartBeat, float fEndBeat ) { const int first_row = BeatToNoteRow( fStartBeat ); const int last_row = min( BeatToNoteRow(fEndBeat), in.GetLastRow() ); // // Change whole rows at a time to be tap notes. Otherwise, it causes // major problems for our scoring system. -Chris // int iRowCount = 0; int iPlaceEveryRows = 6; for( int r=first_row; r<=last_row; r++ ) { if( !in.IsRowEmpty(r) ) { iRowCount++; // place every 6 or 7 rows if( iRowCount>=iPlaceEveryRows ) { for( int t=0; t last_row ) continue; // Only place a mines if there's not another step nearby int iMineRangeBegin = BeatToNoteRow( fMineBeat-0.5f ) + 1; int iMineRangeEnd = BeatToNoteRow( fMineBeat+0.5f ) - 1; if( !in.IsRangeEmpty(hn.iTrack, iMineRangeBegin, iMineRangeEnd) ) continue; // Add a mine right after the hold end. in.SetTapNote(hn.iTrack,iMineRow,TAP_MINE); // Convert all notes in this row to mines. for( int t=0; t viTracks; in.GetTracksHeldAtRow( iRowEcho, viTracks ); // don't lay if holding 2 already if( viTracks.size() >= 2 ) continue; // don't lay // don't lay echos on top of a HoldNote if( find(viTracks.begin(),viTracks.end(),iEchoTrack) != viTracks.end() ) continue; // don't lay } in.SetTapNote( iEchoTrack, iRowEcho, TAP_ADDITION ); } } void NoteDataUtil::Planted( NoteData &in, float fStartBeat, float fEndBeat ) { ConvertTapsToHolds( in, 1, fStartBeat, fEndBeat ); } void NoteDataUtil::Twister( NoteData &in, float fStartBeat, float fEndBeat ) { ConvertTapsToHolds( in, 2, fStartBeat, fEndBeat ); } void NoteDataUtil::ConvertTapsToHolds( NoteData &in, int iSimultaneousHolds, float fStartBeat, float fEndBeat ) { // Convert all taps to freezes. const int first_row = BeatToNoteRow( fStartBeat ); const int last_row = min( BeatToNoteRow(fEndBeat), in.GetLastRow() ); for( int r=first_row; r<=last_row; r++ ) { for( int t=0; t= 1 ) continue; int iOppositeTrack = iTrackMapping[t]; in.SetTapNote( iOppositeTrack, r, TAP_ADDITION ); } } } } void NoteDataUtil::SnapToNearestNoteType( NoteData &in, NoteType nt1, NoteType nt2, float fBeginBeat, float fEndBeat ) { // nt2 is optional and should be -1 if it is not used float fSnapInterval1, fSnapInterval2; switch( nt1 ) { case NOTE_TYPE_4TH: fSnapInterval1 = 1/1.0f; break; case NOTE_TYPE_8TH: fSnapInterval1 = 1/2.0f; break; case NOTE_TYPE_12TH: fSnapInterval1 = 1/3.0f; break; case NOTE_TYPE_16TH: fSnapInterval1 = 1/4.0f; break; case NOTE_TYPE_24TH: fSnapInterval1 = 1/6.0f; break; case NOTE_TYPE_32ND: fSnapInterval1 = 1/8.0f; break; case NOTE_TYPE_48TH: fSnapInterval1 = 1/12.0f; break; case NOTE_TYPE_64TH: fSnapInterval1 = 1/16.0f; break; default: ASSERT( false ); return; } switch( nt2 ) { case NOTE_TYPE_4TH: fSnapInterval2 = 1/1.0f; break; case NOTE_TYPE_8TH: fSnapInterval2 = 1/2.0f; break; case NOTE_TYPE_12TH: fSnapInterval2 = 1/3.0f; break; case NOTE_TYPE_16TH: fSnapInterval2 = 1/4.0f; break; case NOTE_TYPE_24TH: fSnapInterval2 = 1/6.0f; break; case NOTE_TYPE_32ND: fSnapInterval2 = 1/8.0f; break; case NOTE_TYPE_48TH: fSnapInterval2 = 1/12.0f; break; case NOTE_TYPE_64TH: fSnapInterval2 = 1/16.0f; break; case -1: fSnapInterval2 = 10000; break; // nothing will ever snap to this. That's what we want! default: ASSERT( false ); return; } int iNoteIndexBegin = BeatToNoteRow( fBeginBeat ); int iNoteIndexEnd = BeatToNoteRow( fEndBeat ); in.ConvertHoldNotesTo2sAnd3s(); // iterate over all TapNotes in the interval and snap them for( int i=iNoteIndexBegin; i<=iNoteIndexEnd; i++ ) { int iOldIndex = i; float fOldBeat = NoteRowToBeat( iOldIndex ); float fNewBeat1 = froundf( fOldBeat, fSnapInterval1 ); float fNewBeat2 = froundf( fOldBeat, fSnapInterval2 ); bool bNewBeat1IsCloser = fabsf(fNewBeat1-fOldBeat) < fabsf(fNewBeat2-fOldBeat); float fNewBeat = bNewBeat1IsCloser ? fNewBeat1 : fNewBeat2; int iNewIndex = BeatToNoteRow( fNewBeat ); for( int c=0; c0; t-- ) // in.GetNumTracks()-1 times { int iTrackEarlier = t; int iTrackLater = (t+1) % in.GetNumTracks(); // swap TapNote tnEarlier = in.GetTapNote(iTrackEarlier, r); TapNote tnLater = in.GetTapNote(iTrackLater, r); in.SetTapNote(iTrackEarlier, r, tnLater); in.SetTapNote(iTrackLater, r, tnEarlier); } } in.Convert4sToHoldNotes(); } struct ValidRow { StepsType st; bool bValidMask[MAX_NOTE_TRACKS]; }; #define T true #define f false const ValidRow g_ValidRows[] = { { STEPS_TYPE_DANCE_DOUBLE, { T,T,T,T,f,f,f,f } }, { STEPS_TYPE_DANCE_DOUBLE, { f,T,T,T,T,f,f,f } }, { STEPS_TYPE_DANCE_DOUBLE, { f,f,f,T,T,T,T,f } }, { STEPS_TYPE_DANCE_DOUBLE, { f,f,f,f,T,T,T,T } }, }; void NoteDataUtil::FixImpossibleRows( NoteData &in, StepsType st ) { vector vpValidRowsToCheck; for( unsigned i=0; im_fMusicLengthSeconds; sec+=30 ) { float fBeat = pSong->GetBeatFromElapsedTime( sec ); int iBeat = (int)fBeat; int iTrack = iBeat % nd.GetNumTracks(); // deterministically calculates track Attack attack; attack.fStartSecond = -1; attack.fSecsRemaining = 15; attack.sModifier = szAttacks[rand()%ARRAYSIZE(szAttacks)]; attack.level = ATTACK_LEVEL_1; nd.SetTapAttackNote( iTrack, BeatToNoteRow(fBeat), attack ); } } #if 0 // undo this if ScaleRegion breaks more things than it fixes void NoteDataUtil::Scale( NoteData &nd, float fScale ) { ASSERT( fScale > 0 ); NoteData temp; temp.CopyAll( &nd ); nd.ClearAll(); for( int r=0; r<=temp.GetLastRow(); r++ ) { for( int t=0; t 0 ); ASSERT( fStartBeat < fEndBeat ); ASSERT( fStartBeat >= 0 ); NoteData temp1, temp2; temp1.Config( nd ); temp2.Config( nd ); const int iFirstRowAtEndOfRegion = min( nd.GetLastRow(), BeatToNoteRowNotRounded(fEndBeat) ); const int iScaledFirstRowAfterRegion = (int)((fStartBeat + (fEndBeat - fStartBeat) * fScale) * ROWS_PER_BEAT); if( fStartBeat != 0 ) temp1.CopyRange( &nd, 0, BeatToNoteRowNotRounded(fStartBeat) ); if( nd.GetLastRow() > iFirstRowAtEndOfRegion ) temp1.CopyRange( &nd, iFirstRowAtEndOfRegion, nd.GetLastRow(), iScaledFirstRowAfterRegion); temp2.CopyRange( &nd, BeatToNoteRowNotRounded(fStartBeat), iFirstRowAtEndOfRegion ); nd.ClearAll(); for( int r=0; r<=temp2.GetLastRow(); r++ ) { for( int t=0; t 0 ) // add blank rows iPasteAtRow += BeatToNoteRow( fBeatsToShift ); else // delete rows iTakeFromRow += BeatToNoteRow( -fBeatsToShift ); temp.CopyRange( &nd, iTakeFromRow, nd.GetLastRow() ); nd.ClearRange( min(iTakeFromRow,iPasteAtRow), nd.GetLastRow() ); nd.CopyRange( &temp, 0, temp.GetLastRow(), iPasteAtRow ); }