#include "global.h" #include "NotesLoaderKSF.h" #include "RageException.h" #include "RageUtil_CharConversions.h" #include "MsdFile.h" #include "RageLog.h" #include "RageUtil.h" #include "NoteData.h" #include "NoteTypes.h" #include "song.h" #include "Steps.h" /* Global variables aren't the best way to go about this, but this * will be required to start implementing the Direct Move syntax. */ #if 0 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, MAX_NOTE_ROW ); out.ClearRange( FromRow, MAX_NOTE_ROW ); out.CopyRange( &tmp, 0, MAX_NOTE_ROW, ToRow ); } /* 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 ); } } */ } #endif bool KSFLoader::LoadFromKSFFile( const RString &sPath, Steps &out, const Song &song ) { LOG->Trace( "Steps::LoadFromKSFFile( '%s' )", sPath.c_str() ); MsdFile msd; if( !msd.ReadFile( sPath ) ) { LOG->UserLog( "Error opening file '%s'.", sPath.c_str() ); return false; } m_iTickCount = -1; // this is the value we read for TICKCOUNT m_vNoteRows.clear(); //Start from a clean slate. for( unsigned i=0; iUserLog( "KSF file \"%s\" has an invalid tick count: %d", sPath.c_str(), m_iTickCount ); return false; } } else if( 0==stricmp(sValueName,"STEP") ) { RString theSteps = sParams[1]; TrimLeft( theSteps ); split( theSteps, "\n", m_vNoteRows, true ); } else if( 0==stricmp(sValueName,"DIFFICULTY") ) { out.SetMeter( max(atoi(sParams[1]), 0) ); } } if( m_iTickCount == -1 ) { m_iTickCount = 2; LOG->UserLog( "\"%s\": TICKCOUNT not found; defaulting to %i", sPath.c_str(), m_iTickCount ); } NoteData notedata; // read it into here { RString sDir, sFName, sExt; splitpath( sPath, sDir, sFName, sExt ); sFName.MakeLower(); out.SetDescription(sFName); if( sFName.find("crazy") != string::npos || sFName.find("nightmare") != 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("freestyle") != 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("normal") != string::npos ) { out.SetDifficulty( DIFFICULTY_EASY ); if( !out.GetMeter() ) out.SetMeter( 4 ); } else { out.SetDifficulty( DIFFICULTY_MEDIUM ); if( !out.GetMeter() ) out.SetMeter( 8 ); } out.m_StepsType = STEPS_TYPE_PUMP_SINGLE; /* Check for "halfdouble" before "double". */ if( sFName.find("halfdouble") != string::npos || sFName.find("h_double") != string::npos ) out.m_StepsType = STEPS_TYPE_PUMP_HALFDOUBLE; else if( sFName.find("double") != string::npos || sFName.find("nightmare") != string::npos ) out.m_StepsType = STEPS_TYPE_PUMP_DOUBLE; else if( sFName.find("_1") != string::npos ) out.m_StepsType = STEPS_TYPE_PUMP_SINGLE; else if( sFName.find("_2") != string::npos ) out.m_StepsType = STEPS_TYPE_PUMP_COUPLE; } switch( out.m_StepsType ) { case STEPS_TYPE_PUMP_SINGLE: notedata.SetNumTracks( 5 ); break; case STEPS_TYPE_PUMP_COUPLE: notedata.SetNumTracks( 10 ); break; case STEPS_TYPE_PUMP_DOUBLE: notedata.SetNumTracks( 10 ); break; case STEPS_TYPE_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; m_bTickChangeNeeded = false; int newTick = -1; m_fCurBeat = 0.0f; float prevBeat = 0.0f; // Used for hold tails. for( unsigned r=0; rUserLog( "File \"%s\" had illegal syntax (\"%s\") which can't be in KIU complient files.", sPath.c_str(), sRowString.c_str() ); return false; } if( BeginsWith(sRowString, "|B") || BeginsWith(sRowString, "|D") ) { // These don't have to be worried about here: the changes and stops were already added. continue; } if ( BeginsWith(sRowString, "|T") ) { RString temp = sRowString.substr(2,sRowString.size()-3); newTick = atoi(temp); m_bTickChangeNeeded = true; continue; } else { LOG->UserLog( "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 ); // Update TICKCOUNT for Direct Move files. if( m_bTickChangeNeeded ) { m_iTickCount = newTick; m_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(m_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; default: LOG->UserLog( "File %s had an invalid row (\"%s\"); corrupt notes ignored", sPath.c_str(), sRowString.c_str() ); return false; } notedata.SetTapNote(t, BeatToNoteRow(m_fCurBeat), tap); } prevBeat = m_fCurBeat; m_fCurBeat = prevBeat + 1.0f / m_iTickCount; } /* We need to remove holes where the BPM increases. */ // if( song.m_Timing.m_BPMSegments.size() > 1 ) // RemoveHoles( notedata, song ); out.SetNoteData( notedata ); out.TidyUpData(); return true; } void KSFLoader::GetApplicableFiles( const RString &sPath, vector &out ) { GetDirListing( sPath + RString("*.ksf"), out ); } void KSFLoader::LoadTags( const RString &str, Song &out ) { /* str is either a #TITLE or a directory component. Fill in missing information. * str is either "title", "artist - title", or "artist - title - difficulty". */ vector asBits; split( str, " - ", asBits, false ); /* Ignore the difficulty, since we get that elsewhere. */ if( asBits.size() == 3 && ( !stricmp(asBits[2], "double") || !stricmp(asBits[2], "easy") || !stricmp(asBits[2], "normal") || !stricmp(asBits[2], "hard") || !stricmp(asBits[2], "crazy") || !stricmp(asBits[2], "nightmare")) ) { asBits.erase( asBits.begin()+2, asBits.begin()+3 ); } RString title, artist; if( asBits.size() == 2 ) { artist = asBits[0]; title = asBits[1]; } else if( asBits.size() == 1 ) { title = asBits[0]; } /* Convert, if possible. Most KSFs are in Korean encodings (CP942/EUC-KR). */ if( !ConvertString( title, "korean" ) ) title = ""; if( !ConvertString( artist, "korean" ) ) artist = ""; if( out.m_sMainTitle == "" ) out.m_sMainTitle = title; if( out.m_sArtist == "" ) out.m_sArtist = artist; } bool KSFLoader::LoadGlobalData( const RString &sPath, Song &out ) { MsdFile msd; if( !msd.ReadFile( sPath ) ) { LOG->UserLog( "Error opening file \"%s\": %s", sPath.c_str(), msd.GetError().c_str() ); return false; } float SMGap1 = 0, SMGap2 = 0, BPM1 = -1, BPMPos2 = -1, BPM2 = -1, BPMPos3 = -1, BPM3 = -1; m_iTickCount = -1; m_bKIUCompliant = false; m_vNoteRows.clear(); for( unsigned i=0; i < msd.GetNumValues(); i++ ) { const MsdFile::value_t &sParams = msd.GetValue(i); RString sValueName = sParams[0]; // handle the data if( 0==stricmp(sValueName,"TITLE") ) LoadTags(sParams[1], out); else if( 0==stricmp(sValueName,"BPM") ) { BPM1 = StringToFloat(sParams[1]); out.AddBPMSegment( BPMSegment(0, BPM1) ); } else if( 0==stricmp(sValueName,"BPM2") ) { m_bKIUCompliant = true; BPM2 = StringToFloat( sParams[1] ); } else if( 0==stricmp(sValueName,"BPM3") ) { m_bKIUCompliant = true; BPM3 = StringToFloat( sParams[1] ); } else if( 0==stricmp(sValueName,"BUNKI") ) { m_bKIUCompliant = true; BPMPos2 = StringToFloat( sParams[1] ) / 100.0f; } else if( 0==stricmp(sValueName,"BUNKI2") ) { m_bKIUCompliant = true; BPMPos3 = StringToFloat( sParams[1] ) / 100.0f; } else if( 0==stricmp(sValueName,"STARTTIME") ) { SMGap1 = -StringToFloat( sParams[1] )/100; out.m_Timing.m_fBeat0OffsetInSeconds = SMGap1; } // This is currently required for more accurate KIU BPM changes. else if( 0==stricmp(sValueName,"STARTTIME2") ) { m_bKIUCompliant = true; SMGap2 = -StringToFloat( sParams[1] )/100; } else if ( 0==stricmp(sValueName,"STARTTIME3") ) { // STARTTIME3 only ensures this is a KIU complient simfile. m_bKIUCompliant = true; } else if ( 0==stricmp(sValueName,"TICKCOUNT") ) { /* TICKCOUNT is will be used below if there are DM complient BPM changes and stops. * It will be called again in LoadFromKSFFile for the actual steps. */ m_iTickCount = atoi( sParams[1] ); m_iTickCount = m_iTickCount > 0 ? m_iTickCount : 2; } else if ( 0==stricmp(sValueName,"STEP") ) { /* STEP will always be the last header in a KSF file by design. Due to * the Direct Move syntax, it is best to get the rows of notes here. */ RString theSteps = sParams[1]; TrimLeft( theSteps ); split( theSteps, "\n", m_vNoteRows, true ); } else if( 0==stricmp(sValueName,"DIFFICULTY")) { /* DIFFICULTY is handled only in LoadFromKSFFile. Ignore it here. */ continue; } else { LOG->UserLog( "In KSF file \"%s\", there was an unexpected value named '%s'", sPath.c_str(), sValueName.c_str() ); } } /* BPM Change checks are done here. If m_bKIUCompliant, it's short and sweet. * Otherwise, the whole file has to be processed. Right now, this is only * called once, for the initial file (often the Crazy steps). Hopefully that * will end up changing soon. */ if( m_bKIUCompliant ) { if( BPM2 > 0 && BPMPos2 > 0 ) { const float BeatsPerSecond = BPM1 / 60.0f; const float beat = (BPMPos2 + SMGap1) * BeatsPerSecond; LOG->Trace( "BPM %f, BPS %f, BPMPos2 %f, beat %f", BPM1, BeatsPerSecond, BPMPos2, beat ); out.AddBPMSegment( BPMSegment(BeatToNoteRow(beat), BPM2) ); } if( BPM3 > 0 && BPMPos3 > 0 ) { const float BeatsPerSecond = BPM2 / 60.0f; //The line below isn't perfect, but works better than previous versions. const float beat = (BPMPos3 + SMGap2) * BeatsPerSecond; LOG->Trace( "BPM %f, BPS %f, BPMPos3 %f, beat %f", BPM2, BeatsPerSecond, BPMPos3, beat ); out.AddBPMSegment( BPMSegment(BeatToNoteRow(beat), BPM3) ); } } else { int tickToChange = m_iTickCount; m_fCurBeat = 0.0f; float speedToChange = 0.0f, timeToStop = 0.0f; m_bDMRequired = m_bBPMChangeNeeded = m_bBPMStopNeeded = m_bTickChangeNeeded = false; for (unsigned i=0; i < m_vNoteRows.size(); i++) { RString& NoteRowString = m_vNoteRows[i]; StripCrnl( NoteRowString ); if( NoteRowString == "" ) continue; // Empty rows do us no good. if( NoteRowString == "2222222222222" ) // Row of 2s = end. Confirm KIUCompliency here. { if (!m_bDMRequired) m_bKIUCompliant = true; break; } /* This is where the DMRequired test will take place. */ if( NoteRowString.size() != 13) { if (BeginsWith(NoteRowString, "|T") || BeginsWith(NoteRowString, "|B") || BeginsWith(NoteRowString, "|D") ) { m_bDMRequired = true; RString temp = NoteRowString.substr(2,NoteRowString.size()-3); float numTemp = StringToFloat(temp); if (BeginsWith(NoteRowString, "|T")) { m_bTickChangeNeeded = true; tickToChange = (int)numTemp; continue; } else if (BeginsWith(NoteRowString, "|B")) { m_bBPMChangeNeeded = true; speedToChange = numTemp; continue; } else { m_bBPMStopNeeded = true; timeToStop = numTemp / 1000.0f; continue; } } else { /* Quit while we're ahead if any bad syntax is spotted. */ LOG->UserLog( "File %s has a RowString which matches no known syntax rules (\"%s\"); bad file", sPath.c_str(), NoteRowString.c_str() ); return false; } } if( m_bTickChangeNeeded ) { m_iTickCount = tickToChange; m_bTickChangeNeeded = false; } if( m_bBPMChangeNeeded ) { LOG->Trace( "Adding tempo change of %f BPM at beat %f", speedToChange, m_fCurBeat ); out.AddBPMSegment( BPMSegment(BeatToNoteRow(m_fCurBeat), speedToChange) ); m_bBPMChangeNeeded = false; } if( m_bBPMStopNeeded ) { LOG->Trace( "Adding tempo freeze of %f seconds at beat %f", timeToStop, m_fCurBeat ); out.AddStopSegment( StopSegment(BeatToNoteRow(m_fCurBeat),timeToStop) ); m_bBPMStopNeeded = false; } m_fCurBeat += 1.0f / m_iTickCount; } } /* Try to fill in missing bits of information from the pathname. */ { vector asBits; split( sPath, "/", asBits, true); ASSERT( asBits.size() > 1 ); LoadTags( asBits[asBits.size()-2], out ); } // search for music with song in the file name vector arrayPossibleMusic; GetDirListing( out.GetSongDir() + RString("song.mp3"), arrayPossibleMusic ); GetDirListing( out.GetSongDir() + RString("song.ogg"), arrayPossibleMusic ); GetDirListing( out.GetSongDir() + RString("song.wav"), arrayPossibleMusic ); if( !arrayPossibleMusic.empty() ) // we found a match out.m_sMusicFile = arrayPossibleMusic[0]; return true; } bool KSFLoader::LoadFromDir( const RString &sDir, Song &out ) { LOG->Trace( "Song::LoadFromKSFDir(%s)", sDir.c_str() ); vector arrayKSFFileNames; GetDirListing( sDir + RString("*.ksf"), arrayKSFFileNames ); /* We shouldn't have been called to begin with if there were no KSFs. */ if( arrayKSFFileNames.empty() ) { LOG->UserLog( "Couldn't find any KSF files in '%s'", sDir.c_str() ); return false; } /* If only the first file is read, it will cause problems for other simfiles with * different BPM changes and tickcounts. This command will probably have to be * changed in the future. */ if( !LoadGlobalData(out.GetSongDir() + arrayKSFFileNames[0], out) ) return false; // load the Steps from the rest of the KSF files for( unsigned i=0; i