void Steps::CreateBlank( StepsType ntTo ) { m_StepsType = ntTo; NoteData noteData; noteData.SetNumTracks( GAMEMAN->GetStepsTypeInfo(ntTo).iNumTracks ); this->SetNoteData( noteData ); }
void Steps::CalculateRadarValues( float fMusicLengthSeconds ) { // If we're autogen, don't calculate values. GetRadarValues will take from our parent. if( parent != NULL ) return; if( m_bAreCachedRadarValuesJustLoaded ) { m_bAreCachedRadarValuesJustLoaded = false; return; } // Do write radar values, and leave it up to the reading app whether they want to trust // the cached values without recalculating them. /* // If we're an edit, leave the RadarValues invalid. if( IsAnEdit() ) return; */ NoteData tempNoteData; this->GetNoteData( tempNoteData ); FOREACH_PlayerNumber( pn ) m_CachedRadarValues[pn].Zero(); GAMESTATE->SetProcessedTimingData(this->GetTimingData()); if( tempNoteData.IsComposite() ) { vector<NoteData> vParts; NoteDataUtil::SplitCompositeNoteData( tempNoteData, vParts ); for( size_t pn = 0; pn < min(vParts.size(), size_t(NUM_PLAYERS)); ++pn ) NoteDataUtil::CalculateRadarValues( vParts[pn], fMusicLengthSeconds, m_CachedRadarValues[pn] ); } else if (GAMEMAN->GetStepsTypeInfo(this->m_StepsType).m_StepsTypeCategory == StepsTypeCategory_Couple) { NoteData p1 = tempNoteData; // XXX: Assumption that couple will always have an even number of notes. const int tracks = tempNoteData.GetNumTracks() / 2; p1.SetNumTracks(tracks); NoteDataUtil::CalculateRadarValues(p1, fMusicLengthSeconds, m_CachedRadarValues[PLAYER_1]); // at this point, p2 is tempNoteData. NoteDataUtil::ShiftTracks(tempNoteData, tracks); tempNoteData.SetNumTracks(tracks); NoteDataUtil::CalculateRadarValues(tempNoteData, fMusicLengthSeconds, m_CachedRadarValues[PLAYER_2]); } else { NoteDataUtil::CalculateRadarValues( tempNoteData, fMusicLengthSeconds, m_CachedRadarValues[0] ); fill_n( m_CachedRadarValues + 1, NUM_PLAYERS-1, m_CachedRadarValues[0] ); } GAMESTATE->SetProcessedTimingData(NULL); }
void Steps::CopyFrom( Steps* pSource, StepsType ntTo, float fMusicLengthSeconds ) // pSource does not have to be of the same StepsType { m_StepsType = ntTo; NoteData noteData; pSource->GetNoteData( noteData ); noteData.SetNumTracks( GAMEMAN->GetStepsTypeInfo(ntTo).iNumTracks ); parent = NULL; this->SetNoteData( noteData ); this->SetDescription( pSource->GetDescription() ); this->SetDifficulty( pSource->GetDifficulty() ); this->SetMeter( pSource->GetMeter() ); this->CalculateRadarValues( fMusicLengthSeconds ); }
void Steps::GetNoteData( NoteData& noteDataOut ) const { Decompress(); if( m_bNoteDataIsFilled ) { noteDataOut = *m_pNoteData; } else { noteDataOut.ClearAll(); noteDataOut.SetNumTracks( GAMEMAN->GetStepsTypeInfo(m_StepsType).iNumTracks ); } }
void KSFLoader::RemoveHoles( NoteData &out, const Song &song ) { /* Start at the second BPM segment; the first one is already aligned. */ for( unsigned seg = 1; seg < song.m_Timing.m_BPMSegments.size(); ++seg ) { // const float FromBeat = song.m_Timing.m_BPMSegments[seg].m_fStartBeat; const float FromBeat = song.m_Timing.m_BPMSegments[seg].m_fStartBeat * song.m_BPMSegments[seg].m_fBPM / song.m_BPMSegments[0].m_fBPM; const int FromRow = (int) BeatToNoteRow(FromBeat); const int ToRow = (int) BeatToNoteRow(song.m_Timing.m_BPMSegments[seg].m_fStartBeat); LOG->Trace("from %f (%i) to (%i)", FromBeat, FromRow, ToRow); // const int ToRow = (int) roundf(FromRow * song.m_Timing.m_BPMSegments[0].m_fBPM / song.m_BPMSegments[seg].m_fBPM); // const int Rows = out.GetLastRow() - FromRow + 1; // int LastRow; // if(seg+1 < song.m_Timing.m_BPMSegments().size()) // LastRow = (int) NoteRowToBeat( song.m_Timing.m_BPMSegments[seg+1].m_fStartBeat ) - 1; // else // LastRow = out.GetLastRow(); NoteData tmp; tmp.SetNumTracks(out.GetNumTracks()); tmp.CopyRange( &out, FromRow, out.GetLastRow() ); out.ClearRange( FromRow, out.GetLastRow() ); out.CopyRange( &tmp, 0, tmp.GetLastRow(), ToRow ); } /* out.ConvertHoldNotesTo2sAnd3s(); for( t = 0; t < notedata.GetNumTracks(); ++t ) { const float CurBPM = song.GetBPMAtBeat( NoteRowToBeat(row) ); song.m_Timing.m_BPMSegments.size() for( int row = 0; row <= notedata.GetLastRow(); ++row ) { TapNote tn = notedata.GetTapNote(t, row); if( tn == TAP_EMPTY ) continue; const int RealRow = (int) roundf(row * OrigBPM / CurBPM); if( RealRow == row ) continue; LOG->Trace("from %i to %i", row, RealRow); notedata.SetTapNote( t, RealRow, tn ); notedata.SetTapNote( t, row, TAP_EMPTY ); } } out.Convert2sAnd3sToHoldNotes(); */ }
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; }
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; }
static bool LoadFromBMSFile( const RString &sPath, const NameToData_t &mapNameToData, Steps &out, const MeasureToTimeSig_t &sigAdjustments, const map<RString,int> &idToKeySoundIndex ) { 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 = atoi(sData); if( GetTagFromMap( mapNameToData, "#playlevel", sData ) ) out.SetMeter( atoi(sData) ); NoteData ndNotes; ndNotes.SetNumTracks( NUM_BMS_TRACKS ); /* Read time signatures. Note that these can differ across files in the same * song. */ MeasureToTimeSig_t mapMeasureToTimeSig; ReadTimeSigs( mapNameToData, mapMeasureToTimeSig ); int iHoldStarts[NUM_BMS_TRACKS]; int iHoldPrevs[NUM_BMS_TRACKS]; for( int i = 0; i < NUM_BMS_TRACKS; ++i ) { iHoldStarts[i] = -1; iHoldPrevs[i] = -1; } 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 = atoi( sName.substr(1,3).c_str() ); int iRawTrackNum = atoi( sName.substr(4,2).c_str() ); int iRowNo = GetMeasureStartRow( mapMeasureToTimeSig, iMeasureNo, sigAdjustments ); float fBeatsPerMeasure = GetBeatsPerMeasure( mapMeasureToTimeSig, iMeasureNo, sigAdjustments ); const RString &sNoteData = it->second; vector<TapNote> vTapNotes; for( size_t i=0; i+1<sNoteData.size(); i+=2 ) { RString sNoteId = sNoteData.substr( i, 2 ); if( sNoteId != "00" ) { vTapNotes.push_back( TAP_ORIGINAL_TAP ); map<RString,int>::const_iterator it = idToKeySoundIndex.find( sNoteId ); if( it != idToKeySoundIndex.end() ) vTapNotes.back().iKeysoundIndex = it->second; } else { vTapNotes.push_back( TAP_EMPTY ); } } const unsigned iNumNotesInThisMeasure = vTapNotes.size(); for( unsigned j=0; j<iNumNotesInThisMeasure; j++ ) { float fPercentThroughMeasure = (float)j/(float)iNumNotesInThisMeasure; int row = iRowNo + lrintf( fPercentThroughMeasure * fBeatsPerMeasure * ROWS_PER_BEAT ); // some BMS files seem to have funky alignment, causing us to write gigantic cache files. // Try to correct for this by quantizing. row = Quantize( row, ROWS_PER_MEASURE/64 ); BmsTrack bmsTrack; bool bIsHold; if( ConvertRawTrackToTapNote(iRawTrackNum, bmsTrack, bIsHold) ) { TapNote &tn = vTapNotes[j]; if( tn.type != TapNote::empty ) { if( bmsTrack == BMS_AUTO_KEYSOUND_1 ) { // shift the auto keysound as far right as possible int iLastEmptyTrack = -1; if( ndNotes.GetTapLastEmptyTrack(row, iLastEmptyTrack) && iLastEmptyTrack >= BMS_AUTO_KEYSOUND_1 ) { tn.type = TapNote::autoKeysound; bmsTrack = (BmsTrack)iLastEmptyTrack; } else { // no room for this note. Drop it. continue; } } else if( bIsHold ) { if( iHoldStarts[bmsTrack] == -1 ) { // Start of a hold. iHoldStarts[bmsTrack] = row; iHoldPrevs[bmsTrack] = row; } else { // We're continuing a hold. iHoldPrevs[bmsTrack] = row; } continue; } } if( iHoldStarts[bmsTrack] != -1 ) { // This is ending a hold. const int iBegin = iHoldStarts[bmsTrack]; const int iEnd = iHoldPrevs[bmsTrack]; if( iBegin < iEnd ) ndNotes.AddHoldNote( bmsTrack, iBegin, iEnd, TAP_ORIGINAL_HOLD_HEAD ); else ndNotes.SetTapNote( bmsTrack, iBegin, TAP_ORIGINAL_TAP ); iHoldStarts[bmsTrack] = -1; iHoldPrevs[bmsTrack] = -1; } // Don't bother inserting empty taps. if( tn.type != TapNote::empty ) ndNotes.SetTapNote( bmsTrack, row, tn ); } } } // We're done reading in all of the BMS values. Time to check for any unfinished holds. for( int iTrack = 0; iTrack < NUM_BMS_TRACKS; ++iTrack ) { const int iBegin = iHoldStarts[iTrack]; const int iEnd = iHoldPrevs[iTrack]; if( iBegin == -1 ) continue; if( iBegin < iEnd ) ndNotes.AddHoldNote( iTrack, iBegin, iEnd, TAP_ORIGINAL_HOLD_HEAD ); else ndNotes.SetTapNote( iTrack, iBegin, TAP_ORIGINAL_TAP ); } out.m_StepsType = DetermineStepsType( iPlayer, ndNotes, sPath ); if( out.m_StepsType == StepsType_beat_single5 && GetTagFromMap( mapNameToData, "#title", sData ) ) { /* Hack: guess at 6-panel. */ // extract the Steps description (looks like 'Music <BASIC>') 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; } // shift all of the autokeysound tracks onto the main tracks for( int t=BMS_AUTO_KEYSOUND_1; t<BMS_AUTO_KEYSOUND_1+NUM_AUTO_KEYSOUND_TRACKS; t++ ) { FOREACH_NONEMPTY_ROW_IN_TRACK( ndNotes, t, row ) { TapNote tn = ndNotes.GetTapNote( t, row ); int iEmptyTrack; if( ndNotes.GetTapFirstEmptyTrack(row, iEmptyTrack) ) { ndNotes.SetTapNote( iEmptyTrack, row, tn ); ndNotes.SetTapNote( t, row, TAP_EMPTY ); } else { LOG->UserLog( "Song file", sPath, "has no room to shift the autokeysound tracks." ); } } }
static NoteData ParseNoteData(RString &step1, RString &step2, Steps &out, const RString &path) { 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 ); } NoteData newNoteData; newNoteData.SetNumTracks( g_mapDanceNoteToNoteDataColumn.size() ); for( int pad=0; pad<2; pad++ ) // foreach pad { RString sStepData; switch( pad ) { case 0: sStepData = step1; break; case 1: if( step2 == "" ) // no data continue; // skip sStepData = step2; 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<sStepData.size(); ) { char c = sStepData[i++]; switch( c ) { // begins a series case '(': fCurrentIncrementer = 1.0/16 * BEATS_PER_MEASURE; break; case '[': fCurrentIncrementer = 1.0/24 * BEATS_PER_MEASURE; break; case '{': fCurrentIncrementer = 1.0/64 * BEATS_PER_MEASURE; break; case '`': fCurrentIncrementer = 1.0/192 * BEATS_PER_MEASURE; break; // ends a series case ')': case ']': case '}': case '\'': case '>': fCurrentIncrementer = 1.0/8 * BEATS_PER_MEASURE; break; default: // this is a note character { if( c == '!' ) { LOG->UserLog( "Song file", path, "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, path ); if( iCol1 != -1 ) newNoteData.SetTapNote(iCol1, iIndex, TAP_ORIGINAL_TAP); if( iCol2 != -1 ) newNoteData.SetTapNote(iCol2, iIndex, TAP_ORIGINAL_TAP); if(i>=sStepData.length()) { break; //we ran out of data //while looking for the ending > mark } if( sStepData[i] == '!' ) { i++; const char holdChar = sStepData[i++]; DWIcharToNoteCol(holdChar, (GameController)pad, iCol1, iCol2, path ); 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; t<newNoteData.GetNumTracks(); ++t ) { FOREACH_NONEMPTY_ROW_IN_TRACK( newNoteData, t, iHeadRow ) { TapNote tn = newNoteData.GetTapNote( t, iHeadRow ); if( tn.type != TapNote::hold_head ) continue; int iTailRow = iHeadRow; bool bFound = false; while( !bFound && newNoteData.GetNextTapNoteRowForTrack(t, iTailRow) ) { const TapNote &TailTap = newNoteData.GetTapNote( t, iTailRow ); if( TailTap.type == TapNote::empty ) continue; newNoteData.SetTapNote( t, iTailRow, TAP_EMPTY ); tn.iDuration = iTailRow - iHeadRow; newNoteData.SetTapNote( t, iHeadRow, tn ); bFound = true; } if( !bFound ) { /* The hold was never closed. */ LOG->UserLog("Song file", path, "failed to close a hold note in \"%s\" on track %i", DifficultyToString(out.GetDifficulty()).c_str(), t); newNoteData.SetTapNote( t, iHeadRow, TAP_EMPTY ); } } }