bool SMLoader::LoadTimingFromFile( const CString &fn, TimingData &out ) { MsdFile msd; if( !msd.ReadFile( fn ) ) { LOG->Warn( "Couldn't load %s, \"%s\"", fn.c_str(), msd.GetError().c_str() ); return false; } out.m_sFile = fn; LoadTimingFromSMFile( msd, out ); return true; }
static bool LoadGlobalData( const std::string &sPath, Song &out, bool &bKIUCompliant ) { 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; } // changed up there in case of something is found inside the SONGFILE tag in the head ksf -DaisuMaster // search for music with song in the file name vector<std::string> arrayPossibleMusic; GetDirListing( out.GetSongDir() + std::string("song.mp3"), arrayPossibleMusic ); GetDirListing( out.GetSongDir() + std::string("song.oga"), arrayPossibleMusic ); GetDirListing( out.GetSongDir() + std::string("song.ogg"), arrayPossibleMusic ); GetDirListing( out.GetSongDir() + std::string("song.wav"), arrayPossibleMusic ); if( !arrayPossibleMusic.empty() ) // we found a match { out.m_sMusicFile = arrayPossibleMusic[0]; } // ^this was below, at the end float SMGap1 = 0, SMGap2 = 0, BPM1 = -1, BPMPos2 = -1, BPM2 = -1, BPMPos3 = -1, BPM3 = -1; int iTickCount = -1; bKIUCompliant = false; vector<std::string> vNoteRows; for( unsigned i=0; i < msd.GetNumValues(); i++ ) { const MsdFile::value_t &sParams = msd.GetValue(i); std::string sValueName = Rage::make_upper(sParams[0]); // handle the data if( sValueName=="TITLE" ) LoadTags(sParams[1], out); else if( sValueName=="BPM" ) { BPM1 = StringToFloat(sParams[1]); out.m_SongTiming.AddSegment( BPMSegment(0, BPM1) ); } else if( sValueName=="BPM2" ) { bKIUCompliant = true; BPM2 = StringToFloat( sParams[1] ); } else if( sValueName=="BPM3" ) { bKIUCompliant = true; BPM3 = StringToFloat( sParams[1] ); } else if( sValueName=="BUNKI" ) { bKIUCompliant = true; BPMPos2 = StringToFloat( sParams[1] ) / 100.0f; } else if( sValueName=="BUNKI2" ) { bKIUCompliant = true; BPMPos3 = StringToFloat( sParams[1] ) / 100.0f; } else if( sValueName=="STARTTIME" ) { SMGap1 = -StringToFloat( sParams[1] )/100; out.m_SongTiming.set_offset(SMGap1); } // This is currently required for more accurate KIU BPM changes. else if( sValueName=="STARTTIME2" ) { bKIUCompliant = true; SMGap2 = -StringToFloat( sParams[1] )/100; } else if ( sValueName=="STARTTIME3" ) { // STARTTIME3 only ensures this is a KIU compliant simfile. //bKIUCompliant = true; } else if ( sValueName=="TICKCOUNT" ) { ProcessTickcounts(sParams[1], iTickCount, out.m_SongTiming); } else if ( 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. */ std::string theSteps = Rage::trim_left(sParams[1]); auto toDump = Rage::split(theSteps, "\n", Rage::EmptyEntries::skip); vNoteRows.insert(vNoteRows.end(), std::make_move_iterator(toDump.begin()), std::make_move_iterator(toDump.end())); } else if( sValueName=="DIFFICULTY" || sValueName=="PLAYER" ) { /* DIFFICULTY and PLAYER are handled only in LoadFromKSFFile. Ignore those here. */ continue; } // New cases noted in Aldo_MX's code: else if( sValueName=="MUSICINTRO" || sValueName=="INTRO" ) { out.m_fMusicSampleStartSeconds = HHMMSSToSeconds( sParams[1] ); } else if( sValueName=="TITLEFILE" ) { out.m_sBackgroundFile = sParams[1]; } else if( sValueName=="DISCFILE" ) { out.m_sBannerFile = sParams[1]; } else if( sValueName=="SONGFILE" ) { out.m_sMusicFile = sParams[1]; } //else if( sValueName=="INTROFILE" ) //{ // nothing to add... //} // end new cases else { LOG->UserLog( "Song file", sPath, "has an unexpected value named \"%s\".", sValueName.c_str() ); } } //intro length in piu mixes is generally 7 seconds out.m_fMusicSampleLengthSeconds = 7.0f; /* BPM Change checks are done here. If 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( bKIUCompliant ) { if( BPM2 > 0 && BPMPos2 > 0 ) { HandleBunki( out.m_SongTiming, BPM1, BPM2, SMGap1, BPMPos2 ); } if( BPM3 > 0 && BPMPos3 > 0 ) { HandleBunki( out.m_SongTiming, BPM2, BPM3, SMGap2, BPMPos3 ); } } else { float fCurBeat = 0.0f; bool bDMRequired = false; for (auto &NoteRowString: vNoteRows) { NoteRowString = Rage::trim_right(NoteRowString, "\r\n"); if( NoteRowString == "" ) continue; // ignore empty rows. if( NoteRowString == "2222222222222" ) // Row of 2s = end. Confirm KIUCompliency here. { if (!bDMRequired) { bKIUCompliant = true; } break; } // This is where the DMRequired test will take place. if ( Rage::starts_with( NoteRowString, "|" ) ) { // have a static timing for everything bDMRequired = true; continue; } else { // ignore whatever else... //continue; } fCurBeat += 1.0f / iTickCount; } } // Try to fill in missing bits of information from the pathname. { auto asBits = Rage::split(sPath, "/", Rage::EmptyEntries::skip); ASSERT( asBits.size() > 1 ); LoadTags( asBits[asBits.size()-2], out ); } return true; }
static bool LoadFromKSFFile( const std::string &sPath, Steps &out, Song &song, bool bKIUCompliant ) { using std::max; LOG->Trace( "Steps::LoadFromKSFFile( '%s' )", sPath.c_str() ); MsdFile msd; if( !msd.ReadFile( sPath, false ) ) // don't unescape { LOG->UserLog( "Song file", sPath, "couldn't be opened: %s", msd.GetError().c_str() ); return false; } // this is the value we read for TICKCOUNT int iTickCount = -1; // used to adapt weird tickcounts //float fScrollRatio = 1.0f; -- uncomment when ready to use. vector<std::string> vNoteRows; // According to Aldo_MX, there is a default BPM and it's 60. -aj bool bDoublesChart = false; TimingData stepsTiming; float SMGap1 = 0, SMGap2 = 0, BPM1 = -1, BPMPos2 = -1, BPM2 = -1, BPMPos3 = -1, BPM3 = -1; for( unsigned i=0; i<msd.GetNumValues(); i++ ) { const MsdFile::value_t &sParams = msd.GetValue( i ); std::string sValueName = Rage::make_upper(sParams[0]); /* handle the data...well, not this data: not related to steps. * Skips INTRO, MUSICINTRO, TITLEFILE, DISCFILE, SONGFILE. */ if (sValueName=="TITLE" || Rage::ends_with(sValueName, "INTRO") || Rage::ends_with(sValueName, "FILE") ) { } else if( sValueName=="BPM" ) { BPM1 = StringToFloat(sParams[1]); stepsTiming.AddSegment( BPMSegment(0, BPM1) ); } else if( sValueName=="BPM2" ) { if (bKIUCompliant) { BPM2 = StringToFloat( sParams[1] ); } else { // LOG an error. } } else if( sValueName=="BPM3" ) { if (bKIUCompliant) { BPM3 = StringToFloat( sParams[1] ); } else { // LOG an error. } } else if( sValueName=="BUNKI" ) { if (bKIUCompliant) { BPMPos2 = StringToFloat( sParams[1] ) / 100.0f; } else { // LOG an error. } } else if( sValueName=="BUNKI2" ) { if (bKIUCompliant) { BPMPos3 = StringToFloat( sParams[1] ) / 100.0f; } else { // LOG an error. } } else if( sValueName=="STARTTIME" ) { SMGap1 = -StringToFloat( sParams[1] )/100; stepsTiming.set_offset(SMGap1); } // This is currently required for more accurate KIU BPM changes. else if( sValueName=="STARTTIME2" ) { if (bKIUCompliant) { SMGap2 = -StringToFloat( sParams[1] )/100; } else { // LOG an error. } } else if ( sValueName=="STARTTIME3" ) { // STARTTIME3 only ensures this is a KIU compliant simfile. bKIUCompliant = true; } else if( sValueName=="TICKCOUNT" ) { iTickCount = StringToInt( sParams[1] ); if( iTickCount <= 0 ) { LOG->UserLog( "Song file", sPath, "has an invalid tick count: %d.", iTickCount ); return false; } stepsTiming.AddSegment( TickcountSegment(0, iTickCount)); } else if( sValueName=="DIFFICULTY" ) { out.SetMeter( max(StringToInt(sParams[1]), 1) ); } // new cases from Aldo_MX's fork: else if( sValueName=="PLAYER" ) { std::string sPlayer = Rage::make_lower(sParams[1]); if( sPlayer.find( "double" ) != string::npos ) { bDoublesChart = true; } } // This should always be last. else if( sValueName=="STEP" ) { std::string theSteps = Rage::trim_left(sParams[1]); auto toDump = Rage::split(theSteps, "\n", Rage::EmptyEntries::skip); vNoteRows.insert(vNoteRows.end(), std::make_move_iterator(toDump.begin()), std::make_move_iterator(toDump.end())); } } if( iTickCount == -1 ) { iTickCount = 4; LOG->UserLog( "Song file", sPath, "doesn't have a TICKCOUNT. Defaulting to %i.", iTickCount ); } // Prepare BPM stuff already if the file uses KSF syntax. if( bKIUCompliant ) { if( BPM2 > 0 && BPMPos2 > 0 ) { HandleBunki( stepsTiming, BPM1, BPM2, SMGap1, BPMPos2 ); } if( BPM3 > 0 && BPMPos3 > 0 ) { HandleBunki( stepsTiming, BPM2, BPM3, SMGap2, BPMPos3 ); } } NoteData notedata; // read it into here { std::string sDir, sFName, sExt; splitpath( sPath, sDir, sFName, sExt ); sFName = Rage::make_lower(sFName); out.SetDescription(sFName); // Check another before anything else... is this okay? -DaisuMaster if( sFName.find("another") != string::npos ) { out.SetDifficulty( Difficulty_Edit ); if( !out.GetMeter() ) out.SetMeter( 25 ); } else if(sFName.find("wild") != string::npos || sFName.find("wd") != string::npos || sFName.find("crazy+") != string::npos || sFName.find("cz+") != string::npos || sFName.find("hardcore") != string::npos ) { out.SetDifficulty( Difficulty_Challenge ); if( !out.GetMeter() ) out.SetMeter( 20 ); } else if(sFName.find("crazy") != string::npos || sFName.find("cz") != string::npos || sFName.find("nightmare") != string::npos || sFName.find("nm") != string::npos || sFName.find("crazydouble") != string::npos ) { out.SetDifficulty( Difficulty_Hard ); if( !out.GetMeter() ) out.SetMeter( 14 ); // Set the meters to the Pump scale, not DDR. } else if(sFName.find("hard") != string::npos || sFName.find("hd") != string::npos || sFName.find("freestyle") != string::npos || sFName.find("fs") != string::npos || sFName.find("double") != string::npos ) { out.SetDifficulty( Difficulty_Medium ); if( !out.GetMeter() ) out.SetMeter( 8 ); } else if(sFName.find("easy") != string::npos || sFName.find("ez") != string::npos || sFName.find("normal") != string::npos ) { // I wonder if I should leave easy fall into the Beginner difficulty... -DaisuMaster out.SetDifficulty( Difficulty_Easy ); if( !out.GetMeter() ) out.SetMeter( 4 ); } else if(sFName.find("beginner") != string::npos || sFName.find("practice") != string::npos || sFName.find("pr") != string::npos ) { out.SetDifficulty( Difficulty_Beginner ); if( !out.GetMeter() ) out.SetMeter( 4 ); } else { out.SetDifficulty( Difficulty_Hard ); if( !out.GetMeter() ) out.SetMeter( 10 ); } out.m_StepsType = StepsType_pump_single; // Check for "halfdouble" before "double". if(sFName.find("halfdouble") != string::npos || sFName.find("half-double") != string::npos || sFName.find("h_double") != string::npos || sFName.find("hdb") != string::npos ) out.m_StepsType = StepsType_pump_halfdouble; // Handle bDoublesChart from above as well. -aj else if(sFName.find("double") != string::npos || sFName.find("nightmare") != string::npos || sFName.find("freestyle") != string::npos || sFName.find("db") != string::npos || sFName.find("nm") != string::npos || sFName.find("fs") != string::npos || bDoublesChart ) out.m_StepsType = StepsType_pump_double; else if( sFName.find("_1") != string::npos ) out.m_StepsType = StepsType_pump_single; else if( sFName.find("_2") != string::npos ) out.m_StepsType = StepsType_pump_couple; } switch( out.m_StepsType ) { case StepsType_pump_single: notedata.SetNumTracks( 5 ); break; case StepsType_pump_couple: notedata.SetNumTracks( 10 ); break; case StepsType_pump_double: notedata.SetNumTracks( 10 ); break; case StepsType_pump_routine: notedata.SetNumTracks( 10 ); break; // future files may have this? case StepsType_pump_halfdouble: notedata.SetNumTracks( 6 ); break; default: FAIL_M( fmt::sprintf("%i", out.m_StepsType) ); } int t = 0; int iHoldStartRow[13]; for( t=0; t<13; t++ ) iHoldStartRow[t] = -1; bool bTickChangeNeeded = false; int newTick = -1; float fCurBeat = 0.0f; float prevBeat = 0.0f; // Used for hold tails. for (auto &sRowString: vNoteRows) { sRowString = Rage::trim_right(sRowString, "\r\n"); if( sRowString == "" ) { continue; // skip } // All 2s indicates the end of the song. if( sRowString == "2222222222222" ) { // Finish any holds that didn't get...well, finished. for( t=0; t < notedata.GetNumTracks(); t++ ) { if( iHoldStartRow[t] != -1 ) // this ends the hold { if( iHoldStartRow[t] == BeatToNoteRow(prevBeat) ) { notedata.SetTapNote( t, iHoldStartRow[t], TAP_ORIGINAL_TAP ); } else { notedata.AddHoldNote(t, iHoldStartRow[t], BeatToNoteRow(prevBeat), TAP_ORIGINAL_HOLD_HEAD ); } } } /* have this row be the last moment in the song, unless * a future step ends later. */ //float curTime = stepsTiming.GetElapsedTimeFromBeat(fCurBeat); //if (curTime > song.GetSpecifiedLastSecond()) //{ // song.SetSpecifiedLastSecond(curTime); //} song.SetSpecifiedLastSecond( song.GetSpecifiedLastSecond() + 4 ); break; } else if( Rage::starts_with(sRowString, "|") ) { /* if (bKIUCompliant) { // Log an error, ignore the line. continue; } */ // gotta do something tricky here: if the bpm is below one then a couple of calculations // for scrollsegments will be made, example, bpm 0.2, tick 4000, the scrollsegment will // be 0. if the tickcount is non a stepmania standard then it will be adapted, a scroll // segment will then be added based on approximations. -DaisuMaster // eh better do it considering the tickcount (high tickcounts) // I'm making some experiments, please spare me... //continue; std::string temp = sRowString.substr(2,sRowString.size()-3); float numTemp = StringToFloat(temp); if (Rage::starts_with(sRowString, "|T")) { // duh iTickCount = static_cast<int>(numTemp); // I have been owned by the man -DaisuMaster stepsTiming.SetTickcountAtBeat( fCurBeat, Rage::clamp(iTickCount, 0, ROWS_PER_BEAT) ); } else if (Rage::starts_with(sRowString, "|B")) { // BPM stepsTiming.SetBPMAtBeat( fCurBeat, numTemp ); } else if (Rage::starts_with(sRowString, "|E")) { // DelayBeat float fCurDelay = 60 / stepsTiming.GetBPMAtBeat(fCurBeat) * numTemp / iTickCount; fCurDelay += stepsTiming.GetDelayAtRow(BeatToNoteRow(fCurBeat) ); stepsTiming.SetDelayAtBeat( fCurBeat, fCurDelay ); } else if (Rage::starts_with(sRowString, "|D")) { // Delays float fCurDelay = stepsTiming.GetStopAtRow(BeatToNoteRow(fCurBeat) ); fCurDelay += numTemp / 1000; stepsTiming.SetDelayAtBeat( fCurBeat, fCurDelay ); } else if (Rage::starts_with(sRowString, "|M") || Rage::starts_with(sRowString, "|C")) { // multipliers/combo ComboSegment seg( BeatToNoteRow(fCurBeat), int(numTemp) ); stepsTiming.AddSegment( seg ); } else if (Rage::starts_with(sRowString, "|S")) { // speed segments } else if (Rage::starts_with(sRowString, "|F")) { // fakes } else if (Rage::starts_with(sRowString, "|X")) { // scroll segments ScrollSegment seg = ScrollSegment( BeatToNoteRow(fCurBeat), numTemp ); stepsTiming.AddSegment( seg ); //return true; } continue; } // Half-doubles is offset; "0011111100000". if( out.m_StepsType == StepsType_pump_halfdouble ) sRowString.erase( 0, 2 ); // Update TICKCOUNT for Direct Move files. if( bTickChangeNeeded ) { iTickCount = newTick; bTickChangeNeeded = false; } for( t=0; t < notedata.GetNumTracks(); t++ ) { if( sRowString[t] == '4' ) { /* Remember when each hold starts; ignore the middle. */ if( iHoldStartRow[t] == -1 ) iHoldStartRow[t] = BeatToNoteRow(fCurBeat); continue; } if( iHoldStartRow[t] != -1 ) // this ends the hold { int iEndRow = BeatToNoteRow(prevBeat); if( iHoldStartRow[t] == iEndRow ) notedata.SetTapNote( t, iHoldStartRow[t], TAP_ORIGINAL_TAP ); else { //notedata.AddHoldNote( t, iHoldStartRow[t], iEndRow , TAP_ORIGINAL_PUMP_HEAD ); notedata.AddHoldNote( t, iHoldStartRow[t], iEndRow , TAP_ORIGINAL_HOLD_HEAD ); } iHoldStartRow[t] = -1; } TapNote tap; switch( sRowString[t] ) { case '0': tap = TAP_EMPTY; break; case '1': tap = TAP_ORIGINAL_TAP; break; //allow setting more notetypes on ksf files, this may come in handy (it should) -DaisuMaster case 'M': case 'm': tap = TAP_ORIGINAL_MINE; break; case 'F': case 'f': tap = TAP_ORIGINAL_FAKE; break; case 'L': case 'l': tap = TAP_ORIGINAL_LIFT; break; default: LOG->UserLog( "Song file", sPath, "has an invalid row \"%s\"; corrupt notes ignored.", sRowString.c_str() ); //return false; tap = TAP_EMPTY; break; } notedata.SetTapNote(t, BeatToNoteRow(fCurBeat), tap); } prevBeat = fCurBeat; fCurBeat = prevBeat + 1.0f / iTickCount; } out.SetNoteData( notedata ); out.m_Timing = stepsTiming; out.TidyUpData(); out.SetSavedToDisk( true ); // we're loading from disk, so this is by definintion already saved return true; }
void MsdTest() { g_TestFilename = "file"; /* Read check. */ do { g_TestFile = "#FOO;"; g_BytesUntilError = -1; MsdFile test; if( !test.ReadFile("test/file", false) ) Fail( "MSD: ReadFile failed: %s", test.GetError().c_str() ); if( test.GetNumValues() != 1 ) Fail( "MSD: GetNumValues: expected 1, got %i", test.GetNumValues() ); if( test.GetNumParams(0) != 1 ) Fail( "MSD: GetNumParams(0): expected 1, got %i", test.GetNumParams(0) ); RString sStr = test.GetValue(0)[0]; if( sStr != "FOO" ) Fail( "MSD: GetValue failed: expected \"FOO\", got \"%s\"", sStr.c_str() ); } while(false); /* Read error check. */ do { g_TestFile = "#FOO:BAR:BAZ;"; g_BytesUntilError = 5; MsdFile test; if( test.ReadFile("test/file", false) ) Fail( "MSD: ReadFile should have failed" ); if( test.GetError() != "Fake error" ) Fail( "MSD: ReadFile error check: wrong error return: got \"%s\"", test.GetError().c_str() ); } while(false); }
bool UnlockSystem::Load() { LOG->Trace( "UnlockSystem::Load()" ); if( !IsAFile(UNLOCKS_PATH) ) return false; MsdFile msd; if( !msd.ReadFile( UNLOCKS_PATH ) ) { LOG->Warn( "Error opening file '%s' for reading: %s.", UNLOCKS_PATH, msd.GetError().c_str() ); return false; } unsigned i; for( i=0; i<msd.GetNumValues(); i++ ) { int iNumParams = msd.GetNumParams(i); const MsdFile::value_t &sParams = msd.GetValue(i); CString sValueName = sParams[0]; if( iNumParams < 1 ) { LOG->Warn("Got \"%s\" tag with no parameters", sValueName.c_str()); continue; } if( !stricmp(sParams[0],"ROULETTE") ) { for( unsigned j = 1; j < sParams.params.size(); ++j ) m_RouletteCodes.insert( atoi(sParams[j]) ); continue; } if( stricmp(sParams[0],"UNLOCK") ) { LOG->Warn("Unrecognized unlock tag \"%s\", ignored.", sValueName.c_str()); continue; } UnlockEntry current; current.m_sSongName = sParams[1]; LOG->Trace("Song entry: %s", current.m_sSongName.c_str() ); CStringArray UnlockTypes; split( sParams[2], ",", UnlockTypes ); for( unsigned j=0; j<UnlockTypes.size(); ++j ) { CStringArray readparam; split( UnlockTypes[j], "=", readparam ); const CString &unlock_type = readparam[0]; LOG->Trace("UnlockTypes line: %s", UnlockTypes[j].c_str() ); const float fVal = strtof( readparam[1], NULL ); const int iVal = atoi( readparam[1] ); const UnlockType ut = StringToUnlockType( unlock_type ); if( ut != UNLOCK_INVALID ) current.m_fRequired[ut] = fVal; if( unlock_type == "CODE" ) current.m_iCode = iVal; if( unlock_type == "RO" ) { current.m_iCode = iVal; m_RouletteCodes.insert( iVal ); } } m_SongEntries.push_back(current); } UpdateSongs(); for(i=0; i < m_SongEntries.size(); i++) { CString str = ssprintf( "Unlock: %s; ", m_SongEntries[i].m_sSongName.c_str() ); for( int j = 0; j < NUM_UNLOCK_TYPES; ++j ) if( m_SongEntries[i].m_fRequired[j] ) str += ssprintf( "%s = %f; ", g_UnlockNames[j], m_SongEntries[i].m_fRequired[j] ); str += ssprintf( "code = %i ", m_SongEntries[i].m_iCode ); str += m_SongEntries[i].IsLocked()? "locked":"unlocked"; if( m_SongEntries[i].m_pSong ) str += ( " (found song)" ); if( m_SongEntries[i].m_pCourse ) str += ( " (found course)" ); LOG->Trace( "%s", str.c_str() ); } return true; }
bool KSFLoader::LoadFromKSFFile( const CString &sPath, Steps &out, const Song &song ) { LOG->Trace( "Steps::LoadFromKSFFile( '%s' )", sPath.c_str() ); MsdFile msd; if( !msd.ReadFile( sPath ) ) RageException::Throw( "Error opening file '%s'.", sPath.c_str() ); int iTickCount = -1; // this is the value we read for TICKCOUNT CStringArray asRows; for( unsigned i=0; i<msd.GetNumValues(); i++ ) { const MsdFile::value_t &sParams = msd.GetValue(i); CString sValueName = sParams[0]; // handle the data if( 0==stricmp(sValueName,"TICKCOUNT") ) iTickCount = atoi(sParams[1]); else if( 0==stricmp(sValueName,"STEP") ) { CString step = sParams[1]; TrimLeft(step); split( step, "\n", asRows, true ); } else if( 0==stricmp(sValueName,"DIFFICULTY") ) out.SetMeter(atoi(sParams[1])); } if( iTickCount == -1 ) { iTickCount = 2; LOG->Warn( "\"%s\": TICKCOUNT not found; defaulting to %i", sPath.c_str(), iTickCount ); } NoteData notedata; // read it into here { CString sDir, sFName, sExt; splitpath( sPath, sDir, sFName, sExt ); sFName.MakeLower(); out.SetDescription(sFName); if( sFName.Find("crazy")!=-1 ) { out.SetDifficulty(DIFFICULTY_HARD); if(!out.GetMeter()) out.SetMeter(8); } else if( sFName.Find("hard")!=-1 ) { out.SetDifficulty(DIFFICULTY_MEDIUM); if(!out.GetMeter()) out.SetMeter(5); } else if( sFName.Find("easy")!=-1 ) { out.SetDifficulty(DIFFICULTY_EASY); if(!out.GetMeter()) out.SetMeter(2); } else { out.SetDifficulty(DIFFICULTY_MEDIUM); if(!out.GetMeter()) out.SetMeter(5); } notedata.SetNumTracks( 5 ); out.m_StepsType = STEPS_TYPE_PUMP_SINGLE; /* Check for "halfdouble" before "double". */ if( sFName.Find("halfdouble") != -1 || sFName.Find("h_double") != -1 ) { notedata.SetNumTracks( 6 ); out.m_StepsType = STEPS_TYPE_PUMP_HALFDOUBLE; } else if( sFName.Find("double") != -1 ) { notedata.SetNumTracks( 10 ); out.m_StepsType = STEPS_TYPE_PUMP_DOUBLE; } else if( sFName.Find("_2") != -1 ) { notedata.SetNumTracks( 10 ); out.m_StepsType = STEPS_TYPE_PUMP_COUPLE; } } int iHoldStartRow[13]; int t; for( t=0; t<13; t++ ) iHoldStartRow[t] = -1; for( unsigned r=0; r<asRows.size(); r++ ) { CString& sRowString = asRows[r]; StripCrnl( sRowString ); if( sRowString == "" ) continue; // skip /* All 2s indicates the end of the song. */ if( sRowString == "2222222222222" ) break; if(sRowString.size() != 13) { LOG->Warn("File %s had a RowString with an improper length (\"%s\"); corrupt notes ignored", sPath.c_str(), sRowString.c_str()); return false; } /* Half-doubles is offset; "0011111100000". */ if( out.m_StepsType == STEPS_TYPE_PUMP_HALFDOUBLE ) sRowString.erase( 0, 2 ); // the length of a note in a row depends on TICKCOUNT float fBeatThisRow = r/(float)iTickCount; int row = BeatToNoteRow(fBeatThisRow); for( int t=0; t < notedata.GetNumTracks(); t++ ) { if( sRowString[t] == '4' ) { /* Remember when each hold starts; ignore the middle. */ if( iHoldStartRow[t] == -1 ) iHoldStartRow[t] = r; continue; } if( iHoldStartRow[t] != -1 ) // this ends the hold { HoldNote hn ( t, /* button */ BeatToNoteRow(iHoldStartRow[t]/(float)iTickCount), /* start */ BeatToNoteRow((r-1)/(float)iTickCount) /* end */ ); notedata.AddHoldNote( hn ); iHoldStartRow[t] = -1; } TapNote tap; switch(sRowString[t]) { case '0': tap = TAP_EMPTY; break; case '1': tap = TAP_ORIGINAL_TAP; break; default: ASSERT(0); tap = TAP_EMPTY; break; } notedata.SetTapNote(t, row, tap); } } /* We need to remove holes where the BPM increases. */ // if( song.m_Timing.m_BPMSegments.size() > 1 ) // RemoveHoles( notedata, song ); out.SetNoteData(¬edata); out.TidyUpData(); return true; }
bool KSFLoader::LoadGlobalData( const CString &sPath, Song &out ) { MsdFile msd; if( !msd.ReadFile( sPath ) ) RageException::Throw( "Error opening file \"%s\": %s", sPath.c_str(), msd.GetError().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, strtof(sParams[1], NULL)) ); else if( 0==stricmp(sValueName,"BPM2") ) BPM2 = strtof( sParams[1], NULL ); else if( 0==stricmp(sValueName,"BPM3") ) BPM3 = strtof( sParams[1], NULL ); else if( 0==stricmp(sValueName,"BUNKI") ) BPMPos2 = strtof( sParams[1], NULL ) / 100.0f; else if( 0==stricmp(sValueName,"BUNKI2") ) BPMPos3 = strtof( sParams[1], NULL ) / 100.0f; else if( 0==stricmp(sValueName,"STARTTIME") ) out.m_Timing.m_fBeat0OffsetInSeconds = -strtof( sParams[1], NULL )/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; }
void SMLoader::LoadTimingFromSMFile( const MsdFile &msd, TimingData &out ) { out.m_fBeat0OffsetInSeconds = 0; out.m_BPMSegments.clear(); out.m_StopSegments.clear(); for( unsigned i=0; i<msd.GetNumValues(); i++ ) { const MsdFile::value_t &sParams = msd.GetValue(i); const CString sValueName = sParams[0]; if( 0==stricmp(sValueName,"OFFSET") ) out.m_fBeat0OffsetInSeconds = strtof( sParams[1], NULL ); else if( 0==stricmp(sValueName,"STOPS") || 0==stricmp(sValueName,"FREEZES") ) { CStringArray arrayFreezeExpressions; split( sParams[1], ",", arrayFreezeExpressions ); for( unsigned f=0; f<arrayFreezeExpressions.size(); f++ ) { CStringArray arrayFreezeValues; split( arrayFreezeExpressions[f], "=", arrayFreezeValues ); /* XXX: Once we have a way to display warnings that the user actually * cares about (unlike most warnings), this should be one of them. */ if( arrayFreezeValues.size() != 2 ) { LOG->Warn( "Invalid #%s value \"%s\" (must have exactly one '='), ignored", sValueName.c_str(), arrayFreezeExpressions[f].c_str() ); continue; } const float fFreezeBeat = strtof( arrayFreezeValues[0], NULL ); const float fFreezeSeconds = strtof( arrayFreezeValues[1], NULL ); StopSegment new_seg; new_seg.m_fStartBeat = fFreezeBeat; new_seg.m_fStopSeconds = fFreezeSeconds; // LOG->Trace( "Adding a freeze segment: beat: %f, seconds = %f", new_seg.m_fStartBeat, new_seg.m_fStopSeconds ); out.AddStopSegment( new_seg ); } } else if( 0==stricmp(sValueName,"BPMS") ) { CStringArray arrayBPMChangeExpressions; split( sParams[1], ",", arrayBPMChangeExpressions ); for( unsigned b=0; b<arrayBPMChangeExpressions.size(); b++ ) { CStringArray arrayBPMChangeValues; split( arrayBPMChangeExpressions[b], "=", arrayBPMChangeValues ); /* XXX: Once we have a way to display warnings that the user actually * cares about (unlike most warnings), this should be one of them. */ if(arrayBPMChangeValues.size() != 2) { LOG->Warn( "Invalid #%s value \"%s\" (must have exactly one '='), ignored", sValueName.c_str(), arrayBPMChangeExpressions[b].c_str() ); continue; } const float fBeat = strtof( arrayBPMChangeValues[0], NULL ); const float fNewBPM = strtof( arrayBPMChangeValues[1], NULL ); BPMSegment new_seg; new_seg.m_fStartBeat = fBeat; new_seg.m_fBPM = fNewBPM; out.AddBPMSegment( new_seg ); } } } }
bool SMLoader::LoadEdit( CString sEditFilePath, ProfileSlot slot ) { LOG->Trace( "Song::LoadEdit(%s)", sEditFilePath.c_str() ); int iBytes = FILEMAN->GetFileSizeInBytes( sEditFilePath ); if( iBytes > MAX_EDIT_SIZE_BYTES ) { LOG->Warn( "The edit '%s' is unreasonably large. It won't be loaded.", sEditFilePath.c_str() ); return false; } MsdFile msd; if( !msd.ReadFile( sEditFilePath ) ) RageException::Throw( "Error opening file \"%s\": %s", sEditFilePath.c_str(), msd.GetError().c_str() ); Song* pSong = NULL; for( unsigned i=0; i<msd.GetNumValues(); i++ ) { int iNumParams = msd.GetNumParams(i); const MsdFile::value_t &sParams = msd.GetValue(i); const CString sValueName = sParams[0]; // handle the data if( 0==stricmp(sValueName,"SONG") ) { if( pSong ) { LOG->Warn( "The edit file '%s' has more than one #SONG tag.", sEditFilePath.c_str() ); return false; } CString sSongFullTitle = sParams[1]; sSongFullTitle.Replace( '\\', '/' ); pSong = SONGMAN->FindSong( sSongFullTitle ); if( pSong == NULL ) { LOG->Warn( "The edit file '%s' required a song '%s' that isn't present.", sEditFilePath.c_str(), sSongFullTitle.c_str() ); return false; } if( pSong->GetNumStepsLoadedFromProfile(slot) >= MAX_EDITS_PER_SONG_PER_PROFILE ) { LOG->Warn( "The song '%s' already has the maximum number of edits allowed for ProfileSlotP%d.", sSongFullTitle.c_str(), slot+1 ); return false; } } else if( 0==stricmp(sValueName,"NOTES") ) { if( pSong == NULL ) { LOG->Warn( "The edit file '%s' has doesn't have a #SONG tag preceeding the first #NOTES tag.", sEditFilePath.c_str() ); return false; } if( iNumParams < 7 ) { LOG->Trace( "The song file '%s' is has %d fields in a #NOTES tag, but should have at least %d.", sEditFilePath.c_str(), iNumParams, 7 ); continue; } Steps* pNewNotes = new Steps; ASSERT( pNewNotes ); LoadFromSMTokens( sParams[1], sParams[2], sParams[3], sParams[4], sParams[5], sParams[6], (iNumParams>=8)?sParams[7]:CString(""), *pNewNotes); pNewNotes->SetLoadedFromProfile( slot ); pNewNotes->SetDifficulty( DIFFICULTY_EDIT ); if( pSong->IsEditAlreadyLoaded(pNewNotes) ) { LOG->Warn( "The edit file '%s' is a duplicate of another edit that was already loaded.", sEditFilePath.c_str() ); SAFE_DELETE( pNewNotes ); return false; } pSong->AddSteps( pNewNotes ); return true; // Only allow one Steps per edit file! } else LOG->Trace( "Unexpected value named '%s'", sValueName.c_str() ); } return true; }
bool SMLoader::LoadFromSMFile( CString sPath, Song &out ) { LOG->Trace( "Song::LoadFromSMFile(%s)", sPath.c_str() ); MsdFile msd; if( !msd.ReadFile( sPath ) ) RageException::Throw( "Error opening file \"%s\": %s", sPath.c_str(), msd.GetError().c_str() ); out.m_Timing.m_sFile = sPath; LoadTimingFromSMFile( msd, out.m_Timing ); for( unsigned i=0; i<msd.GetNumValues(); i++ ) { int iNumParams = msd.GetNumParams(i); const MsdFile::value_t &sParams = msd.GetValue(i); const CString sValueName = sParams[0]; // handle the data /* Don't use GetMainAndSubTitlesFromFullTitle; that's only for heuristically * splitting other formats that *don't* natively support #SUBTITLE. */ if( 0==stricmp(sValueName,"TITLE") ) out.m_sMainTitle = sParams[1]; else if( 0==stricmp(sValueName,"SUBTITLE") ) out.m_sSubTitle = sParams[1]; else if( 0==stricmp(sValueName,"ARTIST") ) out.m_sArtist = sParams[1]; else if( 0==stricmp(sValueName,"TITLETRANSLIT") ) out.m_sMainTitleTranslit = sParams[1]; else if( 0==stricmp(sValueName,"SUBTITLETRANSLIT") ) out.m_sSubTitleTranslit = sParams[1]; else if( 0==stricmp(sValueName,"ARTISTTRANSLIT") ) out.m_sArtistTranslit = sParams[1]; else if( 0==stricmp(sValueName,"CREDIT") ) out.m_sCredit = sParams[1]; else if( 0==stricmp(sValueName,"BANNER") ) out.m_sBannerFile = sParams[1]; else if( 0==stricmp(sValueName,"BACKGROUND") ) out.m_sBackgroundFile = sParams[1]; /* Save "#LYRICS" for later, so we can add an internal lyrics tag. */ else if( 0==stricmp(sValueName,"LYRICSPATH") ) out.m_sLyricsFile = sParams[1]; else if( 0==stricmp(sValueName,"CDTITLE") ) out.m_sCDTitleFile = sParams[1]; else if( 0==stricmp(sValueName,"MUSIC") ) out.m_sMusicFile = sParams[1]; else if( 0==stricmp(sValueName,"MUSICLENGTH") ) { if(!FromCache) continue; out.m_fMusicLengthSeconds = strtof( sParams[1], NULL ); } else if( 0==stricmp(sValueName,"MUSICBYTES") ) ; /* ignore */ /* We calculate these. Some SMs in circulation have bogus values for * these, so make sure we always calculate it ourself. */ else if( 0==stricmp(sValueName,"FIRSTBEAT") ) { if(!FromCache) continue; out.m_fFirstBeat = strtof( sParams[1], NULL ); } else if( 0==stricmp(sValueName,"LASTBEAT") ) { if(!FromCache) LOG->Trace("Ignored #LASTBEAT (cache only)"); out.m_fLastBeat = strtof( sParams[1], NULL ); } else if( 0==stricmp(sValueName,"SONGFILENAME") ) { if( FromCache ) out.m_sSongFileName = sParams[1]; } else if( 0==stricmp(sValueName,"HASMUSIC") ) { if( FromCache ) out.m_bHasMusic = atoi( sParams[1] ) != 0; } else if( 0==stricmp(sValueName,"HASBANNER") ) { if( FromCache ) out.m_bHasBanner = atoi( sParams[1] ) != 0; } else if( 0==stricmp(sValueName,"SAMPLESTART") ) out.m_fMusicSampleStartSeconds = HHMMSSToSeconds( sParams[1] ); else if( 0==stricmp(sValueName,"SAMPLELENGTH") ) out.m_fMusicSampleLengthSeconds = HHMMSSToSeconds( sParams[1] ); else if( 0==stricmp(sValueName,"DISPLAYBPM") ) { // #DISPLAYBPM:[xxx][xxx:xxx]|[*]; if( sParams[1] == "*" ) out.m_DisplayBPMType = Song::DISPLAY_RANDOM; else { out.m_DisplayBPMType = Song::DISPLAY_SPECIFIED; out.m_fSpecifiedBPMMin = strtof( sParams[1], NULL ); if( sParams[2].empty() ) out.m_fSpecifiedBPMMax = out.m_fSpecifiedBPMMin; else out.m_fSpecifiedBPMMax = strtof( sParams[2], NULL ); } } else if( 0==stricmp(sValueName,"SELECTABLE") ) { if(!stricmp(sParams[1],"YES")) out.m_SelectionDisplay = out.SHOW_ALWAYS; else if(!stricmp(sParams[1],"NO")) out.m_SelectionDisplay = out.SHOW_NEVER; else if(!stricmp(sParams[1],"ROULETTE")) out.m_SelectionDisplay = out.SHOW_ROULETTE; else LOG->Warn( "The song file '%s' has an unknown #SELECTABLE value, '%s'; ignored.", sPath.c_str(), sParams[1].c_str()); } else if( 0==stricmp(sValueName,"BGCHANGES") || 0==stricmp(sValueName,"ANIMATIONS") ) { CStringArray aBGChangeExpressions; split( sParams[1], ",", aBGChangeExpressions ); for( unsigned b=0; b<aBGChangeExpressions.size(); b++ ) { BackgroundChange change; if( LoadFromBGChangesString( change, aBGChangeExpressions[b] ) ) out.AddBackgroundChange( change ); } } else if( 0==stricmp(sValueName,"FGCHANGES") ) { CStringArray aFGChangeExpressions; split( sParams[1], ",", aFGChangeExpressions ); for( unsigned b=0; b<aFGChangeExpressions.size(); b++ ) { BackgroundChange change; if( LoadFromBGChangesString( change, aFGChangeExpressions[b] ) ) out.AddForegroundChange( change ); } } else if( 0==stricmp(sValueName,"NOTES") ) { if( iNumParams < 7 ) { LOG->Trace( "The song file '%s' is has %d fields in a #NOTES tag, but should have at least %d.", sPath.c_str(), iNumParams, 7 ); continue; } Steps* pNewNotes = new Steps; ASSERT( pNewNotes ); LoadFromSMTokens( sParams[1], sParams[2], sParams[3], sParams[4], sParams[5], sParams[6], (iNumParams>=8)?sParams[7]:CString(""), *pNewNotes); out.AddSteps( pNewNotes ); } else if( 0==stricmp(sValueName,"OFFSET") || 0==stricmp(sValueName,"BPMS") || 0==stricmp(sValueName,"STOPS") || 0==stricmp(sValueName,"FREEZES") ) ; else LOG->Trace( "Unexpected value named '%s'", sValueName.c_str() ); } return true; }