#include "global.h" #include "NotesLoaderSSC.h" #include "BackgroundUtil.h" #include "GameManager.h" #include "MsdFile.h" // No JSON here. #include "NoteTypes.h" #include "NotesLoaderSM.h" // For programming shortcuts. #include "RageFileManager.h" #include "RageLog.h" #include "RageUtil.h" #include "Song.h" #include "SongManager.h" #include "Steps.h" #include "Attack.h" #include "PrefsManager.h" // Everything from this line to the creation of parser_helper exists to // speed up parsing by allowing the use of std::map. All these functions // are put into a map of function pointers which is used when loading. // -Kyz /****************************************************************/ struct StepsTagInfo { SSCLoader* loader; Song* song; Steps* steps; TimingData* timing; const MsdFile::value_t* params; const RString& path; bool has_own_timing; bool ssc_format; bool from_cache; bool for_load_edit; StepsTagInfo(SSCLoader* l, Song* s, const RString& p, bool fc) :loader(l), song(s), path(p), has_own_timing(false), ssc_format(false), from_cache(fc), for_load_edit(false) {} }; struct SongTagInfo { SSCLoader* loader; Song* song; const MsdFile::value_t* params; const RString& path; bool from_cache; SongTagInfo(SSCLoader* l, Song* s, const RString& p, bool fc) :loader(l), song(s), path(p), from_cache(fc) {} }; // LoadNoteDataFromSimfile uses LoadNoteDataTagIDs because its parts operate // on state variables internal to the function. enum LoadNoteDataTagIDs { LNDID_version, LNDID_stepstype, LNDID_chartname, LNDID_description, LNDID_difficulty, LNDID_meter, LNDID_credit, LNDID_notes, LNDID_notes2, LNDID_notedata }; typedef void (*steps_tag_func_t)(StepsTagInfo& info); typedef void (*song_tag_func_t)(SongTagInfo& info); // Functions for song tags go below this line. -Kyz /****************************************************************/ void SetVersion(SongTagInfo& info) { info.song->m_fVersion = StringToFloat((*info.params)[1]); } void SetTitle(SongTagInfo& info) { info.song->m_sMainTitle = (*info.params)[1]; info.loader->SetSongTitle((*info.params)[1]); } void SetSubtitle(SongTagInfo& info) { info.song->m_sSubTitle = (*info.params)[1]; } void SetArtist(SongTagInfo& info) { info.song->m_sArtist = (*info.params)[1]; } void SetMainTitleTranslit(SongTagInfo& info) { info.song->m_sMainTitleTranslit = (*info.params)[1]; } void SetSubtitleTranslit(SongTagInfo& info) { info.song->m_sSubTitleTranslit = (*info.params)[1]; } void SetArtistTranslit(SongTagInfo& info) { info.song->m_sArtistTranslit = (*info.params)[1]; } void SetGenre(SongTagInfo& info) { info.song->m_sGenre = (*info.params)[1]; } void SetOrigin(SongTagInfo& info) { info.song->m_sOrigin = (*info.params)[1]; } void SetCredit(SongTagInfo& info) { info.song->m_sCredit = (*info.params)[1]; } void SetBanner(SongTagInfo& info) { info.song->m_sBannerFile = (*info.params)[1]; } void SetBackground(SongTagInfo& info) { info.song->m_sBackgroundFile = (*info.params)[1]; } void SetPreviewVid(SongTagInfo& info) { info.song->m_sPreviewVidFile = (*info.params)[1]; } void SetJacket(SongTagInfo& info) { info.song->m_sJacketFile = (*info.params)[1]; } void SetCDImage(SongTagInfo& info) { info.song->m_sCDFile = (*info.params)[1]; } void SetDiscImage(SongTagInfo& info) { info.song->m_sDiscFile = (*info.params)[1]; } void SetLyricsPath(SongTagInfo& info) { info.song->m_sLyricsFile = (*info.params)[1]; } void SetCDTitle(SongTagInfo& info) { info.song->m_sCDTitleFile = (*info.params)[1]; } void SetMusic(SongTagInfo& info) { info.song->m_sMusicFile = (*info.params)[1]; } void SetPreview(SongTagInfo& info) { info.song->m_PreviewFile= (*info.params)[1]; } void SetInstrumentTrack(SongTagInfo& info) { info.loader->ProcessInstrumentTracks(*info.song, (*info.params)[1]); } void SetMusicLength(SongTagInfo& info) { if(info.from_cache) info.song->m_fMusicLengthSeconds = StringToFloat((*info.params)[1]); } void SetLastSecondHint(SongTagInfo& info) { info.song->SetSpecifiedLastSecond(StringToFloat((*info.params)[1])); } void SetSampleStart(SongTagInfo& info) { info.song->m_fMusicSampleStartSeconds = HHMMSSToSeconds((*info.params)[1]); } void SetSampleLength(SongTagInfo& info) { info.song->m_fMusicSampleLengthSeconds = HHMMSSToSeconds((*info.params)[1]); } void SetDisplayBPM(SongTagInfo& info) { // #DISPLAYBPM:[xxx][xxx:xxx]|[*]; if((*info.params)[1] == "*") { info.song->m_DisplayBPMType = DISPLAY_BPM_RANDOM; } else { info.song->m_DisplayBPMType = DISPLAY_BPM_SPECIFIED; info.song->m_fSpecifiedBPMMin = StringToFloat((*info.params)[1]); if((*info.params)[2].empty()) { info.song->m_fSpecifiedBPMMax = info.song->m_fSpecifiedBPMMin; } else { info.song->m_fSpecifiedBPMMax = StringToFloat((*info.params)[2]); } } } void SetSelectable(SongTagInfo& info) { if((*info.params)[1].EqualsNoCase("YES")) { info.song->m_SelectionDisplay = info.song->SHOW_ALWAYS; } else if((*info.params)[1].EqualsNoCase("NO")) { info.song->m_SelectionDisplay = info.song->SHOW_NEVER; } // ROULETTE from 3.9 is no longer in use. else if((*info.params)[1].EqualsNoCase("ROULETTE")) { info.song->m_SelectionDisplay = info.song->SHOW_ALWAYS; } /* The following two cases are just fixes to make sure simfiles that * used 3.9+ features are not excluded here */ else if((*info.params)[1].EqualsNoCase("ES") || (*info.params)[1].EqualsNoCase("OMES")) { info.song->m_SelectionDisplay = info.song->SHOW_ALWAYS; } else if(StringToInt((*info.params)[1]) > 0) { info.song->m_SelectionDisplay = info.song->SHOW_ALWAYS; } else { LOG->UserLog("Song file", info.path, "has an unknown #SELECTABLE value, \"%s\"; ignored.", (*info.params)[1].c_str()); } } void SetBGChanges(SongTagInfo& info) { info.loader->ProcessBGChanges(*info.song, (*info.params)[0], info.path, (*info.params)[1]); } void SetFGChanges(SongTagInfo& info) { vector aFGChangeExpressions; split((*info.params)[1], ",", aFGChangeExpressions); for(size_t b = 0; b < aFGChangeExpressions.size(); ++b) { BackgroundChange change; if(info.loader->LoadFromBGChangesString(change, aFGChangeExpressions[b])) { info.song->AddForegroundChange(change); } } } void SetKeysounds(SongTagInfo& info) { RString keysounds = (*info.params)[1]; if(keysounds.length() >= 2 && keysounds.substr(0, 2) == "\\#") { keysounds = keysounds.substr(1); } split(keysounds, ",", info.song->m_vsKeysoundFile); } void SetAttacks(SongTagInfo& info) { info.loader->ProcessAttackString(info.song->m_sAttackString, (*info.params)); info.loader->ProcessAttacks(info.song->m_Attacks, (*info.params)); } void SetOffset(SongTagInfo& info) { info.song->m_SongTiming.m_fBeat0OffsetInSeconds = StringToFloat((*info.params)[1]); } void SetSongStops(SongTagInfo& info) { info.loader->ProcessStops(info.song->m_SongTiming, (*info.params)[1]); } void SetSongDelays(SongTagInfo& info) { info.loader->ProcessDelays(info.song->m_SongTiming, (*info.params)[1]); } void SetSongBPMs(SongTagInfo& info) { info.loader->ProcessBPMs(info.song->m_SongTiming, (*info.params)[1]); } void SetSongWarps(SongTagInfo& info) { info.loader->ProcessWarps( info.song->m_SongTiming, (*info.params)[1], info.song->m_fVersion ); } void SetSongLabels(SongTagInfo& info) { info.loader->ProcessLabels( info.song->m_SongTiming, (*info.params)[1] ); } void SetSongTimeSignatures(SongTagInfo& info) { info.loader->ProcessTimeSignatures(info.song->m_SongTiming, (*info.params)[1]); } void SetSongTickCounts(SongTagInfo& info) { info.loader->ProcessTickcounts(info.song->m_SongTiming, (*info.params)[1]); } void SetSongCombos(SongTagInfo& info) { info.loader->ProcessCombos( info.song->m_SongTiming, (*info.params)[1] ); } void SetSongSpeeds(SongTagInfo& info) { info.loader->ProcessSpeeds(info.song->m_SongTiming, (*info.params)[1]); } void SetSongScrolls(SongTagInfo& info) { info.loader->ProcessScrolls(info.song->m_SongTiming, (*info.params)[1]); } void SetSongFakes(SongTagInfo& info) { info.loader->ProcessFakes(info.song->m_SongTiming, (*info.params)[1]); } void SetFirstSecond(SongTagInfo& info) { if(info.from_cache) { info.song->SetFirstSecond(StringToFloat((*info.params)[1])); } } void SetLastSecond(SongTagInfo& info) { if(info.from_cache) { info.song->SetLastSecond(StringToFloat((*info.params)[1])); } } void SetSongFilename(SongTagInfo& info) { if(info.from_cache) { info.song->m_sSongFileName = (*info.params)[1]; } } void SetHasMusic(SongTagInfo& info) { if(info.from_cache) { info.song->m_bHasMusic = StringToInt((*info.params)[1]) != 0; } } void SetHasBanner(SongTagInfo& info) { if(info.from_cache) { info.song->m_bHasBanner = StringToInt((*info.params)[1]) != 0; } } // Functions for steps tags go below this line. -Kyz /****************************************************************/ void SetStepsVersion(StepsTagInfo& info) { info.song->m_fVersion = StringToFloat((*info.params)[1]); } void SetChartName(StepsTagInfo& info) { info.steps->SetChartName((*info.params)[1]); } void SetStepsType(StepsTagInfo& info) { info.steps->m_StepsType = GAMEMAN->StringToStepsType((*info.params)[1]); info.steps->m_StepsTypeStr= (*info.params)[1]; info.ssc_format= true; } void SetChartStyle(StepsTagInfo& info) { info.steps->SetChartStyle((*info.params)[1]); info.ssc_format= true; } void SetDescription(StepsTagInfo& info) { if(info.song->m_fVersion < VERSION_CHART_NAME_TAG && !info.for_load_edit) { info.steps->SetChartName((*info.params)[1]); } else { info.steps->SetDescription((*info.params)[1]); } info.ssc_format= true; } void SetDifficulty(StepsTagInfo& info) { info.steps->SetDifficulty(StringToDifficulty((*info.params)[1])); info.ssc_format= true; } void SetMeter(StepsTagInfo& info) { info.steps->SetMeter(StringToInt((*info.params)[1])); info.ssc_format= true; } void SetRadarValues(StepsTagInfo& info) { if(info.from_cache || info.for_load_edit) { vector values; split((*info.params)[1], ",", values, true); // Instead of trying to use the version to figure out how many // categories to expect, look at the number of values and split them // evenly. -Kyz size_t cats_per_player= values.size() / NUM_PlayerNumber; RadarValues v[NUM_PLAYERS]; FOREACH_PlayerNumber(pn) { for(size_t i= 0; i < cats_per_player; ++i) { v[pn][i]= StringToFloat(values[pn * cats_per_player + i]); } } info.steps->SetCachedRadarValues(v); } else { // just recalc at time. } info.ssc_format= true; } void SetCredit(StepsTagInfo& info) { info.steps->SetCredit((*info.params)[1]); info.ssc_format= true; } void SetStepsMusic(StepsTagInfo& info) { info.steps->SetMusicFile((*info.params)[1]); } void SetStepsBPMs(StepsTagInfo& info) { if(info.song->m_fVersion >= VERSION_SPLIT_TIMING || info.for_load_edit) { info.loader->ProcessBPMs(*info.timing, (*info.params)[1]); info.has_own_timing = true; } info.ssc_format= true; } void SetStepsStops(StepsTagInfo& info) { if(info.song->m_fVersion >= VERSION_SPLIT_TIMING || info.for_load_edit) { info.loader->ProcessStops(*info.timing, (*info.params)[1]); info.has_own_timing = true; } info.ssc_format= true; } void SetStepsDelays(StepsTagInfo& info) { if(info.song->m_fVersion >= VERSION_SPLIT_TIMING || info.for_load_edit) { info.loader->ProcessDelays(*info.timing, (*info.params)[1]); info.has_own_timing = true; } info.ssc_format= true; } void SetStepsTimeSignatures(StepsTagInfo& info) { if(info.song->m_fVersion >= VERSION_SPLIT_TIMING || info.for_load_edit) { info.loader->ProcessTimeSignatures(*info.timing, (*info.params)[1]); info.has_own_timing = true; } info.ssc_format= true; } void SetStepsTickCounts(StepsTagInfo& info) { if(info.song->m_fVersion >= VERSION_SPLIT_TIMING || info.for_load_edit) { info.loader->ProcessTickcounts(*info.timing, (*info.params)[1]); info.has_own_timing = true; } info.ssc_format= true; } void SetStepsCombos(StepsTagInfo& info) { if(info.song->m_fVersion >= VERSION_SPLIT_TIMING || info.for_load_edit) { info.loader->ProcessCombos(*info.timing, (*info.params)[1]); info.has_own_timing = true; } info.ssc_format= true; } void SetStepsWarps(StepsTagInfo& info) { if(info.song->m_fVersion >= VERSION_SPLIT_TIMING || info.for_load_edit) { info.loader->ProcessWarps(*info.timing, (*info.params)[1], info.song->m_fVersion); info.has_own_timing = true; } info.ssc_format= true; } void SetStepsSpeeds(StepsTagInfo& info) { if(info.song->m_fVersion >= VERSION_SPLIT_TIMING || info.for_load_edit) { info.loader->ProcessSpeeds(*info.timing, (*info.params)[1]); info.has_own_timing = true; } info.ssc_format= true; } void SetStepsScrolls(StepsTagInfo& info) { if(info.song->m_fVersion >= VERSION_SPLIT_TIMING || info.for_load_edit) { info.loader->ProcessScrolls(*info.timing, (*info.params)[1]); info.has_own_timing = true; } info.ssc_format= true; } void SetStepsFakes(StepsTagInfo& info) { if(info.song->m_fVersion >= VERSION_SPLIT_TIMING || info.for_load_edit) { info.loader->ProcessFakes(*info.timing, (*info.params)[1]); info.has_own_timing = true; } info.ssc_format= true; } void SetStepsLabels(StepsTagInfo& info) { if(info.song->m_fVersion >= VERSION_SPLIT_TIMING || info.for_load_edit) { info.loader->ProcessLabels(*info.timing, (*info.params)[1]); info.has_own_timing = true; } info.ssc_format= true; } void SetStepsAttacks(StepsTagInfo& info) { if(info.song->m_fVersion >= VERSION_SPLIT_TIMING || info.for_load_edit) { info.loader->ProcessAttackString(info.steps->m_sAttackString, *info.params); info.loader->ProcessAttacks(info.steps->m_Attacks, *info.params); } } void SetStepsOffset(StepsTagInfo& info) { if(info.song->m_fVersion >= VERSION_SPLIT_TIMING || info.for_load_edit) { info.timing->m_fBeat0OffsetInSeconds = StringToFloat((*info.params)[1]); info.has_own_timing = true; } } void SetStepsDisplayBPM(StepsTagInfo& info) { // #DISPLAYBPM:[xxx][xxx:xxx]|[*]; if((*info.params)[1] == "*") { info.steps->SetDisplayBPM(DISPLAY_BPM_RANDOM); } else { info.steps->SetDisplayBPM(DISPLAY_BPM_SPECIFIED); float min = StringToFloat((*info.params)[1]); info.steps->SetMinBPM(min); if((*info.params)[2].empty()) { info.steps->SetMaxBPM(min); } else { info.steps->SetMaxBPM(StringToFloat((*info.params)[2])); } } } typedef std::map steps_handler_map_t; typedef std::map song_handler_map_t; typedef std::map load_note_data_handler_map_t; struct ssc_parser_helper_t { steps_handler_map_t steps_tag_handlers; song_handler_map_t song_tag_handlers; load_note_data_handler_map_t load_note_data_handlers; // Unless signed, the comments in this tag list are not by me. They were // moved here when converting from the else if chain. -Kyz ssc_parser_helper_t() { song_tag_handlers["VERSION"]= &SetVersion; song_tag_handlers["TITLE"]= &SetTitle; song_tag_handlers["SUBTITLE"]= &SetSubtitle; song_tag_handlers["ARTIST"]= &SetArtist; song_tag_handlers["TITLETRANSLIT"]= &SetMainTitleTranslit; song_tag_handlers["SUBTITLETRANSLIT"]= &SetSubtitleTranslit; song_tag_handlers["ARTISTTRANSLIT"]= &SetArtistTranslit; song_tag_handlers["GENRE"]= &SetGenre; song_tag_handlers["ORIGIN"]= &SetOrigin; song_tag_handlers["CREDIT"]= &SetCredit; song_tag_handlers["BANNER"]= &SetBanner; song_tag_handlers["BACKGROUND"]= &SetBackground; song_tag_handlers["PREVIEWVID"]= &SetPreviewVid; song_tag_handlers["JACKET"]= &SetJacket; song_tag_handlers["CDIMAGE"]= &SetCDImage; song_tag_handlers["DISCIMAGE"]= &SetDiscImage; song_tag_handlers["LYRICSPATH"]= &SetLyricsPath; song_tag_handlers["CDTITLE"]= &SetCDTitle; song_tag_handlers["MUSIC"]= &SetMusic; song_tag_handlers["PREVIEW"]= &SetPreview; song_tag_handlers["INSTRUMENTTRACK"]= &SetInstrumentTrack; song_tag_handlers["MUSICLENGTH"]= &SetMusicLength; song_tag_handlers["LASTSECONDHINT"]= &SetLastSecondHint; song_tag_handlers["SAMPLESTART"]= &SetSampleStart; song_tag_handlers["SAMPLELENGTH"]= &SetSampleLength; song_tag_handlers["DISPLAYBPM"]= &SetDisplayBPM; song_tag_handlers["SELECTABLE"]= &SetSelectable; // It's a bit odd to have the tag that exists for backwards compatibility // in this list and not the replacement, but the BGCHANGES tag has a // number on the end, allowing up to NUM_BackgroundLayer tags, so it // can't fit in the map. -Kyz song_tag_handlers["ANIMATIONS"]= &SetBGChanges; song_tag_handlers["FGCHANGES"]= &SetFGChanges; song_tag_handlers["KEYSOUNDS"]= &SetKeysounds; song_tag_handlers["ATTACKS"]= &SetAttacks; song_tag_handlers["OFFSET"]= &SetOffset; /* Below are the song based timings that should only be used * if the steps do not have their own timing. */ song_tag_handlers["STOPS"]= &SetSongStops; song_tag_handlers["DELAYS"]= &SetSongDelays; song_tag_handlers["BPMS"]= &SetSongBPMs; song_tag_handlers["WARPS"]= &SetSongWarps; song_tag_handlers["LABELS"]= &SetSongLabels; song_tag_handlers["TIMESIGNATURES"]= &SetSongTimeSignatures; song_tag_handlers["TICKCOUNTS"]= &SetSongTickCounts; song_tag_handlers["COMBOS"]= &SetSongCombos; song_tag_handlers["SPEEDS"]= &SetSongSpeeds; song_tag_handlers["SCROLLS"]= &SetSongScrolls; song_tag_handlers["FAKES"]= &SetSongFakes; /* The following are cache tags. Never fill their values * directly: only from the cached version. */ song_tag_handlers["FIRSTSECOND"]= &SetFirstSecond; song_tag_handlers["LASTSECOND"]= &SetLastSecond; song_tag_handlers["SONGFILENAME"]= &SetSongFilename; song_tag_handlers["HASMUSIC"]= &SetHasMusic; song_tag_handlers["HASBANNER"]= &SetHasBanner; /* Tags that no longer exist, listed for posterity. May their names * never be forgotten for their service to Stepmania. -Kyz * LASTBEATHINT: // unable to parse due to tag position. Ignore. * MUSICBYTES: // ignore * FIRSTBEAT: // no longer used. * LASTBEAT: // no longer used. */ steps_tag_handlers["VERSION"]= &SetStepsVersion; steps_tag_handlers["CHARTNAME"]= &SetChartName; steps_tag_handlers["STEPSTYPE"]= &SetStepsType; steps_tag_handlers["CHARTSTYLE"]= &SetChartStyle; steps_tag_handlers["DESCRIPTION"]= &SetDescription; steps_tag_handlers["DIFFICULTY"]= &SetDifficulty; steps_tag_handlers["METER"]= &SetMeter; steps_tag_handlers["RADARVALUES"]= &SetRadarValues; steps_tag_handlers["CREDIT"]= &SetCredit; steps_tag_handlers["MUSIC"]= &SetStepsMusic; steps_tag_handlers["BPMS"]= &SetStepsBPMs; steps_tag_handlers["STOPS"]= &SetStepsStops; steps_tag_handlers["DELAYS"]= &SetStepsDelays; steps_tag_handlers["TIMESIGNATURES"]= &SetStepsTimeSignatures; steps_tag_handlers["TICKCOUNTS"]= &SetStepsTickCounts; steps_tag_handlers["COMBOS"]= &SetStepsCombos; steps_tag_handlers["WARPS"]= &SetStepsWarps; steps_tag_handlers["SPEEDS"]= &SetStepsSpeeds; steps_tag_handlers["SCROLLS"]= &SetStepsScrolls; steps_tag_handlers["FAKES"]= &SetStepsFakes; steps_tag_handlers["LABELS"]= &SetStepsLabels; /* If this is called, the chart does not use the same attacks * as the Song's timing. No other changes are required. */ steps_tag_handlers["ATTACKS"]= &SetStepsAttacks; steps_tag_handlers["OFFSET"]= &SetStepsOffset; steps_tag_handlers["DISPLAYBPM"]= &SetStepsDisplayBPM; load_note_data_handlers["VERSION"]= LNDID_version; load_note_data_handlers["STEPSTYPE"]= LNDID_stepstype; load_note_data_handlers["CHARTNAME"]= LNDID_chartname; load_note_data_handlers["DESCRIPTION"]= LNDID_description; load_note_data_handlers["DIFFICULTY"]= LNDID_difficulty; load_note_data_handlers["METER"]= LNDID_meter; load_note_data_handlers["CREDIT"]= LNDID_credit; load_note_data_handlers["NOTES"]= LNDID_notes; load_note_data_handlers["NOTES2"]= LNDID_notes2; load_note_data_handlers["NOTEDATA"]= LNDID_notedata; } }; ssc_parser_helper_t parser_helper; // End parser_helper related functions. -Kyz /****************************************************************/ void SSCLoader::ProcessBPMs( TimingData &out, const RString sParam ) { vector arrayBPMExpressions; split( sParam, ",", arrayBPMExpressions ); for( unsigned b=0; b arrayBPMValues; split( arrayBPMExpressions[b], "=", arrayBPMValues ); if( arrayBPMValues.size() != 2 ) { LOG->UserLog("Song file", this->GetSongTitle(), "has an invalid #BPMS value \"%s\" (must have exactly one '='), ignored.", arrayBPMExpressions[b].c_str() ); continue; } const float fBeat = StringToFloat( arrayBPMValues[0] ); const float fNewBPM = StringToFloat( arrayBPMValues[1] ); if( fBeat >= 0 && fNewBPM > 0 ) { out.AddSegment( BPMSegment(BeatToNoteRow(fBeat), fNewBPM) ); } else { LOG->UserLog("Song file", this->GetSongTitle(), "has an invalid BPM at beat %f, BPM %f.", fBeat, fNewBPM ); } } } void SSCLoader::ProcessStops( TimingData &out, const RString sParam ) { vector arrayStopExpressions; split( sParam, ",", arrayStopExpressions ); for( unsigned b=0; b 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::ProcessWarps( TimingData &out, const RString sParam, const float fVersion ) { vector arrayWarpExpressions; split( sParam, ",", arrayWarpExpressions ); for( unsigned b=0; b 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::ProcessLabels( TimingData &out, const RString sParam ) { vector arrayLabelExpressions; split( sParam, ",", arrayLabelExpressions ); for( unsigned b=0; b 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::ProcessCombos( TimingData &out, const RString line, const int rowsPerBeat ) { vector arrayComboExpressions; split( line, ",", arrayComboExpressions ); for( unsigned f=0; f 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 SSCLoader::ProcessScrolls( TimingData &out, const RString sParam ) { vector vs1; split( sParam, ",", vs1 ); FOREACH_CONST( RString, vs1, s1 ) { vector vs2; split( *s1, "=", vs2 ); if( vs2.size() < 2 ) { LOG->UserLog("Song file", this->GetSongTitle(), "has an scroll change with %i values.", static_cast(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) ); } } bool SSCLoader::LoadNoteDataFromSimfile( const RString & cachePath, Steps &out ) { LOG->Trace( "Loading notes from %s", cachePath.c_str() ); MsdFile msd; if (!msd.ReadFile(cachePath, true)) { LOG->UserLog("Unable to load any notes from", cachePath, "for this reason: %s", msd.GetError().c_str()); return false; } bool tryingSteps = false; float storedVersion = 0; const unsigned values = msd.GetNumValues(); for (unsigned i = 0; i < values; i++) { const MsdFile::value_t ¶ms = msd.GetValue(i); RString valueName = params[0]; valueName.MakeUpper(); RString matcher = params[1]; // mainly for debugging. Trim(matcher); load_note_data_handler_map_t::iterator handler= parser_helper.load_note_data_handlers.find(valueName); if(handler != parser_helper.load_note_data_handlers.end()) { if(tryingSteps) { switch(handler->second) { case LNDID_version: // Note that version is in both switches. Formerly, it was // checked before the tryingSteps condition. -Kyz storedVersion = StringToFloat(matcher); break; case LNDID_stepstype: if(out.m_StepsType != GAMEMAN->StringToStepsType(matcher)) { tryingSteps = false; } break; case LNDID_chartname: if(storedVersion >= VERSION_CHART_NAME_TAG && out.GetChartName() != matcher) { tryingSteps = false; } break; case LNDID_description: if(storedVersion < VERSION_CHART_NAME_TAG) { if(out.GetChartName() != matcher) { tryingSteps = false; } } else if(out.GetDescription() != matcher) { tryingSteps = false; } break; case LNDID_difficulty: // Accept any difficulty if it's an edit because LoadEditFromMsd // forces edits onto Edit difficulty even if they have a difficulty // tag. -Kyz if(out.GetDifficulty() != StringToDifficulty(matcher) && !(out.GetDifficulty() == Difficulty_Edit && GetExtension(cachePath).MakeLower() == "edit")) { tryingSteps = false; } break; case LNDID_meter: if(out.GetMeter() != StringToInt(matcher)) { tryingSteps = false; } break; case LNDID_credit: if(out.GetCredit() != matcher) { tryingSteps = false; } break; case LNDID_notes: case LNDID_notes2: out.SetSMNoteData(matcher); out.TidyUpData(); return true; break; } } else { switch(handler->second) { case LNDID_version: // Note that version is in both switches. Formerly, it was // checked before the tryingSteps condition. -Kyz storedVersion = StringToFloat(matcher); break; case LNDID_notedata: tryingSteps = true; break; } } } else { // Silently ignore unrecognized tags, as was done before. -Kyz } } return false; } bool SSCLoader::LoadFromSimfile( const RString &sPath, Song &out, bool bFromCache ) { LOG->Trace( "Song::LoadFromSSCFile(%s)", sPath.c_str() ); MsdFile msd; if( !msd.ReadFile( sPath, true ) ) { LOG->UserLog( "Song file", sPath, "couldn't be opened: %s", msd.GetError().c_str() ); return false; } out.m_SongTiming.m_sFile = sPath; // songs still have their fallback timing. out.m_sSongFileName = sPath; int state = GETTING_SONG_INFO; const unsigned values = msd.GetNumValues(); Steps* pNewNotes = NULL; TimingData stepsTiming; SongTagInfo reused_song_info(&*this, &out, sPath, bFromCache); StepsTagInfo reused_steps_info(&*this, &out, sPath, bFromCache); for( unsigned i = 0; i < values; i++ ) { const MsdFile::value_t &sParams = msd.GetValue(i); RString sValueName = sParams[0]; sValueName.MakeUpper(); switch (state) { case GETTING_SONG_INFO: { reused_song_info.params= &sParams; song_handler_map_t::iterator handler= parser_helper.song_tag_handlers.find(sValueName); if(handler != parser_helper.song_tag_handlers.end()) { handler->second(reused_song_info); } else if(sValueName.Left(strlen("BGCHANGES"))=="BGCHANGES") { SetBGChanges(reused_song_info); } // This tag will get us to the next section. else if(sValueName == "NOTEDATA") { state = GETTING_STEP_INFO; pNewNotes = out.CreateSteps(); stepsTiming = TimingData(out.m_SongTiming.m_fBeat0OffsetInSeconds); reused_steps_info.has_own_timing = false; reused_steps_info.steps= pNewNotes; reused_steps_info.timing= &stepsTiming; } else { // Silently ignore unrecognized tags, as was done before. -Kyz } break; } case GETTING_STEP_INFO: { reused_steps_info.params= &sParams; steps_handler_map_t::iterator handler= parser_helper.steps_tag_handlers.find(sValueName); if(handler != parser_helper.steps_tag_handlers.end()) { handler->second(reused_steps_info); } else if(sValueName=="NOTES" || sValueName=="NOTES2") { state = GETTING_SONG_INFO; if(reused_steps_info.has_own_timing) { pNewNotes->m_Timing = stepsTiming; } reused_steps_info.has_own_timing = false; pNewNotes->SetSMNoteData(sParams[1]); pNewNotes->TidyUpData(); pNewNotes->SetFilename(sPath); out.AddSteps(pNewNotes); } else if(sValueName=="STEPFILENAME") { state = GETTING_SONG_INFO; if(reused_steps_info.has_own_timing) { pNewNotes->m_Timing = stepsTiming; } reused_steps_info.has_own_timing = false; pNewNotes->SetFilename(sParams[1]); out.AddSteps(pNewNotes); } else { // Silently ignore unrecognized tags, as was done before. -Kyz } break; } } } out.m_fVersion = STEPFILE_VERSION_NUMBER; TidyUpData(out, bFromCache); return true; } bool SSCLoader::LoadEditFromFile( RString sEditFilePath, ProfileSlot slot, bool bAddStepsToSong, Song *givenSong /* =NULL */ ) { LOG->Trace( "SSCLoader::LoadEditFromFile(%s)", sEditFilePath.c_str() ); int iBytes = FILEMAN->GetFileSizeInBytes( sEditFilePath ); if( iBytes > MAX_EDIT_STEPS_SIZE_BYTES ) { LOG->UserLog("Edit file", sEditFilePath, "is unreasonably large. It won't be loaded." ); return false; } MsdFile msd; if( !msd.ReadFile( sEditFilePath, true ) ) // unescape { LOG->UserLog("Edit file", sEditFilePath, "couldn't be opened: %s", msd.GetError().c_str() ); return false; } return LoadEditFromMsd( msd, sEditFilePath, slot, bAddStepsToSong, givenSong ); } bool SSCLoader::LoadEditFromMsd(const MsdFile &msd, const RString &sEditFilePath, ProfileSlot slot, bool bAddStepsToSong, Song *givenSong /* =NULL */ ) { Song* pSong = givenSong; Steps* pNewNotes = NULL; TimingData stepsTiming; StepsTagInfo reused_steps_info(&*this, pSong, sEditFilePath, false); reused_steps_info.for_load_edit= true; reused_steps_info.timing= &stepsTiming; for(unsigned int i = 0; i < msd.GetNumValues(); ++i) { int iNumParams = msd.GetNumParams(i); const MsdFile::value_t &sParams = msd.GetValue(i); RString sValueName = sParams[0]; sValueName.MakeUpper(); if(pSong != NULL) { reused_steps_info.params= &sParams; steps_handler_map_t::iterator handler= parser_helper.steps_tag_handlers.find(sValueName); if(pNewNotes != NULL && handler != parser_helper.steps_tag_handlers.end()) { handler->second(reused_steps_info); } else if(sValueName=="NOTEDATA") { pNewNotes = pSong->CreateSteps(); reused_steps_info.steps= pNewNotes; reused_steps_info.ssc_format= true; } else if(sValueName=="NOTES") { if(pSong == NULL) { LOG->UserLog("Edit file", sEditFilePath, "doesn't have a #SONG tag preceeding the first #NOTES tag," " and is not in a valid song-specific folder."); return false; } if(!reused_steps_info.ssc_format && iNumParams < 7) { LOG->UserLog("Edit file", sEditFilePath, "has %d fields in a #NOTES tag, but should have at least 7.", iNumParams); continue; } // I have no idea what the purpose of this bAddStepsToSong flag is. // It looks like it causes LoadEditFromMsd to just throw away // whatever steps data was loaded, possibly leaking the memory used // by pNewNotes. It was here before I rewrote LoadEditFromMsd to // use a map instead of an else if chain, so I preserved it. -Kyz if(!bAddStepsToSong) { return true; } // Force the difficulty to edit in case the edit set its own // difficulty because IsEditAlreadyLoaded has an assert and edits // shouldn't be able to add charts of other difficulties. -Kyz if(pNewNotes != NULL) { pNewNotes->SetDifficulty(Difficulty_Edit); if(pSong->IsEditAlreadyLoaded(pNewNotes)) { LOG->UserLog("Edit file", sEditFilePath, "is a duplicate of another edit that was already loaded."); SAFE_DELETE(pNewNotes); return false; } } if(reused_steps_info.ssc_format) { if(reused_steps_info.has_own_timing) { pNewNotes->m_Timing = stepsTiming; } pNewNotes->SetSMNoteData(sParams[1]); pNewNotes->TidyUpData(); } else { pNewNotes = pSong->CreateSteps(); LoadFromTokens(sParams[1], sParams[2], sParams[3], sParams[4], sParams[5], sParams[6], *pNewNotes); } pNewNotes->SetLoadedFromProfile(slot); pNewNotes->SetDifficulty(Difficulty_Edit); pNewNotes->SetFilename(sEditFilePath); pSong->AddSteps(pNewNotes); return true; // Only allow one Steps per edit file! } else { LOG->UserLog("Edit file", sEditFilePath, "has an unexpected value \"%s\".", sValueName.c_str()); } } else { if(sValueName == "SONG") { if(pSong) { /* LOG->UserLog("Edit file", sEditFilePath, "has more than one #SONG tag."); return false; */ continue; } RString sSongFullTitle = sParams[1]; this->SetSongTitle(sParams[1]); sSongFullTitle.Replace('\\', '/'); pSong = SONGMAN->FindSong(sSongFullTitle); reused_steps_info.song= pSong; if(pSong == NULL) { LOG->UserLog("Edit file", sEditFilePath, "requires a song \"%s\" that isn't present.", sSongFullTitle.c_str()); return false; } if(pSong->GetNumStepsLoadedFromProfile(slot) >= MAX_EDITS_PER_SONG_PER_PROFILE) { LOG->UserLog("Song file", sSongFullTitle, "already has the maximum number of edits allowed for ProfileSlotP%d.", slot+1); return false; } reused_steps_info.song= pSong; } } } // Edit had no valid #NOTES sections return false; } /* * (c) 2011 Jason Felds * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, and/or sell copies of the Software, and to permit persons to * whom the Software is furnished to do so, provided that the above * copyright notice(s) and this permission notice appear in all copies of * the Software and that both the above copyright notice(s) and this * permission notice appear in supporting documentation. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF * THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS * INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT * OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS * OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR * PERFORMANCE OF THIS SOFTWARE. */