#include "global.h" #include "NotesLoaderDWI.h" #include "RageLog.h" #include "MsdFile.h" #include "RageUtil.h" #include "RageUtil_CharConversions.h" #include "NoteData.h" #include "Song.h" #include "Steps.h" #include "GameInput.h" #include "NotesLoader.h" #include "PrefsManager.h" #include static std::map g_mapDanceNoteToNoteDataColumn; /** @brief The different types of core DWI arrows and pads. */ enum { DANCE_NOTE_NONE = 0, DANCE_NOTE_PAD1_LEFT, DANCE_NOTE_PAD1_UPLEFT, DANCE_NOTE_PAD1_DOWN, DANCE_NOTE_PAD1_UP, DANCE_NOTE_PAD1_UPRIGHT, DANCE_NOTE_PAD1_RIGHT, DANCE_NOTE_PAD2_LEFT, DANCE_NOTE_PAD2_UPLEFT, DANCE_NOTE_PAD2_DOWN, DANCE_NOTE_PAD2_UP, DANCE_NOTE_PAD2_UPRIGHT, DANCE_NOTE_PAD2_RIGHT }; /** * @brief Turn the individual character to the proper note. * @param c The character in question. * @param i The player. * @param note1Out The first result based on the character. * @param note2Out The second result based on the character. * @param sPath the path to the file. */ static void DWIcharToNote( char c, GameController i, int ¬e1Out, int ¬e2Out, const RString &sPath ) { switch( c ) { case '0': note1Out = DANCE_NOTE_NONE; note2Out = DANCE_NOTE_NONE; break; case '1': note1Out = DANCE_NOTE_PAD1_DOWN; note2Out = DANCE_NOTE_PAD1_LEFT; break; case '2': note1Out = DANCE_NOTE_PAD1_DOWN; note2Out = DANCE_NOTE_NONE; break; case '3': note1Out = DANCE_NOTE_PAD1_DOWN; note2Out = DANCE_NOTE_PAD1_RIGHT; break; case '4': note1Out = DANCE_NOTE_PAD1_LEFT; note2Out = DANCE_NOTE_NONE; break; case '5': note1Out = DANCE_NOTE_NONE; note2Out = DANCE_NOTE_NONE; break; case '6': note1Out = DANCE_NOTE_PAD1_RIGHT; note2Out = DANCE_NOTE_NONE; break; case '7': note1Out = DANCE_NOTE_PAD1_UP; note2Out = DANCE_NOTE_PAD1_LEFT; break; case '8': note1Out = DANCE_NOTE_PAD1_UP; note2Out = DANCE_NOTE_NONE; break; case '9': note1Out = DANCE_NOTE_PAD1_UP; note2Out = DANCE_NOTE_PAD1_RIGHT; break; case 'A': note1Out = DANCE_NOTE_PAD1_UP; note2Out = DANCE_NOTE_PAD1_DOWN; break; case 'B': note1Out = DANCE_NOTE_PAD1_LEFT; note2Out = DANCE_NOTE_PAD1_RIGHT; break; case 'C': note1Out = DANCE_NOTE_PAD1_UPLEFT; note2Out = DANCE_NOTE_NONE; break; case 'D': note1Out = DANCE_NOTE_PAD1_UPRIGHT; note2Out = DANCE_NOTE_NONE; break; case 'E': note1Out = DANCE_NOTE_PAD1_LEFT; note2Out = DANCE_NOTE_PAD1_UPLEFT; break; case 'F': note1Out = DANCE_NOTE_PAD1_UPLEFT; note2Out = DANCE_NOTE_PAD1_DOWN; break; case 'G': note1Out = DANCE_NOTE_PAD1_UPLEFT; note2Out = DANCE_NOTE_PAD1_UP; break; case 'H': note1Out = DANCE_NOTE_PAD1_UPLEFT; note2Out = DANCE_NOTE_PAD1_RIGHT; break; case 'I': note1Out = DANCE_NOTE_PAD1_LEFT; note2Out = DANCE_NOTE_PAD1_UPRIGHT; break; case 'J': note1Out = DANCE_NOTE_PAD1_DOWN; note2Out = DANCE_NOTE_PAD1_UPRIGHT; break; case 'K': note1Out = DANCE_NOTE_PAD1_UP; note2Out = DANCE_NOTE_PAD1_UPRIGHT; break; case 'L': note1Out = DANCE_NOTE_PAD1_UPRIGHT; note2Out = DANCE_NOTE_PAD1_RIGHT; break; case 'M': note1Out = DANCE_NOTE_PAD1_UPLEFT; note2Out = DANCE_NOTE_PAD1_UPRIGHT; break; default: LOG->UserLog( "Song file", sPath, "has an invalid DWI note character '%c'.", c ); note1Out = DANCE_NOTE_NONE; note2Out = DANCE_NOTE_NONE; break; } switch( i ) { case GameController_1: break; case GameController_2: if( note1Out != DANCE_NOTE_NONE ) note1Out += 6; if( note2Out != DANCE_NOTE_NONE ) note2Out += 6; break; default: ASSERT( false ); } } /** * @brief Determine the note column[s] to place notes. * @param c The character in question. * @param i The player. * @param col1Out The first result based on the character. * @param col2Out The second result based on the character. * @param sPath the path to the file. */ static void DWIcharToNoteCol( char c, GameController i, int &col1Out, int &col2Out, const RString &sPath ) { int note1, note2; DWIcharToNote( c, i, note1, note2, sPath ); if( note1 != DANCE_NOTE_NONE ) col1Out = g_mapDanceNoteToNoteDataColumn[note1]; else col1Out = -1; if( note2 != DANCE_NOTE_NONE ) col2Out = g_mapDanceNoteToNoteDataColumn[note2]; else col2Out = -1; } /** * @brief Determine if the note in question is a 192nd note. * * DWI used to use <...> to indicate 1/192nd notes; at some * point, <...> was changed to indicate jumps, and `' was used for * 1/192nds. So, we have to do a check to figure out what it really * means. If it contains 0s, it's most likely 192nds; otherwise, * it's most likely a jump. Search for a 0 before the next >: * @param sStepData the step data. * @param pos the position of the step data. * @return true if it's a 192nd note, false otherwise. */ static bool Is192( const RString &sStepData, size_t pos ) { while( pos < sStepData.size() ) { if( sStepData[pos] == '>' ) return false; if( sStepData[pos] == '0' ) return true; ++pos; } return false; } /** @brief All DWI files use 4 beats per measure. */ const int BEATS_PER_MEASURE = 4; /** * @brief Look through the notes tag to extract the data. * @param sMode the steps type. * @param sDescription the difficulty. * @param sNumFeet the meter. * @param sStepData1 the guaranteed step data. * @param sStepData2 used if sMode is double or couple. * @param out the step data. * @param sPath the path to the file. * @return the success or failure of the operation. */ static bool LoadFromDWITokens( RString sMode, RString sDescription, RString sNumFeet, RString sStepData1, RString sStepData2, Steps &out, const RString &sPath ) { CHECKPOINT_M( "DWILoader::LoadFromDWITokens()" ); out.m_StepsType = StepsType_Invalid; if( sMode == "SINGLE" ) out.m_StepsType = StepsType_dance_single; else if( sMode == "DOUBLE" ) out.m_StepsType = StepsType_dance_double; else if( sMode == "COUPLE" ) out.m_StepsType = StepsType_dance_couple; else if( sMode == "SOLO" ) out.m_StepsType = StepsType_dance_solo; else { ASSERT(0); // Unrecognized DWI notes format out.m_StepsType = StepsType_dance_single; } 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 ); } int iNumFeet = atoi(sNumFeet); // out.SetDescription(sDescription); // Don't put garbage in the description. out.SetMeter(iNumFeet); out.SetDifficulty( DwiCompatibleStringToDifficulty(sDescription) ); NoteData newNoteData; newNoteData.SetNumTracks( g_mapDanceNoteToNoteDataColumn.size() ); for( int pad=0; pad<2; pad++ ) // foreach pad { RString sStepData; switch( pad ) { case 0: sStepData = sStepData1; break; case 1: if( sStepData2 == "" ) // no data continue; // skip sStepData = sStepData2; 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': fCurrentIncrementer = 1.0/8 * BEATS_PER_MEASURE; break; default: // this is a note character { if( c == '!' ) { LOG->UserLog( "Song file", sPath, "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, sPath ); if( iCol1 != -1 ) newNoteData.SetTapNote(iCol1, iIndex, TAP_ORIGINAL_TAP); if( iCol2 != -1 ) newNoteData.SetTapNote(iCol2, iIndex, TAP_ORIGINAL_TAP); if( sStepData[i] == '!' ) { i++; const char holdChar = sStepData[i++]; DWIcharToNoteCol( holdChar, (GameController)pad, iCol1, iCol2, sPath ); 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; tUserLog( "Song file", sPath, "failed to close a hold note in \"%s\" on track %i", sDescription.c_str(), t ); newNoteData.SetTapNote( t, iHeadRow, TAP_EMPTY ); } } } ASSERT( newNoteData.GetNumTracks() > 0 ); out.SetNoteData( newNoteData ); out.TidyUpData(); out.SetSavedToDisk( true ); // we're loading from disk, so this is by definintion already saved return true; } /** * @brief Turn the DWI style timestamp into a compatible time for our system. * * This value can be in either "HH:MM:SS.sssss", "MM:SS.sssss", "SSS.sssss" * or milliseconds. * @param arg1 Either hours, minutes, or seconds, depending on other args. * @param arg2 Either minutes or seconds, depending on other args. * @param arg3 Seconds if not empty. * @return the proper timestamp. */ static float ParseBrokenDWITimestamp( const RString &arg1, const RString &arg2, const RString &arg3 ) { if( arg1.empty() ) return 0; /* 1+ args */ if( arg2.empty() ) { /* If the value contains a period, treat it as seconds; otherwise ms. */ if( arg1.find_first_of(".") != arg1.npos ) return StringToFloat( arg1 ); else return StringToFloat( arg1 ) / 1000.f; } /* 2+ args */ if( arg3.empty() ) return HHMMSSToSeconds( arg1+":"+arg2 ); /* 3+ args */ return HHMMSSToSeconds( arg1+":"+arg2+":"+arg3 ); } void DWILoader::GetApplicableFiles( const RString &sPath, vector &out ) { GetDirListing( sPath + RString("*.dwi"), out ); } bool DWILoader::LoadFromDir( const RString &sPath_, Song &out, set &BlacklistedImages ) { vector aFileNames; GetApplicableFiles( sPath_, aFileNames ); if( aFileNames.size() > 1 ) { LOG->UserLog( "Song", sPath_, "has more than one DWI file. There should be only one!" ); return false; } /* We should have exactly one; if we had none, we shouldn't have been called to begin with. */ ASSERT( aFileNames.size() == 1 ); const RString sPath = sPath_ + aFileNames[0]; LOG->Trace( "Song::LoadFromDWIFile(%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; } for( unsigned i=0; iUserLog( "Song file", sPath, "has tag \"%s\" with no parameters.", sValueName.c_str() ); continue; } // handle the data if( 0==stricmp(sValueName,"FILE") ) out.m_sMusicFile = sParams[1]; else if( 0==stricmp(sValueName,"TITLE") ) { NotesLoader::GetMainAndSubTitlesFromFullTitle( sParams[1], out.m_sMainTitle, out.m_sSubTitle ); /* As far as I know, there's no spec on the encoding of this text. (I didn't * look very hard, though.) I've seen at least one file in ISO-8859-1. */ ConvertString( out.m_sMainTitle, "utf-8,english" ); ConvertString( out.m_sSubTitle, "utf-8,english" ); } else if( 0==stricmp(sValueName,"ARTIST") ) { out.m_sArtist = sParams[1]; ConvertString( out.m_sArtist, "utf-8,english" ); } else if( 0==stricmp(sValueName,"GENRE") ) { out.m_sGenre = sParams[1]; ConvertString( out.m_sGenre, "utf-8,english" ); } else if( 0==stricmp(sValueName,"CDTITLE") ) out.m_sCDTitleFile = sParams[1]; else if( 0==stricmp(sValueName,"BPM") ) { const float fBPM = StringToFloat( sParams[1] ); if( PREFSMAN->m_bQuirksMode ) { out.AddBPMSegment( BPMSegment(0, fBPM) ); } else{ if( fBPM > 0.0f ) out.AddBPMSegment( BPMSegment(0, fBPM) ); else LOG->UserLog( "Song file", sPath, "has an invalid BPM change at beat %f, BPM %f.", NoteRowToBeat(0), fBPM ); } } else if( 0==stricmp(sValueName,"DISPLAYBPM") ) { // #DISPLAYBPM:[xxx..xxx]|[xxx]|[*]; int iMin, iMax; /* We can't parse this as a float with sscanf, since '.' is a valid * character in a float. (We could do it with a regex, but it's not * worth bothering with since we don't display fractional BPM anyway.) */ if( sscanf( sParams[1], "%i..%i", &iMin, &iMax ) == 2 ) { out.m_DisplayBPMType = DISPLAY_BPM_SPECIFIED; out.m_fSpecifiedBPMMin = (float) iMin; out.m_fSpecifiedBPMMax = (float) iMax; } else if( sscanf( sParams[1], "%i", &iMin ) == 1 ) { out.m_DisplayBPMType = DISPLAY_BPM_SPECIFIED; out.m_fSpecifiedBPMMin = out.m_fSpecifiedBPMMax = (float) iMin; } else { out.m_DisplayBPMType = DISPLAY_BPM_RANDOM; } } else if( 0==stricmp(sValueName,"GAP") ) // the units of GAP is 1/1000 second out.m_Timing.m_fBeat0OffsetInSeconds = -atoi( sParams[1] ) / 1000.0f; else if( 0==stricmp(sValueName,"SAMPLESTART") ) out.m_fMusicSampleStartSeconds = ParseBrokenDWITimestamp(sParams[1], sParams[2], sParams[3]); else if( 0==stricmp(sValueName,"SAMPLELENGTH") ) out.m_fMusicSampleLengthSeconds = ParseBrokenDWITimestamp(sParams[1], sParams[2], sParams[3]); else if( 0==stricmp(sValueName,"FREEZE") ) { vector arrayFreezeExpressions; split( sParams[1], ",", arrayFreezeExpressions ); for( unsigned f=0; f arrayFreezeValues; split( arrayFreezeExpressions[f], "=", arrayFreezeValues ); if( arrayFreezeValues.size() != 2 ) { LOG->UserLog( "Song file", sPath, "has an invalid FREEZE: '%s'.", arrayFreezeExpressions[f].c_str() ); continue; } int iFreezeRow = BeatToNoteRow( StringToFloat(arrayFreezeValues[0]) / 4.0f ); float fFreezeSeconds = StringToFloat( arrayFreezeValues[1] ) / 1000.0f; out.AddStopSegment( StopSegment(iFreezeRow, fFreezeSeconds) ); // LOG->Trace( "Adding a freeze segment: beat: %f, seconds = %f", fFreezeBeat, fFreezeSeconds ); } } else if( 0==stricmp(sValueName,"CHANGEBPM") || 0==stricmp(sValueName,"BPMCHANGE") ) { vector arrayBPMChangeExpressions; split( sParams[1], ",", arrayBPMChangeExpressions ); for( unsigned b=0; b arrayBPMChangeValues; split( arrayBPMChangeExpressions[b], "=", arrayBPMChangeValues ); if( arrayBPMChangeValues.size() != 2 ) { LOG->UserLog( "Song file", sPath, "has an invalid CHANGEBPM: '%s'.", arrayBPMChangeExpressions[b].c_str() ); continue; } int iStartIndex = BeatToNoteRow( StringToFloat(arrayBPMChangeValues[0]) / 4.0f ); float fBPM = StringToFloat( arrayBPMChangeValues[1] ); if( fBPM > 0.0f ) { BPMSegment bs( iStartIndex, fBPM ); out.AddBPMSegment( bs ); } else { LOG->UserLog( "Song file", sPath, "has an invalid BPM change at beat %f, BPM %f.", NoteRowToBeat(iStartIndex), fBPM ); } } } else if( 0==stricmp(sValueName,"SINGLE") || 0==stricmp(sValueName,"DOUBLE") || 0==stricmp(sValueName,"COUPLE") || 0==stricmp(sValueName,"SOLO") ) { Steps* pNewNotes = new Steps; LoadFromDWITokens( sParams[0], sParams[1], sParams[2], sParams[3], (iNumParams==5) ? sParams[4] : RString(""), *pNewNotes, sPath ); if( pNewNotes->m_StepsType != StepsType_Invalid ) out.AddSteps( pNewNotes ); else delete pNewNotes; } else if( 0==stricmp(sValueName,"DISPLAYTITLE") || 0==stricmp(sValueName,"DISPLAYARTIST") ) { /* We don't want to support these tags. However, we don't want * to pick up images used here as song images (eg. banners). */ RString param = sParams[1]; /* "{foo} ... {foo2}" */ size_t pos = 0; while( pos < RString::npos ) { size_t startpos = param.find('{', pos); if( startpos == RString::npos ) break; size_t endpos = param.find('}', startpos); if( endpos == RString::npos ) break; RString sub = param.substr( startpos+1, endpos-startpos-1 ); pos = endpos + 1; sub.MakeLower(); BlacklistedImages.insert( sub ); } } else { // do nothing. We don't care about this value name } } return true; } /* * (c) 2001-2004 Chris Danford, Glenn Maynard * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, and/or sell copies of the Software, and to permit persons to * whom the Software is furnished to do so, provided that the above * copyright notice(s) and this permission notice appear in all copies of * the Software and that both the above copyright notice(s) and this * permission notice appear in supporting documentation. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF * THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS * INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT * OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS * OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR * PERFORMANCE OF THIS SOFTWARE. */