Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- #include "global.h"
- #include "NotesLoaderCHART.h"
- #include "RageUtil_CharConversions.h"
- #include "RageFile.h"
- #include "TimingData.h"
- #include "RageUtil.h"
- #include "RageLog.h"
- #include "BackgroundUtil.h"
- #include "NoteData.h"
- #include "Song.h"
- #include "Steps.h"
- #include "GameManager.h"
- #include "Difficulty.h"
- #include <string>
- #include <sstream>
- /**
- Example .chart file layout:
- [Song]
- {
- Name = "SONGNAME"
- Artist = "ARTIST"
- Charter = "CHARTERNAME"
- Offset = 0 (float, starting point in seconds)
- Resolution = 192 (ticks per beat)
- Player2 = bass (unused)
- Difficulty = 0 (?)
- PreviewStart = 0.00 (start of preview in seconds)
- PreviewEnd = 0.00 (end " " " " )
- Genre = "GENRE"
- MediaType = "IMPORT MEDIA, DEFAULT CD?" (unused ?)
- MusicStream = "AUDIOFILE NAME WITH EXTENSION"
- }
- [SyncTrack]
- {
- 0 = B 240000
- 0 = TS 4
- iTicks = <B, TS> BPM (to 3 decimal places as an int (or TS is X/4))
- }
- [Events]
- {
- iTicks = E "section SECTION_NAME_WITH_UNDERSCORES" (unused?)
- }
- [ExpertSingle] (always look for "Single" for now, first part is the difficulty)
- (Expert = Challenge, Hard = Hard, Medium = Medium, Easy = Easy)
- {
- iTicks = N iTrack iDuration
- (some lines may have S instead of N, that's for star power. Ignore those lines)
- }
- (other difficulties/modes go here)
- This is completely different layout than the already made file parsers, so I needed to make my own in here
- */
- // I'm not even going to pretend that the following wasn't copy/pasted from MsdFile.cpp, I needed to parse this somehow
- // Well it was originally, now it's just a mess
- void ReadBuf( const char *buf, int len, Song *outSong, Steps *outSteps )
- {
- bool parseSongInfo = outSong != NULL;
- bool bDontReadThis = false;
- int iMode = 0;
- int iPrevNoteMark = 0;
- int iPrevNoteTrack = 0;
- int iPrevNoteLength = 0;
- bool bPrevNoteIsHOPO = false;
- // 192 is default resolution per beat in GH
- int resolution = 192;
- bool readingNotes = false;
- vector<string> headerInfo(3);
- // NoteData to store stuff that's being parsed
- NoteData notedata;
- // TimingData to store other stuff being parsed
- TimingData timing;
- // Pointer to Steps that may or may not need to be added, depending on if we're parsing for a Song or not
- Steps* pNewNotes = NULL;
- // Dunno if this will actually do anything, but it seems to be defaults put into SSC files for some reason?
- timing.AddSegment(ComboSegment(0.0,1,1));
- timing.AddSegment(ScrollSegment(0.0,1.0));
- // Create timing data inside song if none exists
- //if( parseSongInfo && outSong->m_SongTiming == NULL ) outSong->m_SongTiming = TimingData();
- // Create timing data for the steps if none exists
- //if( !parseSongInfo && outSteps->m_Timing == NULL ) outSteps->m_Timing = TimingData();
- // Set notedata tracks
- notedata.SetNumTracks(6);
- // Screw efficiency, just gonna parse this word by word like I was gonna do with Python
- std::string bufStr(buf);
- std::istringstream iss(bufStr);
- std::string line;
- // Parse line-by-line
- while( std::getline(iss,line) )
- {
- // Remove tabs
- line.erase(std::remove(line.begin(), line.end(), '\t'), line.end());
- vector<string> vsWords;
- std::istringstream lineIss(line);
- // Separate by spaces
- copy( istream_iterator<string>(lineIss), istream_iterator<string>(), back_inserter(vsWords) );
- int i = 0;
- int numWords = vsWords.size();
- // Don't need to parse through every word, since each of these if blocks parse the entire line
- /**
- Change the mode once you hit a section tag. Until the actual notes, the tags should always be in the order:
- Song, SyncTrack, Events, then comes the notes which are tagged by difficulty and game mode
- */
- if( vsWords[i].at(0) == '[' ) {
- if( iMode < 4 ) ++iMode;
- // Labels actually have a use! how 'bout that!
- //if( iMode == 3 ) bDontReadThis = true;
- bDontReadThis = false;
- if( iMode == 4 ) {
- // If it's not single mode, we don't care about it
- if( vsWords[i].find("Single") != std::string::npos ) {
- // If we're parsing a song, add the previous notes parsed, then get space for new steps
- if( parseSongInfo ) {
- if( readingNotes ) {
- pNewNotes->m_Timing = timing;
- pNewNotes->SetNoteData( notedata );
- pNewNotes->TidyUpData();
- outSong->AddSteps( pNewNotes );
- // Reset notedata for new set of steps
- notedata = NoteData();
- notedata.SetNumTracks(6);
- } else {
- outSong->m_SongTiming = timing;
- }
- pNewNotes = outSong->CreateSteps();
- pNewNotes->SetChartStyle("Guitar");
- pNewNotes->SetCredit( headerInfo[1] );
- pNewNotes->SetDescription( headerInfo[1] );
- pNewNotes->SetMusicFile( headerInfo[2] );
- // Charts only load for guitar mode... for now.
- pNewNotes->m_StepsType = StepsType_guitar_solo;
- }
- if( !readingNotes ) readingNotes = true;
- if( vsWords[i].find("Expert") != std::string::npos ) {
- // Expert == Challenge
- if( !parseSongInfo && outSteps->GetDifficulty() != Difficulty_Challenge ) {
- // This is the incorrect difficulty for the desired steps, continue to the next noteinfo tag
- bDontReadThis = true;
- continue;
- }
- if( parseSongInfo ) {
- pNewNotes->SetDifficulty( Difficulty_Challenge );
- pNewNotes->SetChartName( headerInfo[0]+" - Expert" );
- continue;
- }
- } else if( vsWords[i].find("Hard") != std::string::npos ) {
- if( !parseSongInfo && outSteps->GetDifficulty() != Difficulty_Hard ) {
- bDontReadThis = true;
- continue;
- }
- if( parseSongInfo ) {
- pNewNotes->SetDifficulty( Difficulty_Hard );
- pNewNotes->SetChartName( headerInfo[0]+" - Hard" );
- continue;
- }
- } else if( vsWords[i].find("Medium") != std::string::npos ) {
- if( !parseSongInfo && outSteps->GetDifficulty() != Difficulty_Medium ) {
- bDontReadThis = true;
- continue;
- }
- if( parseSongInfo ) {
- pNewNotes->SetDifficulty( Difficulty_Medium );
- pNewNotes->SetChartName( headerInfo[0]+" - Medium" );
- continue;
- }
- } else if( vsWords[i].find("Easy") != std::string::npos ) {
- if( !parseSongInfo && outSteps->GetDifficulty() != Difficulty_Easy ) {
- bDontReadThis = true;
- continue;
- }
- if( parseSongInfo ) {
- pNewNotes->SetDifficulty( Difficulty_Easy );
- pNewNotes->SetChartName( headerInfo[0]+" - Easy" );
- continue;
- }
- }
- // Custom Guitar Hero charts have no beginner difficulty, this was added starting in GH World Tour
- } else {
- bDontReadThis = true;
- continue;
- }
- } else {
- continue;
- }
- }
- /* Curly braces are just filler */
- if( !bDontReadThis && vsWords[i].at(0) != '{' && vsWords[i].at(0) != '}' )
- {
- if( iMode == 1 ) {
- // Parse song information
- // Separation by words also means that titles, artists and other info is likely broken up, stitch it back together
- int j = 2;
- string wholeString = "";
- while(j < numWords) {
- if(j>2) wholeString += " ";
- wholeString += vsWords[j];
- j++;
- }
- if( !vsWords[i].compare("Name") )
- {
- string mainTitle = wholeString.substr(1, wholeString.size()-2);
- headerInfo[0] = mainTitle;
- if( parseSongInfo ) outSong->m_sMainTitle = mainTitle;
- } else if( !vsWords[i].compare("Artist") )
- {
- if( parseSongInfo ) outSong->m_sArtist = wholeString.substr(1, wholeString.size()-2);
- } else if( !vsWords[i].compare("Charter") )
- {
- headerInfo[1] = wholeString.substr(1, wholeString.size()-2);
- } else if( !vsWords[i].compare("Offset") )
- {
- timing.m_fBeat0OffsetInSeconds = atof(vsWords[i+2].c_str());
- } else if( !vsWords[i].compare("Resolution") )
- {
- /* Here's the thing, Guitar Hero has a default resolution of 192 'ticks' per beat. Comparatively, Stepmania uses
- * 192 ticks per MEASURE as a maximum. This causes a lot of problems. If the resolution is >48, just make it 48
- * But only for setting the tickcount segment, still need the original resolution for note and timing parsing
- */
- resolution = atoi(vsWords[i+2].c_str());
- /* testing
- if( parseSongInfo ) {
- if( resolution <= 48 ) outSong->m_SongTiming.AddSegment(TickcountSegment(0, resolution));
- else outSong->m_SongTiming.AddSegment(TickcountSegment(0, 48));
- } */
- } else if( !vsWords[i].compare("Difficulty") )
- {
- /* Uh, skip this for now, causes Stepmania to crash...
- if( parseSongInfo ) pNewNotes->SetMeter(atoi(vsWords[i+2].c_str()) + 1);
- else outSteps->SetMeter(atoi(vsWords[i+2].c_str()) + 1);
- */
- } else if( !vsWords[i].compare("PreviewStart") )
- {
- if( parseSongInfo ) outSong->m_fMusicSampleStartSeconds = atof(vsWords[i+2].c_str());
- } else if( !vsWords[i].compare("PreviewEnd") )
- {
- if( parseSongInfo )
- {
- // Default to 12 second length, just seems like the default in StepMania
- float estSampleSec = atof(vsWords[i+2].c_str()) - outSong->m_fMusicSampleStartSeconds;
- if( estSampleSec <= 12 ) outSong->m_fMusicSampleStartSeconds = 12;
- else outSong->m_fMusicSampleLengthSeconds = estSampleSec;
- }
- } else if( !vsWords[i].compare("MusicStream") )
- {
- string songFile = wholeString.substr(1, wholeString.size()-2);
- if( parseSongInfo ) outSong->m_sMusicFile = songFile;
- else outSteps->SetMusicFile( songFile );
- headerInfo[2] = songFile;
- }
- } else if( iMode == 2 ) {
- // Parse BPM and time signature changes
- // Steps and songs can have different timing data. Why?
- int startMark = atoi(vsWords[i].c_str());
- float newBPM = atof(vsWords[i+3].c_str()) / 1000;
- if( !vsWords[i+2].compare( "B" ) )
- {
- if( startMark == 0 ) timing.AddSegment( BPMSegment(0, newBPM ));
- else timing.SetBPMAtBeat((float)startMark/resolution, newBPM);
- } else if( !vsWords[i+2].compare( "TS" ) )
- {
- // Fun fact, Guitar Hero songs are always in a time signature of x/4
- timing.AddSegment( TimeSignatureSegment(BeatToNoteRow(atof(vsWords[i].c_str()) / resolution), atoi(vsWords[i+3].c_str()), 4) );
- }
- } else if( iMode == 3 ) {
- // Parse section labels
- int end = vsWords.size()-1;
- string sectionTitle = vsWords[end].substr(0, vsWords[end].size()-1);
- timing.AddSegment(LabelSegment(BeatToNoteRow(atof(vsWords[i].c_str())/resolution),sectionTitle));
- } else if( iMode == 4 ) {
- // Parse the notedata
- // Some other markers for star power and such are thrown in here, ignore them
- if( !vsWords[i+2].compare( "N" ) )
- {
- /* A note on sustained notes, Guitar Hero likes to have holds end exactly on beats, unfortunately this means that
- * sometimes a hold note can overlap into the next hold if they're in the same track. Need to check for this and
- * correct it by shortening the first hold slightly (1/4 resolution)
- */
- int iNoteMark = atoi(vsWords[i].c_str());
- int iNoteTrack = atoi(vsWords[i+3].c_str());
- int iNoteLength = atoi(vsWords[i+4].c_str());
- if( iNoteTrack == iPrevNoteTrack && iPrevNoteLength + iPrevNoteMark >= iNoteMark ) {
- notedata.SetTapNote(iPrevNoteTrack, BeatToNoteRow((float)iPrevNoteMark/resolution), TAP_EMPTY);
- iPrevNoteLength = iNoteMark - iPrevNoteMark - (resolution / 4);
- notedata.AddHoldNote(iPrevNoteTrack, BeatToNoteRow((float)iPrevNoteMark/resolution),
- BeatToNoteRow((float)(iPrevNoteMark+iPrevNoteLength)/resolution), TAP_ORIGINAL_GEM_HOLD);
- }
- // If this note and the previous note are on the same beat and the previous note was a HOPO,
- // Change it to a normal gem
- if( bPrevNoteIsHOPO && (iNoteMark == iPrevNoteMark) )
- {
- if( iPrevNoteLength ) // Previous note was a hold
- {
- notedata.AddHoldNote(iPrevNoteTrack, BeatToNoteRow((float)iPrevNoteMark/resolution),
- BeatToNoteRow((float)(iPrevNoteMark+iPrevNoteLength)/resolution), TAP_ORIGINAL_GEM_HOLD);
- } else
- {
- notedata.SetTapNote(iPrevNoteTrack, BeatToNoteRow((float)iPrevNoteMark/resolution), TAP_ORIGINAL_GEM);
- }
- }
- // If the difference between this note and the last is <= 1/16th note (and > 0), and they're
- // on different tracks, make this a HOPO
- if( (iNoteMark - iPrevNoteMark <= resolution / 16) && (iNoteMark != iPrevNoteMark) && (iNoteTrack != iPrevNoteTrack) )
- {
- if( iNoteLength )
- {
- notedata.AddHoldNote(iNoteTrack, BeatToNoteRow((float)iNoteMark/resolution),
- BeatToNoteRow((float)(iNoteMark+iNoteLength)/resolution), TAP_ORIGINAL_HOPO_HOLD);
- } else
- {
- notedata.SetTapNote(iNoteTrack, BeatToNoteRow((float)iNoteMark/resolution), TAP_ORIGINAL_HOPO);
- }
- bPrevNoteIsHOPO = true;
- }
- // Otherwise, plop a Gem in there
- else
- {
- if( iNoteLength )
- {
- notedata.AddHoldNote(iNoteTrack, BeatToNoteRow((float)iNoteMark/resolution),
- BeatToNoteRow((float)(iNoteMark+iNoteLength)/resolution), TAP_ORIGINAL_GEM_HOLD);
- } else
- {
- notedata.SetTapNote(iNoteTrack, BeatToNoteRow((float)iNoteMark/resolution),TAP_ORIGINAL_GEM);
- }
- bPrevNoteIsHOPO = false;
- }
- iPrevNoteMark = iNoteMark;
- iPrevNoteTrack = iNoteTrack;
- iPrevNoteLength = iNoteLength;
- }
- }
- }
- }
- // If we reached the end of the file while still reading notes, add them to the song, if we're parsing that
- if( parseSongInfo && readingNotes ) {
- pNewNotes->m_Timing = timing;
- pNewNotes->SetNoteData( notedata );
- pNewNotes->TidyUpData();
- outSong->AddSteps(pNewNotes);
- }
- if( !parseSongInfo )
- {
- outSteps->m_Timing = timing;
- outSteps->SetNoteData( notedata );
- outSteps->TidyUpData();
- }
- }
- // Returns true if successful, false otherwise
- // Pass Steps and Song by pointer, so they can be null incase they aren't used
- bool ReadFile( RString sNewPath, Song *outSong, Steps *outSteps )
- {
- RageFile f;
- /* Open a file. */
- if( !f.Open( sNewPath ) )return false;
- // allocate a string to hold the file
- RString FileString;
- FileString.reserve( f.GetFileSize() );
- int iBytesRead = f.Read( FileString );
- if( iBytesRead == -1 )return false;
- ReadBuf( FileString.c_str(), iBytesRead, outSong, outSteps );
- return true;
- }
- void CHARTLoader::GetApplicableFiles( const RString &sPath, vector<RString> &out )
- {
- GetDirListing( sPath + RString("*.chart"), out );
- }
- bool CHARTLoader::LoadFromDir( const RString &sDir, Song &out ) {
- LOG->Trace( "CHARTLoader::LoadFromDir(%s)", sDir.c_str() );
- vector<RString> arrayCHARTFileNames;
- GetDirListing( sDir + RString("*.chart"), arrayCHARTFileNames );
- // We shouldn't have been called to begin with if there were no CHARTs.
- ASSERT( arrayCHARTFileNames.size() != 0 );
- RString dir = out.GetSongDir();
- // Only need to use the first file, since there should only be 1
- return ReadFile( dir+arrayCHARTFileNames[0], &out, NULL );
- }
- bool CHARTLoader::LoadNoteDataFromSimfile( const RString & cachePath, Steps &out ) {
- // This is simple since the path is already given to us
- return ReadFile(cachePath, NULL, &out);
- }
Advertisement
Add Comment
Please, Sign In to add comment