int NoteData::GetNumHands( float fStartBeat, float fEndBeat ) const { /* Count the number of times you have to use your hands. This includes * three taps at the same time, a tap while two hold notes are being held, * etc. Only count rows that have at least one tap note (hold heads count). * Otherwise, every row of hold notes counts, so three simultaneous hold * notes will count as hundreds of "hands". */ if( fEndBeat == -1 ) fEndBeat = GetNumBeats(); int iStartIndex = BeatToNoteRow( fStartBeat ); int iEndIndex = BeatToNoteRow( fEndBeat ); /* Clamp to known-good ranges. */ iStartIndex = max( iStartIndex, 0 ); iEndIndex = min( iEndIndex, GetNumRows()-1 ); int iNum = 0; for( int i=iStartIndex; i<=iEndIndex; i++ ) { if( !RowNeedsHands(i) ) continue; iNum++; } return iNum; }
int NoteData::GetNumN( int MinTaps, float fStartBeat, float fEndBeat ) const { if( fEndBeat == -1 ) fEndBeat = GetNumBeats(); int iStartIndex = BeatToNoteRow( fStartBeat ); int iEndIndex = BeatToNoteRow( fEndBeat ); /* Clamp to known-good ranges. */ iStartIndex = max( iStartIndex, 0 ); iEndIndex = min( iEndIndex, GetNumRows()-1 ); int iNum = 0; for( int i=iStartIndex; i<=iEndIndex; i++ ) { int iNumNotesThisIndex = 0; for( int t=0; t<m_iNumTracks; t++ ) { TapNote tn = GetTapNoteX(t, i); if( tn.type != TapNote::mine && tn.type != TapNote::empty ) // mines don't count iNumNotesThisIndex++; } if( iNumNotesThisIndex >= MinTaps ) iNum++; } return iNum; }
int NoteData::GetNumTapNotes( float fStartBeat, float fEndBeat ) const { int iNumNotes = 0; if( fEndBeat == -1 ) fEndBeat = GetNumBeats(); int iStartIndex = BeatToNoteRow( fStartBeat ); int iEndIndex = BeatToNoteRow( fEndBeat ); /* Clamp to known-good ranges. */ iStartIndex = max( iStartIndex, 0 ); iEndIndex = min( iEndIndex, GetNumRows()-1 ); for( int t=0; t<m_iNumTracks; t++ ) { for( int i=iStartIndex; i<=iEndIndex; i++ ) { TapNote tn = GetTapNoteX(t, i); if( tn.type != TapNote::empty && tn.type != TapNote::mine ) iNumNotes++; } } return iNumNotes; }
int NoteDataWithScoring::GetNumNWithScore( TapNoteScore tns, int MinTaps, const float fStartBeat, float fEndBeat ) const { if( fEndBeat == -1 ) fEndBeat = GetNumBeats(); int iStartIndex = BeatToNoteRow( fStartBeat ); int iEndIndex = BeatToNoteRow( fEndBeat ); iStartIndex = max( iStartIndex, 0 ); iEndIndex = min( iEndIndex, GetNumRows()-1 ); int iNumSuccessfulDoubles = 0; for( int i=iStartIndex; i<=iEndIndex; i++ ) { int iNumNotesThisIndex = 0; TapNoteScore minTapNoteScore = TNS_MARVELOUS; for( int t=0; t<GetNumTracks(); t++ ) { switch( GetTapNote(t, i).type ) { case TapNote::tap: case TapNote::hold_head: iNumNotesThisIndex++; minTapNoteScore = min( minTapNoteScore, GetTapNoteScore(t, i) ); } } if( iNumNotesThisIndex >= MinTaps && minTapNoteScore >= tns ) iNumSuccessfulDoubles++; } return iNumSuccessfulDoubles; }
/* See NoteData::GetNumHands(). */ int NoteDataWithScoring::GetSuccessfulHands( float fStartBeat, float fEndBeat ) const { if( fEndBeat == -1 ) fEndBeat = GetNumBeats(); int iStartIndex = BeatToNoteRow( fStartBeat ); int iEndIndex = BeatToNoteRow( fEndBeat ); /* Clamp to known-good ranges. */ iStartIndex = max( iStartIndex, 0 ); iEndIndex = min( iEndIndex, GetNumRows()-1 ); int iNum = 0; for( int i=iStartIndex; i<=iEndIndex; i++ ) { if( !RowNeedsHands(i) ) continue; bool Missed = false; for( int t=0; t<GetNumTracks(); t++ ) { TapNote tn = GetTapNoteX(t, i); if( tn.type == TapNote::empty ) continue; if( tn.type == TapNote::mine ) // mines don't count continue; if( GetTapNoteScore(t, i) <= TNS_BOO ) Missed = true; } if( Missed ) continue; /* Check hold scores. */ for( int j=0; j<GetNumHoldNotes(); j++ ) { const HoldNote &hn = GetHoldNote(j); HoldNoteResult hnr = GetHoldNoteResult( hn ); /* Check if the row we're checking is in range. */ if( !hn.RowIsInRange(i) ) continue; /* If a hold is released *after* a hands containing it, the hands is * still good. So, ignore the judgement and only examine iLastHeldRow * to be sure that the hold was still held at the point of this row. * (Note that if the hold head tap was missed, then iLastHeldRow == i * and this won't fail--but the tap check above will have already failed.) */ if( hnr.iLastHeldRow < i ) Missed = true; } if( !Missed ) iNum++; } return iNum; }
int NoteData::GetNumRowsWithTapOrHoldHead( float fStartBeat, float fEndBeat ) const { int iNumNotes = 0; if(fEndBeat == -1) fEndBeat = GetNumBeats(); int iStartIndex = BeatToNoteRow( fStartBeat ); int iEndIndex = BeatToNoteRow( fEndBeat ); for( int i=iStartIndex; i<=iEndIndex; i++ ) if( IsThereATapOrHoldHeadAtRow(i) ) iNumNotes++; return iNumNotes; }
void KSFLoader::RemoveHoles( NoteData &out, const Song &song ) { /* Start at the second BPM segment; the first one is already aligned. */ for( unsigned seg = 1; seg < song.m_Timing.m_BPMSegments.size(); ++seg ) { // const float FromBeat = song.m_Timing.m_BPMSegments[seg].m_fStartBeat; const float FromBeat = song.m_Timing.m_BPMSegments[seg].m_fStartBeat * song.m_BPMSegments[seg].m_fBPM / song.m_BPMSegments[0].m_fBPM; const int FromRow = (int) BeatToNoteRow(FromBeat); const int ToRow = (int) BeatToNoteRow(song.m_Timing.m_BPMSegments[seg].m_fStartBeat); LOG->Trace("from %f (%i) to (%i)", FromBeat, FromRow, ToRow); // const int ToRow = (int) roundf(FromRow * song.m_Timing.m_BPMSegments[0].m_fBPM / song.m_BPMSegments[seg].m_fBPM); // const int Rows = out.GetLastRow() - FromRow + 1; // int LastRow; // if(seg+1 < song.m_Timing.m_BPMSegments().size()) // LastRow = (int) NoteRowToBeat( song.m_Timing.m_BPMSegments[seg+1].m_fStartBeat ) - 1; // else // LastRow = out.GetLastRow(); NoteData tmp; tmp.SetNumTracks(out.GetNumTracks()); tmp.CopyRange( &out, FromRow, out.GetLastRow() ); out.ClearRange( FromRow, out.GetLastRow() ); out.CopyRange( &tmp, 0, tmp.GetLastRow(), ToRow ); } /* out.ConvertHoldNotesTo2sAnd3s(); for( t = 0; t < notedata.GetNumTracks(); ++t ) { const float CurBPM = song.GetBPMAtBeat( NoteRowToBeat(row) ); song.m_Timing.m_BPMSegments.size() for( int row = 0; row <= notedata.GetLastRow(); ++row ) { TapNote tn = notedata.GetTapNote(t, row); if( tn == TAP_EMPTY ) continue; const int RealRow = (int) roundf(row * OrigBPM / CurBPM); if( RealRow == row ) continue; LOG->Trace("from %i to %i", row, RealRow); notedata.SetTapNote( t, RealRow, tn ); notedata.SetTapNote( t, row, TAP_EMPTY ); } } out.Convert2sAnd3sToHoldNotes(); */ }
void SSCLoader::ProcessLabels( TimingData &out, const RString sParam ) { vector<RString> arrayLabelExpressions; split( sParam, ",", arrayLabelExpressions ); for( unsigned b=0; b<arrayLabelExpressions.size(); b++ ) { vector<RString> arrayLabelValues; split( arrayLabelExpressions[b], "=", arrayLabelValues ); if( arrayLabelValues.size() != 2 ) { LOG->UserLog("Song file", this->GetSongTitle(), "has an invalid #LABELS value \"%s\" (must have exactly one '='), ignored.", arrayLabelExpressions[b].c_str() ); continue; } const float fBeat = StringToFloat( arrayLabelValues[0] ); RString sLabel = arrayLabelValues[1]; TrimRight(sLabel); if( fBeat >= 0.0f ) out.AddSegment( LabelSegment(BeatToNoteRow(fBeat), sLabel) ); else { LOG->UserLog("Song file", this->GetSongTitle(), "has an invalid Label at beat %f called %s.", fBeat, sLabel.c_str() ); } } }
void SSCLoader::ProcessStops( TimingData &out, const RString sParam ) { vector<RString> arrayStopExpressions; split( sParam, ",", arrayStopExpressions ); for( unsigned b=0; b<arrayStopExpressions.size(); b++ ) { vector<RString> arrayStopValues; split( arrayStopExpressions[b], "=", arrayStopValues ); if( arrayStopValues.size() != 2 ) { LOG->UserLog("Song file", this->GetSongTitle(), "has an invalid #STOPS value \"%s\" (must have exactly one '='), ignored.", arrayStopExpressions[b].c_str() ); continue; } const float fBeat = StringToFloat( arrayStopValues[0] ); const float fNewStop = StringToFloat( arrayStopValues[1] ); if( fBeat >= 0 && fNewStop > 0 ) out.AddSegment( StopSegment(BeatToNoteRow(fBeat), fNewStop) ); else { LOG->UserLog("Song file", this->GetSongTitle(), "has an invalid Stop at beat %f, length %f.", fBeat, fNewStop ); } } }
int NoteData::GetNumHoldNotes( float fStartBeat, float fEndBeat ) const { if( fEndBeat == -1 ) fEndBeat = GetNumBeats(); int iStartIndex = BeatToNoteRow( fStartBeat ); int iEndIndex = BeatToNoteRow( fEndBeat ); int iNumHolds = 0; for( int i=0; i<GetNumHoldNotes(); i++ ) { const HoldNote &hn = GetHoldNote(i); if( iStartIndex <= hn.iStartRow && hn.iEndRow <= iEndIndex ) iNumHolds++; } return iNumHolds; }
static int GetMeasureStartRow( const MeasureToTimeSig_t &sigs, int iMeasureNo, const MeasureToTimeSig_t &sigAdjustments ) { int iRowNo = 0; for( int i = 0; i < iMeasureNo; ++i ) iRowNo += BeatToNoteRow( GetBeatsPerMeasure(sigs, i, sigAdjustments) ); return iRowNo; }
void SSCLoader::ProcessScrolls( TimingData &out, const RString sParam ) { vector<RString> vs1; split( sParam, ",", vs1 ); FOREACH_CONST( RString, vs1, s1 ) { vector<RString> vs2; split( *s1, "=", vs2 ); if( vs2.size() < 2 ) { LOG->UserLog("Song file", this->GetSongTitle(), "has an scroll change with %i values.", static_cast<int>(vs2.size()) ); continue; } const float fBeat = StringToFloat( vs2[0] ); const float fRatio = StringToFloat( vs2[1] ); if( fBeat < 0 ) { LOG->UserLog("Song file", this->GetSongTitle(), "has an scroll change with beat %f.", fBeat ); continue; } out.AddSegment( ScrollSegment(BeatToNoteRow(fBeat), fRatio) ); }
static void HandleBunki( TimingData &timing, const float fEarlyBPM, const float fCurBPM, const float fGap, const float fPos ) { const float BeatsPerSecond = fEarlyBPM / 60.0f; const float beat = (fPos + fGap) * BeatsPerSecond; LOG->Trace( "BPM %f, BPS %f, BPMPos %f, beat %f", fEarlyBPM, BeatsPerSecond, fPos, beat ); timing.AddSegment( BPMSegment(BeatToNoteRow(beat), fCurBPM) ); }
int NoteDataWithScoring::GetNumHoldNotesWithScore( HoldNoteScore hns, const float fStartBeat, float fEndBeat ) const { int iNumSuccessfulHolds = 0; if(fEndBeat == -1) fEndBeat = GetNumBeats(); int iStartIndex = BeatToNoteRow( fStartBeat ); int iEndIndex = BeatToNoteRow( fEndBeat ); for( int i=0; i<GetNumHoldNotes(); i++ ) { const HoldNote &hn = GetHoldNote(i); if( iStartIndex > hn.iStartRow || hn.iEndRow > iEndIndex ) continue; if( GetHoldNoteScore(hn) == hns ) iNumSuccessfulHolds++; } return iNumSuccessfulHolds; }
int NoteDataWithScoring::GetNumTapNotesWithScore( TapNoteScore tns, const float fStartBeat, float fEndBeat ) const { int iNumSuccessfulTapNotes = 0; if(fEndBeat == -1) fEndBeat = GetNumBeats()+1; unsigned iStartIndex = BeatToNoteRow( fStartBeat ); unsigned iEndIndex = BeatToNoteRow( fEndBeat ); for( unsigned i=iStartIndex; i<min(float(iEndIndex), float(m_TapNoteScores[0].size())); i++ ) { for( int t=0; t<GetNumTracks(); t++ ) { if( this->GetTapNote(t, i).type != TapNote::empty && GetTapNoteScore(t, i) >= tns ) iNumSuccessfulTapNotes++; } } return iNumSuccessfulTapNotes; }
int NoteDataWithScoring::GetSuccessfulMines( float fStartBeat, float fEndBeat ) const { if( fEndBeat == -1 ) fEndBeat = GetNumBeats(); int iStartIndex = BeatToNoteRow( fStartBeat ); int iEndIndex = BeatToNoteRow( fEndBeat ); iStartIndex = max( iStartIndex, 0 ); iEndIndex = min( iEndIndex, GetNumRows()-1 ); int iNumSuccessfulMinesNotes = 0; for( int i=iStartIndex; i<=iEndIndex; i++ ) { for( int t=0; t<GetNumTracks(); t++ ) { if( this->GetTapNote(t,i).type == TapNote::mine && GetTapNoteScore(t, i) != TNS_HIT_MINE ) iNumSuccessfulMinesNotes++; } } return iNumSuccessfulMinesNotes; }
void SoundEffectControl::Update( float fDeltaTime ) { if( SOUND_PROPERTY == "" ) return; float fLevel = INPUTMAPPER->GetLevel( GAME_BUTTON_EFFECT_UP, m_pPlayerState->m_PlayerNumber ); fLevel -= INPUTMAPPER->GetLevel( GAME_BUTTON_EFFECT_DOWN, m_pPlayerState->m_PlayerNumber ); CLAMP( fLevel, -1.0f, +1.0f ); if( LOCK_TO_HOLD ) { int iRow = BeatToNoteRow( GAMESTATE->m_Position.m_fSongBeat ); int iHoldsHeld, iHoldsLetGo; HoldsBeingHeld( iRow, iHoldsHeld, iHoldsLetGo ); /* If no holds are being held, or any have been missed, lock the effect off until * the button has been released. */ if( iHoldsLetGo > 0 || iHoldsHeld == 0 ) m_bLocked = true; /* If the button is released, unlock it when the level crosses or reaches 0. */ if( m_bLocked ) { if( (fLevel <= 0.0f && m_fLastLevel >= 0.0f) || (fLevel >= 0.0f && m_fLastLevel <= 0.0f) ) m_bLocked = false; } } m_fLastLevel = fLevel; if( m_bLocked ) fLevel = 0.0f; m_fSample = fLevel; m_pPlayerState->m_EffectHistory.AddSample( m_fSample, fDeltaTime ); float fPropertyMin = PROPERTY_MIN; float fPropertyCenter = PROPERTY_CENTER; float fPropertyMax = PROPERTY_MAX; float fCurrent; if( m_fSample < 0 ) fCurrent = SCALE( m_fSample, 0.0f, -1.0f, fPropertyCenter, fPropertyMin ); else fCurrent = SCALE( m_fSample, 0.0f, +1.0f, fPropertyCenter, fPropertyMax ); if( m_pSoundReader ) m_pSoundReader->SetProperty( SOUND_PROPERTY, fCurrent ); }
void SSCLoader::ProcessWarps( TimingData &out, const RString sParam, const float fVersion ) { vector<RString> arrayWarpExpressions; split( sParam, ",", arrayWarpExpressions ); for( unsigned b=0; b<arrayWarpExpressions.size(); b++ ) { vector<RString> arrayWarpValues; split( arrayWarpExpressions[b], "=", arrayWarpValues ); if( arrayWarpValues.size() != 2 ) { LOG->UserLog("Song file", this->GetSongTitle(), "has an invalid #WARPS value \"%s\" (must have exactly one '='), ignored.", arrayWarpExpressions[b].c_str() ); continue; } const float fBeat = StringToFloat( arrayWarpValues[0] ); const float fNewBeat = StringToFloat( arrayWarpValues[1] ); // Early versions were absolute in beats. They should be relative. if( ( fVersion < VERSION_SPLIT_TIMING && fNewBeat > fBeat ) ) { out.AddSegment( WarpSegment(BeatToNoteRow(fBeat), fNewBeat - fBeat) ); } else if( fNewBeat > 0 ) out.AddSegment( WarpSegment(BeatToNoteRow(fBeat), fNewBeat) ); else { LOG->UserLog("Song file", this->GetSongTitle(), "has an invalid Warp at beat %f, BPM %f.", fBeat, fNewBeat ); } } }
void SSCLoader::ProcessCombos( TimingData &out, const RString line, const int rowsPerBeat ) { vector<RString> arrayComboExpressions; split( line, ",", arrayComboExpressions ); for( unsigned f=0; f<arrayComboExpressions.size(); f++ ) { vector<RString> arrayComboValues; split( arrayComboExpressions[f], "=", arrayComboValues ); unsigned size = arrayComboValues.size(); if( size < 2 ) { LOG->UserLog("Song file", this->GetSongTitle(), "has an invalid #COMBOS value \"%s\" (must have at least one '='), ignored.", arrayComboExpressions[f].c_str() ); continue; } const float fComboBeat = StringToFloat( arrayComboValues[0] ); const int iCombos = StringToInt( arrayComboValues[1] ); const int iMisses = (size == 2 ? iCombos : StringToInt(arrayComboValues[2])); out.AddSegment( ComboSegment( BeatToNoteRow(fComboBeat), iCombos, iMisses ) ); } }
/* For visibility testing: if bAbsolute is false, random modifiers must return the * minimum possible scroll speed. */ float ArrowEffects::GetYOffset( const PlayerState* pPlayerState, int iCol, float fNoteBeat, float &fPeakYOffsetOut, bool &bIsPastPeakOut, bool bAbsolute ) { // Default values that are returned if boomerang is off. fPeakYOffsetOut = FLT_MAX; bIsPastPeakOut = true; float fYOffset = 0; /* Usually, fTimeSpacing is 0 or 1, in which case we use entirely beat spacing or * entirely time spacing (respectively). Occasionally, we tween between them. */ if( pPlayerState->m_CurrentPlayerOptions.m_fTimeSpacing != 1.0f ) { float fSongBeat = GAMESTATE->m_fSongBeat; float fBeatsUntilStep = fNoteBeat - fSongBeat; float fYOffsetBeatSpacing = fBeatsUntilStep * ARROW_SPACING; fYOffset += fYOffsetBeatSpacing * (1-pPlayerState->m_CurrentPlayerOptions.m_fTimeSpacing); } if( pPlayerState->m_CurrentPlayerOptions.m_fTimeSpacing != 0.0f ) { float fSongSeconds = GAMESTATE->m_fMusicSeconds; float fNoteSeconds = GAMESTATE->m_pCurSong->GetElapsedTimeFromBeat(fNoteBeat); float fSecondsUntilStep = fNoteSeconds - fSongSeconds; float fBPM = pPlayerState->m_CurrentPlayerOptions.m_fScrollBPM; float fBPS = fBPM/60.f; float fYOffsetTimeSpacing = fSecondsUntilStep * fBPS * ARROW_SPACING; fYOffset += fYOffsetTimeSpacing * pPlayerState->m_CurrentPlayerOptions.m_fTimeSpacing; } // don't mess with the arrows after they've crossed 0 if( fYOffset < 0 ) return fYOffset * pPlayerState->m_CurrentPlayerOptions.m_fScrollSpeed; const float* fAccels = pPlayerState->m_CurrentPlayerOptions.m_fAccels; //const float* fEffects = pPlayerState->m_CurrentPlayerOptions.m_fEffects; float fYAdjust = 0; // fill this in depending on PlayerOptions if( fAccels[PlayerOptions::ACCEL_BOOST] != 0 ) { float fEffectHeight = GetNoteFieldHeight(pPlayerState); float fNewYOffset = fYOffset * 1.5f / ((fYOffset+fEffectHeight/1.2f)/fEffectHeight); float fAccelYAdjust = fAccels[PlayerOptions::ACCEL_BOOST] * (fNewYOffset - fYOffset); // TRICKY: Clamp this value, or else BOOST+BOOMERANG will draw a ton of arrows on the screen. CLAMP( fAccelYAdjust, -400.f, 400.f ); fYAdjust += fAccelYAdjust; } if( fAccels[PlayerOptions::ACCEL_BRAKE] != 0 ) { float fEffectHeight = GetNoteFieldHeight(pPlayerState); float fScale = SCALE( fYOffset, 0.f, fEffectHeight, 0, 1.f ); float fNewYOffset = fYOffset * fScale; float fBrakeYAdjust = fAccels[PlayerOptions::ACCEL_BRAKE] * (fNewYOffset - fYOffset); // TRICKY: Clamp this value the same way as BOOST so that in BOOST+BRAKE, BRAKE doesn't overpower BOOST CLAMP( fBrakeYAdjust, -400.f, 400.f ); fYAdjust += fBrakeYAdjust; } if( fAccels[PlayerOptions::ACCEL_WAVE] != 0 ) fYAdjust += fAccels[PlayerOptions::ACCEL_WAVE] * 20.0f*RageFastSin( fYOffset/38.0f ); fYOffset += fYAdjust; // // Factor in boomerang // if( fAccels[PlayerOptions::ACCEL_BOOMERANG] != 0 ) { float fOriginalYOffset = fYOffset; fYOffset = (-1*fOriginalYOffset*fOriginalYOffset/SCREEN_HEIGHT) + 1.5f*fOriginalYOffset; float fPeakAtYOffset = SCREEN_HEIGHT * 0.75f; // zero point of function above fPeakYOffsetOut = (-1*fPeakAtYOffset*fPeakAtYOffset/SCREEN_HEIGHT) + 1.5f*fPeakAtYOffset; bIsPastPeakOut = fOriginalYOffset < fPeakAtYOffset; } // // Factor in scroll speed // float fScrollSpeed = pPlayerState->m_CurrentPlayerOptions.m_fScrollSpeed; if( pPlayerState->m_CurrentPlayerOptions.m_fRandomSpeed > 0 && !bAbsolute ) { int seed = GAMESTATE->m_iStageSeed + ( BeatToNoteRow( fNoteBeat ) << 8 ) + (iCol * 100); /* Temporary hack: the first call to RandomFloat isn't "random"; it takes an extra * call to get the RNG rolling. */ RandomFloat( seed ); float fRandom = RandomFloat( seed ); /* Random speed always increases speed: a random speed of 10 indicates [1,11]. * This keeps it consistent with other mods: 0 means no effect. */ fScrollSpeed *= SCALE( fRandom, 0.0f, 1.0f, 1.0f, pPlayerState->m_CurrentPlayerOptions.m_fRandomSpeed + 1.0f ); } if( fAccels[PlayerOptions::ACCEL_EXPAND] != 0 ) { /* Timers can't be global, since they'll be initialized before SDL. */ static RageTimer timerExpand; if( !GAMESTATE->m_bFreeze ) g_fExpandSeconds += timerExpand.GetDeltaTime(); else timerExpand.GetDeltaTime(); // throw away float fExpandMultiplier = SCALE( RageFastCos(g_fExpandSeconds*3), -1, 1, 0.75f, 1.75f ); fScrollSpeed *= SCALE( fAccels[PlayerOptions::ACCEL_EXPAND], 0.f, 1.f, 1.f, fExpandMultiplier ); } fYOffset *= fScrollSpeed; fPeakYOffsetOut *= fScrollSpeed; return fYOffset; }
static NoteData ParseNoteData(RString &step1, RString &step2, Steps &out, const RString &path) { g_mapDanceNoteToNoteDataColumn.clear(); switch( out.m_StepsType ) { case StepsType_dance_single: g_mapDanceNoteToNoteDataColumn[DANCE_NOTE_PAD1_LEFT] = 0; g_mapDanceNoteToNoteDataColumn[DANCE_NOTE_PAD1_DOWN] = 1; g_mapDanceNoteToNoteDataColumn[DANCE_NOTE_PAD1_UP] = 2; g_mapDanceNoteToNoteDataColumn[DANCE_NOTE_PAD1_RIGHT] = 3; break; case StepsType_dance_double: case StepsType_dance_couple: g_mapDanceNoteToNoteDataColumn[DANCE_NOTE_PAD1_LEFT] = 0; g_mapDanceNoteToNoteDataColumn[DANCE_NOTE_PAD1_DOWN] = 1; g_mapDanceNoteToNoteDataColumn[DANCE_NOTE_PAD1_UP] = 2; g_mapDanceNoteToNoteDataColumn[DANCE_NOTE_PAD1_RIGHT] = 3; g_mapDanceNoteToNoteDataColumn[DANCE_NOTE_PAD2_LEFT] = 4; g_mapDanceNoteToNoteDataColumn[DANCE_NOTE_PAD2_DOWN] = 5; g_mapDanceNoteToNoteDataColumn[DANCE_NOTE_PAD2_UP] = 6; g_mapDanceNoteToNoteDataColumn[DANCE_NOTE_PAD2_RIGHT] = 7; break; case StepsType_dance_solo: g_mapDanceNoteToNoteDataColumn[DANCE_NOTE_PAD1_LEFT] = 0; g_mapDanceNoteToNoteDataColumn[DANCE_NOTE_PAD1_UPLEFT] = 1; g_mapDanceNoteToNoteDataColumn[DANCE_NOTE_PAD1_DOWN] = 2; g_mapDanceNoteToNoteDataColumn[DANCE_NOTE_PAD1_UP] = 3; g_mapDanceNoteToNoteDataColumn[DANCE_NOTE_PAD1_UPRIGHT] = 4; g_mapDanceNoteToNoteDataColumn[DANCE_NOTE_PAD1_RIGHT] = 5; break; DEFAULT_FAIL( out.m_StepsType ); } NoteData newNoteData; newNoteData.SetNumTracks( g_mapDanceNoteToNoteDataColumn.size() ); for( int pad=0; pad<2; pad++ ) // foreach pad { RString sStepData; switch( pad ) { case 0: sStepData = step1; break; case 1: if( step2 == "" ) // no data continue; // skip sStepData = step2; break; DEFAULT_FAIL( pad ); } sStepData.Replace("\n", ""); sStepData.Replace("\r", ""); sStepData.Replace("\t", ""); sStepData.Replace(" ", ""); double fCurrentBeat = 0; double fCurrentIncrementer = 1.0/8 * BEATS_PER_MEASURE; for( size_t i=0; i<sStepData.size(); ) { char c = sStepData[i++]; switch( c ) { // begins a series case '(': fCurrentIncrementer = 1.0/16 * BEATS_PER_MEASURE; break; case '[': fCurrentIncrementer = 1.0/24 * BEATS_PER_MEASURE; break; case '{': fCurrentIncrementer = 1.0/64 * BEATS_PER_MEASURE; break; case '`': fCurrentIncrementer = 1.0/192 * BEATS_PER_MEASURE; break; // ends a series case ')': case ']': case '}': case '\'': case '>': fCurrentIncrementer = 1.0/8 * BEATS_PER_MEASURE; break; default: // this is a note character { if( c == '!' ) { LOG->UserLog( "Song file", path, "has an unexpected character: '!'." ); continue; } bool jump = false; if( c == '<' ) { /* Arr. Is this a jump or a 1/192 marker? */ if( Is192( sStepData, i ) ) { fCurrentIncrementer = 1.0/192 * BEATS_PER_MEASURE; break; } /* It's a jump. * We need to keep reading notes until we hit a >. */ jump = true; i++; } const int iIndex = BeatToNoteRow( (float)fCurrentBeat ); i--; do { c = sStepData[i++]; if( jump && c == '>' ) break; int iCol1, iCol2; DWIcharToNoteCol( c, (GameController)pad, iCol1, iCol2, path ); if( iCol1 != -1 ) newNoteData.SetTapNote(iCol1, iIndex, TAP_ORIGINAL_TAP); if( iCol2 != -1 ) newNoteData.SetTapNote(iCol2, iIndex, TAP_ORIGINAL_TAP); if(i>=sStepData.length()) { break; //we ran out of data //while looking for the ending > mark } if( sStepData[i] == '!' ) { i++; const char holdChar = sStepData[i++]; DWIcharToNoteCol(holdChar, (GameController)pad, iCol1, iCol2, path ); if( iCol1 != -1 ) newNoteData.SetTapNote(iCol1, iIndex, TAP_ORIGINAL_HOLD_HEAD); if( iCol2 != -1 ) newNoteData.SetTapNote(iCol2, iIndex, TAP_ORIGINAL_HOLD_HEAD); } } while( jump ); fCurrentBeat += fCurrentIncrementer; } break; } } } /* Fill in iDuration. */ for( int t=0; t<newNoteData.GetNumTracks(); ++t ) { FOREACH_NONEMPTY_ROW_IN_TRACK( newNoteData, t, iHeadRow ) { TapNote tn = newNoteData.GetTapNote( t, iHeadRow ); if( tn.type != TapNote::hold_head ) continue; int iTailRow = iHeadRow; bool bFound = false; while( !bFound && newNoteData.GetNextTapNoteRowForTrack(t, iTailRow) ) { const TapNote &TailTap = newNoteData.GetTapNote( t, iTailRow ); if( TailTap.type == TapNote::empty ) continue; newNoteData.SetTapNote( t, iTailRow, TAP_EMPTY ); tn.iDuration = iTailRow - iHeadRow; newNoteData.SetTapNote( t, iHeadRow, tn ); bFound = true; } if( !bFound ) { /* The hold was never closed. */ LOG->UserLog("Song file", path, "failed to close a hold note in \"%s\" on track %i", DifficultyToString(out.GetDifficulty()).c_str(), t); newNoteData.SetTapNote( t, iHeadRow, TAP_EMPTY ); } } }
static void WriteDWINotesField( RageFile &f, const Steps &out, int start ) { NoteData notedata; out.GetNoteData( notedata ); NoteDataUtil::InsertHoldTails( notedata ); const int iLastMeasure = int( notedata.GetLastBeat()/BEATS_PER_MEASURE ); for( int m=0; m<=iLastMeasure; m++ ) // foreach measure { NoteType nt = NoteDataUtil::GetSmallestNoteTypeForMeasure( notedata, m ); double fCurrentIncrementer = 0; switch( nt ) { case NOTE_TYPE_4TH: case NOTE_TYPE_8TH: fCurrentIncrementer = 1.0/8 * BEATS_PER_MEASURE; break; case NOTE_TYPE_12TH: case NOTE_TYPE_24TH: f.Write( "[" ); fCurrentIncrementer = 1.0/24 * BEATS_PER_MEASURE; break; case NOTE_TYPE_16TH: f.Write( "(" ); fCurrentIncrementer = 1.0/16 * BEATS_PER_MEASURE; break; case NOTE_TYPE_32ND: case NOTE_TYPE_64TH: f.Write( "{" ); fCurrentIncrementer = 1.0/64 * BEATS_PER_MEASURE; break; case NOTE_TYPE_48TH: case NOTE_TYPE_192ND: case NoteType_Invalid: // since, for whatever reason, the only way to do // 48ths is through a block of 192nds... f.Write( "`" ); fCurrentIncrementer = 1.0/192 * BEATS_PER_MEASURE; break; default: ASSERT_M(0, ssprintf("nt = %d",nt) ); break; } double fFirstBeatInMeasure = m * BEATS_PER_MEASURE; double fLastBeatInMeasure = (m+1) * BEATS_PER_MEASURE; for( double b=fFirstBeatInMeasure; b<=fLastBeatInMeasure-1/64.0f; b+=fCurrentIncrementer ) // need the -0.0001 to account for rounding errors { int row = BeatToNoteRow( (float)b ); RString str; switch( out.m_StepsType ) { case StepsType_dance_single: case StepsType_dance_couple: case StepsType_dance_double: str = NotesToDWIString( notedata.GetTapNote(start+0, row), notedata.GetTapNote(start+1, row), notedata.GetTapNote(start+2, row), notedata.GetTapNote(start+3, row) ); // Blank out the notes so we don't write them again if the incrementer is small notedata.SetTapNote(start+0, row, TAP_EMPTY); notedata.SetTapNote(start+1, row, TAP_EMPTY); notedata.SetTapNote(start+2, row, TAP_EMPTY); notedata.SetTapNote(start+3, row, TAP_EMPTY); break; case StepsType_dance_solo: str = NotesToDWIString( notedata.GetTapNote(0, row), notedata.GetTapNote(1, row), notedata.GetTapNote(2, row), notedata.GetTapNote(3, row), notedata.GetTapNote(4, row), notedata.GetTapNote(5, row) ); // Blank out the notes so we don't write them again if the incrementer is small notedata.SetTapNote(start+0, row, TAP_EMPTY); notedata.SetTapNote(start+1, row, TAP_EMPTY); notedata.SetTapNote(start+2, row, TAP_EMPTY); notedata.SetTapNote(start+3, row, TAP_EMPTY); notedata.SetTapNote(start+4, row, TAP_EMPTY); notedata.SetTapNote(start+5, row, TAP_EMPTY); break; default: ASSERT(0); // not a type supported by DWI. We shouldn't have called in here if that's the case } f.Write( str ); } switch( nt ) { case NOTE_TYPE_4TH: case NOTE_TYPE_8TH: break; case NOTE_TYPE_12TH: case NOTE_TYPE_24TH: f.Write( "]" ); break; case NOTE_TYPE_16TH: f.Write( ")" ); break; case NOTE_TYPE_32ND: case NOTE_TYPE_64TH: f.Write( "}" ); break; case NOTE_TYPE_48TH: case NOTE_TYPE_192ND: case NoteType_Invalid: f.Write( "'" ); break; default: ASSERT(0); // fall though } f.PutLine( "" ); } }
bool KSFLoader::LoadFromKSFFile( const CString &sPath, Steps &out, const Song &song ) { LOG->Trace( "Steps::LoadFromKSFFile( '%s' )", sPath.c_str() ); MsdFile msd; if( !msd.ReadFile( sPath ) ) RageException::Throw( "Error opening file '%s'.", sPath.c_str() ); int iTickCount = -1; // this is the value we read for TICKCOUNT CStringArray asRows; for( unsigned i=0; i<msd.GetNumValues(); i++ ) { const MsdFile::value_t &sParams = msd.GetValue(i); CString sValueName = sParams[0]; // handle the data if( 0==stricmp(sValueName,"TICKCOUNT") ) iTickCount = atoi(sParams[1]); else if( 0==stricmp(sValueName,"STEP") ) { CString step = sParams[1]; TrimLeft(step); split( step, "\n", asRows, true ); } else if( 0==stricmp(sValueName,"DIFFICULTY") ) out.SetMeter(atoi(sParams[1])); } if( iTickCount == -1 ) { iTickCount = 2; LOG->Warn( "\"%s\": TICKCOUNT not found; defaulting to %i", sPath.c_str(), iTickCount ); } NoteData notedata; // read it into here { CString sDir, sFName, sExt; splitpath( sPath, sDir, sFName, sExt ); sFName.MakeLower(); out.SetDescription(sFName); if( sFName.Find("crazy")!=-1 ) { out.SetDifficulty(DIFFICULTY_HARD); if(!out.GetMeter()) out.SetMeter(8); } else if( sFName.Find("hard")!=-1 ) { out.SetDifficulty(DIFFICULTY_MEDIUM); if(!out.GetMeter()) out.SetMeter(5); } else if( sFName.Find("easy")!=-1 ) { out.SetDifficulty(DIFFICULTY_EASY); if(!out.GetMeter()) out.SetMeter(2); } else { out.SetDifficulty(DIFFICULTY_MEDIUM); if(!out.GetMeter()) out.SetMeter(5); } notedata.SetNumTracks( 5 ); out.m_StepsType = STEPS_TYPE_PUMP_SINGLE; /* Check for "halfdouble" before "double". */ if( sFName.Find("halfdouble") != -1 || sFName.Find("h_double") != -1 ) { notedata.SetNumTracks( 6 ); out.m_StepsType = STEPS_TYPE_PUMP_HALFDOUBLE; } else if( sFName.Find("double") != -1 ) { notedata.SetNumTracks( 10 ); out.m_StepsType = STEPS_TYPE_PUMP_DOUBLE; } else if( sFName.Find("_2") != -1 ) { notedata.SetNumTracks( 10 ); out.m_StepsType = STEPS_TYPE_PUMP_COUPLE; } } int iHoldStartRow[13]; int t; for( t=0; t<13; t++ ) iHoldStartRow[t] = -1; for( unsigned r=0; r<asRows.size(); r++ ) { CString& sRowString = asRows[r]; StripCrnl( sRowString ); if( sRowString == "" ) continue; // skip /* All 2s indicates the end of the song. */ if( sRowString == "2222222222222" ) break; if(sRowString.size() != 13) { LOG->Warn("File %s had a RowString with an improper length (\"%s\"); corrupt notes ignored", sPath.c_str(), sRowString.c_str()); return false; } /* Half-doubles is offset; "0011111100000". */ if( out.m_StepsType == STEPS_TYPE_PUMP_HALFDOUBLE ) sRowString.erase( 0, 2 ); // the length of a note in a row depends on TICKCOUNT float fBeatThisRow = r/(float)iTickCount; int row = BeatToNoteRow(fBeatThisRow); for( int t=0; t < notedata.GetNumTracks(); t++ ) { if( sRowString[t] == '4' ) { /* Remember when each hold starts; ignore the middle. */ if( iHoldStartRow[t] == -1 ) iHoldStartRow[t] = r; continue; } if( iHoldStartRow[t] != -1 ) // this ends the hold { HoldNote hn ( t, /* button */ BeatToNoteRow(iHoldStartRow[t]/(float)iTickCount), /* start */ BeatToNoteRow((r-1)/(float)iTickCount) /* end */ ); notedata.AddHoldNote( hn ); iHoldStartRow[t] = -1; } TapNote tap; switch(sRowString[t]) { case '0': tap = TAP_EMPTY; break; case '1': tap = TAP_ORIGINAL_TAP; break; default: ASSERT(0); tap = TAP_EMPTY; break; } notedata.SetTapNote(t, row, tap); } } /* We need to remove holes where the BPM increases. */ // if( song.m_Timing.m_BPMSegments.size() > 1 ) // RemoveHoles( notedata, song ); out.SetNoteData(¬edata); out.TidyUpData(); return true; }
void BackgroundImpl::LoadFromRandom( float fFirstBeat, float fEndBeat, const BackgroundChange &change ) { int iStartRow = BeatToNoteRow(fFirstBeat); int iEndRow = BeatToNoteRow(fEndBeat); const TimingData &timing = m_pSong->m_SongTiming; // change BG every time signature change or 4 measures const vector<TimingSegment *> &tSigs = timing.GetTimingSegments(SEGMENT_TIME_SIG); for (unsigned i = 0; i < tSigs.size(); i++) { TimeSignatureSegment *ts = static_cast<TimeSignatureSegment *>(tSigs[i]); int iSegmentEndRow = (i + 1 == tSigs.size()) ? iEndRow : tSigs[i+1]->GetRow(); for(int j=max(ts->GetRow(),iStartRow); j<min(iEndRow,iSegmentEndRow); j+=4*ts->GetNoteRowsPerMeasure()) { // Don't fade. It causes frame rate dip, especially on slower machines. BackgroundDef bd = m_Layer[0].CreateRandomBGA(m_pSong, change.m_def.m_sEffect, m_RandomBGAnimations, this); if( !bd.IsEmpty() ) { BackgroundChange c = change; c.m_def = bd; c.m_fStartBeat = NoteRowToBeat(j); m_Layer[0].m_aBGChanges.push_back( c ); } } } // change BG every BPM change that is at the beginning of a measure const vector<TimingSegment *> &bpms = timing.GetTimingSegments(SEGMENT_BPM); for( unsigned i=0; i<bpms.size(); i++ ) { bool bAtBeginningOfMeasure = false; for (unsigned j=0; j<tSigs.size(); j++) { TimeSignatureSegment *ts = static_cast<TimeSignatureSegment *>(tSigs[j]); if ((bpms[i]->GetRow() - ts->GetRow()) % ts->GetNoteRowsPerMeasure() == 0) { bAtBeginningOfMeasure = true; break; } } if( !bAtBeginningOfMeasure ) continue; // skip // start so that we don't create a BGChange right on top of fEndBeat bool bInRange = bpms[i]->GetRow() >= iStartRow && bpms[i]->GetRow() < iEndRow; if( !bInRange ) continue; // skip BackgroundDef bd = m_Layer[0].CreateRandomBGA( m_pSong, change.m_def.m_sEffect, m_RandomBGAnimations, this ); if( !bd.IsEmpty() ) { BackgroundChange c = change; c.m_def.m_sFile1 = bd.m_sFile1; c.m_def.m_sFile2 = bd.m_sFile2; c.m_fStartBeat = bpms[i]->GetBeat(); m_Layer[0].m_aBGChanges.push_back( c ); } } }
NoteType BeatToNoteType( float fBeat ) { return GetNoteType( BeatToNoteRow(fBeat) ); }
/* For visibility testing: if bAbsolute is false, random modifiers must return * the minimum possible scroll speed. */ float ArrowEffects::GetYOffset( const PlayerState* pPlayerState, int iCol, float fNoteBeat, float &fPeakYOffsetOut, bool &bIsPastPeakOut, bool bAbsolute ) { // Default values that are returned if boomerang is off. fPeakYOffsetOut = FLT_MAX; bIsPastPeakOut = true; float fYOffset = 0; const SongPosition &position = pPlayerState->GetDisplayedPosition(); float fSongBeat = position.m_fSongBeatVisible; Steps *pCurSteps = GAMESTATE->m_pCurSteps[pPlayerState->m_PlayerNumber]; /* Usually, fTimeSpacing is 0 or 1, in which case we use entirely beat spacing or * entirely time spacing (respectively). Occasionally, we tween between them. */ if( curr_options->m_fTimeSpacing != 1.0f ) { if( GAMESTATE->m_bInStepEditor ) { // Use constant spacing in step editor fYOffset = fNoteBeat - fSongBeat; } else { fYOffset = GetDisplayedBeat(pPlayerState, fNoteBeat) - GetDisplayedBeat(pPlayerState, fSongBeat); fYOffset *= pCurSteps->GetTimingData()->GetDisplayedSpeedPercent( position.m_fSongBeatVisible, position.m_fMusicSecondsVisible ); } fYOffset *= 1 - curr_options->m_fTimeSpacing; } if( curr_options->m_fTimeSpacing != 0.0f ) { float fSongSeconds = GAMESTATE->m_Position.m_fMusicSecondsVisible; float fNoteSeconds = pCurSteps->GetTimingData()->GetElapsedTimeFromBeat(fNoteBeat); float fSecondsUntilStep = fNoteSeconds - fSongSeconds; float fBPM = curr_options->m_fScrollBPM; float fBPS = fBPM/60.f / GAMESTATE->m_SongOptions.GetCurrent().m_fMusicRate; float fYOffsetTimeSpacing = fSecondsUntilStep * fBPS; fYOffset += fYOffsetTimeSpacing * curr_options->m_fTimeSpacing; } // TODO: If we allow noteskins to have metricable row spacing // (per issue 24), edit this to reflect that. -aj fYOffset *= ARROW_SPACING; // Factor in scroll speed float fScrollSpeed = curr_options->m_fScrollSpeed; if(curr_options->m_fMaxScrollBPM != 0) { fScrollSpeed= curr_options->m_fMaxScrollBPM / (pPlayerState->m_fReadBPM * GAMESTATE->m_SongOptions.GetCurrent().m_fMusicRate); } // don't mess with the arrows after they've crossed 0 if( fYOffset < 0 ) { return fYOffset * fScrollSpeed; } const float* fAccels = curr_options->m_fAccels; //const float* fEffects = curr_options->m_fEffects; float fYAdjust = 0; // fill this in depending on PlayerOptions if( fAccels[PlayerOptions::ACCEL_BOOST] != 0 ) { float fEffectHeight = GetNoteFieldHeight(); float fNewYOffset = fYOffset * 1.5f / ((fYOffset+fEffectHeight/1.2f)/fEffectHeight); float fAccelYAdjust = fAccels[PlayerOptions::ACCEL_BOOST] * (fNewYOffset - fYOffset); // TRICKY: Clamp this value, or else BOOST+BOOMERANG will draw a ton of arrows on the screen. CLAMP( fAccelYAdjust, BOOST_MOD_MIN_CLAMP, BOOST_MOD_MAX_CLAMP ); fYAdjust += fAccelYAdjust; } if( fAccels[PlayerOptions::ACCEL_BRAKE] != 0 ) { float fEffectHeight = GetNoteFieldHeight(); float fScale = SCALE( fYOffset, 0.f, fEffectHeight, 0, 1.f ); float fNewYOffset = fYOffset * fScale; float fBrakeYAdjust = fAccels[PlayerOptions::ACCEL_BRAKE] * (fNewYOffset - fYOffset); // TRICKY: Clamp this value the same way as BOOST so that in BOOST+BRAKE, BRAKE doesn't overpower BOOST CLAMP( fBrakeYAdjust, BRAKE_MOD_MIN_CLAMP, BRAKE_MOD_MAX_CLAMP ); fYAdjust += fBrakeYAdjust; } if( fAccels[PlayerOptions::ACCEL_WAVE] != 0 ) fYAdjust += fAccels[PlayerOptions::ACCEL_WAVE] * WAVE_MOD_MAGNITUDE *RageFastSin( fYOffset/WAVE_MOD_HEIGHT ); fYOffset += fYAdjust; // Factor in boomerang if( fAccels[PlayerOptions::ACCEL_BOOMERANG] != 0 ) { float fPeakAtYOffset = SCREEN_HEIGHT * BOOMERANG_PEAK_PERCENTAGE; // zero point of boomerang function fPeakYOffsetOut = (-1*fPeakAtYOffset*fPeakAtYOffset/SCREEN_HEIGHT) + 1.5f*fPeakAtYOffset; bIsPastPeakOut = fYOffset < fPeakAtYOffset; fYOffset = (-1*fYOffset*fYOffset/SCREEN_HEIGHT) + 1.5f*fYOffset; } if( curr_options->m_fRandomSpeed > 0 && !bAbsolute ) { // Generate a deterministically "random" speed for each arrow. unsigned seed = GAMESTATE->m_iStageSeed + ( BeatToNoteRow( fNoteBeat ) << 8 ) + (iCol * 100); for( int i = 0; i < 3; ++i ) seed = ((seed * 1664525u) + 1013904223u) & 0xFFFFFFFF; float fRandom = seed / 4294967296.0f; /* Random speed always increases speed: a random speed of 10 indicates * [1,11]. This keeps it consistent with other mods: 0 means no effect. */ fScrollSpeed *= SCALE( fRandom, 0.0f, 1.0f, 1.0f, curr_options->m_fRandomSpeed + 1.0f ); } if( fAccels[PlayerOptions::ACCEL_EXPAND] != 0 ) { // TODO: Don't index by PlayerNumber. PerPlayerData &data = g_EffectData[pPlayerState->m_PlayerNumber]; float fExpandMultiplier = SCALE( RageFastCos(data.m_fExpandSeconds*EXPAND_MULTIPLIER_FREQUENCY), EXPAND_MULTIPLIER_SCALE_FROM_LOW, EXPAND_MULTIPLIER_SCALE_FROM_HIGH, EXPAND_MULTIPLIER_SCALE_TO_LOW, EXPAND_MULTIPLIER_SCALE_TO_HIGH ); fScrollSpeed *= SCALE( fAccels[PlayerOptions::ACCEL_EXPAND], EXPAND_SPEED_SCALE_FROM_LOW, EXPAND_SPEED_SCALE_FROM_HIGH, EXPAND_SPEED_SCALE_TO_LOW, fExpandMultiplier ); } fYOffset *= fScrollSpeed; fPeakYOffsetOut *= fScrollSpeed; return fYOffset; }
static bool LoadFromKSFFile( const RString &sPath, Steps &out, Song &song, bool bKIUCompliant ) { LOG->Trace( "Steps::LoadFromKSFFile( '%s' )", sPath.c_str() ); MsdFile msd; if( !msd.ReadFile( sPath, false ) ) // don't unescape { LOG->UserLog( "Song file", sPath, "couldn't be opened: %s", msd.GetError().c_str() ); return false; } // this is the value we read for TICKCOUNT int iTickCount = -1; // used to adapt weird tickcounts //float fScrollRatio = 1.0f; -- uncomment when ready to use. vector<RString> vNoteRows; // According to Aldo_MX, there is a default BPM and it's 60. -aj bool bDoublesChart = false; TimingData stepsTiming; float SMGap1 = 0, SMGap2 = 0, BPM1 = -1, BPMPos2 = -1, BPM2 = -1, BPMPos3 = -1, BPM3 = -1; for( unsigned i=0; i<msd.GetNumValues(); i++ ) { const MsdFile::value_t &sParams = msd.GetValue( i ); RString sValueName = sParams[0]; sValueName.MakeUpper(); /* handle the data...well, not this data: not related to steps. * Skips INTRO, MUSICINTRO, TITLEFILE, DISCFILE, SONGFILE. */ if (sValueName=="TITLE" || EndsWith(sValueName, "INTRO") || EndsWith(sValueName, "FILE") ) { } else if( sValueName=="BPM" ) { BPM1 = StringToFloat(sParams[1]); stepsTiming.AddSegment( BPMSegment(0, BPM1) ); } else if( sValueName=="BPM2" ) { if (bKIUCompliant) { BPM2 = StringToFloat( sParams[1] ); } else { // LOG an error. } } else if( sValueName=="BPM3" ) { if (bKIUCompliant) { BPM3 = StringToFloat( sParams[1] ); } else { // LOG an error. } } else if( sValueName=="BUNKI" ) { if (bKIUCompliant) { BPMPos2 = StringToFloat( sParams[1] ) / 100.0f; } else { // LOG an error. } } else if( sValueName=="BUNKI2" ) { if (bKIUCompliant) { BPMPos3 = StringToFloat( sParams[1] ) / 100.0f; } else { // LOG an error. } } else if( sValueName=="STARTTIME" ) { SMGap1 = -StringToFloat( sParams[1] )/100; stepsTiming.m_fBeat0OffsetInSeconds = SMGap1; } // This is currently required for more accurate KIU BPM changes. else if( sValueName=="STARTTIME2" ) { if (bKIUCompliant) { SMGap2 = -StringToFloat( sParams[1] )/100; } else { // LOG an error. } } else if ( sValueName=="STARTTIME3" ) { // STARTTIME3 only ensures this is a KIU compliant simfile. bKIUCompliant = true; } else if( sValueName=="TICKCOUNT" ) { iTickCount = StringToInt( sParams[1] ); if( iTickCount <= 0 ) { LOG->UserLog( "Song file", sPath, "has an invalid tick count: %d.", iTickCount ); return false; } stepsTiming.AddSegment( TickcountSegment(0, iTickCount)); } else if( sValueName=="DIFFICULTY" ) { out.SetMeter( max(StringToInt(sParams[1]), 1) ); } // new cases from Aldo_MX's fork: else if( sValueName=="PLAYER" ) { RString sPlayer = sParams[1]; sPlayer.MakeLower(); if( sPlayer.find( "double" ) != string::npos ) bDoublesChart = true; } // This should always be last. else if( sValueName=="STEP" ) { RString theSteps = sParams[1]; TrimLeft( theSteps ); split( theSteps, "\n", vNoteRows, true ); } } if( iTickCount == -1 ) { iTickCount = 4; LOG->UserLog( "Song file", sPath, "doesn't have a TICKCOUNT. Defaulting to %i.", iTickCount ); } // Prepare BPM stuff already if the file uses KSF syntax. if( bKIUCompliant ) { if( BPM2 > 0 && BPMPos2 > 0 ) { HandleBunki( stepsTiming, BPM1, BPM2, SMGap1, BPMPos2 ); } if( BPM3 > 0 && BPMPos3 > 0 ) { HandleBunki( stepsTiming, BPM2, BPM3, SMGap2, BPMPos3 ); } } NoteData notedata; // read it into here { RString sDir, sFName, sExt; splitpath( sPath, sDir, sFName, sExt ); sFName.MakeLower(); out.SetDescription(sFName); // Check another before anything else... is this okay? -DaisuMaster if( sFName.find("another") != string::npos ) { out.SetDifficulty( Difficulty_Edit ); if( !out.GetMeter() ) out.SetMeter( 25 ); } else if(sFName.find("wild") != string::npos || sFName.find("wd") != string::npos || sFName.find("crazy+") != string::npos || sFName.find("cz+") != string::npos || sFName.find("hardcore") != string::npos ) { out.SetDifficulty( Difficulty_Challenge ); if( !out.GetMeter() ) out.SetMeter( 20 ); } else if(sFName.find("crazy") != string::npos || sFName.find("cz") != string::npos || sFName.find("nightmare") != string::npos || sFName.find("nm") != string::npos || sFName.find("crazydouble") != string::npos ) { out.SetDifficulty( Difficulty_Hard ); if( !out.GetMeter() ) out.SetMeter( 14 ); // Set the meters to the Pump scale, not DDR. } else if(sFName.find("hard") != string::npos || sFName.find("hd") != string::npos || sFName.find("freestyle") != string::npos || sFName.find("fs") != string::npos || sFName.find("double") != string::npos ) { out.SetDifficulty( Difficulty_Medium ); if( !out.GetMeter() ) out.SetMeter( 8 ); } else if(sFName.find("easy") != string::npos || sFName.find("ez") != string::npos || sFName.find("normal") != string::npos ) { // I wonder if I should leave easy fall into the Beginner difficulty... -DaisuMaster out.SetDifficulty( Difficulty_Easy ); if( !out.GetMeter() ) out.SetMeter( 4 ); } else if(sFName.find("beginner") != string::npos || sFName.find("practice") != string::npos || sFName.find("pr") != string::npos ) { out.SetDifficulty( Difficulty_Beginner ); if( !out.GetMeter() ) out.SetMeter( 4 ); } else { out.SetDifficulty( Difficulty_Hard ); if( !out.GetMeter() ) out.SetMeter( 10 ); } out.m_StepsType = StepsType_pump_single; // Check for "halfdouble" before "double". if(sFName.find("halfdouble") != string::npos || sFName.find("half-double") != string::npos || sFName.find("h_double") != string::npos || sFName.find("hdb") != string::npos ) out.m_StepsType = StepsType_pump_halfdouble; // Handle bDoublesChart from above as well. -aj else if(sFName.find("double") != string::npos || sFName.find("nightmare") != string::npos || sFName.find("freestyle") != string::npos || sFName.find("db") != string::npos || sFName.find("nm") != string::npos || sFName.find("fs") != string::npos || bDoublesChart ) out.m_StepsType = StepsType_pump_double; else if( sFName.find("_1") != string::npos ) out.m_StepsType = StepsType_pump_single; else if( sFName.find("_2") != string::npos ) out.m_StepsType = StepsType_pump_couple; } switch( out.m_StepsType ) { case StepsType_pump_single: notedata.SetNumTracks( 5 ); break; case StepsType_pump_couple: notedata.SetNumTracks( 10 ); break; case StepsType_pump_double: notedata.SetNumTracks( 10 ); break; case StepsType_pump_routine: notedata.SetNumTracks( 10 ); break; // future files may have this? case StepsType_pump_halfdouble: notedata.SetNumTracks( 6 ); break; default: FAIL_M( ssprintf("%i", out.m_StepsType) ); } int t = 0; int iHoldStartRow[13]; for( t=0; t<13; t++ ) iHoldStartRow[t] = -1; bool bTickChangeNeeded = false; int newTick = -1; float fCurBeat = 0.0f; float prevBeat = 0.0f; // Used for hold tails. for( unsigned r=0; r<vNoteRows.size(); r++ ) { RString& sRowString = vNoteRows[r]; StripCrnl( sRowString ); if( sRowString == "" ) continue; // skip // All 2s indicates the end of the song. else if( sRowString == "2222222222222" ) { // Finish any holds that didn't get...well, finished. for( t=0; t < notedata.GetNumTracks(); t++ ) { if( iHoldStartRow[t] != -1 ) // this ends the hold { if( iHoldStartRow[t] == BeatToNoteRow(prevBeat) ) notedata.SetTapNote( t, iHoldStartRow[t], TAP_ORIGINAL_TAP ); else notedata.AddHoldNote(t, iHoldStartRow[t], BeatToNoteRow(prevBeat), TAP_ORIGINAL_HOLD_HEAD ); } } /* have this row be the last moment in the song, unless * a future step ends later. */ //float curTime = stepsTiming.GetElapsedTimeFromBeat(fCurBeat); //if (curTime > song.GetSpecifiedLastSecond()) //{ // song.SetSpecifiedLastSecond(curTime); //} song.SetSpecifiedLastSecond( song.GetSpecifiedLastSecond() + 4 ); break; } else if( BeginsWith(sRowString, "|") ) { /* if (bKIUCompliant) { // Log an error, ignore the line. continue; } */ // gotta do something tricky here: if the bpm is below one then a couple of calculations // for scrollsegments will be made, example, bpm 0.2, tick 4000, the scrollsegment will // be 0. if the tickcount is non a stepmania standard then it will be adapted, a scroll // segment will then be added based on approximations. -DaisuMaster // eh better do it considering the tickcount (high tickcounts) // I'm making some experiments, please spare me... //continue; RString temp = sRowString.substr(2,sRowString.size()-3); float numTemp = StringToFloat(temp); if (BeginsWith(sRowString, "|T")) { // duh iTickCount = static_cast<int>(numTemp); // I have been owned by the man -DaisuMaster stepsTiming.SetTickcountAtBeat( fCurBeat, clamp(iTickCount, 0, ROWS_PER_BEAT) ); } else if (BeginsWith(sRowString, "|B")) { // BPM stepsTiming.SetBPMAtBeat( fCurBeat, numTemp ); } else if (BeginsWith(sRowString, "|E")) { // DelayBeat float fCurDelay = 60 / stepsTiming.GetBPMAtBeat(fCurBeat) * numTemp / iTickCount; fCurDelay += stepsTiming.GetDelayAtRow(BeatToNoteRow(fCurBeat) ); stepsTiming.SetDelayAtBeat( fCurBeat, fCurDelay ); } else if (BeginsWith(sRowString, "|D")) { // Delays float fCurDelay = stepsTiming.GetStopAtRow(BeatToNoteRow(fCurBeat) ); fCurDelay += numTemp / 1000; stepsTiming.SetDelayAtBeat( fCurBeat, fCurDelay ); } else if (BeginsWith(sRowString, "|M") || BeginsWith(sRowString, "|C")) { // multipliers/combo ComboSegment seg( BeatToNoteRow(fCurBeat), int(numTemp) ); stepsTiming.AddSegment( seg ); } else if (BeginsWith(sRowString, "|S")) { // speed segments } else if (BeginsWith(sRowString, "|F")) { // fakes } else if (BeginsWith(sRowString, "|X")) { // scroll segments ScrollSegment seg = ScrollSegment( BeatToNoteRow(fCurBeat), numTemp ); stepsTiming.AddSegment( seg ); //return true; } continue; } // Half-doubles is offset; "0011111100000". if( out.m_StepsType == StepsType_pump_halfdouble ) sRowString.erase( 0, 2 ); // Update TICKCOUNT for Direct Move files. if( bTickChangeNeeded ) { iTickCount = newTick; bTickChangeNeeded = false; } for( t=0; t < notedata.GetNumTracks(); t++ ) { if( sRowString[t] == '4' ) { /* Remember when each hold starts; ignore the middle. */ if( iHoldStartRow[t] == -1 ) iHoldStartRow[t] = BeatToNoteRow(fCurBeat); continue; } if( iHoldStartRow[t] != -1 ) // this ends the hold { int iEndRow = BeatToNoteRow(prevBeat); if( iHoldStartRow[t] == iEndRow ) notedata.SetTapNote( t, iHoldStartRow[t], TAP_ORIGINAL_TAP ); else { //notedata.AddHoldNote( t, iHoldStartRow[t], iEndRow , TAP_ORIGINAL_PUMP_HEAD ); notedata.AddHoldNote( t, iHoldStartRow[t], iEndRow , TAP_ORIGINAL_HOLD_HEAD ); } iHoldStartRow[t] = -1; } TapNote tap; switch( sRowString[t] ) { case '0': tap = TAP_EMPTY; break; case '1': tap = TAP_ORIGINAL_TAP; break; //allow setting more notetypes on ksf files, this may come in handy (it should) -DaisuMaster case 'M': case 'm': tap = TAP_ORIGINAL_MINE; break; case 'F': case 'f': tap = TAP_ORIGINAL_FAKE; break; case 'L': case 'l': tap = TAP_ORIGINAL_LIFT; break; default: LOG->UserLog( "Song file", sPath, "has an invalid row \"%s\"; corrupt notes ignored.", sRowString.c_str() ); //return false; tap = TAP_EMPTY; break; } notedata.SetTapNote(t, BeatToNoteRow(fCurBeat), tap); } prevBeat = fCurBeat; fCurBeat = prevBeat + 1.0f / iTickCount; } out.SetNoteData( notedata ); out.m_Timing = stepsTiming; out.TidyUpData(); out.SetSavedToDisk( true ); // we're loading from disk, so this is by definintion already saved return true; }