void SongPosition::UpdateSongPosition( float fPositionSeconds, const TimingData &timing, const RageTimer ×tamp ) { if( !timestamp.IsZero() ) m_LastBeatUpdate = timestamp; else m_LastBeatUpdate.Touch(); TimingData::GetBeatArgs beat_info; beat_info.elapsed_time= fPositionSeconds; timing.GetBeatAndBPSFromElapsedTime(beat_info); m_fSongBeat= beat_info.beat; m_fCurBPS= beat_info.bps_out; m_bFreeze= beat_info.freeze_out; m_bDelay= beat_info.delay_out; m_iWarpBeginRow= beat_info.warp_begin_out; m_fWarpDestination= beat_info.warp_dest_out; // "Crash reason : -243478.890625 -48695.773438" // The question is why is -2000 used as the limit? -aj ASSERT_M( m_fSongBeat > -2000, ssprintf("Song beat %f at %f seconds is less than -2000!", m_fSongBeat, fPositionSeconds) ); m_fMusicSeconds = fPositionSeconds; m_fLightSongBeat = timing.GetBeatFromElapsedTime( fPositionSeconds + g_fLightsAheadSeconds ); m_fSongBeatNoOffset = timing.GetBeatFromElapsedTimeNoOffset( fPositionSeconds ); m_fMusicSecondsVisible = fPositionSeconds - g_fVisualDelaySeconds.Get(); beat_info.elapsed_time= m_fMusicSecondsVisible; timing.GetBeatAndBPSFromElapsedTime(beat_info); m_fSongBeatVisible= beat_info.beat; }
MusicPlaying( RageSound *Music ) { m_Timing.AddBPMSegment( BPMSegment(0,120) ); m_NewTiming.AddBPMSegment( BPMSegment(0,120) ); m_HasTiming = false; m_TimingDelayed = false; m_Music = Music; }
void BPStoSPB(TimingData &BPS) { auto BPSCopy = BPS; for (auto i = BPS.begin(); i != BPS.end(); ++i) { double valueBPS = i->Value; i->Value = 1 / valueBPS; i->Time = IntegrateToTime(BPSCopy, i->Time); // Find time in beats based off beats in time } }
void SSCLoader::ProcessStops( TimingData &out, const RString sParam ) { vector<RString> arrayStopExpressions; split( sParam, ",", arrayStopExpressions ); for( unsigned b=0; b<arrayStopExpressions.size(); b++ ) { vector<RString> arrayStopValues; split( arrayStopExpressions[b], "=", arrayStopValues ); if( arrayStopValues.size() != 2 ) { LOG->UserLog("Song file", this->GetSongTitle(), "has an invalid #STOPS value \"%s\" (must have exactly one '='), ignored.", arrayStopExpressions[b].c_str() ); continue; } const float fBeat = StringToFloat( arrayStopValues[0] ); const float fNewStop = StringToFloat( arrayStopValues[1] ); if( fBeat >= 0 && fNewStop > 0 ) out.AddSegment( StopSegment(BeatToNoteRow(fBeat), fNewStop) ); else { LOG->UserLog("Song file", this->GetSongTitle(), "has an invalid Stop at beat %f, length %f.", fBeat, fNewStop ); } } }
void SSCLoader::ProcessLabels( TimingData &out, const RString sParam ) { vector<RString> arrayLabelExpressions; split( sParam, ",", arrayLabelExpressions ); for( unsigned b=0; b<arrayLabelExpressions.size(); b++ ) { vector<RString> arrayLabelValues; split( arrayLabelExpressions[b], "=", arrayLabelValues ); if( arrayLabelValues.size() != 2 ) { LOG->UserLog("Song file", this->GetSongTitle(), "has an invalid #LABELS value \"%s\" (must have exactly one '='), ignored.", arrayLabelExpressions[b].c_str() ); continue; } const float fBeat = StringToFloat( arrayLabelValues[0] ); RString sLabel = arrayLabelValues[1]; TrimRight(sLabel); if( fBeat >= 0.0f ) out.AddSegment( LabelSegment(BeatToNoteRow(fBeat), sLabel) ); else { LOG->UserLog("Song file", this->GetSongTitle(), "has an invalid Label at beat %f called %s.", fBeat, sLabel.c_str() ); } } }
void SSCLoader::ProcessScrolls( TimingData &out, const RString sParam ) { vector<RString> vs1; split( sParam, ",", vs1 ); FOREACH_CONST( RString, vs1, s1 ) { vector<RString> vs2; split( *s1, "=", vs2 ); if( vs2.size() < 2 ) { LOG->UserLog("Song file", this->GetSongTitle(), "has an scroll change with %i values.", static_cast<int>(vs2.size()) ); continue; } const float fBeat = StringToFloat( vs2[0] ); const float fRatio = StringToFloat( vs2[1] ); if( fBeat < 0 ) { LOG->UserLog("Song file", this->GetSongTitle(), "has an scroll change with beat %f.", fBeat ); continue; } out.AddSegment( ScrollSegment(BeatToNoteRow(fBeat), fRatio) ); }
static void HandleBunki( TimingData &timing, const float fEarlyBPM, const float fCurBPM, const float fGap, const float fPos ) { const float BeatsPerSecond = fEarlyBPM / 60.0f; const float beat = (fPos + fGap) * BeatsPerSecond; LOG->Trace( "BPM %f, BPS %f, BPMPos %f, beat %f", fEarlyBPM, BeatsPerSecond, fPos, beat ); timing.AddSegment( BPMSegment(BeatToNoteRow(beat), fCurBPM) ); }
static void ProcessTickcounts( const std::string & value, int & ticks, TimingData & timing ) { /* TICKCOUNT will be used below if there are DM compliant BPM changes * and stops. It will be called again in LoadFromKSFFile for the * actual steps. */ ticks = StringToInt( value ); ticks = Rage::clamp( ticks, 0, ROWS_PER_BEAT ); if( ticks == 0 ) ticks = TickcountSegment::DEFAULT_TICK_COUNT; timing.AddSegment( TickcountSegment(0, ticks) ); }
void Difficulty::ProcessVSpeeds(TimingData& BPS, TimingData& VerticalSpeeds, double SpeedConstant) { VerticalSpeeds.clear(); if (SpeedConstant) // We're using a CMod, so further processing is pointless { TimingSegment VSpeed; VSpeed.Time = 0; VSpeed.Value = SpeedConstant; VerticalSpeeds.push_back(VSpeed); return; } // Calculate velocity at time based on BPM at time for (auto Time = BPS.begin(); Time != BPS.end(); ++Time) { float VerticalSpeed; TimingSegment VSpeed; if (Time->Value) { float spb = 1 / Time->Value; VerticalSpeed = MeasureBaseSpacing / (spb * 4); } else VerticalSpeed = 0; VSpeed.Value = VerticalSpeed; VSpeed.Time = Time->Time; // We blindly take the BPS time that had offset and drift applied. VerticalSpeeds.push_back(VSpeed); } // Let first speed be not-null. if (VerticalSpeeds.size() && VerticalSpeeds[0].Value == 0) { for (auto i = VerticalSpeeds.begin(); i != VerticalSpeeds.end(); ++i) { if (i->Value != 0) VerticalSpeeds[0].Value = i->Value; } } }
void SSCLoader::ProcessWarps( TimingData &out, const RString sParam, const float fVersion ) { vector<RString> arrayWarpExpressions; split( sParam, ",", arrayWarpExpressions ); for( unsigned b=0; b<arrayWarpExpressions.size(); b++ ) { vector<RString> arrayWarpValues; split( arrayWarpExpressions[b], "=", arrayWarpValues ); if( arrayWarpValues.size() != 2 ) { LOG->UserLog("Song file", this->GetSongTitle(), "has an invalid #WARPS value \"%s\" (must have exactly one '='), ignored.", arrayWarpExpressions[b].c_str() ); continue; } const float fBeat = StringToFloat( arrayWarpValues[0] ); const float fNewBeat = StringToFloat( arrayWarpValues[1] ); // Early versions were absolute in beats. They should be relative. if( ( fVersion < VERSION_SPLIT_TIMING && fNewBeat > fBeat ) ) { out.AddSegment( WarpSegment(BeatToNoteRow(fBeat), fNewBeat - fBeat) ); } else if( fNewBeat > 0 ) out.AddSegment( WarpSegment(BeatToNoteRow(fBeat), fNewBeat) ); else { LOG->UserLog("Song file", this->GetSongTitle(), "has an invalid Warp at beat %f, BPM %f.", fBeat, fNewBeat ); } } }
void SSCLoader::ProcessCombos( TimingData &out, const RString line, const int rowsPerBeat ) { vector<RString> arrayComboExpressions; split( line, ",", arrayComboExpressions ); for( unsigned f=0; f<arrayComboExpressions.size(); f++ ) { vector<RString> arrayComboValues; split( arrayComboExpressions[f], "=", arrayComboValues ); unsigned size = arrayComboValues.size(); if( size < 2 ) { LOG->UserLog("Song file", this->GetSongTitle(), "has an invalid #COMBOS value \"%s\" (must have at least one '='), ignored.", arrayComboExpressions[f].c_str() ); continue; } const float fComboBeat = StringToFloat( arrayComboValues[0] ); const int iCombos = StringToInt( arrayComboValues[1] ); const int iMisses = (size == 2 ? iCombos : StringToInt(arrayComboValues[2])); out.AddSegment( ComboSegment( BeatToNoteRow(fComboBeat), iCombos, iMisses ) ); } }
void run() { #define CHECK(call, exp) \ { \ float ret = call; \ if( call != exp ) { \ LOG->Warn( "Line %i: Got %f, expected %f", __LINE__, ret, exp); \ return; \ } \ } TimingData test; test.AddBPMSegment( BPMSegment(0, 60) ); /* First, trivial sanity checks. */ CHECK( test.GetBeatFromElapsedTime(60), 60.0f ); CHECK( test.GetElapsedTimeFromBeat(60), 60.0f ); /* The first BPM segment extends backwards in time. */ CHECK( test.GetBeatFromElapsedTime(-60), -60.0f ); CHECK( test.GetElapsedTimeFromBeat(-60), -60.0f ); CHECK( test.GetBeatFromElapsedTime(100000), 100000.0f ); CHECK( test.GetElapsedTimeFromBeat(100000), 100000.0f ); CHECK( test.GetBeatFromElapsedTime(-100000), -100000.0f ); CHECK( test.GetElapsedTimeFromBeat(-100000), -100000.0f ); CHECK( test.GetBPMAtBeat(0), 60.0f ); CHECK( test.GetBPMAtBeat(100000), 60.0f ); CHECK( test.GetBPMAtBeat(-100000), 60.0f ); /* 120BPM at beat 10: */ test.AddBPMSegment( BPMSegment(10, 120) ); CHECK( test.GetBPMAtBeat(9.99), 60.0f ); CHECK( test.GetBPMAtBeat(10), 120.0f ); CHECK( test.GetBeatFromElapsedTime(9), 9.0f ); CHECK( test.GetBeatFromElapsedTime(10), 10.0f ); CHECK( test.GetBeatFromElapsedTime(10.5), 11.0f ); CHECK( test.GetElapsedTimeFromBeat(9), 9.0f ); CHECK( test.GetElapsedTimeFromBeat(10), 10.0f ); CHECK( test.GetElapsedTimeFromBeat(11), 10.5f ); /* Add a 5-second stop at beat 10. */ test.AddStopSegment( StopSegment(10, 5) ); /* The stop shouldn't affect GetBPMAtBeat at all. */ CHECK( test.GetBPMAtBeat(9.99), 60.0f ); CHECK( test.GetBPMAtBeat(10), 120.0f ); CHECK( test.GetBeatFromElapsedTime(9), 9.0f ); CHECK( test.GetBeatFromElapsedTime(10), 10.0f ); CHECK( test.GetBeatFromElapsedTime(12), 10.0f ); CHECK( test.GetBeatFromElapsedTime(14), 10.0f ); CHECK( test.GetBeatFromElapsedTime(15), 10.0f ); CHECK( test.GetBeatFromElapsedTime(15.5), 11.0f ); CHECK( test.GetElapsedTimeFromBeat(9), 9.0f ); CHECK( test.GetElapsedTimeFromBeat(10), 10.0f ); CHECK( test.GetElapsedTimeFromBeat(11), 15.5f ); /* Add a 2-second stop at beat 5 and a 5-second stop at beat 15. */ test.m_StopSegments.clear(); test.AddStopSegment( StopSegment(5, 2) ); test.AddStopSegment( StopSegment(15, 5) ); CHECK( test.GetBPMAtBeat(9.99), 60.0f ); CHECK( test.GetBPMAtBeat(10), 120.0f ); CHECK( test.GetBeatFromElapsedTime(1), 1.0f ); CHECK( test.GetBeatFromElapsedTime(2), 2.0f ); CHECK( test.GetBeatFromElapsedTime(5), 5.0f ); // stopped CHECK( test.GetBeatFromElapsedTime(6), 5.0f ); // stopped CHECK( test.GetBeatFromElapsedTime(7), 5.0f ); // stop finished CHECK( test.GetBeatFromElapsedTime(8), 6.0f ); CHECK( test.GetBeatFromElapsedTime(12), 10.0f ); // bpm changes to 120 CHECK( test.GetBeatFromElapsedTime(13), 12.0f ); CHECK( test.GetBeatFromElapsedTime(14), 14.0f ); CHECK( test.GetBeatFromElapsedTime(14.5f), 15.0f ); // stopped CHECK( test.GetBeatFromElapsedTime(15), 15.0f ); // stopped CHECK( test.GetBeatFromElapsedTime(17), 15.0f ); // stopped CHECK( test.GetBeatFromElapsedTime(19.5f), 15.0f ); // stop finished CHECK( test.GetBeatFromElapsedTime(20), 16.0f ); CHECK( test.GetElapsedTimeFromBeat(1), 1.0f ); CHECK( test.GetElapsedTimeFromBeat(2), 2.0f ); CHECK( test.GetElapsedTimeFromBeat(5), 5.0f ); // stopped CHECK( test.GetElapsedTimeFromBeat(6), 8.0f ); CHECK( test.GetElapsedTimeFromBeat(10), 12.0f ); // bpm changes to 120 CHECK( test.GetElapsedTimeFromBeat(12), 13.0f ); CHECK( test.GetElapsedTimeFromBeat(14), 14.0f ); CHECK( test.GetElapsedTimeFromBeat(15.0f), 14.5f ); // stopped CHECK( test.GetElapsedTimeFromBeat(16), 20.0f ); RageTimer foobar; /* We can look up the time of any given beat, then look up the beat of that * time and get the original value. (We can't do this in reverse; the beat * doesn't move during stop segments.) */ int q = 0; for( float f = -10; f < 250; f += 0.002 ) { ++q; // const float t = test.GetElapsedTimeFromBeat( f ); const float b = test.GetBeatFromElapsedTime( f ); /* b == f */ // if( fabsf(b-f) > 0.001 ) // { // LOG->Warn( "%f != %f", b, f ); // return; // } } LOG->Trace("... %i in %f", q, foobar.GetDeltaTime()); TimingData test2; test2.AddBPMSegment( BPMSegment(0, 60) ); test2.AddStopSegment( StopSegment(0, 1) ); CHECK( test2.GetBeatFromElapsedTime(-1), -1.0f ); CHECK( test2.GetBeatFromElapsedTime(0), 0.0f ); CHECK( test2.GetBeatFromElapsedTime(1), 0.0f ); CHECK( test2.GetBeatFromElapsedTime(2), 1.0f ); CHECK( test2.GetElapsedTimeFromBeat(-1), -1.0f ); CHECK( test2.GetElapsedTimeFromBeat(0), 0.0f ); CHECK( test2.GetElapsedTimeFromBeat(1), 2.0f ); CHECK( test2.GetElapsedTimeFromBeat(2), 3.0f ); }
static void Serialize(const TimingData &td, Json::Value &root) { JsonUtil::SerializeVectorPointers( td.GetTimingSegments(SEGMENT_BPM), Serialize, root["BpmSegments"] ); JsonUtil::SerializeVectorPointers( td.GetTimingSegments(SEGMENT_STOP), Serialize, root["StopSegments"] ); }
void Difficulty::GetPlayableData(VectorTN NotesOut, TimingData& BPS, TimingData& VerticalSpeeds, TimingData& Warps, float Drift, double SpeedConstant) { /* We'd like to build the notes' position from 0 to infinity, however the real "zero" position would be the judgment line in other words since "up" is negative relative to 0 and 0 is the judgment line position would actually be judgeline - positiveposition and positiveposition would just be measure * measuresize + fraction * fractionsize In practice, since we use a ms-based model rather than a beat one, we just do regular integration of position = sum(speed_i * duration_i) + speed_current * (time_current - speed_start_time) */ assert(Data != nullptr); ProcessBPS(BPS, Drift); ProcessVSpeeds(BPS, VerticalSpeeds, SpeedConstant); if (!SpeedConstant) // If there is a speed constant having speed changes is not what we want ProcessSpeedVariations(BPS, VerticalSpeeds, Drift); if (SpeedConstant) Warps.clear(); else Warps = Data->Warps; // From here on, we'll just copy the notes out. Otherwise, just leave the processed data. if (!NotesOut) return; for (int KeyIndex = 0; KeyIndex < Channels; KeyIndex++) NotesOut[KeyIndex].clear(); /* For all channels of this difficulty */ for (int KeyIndex = 0; KeyIndex < Channels; KeyIndex++) { int MIdx = 0; /* For each measure of this channel */ for (auto Msr = Data->Measures.begin(); Msr != Data->Measures.end(); ++Msr) { /* For each note in the measure... */ ptrdiff_t total_notes = Msr->Notes[KeyIndex].size(); for (auto Note = 0; Note < total_notes; Note++) { /* Calculate position. (Change this to TrackNote instead of processing?) issue is not having the speed change data there. */ NoteData &CurrentNote = (*Msr).Notes[KeyIndex][Note]; TrackNote NewNote; NewNote.AssignNotedata(CurrentNote); NewNote.AddTime(Drift); float VerticalPosition = IntegrateToTime(VerticalSpeeds, NewNote.GetStartTime()); float HoldEndPosition = IntegrateToTime(VerticalSpeeds, NewNote.GetTimeFinal()); // if upscroll change minus for plus as well as matrix at screengameplay7k if (!CurrentNote.EndTime) NewNote.AssignPosition(VerticalPosition); else NewNote.AssignPosition(VerticalPosition, HoldEndPosition); // Okay, now we want to know what fraction of a beat we're dealing with // this way we can display colored (a la Stepmania) notes. // We should do this before changing time by drift. double cBeat = IntegrateToTime(BPS, NewNote.GetStartTime()); double iBeat = floor(cBeat); double dBeat = (cBeat - iBeat); NewNote.AssignFraction(dBeat); double Wamt = -GetWarpAmountAtTime(CurrentNote.StartTime); NewNote.AddTime(Wamt); if (!SpeedConstant || (NewNote.IsJudgable() && !IsWarpingAt(CurrentNote.StartTime))) NotesOut[KeyIndex].push_back(NewNote); } MIdx++; } // done with the channel - sort it std::stable_sort(NotesOut[KeyIndex].begin(), NotesOut[KeyIndex].end(), [](const TrackNote &A, const TrackNote &B) -> bool { return A.GetVertical() < B.GetVertical(); }); } }
void Difficulty::ProcessSpeedVariations(TimingData& BPS, TimingData& VerticalSpeeds, double Drift) { assert(Data != NULL); TimingData tVSpeeds = VerticalSpeeds; // We need this to store what values to change TimingData &Scrolls = Data->Scrolls; std::sort(Scrolls.begin(), Scrolls.end()); for (TimingData::const_iterator Change = Scrolls.begin(); Change != Scrolls.end(); ++Change) { TimingData::const_iterator NextChange = (Change + 1); double ChangeTime = Change->Time + Drift + Offset; /* Find all VSpeeds if there exists a speed change which is virtually happening at the same time as this VSpeed modify it to be this value * factor */ bool MoveOn = false; for (auto Time = VerticalSpeeds.begin(); Time != VerticalSpeeds.end(); ++Time) { if (abs(ChangeTime - Time->Time) < 0.00001) { Time->Value *= Change->Value; MoveOn = true; } } if (MoveOn) continue; /* There are no collisions- insert a new speed at this time */ if (ChangeTime < 0) continue; float SpeedValue; SpeedValue = SectionValue(tVSpeeds, ChangeTime) * Change->Value; TimingSegment VSpeed; VSpeed.Time = ChangeTime; VSpeed.Value = SpeedValue; VerticalSpeeds.push_back(VSpeed); /* Theorically, if there were a VSpeed change after this one (such as a BPM change) we've got to modify them if they're between this and the next speed change. Apparently, this behaviour is a "bug" since osu!mania resets SV changes after a BPM change. */ if (BPMType == VSRG::Difficulty::BT_BEATSPACE) // Okay, we're an osu!mania chart, leave the resetting. continue; // We're not an osu!mania chart, so it's time to do what should be done. for (auto Time = VerticalSpeeds.begin(); Time != VerticalSpeeds.end(); ++Time) { if (Time->Time > ChangeTime) { // Two options, between two speed changes, or the last one. Second case, NextChange == Scrolls.end(). // Otherwise, just move on // Last speed change if (NextChange == Scrolls.end()) { Time->Value = Change->Value * SectionValue(tVSpeeds, Time->Time); } else { if (Time->Time < NextChange->Time) // Between speed changes Time->Value = Change->Value * SectionValue(tVSpeeds, Time->Time); } } } } std::sort(VerticalSpeeds.begin(), VerticalSpeeds.end()); }
void Difficulty::ProcessBPS(TimingData& BPS, double Drift) { /* Calculate BPS. The algorithm is basically the same as VSpeeds. BPS time is calculated applying the offset and drift. */ assert(Data != NULL); TimingData &StopsTiming = Data->Stops; BPS.clear(); for (auto Time = Timing.begin(); Time != Timing.end(); ++Time) { TimingSegment Seg; Seg.Time = TimeFromTimingKind(Timing, StopsTiming, *Time, BPMType, Offset, Drift); Seg.Value = BPSFromTimingKind(Time->Value, BPMType); BPS.push_back(Seg); } /* Sort for justice */ sort(BPS.begin(), BPS.end()); if (!StopsTiming.size() || BPMType != VSRG::Difficulty::BT_BEAT) // Stops only supported in Beat mode. return; /* Here on, just working with stops. */ for (auto Time = StopsTiming.begin(); Time != StopsTiming.end(); ++Time) { TimingSegment Seg; double TValue = TimeAtBeat(Timing, Offset + Drift, Time->Time) + StopTimeAtBeat(StopsTiming, Time->Time); double TValueN = TimeAtBeat(Timing, Offset + Drift, Time->Time) + StopTimeAtBeat(StopsTiming, Time->Time) + Time->Value; /* Initial Stop */ Seg.Time = TValue; Seg.Value = 0; /* First, eliminate collisions. */ for (auto k = BPS.begin(); k != BPS.end();) { if (k->Time == TValue) /* Equal? Remove the collision, leaving only the 0 in front. */ { k = BPS.erase(k); if (k == BPS.end()) break; else continue; } ++k; } // Okay, the collision is out. Let's push our 0-speeder. BPS.push_back(Seg); // Now we find what bps to restore to. float bpsRestore = bps(SectionValue(Timing, Time->Time)); for (auto k = BPS.begin(); k != BPS.end(); ) { if (k->Time > TValue && k->Time <= TValueN) // So wait, there's BPM changes in between? Holy shit. { bpsRestore = k->Value; /* This is the last speed change in the interval that the stop lasts. We'll use it. */ /* Eliminate this since we're not going to use it. */ k = BPS.erase(k); if (k == BPS.end()) break; continue; } ++k; } /* Restored speed after stop */ Seg.Time = TValueN; Seg.Value = bpsRestore; BPS.push_back(Seg); } std::sort(BPS.begin(), BPS.end()); }
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 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 ); } } } }