Change hold note contents to rows (from beats). This makes it consistent with tap note data. Row numbers are also generally more precise, since they're integers, not floats.
371 lines
10 KiB
C++
371 lines
10 KiB
C++
#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"
|
|
|
|
#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_BPMSegments.size(); ++seg )
|
|
{
|
|
// const float FromBeat = song.m_BPMSegments[seg].m_fStartBeat;
|
|
const float FromBeat = song.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_BPMSegments[seg].m_fStartBeat);
|
|
|
|
LOG->Trace("from %f (%i) to (%i)", FromBeat, FromRow, ToRow);
|
|
// const int ToRow = (int) roundf(FromRow * song.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_BPMSegments().size())
|
|
// LastRow = (int) NoteRowToBeat( song.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_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();
|
|
*/
|
|
}
|
|
#endif
|
|
|
|
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;
|
|
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;
|
|
} else if( sFName.Find("halfdouble") != -1 ) {
|
|
notedata.SetNumTracks( 6 );
|
|
out.m_StepsType = STEPS_TYPE_PUMP_HALFDOUBLE;
|
|
}
|
|
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
// 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_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_BPMSegments.size() > 1 )
|
|
// RemoveHoles( notedata, song );
|
|
|
|
out.SetNoteData(¬edata);
|
|
|
|
out.TidyUpData();
|
|
|
|
return true;
|
|
}
|
|
|
|
void KSFLoader::GetApplicableFiles( CString sPath, CStringArray &out )
|
|
{
|
|
GetDirListing( sPath + CString("*.ksf"), out );
|
|
}
|
|
|
|
void KSFLoader::LoadTags( const CString &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". */
|
|
CStringArray 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")) )
|
|
{
|
|
asBits.erase(asBits.begin()+2, asBits.begin()+3);
|
|
}
|
|
|
|
CString title, artist;
|
|
if( asBits.size() == 2 )
|
|
{
|
|
artist = asBits[0];
|
|
title = asBits[1];
|
|
}
|
|
else
|
|
{
|
|
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 CString &sPath, Song &out )
|
|
{
|
|
MsdFile msd;
|
|
bool bResult = msd.ReadFile( sPath );
|
|
if( !bResult )
|
|
RageException::Throw( "Error opening file '%s'.", sPath.c_str() );
|
|
|
|
float BPMPos2 = -1, BPM2 = -1, BPMPos3 = -1, BPM3 = -1;;
|
|
|
|
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,"TITLE") )
|
|
LoadTags(sParams[1], out);
|
|
else if( 0==stricmp(sValueName,"BPM") )
|
|
out.AddBPMSegment( BPMSegment(0, (float)atof(sParams[1])) );
|
|
else if( 0==stricmp(sValueName,"BPM2") )
|
|
BPM2 = float(atof(sParams[1]));
|
|
else if( 0==stricmp(sValueName,"BPM3") )
|
|
BPM3 = float(atof(sParams[1]));
|
|
else if( 0==stricmp(sValueName,"BUNKI") )
|
|
BPMPos2 = float(atof(sParams[1])) / 100.0f;
|
|
else if( 0==stricmp(sValueName,"BUNKI2") )
|
|
BPMPos3 = float(atof(sParams[1])) / 100.0f;
|
|
else if( 0==stricmp(sValueName,"STARTTIME") )
|
|
out.m_fBeat0OffsetInSeconds = -(float)atof(sParams[1])/100;
|
|
else if( 0==stricmp(sValueName,"TICKCOUNT") ||
|
|
0==stricmp(sValueName,"STEP") ||
|
|
0==stricmp(sValueName,"DIFFICULTY"))
|
|
; /* Handled in LoadFromKSFFile; don't warn. */
|
|
else
|
|
LOG->Trace( "Unexpected value named '%s'", sValueName.c_str() );
|
|
}
|
|
|
|
/* This doesn't work yet: we also need to move the data around, I think, and
|
|
* we should handle more than one BPM change. */
|
|
if( BPM2 > 0 && BPMPos2 > 0 )
|
|
{
|
|
const float BeatsPerSecond = out.GetBPMAtBeat(0) / 60.0f;
|
|
const float beat = BPMPos2 * BeatsPerSecond;
|
|
LOG->Trace("BPM %f, BPS %f, BPMPos2 %f, beat %f",
|
|
out.GetBPMAtBeat(0), BeatsPerSecond, BPMPos2, beat);
|
|
out.AddBPMSegment( BPMSegment(beat, BPM2) );
|
|
}
|
|
|
|
if( BPM3 > 0 && BPMPos3 > 0 )
|
|
{
|
|
const float BeatsPerSecond = out.GetBPMAtBeat(0) / 60.0f;
|
|
const float beat = BPMPos3 * BeatsPerSecond;
|
|
LOG->Trace("BPM %f, BPS %f, BPMPos3 %f, beat %f",
|
|
out.GetBPMAtBeat(0), BeatsPerSecond, BPMPos3, beat);
|
|
out.AddBPMSegment( BPMSegment(beat, BPM3) );
|
|
}
|
|
|
|
/* Try to fill in missing bits of information from the pathname. */
|
|
{
|
|
CStringArray asBits;
|
|
split( sPath, "/", asBits, true);
|
|
|
|
ASSERT(asBits.size() > 1);
|
|
LoadTags(asBits[asBits.size()-2], out);
|
|
}
|
|
|
|
// search for music with song in the file name
|
|
CStringArray arrayPossibleMusic;
|
|
GetDirListing( out.GetSongDir() + CString("song.mp3"), arrayPossibleMusic );
|
|
GetDirListing( out.GetSongDir() + CString("song.ogg"), arrayPossibleMusic );
|
|
GetDirListing( out.GetSongDir() + CString("song.wav"), arrayPossibleMusic );
|
|
|
|
if( !arrayPossibleMusic.empty() ) // we found a match
|
|
out.m_sMusicFile = arrayPossibleMusic[0];
|
|
|
|
return true;
|
|
}
|
|
|
|
bool KSFLoader::LoadFromDir( CString sDir, Song &out )
|
|
{
|
|
LOG->Trace( "Song::LoadFromKSFDir(%s)", sDir.c_str() );
|
|
|
|
CStringArray arrayKSFFileNames;
|
|
GetDirListing( sDir + CString("*.ksf"), arrayKSFFileNames );
|
|
|
|
/* We shouldn't have been called to begin with if there were no KSFs. */
|
|
if( arrayKSFFileNames.empty() )
|
|
RageException::Throw( "Couldn't find any KSF files in '%s'", sDir.c_str() );
|
|
|
|
if(!LoadGlobalData(out.GetSongDir() + arrayKSFFileNames[0], out))
|
|
return false;
|
|
|
|
// load the Steps from the rest of the KSF files
|
|
for( unsigned i=0; i<arrayKSFFileNames.size(); i++ )
|
|
{
|
|
Steps* pNewNotes = new Steps;
|
|
if(!LoadFromKSFFile( out.GetSongDir() + arrayKSFFileNames[i], *pNewNotes, out ))
|
|
{
|
|
delete pNewNotes;
|
|
continue;
|
|
}
|
|
|
|
out.m_apNotes.push_back( pNewNotes );
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|