From b26dc2b3bdc3e2aac19db3ffaea1942755faa652 Mon Sep 17 00:00:00 2001 From: Thai Pangsakulyanont Date: Sat, 10 Sep 2011 01:08:04 +0700 Subject: [PATCH] major rewrite of NotesLoaderBMS. now it needs a lot of testing. - handles BPM changes correctly - songs should load faster - auto-adjusts time signature changes (not sure how well will it perform.) - add some code to differentiate between beat_single5 and popn_five, fixes bug 430 - also, bug 500 seems to be fixed after the rewrite, after some other attempts fail what's not supported yet: - #LNOBJ long note parsing - #LNTYPE 2 long note parsing [http://nvyu.net/rdm/ex.php] --- src/NotesLoaderBMS.cpp | 2014 ++++++++++++++++++---------------------- 1 file changed, 904 insertions(+), 1110 deletions(-) diff --git a/src/NotesLoaderBMS.cpp b/src/NotesLoaderBMS.cpp index a43d4d4d30..b95d04c027 100644 --- a/src/NotesLoaderBMS.cpp +++ b/src/NotesLoaderBMS.cpp @@ -15,9 +15,6 @@ #include "NotesLoader.h" #include "PrefsManager.h" -typedef multimap NameToData_t; -typedef map MeasureToTimeSig_t; - /* BMS encoding: tap-hold * 4&8panel: Player1 Player2 * Left 11-51 21-61 @@ -55,76 +52,6 @@ typedef map MeasureToTimeSig_t; * beat-double uses 21-26. */ -enum BmsTrack -{ - BMS_P1_KEY1 = 0, - BMS_P1_KEY2, - BMS_P1_KEY3, - BMS_P1_KEY4, - BMS_P1_KEY5, - BMS_P1_TURN, - BMS_P1_KEY6, - BMS_P1_KEY7, - BMS_P2_KEY1, - BMS_P2_KEY2, - BMS_P2_KEY3, - BMS_P2_KEY4, - BMS_P2_KEY5, - BMS_P2_TURN, - BMS_P2_KEY6, - BMS_P2_KEY7, - // max 4 simultaneous auto keysounds - BMS_AUTO_KEYSOUND_1, - BMS_AUTO_KEYSOUND_2, - BMS_AUTO_KEYSOUND_3, - BMS_AUTO_KEYSOUND_4, - BMS_AUTO_KEYSOUND_5, - BMS_AUTO_KEYSOUND_6, - BMS_AUTO_KEYSOUND_7, - BMS_AUTO_KEYSOUND_LAST, - NUM_BMS_TRACKS, -}; - -const int NUM_NON_AUTO_KEYSOUND_TRACKS = BMS_AUTO_KEYSOUND_1; -const int NUM_AUTO_KEYSOUND_TRACKS = NUM_BMS_TRACKS - NUM_NON_AUTO_KEYSOUND_TRACKS; - -static bool ConvertRawTrackToTapNote( int iRawTrack, BmsTrack &bmsTrackOut, bool &bIsHoldOut ) -{ - if( iRawTrack > 40 ) - { - bIsHoldOut = true; - iRawTrack -= 40; - } - else - { - bIsHoldOut = false; - } - - switch( iRawTrack ) - { - case 1: bmsTrackOut = BMS_AUTO_KEYSOUND_1; break; - case 11: bmsTrackOut = BMS_P1_KEY1; break; - case 12: bmsTrackOut = BMS_P1_KEY2; break; - case 13: bmsTrackOut = BMS_P1_KEY3; break; - case 14: bmsTrackOut = BMS_P1_KEY4; break; - case 15: bmsTrackOut = BMS_P1_KEY5; break; - case 16: bmsTrackOut = BMS_P1_TURN; break; - case 18: bmsTrackOut = BMS_P1_KEY6; break; - case 19: bmsTrackOut = BMS_P1_KEY7; break; - case 21: bmsTrackOut = BMS_P2_KEY1; break; - case 22: bmsTrackOut = BMS_P2_KEY2; break; - case 23: bmsTrackOut = BMS_P2_KEY3; break; - case 24: bmsTrackOut = BMS_P2_KEY4; break; - case 25: bmsTrackOut = BMS_P2_KEY5; break; - case 26: bmsTrackOut = BMS_P2_TURN; break; - case 28: bmsTrackOut = BMS_P2_KEY6; break; - case 29: bmsTrackOut = BMS_P2_KEY7; break; - default: // unknown track - return false; - } - return true; -} - // Find the largest common substring at the start of both strings. static RString FindLargestInitialSubstring( const RString &string1, const RString &string2 ) { @@ -139,119 +66,6 @@ static RString FindLargestInitialSubstring( const RString &string1, const RStrin return string1.substr( 0, i ); } -static StepsType DetermineStepsType( int iPlayer, const NoteData &nd, const RString &sPath, const int iNumNonEmptyTracks ) -{ - ASSERT( NUM_BMS_TRACKS == nd.GetNumTracks() ); - - switch( iPlayer ) - { - case 1: // "1 player" - /* Track counts: - * 4 - dance 4-panel - * 5 - pop 5-key - * 6 - dance 6-panel, beat 5-key - * 7 - beat 7-key (scratch unused) - * 8 - beat 7-key - * 9 - popn 9-key */ - switch( iNumNonEmptyTracks ) - { - case 4: return StepsType_dance_single; - case 5: return StepsType_popn_five; - case 6: - // FIXME: There's no way to distinguish between these types. - // They use the same tracks. Assume it's a Beat type since they - // are more common. - //return StepsType_dance_solo; - return StepsType_beat_single5; - case 7: - case 8: return StepsType_beat_single7; - case 9: return StepsType_popn_nine; - default: return StepsType_Invalid; - } - case 2: // couple/battle - return StepsType_dance_couple; - case 3: // double - /* Track counts: - * 8 - dance Double - * 12 - beat Double 5-key - * 16 - beat Double 7-key */ - switch( iNumNonEmptyTracks ) - { - case 8: return StepsType_beat_single7; - case 12: return StepsType_beat_double5; - case 16: return StepsType_beat_double7; - case 5: return StepsType_popn_five; - case 9: return StepsType_popn_nine; - default: return StepsType_Invalid; - } - default: - LOG->UserLog( "Song file", sPath, "has an invalid #PLAYER value %d.", iPlayer ); - return StepsType_Invalid; - } -} - -static bool GetTagFromMap( const NameToData_t &mapNameToData, const RString &sName, RString &sOut ) -{ - NameToData_t::const_iterator it; - it = mapNameToData.find( sName ); - if( it == mapNameToData.end() ) - return false; - - sOut = it->second; - - return true; -} - -/* Finds the longest common match for the given tag in all files. If the given tag - * was found in at least one file, returns true; otherwise returns false. */ -static bool GetCommonTagFromMapList( const vector &aBMSData, const RString &sName, RString &sOut ) -{ - bool bFoundOne = false; - for( unsigned i=0; i < aBMSData.size(); i++ ) - { - RString sTag; - if( !GetTagFromMap( aBMSData[i], sName, sTag ) ) - continue; - - if( !bFoundOne ) - { - bFoundOne = true; - sOut = sTag; - } - else - { - sOut = FindLargestInitialSubstring( sOut, sTag ); - } - } - - return bFoundOne; -} - - -static float GetBeatsPerMeasure( const MeasureToTimeSig_t &sigs, int iMeasure, const MeasureToTimeSig_t &sigAdjustments ) -{ - map::const_iterator time_sig = sigs.find( iMeasure ); - - float fRet = 4.0f; - if( time_sig != sigs.end() ) - fRet *= time_sig->second; - - time_sig = sigAdjustments.find( iMeasure ); - if( time_sig != sigAdjustments.end() ) - fRet *= time_sig->second; - - return fRet; -} - -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; -} - - static void SearchForDifficulty( RString sTag, Steps *pOut ) { sTag.MakeLower(); @@ -274,766 +88,6 @@ static void SearchForDifficulty( RString sTag, Steps *pOut ) LOG->Trace( "Tag \"%s\" is %s", sTag.c_str(), DifficultyToString(pOut->GetDifficulty()).c_str() ); } -static bool ReadBMSFile( const RString &sPath, NameToData_t &mapNameToData ) -{ - RageFile file; - if( !file.Open(sPath) ) - { - LOG->UserLog( "Song file", sPath, "couldn't be opened: %s", file.GetError().c_str() ); - return false; - } - - while( !file.AtEOF() ) - { - RString line; - if( file.GetLine(line) == -1 ) - { - LOG->UserLog( "Song file", sPath, "had a read error: %s", file.GetError().c_str() ); - return false; - } - - StripCrnl( line ); - - // BMS value names can be separated by a space or a colon. - size_t iIndexOfSeparator = line.find_first_of( ": " ); - RString value_name = line.substr( 0, iIndexOfSeparator ); - RString value_data; - if( iIndexOfSeparator != line.npos ) - value_data = line.substr( iIndexOfSeparator+1 ); - - value_name.MakeLower(); - mapNameToData.insert( make_pair(value_name, value_data) ); - } - - return true; -} - -enum -{ - BMS_TRACK_TIME_SIG = 2, - BMS_TRACK_BPM = 3, - BMS_TRACK_BPM_REF = 8, - BMS_TRACK_STOP = 9 -}; - -/* Time signatures are often abused to tweak sync. Real time signatures should - * cause us to adjust the row offsets so one beat remains one beat. Fake time signatures, - * like 1.001 or 0.999, should be removed and converted to BPM changes. This is much - * more accurate, and prevents the whole song from being shifted off of the beat, causing - * BeatToNoteType to be wrong. - * - * Evaluate each time signature, and guess which time signatures should be converted - * to BPM changes. This isn't perfect, but errors aren't fatal. */ -static void SetTimeSigAdjustments( const MeasureToTimeSig_t &sigs, Song &out, MeasureToTimeSig_t &sigAdjustmentsOut ) -{ - return; -#if 0 - sigAdjustmentsOut.clear(); - - MeasureToTimeSig_t::const_iterator it; - for( it = sigs.begin(); it != sigs.end(); ++it ) - { - int iMeasure = it->first; - float fFactor = it->second; -#if 1 - static const float ValidFactors[] = - { - 0.25f, /* 1/4 */ - 0.5f, /* 2/4 */ - 0.75f, /* 3/4 */ - 0.875f, /* 7/8 */ - 1.0f, - 1.5f, /* 6/4 */ - 1.75f /* 7/4 */ - }; - - bool bValidTimeSignature = false; - for( unsigned i = 0; i < ARRAYLEN(ValidFactors); ++i ) - if( fabsf(fFactor-ValidFactors[i]) < 0.001 ) - bValidTimeSignature = true; - - if( bValidTimeSignature ) - continue; -#else - /* Alternate approach that I tried first: see if the ratio is sane. However, - * some songs have values like "1.4", which comes out to 7/4 and is not a valid - * time signature. */ - // Convert the factor to a ratio, and reduce it. - int iNum = lrintf( fFactor * 1000 ), iDen = 1000; - int iDiv = gcd( iNum, iDen ); - iNum /= iDiv; - iDen /= iDiv; - - /* Real time signatures usually come down to 1/2, 3/4, 7/8, etc. Bogus - * signatures that are only there to adjust sync usually look like 99/100. */ - if( iNum <= 8 && iDen <= 8 ) - continue; -#endif - - /* This time signature is bogus. Convert it to a BPM adjustment for this - * measure. */ - LOG->Trace("Converted time signature %f in measure %i to a BPM segment.", fFactor, iMeasure ); - - /* Note that this GetMeasureStartRow will automatically include any adjustments - * that we've made previously in this loop; as long as we make the timing - * adjustment and the BPM adjustment together, everything remains consistent. - * Adjust sigAdjustmentsOut first, or fAdjustmentEndBeat will be wrong. */ - sigAdjustmentsOut[iMeasure] = 1.0f / fFactor; - int iAdjustmentStartRow = GetMeasureStartRow( sigs, iMeasure, sigAdjustmentsOut ); - int iAdjustmentEndRow = GetMeasureStartRow( sigs, iMeasure+1, sigAdjustmentsOut ); - out.m_Timing.MultiplyBPMInBeatRange( iAdjustmentStartRow, iAdjustmentEndRow, 1.0f / fFactor ); - } -#endif -} - -static void ReadTimeSigs( const NameToData_t &mapNameToData, MeasureToTimeSig_t &out ) -{ - /* some songs have BGA starting before the music, so convertors a put weird time signature - * at first measure, something like #00002:1.55. that made all subsequent notes 192th. - * here, find the lowest measure for notes track, and make it skip the time signatures before it. */ - int iStartMeasureNo = 999; - NameToData_t::const_iterator it; - for( it = mapNameToData.lower_bound("#00000"); it != mapNameToData.end(); ++it ) - { - const RString &sName = it->first; - if( sName.size() != 6 || sName[0] != '#' || !IsAnInt( sName.substr(1,5) ) ) - continue; - // this is step or offset data. Looks like "#00705" - int iMeasureNo = StringToInt( sName.substr(1, 3) ); - int iBMSTrackNo = StringToInt( sName.substr(4, 2) ); - RString nData = it->second; - int totalPairs = nData.size() / 2; - if( iBMSTrackNo != BMS_TRACK_TIME_SIG && iBMSTrackNo != 7 ) - { - for( int i = 0; i < totalPairs; ++i ) - { - RString sPair = nData.substr( i*2, 2 ); - if (sPair == "00") - { - continue; - } - if( iMeasureNo < iStartMeasureNo ) iStartMeasureNo = iMeasureNo; - } - } - } - for( it = mapNameToData.lower_bound("#00000"); it != mapNameToData.end(); ++it ) - { - const RString &sName = it->first; - if( sName.size() != 6 || sName[0] != '#' || !IsAnInt(sName.substr(1, 5)) ) - continue; - - // this is step or offset data. Looks like "#00705" - const RString &sData = it->second; - int iMeasureNo = StringToInt( sName.substr(1, 3) ); - if( iMeasureNo < iStartMeasureNo ) - continue; - int iBMSTrackNo = StringToInt( sName.substr(4, 2) ); - if( iBMSTrackNo == BMS_TRACK_TIME_SIG ) - out[iMeasureNo] = StringToFloat( sData ); - } -} - -static const int BEATS_PER_MEASURE = 4; -static const int ROWS_PER_MEASURE = ROWS_PER_BEAT * BEATS_PER_MEASURE; - -static bool SearchForKeysound( const RString &sPath, RString nDataOriginal, map &mapFilenameToKeysoundIndex, Song &out, int &outKeysoundIndex ) -{ - - // Search for memoized file names: - { - RString nDataToSearchFor = nDataOriginal; - nDataToSearchFor.MakeLower(); - map::iterator it = mapFilenameToKeysoundIndex.find(nDataToSearchFor); - if (it != mapFilenameToKeysoundIndex.end()) { - outKeysoundIndex = it->second; - return true; - } - } - - // FIXME: garbled song names seem to crash the app. - // this might not be the best place to put this code. - if( !utf8_is_valid(nDataOriginal) ) - return false; - - /* Due to bugs in some programs, many BMS files have a "WAV" extension - * on files in the BMS for files that actually have some other extension. - * Do a search. Don't do a wildcard search; if sData is "song.wav", - * we might also have "song.png", which we shouldn't match. */ - RString nData = nDataOriginal; - RString dir = out.GetSongDir(); - if (dir.empty()) - dir = Dirname(sPath); - if( !IsAFile(dir+nData) ) - { - const char *exts[] = { "oga", "ogg", "wav", "mp3", NULL }; // XXX: stop duplicating these everywhere - for( unsigned i = 0; exts[i] != NULL; ++i ) - { - RString fn = SetExtension( nData, exts[i] ); - if( IsAFile(dir+fn) ) - { - nData = fn; - break; - } - } - } - - if( !IsAFile(dir+nData) ) - { - LOG->UserLog( "Song file", dir, "references key \"%s\" that can't be found", nData.c_str() ); - return false; - } - - // Let's again search for memoized file names (we got the normalized one!): - { - RString nDataToSearchFor = nData; - nDataToSearchFor.MakeLower(); - map::iterator it = mapFilenameToKeysoundIndex.find(nDataToSearchFor); - if (it != mapFilenameToKeysoundIndex.end()) { - outKeysoundIndex = it->second; - - { - RString nDataToAdd = nDataOriginal; - nDataToAdd.MakeLower(); - mapFilenameToKeysoundIndex[nDataToAdd] = outKeysoundIndex; - } - - return true; - } - } - - // Now this is a new sample. - out.m_vsKeysoundFile.push_back( nData ); - outKeysoundIndex = out.m_vsKeysoundFile.size() - 1; - - { - RString nDataToAdd = nDataOriginal; - nDataToAdd.MakeLower(); - mapFilenameToKeysoundIndex[nDataToAdd] = outKeysoundIndex; - } - - { - RString nDataToAdd = nData; - nDataToAdd.MakeLower(); - mapFilenameToKeysoundIndex[nDataToAdd] = outKeysoundIndex; - } - - return true; - -} - -static bool SearchForKeysound( const RString &sPath, RString sNoteId, const NameToData_t &mapNameToData, map &mapIdToKeysoundIndex, map &mapFilenameToKeysoundIndex, Song &out, int &outKeysoundIndex ) -{ - - sNoteId.MakeLower(); - { - map::iterator it = mapIdToKeysoundIndex.find(sNoteId); - if (it != mapIdToKeysoundIndex.end()) - { - outKeysoundIndex = it->second; - return outKeysoundIndex >= 0; - } - } - - RString sTagToLookFor = ssprintf( "#wav%s", sNoteId.c_str() ); - RString nDataOriginal; - if( !GetTagFromMap( mapNameToData, sTagToLookFor, nDataOriginal ) ) - { - LOG->UserLog( "Song file", sPath.c_str(), "has tag \"%s\" which cannot be found.", sTagToLookFor.c_str() ); - return false; - } - - bool retval = SearchForKeysound(sPath, nDataOriginal, mapFilenameToKeysoundIndex, out, outKeysoundIndex); - mapIdToKeysoundIndex[sNoteId] = outKeysoundIndex; - return retval; - -} - -static bool LoadFromBMSFile( const RString &sPath, const NameToData_t &mapNameToData, Steps &out, Song &outSong, map &mapFilenameToKeysoundIndex ) -{ - - map mapIdToKeysoundIndex; - map mapNoteRowToBPM; - MeasureToTimeSig_t sigAdjustments; - - LOG->Trace( "Steps::LoadFromBMSFile( '%s' )", sPath.c_str() ); - - out.m_StepsType = StepsType_Invalid; - - // BMS player code. Fill in below and use to determine StepsType. - int iPlayer = -1; - RString sData; - if( GetTagFromMap( mapNameToData, "#player", sData ) ) - iPlayer = StringToInt(sData); - if( GetTagFromMap( mapNameToData, "#playlevel", sData ) ) - out.SetMeter( StringToInt(sData) ); - - NoteData ndNotes; - ndNotes.SetNumTracks( NUM_BMS_TRACKS ); - - // Read BPM - if( GetTagFromMap(mapNameToData, "#bpm", sData) ) - { - const float fBPM = StringToFloat( sData ); - - if( fBPM > 0.0f ) - { - BPMSegment * newSeg = new BPMSegment( 0, fBPM ); - out.m_Timing.AddSegment(SEGMENT_BPM, newSeg ); - LOG->Trace( "Inserting new BPM change at beat %f, BPM %f", NoteRowToBeat(0), fBPM ); - } - else - { - LOG->UserLog( "Song file", sPath.c_str(), "has an invalid BPM change at beat %f, BPM %f.", - NoteRowToBeat(0), fBPM ); - } - } - - - /* Read time signatures. Note that these can differ across files in the same - * song. */ - MeasureToTimeSig_t mapMeasureToTimeSig; - ReadTimeSigs( mapNameToData, mapMeasureToTimeSig ); - - for( NameToData_t::const_iterator it = mapNameToData.lower_bound("#00000"); it != mapNameToData.end(); ++it ) - { - const RString &sName = it->first; - if( sName.size() != 6 || sName[0] != '#' || !IsAnInt( sName.substr(1,5) ) ) - continue; - // this is step or offset data. Looks like "#00705" - int iMeasureNo = atoi( sName.substr(1, 3).c_str() ); - int iBMSTrackNo = atoi( sName.substr(4, 2).c_str() ); - int iStepIndex = GetMeasureStartRow( mapMeasureToTimeSig, iMeasureNo, sigAdjustments ); - float fBeatsPerMeasure = GetBeatsPerMeasure( mapMeasureToTimeSig, iMeasureNo, sigAdjustments ); - int iRowsPerMeasure = BeatToNoteRow( fBeatsPerMeasure ); - - RString nData = it->second; - int totalPairs = nData.size() / 2; - for( int i = 0; i < totalPairs; ++i ) - { - RString sPair = nData.substr( i*2, 2 ); - - int iRow = iStepIndex + (i * iRowsPerMeasure) / totalPairs; - float fBeat = NoteRowToBeat( iRow ); - int iVal = 0; - sscanf( sPair, "%x", &iVal ); - - if (sPair == "00") - { - continue; - } - - switch( iBMSTrackNo ) - { - case BMS_TRACK_BPM: - if( iVal > 0 ) - { - mapNoteRowToBPM[ BeatToNoteRow(fBeat) ] = iVal; - LOG->Trace( "Inserting new BPM change at beat %f, BPM %i", fBeat, iVal ); - } - else - { - LOG->UserLog( "Song file", sPath.c_str(), "has an invalid BPM change at beat %f, BPM %d.", - fBeat, iVal ); - } - break; - - case BMS_TRACK_BPM_REF: - { - RString sTagToLookFor = ssprintf( "#bpm%s", sPair.c_str() ); - RString sBPM; - if( GetTagFromMap( mapNameToData, sTagToLookFor, sBPM ) ) - { - float fBPM = StringToFloat( sBPM ); - mapNoteRowToBPM[ BeatToNoteRow(fBeat) ] = fBPM; - } - else - { - LOG->UserLog( "Song file", sPath.c_str(), "has tag \"%s\" which cannot be found.", sTagToLookFor.c_str() ); - } - break; - } - case BMS_TRACK_STOP: - { - if( iVal == 0 ) - { - break; - } - RString sTagToLookFor = ssprintf( "#stop%02x", iVal ); - RString sBeats; - if( GetTagFromMap( mapNameToData, sTagToLookFor, sBeats ) ) - { - // find the BPM at the time of this freeze - float fBPS = out.m_Timing.GetBPMAtBeat(fBeat) / 60.0f; - float fBeats = StringToFloat( sBeats ) / 48.0f; - float fFreezeSecs = fBeats / fBPS; - - StopSegment * newSeg = new StopSegment( fBeat, fFreezeSecs ); - out.m_Timing.AddSegment( SEGMENT_STOP, newSeg ); - LOG->Trace( "Inserting new Freeze at beat %f, secs %f", fBeat, newSeg->GetPause() ); - } - else - { - LOG->UserLog( "Song file", sPath.c_str(), "has tag \"%s\" which cannot be found.", sTagToLookFor.c_str() ); - } - break; - } - } - } - - } - - for( map::iterator it = mapNoteRowToBPM.begin(); it != mapNoteRowToBPM.end(); it ++ ) - { - out.m_Timing.SetBPMAtRow( it->first, it->second ); - } - - // Now that we're done reading BPMs, factor out weird time signatures. - SetTimeSigAdjustments( mapMeasureToTimeSig, outSong, sigAdjustments ); - - int iHoldStarts[NUM_BMS_TRACKS]; - TapNote iHoldHeads[NUM_BMS_TRACKS]; - - for( int i = 0; i < NUM_BMS_TRACKS; ++i ) - { - iHoldStarts[i] = -1; - iHoldHeads[i] = TAP_EMPTY; - } - - NameToData_t::const_iterator it; - - bool hasBGM = false; - for( it = mapNameToData.lower_bound("#00000"); it != mapNameToData.end(); ++it ) - { - const RString &sName = it->first; - if( sName.size() != 6 || sName[0] != '#' || !IsAnInt( sName.substr(1, 5) ) ) - continue; - - // this is step or offset data. Looks like "#00705" - int iMeasureNo = StringToInt( sName.substr(1,3) ); - int iRawTrackNum = StringToInt( sName.substr(4,2) ); - int iRowNo = GetMeasureStartRow( mapMeasureToTimeSig, iMeasureNo, sigAdjustments ); - float fBeatsPerMeasure = GetBeatsPerMeasure( mapMeasureToTimeSig, iMeasureNo, sigAdjustments ); - const RString &sNoteData = it->second; - - vector vTapNotes; - for( size_t i=0; i+1= BMS_AUTO_KEYSOUND_1 ) - { - tn.type = TapNote::autoKeysound; - bmsTrack = (BmsTrack)iLastEmptyTrack; - } - else - { - // no room for this note. Drop it. - continue; - } - } - else if( bIsHold ) - { - tn.type = TapNote::hold_head; - tn.subType = TapNote::hold_head_hold; - } - } - // Don't bother inserting empty taps. - if( tn.type != TapNote::empty ) - ndNotes.SetTapNote( bmsTrack, row, tn ); - } - } - } - - if (!hasBGM) - { - LOG->Warn("The song at %s is missing a #XXX01 tag! We're unable to load.", sPath.c_str()); - return false; - } - - /* Handles hold notes like uBMPlay. - * Different BMS simulators support hold notes differently. - * See http://nvyu.net/rdm/ex.php for more info. - */ - for( int t=BMS_P1_KEY1; t<=BMS_P2_KEY7; t++ ) - { - int iHoldHeadRow = -1; - TapNote tnHoldHead; - FOREACH_NONEMPTY_ROW_IN_TRACK( ndNotes, t, row ) - { - TapNote tn = ndNotes.GetTapNote( t, row ); - if ( tn.type == TapNote::hold_head || tn.type == TapNote::tap ) - { - if ( iHoldHeadRow != -1 ) - { - // Delete head and tail, and add the hold note there. - ndNotes.SetTapNote( t, row, TAP_EMPTY ); - ndNotes.SetTapNote( t, iHoldHeadRow, TAP_EMPTY ); - if ( iHoldHeadRow < row ) - { - ndNotes.AddHoldNote( t, iHoldHeadRow, row, tnHoldHead ); - } - iHoldHeadRow = -1; - } - else if ( tn.type == TapNote::hold_head ) - { - // Head of the hold note, store it to find the tail. - iHoldHeadRow = row; - tnHoldHead = tn; - - // Replace with a tap note. - tn.type = TapNote::tap; - tn.subType = TapNote::SubType_Invalid; - ndNotes.SetTapNote( t, row, tn ); - } - } - } - } - - bool bTrackHasNote[NUM_NON_AUTO_KEYSOUND_TRACKS]; - ZERO( bTrackHasNote ); - - int iLastRow = ndNotes.GetLastRow(); - for( int t=0; t') - const size_t iOpenBracket = sData.find_first_of( "<(" ); - const size_t iCloseBracket = sData.find_first_of( ">)", iOpenBracket ); - - // if there's a 6 in the description, it's probably part of "6panel" or "6-panel" - if( sData.find('6', iOpenBracket) < iCloseBracket ) - out.m_StepsType = StepsType_dance_solo; - } - - if( out.m_StepsType == StepsType_Invalid ) - { - LOG->UserLog( "Song file", sPath, "has an unknown steps type" ); - return false; - } - - int iNumNewTracks = GAMEMAN->GetStepsTypeInfo( out.m_StepsType ).iNumTracks; - vector iTransformNewToOld; - iTransformNewToOld.resize( iNumNewTracks, -1 ); - - switch( out.m_StepsType ) - { - case StepsType_dance_single: - iTransformNewToOld[0] = BMS_P1_KEY1; - iTransformNewToOld[1] = BMS_P1_KEY3; - iTransformNewToOld[2] = BMS_P1_KEY5; - iTransformNewToOld[3] = BMS_P1_TURN; - break; - case StepsType_dance_double: - case StepsType_dance_couple: - iTransformNewToOld[0] = BMS_P1_KEY1; - iTransformNewToOld[1] = BMS_P1_KEY3; - iTransformNewToOld[2] = BMS_P1_KEY5; - iTransformNewToOld[3] = BMS_P1_TURN; - iTransformNewToOld[4] = BMS_P2_KEY1; - iTransformNewToOld[5] = BMS_P2_KEY3; - iTransformNewToOld[6] = BMS_P2_KEY5; - iTransformNewToOld[7] = BMS_P2_TURN; - break; - case StepsType_dance_solo: - case StepsType_beat_single5: - // Hey! Why are these exactly the same? :-) - iTransformNewToOld[0] = BMS_P1_KEY1; - iTransformNewToOld[1] = BMS_P1_KEY2; - iTransformNewToOld[2] = BMS_P1_KEY3; - iTransformNewToOld[3] = BMS_P1_KEY4; - iTransformNewToOld[4] = BMS_P1_KEY5; - iTransformNewToOld[5] = BMS_P1_TURN; - break; - case StepsType_popn_five: - iTransformNewToOld[0] = BMS_P1_KEY3; - iTransformNewToOld[1] = BMS_P1_KEY4; - iTransformNewToOld[2] = BMS_P1_KEY5; - // fix these columns! - iTransformNewToOld[3] = BMS_P2_KEY2; - iTransformNewToOld[4] = BMS_P2_KEY3; - break; - case StepsType_popn_nine: - iTransformNewToOld[0] = BMS_P1_KEY1; // lwhite - iTransformNewToOld[1] = BMS_P1_KEY2; // lyellow - iTransformNewToOld[2] = BMS_P1_KEY3; // lgreen - iTransformNewToOld[3] = BMS_P1_KEY4; // lblue - iTransformNewToOld[4] = BMS_P1_KEY5; // red - // fix these columns! - iTransformNewToOld[5] = BMS_P2_KEY2; // rblue - iTransformNewToOld[6] = BMS_P2_KEY3; // rgreen - iTransformNewToOld[7] = BMS_P2_KEY4; // ryellow - iTransformNewToOld[8] = BMS_P2_KEY5; // rwhite - break; - case StepsType_beat_double5: - iTransformNewToOld[0] = BMS_P1_KEY1; - iTransformNewToOld[1] = BMS_P1_KEY2; - iTransformNewToOld[2] = BMS_P1_KEY3; - iTransformNewToOld[3] = BMS_P1_KEY4; - iTransformNewToOld[4] = BMS_P1_KEY5; - iTransformNewToOld[5] = BMS_P1_TURN; - iTransformNewToOld[6] = BMS_P2_KEY1; - iTransformNewToOld[7] = BMS_P2_KEY2; - iTransformNewToOld[8] = BMS_P2_KEY3; - iTransformNewToOld[9] = BMS_P2_KEY4; - iTransformNewToOld[10] = BMS_P2_KEY5; - iTransformNewToOld[11] = BMS_P2_TURN; - break; - case StepsType_beat_single7: - if( !bTrackHasNote[BMS_P1_KEY7] && bTrackHasNote[BMS_P1_TURN] ) - { - /* special case for o2mania style charts: - * the turntable is used for first key while the real 7th key is not used. */ - iTransformNewToOld[0] = BMS_P1_TURN; - iTransformNewToOld[1] = BMS_P1_KEY1; - iTransformNewToOld[2] = BMS_P1_KEY2; - iTransformNewToOld[3] = BMS_P1_KEY3; - iTransformNewToOld[4] = BMS_P1_KEY4; - iTransformNewToOld[5] = BMS_P1_KEY5; - iTransformNewToOld[6] = BMS_P1_KEY6; - iTransformNewToOld[7] = BMS_P1_KEY7; - } - else - { - iTransformNewToOld[0] = BMS_P1_KEY1; - iTransformNewToOld[1] = BMS_P1_KEY2; - iTransformNewToOld[2] = BMS_P1_KEY3; - iTransformNewToOld[3] = BMS_P1_KEY4; - iTransformNewToOld[4] = BMS_P1_KEY5; - iTransformNewToOld[5] = BMS_P1_KEY6; - iTransformNewToOld[6] = BMS_P1_KEY7; - iTransformNewToOld[7] = BMS_P1_TURN; - } - break; - case StepsType_beat_double7: - iTransformNewToOld[0] = BMS_P1_KEY1; - iTransformNewToOld[1] = BMS_P1_KEY2; - iTransformNewToOld[2] = BMS_P1_KEY3; - iTransformNewToOld[3] = BMS_P1_KEY4; - iTransformNewToOld[4] = BMS_P1_KEY5; - iTransformNewToOld[5] = BMS_P1_KEY6; - iTransformNewToOld[6] = BMS_P1_KEY7; - iTransformNewToOld[7] = BMS_P1_TURN; - iTransformNewToOld[8] = BMS_P2_KEY1; - iTransformNewToOld[9] = BMS_P2_KEY2; - iTransformNewToOld[10] = BMS_P2_KEY3; - iTransformNewToOld[11] = BMS_P2_KEY4; - iTransformNewToOld[12] = BMS_P2_KEY5; - iTransformNewToOld[13] = BMS_P2_KEY6; - iTransformNewToOld[14] = BMS_P2_KEY7; - iTransformNewToOld[15] = BMS_P2_TURN; - break; - default: - ASSERT_M(0, ssprintf("Invalid StepsType when parsing BMS file %s!", sPath.c_str())); - } - - // shift all of the autokeysound tracks onto the main tracks - // Moved here so that most sounds are inserted. - for( int t=BMS_AUTO_KEYSOUND_1+NUM_AUTO_KEYSOUND_TRACKS-1; t>=BMS_AUTO_KEYSOUND_1; t-- ) - { - FOREACH_NONEMPTY_ROW_IN_TRACK( ndNotes, t, row ) - { - TapNote tn = ndNotes.GetTapNote( t, row ); - int iEmptyTrack = -1; - for( int i=0; i -1 ) - { - ndNotes.SetTapNote( iEmptyTrack, row, tn ); - ndNotes.SetTapNote( t, row, TAP_EMPTY ); - } - else - { - LOG->UserLog( "Song file", sPath, "has too much simultaneous autokeysound tracks." ); - } - } - } - - NoteData noteData2; - noteData2.SetNumTracks( iNumNewTracks ); - noteData2.LoadTransformed( ndNotes, iNumNewTracks, &*iTransformNewToOld.begin() ); - - out.SetNoteData( noteData2 ); - - out.TidyUpData(); - - out.SetSavedToDisk( true ); // we're loading from disk, so this is by definintion already saved - - return true; -} - -static void ReadGlobalTags( const NameToData_t &mapNameToData, Song &out ) -{ - RString sData; - if( GetTagFromMap(mapNameToData, "#title", sData) ) - NotesLoader::GetMainAndSubTitlesFromFullTitle( sData, out.m_sMainTitle, out.m_sSubTitle ); - - GetTagFromMap( mapNameToData, "#artist", out.m_sArtist ); - GetTagFromMap( mapNameToData, "#genre", out.m_sGenre ); - GetTagFromMap( mapNameToData, "#backbmp", out.m_sBackgroundFile ); - GetTagFromMap( mapNameToData, "#wav", out.m_sMusicFile ); - -} - static void SlideDuplicateDifficulties( Song &p ) { /* BMS files have to guess the Difficulty from the meter; this is inaccurate, @@ -1071,154 +125,810 @@ void BMSLoader::GetApplicableFiles( const RString &sPath, vector &out ) GetDirListing( sPath + RString("*.bml"), out ); } -bool BMSLoader::LoadNoteDataFromSimfile( const RString & cachePath, Steps & out ) -{ - Song *pSong = SONGMAN->GetSongFromSteps( &out ); - Song &song = *pSong; - // TODO: Simplify this copy/paste from LoadFromDir. - - vector BMSData; - BMSData.push_back(NameToData_t()); - ReadBMSFile(cachePath, BMSData.back()); - - RString commonSubstring; - GetCommonTagFromMapList( BMSData, "#title", commonSubstring ); - - Steps *copy = song.CreateSteps(); - - copy->SetDifficulty( Difficulty_Medium ); - RString sTag; - if( GetTagFromMap( BMSData[0], "#title", sTag ) && sTag.size() != commonSubstring.size() ) - { - sTag = sTag.substr( commonSubstring.size(), sTag.size() - commonSubstring.size() ); - sTag.MakeLower(); - - if( sTag.find('l') != sTag.npos ) - { - unsigned lPos = sTag.find('l'); - if( lPos > 2 && sTag.substr(lPos-2,4) == "solo" ) - { - copy->SetDifficulty( Difficulty_Edit ); - } - else - { - copy->SetDifficulty( Difficulty_Easy ); - } - } - else if( sTag.find('a') != sTag.npos ) - copy->SetDifficulty( Difficulty_Hard ); - else if( sTag.find('b') != sTag.npos ) - copy->SetDifficulty( Difficulty_Beginner ); - } - if( commonSubstring == "" ) - { - copy->SetDifficulty(Difficulty_Medium); - RString localTag; - if (GetTagFromMap(BMSData[0], "#title#", localTag)) - SearchForDifficulty(localTag, copy); - } - ReadGlobalTags( BMSData[0], song ); - if( commonSubstring.size() > 2 && commonSubstring[commonSubstring.size() - 2] == ' ' ) - { - switch( commonSubstring[commonSubstring.size() - 1] ) - { - case '[': - case '(': - case '<': - commonSubstring = commonSubstring.substr(0, commonSubstring.size() - 2); - default: - break; - } - } - map mapFilenameToKeysoundIndex; - - for (unsigned i = 0; i < pSong->m_vsKeysoundFile.size(); i ++) { - mapFilenameToKeysoundIndex[pSong->m_vsKeysoundFile[i]] = i; - } - const bool ok = LoadFromBMSFile( cachePath, BMSData[0], *copy, song, mapFilenameToKeysoundIndex ); - if( ok ) +/*=========================================================================== + *=========================================================================== + *=========================================================================== + *=========================================================================== + */ + +struct BMSObject +{ + int channel; + int measure; + float position; + bool flag; + RString value; + bool operator<(const BMSObject &other) const { - out.SetNoteData(copy->GetNoteData()); + if( measure < other.measure ) return true; + if( measure > other.measure ) return false; + if( position < other.position ) return true; + if( position > other.position ) return false; + if( channel == 1 ) return false; + if( other.channel == 1 ) return true; + return channel < other.channel; } - return ok; +}; + + + + +struct BMSMeasure +{ + float size; +}; + + + +typedef map BMSHeaders; +typedef map BMSMeasures; +typedef vector BMSObjects; + +class BMSChart +{ + +public: + BMSChart(); + bool Load( const RString &path ); + bool GetHeader( const RString &header, RString &out ); + RString path; + + BMSObjects objects; + BMSHeaders headers; + BMSMeasures measures; + + void TidyUpData(); + +}; + +BMSChart::BMSChart() +{ } -bool BMSLoader::LoadFromDir( const RString &sDir, Song &out ) +bool BMSChart::GetHeader( const RString &header, RString &out ) { - LOG->Trace( "Song::LoadFromBMSDir(%s)", sDir.c_str() ); + if( headers.find(header) == headers.end() ) return false; + out = headers[header]; + return true; +} - ASSERT( out.m_vsKeysoundFile.empty() ); - - vector arrayBMSFileNames; - GetApplicableFiles( sDir, arrayBMSFileNames ); - - /* We should have at least one; if we had none, we shouldn't have been - * called to begin with. */ - ASSERT( arrayBMSFileNames.size() ); - - // Read all BMS files. - vector aBMSData; - for( unsigned i=0; iUserLog( "Song file", path, "couldn't be opened: %s", file.GetError().c_str() ); + return false; } - RString commonSubstring; - GetCommonTagFromMapList( aBMSData, "#title", commonSubstring ); - - if( commonSubstring == "" ) + while( !file.AtEOF() ) { - // All bets are off; the titles don't match at all. - // At this rate we're lucky if we even get the title right. - LOG->UserLog( "Song", sDir, "has BMS files with inconsistent titles." ); - } - - // Create a Steps for each. - vector apSteps; - for( unsigned i=0; iSetDifficulty( Difficulty_Medium ); - RString sTag; - if( GetTagFromMap( aBMSData[i], "#title", sTag ) && sTag.size() != commonSubstring.size() ) + RString line; + if( file.GetLine(line) == -1 ) { - sTag = sTag.substr( commonSubstring.size(), sTag.size() - commonSubstring.size() ); - sTag.MakeLower(); + LOG->UserLog( "Song file", path, "had a read error: %s", file.GetError().c_str() ); + return false; + } - // XXX: We should do this with filenames too, I have plenty of examples. - // however, filenames will be trickier, as stuff at the beginning AND - // end change per-file, so we'll need a fancier FindLargestInitialSubstring() + StripCrnl( line ); - // XXX: This matches (double), but I haven't seen it used. Again, MORE EXAMPLES NEEDED - if( sTag.find('l') != sTag.npos ) + if( line.size() > 1 && line[0] == '#' ) + { + if( line.size() >= 7 && + ( '0' <= line[1] && line[1] <= '9' ) && + ( '0' <= line[2] && line[2] <= '9' ) && + ( '0' <= line[3] && line[3] <= '9' ) && + ( '0' <= line[4] && line[4] <= '9' ) && + ( '0' <= line[5] && line[5] <= '9' ) && + line[6] == ':' ) { - unsigned lPos = sTag.find('l'); - if( lPos > 2 && sTag.substr(lPos-2,4) == "solo" ) + RString data = line.substr(7); + int measure = atoi( line.substr(1, 3).c_str() ); + int channel = atoi( line.substr(4, 2).c_str() ); + bool flag = false; + if( channel == 2 ) { - // (solo) -- an edit, apparently (Thanks Glenn!) - pSteps->SetDifficulty( Difficulty_Edit ); + // special channel: time signature + this->measures[measure] = (BMSMeasure){ StringToFloat(data) }; } else { - // Any of [L7] [L14] (LIGHT7) (LIGHT14) (LIGHT) [L] ... you get the idea. - pSteps->SetDifficulty( Difficulty_Easy ); + if( channel >= 51 ) + { + channel -= 40; + flag = true; + } + int count = data.size() / 2; + for( int i = 0; i < count; i ++ ) + { + RString value = data.substr( 2 * i, 2 ); + if( value != "00" ) + { + value.MakeLower(); + BMSObject o = { channel, measure, (float)i / count, flag, value }; + objects.push_back( o ); + } + } } } - // [A] (A) [ANOTHER] (ANOTHER) (ANOTHER7) Another (DP ANOTHER) (Another) -ANOTHER- [A7] [A14] etc etc etc - else if( sTag.find('a') != sTag.npos ) - pSteps->SetDifficulty( Difficulty_Hard ); - // XXX: Can also match (double), but should match [B] or [B7] - else if( sTag.find('b') != sTag.npos ) - pSteps->SetDifficulty( Difficulty_Beginner ); - // Other tags I've seen here include (5KEYS) (10KEYS) (7keys) (14keys) (dp) [MIX] [14] (14 Keys Mix) - // XXX: I'm sure [MIX] means something... anyone know? + else + { + size_t space = line.find(' '); + RString name = line.substr( 0, space ); + RString value = ""; + if( space != line.npos ) + value = line.substr( space+1 ); + name.MakeLower(); + headers[name] = value; + } + } + } + + TidyUpData(); + + return true; +} + +void BMSChart::TidyUpData() +{ + sort( objects.begin(), objects.end() ); +} + + + + +class BMSSong { + + map mapKeysoundToIndex; + Song *out; + +public: + BMSSong( Song *song ); + unsigned AllocateKeysound( RString filename, RString path ); + Song *GetSong(); + +}; + +BMSSong::BMSSong( Song *song ) +{ + out = song; + + // import existing keysounds from song + for( unsigned i = 0; i < out->m_vsKeysoundFile.size(); i ++ ) + { + mapKeysoundToIndex[out->m_vsKeysoundFile[i]] = i; + } + +} + +Song *BMSSong::GetSong() +{ + return out; +} + +unsigned BMSSong::AllocateKeysound( RString filename, RString path ) +{ + if( mapKeysoundToIndex.find( filename ) != mapKeysoundToIndex.end() ) + { + return mapKeysoundToIndex[filename]; + } + + // try to normalize the filename first! + + // FIXME: garbled song names seem to crash the app. + // this might not be the best place to put this code. + if( !utf8_is_valid(filename) ) + return false; + + /* Due to bugs in some programs, many BMS files have a "WAV" extension + * on files in the BMS for files that actually have some other extension. + * Do a search. Don't do a wildcard search; if sData is "song.wav", + * we might also have "song.png", which we shouldn't match. */ + RString normalizedFilename = filename; + RString dir = out->GetSongDir(); + + if (dir.empty()) + dir = Dirname(path); + + if( !IsAFile(dir + normalizedFilename) ) + { + const char *exts[] = { "oga", "ogg", "wav", "mp3", NULL }; // XXX: stop duplicating these everywhere + for( unsigned i = 0; exts[i] != NULL; ++i ) + { + RString fn = SetExtension( normalizedFilename, exts[i] ); + if( IsAFile(dir + fn) ) + { + normalizedFilename = fn; + break; + } + } + } + + if( !IsAFile(dir + normalizedFilename) ) + { + mapKeysoundToIndex[filename] = -1; + LOG->UserLog( "Song file", dir, "references key \"%s\" that can't be found", normalizedFilename.c_str() ); + return false; + } + + if( mapKeysoundToIndex.find( normalizedFilename ) != mapKeysoundToIndex.end() ) + { + mapKeysoundToIndex[filename] = mapKeysoundToIndex[normalizedFilename]; + return mapKeysoundToIndex[normalizedFilename]; + } + + unsigned index = out->m_vsKeysoundFile.size(); + out->m_vsKeysoundFile.push_back( normalizedFilename ); + mapKeysoundToIndex[filename] = index; + mapKeysoundToIndex[normalizedFilename] = index; + return index; + +} + + +struct BMSChartInfo { + + RString title; + RString artist; + RString genre; + + RString backgroundFile; + RString musicFile; + +}; + +class BMSChartReader { + + BMSChart *in; + Steps *out; + BMSSong *song; + + void ReadHeaders(); + void CalculateStepsType(); + bool ReadNoteData(); + + StepsType DetermineStepsType(); + + int lntype; + RString lnobj; + + int nonEmptyTracksCount; + map nonEmptyTracks; + + int GetKeysound( const BMSObject &obj ); + + map mapValueToKeysoundIndex; + +public: + BMSChartReader( BMSChart *chart, Steps *steps, BMSSong *song ); + bool Read(); + + Steps *GetSteps(); + + BMSChartInfo info; + int player; + float initialBPM; + +}; + +BMSChartReader::BMSChartReader( BMSChart *chart, Steps *steps, BMSSong *bmsSong ) +{ + this->in = chart; + this->out = steps; + this->song = bmsSong; +} + +bool BMSChartReader::Read() +{ + ReadHeaders(); + CalculateStepsType(); + if( !ReadNoteData() ) return false; + return true; +} + +void BMSChartReader::ReadHeaders() +{ + lntype = 1; + player = 1; + for( BMSHeaders::iterator it = in->headers.begin(); it != in->headers.end(); it ++ ) + { + if( it->first == "#player" ) + { + player = atoi(it->second.c_str()); + } + else if( it->first == "#title" ) + { + info.title = it->second; + } + else if( it->first == "#artist" ) + { + info.artist = it->second; + } + else if( it->first == "#genre" ) + { + info.genre = it->second; + } + else if( it->first == "#backbmp" ) + { + info.backgroundFile = it->second; + } + else if( it->first == "#wav" ) + { + info.musicFile = it->second; + } + else if( it->first == "#bpm" ) + { + initialBPM = StringToFloat(it->second); + } + else if( it->first == "#lntype" ) + { + int myLntype = atoi(it->second.c_str()); + if( myLntype == 1 ) + { + lntype = myLntype; + // XXX: we only support #LNTYPE 1 for now. + } + } + else if( it->first == "#lnobj" ) + { + lnobj = it->second; + // TODO: support lnobj + } + else if( it->first == "#playlevel" ) + { + out->SetMeter( StringToInt(it->second) ); + } + } +} + +void BMSChartReader::CalculateStepsType() +{ + + for( unsigned i = 0; i < in->objects.size(); i ++ ) + { + BMSObject &obj = in->objects[i]; + int channel = obj.channel; + if( (11 <= channel && channel <= 19) || (21 <= channel && channel <= 29) ) + { + nonEmptyTracks[channel] = true; + } + } + + nonEmptyTracksCount = nonEmptyTracks.size(); + out->m_StepsType = DetermineStepsType(); + +} + +enum BmsRawChannel +{ + BMS_RAW_P1_KEY1 = 11, + BMS_RAW_P1_KEY2 = 12, + BMS_RAW_P1_KEY3 = 13, + BMS_RAW_P1_KEY4 = 14, + BMS_RAW_P1_KEY5 = 15, + BMS_RAW_P1_TURN = 16, + BMS_RAW_P1_KEY6 = 18, + BMS_RAW_P1_KEY7 = 19, + BMS_RAW_P2_KEY1 = 21, + BMS_RAW_P2_KEY2 = 22, + BMS_RAW_P2_KEY3 = 23, + BMS_RAW_P2_KEY4 = 24, + BMS_RAW_P2_KEY5 = 25, + BMS_RAW_P2_TURN = 26, + BMS_RAW_P2_KEY6 = 28, + BMS_RAW_P2_KEY7 = 29 +}; + +StepsType BMSChartReader::DetermineStepsType() +{ + switch( player ) + { + case 1: // "1 player" + switch( nonEmptyTracksCount ) + { + case 4: return StepsType_dance_single; + case 5: + if( nonEmptyTracks.find(BMS_RAW_P2_KEY2) != nonEmptyTracks.end() ) return StepsType_popn_five; + case 6: + // FIXME: There's no way to distinguish between these types. + // They use the same tracks. Assume it's a Beat type since they + // are more common. + //return StepsType_dance_solo; + return StepsType_beat_single5; + case 7: + case 8: return StepsType_beat_single7; + case 9: return StepsType_popn_nine; + default: return StepsType_Invalid; + } + case 2: // couple/battle + return StepsType_dance_couple; + case 3: // double + switch( nonEmptyTracksCount ) + { + case 8: return StepsType_beat_single7; + case 12: return StepsType_beat_double5; + case 16: return StepsType_beat_double7; + case 5: return StepsType_popn_five; + case 9: return StepsType_popn_nine; + default: return StepsType_Invalid; + } + default: + LOG->UserLog( "Song file", in->path, "has an invalid #PLAYER value %d.", player ); + return StepsType_Invalid; + } +} + +int BMSChartReader::GetKeysound( const BMSObject &obj ) +{ + map::iterator it = mapValueToKeysoundIndex.find(obj.value); + if( it == mapValueToKeysoundIndex.end() ) + { + int index = -1; + BMSHeaders::iterator iu = in->headers.find("#wav" + obj.value); + if( iu != in->headers.end() ) + { + index = song->AllocateKeysound(iu->second, in->path); + } + mapValueToKeysoundIndex[obj.value] = index; + return index; + } + else + { + return it->second; + } +} + +bool BMSChartReader::ReadNoteData() +{ + if( out->m_StepsType == StepsType_Invalid ) + { + LOG->UserLog( "Song file", in->path, "has an unknown steps type" ); + return false; + } + + float currentBPM; + int tracks = GAMEMAN->GetStepsTypeInfo( out->m_StepsType ).iNumTracks; + + NoteData nd; + TimingData td; + + nd.SetNumTracks( tracks ); + td.SetBPMAtRow( 0, currentBPM = initialBPM ); + + // set up note transformation vector. + int *transform = new int[tracks]; + int *holdStart = new int[tracks]; + + for( int i = 0; i < tracks; i ++ ) holdStart[i] = -1; + + switch( out->m_StepsType ) + { + case StepsType_dance_single: + transform[0] = BMS_RAW_P1_KEY1; + transform[1] = BMS_RAW_P1_KEY3; + transform[2] = BMS_RAW_P1_KEY5; + transform[3] = BMS_RAW_P1_TURN; + break; + case StepsType_dance_double: + case StepsType_dance_couple: + transform[0] = BMS_RAW_P1_KEY1; + transform[1] = BMS_RAW_P1_KEY3; + transform[2] = BMS_RAW_P1_KEY5; + transform[3] = BMS_RAW_P1_TURN; + transform[4] = BMS_RAW_P2_KEY1; + transform[5] = BMS_RAW_P2_KEY3; + transform[6] = BMS_RAW_P2_KEY5; + transform[7] = BMS_RAW_P2_TURN; + break; + case StepsType_dance_solo: + case StepsType_beat_single5: + // Hey! Why are these exactly the same? :-) + transform[0] = BMS_RAW_P1_KEY1; + transform[1] = BMS_RAW_P1_KEY2; + transform[2] = BMS_RAW_P1_KEY3; + transform[3] = BMS_RAW_P1_KEY4; + transform[4] = BMS_RAW_P1_KEY5; + transform[5] = BMS_RAW_P1_TURN; + break; + case StepsType_popn_five: + transform[0] = BMS_RAW_P1_KEY3; + transform[1] = BMS_RAW_P1_KEY4; + transform[2] = BMS_RAW_P1_KEY5; + // fix these columns! + transform[3] = BMS_RAW_P2_KEY2; + transform[4] = BMS_RAW_P2_KEY3; + break; + case StepsType_popn_nine: + transform[0] = BMS_RAW_P1_KEY1; // lwhite + transform[1] = BMS_RAW_P1_KEY2; // lyellow + transform[2] = BMS_RAW_P1_KEY3; // lgreen + transform[3] = BMS_RAW_P1_KEY4; // lblue + transform[4] = BMS_RAW_P1_KEY5; // red + // fix these columns! + transform[5] = BMS_RAW_P2_KEY2; // rblue + transform[6] = BMS_RAW_P2_KEY3; // rgreen + transform[7] = BMS_RAW_P2_KEY4; // ryellow + transform[8] = BMS_RAW_P2_KEY5; // rwhite + break; + case StepsType_beat_double5: + transform[0] = BMS_RAW_P1_KEY1; + transform[1] = BMS_RAW_P1_KEY2; + transform[2] = BMS_RAW_P1_KEY3; + transform[3] = BMS_RAW_P1_KEY4; + transform[4] = BMS_RAW_P1_KEY5; + transform[5] = BMS_RAW_P1_TURN; + transform[6] = BMS_RAW_P2_KEY1; + transform[7] = BMS_RAW_P2_KEY2; + transform[8] = BMS_RAW_P2_KEY3; + transform[9] = BMS_RAW_P2_KEY4; + transform[10] = BMS_RAW_P2_KEY5; + transform[11] = BMS_RAW_P2_TURN; + break; + case StepsType_beat_single7: + if( nonEmptyTracks.find(BMS_RAW_P1_KEY7) == nonEmptyTracks.end() + && nonEmptyTracks.find(BMS_RAW_P1_TURN) != nonEmptyTracks.end() ) + { + /* special case for o2mania style charts: + * the turntable is used for first key while the real 7th key is not used. */ + transform[0] = BMS_RAW_P1_TURN; + transform[1] = BMS_RAW_P1_KEY1; + transform[2] = BMS_RAW_P1_KEY2; + transform[3] = BMS_RAW_P1_KEY3; + transform[4] = BMS_RAW_P1_KEY4; + transform[5] = BMS_RAW_P1_KEY5; + transform[6] = BMS_RAW_P1_KEY6; + transform[7] = BMS_RAW_P1_KEY7; + } + else + { + transform[0] = BMS_RAW_P1_KEY1; + transform[1] = BMS_RAW_P1_KEY2; + transform[2] = BMS_RAW_P1_KEY3; + transform[3] = BMS_RAW_P1_KEY4; + transform[4] = BMS_RAW_P1_KEY5; + transform[5] = BMS_RAW_P1_KEY6; + transform[6] = BMS_RAW_P1_KEY7; + transform[7] = BMS_RAW_P1_TURN; + } + break; + case StepsType_beat_double7: + transform[0] = BMS_RAW_P1_KEY1; + transform[1] = BMS_RAW_P1_KEY2; + transform[2] = BMS_RAW_P1_KEY3; + transform[3] = BMS_RAW_P1_KEY4; + transform[4] = BMS_RAW_P1_KEY5; + transform[5] = BMS_RAW_P1_KEY6; + transform[6] = BMS_RAW_P1_KEY7; + transform[7] = BMS_RAW_P1_TURN; + transform[8] = BMS_RAW_P2_KEY1; + transform[9] = BMS_RAW_P2_KEY2; + transform[10] = BMS_RAW_P2_KEY3; + transform[11] = BMS_RAW_P2_KEY4; + transform[12] = BMS_RAW_P2_KEY5; + transform[13] = BMS_RAW_P2_KEY6; + transform[14] = BMS_RAW_P2_KEY7; + transform[15] = BMS_RAW_P2_TURN; + break; + default: + ASSERT_M(0, ssprintf("Invalid StepsType when parsing BMS file %s!", in->path.c_str())); + } + + int reverseTransform[30]; + for( int i = 0; i < 30; i ++ ) reverseTransform[i] = -1; + for( int i = 0; i < tracks; i ++ ) reverseTransform[transform[i]] = i; + + int trackMeasure = -1; + float measureStartBeat = 0.0f; + float measureSize = 0.0f; + float adjustedMeasureSize = 0.0f; + float measureAdjust = 1.0f; + int firstNoteMeasure = 0; + + for( unsigned i = 0; i < in->objects.size(); i ++ ) + { + BMSObject &obj = in->objects[i]; + int channel = obj.channel; + firstNoteMeasure = obj.measure; + if( channel == 3 || channel == 8 || channel == 9 || channel == 1 || (11 <= channel && channel <= 19) || (21 <= channel && channel <= 29) ) + { + break; + } + } + + for( unsigned i = 0; i < in->objects.size(); i ++ ) + { + BMSObject &obj = in->objects[i]; + while( trackMeasure < obj.measure ) + { + trackMeasure ++; + measureStartBeat += adjustedMeasureSize; + measureSize = 4.0f; + BMSMeasures::iterator it = in->measures.find(trackMeasure); + if( it != in->measures.end() ) measureSize = it->second.size * 4.0f; + adjustedMeasureSize = measureSize; + if( trackMeasure < firstNoteMeasure ) adjustedMeasureSize = measureSize = 4.0f; + + // measure size adjustment + // XXX: need more testing / fine-tuning! + int sixteenths = lrintf(measureSize * 4.0f); + if( sixteenths > 1 ) adjustedMeasureSize = (float)sixteenths / 4.0f; + measureAdjust = adjustedMeasureSize / measureSize; + td.SetBPMAtRow( BeatToNoteRow(measureStartBeat), measureAdjust * currentBPM ); + // end measure size adjustment + + } + + int row = BeatToNoteRow( measureStartBeat + adjustedMeasureSize * obj.position ); + int channel = obj.channel; + bool hold = obj.flag; + + if( channel == 3 ) // bpm change + { + int bpm; + if( sscanf(obj.value, "%x", &bpm) == 1 ) + { + if( bpm > 0 ) td.SetBPMAtRow( row, measureAdjust * (currentBPM = bpm) ); + } + } + else if( channel == 8 ) // bpm change (extended) + { + RString search = ssprintf( "#bpm%s", obj.value.c_str() ); + BMSHeaders::iterator it = in->headers.find( search ); + if( it != in->headers.end() ) + { + td.SetBPMAtRow( row, measureAdjust * (currentBPM = StringToFloat(it->second)) ); + } + else + { + LOG->UserLog( "Song file", in->path.c_str(), "has tag \"%s\" which cannot be found.", search.c_str() ); + } + } + else if( channel == 9 ) // stops + { + RString search = ssprintf( "#stop%s", obj.value.c_str() ); + BMSHeaders::iterator it = in->headers.find( search ); + if( it != in->headers.end() ) + { + td.SetStopAtRow( row, (StringToFloat(it->second) / 48.0f) * (currentBPM / 60.0f) ); + } + else + { + LOG->UserLog( "Song file", in->path.c_str(), "has tag \"%s\" which cannot be found.", search.c_str() ); + } + } + else if( channel < 30 && reverseTransform[channel] != -1 ) // player notes! + { + int track = reverseTransform[channel]; + if( holdStart[track] != -1 ) + { + // this object is the end of the hold note. + TapNote tn = nd.GetTapNote(track, holdStart[track]); + tn.type = TapNote::hold_head; + tn.subType = TapNote::hold_head_hold; + nd.AddHoldNote( track, holdStart[track], row, tn ); + holdStart[track] = -1; + } + else + { + TapNote tn = TAP_ORIGINAL_TAP; + tn.iKeysoundIndex = GetKeysound(obj); + nd.SetTapNote( track, row, tn ); + if( hold ) holdStart[track] = row; + } + } + else if( channel == 1 || (11 <= channel && channel <= 19) || (21 <= channel && channel <= 29) ) // auto-keysound and other notes + { + for( int t = 0; t < tracks; t ++ ) + { + if( nd.GetTapNote( t, row ) == TAP_EMPTY && holdStart[t] == -1 ) + { + TapNote tn = TAP_ORIGINAL_TAP; + tn.type = TapNote::autoKeysound; + tn.iKeysoundIndex = GetKeysound(obj); + nd.SetTapNote( t, row, tn ); + break; + } + } + } + } + + delete transform; + delete holdStart; + + td.TidyUpData(); + out->SetNoteData(nd); + out->m_Timing = td; + out->TidyUpData(); + out->SetSavedToDisk( true ); // we're loading from disk, so this is by definintion already saved + + + return true; +} + +Steps *BMSChartReader::GetSteps() +{ + return out; +} + + + +struct BMSStepsInfo { + Steps *steps; + BMSChartInfo info; +}; + +class BMSSongLoader +{ + RString dir; + BMSSong song; + vector loadedSteps; +public: + BMSSongLoader( RString songDir, Song *outSong ); + bool Load( RString fileName ); + void AddToSong(); +}; + +BMSSongLoader::BMSSongLoader( RString songDir, Song *outSong ): song(outSong), dir(songDir) +{ +} + +bool BMSSongLoader::Load( RString fileName ) +{ + + // before doing anything else, load the chart first! + BMSChart chart; + if( !chart.Load( dir + fileName ) ) return false; + + // and then read the chart into the steps. + Steps *steps = song.GetSong()->CreateSteps(); + steps->SetFilename( dir + fileName ); + + BMSChartReader reader( &chart, steps, &song ); + if( !reader.Read() ) + { + delete steps; + return false; + } + + // add it to our song + song.GetSong()->AddSteps( steps ); + + // add the chart reader instance to our list. + BMSStepsInfo si = { steps, reader.info }; + loadedSteps.push_back(si); + + return true; + +} + +void BMSSongLoader::AddToSong() +{ + + RString commonSubstring = ""; + + { + bool found = false; + for( unsigned i = 0; i < loadedSteps.size(); i ++ ) + { + if( loadedSteps[i].info.title == "" ) continue; + if( !found ) + { + commonSubstring = loadedSteps[i].info.title; + found = true; + } + else + { + commonSubstring = FindLargestInitialSubstring( commonSubstring, loadedSteps[i].info.title ); + } + } + if( commonSubstring == "" ) + { + // All bets are off; the titles don't match at all. + // At this rate we're lucky if we even get the title right. + LOG->UserLog( "Song", dir, "has BMS files with inconsistent titles." ); } } @@ -1228,27 +938,85 @@ bool BMSLoader::LoadFromDir( const RString &sDir, Song &out ) // From here on in, it's nothing but guesswork. // Try to figure out the difficulty of each file. - for( unsigned i=0; iSetDifficulty( Difficulty_Medium ); + + RString title = loadedSteps[i].info.title; + // XXX: Is this really effective if Common Substring parsing failed? - Steps *pSteps = apSteps[i]; - pSteps->SetDifficulty( Difficulty_Medium ); - RString sTag; - if( GetTagFromMap( aBMSData[i], "#title", sTag ) ) - SearchForDifficulty( sTag, pSteps ); + if( title != "" ) SearchForDifficulty( title, steps ); + } + } + else + { + // Now, with our fancy little substring, trim the titles and + // figure out where each goes. + for( unsigned i = 0; i < loadedSteps.size(); i ++ ) + { + Steps *steps = loadedSteps[i].steps; + steps->SetDifficulty( Difficulty_Medium ); + + RString title = loadedSteps[i].info.title; + + if( title != "" && title.size() != commonSubstring.size() ) + { + RString tag = title.substr( commonSubstring.size(), title.size() - commonSubstring.size() ); + tag.MakeLower(); + + // XXX: We should do this with filenames too, I have plenty of examples. + // however, filenames will be trickier, as stuff at the beginning AND + // end change per-file, so we'll need a fancier FindLargestInitialSubstring() + + // XXX: This matches (double), but I haven't seen it used. Again, MORE EXAMPLES NEEDED + if( tag.find('l') != tag.npos ) + { + unsigned pos = tag.find('l'); + if( pos > 2 && tag.substr(pos - 2, 4) == "solo" ) + { + // (solo) -- an edit, apparently (Thanks Glenn!) + steps->SetDifficulty( Difficulty_Edit ); + } + else + { + // Any of [L7] [L14] (LIGHT7) (LIGHT14) (LIGHT) [L] ... you get the idea. + steps->SetDifficulty( Difficulty_Easy ); + } + } + // [A] (A) [ANOTHER] (ANOTHER) (ANOTHER7) Another (DP ANOTHER) (Another) -ANOTHER- [A7] [A14] etc etc etc + else if( tag.find('a') != tag.npos ) + steps->SetDifficulty( Difficulty_Hard ); + // XXX: Can also match (double), but should match [B] or [B7] + else if( tag.find('b') != tag.npos ) + steps->SetDifficulty( Difficulty_Beginner ); + // Other tags I've seen here include (5KEYS) (10KEYS) (7keys) (14keys) (dp) [MIX] [14] (14 Keys Mix) + // XXX: I'm sure [MIX] means something... anyone know? + } } } /* Prefer to read global tags from a Difficulty_Medium file. These tend to * have the least cruft in the #TITLE tag, so it's more likely to get a clean * title. */ - int iMainDataIndex = 0; - for( unsigned i=1; iGetDifficulty() == Difficulty_Medium ) - iMainDataIndex = i; + int mainIndex = 0; + for( unsigned i = 0; i < loadedSteps.size(); i ++ ) + if( loadedSteps[i].steps->GetDifficulty() == Difficulty_Medium ) + mainIndex = i; - ReadGlobalTags( aBMSData[iMainDataIndex], out ); - out.m_sSongFileName = out.GetSongDir() + arrayBMSFileNames[iMainDataIndex]; + Song *out = song.GetSong(); + + { + const BMSStepsInfo &main = loadedSteps[mainIndex]; + out->m_sSongFileName = main.steps->GetFilename(); + if( main.info.title != "" ) + NotesLoader::GetMainAndSubTitlesFromFullTitle( main.info.title, out->m_sMainTitle, out->m_sSubTitle ); + out->m_sArtist = main.info.artist; + out->m_sGenre = main.info.genre; + out->m_sBackgroundFile = main.info.backgroundFile; + out->m_sMusicFile = main.info.musicFile; + out->m_SongTiming = main.steps->m_Timing; + } // The brackets before the difficulty are in common substring, so remove them if it's found. if( commonSubstring.size() > 2 && commonSubstring[commonSubstring.size() - 2] == ' ' ) @@ -1261,42 +1029,68 @@ bool BMSLoader::LoadFromDir( const RString &sDir, Song &out ) commonSubstring = commonSubstring.substr(0, commonSubstring.size() - 2); } } - + // Override what that global tag said about the title if we have a good substring. // Prevents clobbering and catches "MySong (7keys)" / "MySong (Another) (7keys)" // Also catches "MySong (7keys)" / "MySong (14keys)" if( commonSubstring != "" ) - NotesLoader::GetMainAndSubTitlesFromFullTitle( commonSubstring, out.m_sMainTitle, out.m_sSubTitle ); + NotesLoader::GetMainAndSubTitlesFromFullTitle( commonSubstring, out->m_sMainTitle, out->m_sSubTitle ); - // Now that we've parsed the keysound data, load the Steps from the rest - // of the .bms files. - map mapFilenameToKeysoundIndex; - for( unsigned i=0; i(iMainDataIndex) ) - out.m_SongTiming = pNewNotes->m_Timing; - - pNewNotes->SetFilename(out.GetSongDir() + arrayBMSFileNames[i]); - out.AddSteps( pNewNotes ); - } - else - delete pNewNotes; - } + SlideDuplicateDifficulties( *out ); - SlideDuplicateDifficulties( out ); + ConvertString( out->m_sMainTitle, "utf-8,japanese" ); + ConvertString( out->m_sArtist, "utf-8,japanese" ); + ConvertString( out->m_sGenre, "utf-8,japanese" ); - ConvertString( out.m_sMainTitle, "utf-8,japanese" ); - ConvertString( out.m_sArtist, "utf-8,japanese" ); - ConvertString( out.m_sGenre, "utf-8,japanese" ); +} + +/*=========================================================================== + *=========================================================================== + *=========================================================================== + *=========================================================================== + */ + +bool BMSLoader::LoadNoteDataFromSimfile( const RString & cachePath, Steps & out ) +{ + Song *pSong = SONGMAN->GetSongFromSteps( &out ); + + // before doing anything else, load the chart first! + BMSChart chart; + if( !chart.Load( cachePath ) ) return false; + + BMSSong song(pSong); + + BMSChartReader reader( &chart, &out, &song ); + if( !reader.Read() ) return false; return true; } +bool BMSLoader::LoadFromDir( const RString &sDir, Song &out ) +{ + + LOG->Trace( "Song::LoadFromBMSDir(%s)", sDir.c_str() ); + + ASSERT( out.m_vsKeysoundFile.empty() ); + + vector arrayBMSFileNames; + GetApplicableFiles( sDir, arrayBMSFileNames ); + + /* We should have at least one; if we had none, we shouldn't have been + * called to begin with. */ + ASSERT( arrayBMSFileNames.size() ); + + BMSSongLoader loader( sDir, &out ); + for( unsigned i=0; i