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::SetNoteData( const NoteData& noteDataNew ) { ASSERT( noteDataNew.GetNumTracks() == GAMEMAN->GetStepsTypeInfo(m_StepsType).iNumTracks ); DeAutogen( false ); *m_pNoteData = noteDataNew; m_bNoteDataIsFilled = true; m_sNoteDataCompressed = RString(); m_iHash = 0; }
void Steps::SetNoteData( const NoteData& noteDataNew ) { ASSERT( noteDataNew.GetNumTracks() == GAMEMAN->GetStepsTypeInfo(m_StepsType).iNumTracks ); DeAutogen( false ); *m_pNoteData = noteDataNew; m_bNoteDataIsFilled = true; m_sNoteDataCompressed = RString(); m_iHash = 0; m_sFilename = RString(); // We can no longer read from the file because it has changed in memory. }
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(); */ }
void ScoreKeeperNormal::GetRowCounts( const NoteData &nd, int iRow, int &iNumHitContinueCombo, int &iNumHitMaintainCombo, int &iNumBreakCombo ) { iNumHitContinueCombo = iNumHitMaintainCombo = iNumBreakCombo = 0; for( int track = 0; track < nd.GetNumTracks(); ++track ) { const TapNote &tn = nd.GetTapNote( track, iRow ); if( tn.type != TapNoteType_Tap && tn.type != TapNoteType_HoldHead && tn.type != TapNoteType_Lift ) continue; TapNoteScore tns = tn.result.tns; if( tns >= m_MinScoreToContinueCombo ) ++iNumHitContinueCombo; else if( tns >= m_MinScoreToMaintainCombo ) ++iNumHitMaintainCombo; else ++iNumBreakCombo; } }
/* Return the minimum tap score of a row. If the row isn't complete (not all * taps have been hit), return TNS_None or TNS_Miss. */ TapNoteScore NoteDataWithScoring::MinTapNoteScore( const NoteData &in, unsigned row, PlayerNumber plnum ) { //LOG->Trace("Hey I'm NoteDataWithScoring::MinTapNoteScore"); TapNoteScore score = TNS_W1; for( int t=0; t<in.GetNumTracks(); t++ ) { // Ignore mines (and fake arrows), or the score will always be TNS_None. const TapNote &tn = in.GetTapNote( t, row ); if (tn.type == TapNoteType_Empty || tn.type == TapNoteType_Mine || tn.type == TapNoteType_Fake || tn.type == TapNoteType_AutoKeysound || ( plnum != PlayerNumber_Invalid && tn.pn != plnum ) ) continue; score = min( score, tn.result.tns ); } //LOG->Trace( ssprintf("OMG score is?? %s",TapNoteScoreToString(score).c_str()) ); return score; }
static StepsType DetermineStepsType( int iPlayer, const NoteData &nd, const RString &sPath ) { ASSERT( NUM_PMS_TRACKS == nd.GetNumTracks() ); bool bTrackHasNote[NUM_NON_AUTO_KEYSOUND_TRACKS]; ZERO( bTrackHasNote ); int iLastRow = nd.GetLastRow(); for( int t=0; t<NUM_NON_AUTO_KEYSOUND_TRACKS; t++ ) { for( int r=0; r<=iLastRow; r++ ) { if( nd.GetTapNote(t, r).type != TapNote::empty ) { bTrackHasNote[t] = true; break; } } } int iNumNonEmptyTracks = 0; for( int t=0; t<NUM_NON_AUTO_KEYSOUND_TRACKS; t++ ) if( bTrackHasNote[t] ) iNumNonEmptyTracks++; switch( iPlayer ) { case 1: switch( iNumNonEmptyTracks ) { 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 void Serialize( const NoteData &o, Json::Value &root ) { root = Json::Value(Json::arrayValue); for(int t=0; t < o.GetNumTracks(); t++ ) { NoteData::TrackMap::const_iterator begin, end; o.GetTapNoteRange( t, 0, MAX_NOTE_ROW, begin, end ); //NoteData::TrackMap tm = o.GetTrack(t); //FOREACHM_CONST( int, TapNote, tm, iter ) for( ; begin != end; ++begin ) { int iRow = begin->first; TapNote tn = begin->second; root.resize( root.size()+1 ); Json::Value &root2 = root[ root.size()-1 ]; root2 = Json::Value(Json::arrayValue); root2.resize(3); root2[(unsigned)0] = NoteRowToBeat(iRow); root2[1] = t; Serialize( tn, root2[2] ); } } }
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 StepsType DetermineStepsType( int iPlayer, const NoteData &nd, const RString &sPath ) { ASSERT( NUM_BMS_TRACKS == nd.GetNumTracks() ); bool bTrackHasNote[NUM_NON_AUTO_KEYSOUND_TRACKS]; ZERO( bTrackHasNote ); int iLastRow = nd.GetLastRow(); for( int t=0; t<NUM_NON_AUTO_KEYSOUND_TRACKS; t++ ) { for( int r=0; r<=iLastRow; r++ ) { if( nd.GetTapNote(t, r).type != TapNote::empty ) { bTrackHasNote[t] = true; break; } } } int iNumNonEmptyTracks = 0; for( int t=0; t<NUM_NON_AUTO_KEYSOUND_TRACKS; t++ ) if( bTrackHasNote[t] ) iNumNonEmptyTracks++; switch( iPlayer ) { case 1: // "1 player" /* Track counts: * 4 - dance 4-panel * 5 - pop 5-key * 6 - dance 6-panel, beat 5-key * 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 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; default: return StepsType_Invalid; } default: LOG->UserLog( "Song file", sPath, "has an invalid #PLAYER value %d.", iPlayer ); return StepsType_Invalid; } }
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 ); } } }