2011-03-17 01:47:30 -04:00
|
|
|
#include "global.h"
|
|
|
|
|
#include "NotesLoaderBMS.h"
|
|
|
|
|
#include "NoteData.h"
|
|
|
|
|
#include "GameConstantsAndTypes.h"
|
|
|
|
|
#include "RageLog.h"
|
|
|
|
|
#include "GameManager.h"
|
2011-08-08 00:29:32 +07:00
|
|
|
#include "SongManager.h"
|
2011-03-17 01:47:30 -04:00
|
|
|
#include "RageFile.h"
|
|
|
|
|
#include "SongUtil.h"
|
|
|
|
|
#include "StepsUtil.h"
|
|
|
|
|
#include "Song.h"
|
|
|
|
|
#include "Steps.h"
|
|
|
|
|
#include "RageUtil_CharConversions.h"
|
|
|
|
|
#include "NoteTypes.h"
|
|
|
|
|
#include "NotesLoader.h"
|
|
|
|
|
#include "PrefsManager.h"
|
|
|
|
|
|
|
|
|
|
/* BMS encoding: tap-hold
|
|
|
|
|
* 4&8panel: Player1 Player2
|
|
|
|
|
* Left 11-51 21-61
|
|
|
|
|
* Down 13-53 23-63
|
|
|
|
|
* Up 15-55 25-65
|
|
|
|
|
* Right 16-56 26-66
|
|
|
|
|
*
|
|
|
|
|
* 6panel: Player1
|
|
|
|
|
* Left 11-51
|
|
|
|
|
* Left+Up 12-52
|
|
|
|
|
* Down 13-53
|
|
|
|
|
* Up 14-54
|
|
|
|
|
* Up+Right 15-55
|
|
|
|
|
* Right 16-56
|
|
|
|
|
*
|
|
|
|
|
* Notice that 15 and 25 have double meanings! What were they thinking???
|
|
|
|
|
* While reading in, use the 6 panel mapping. After reading in, detect if
|
|
|
|
|
* only 4 notes are used. If so, shift the Up+Right column back to the Up
|
|
|
|
|
* column
|
|
|
|
|
*
|
|
|
|
|
* BMSes are used for games besides dance and so we're borking up BMSes that are for popn/beat/etc.
|
|
|
|
|
*
|
|
|
|
|
* popn-nine: 11-15,22-25
|
|
|
|
|
* popn-five: 13-15,21-22
|
|
|
|
|
* beat-single5: 11-16
|
|
|
|
|
* beat-double5: 11-16,21-26
|
|
|
|
|
* beat-single7: 11-16,18-19
|
|
|
|
|
* beat-double7: 11-16,18-19,21-26,28-29
|
|
|
|
|
*
|
|
|
|
|
* So the magics for these are:
|
|
|
|
|
* popn-nine: nothing >5, with 12, 14, 22 and/or 24
|
|
|
|
|
* popn-five: nothing >5, with 14 and/or 22
|
|
|
|
|
* beat-*: can't tell difference between beat-single and dance-solo
|
|
|
|
|
* 18/19 marks beat-single7, 28/29 marks beat-double7
|
|
|
|
|
* beat-double uses 21-26.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
// Find the largest common substring at the start of both strings.
|
|
|
|
|
static RString FindLargestInitialSubstring( const RString &string1, const RString &string2 )
|
|
|
|
|
{
|
|
|
|
|
// First see if the whole first string matches an appropriately-sized
|
|
|
|
|
// substring of the second, then keep chopping off the last character of
|
|
|
|
|
// each until they match.
|
|
|
|
|
unsigned i;
|
|
|
|
|
for( i = 0; i < string1.size() && i < string2.size(); ++i )
|
|
|
|
|
if( string1[i] != string2[i] )
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
return string1.substr( 0, i );
|
|
|
|
|
}
|
|
|
|
|
|
2011-09-10 01:08:04 +07:00
|
|
|
static void SearchForDifficulty( RString sTag, Steps *pOut )
|
2011-03-17 01:47:30 -04:00
|
|
|
{
|
2011-09-10 01:08:04 +07:00
|
|
|
sTag.MakeLower();
|
2011-03-17 01:47:30 -04:00
|
|
|
|
2011-09-10 01:08:04 +07:00
|
|
|
// Only match "Light" in parentheses.
|
|
|
|
|
if( sTag.find( "(light" ) != sTag.npos )
|
2011-03-17 01:47:30 -04:00
|
|
|
{
|
2011-09-10 01:08:04 +07:00
|
|
|
pOut->SetDifficulty( Difficulty_Easy );
|
|
|
|
|
}
|
|
|
|
|
else if( sTag.find( "another" ) != sTag.npos )
|
|
|
|
|
{
|
|
|
|
|
pOut->SetDifficulty( Difficulty_Hard );
|
|
|
|
|
}
|
|
|
|
|
else if( sTag.find( "(solo)" ) != sTag.npos )
|
|
|
|
|
{
|
|
|
|
|
pOut->SetDescription( "Solo" );
|
|
|
|
|
pOut->SetDifficulty( Difficulty_Edit );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
LOG->Trace( "Tag \"%s\" is %s", sTag.c_str(), DifficultyToString(pOut->GetDifficulty()).c_str() );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void SlideDuplicateDifficulties( Song &p )
|
|
|
|
|
{
|
|
|
|
|
/* BMS files have to guess the Difficulty from the meter; this is inaccurate,
|
2011-09-10 02:05:12 -05:00
|
|
|
* and often leads to duplicates. Slide duplicate difficulties upwards.
|
|
|
|
|
* We only do this with BMS files, since a very common bug was having *all*
|
2011-09-10 01:08:04 +07:00
|
|
|
* difficulties slid upwards due to (for example) having two beginner steps.
|
|
|
|
|
* We do a second pass in Song::TidyUpData to eliminate any remaining duplicates
|
|
|
|
|
* after this. */
|
|
|
|
|
FOREACH_ENUM( StepsType,st )
|
|
|
|
|
{
|
|
|
|
|
FOREACH_ENUM( Difficulty, dc )
|
2011-03-17 01:47:30 -04:00
|
|
|
{
|
2011-09-10 01:08:04 +07:00
|
|
|
if( dc == Difficulty_Edit )
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
vector<Steps*> vSteps;
|
|
|
|
|
SongUtil::GetSteps( &p, vSteps, st, dc );
|
|
|
|
|
|
|
|
|
|
StepsUtil::SortNotesArrayByDifficulty( vSteps );
|
|
|
|
|
for( unsigned k=1; k<vSteps.size(); k++ )
|
|
|
|
|
{
|
|
|
|
|
Steps* pSteps = vSteps[k];
|
|
|
|
|
|
|
|
|
|
Difficulty dc2 = min( (Difficulty)(dc+1), Difficulty_Challenge );
|
|
|
|
|
pSteps->SetDifficulty( dc2 );
|
|
|
|
|
}
|
2011-03-17 01:47:30 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2011-09-10 01:08:04 +07:00
|
|
|
void BMSLoader::GetApplicableFiles( const RString &sPath, vector<RString> &out )
|
2011-03-17 01:47:30 -04:00
|
|
|
{
|
2011-09-10 01:08:04 +07:00
|
|
|
GetDirListing( sPath + RString("*.bms"), out );
|
|
|
|
|
GetDirListing( sPath + RString("*.bme"), out );
|
|
|
|
|
GetDirListing( sPath + RString("*.bml"), out );
|
|
|
|
|
}
|
2011-03-17 01:47:30 -04:00
|
|
|
|
2011-09-10 02:05:12 -05:00
|
|
|
/*===========================================================================*/
|
2011-03-17 01:47:30 -04:00
|
|
|
|
2011-09-10 01:08:04 +07:00
|
|
|
struct BMSObject
|
2011-03-17 01:47:30 -04:00
|
|
|
{
|
2011-09-10 01:08:04 +07:00
|
|
|
int channel;
|
|
|
|
|
int measure;
|
|
|
|
|
float position;
|
|
|
|
|
bool flag;
|
|
|
|
|
RString value;
|
|
|
|
|
bool operator<(const BMSObject &other) const
|
2011-03-17 01:47:30 -04:00
|
|
|
{
|
2011-09-10 01:08:04 +07:00
|
|
|
if( measure < other.measure ) return true;
|
|
|
|
|
if( measure > other.measure ) return false;
|
|
|
|
|
if( position < other.position ) return true;
|
|
|
|
|
if( position > other.position ) return false;
|
|
|
|
|
if( channel == 1 ) return false;
|
|
|
|
|
if( other.channel == 1 ) return true;
|
|
|
|
|
return channel < other.channel;
|
2011-03-17 01:47:30 -04:00
|
|
|
}
|
2011-09-10 01:08:04 +07:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
struct BMSMeasure
|
2011-03-17 01:47:30 -04:00
|
|
|
{
|
2011-09-10 01:08:04 +07:00
|
|
|
float size;
|
|
|
|
|
};
|
2011-03-17 01:47:30 -04:00
|
|
|
|
2011-09-10 01:08:04 +07:00
|
|
|
typedef map<RString, RString> BMSHeaders;
|
|
|
|
|
typedef map<int, BMSMeasure> BMSMeasures;
|
|
|
|
|
typedef vector<BMSObject> BMSObjects;
|
2011-03-17 01:47:30 -04:00
|
|
|
|
2011-09-10 01:08:04 +07:00
|
|
|
class BMSChart
|
2011-03-17 01:47:30 -04:00
|
|
|
{
|
|
|
|
|
|
2011-09-10 01:08:04 +07:00
|
|
|
public:
|
|
|
|
|
BMSChart();
|
|
|
|
|
bool Load( const RString &path );
|
|
|
|
|
bool GetHeader( const RString &header, RString &out );
|
|
|
|
|
RString path;
|
2011-03-17 01:47:30 -04:00
|
|
|
|
2011-09-10 01:08:04 +07:00
|
|
|
BMSObjects objects;
|
|
|
|
|
BMSHeaders headers;
|
|
|
|
|
BMSMeasures measures;
|
2011-03-17 01:47:30 -04:00
|
|
|
|
2011-09-10 01:08:04 +07:00
|
|
|
void TidyUpData();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
BMSChart::BMSChart()
|
|
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool BMSChart::GetHeader( const RString &header, RString &out )
|
|
|
|
|
{
|
|
|
|
|
if( headers.find(header) == headers.end() ) return false;
|
|
|
|
|
out = headers[header];
|
|
|
|
|
return true;
|
2011-03-17 01:47:30 -04:00
|
|
|
}
|
|
|
|
|
|
2011-09-10 01:08:04 +07:00
|
|
|
bool BMSChart::Load( const RString &chartPath )
|
2011-03-17 01:47:30 -04:00
|
|
|
{
|
2011-09-10 01:08:04 +07:00
|
|
|
path = chartPath;
|
2011-09-10 02:05:12 -05:00
|
|
|
|
2011-03-17 01:47:30 -04:00
|
|
|
RageFile file;
|
2011-09-10 01:08:04 +07:00
|
|
|
if( !file.Open(path) )
|
2011-03-17 01:47:30 -04:00
|
|
|
{
|
2011-09-10 01:08:04 +07:00
|
|
|
LOG->UserLog( "Song file", path, "couldn't be opened: %s", file.GetError().c_str() );
|
2011-03-17 01:47:30 -04:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
while( !file.AtEOF() )
|
|
|
|
|
{
|
|
|
|
|
RString line;
|
|
|
|
|
if( file.GetLine(line) == -1 )
|
|
|
|
|
{
|
2011-09-10 01:08:04 +07:00
|
|
|
LOG->UserLog( "Song file", path, "had a read error: %s", file.GetError().c_str() );
|
2011-03-17 01:47:30 -04:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
StripCrnl( line );
|
|
|
|
|
|
2011-09-10 01:08:04 +07:00
|
|
|
if( line.size() > 1 && line[0] == '#' )
|
|
|
|
|
{
|
|
|
|
|
if( line.size() >= 7 &&
|
|
|
|
|
( '0' <= line[1] && line[1] <= '9' ) &&
|
|
|
|
|
( '0' <= line[2] && line[2] <= '9' ) &&
|
|
|
|
|
( '0' <= line[3] && line[3] <= '9' ) &&
|
|
|
|
|
( '0' <= line[4] && line[4] <= '9' ) &&
|
|
|
|
|
( '0' <= line[5] && line[5] <= '9' ) &&
|
|
|
|
|
line[6] == ':' )
|
|
|
|
|
{
|
|
|
|
|
RString data = line.substr(7);
|
|
|
|
|
int measure = atoi( line.substr(1, 3).c_str() );
|
|
|
|
|
int channel = atoi( line.substr(4, 2).c_str() );
|
|
|
|
|
bool flag = false;
|
|
|
|
|
if( channel == 2 )
|
|
|
|
|
{
|
|
|
|
|
// special channel: time signature
|
2011-09-10 15:14:04 +07:00
|
|
|
BMSMeasure m = { StringToFloat(data) };
|
|
|
|
|
this->measures[measure] = m;
|
2011-09-10 01:08:04 +07:00
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
if( channel >= 51 )
|
|
|
|
|
{
|
|
|
|
|
channel -= 40;
|
|
|
|
|
flag = true;
|
|
|
|
|
}
|
|
|
|
|
int count = data.size() / 2;
|
|
|
|
|
for( int i = 0; i < count; i ++ )
|
|
|
|
|
{
|
|
|
|
|
RString value = data.substr( 2 * i, 2 );
|
|
|
|
|
if( value != "00" )
|
|
|
|
|
{
|
|
|
|
|
value.MakeLower();
|
|
|
|
|
BMSObject o = { channel, measure, (float)i / count, flag, value };
|
|
|
|
|
objects.push_back( o );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
size_t space = line.find(' ');
|
|
|
|
|
RString name = line.substr( 0, space );
|
|
|
|
|
RString value = "";
|
|
|
|
|
if( space != line.npos )
|
|
|
|
|
value = line.substr( space+1 );
|
|
|
|
|
name.MakeLower();
|
|
|
|
|
headers[name] = value;
|
|
|
|
|
}
|
|
|
|
|
}
|
2011-03-17 01:47:30 -04:00
|
|
|
}
|
|
|
|
|
|
2011-09-10 01:08:04 +07:00
|
|
|
TidyUpData();
|
|
|
|
|
|
2011-03-17 01:47:30 -04:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2011-09-10 01:08:04 +07:00
|
|
|
void BMSChart::TidyUpData()
|
2011-03-17 01:47:30 -04:00
|
|
|
{
|
2011-09-10 01:08:04 +07:00
|
|
|
sort( objects.begin(), objects.end() );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class BMSSong {
|
|
|
|
|
|
|
|
|
|
map<RString, int> mapKeysoundToIndex;
|
|
|
|
|
Song *out;
|
|
|
|
|
|
|
|
|
|
public:
|
|
|
|
|
BMSSong( Song *song );
|
2011-10-09 18:33:45 +07:00
|
|
|
int AllocateKeysound( RString filename, RString path );
|
2011-09-10 01:08:04 +07:00
|
|
|
Song *GetSong();
|
2011-03-17 01:47:30 -04:00
|
|
|
};
|
|
|
|
|
|
2011-09-10 01:08:04 +07:00
|
|
|
BMSSong::BMSSong( Song *song )
|
2011-03-17 01:47:30 -04:00
|
|
|
{
|
2011-09-10 01:08:04 +07:00
|
|
|
out = song;
|
|
|
|
|
|
|
|
|
|
// import existing keysounds from song
|
|
|
|
|
for( unsigned i = 0; i < out->m_vsKeysoundFile.size(); i ++ )
|
2011-03-17 01:47:30 -04:00
|
|
|
{
|
2011-09-10 01:08:04 +07:00
|
|
|
mapKeysoundToIndex[out->m_vsKeysoundFile[i]] = i;
|
2011-03-17 01:47:30 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2011-09-10 01:08:04 +07:00
|
|
|
Song *BMSSong::GetSong()
|
2011-03-17 01:47:30 -04:00
|
|
|
{
|
2011-09-10 01:08:04 +07:00
|
|
|
return out;
|
2011-03-17 01:47:30 -04:00
|
|
|
}
|
|
|
|
|
|
2011-10-09 18:33:45 +07:00
|
|
|
int BMSSong::AllocateKeysound( RString filename, RString path )
|
2011-03-17 01:47:30 -04:00
|
|
|
{
|
2011-09-10 01:08:04 +07:00
|
|
|
if( mapKeysoundToIndex.find( filename ) != mapKeysoundToIndex.end() )
|
2011-05-12 16:44:24 +07:00
|
|
|
{
|
2011-09-10 01:08:04 +07:00
|
|
|
return mapKeysoundToIndex[filename];
|
2011-05-12 16:44:24 +07:00
|
|
|
}
|
|
|
|
|
|
2011-09-10 01:08:04 +07:00
|
|
|
// try to normalize the filename first!
|
2011-09-10 02:05:12 -05:00
|
|
|
|
2011-05-12 16:44:24 +07:00
|
|
|
// FIXME: garbled song names seem to crash the app.
|
|
|
|
|
// this might not be the best place to put this code.
|
2011-09-10 01:08:04 +07:00
|
|
|
if( !utf8_is_valid(filename) )
|
2011-10-09 18:33:45 +07:00
|
|
|
return -1;
|
2011-09-10 01:08:04 +07:00
|
|
|
|
2011-05-12 16:44:24 +07:00
|
|
|
/* Due to bugs in some programs, many BMS files have a "WAV" extension
|
|
|
|
|
* on files in the BMS for files that actually have some other extension.
|
|
|
|
|
* Do a search. Don't do a wildcard search; if sData is "song.wav",
|
|
|
|
|
* we might also have "song.png", which we shouldn't match. */
|
2011-09-10 01:08:04 +07:00
|
|
|
RString normalizedFilename = filename;
|
|
|
|
|
RString dir = out->GetSongDir();
|
|
|
|
|
|
2011-07-17 16:06:40 -04:00
|
|
|
if (dir.empty())
|
2011-09-10 01:08:04 +07:00
|
|
|
dir = Dirname(path);
|
2011-09-10 02:05:12 -05:00
|
|
|
|
2011-09-10 01:08:04 +07:00
|
|
|
if( !IsAFile(dir + normalizedFilename) )
|
2011-05-12 16:44:24 +07:00
|
|
|
{
|
|
|
|
|
const char *exts[] = { "oga", "ogg", "wav", "mp3", NULL }; // XXX: stop duplicating these everywhere
|
|
|
|
|
for( unsigned i = 0; exts[i] != NULL; ++i )
|
|
|
|
|
{
|
2011-09-10 01:08:04 +07:00
|
|
|
RString fn = SetExtension( normalizedFilename, exts[i] );
|
|
|
|
|
if( IsAFile(dir + fn) )
|
2011-05-12 16:44:24 +07:00
|
|
|
{
|
2011-09-10 01:08:04 +07:00
|
|
|
normalizedFilename = fn;
|
2011-05-12 16:44:24 +07:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2011-09-10 02:05:12 -05:00
|
|
|
|
2011-09-10 01:08:04 +07:00
|
|
|
if( !IsAFile(dir + normalizedFilename) )
|
2011-05-12 16:44:24 +07:00
|
|
|
{
|
2011-09-10 01:08:04 +07:00
|
|
|
mapKeysoundToIndex[filename] = -1;
|
|
|
|
|
LOG->UserLog( "Song file", dir, "references key \"%s\" that can't be found", normalizedFilename.c_str() );
|
2011-10-09 18:33:45 +07:00
|
|
|
return -1;
|
2011-05-12 16:44:24 +07:00
|
|
|
}
|
2011-09-10 02:05:12 -05:00
|
|
|
|
2011-09-10 01:08:04 +07:00
|
|
|
if( mapKeysoundToIndex.find( normalizedFilename ) != mapKeysoundToIndex.end() )
|
2011-05-12 16:44:24 +07:00
|
|
|
{
|
2011-09-10 01:08:04 +07:00
|
|
|
mapKeysoundToIndex[filename] = mapKeysoundToIndex[normalizedFilename];
|
|
|
|
|
return mapKeysoundToIndex[normalizedFilename];
|
2011-05-12 16:44:24 +07:00
|
|
|
}
|
|
|
|
|
|
2011-09-10 01:08:04 +07:00
|
|
|
unsigned index = out->m_vsKeysoundFile.size();
|
|
|
|
|
out->m_vsKeysoundFile.push_back( normalizedFilename );
|
|
|
|
|
mapKeysoundToIndex[filename] = index;
|
|
|
|
|
mapKeysoundToIndex[normalizedFilename] = index;
|
|
|
|
|
return index;
|
2011-05-12 16:44:24 +07:00
|
|
|
}
|
|
|
|
|
|
2011-09-10 01:08:04 +07:00
|
|
|
struct BMSChartInfo {
|
|
|
|
|
RString title;
|
|
|
|
|
RString artist;
|
|
|
|
|
RString genre;
|
|
|
|
|
|
|
|
|
|
RString backgroundFile;
|
|
|
|
|
RString musicFile;
|
|
|
|
|
};
|
2011-05-12 16:44:24 +07:00
|
|
|
|
2011-09-10 01:08:04 +07:00
|
|
|
class BMSChartReader {
|
|
|
|
|
BMSChart *in;
|
|
|
|
|
Steps *out;
|
|
|
|
|
BMSSong *song;
|
2011-05-12 16:44:24 +07:00
|
|
|
|
2011-09-10 01:08:04 +07:00
|
|
|
void ReadHeaders();
|
|
|
|
|
void CalculateStepsType();
|
|
|
|
|
bool ReadNoteData();
|
2011-03-17 01:47:30 -04:00
|
|
|
|
2011-09-10 01:08:04 +07:00
|
|
|
StepsType DetermineStepsType();
|
2011-03-17 01:47:30 -04:00
|
|
|
|
2011-09-10 01:08:04 +07:00
|
|
|
int lntype;
|
|
|
|
|
RString lnobj;
|
2011-03-17 01:47:30 -04:00
|
|
|
|
2011-09-10 01:08:04 +07:00
|
|
|
int nonEmptyTracksCount;
|
|
|
|
|
map<int, bool> nonEmptyTracks;
|
2011-03-17 01:47:30 -04:00
|
|
|
|
2011-09-10 01:08:04 +07:00
|
|
|
int GetKeysound( const BMSObject &obj );
|
|
|
|
|
|
|
|
|
|
map<RString, int> mapValueToKeysoundIndex;
|
|
|
|
|
|
|
|
|
|
public:
|
|
|
|
|
BMSChartReader( BMSChart *chart, Steps *steps, BMSSong *song );
|
|
|
|
|
bool Read();
|
|
|
|
|
|
|
|
|
|
Steps *GetSteps();
|
|
|
|
|
|
|
|
|
|
BMSChartInfo info;
|
|
|
|
|
int player;
|
|
|
|
|
float initialBPM;
|
2011-05-12 16:44:24 +07:00
|
|
|
|
2011-09-10 01:08:04 +07:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
BMSChartReader::BMSChartReader( BMSChart *chart, Steps *steps, BMSSong *bmsSong )
|
|
|
|
|
{
|
|
|
|
|
this->in = chart;
|
|
|
|
|
this->out = steps;
|
|
|
|
|
this->song = bmsSong;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool BMSChartReader::Read()
|
|
|
|
|
{
|
|
|
|
|
ReadHeaders();
|
|
|
|
|
CalculateStepsType();
|
|
|
|
|
if( !ReadNoteData() ) return false;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void BMSChartReader::ReadHeaders()
|
|
|
|
|
{
|
|
|
|
|
lntype = 1;
|
|
|
|
|
player = 1;
|
|
|
|
|
for( BMSHeaders::iterator it = in->headers.begin(); it != in->headers.end(); it ++ )
|
|
|
|
|
{
|
|
|
|
|
if( it->first == "#player" )
|
2011-05-12 16:44:24 +07:00
|
|
|
{
|
2011-09-10 01:08:04 +07:00
|
|
|
player = atoi(it->second.c_str());
|
2011-05-12 16:44:24 +07:00
|
|
|
}
|
2011-09-10 01:08:04 +07:00
|
|
|
else if( it->first == "#title" )
|
|
|
|
|
{
|
|
|
|
|
info.title = it->second;
|
|
|
|
|
}
|
|
|
|
|
else if( it->first == "#artist" )
|
|
|
|
|
{
|
|
|
|
|
info.artist = it->second;
|
|
|
|
|
}
|
|
|
|
|
else if( it->first == "#genre" )
|
2011-05-12 16:44:24 +07:00
|
|
|
{
|
2011-09-10 01:08:04 +07:00
|
|
|
info.genre = it->second;
|
|
|
|
|
}
|
|
|
|
|
else if( it->first == "#backbmp" )
|
|
|
|
|
{
|
|
|
|
|
info.backgroundFile = it->second;
|
|
|
|
|
}
|
|
|
|
|
else if( it->first == "#wav" )
|
|
|
|
|
{
|
|
|
|
|
info.musicFile = it->second;
|
|
|
|
|
}
|
|
|
|
|
else if( it->first == "#bpm" )
|
|
|
|
|
{
|
|
|
|
|
initialBPM = StringToFloat(it->second);
|
|
|
|
|
}
|
|
|
|
|
else if( it->first == "#lntype" )
|
|
|
|
|
{
|
|
|
|
|
int myLntype = atoi(it->second.c_str());
|
|
|
|
|
if( myLntype == 1 )
|
|
|
|
|
{
|
|
|
|
|
lntype = myLntype;
|
|
|
|
|
// XXX: we only support #LNTYPE 1 for now.
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else if( it->first == "#lnobj" )
|
|
|
|
|
{
|
|
|
|
|
lnobj = it->second;
|
|
|
|
|
// TODO: support lnobj
|
|
|
|
|
}
|
|
|
|
|
else if( it->first == "#playlevel" )
|
|
|
|
|
{
|
|
|
|
|
out->SetMeter( StringToInt(it->second) );
|
2011-05-12 16:44:24 +07:00
|
|
|
}
|
|
|
|
|
}
|
2011-09-10 01:08:04 +07:00
|
|
|
}
|
2011-05-12 16:44:24 +07:00
|
|
|
|
2011-09-10 01:08:04 +07:00
|
|
|
void BMSChartReader::CalculateStepsType()
|
|
|
|
|
{
|
|
|
|
|
for( unsigned i = 0; i < in->objects.size(); i ++ )
|
2011-05-12 16:44:24 +07:00
|
|
|
{
|
2011-09-10 01:08:04 +07:00
|
|
|
BMSObject &obj = in->objects[i];
|
|
|
|
|
int channel = obj.channel;
|
|
|
|
|
if( (11 <= channel && channel <= 19) || (21 <= channel && channel <= 29) )
|
2011-05-12 16:44:24 +07:00
|
|
|
{
|
2011-09-10 01:08:04 +07:00
|
|
|
nonEmptyTracks[channel] = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
2011-05-12 16:44:24 +07:00
|
|
|
|
2011-09-10 01:08:04 +07:00
|
|
|
nonEmptyTracksCount = nonEmptyTracks.size();
|
|
|
|
|
out->m_StepsType = DetermineStepsType();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
enum BmsRawChannel
|
|
|
|
|
{
|
|
|
|
|
BMS_RAW_P1_KEY1 = 11,
|
|
|
|
|
BMS_RAW_P1_KEY2 = 12,
|
|
|
|
|
BMS_RAW_P1_KEY3 = 13,
|
|
|
|
|
BMS_RAW_P1_KEY4 = 14,
|
|
|
|
|
BMS_RAW_P1_KEY5 = 15,
|
|
|
|
|
BMS_RAW_P1_TURN = 16,
|
|
|
|
|
BMS_RAW_P1_KEY6 = 18,
|
|
|
|
|
BMS_RAW_P1_KEY7 = 19,
|
|
|
|
|
BMS_RAW_P2_KEY1 = 21,
|
|
|
|
|
BMS_RAW_P2_KEY2 = 22,
|
|
|
|
|
BMS_RAW_P2_KEY3 = 23,
|
|
|
|
|
BMS_RAW_P2_KEY4 = 24,
|
|
|
|
|
BMS_RAW_P2_KEY5 = 25,
|
|
|
|
|
BMS_RAW_P2_TURN = 26,
|
|
|
|
|
BMS_RAW_P2_KEY6 = 28,
|
|
|
|
|
BMS_RAW_P2_KEY7 = 29
|
|
|
|
|
};
|
2011-05-12 16:44:24 +07:00
|
|
|
|
2011-09-10 01:08:04 +07:00
|
|
|
StepsType BMSChartReader::DetermineStepsType()
|
|
|
|
|
{
|
|
|
|
|
switch( player )
|
|
|
|
|
{
|
|
|
|
|
case 1: // "1 player"
|
|
|
|
|
switch( nonEmptyTracksCount )
|
2011-05-12 16:44:24 +07:00
|
|
|
{
|
2011-09-10 01:08:04 +07:00
|
|
|
case 4: return StepsType_dance_single;
|
|
|
|
|
case 5:
|
|
|
|
|
if( nonEmptyTracks.find(BMS_RAW_P2_KEY2) != nonEmptyTracks.end() ) 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 7:
|
|
|
|
|
case 8: return StepsType_beat_single7;
|
|
|
|
|
case 9: return StepsType_popn_nine;
|
|
|
|
|
default: return StepsType_Invalid;
|
2011-05-12 16:44:24 +07:00
|
|
|
}
|
2011-09-10 01:08:04 +07:00
|
|
|
case 2: // couple/battle
|
|
|
|
|
return StepsType_dance_couple;
|
|
|
|
|
case 3: // double
|
|
|
|
|
switch( nonEmptyTracksCount )
|
2011-05-12 16:44:24 +07:00
|
|
|
{
|
2011-09-10 01:08:04 +07:00
|
|
|
case 8: return StepsType_beat_single7;
|
|
|
|
|
case 12: return StepsType_beat_double5;
|
|
|
|
|
case 16: return StepsType_beat_double7;
|
|
|
|
|
case 5: return StepsType_popn_five;
|
|
|
|
|
case 9: return StepsType_popn_nine;
|
|
|
|
|
default: return StepsType_Invalid;
|
2011-05-12 16:44:24 +07:00
|
|
|
}
|
2011-09-10 01:08:04 +07:00
|
|
|
default:
|
|
|
|
|
LOG->UserLog( "Song file", in->path, "has an invalid #PLAYER value %d.", player );
|
|
|
|
|
return StepsType_Invalid;
|
2011-03-17 01:47:30 -04:00
|
|
|
}
|
2011-09-10 01:08:04 +07:00
|
|
|
}
|
2011-03-17 01:47:30 -04:00
|
|
|
|
2011-09-10 01:08:04 +07:00
|
|
|
int BMSChartReader::GetKeysound( const BMSObject &obj )
|
|
|
|
|
{
|
|
|
|
|
map<RString, int>::iterator it = mapValueToKeysoundIndex.find(obj.value);
|
|
|
|
|
if( it == mapValueToKeysoundIndex.end() )
|
2011-03-17 01:47:30 -04:00
|
|
|
{
|
2011-09-10 01:08:04 +07:00
|
|
|
int index = -1;
|
|
|
|
|
BMSHeaders::iterator iu = in->headers.find("#wav" + obj.value);
|
|
|
|
|
if( iu != in->headers.end() )
|
2011-03-17 01:47:30 -04:00
|
|
|
{
|
2011-09-10 01:08:04 +07:00
|
|
|
index = song->AllocateKeysound(iu->second, in->path);
|
2011-03-17 01:47:30 -04:00
|
|
|
}
|
2011-09-10 01:08:04 +07:00
|
|
|
mapValueToKeysoundIndex[obj.value] = index;
|
|
|
|
|
return index;
|
2011-03-17 01:47:30 -04:00
|
|
|
}
|
2011-09-10 01:08:04 +07:00
|
|
|
else
|
2011-03-17 01:47:30 -04:00
|
|
|
{
|
2011-09-10 01:08:04 +07:00
|
|
|
return it->second;
|
2011-03-17 01:47:30 -04:00
|
|
|
}
|
2011-09-10 01:08:04 +07:00
|
|
|
}
|
|
|
|
|
|
2011-09-10 17:11:46 +07:00
|
|
|
struct BMSAutoKeysound {
|
|
|
|
|
int row;
|
|
|
|
|
int index;
|
|
|
|
|
};
|
|
|
|
|
|
2011-09-10 01:08:04 +07:00
|
|
|
bool BMSChartReader::ReadNoteData()
|
|
|
|
|
{
|
|
|
|
|
if( out->m_StepsType == StepsType_Invalid )
|
2011-03-18 01:19:06 +07:00
|
|
|
{
|
2011-09-10 01:08:04 +07:00
|
|
|
LOG->UserLog( "Song file", in->path, "has an unknown steps type" );
|
|
|
|
|
return false;
|
2011-03-18 01:19:06 +07:00
|
|
|
}
|
2011-09-10 02:05:12 -05:00
|
|
|
|
2011-09-10 01:08:04 +07:00
|
|
|
float currentBPM;
|
|
|
|
|
int tracks = GAMEMAN->GetStepsTypeInfo( out->m_StepsType ).iNumTracks;
|
2011-09-10 02:05:12 -05:00
|
|
|
|
2011-09-10 01:08:04 +07:00
|
|
|
NoteData nd;
|
|
|
|
|
TimingData td;
|
2011-03-17 01:47:30 -04:00
|
|
|
|
2011-09-10 01:08:04 +07:00
|
|
|
nd.SetNumTracks( tracks );
|
|
|
|
|
td.SetBPMAtRow( 0, currentBPM = initialBPM );
|
2011-03-17 01:47:30 -04:00
|
|
|
|
2011-09-10 01:08:04 +07:00
|
|
|
// set up note transformation vector.
|
|
|
|
|
int *transform = new int[tracks];
|
|
|
|
|
int *holdStart = new int[tracks];
|
2011-03-17 01:47:30 -04:00
|
|
|
|
2011-09-10 01:08:04 +07:00
|
|
|
for( int i = 0; i < tracks; i ++ ) holdStart[i] = -1;
|
2011-03-17 01:47:30 -04:00
|
|
|
|
2011-09-10 01:08:04 +07:00
|
|
|
switch( out->m_StepsType )
|
2011-03-17 01:47:30 -04:00
|
|
|
{
|
|
|
|
|
case StepsType_dance_single:
|
2011-09-10 01:08:04 +07:00
|
|
|
transform[0] = BMS_RAW_P1_KEY1;
|
|
|
|
|
transform[1] = BMS_RAW_P1_KEY3;
|
|
|
|
|
transform[2] = BMS_RAW_P1_KEY5;
|
|
|
|
|
transform[3] = BMS_RAW_P1_TURN;
|
2011-03-17 01:47:30 -04:00
|
|
|
break;
|
|
|
|
|
case StepsType_dance_double:
|
|
|
|
|
case StepsType_dance_couple:
|
2011-09-10 01:08:04 +07:00
|
|
|
transform[0] = BMS_RAW_P1_KEY1;
|
|
|
|
|
transform[1] = BMS_RAW_P1_KEY3;
|
|
|
|
|
transform[2] = BMS_RAW_P1_KEY5;
|
|
|
|
|
transform[3] = BMS_RAW_P1_TURN;
|
|
|
|
|
transform[4] = BMS_RAW_P2_KEY1;
|
|
|
|
|
transform[5] = BMS_RAW_P2_KEY3;
|
|
|
|
|
transform[6] = BMS_RAW_P2_KEY5;
|
|
|
|
|
transform[7] = BMS_RAW_P2_TURN;
|
2011-03-17 01:47:30 -04:00
|
|
|
break;
|
|
|
|
|
case StepsType_dance_solo:
|
|
|
|
|
case StepsType_beat_single5:
|
|
|
|
|
// Hey! Why are these exactly the same? :-)
|
2011-09-10 01:08:04 +07:00
|
|
|
transform[0] = BMS_RAW_P1_KEY1;
|
|
|
|
|
transform[1] = BMS_RAW_P1_KEY2;
|
|
|
|
|
transform[2] = BMS_RAW_P1_KEY3;
|
|
|
|
|
transform[3] = BMS_RAW_P1_KEY4;
|
|
|
|
|
transform[4] = BMS_RAW_P1_KEY5;
|
|
|
|
|
transform[5] = BMS_RAW_P1_TURN;
|
2011-03-17 01:47:30 -04:00
|
|
|
break;
|
|
|
|
|
case StepsType_popn_five:
|
2011-09-10 01:08:04 +07:00
|
|
|
transform[0] = BMS_RAW_P1_KEY3;
|
|
|
|
|
transform[1] = BMS_RAW_P1_KEY4;
|
|
|
|
|
transform[2] = BMS_RAW_P1_KEY5;
|
2011-03-17 01:47:30 -04:00
|
|
|
// fix these columns!
|
2011-09-10 01:08:04 +07:00
|
|
|
transform[3] = BMS_RAW_P2_KEY2;
|
|
|
|
|
transform[4] = BMS_RAW_P2_KEY3;
|
2011-03-17 01:47:30 -04:00
|
|
|
break;
|
|
|
|
|
case StepsType_popn_nine:
|
2011-09-10 01:08:04 +07:00
|
|
|
transform[0] = BMS_RAW_P1_KEY1; // lwhite
|
|
|
|
|
transform[1] = BMS_RAW_P1_KEY2; // lyellow
|
|
|
|
|
transform[2] = BMS_RAW_P1_KEY3; // lgreen
|
|
|
|
|
transform[3] = BMS_RAW_P1_KEY4; // lblue
|
|
|
|
|
transform[4] = BMS_RAW_P1_KEY5; // red
|
2011-03-17 01:47:30 -04:00
|
|
|
// fix these columns!
|
2011-09-10 01:08:04 +07:00
|
|
|
transform[5] = BMS_RAW_P2_KEY2; // rblue
|
|
|
|
|
transform[6] = BMS_RAW_P2_KEY3; // rgreen
|
|
|
|
|
transform[7] = BMS_RAW_P2_KEY4; // ryellow
|
|
|
|
|
transform[8] = BMS_RAW_P2_KEY5; // rwhite
|
2011-03-17 01:47:30 -04:00
|
|
|
break;
|
|
|
|
|
case StepsType_beat_double5:
|
2011-09-10 01:08:04 +07:00
|
|
|
transform[0] = BMS_RAW_P1_KEY1;
|
|
|
|
|
transform[1] = BMS_RAW_P1_KEY2;
|
|
|
|
|
transform[2] = BMS_RAW_P1_KEY3;
|
|
|
|
|
transform[3] = BMS_RAW_P1_KEY4;
|
|
|
|
|
transform[4] = BMS_RAW_P1_KEY5;
|
|
|
|
|
transform[5] = BMS_RAW_P1_TURN;
|
|
|
|
|
transform[6] = BMS_RAW_P2_KEY1;
|
|
|
|
|
transform[7] = BMS_RAW_P2_KEY2;
|
|
|
|
|
transform[8] = BMS_RAW_P2_KEY3;
|
|
|
|
|
transform[9] = BMS_RAW_P2_KEY4;
|
|
|
|
|
transform[10] = BMS_RAW_P2_KEY5;
|
|
|
|
|
transform[11] = BMS_RAW_P2_TURN;
|
2011-03-17 01:47:30 -04:00
|
|
|
break;
|
|
|
|
|
case StepsType_beat_single7:
|
2011-09-10 01:08:04 +07:00
|
|
|
if( nonEmptyTracks.find(BMS_RAW_P1_KEY7) == nonEmptyTracks.end()
|
|
|
|
|
&& nonEmptyTracks.find(BMS_RAW_P1_TURN) != nonEmptyTracks.end() )
|
2011-03-18 01:19:06 +07:00
|
|
|
{
|
|
|
|
|
/* special case for o2mania style charts:
|
|
|
|
|
* the turntable is used for first key while the real 7th key is not used. */
|
2011-09-10 01:08:04 +07:00
|
|
|
transform[0] = BMS_RAW_P1_TURN;
|
|
|
|
|
transform[1] = BMS_RAW_P1_KEY1;
|
|
|
|
|
transform[2] = BMS_RAW_P1_KEY2;
|
|
|
|
|
transform[3] = BMS_RAW_P1_KEY3;
|
|
|
|
|
transform[4] = BMS_RAW_P1_KEY4;
|
|
|
|
|
transform[5] = BMS_RAW_P1_KEY5;
|
|
|
|
|
transform[6] = BMS_RAW_P1_KEY6;
|
|
|
|
|
transform[7] = BMS_RAW_P1_KEY7;
|
2011-03-18 01:19:06 +07:00
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
2011-09-10 01:08:04 +07:00
|
|
|
transform[0] = BMS_RAW_P1_KEY1;
|
|
|
|
|
transform[1] = BMS_RAW_P1_KEY2;
|
|
|
|
|
transform[2] = BMS_RAW_P1_KEY3;
|
|
|
|
|
transform[3] = BMS_RAW_P1_KEY4;
|
|
|
|
|
transform[4] = BMS_RAW_P1_KEY5;
|
|
|
|
|
transform[5] = BMS_RAW_P1_KEY6;
|
|
|
|
|
transform[6] = BMS_RAW_P1_KEY7;
|
|
|
|
|
transform[7] = BMS_RAW_P1_TURN;
|
2011-03-18 01:19:06 +07:00
|
|
|
}
|
2011-03-17 01:47:30 -04:00
|
|
|
break;
|
|
|
|
|
case StepsType_beat_double7:
|
2011-09-10 01:08:04 +07:00
|
|
|
transform[0] = BMS_RAW_P1_KEY1;
|
|
|
|
|
transform[1] = BMS_RAW_P1_KEY2;
|
|
|
|
|
transform[2] = BMS_RAW_P1_KEY3;
|
|
|
|
|
transform[3] = BMS_RAW_P1_KEY4;
|
|
|
|
|
transform[4] = BMS_RAW_P1_KEY5;
|
|
|
|
|
transform[5] = BMS_RAW_P1_KEY6;
|
|
|
|
|
transform[6] = BMS_RAW_P1_KEY7;
|
|
|
|
|
transform[7] = BMS_RAW_P1_TURN;
|
|
|
|
|
transform[8] = BMS_RAW_P2_KEY1;
|
|
|
|
|
transform[9] = BMS_RAW_P2_KEY2;
|
|
|
|
|
transform[10] = BMS_RAW_P2_KEY3;
|
|
|
|
|
transform[11] = BMS_RAW_P2_KEY4;
|
|
|
|
|
transform[12] = BMS_RAW_P2_KEY5;
|
|
|
|
|
transform[13] = BMS_RAW_P2_KEY6;
|
|
|
|
|
transform[14] = BMS_RAW_P2_KEY7;
|
|
|
|
|
transform[15] = BMS_RAW_P2_TURN;
|
2011-03-17 01:47:30 -04:00
|
|
|
break;
|
|
|
|
|
default:
|
2011-09-10 01:08:04 +07:00
|
|
|
ASSERT_M(0, ssprintf("Invalid StepsType when parsing BMS file %s!", in->path.c_str()));
|
2011-03-17 01:47:30 -04:00
|
|
|
}
|
|
|
|
|
|
2011-09-10 01:08:04 +07:00
|
|
|
int reverseTransform[30];
|
|
|
|
|
for( int i = 0; i < 30; i ++ ) reverseTransform[i] = -1;
|
|
|
|
|
for( int i = 0; i < tracks; i ++ ) reverseTransform[transform[i]] = i;
|
|
|
|
|
|
|
|
|
|
int trackMeasure = -1;
|
|
|
|
|
float measureStartBeat = 0.0f;
|
|
|
|
|
float measureSize = 0.0f;
|
|
|
|
|
float adjustedMeasureSize = 0.0f;
|
|
|
|
|
float measureAdjust = 1.0f;
|
|
|
|
|
int firstNoteMeasure = 0;
|
2011-09-10 02:05:12 -05:00
|
|
|
|
2011-09-10 01:08:04 +07:00
|
|
|
for( unsigned i = 0; i < in->objects.size(); i ++ )
|
2011-03-17 01:47:30 -04:00
|
|
|
{
|
2011-09-10 01:08:04 +07:00
|
|
|
BMSObject &obj = in->objects[i];
|
|
|
|
|
int channel = obj.channel;
|
|
|
|
|
firstNoteMeasure = obj.measure;
|
|
|
|
|
if( channel == 3 || channel == 8 || channel == 9 || channel == 1 || (11 <= channel && channel <= 19) || (21 <= channel && channel <= 29) )
|
|
|
|
|
{
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
2011-09-10 17:11:46 +07:00
|
|
|
|
|
|
|
|
vector<BMSAutoKeysound> autos;
|
2011-09-10 02:05:12 -05:00
|
|
|
|
2011-09-10 01:08:04 +07:00
|
|
|
for( unsigned i = 0; i < in->objects.size(); i ++ )
|
|
|
|
|
{
|
|
|
|
|
BMSObject &obj = in->objects[i];
|
|
|
|
|
while( trackMeasure < obj.measure )
|
|
|
|
|
{
|
|
|
|
|
trackMeasure ++;
|
|
|
|
|
measureStartBeat += adjustedMeasureSize;
|
|
|
|
|
measureSize = 4.0f;
|
|
|
|
|
BMSMeasures::iterator it = in->measures.find(trackMeasure);
|
|
|
|
|
if( it != in->measures.end() ) measureSize = it->second.size * 4.0f;
|
|
|
|
|
adjustedMeasureSize = measureSize;
|
|
|
|
|
if( trackMeasure < firstNoteMeasure ) adjustedMeasureSize = measureSize = 4.0f;
|
2011-09-10 02:05:12 -05:00
|
|
|
|
2011-09-10 01:08:04 +07:00
|
|
|
// measure size adjustment
|
|
|
|
|
// XXX: need more testing / fine-tuning!
|
|
|
|
|
int sixteenths = lrintf(measureSize * 4.0f);
|
|
|
|
|
if( sixteenths > 1 ) adjustedMeasureSize = (float)sixteenths / 4.0f;
|
|
|
|
|
measureAdjust = adjustedMeasureSize / measureSize;
|
|
|
|
|
td.SetBPMAtRow( BeatToNoteRow(measureStartBeat), measureAdjust * currentBPM );
|
|
|
|
|
// end measure size adjustment
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int row = BeatToNoteRow( measureStartBeat + adjustedMeasureSize * obj.position );
|
|
|
|
|
int channel = obj.channel;
|
|
|
|
|
bool hold = obj.flag;
|
|
|
|
|
|
|
|
|
|
if( channel == 3 ) // bpm change
|
2011-03-17 01:47:30 -04:00
|
|
|
{
|
2011-09-10 01:08:04 +07:00
|
|
|
int bpm;
|
|
|
|
|
if( sscanf(obj.value, "%x", &bpm) == 1 )
|
2011-03-17 01:47:30 -04:00
|
|
|
{
|
2011-09-10 01:08:04 +07:00
|
|
|
if( bpm > 0 ) td.SetBPMAtRow( row, measureAdjust * (currentBPM = bpm) );
|
2011-03-17 01:47:30 -04:00
|
|
|
}
|
2011-09-10 01:08:04 +07:00
|
|
|
}
|
|
|
|
|
else if( channel == 8 ) // bpm change (extended)
|
|
|
|
|
{
|
|
|
|
|
RString search = ssprintf( "#bpm%s", obj.value.c_str() );
|
|
|
|
|
BMSHeaders::iterator it = in->headers.find( search );
|
|
|
|
|
if( it != in->headers.end() )
|
2011-03-17 01:47:30 -04:00
|
|
|
{
|
2011-09-10 01:08:04 +07:00
|
|
|
td.SetBPMAtRow( row, measureAdjust * (currentBPM = StringToFloat(it->second)) );
|
2011-03-17 01:47:30 -04:00
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
2011-09-10 01:08:04 +07:00
|
|
|
LOG->UserLog( "Song file", in->path.c_str(), "has tag \"%s\" which cannot be found.", search.c_str() );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else if( channel == 9 ) // stops
|
|
|
|
|
{
|
|
|
|
|
RString search = ssprintf( "#stop%s", obj.value.c_str() );
|
|
|
|
|
BMSHeaders::iterator it = in->headers.find( search );
|
|
|
|
|
if( it != in->headers.end() )
|
|
|
|
|
{
|
|
|
|
|
td.SetStopAtRow( row, (StringToFloat(it->second) / 48.0f) * (currentBPM / 60.0f) );
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
LOG->UserLog( "Song file", in->path.c_str(), "has tag \"%s\" which cannot be found.", search.c_str() );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else if( channel < 30 && reverseTransform[channel] != -1 ) // player notes!
|
|
|
|
|
{
|
|
|
|
|
int track = reverseTransform[channel];
|
|
|
|
|
if( holdStart[track] != -1 )
|
|
|
|
|
{
|
|
|
|
|
// this object is the end of the hold note.
|
|
|
|
|
TapNote tn = nd.GetTapNote(track, holdStart[track]);
|
|
|
|
|
tn.type = TapNote::hold_head;
|
|
|
|
|
tn.subType = TapNote::hold_head_hold;
|
|
|
|
|
nd.AddHoldNote( track, holdStart[track], row, tn );
|
|
|
|
|
holdStart[track] = -1;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
TapNote tn = TAP_ORIGINAL_TAP;
|
|
|
|
|
tn.iKeysoundIndex = GetKeysound(obj);
|
|
|
|
|
nd.SetTapNote( track, row, tn );
|
|
|
|
|
if( hold ) holdStart[track] = row;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else if( channel == 1 || (11 <= channel && channel <= 19) || (21 <= channel && channel <= 29) ) // auto-keysound and other notes
|
|
|
|
|
{
|
2011-09-10 17:11:46 +07:00
|
|
|
BMSAutoKeysound ak = { row, GetKeysound(obj) };
|
|
|
|
|
autos.push_back( ak );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int rowsToLook[3] = { 0, -1, 1 };
|
|
|
|
|
for( unsigned i = 0; i < autos.size(); i ++ )
|
|
|
|
|
{
|
|
|
|
|
BMSAutoKeysound &ak = autos[i];
|
|
|
|
|
bool found = false;
|
|
|
|
|
for( int j = 0; j < 3; j ++ )
|
|
|
|
|
{
|
|
|
|
|
int row = ak.row + rowsToLook[j];
|
2011-09-10 01:08:04 +07:00
|
|
|
for( int t = 0; t < tracks; t ++ )
|
|
|
|
|
{
|
2011-09-10 17:11:46 +07:00
|
|
|
if( nd.GetTapNote( t, row ) == TAP_EMPTY && !nd.IsHoldNoteAtRow( t, row ) )
|
2011-09-10 01:08:04 +07:00
|
|
|
{
|
|
|
|
|
TapNote tn = TAP_ORIGINAL_TAP;
|
|
|
|
|
tn.type = TapNote::autoKeysound;
|
2011-09-10 17:11:46 +07:00
|
|
|
tn.iKeysoundIndex = ak.index;
|
2011-09-10 01:08:04 +07:00
|
|
|
nd.SetTapNote( t, row, tn );
|
2011-09-10 17:11:46 +07:00
|
|
|
found = true;
|
2011-09-10 01:08:04 +07:00
|
|
|
break;
|
|
|
|
|
}
|
2011-03-17 01:47:30 -04:00
|
|
|
}
|
2011-09-10 17:11:46 +07:00
|
|
|
if( found ) break;
|
2011-03-17 01:47:30 -04:00
|
|
|
}
|
|
|
|
|
}
|
2011-09-10 02:05:12 -05:00
|
|
|
|
2011-09-10 01:08:04 +07:00
|
|
|
delete transform;
|
|
|
|
|
delete holdStart;
|
2011-03-17 01:47:30 -04:00
|
|
|
|
2011-09-10 01:08:04 +07:00
|
|
|
td.TidyUpData();
|
|
|
|
|
out->SetNoteData(nd);
|
|
|
|
|
out->m_Timing = td;
|
|
|
|
|
out->TidyUpData();
|
|
|
|
|
out->SetSavedToDisk( true ); // we're loading from disk, so this is by definintion already saved
|
2011-03-17 01:47:30 -04:00
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2011-09-10 01:08:04 +07:00
|
|
|
Steps *BMSChartReader::GetSteps()
|
2011-03-17 01:47:30 -04:00
|
|
|
{
|
2011-09-10 01:08:04 +07:00
|
|
|
return out;
|
|
|
|
|
}
|
2011-03-17 01:47:30 -04:00
|
|
|
|
2011-09-10 01:08:04 +07:00
|
|
|
struct BMSStepsInfo {
|
|
|
|
|
Steps *steps;
|
|
|
|
|
BMSChartInfo info;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
class BMSSongLoader
|
|
|
|
|
{
|
|
|
|
|
RString dir;
|
|
|
|
|
BMSSong song;
|
|
|
|
|
vector<BMSStepsInfo> loadedSteps;
|
|
|
|
|
public:
|
|
|
|
|
BMSSongLoader( RString songDir, Song *outSong );
|
|
|
|
|
bool Load( RString fileName );
|
|
|
|
|
void AddToSong();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
BMSSongLoader::BMSSongLoader( RString songDir, Song *outSong ): song(outSong), dir(songDir)
|
|
|
|
|
{
|
2011-03-17 01:47:30 -04:00
|
|
|
}
|
|
|
|
|
|
2011-09-10 01:08:04 +07:00
|
|
|
bool BMSSongLoader::Load( RString fileName )
|
2011-03-17 01:47:30 -04:00
|
|
|
{
|
2011-09-10 01:08:04 +07:00
|
|
|
// before doing anything else, load the chart first!
|
|
|
|
|
BMSChart chart;
|
|
|
|
|
if( !chart.Load( dir + fileName ) ) return false;
|
2011-03-17 01:47:30 -04:00
|
|
|
|
2011-09-10 01:08:04 +07:00
|
|
|
// and then read the chart into the steps.
|
|
|
|
|
Steps *steps = song.GetSong()->CreateSteps();
|
|
|
|
|
steps->SetFilename( dir + fileName );
|
2011-03-17 01:47:30 -04:00
|
|
|
|
2011-09-10 01:08:04 +07:00
|
|
|
BMSChartReader reader( &chart, steps, &song );
|
|
|
|
|
if( !reader.Read() )
|
|
|
|
|
{
|
|
|
|
|
delete steps;
|
|
|
|
|
return false;
|
2011-03-17 01:47:30 -04:00
|
|
|
}
|
|
|
|
|
|
2011-09-10 01:08:04 +07:00
|
|
|
// add it to our song
|
|
|
|
|
song.GetSong()->AddSteps( steps );
|
|
|
|
|
|
|
|
|
|
// add the chart reader instance to our list.
|
|
|
|
|
BMSStepsInfo si = { steps, reader.info };
|
|
|
|
|
loadedSteps.push_back(si);
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
|
2011-03-17 01:47:30 -04:00
|
|
|
}
|
|
|
|
|
|
2011-09-10 01:08:04 +07:00
|
|
|
void BMSSongLoader::AddToSong()
|
2011-07-17 16:06:40 -04:00
|
|
|
{
|
2011-09-10 01:08:04 +07:00
|
|
|
RString commonSubstring = "";
|
|
|
|
|
|
2011-07-17 16:06:40 -04:00
|
|
|
{
|
2011-09-10 01:08:04 +07:00
|
|
|
bool found = false;
|
|
|
|
|
for( unsigned i = 0; i < loadedSteps.size(); i ++ )
|
2011-07-17 16:06:40 -04:00
|
|
|
{
|
2011-09-10 01:08:04 +07:00
|
|
|
if( loadedSteps[i].info.title == "" ) continue;
|
|
|
|
|
if( !found )
|
2011-07-17 16:06:40 -04:00
|
|
|
{
|
2011-09-10 01:08:04 +07:00
|
|
|
commonSubstring = loadedSteps[i].info.title;
|
|
|
|
|
found = true;
|
2011-07-17 16:06:40 -04:00
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
2011-09-10 01:08:04 +07:00
|
|
|
commonSubstring = FindLargestInitialSubstring( commonSubstring, loadedSteps[i].info.title );
|
2011-07-17 16:06:40 -04:00
|
|
|
}
|
|
|
|
|
}
|
2011-09-10 01:08:04 +07:00
|
|
|
if( commonSubstring == "" )
|
2011-07-17 16:06:40 -04:00
|
|
|
{
|
2011-09-10 01:08:04 +07:00
|
|
|
// All bets are off; the titles don't match at all.
|
|
|
|
|
// At this rate we're lucky if we even get the title right.
|
|
|
|
|
LOG->UserLog( "Song", dir, "has BMS files with inconsistent titles." );
|
2011-07-17 16:06:40 -04:00
|
|
|
}
|
|
|
|
|
}
|
2011-08-08 00:29:32 +07:00
|
|
|
|
2011-09-10 01:08:04 +07:00
|
|
|
if( commonSubstring == "" )
|
2011-07-17 16:06:40 -04:00
|
|
|
{
|
2011-09-10 01:08:04 +07:00
|
|
|
// As said before, all bets are off.
|
|
|
|
|
// From here on in, it's nothing but guesswork.
|
2011-03-17 01:47:30 -04:00
|
|
|
|
2011-09-10 01:08:04 +07:00
|
|
|
// Try to figure out the difficulty of each file.
|
|
|
|
|
for( unsigned i = 0; i < loadedSteps.size(); i ++ )
|
|
|
|
|
{
|
|
|
|
|
Steps *steps = loadedSteps[i].steps;
|
|
|
|
|
steps->SetDifficulty( Difficulty_Medium );
|
2011-03-17 01:47:30 -04:00
|
|
|
|
2011-09-10 01:08:04 +07:00
|
|
|
RString title = loadedSteps[i].info.title;
|
2011-03-17 01:47:30 -04:00
|
|
|
|
2011-09-10 01:08:04 +07:00
|
|
|
// XXX: Is this really effective if Common Substring parsing failed?
|
|
|
|
|
if( title != "" ) SearchForDifficulty( title, steps );
|
|
|
|
|
}
|
2011-03-17 01:47:30 -04:00
|
|
|
}
|
2011-09-10 01:08:04 +07:00
|
|
|
else
|
2011-03-17 01:47:30 -04:00
|
|
|
{
|
2011-09-10 01:08:04 +07:00
|
|
|
// Now, with our fancy little substring, trim the titles and
|
|
|
|
|
// figure out where each goes.
|
|
|
|
|
for( unsigned i = 0; i < loadedSteps.size(); i ++ )
|
2011-03-17 01:47:30 -04:00
|
|
|
{
|
2011-09-10 01:08:04 +07:00
|
|
|
Steps *steps = loadedSteps[i].steps;
|
|
|
|
|
steps->SetDifficulty( Difficulty_Medium );
|
2011-03-17 01:47:30 -04:00
|
|
|
|
2011-09-10 01:08:04 +07:00
|
|
|
RString title = loadedSteps[i].info.title;
|
2011-03-17 01:47:30 -04:00
|
|
|
|
2011-09-10 01:08:04 +07:00
|
|
|
if( title != "" && title.size() != commonSubstring.size() )
|
2011-03-17 01:47:30 -04:00
|
|
|
{
|
2011-09-10 01:08:04 +07:00
|
|
|
RString tag = title.substr( commonSubstring.size(), title.size() - commonSubstring.size() );
|
|
|
|
|
tag.MakeLower();
|
|
|
|
|
|
|
|
|
|
// XXX: We should do this with filenames too, I have plenty of examples.
|
|
|
|
|
// however, filenames will be trickier, as stuff at the beginning AND
|
|
|
|
|
// end change per-file, so we'll need a fancier FindLargestInitialSubstring()
|
|
|
|
|
|
|
|
|
|
// XXX: This matches (double), but I haven't seen it used. Again, MORE EXAMPLES NEEDED
|
|
|
|
|
if( tag.find('l') != tag.npos )
|
2011-03-17 01:47:30 -04:00
|
|
|
{
|
2011-09-10 01:08:04 +07:00
|
|
|
unsigned pos = tag.find('l');
|
|
|
|
|
if( pos > 2 && tag.substr(pos - 2, 4) == "solo" )
|
|
|
|
|
{
|
|
|
|
|
// (solo) -- an edit, apparently (Thanks Glenn!)
|
|
|
|
|
steps->SetDifficulty( Difficulty_Edit );
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
// Any of [L7] [L14] (LIGHT7) (LIGHT14) (LIGHT) [L] <LIGHT7> <L7>... you get the idea.
|
|
|
|
|
steps->SetDifficulty( Difficulty_Easy );
|
|
|
|
|
}
|
2011-03-17 01:47:30 -04:00
|
|
|
}
|
2011-09-10 01:08:04 +07:00
|
|
|
// [A] <A> (A) [ANOTHER] <ANOTHER> (ANOTHER) (ANOTHER7) Another (DP ANOTHER) (Another) -ANOTHER- [A7] [A14] etc etc etc
|
|
|
|
|
else if( tag.find('a') != tag.npos )
|
|
|
|
|
steps->SetDifficulty( Difficulty_Hard );
|
|
|
|
|
// XXX: Can also match (double), but should match [B] or [B7]
|
|
|
|
|
else if( tag.find('b') != tag.npos )
|
|
|
|
|
steps->SetDifficulty( Difficulty_Beginner );
|
|
|
|
|
// Other tags I've seen here include (5KEYS) (10KEYS) (7keys) (14keys) (dp) [MIX] [14] (14 Keys Mix)
|
|
|
|
|
// XXX: I'm sure [MIX] means something... anyone know?
|
2011-03-17 01:47:30 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Prefer to read global tags from a Difficulty_Medium file. These tend to
|
|
|
|
|
* have the least cruft in the #TITLE tag, so it's more likely to get a clean
|
|
|
|
|
* title. */
|
2011-09-10 01:08:04 +07:00
|
|
|
int mainIndex = 0;
|
|
|
|
|
for( unsigned i = 0; i < loadedSteps.size(); i ++ )
|
|
|
|
|
if( loadedSteps[i].steps->GetDifficulty() == Difficulty_Medium )
|
|
|
|
|
mainIndex = i;
|
|
|
|
|
|
|
|
|
|
Song *out = song.GetSong();
|
2011-03-17 01:47:30 -04:00
|
|
|
|
2011-09-10 01:08:04 +07:00
|
|
|
{
|
|
|
|
|
const BMSStepsInfo &main = loadedSteps[mainIndex];
|
|
|
|
|
out->m_sSongFileName = main.steps->GetFilename();
|
|
|
|
|
if( main.info.title != "" )
|
|
|
|
|
NotesLoader::GetMainAndSubTitlesFromFullTitle( main.info.title, out->m_sMainTitle, out->m_sSubTitle );
|
|
|
|
|
out->m_sArtist = main.info.artist;
|
|
|
|
|
out->m_sGenre = main.info.genre;
|
|
|
|
|
out->m_sBackgroundFile = main.info.backgroundFile;
|
|
|
|
|
out->m_sMusicFile = main.info.musicFile;
|
|
|
|
|
out->m_SongTiming = main.steps->m_Timing;
|
|
|
|
|
}
|
2011-03-17 01:47:30 -04:00
|
|
|
|
2011-03-23 09:59:39 +07:00
|
|
|
// The brackets before the difficulty are in common substring, so remove them if it's found.
|
|
|
|
|
if( commonSubstring.size() > 2 && commonSubstring[commonSubstring.size() - 2] == ' ' )
|
|
|
|
|
{
|
|
|
|
|
switch( commonSubstring[commonSubstring.size() - 1] )
|
|
|
|
|
{
|
|
|
|
|
case '[':
|
|
|
|
|
case '(':
|
|
|
|
|
case '<':
|
|
|
|
|
commonSubstring = commonSubstring.substr(0, commonSubstring.size() - 2);
|
|
|
|
|
}
|
|
|
|
|
}
|
2011-09-10 01:08:04 +07:00
|
|
|
|
2011-03-17 01:47:30 -04:00
|
|
|
// Override what that global tag said about the title if we have a good substring.
|
|
|
|
|
// Prevents clobbering and catches "MySong (7keys)" / "MySong (Another) (7keys)"
|
|
|
|
|
// Also catches "MySong (7keys)" / "MySong (14keys)"
|
|
|
|
|
if( commonSubstring != "" )
|
2011-09-10 01:08:04 +07:00
|
|
|
NotesLoader::GetMainAndSubTitlesFromFullTitle( commonSubstring, out->m_sMainTitle, out->m_sSubTitle );
|
|
|
|
|
|
|
|
|
|
SlideDuplicateDifficulties( *out );
|
|
|
|
|
|
|
|
|
|
ConvertString( out->m_sMainTitle, "utf-8,japanese" );
|
|
|
|
|
ConvertString( out->m_sArtist, "utf-8,japanese" );
|
|
|
|
|
ConvertString( out->m_sGenre, "utf-8,japanese" );
|
2011-03-17 01:47:30 -04:00
|
|
|
|
2011-09-10 01:08:04 +07:00
|
|
|
}
|
|
|
|
|
|
2011-09-10 02:05:12 -05:00
|
|
|
/*===========================================================================*/
|
2011-09-10 01:08:04 +07:00
|
|
|
|
|
|
|
|
bool BMSLoader::LoadNoteDataFromSimfile( const RString & cachePath, Steps & out )
|
|
|
|
|
{
|
|
|
|
|
Song *pSong = SONGMAN->GetSongFromSteps( &out );
|
|
|
|
|
|
|
|
|
|
// before doing anything else, load the chart first!
|
|
|
|
|
BMSChart chart;
|
|
|
|
|
if( !chart.Load( cachePath ) ) return false;
|
|
|
|
|
|
|
|
|
|
BMSSong song(pSong);
|
|
|
|
|
|
|
|
|
|
BMSChartReader reader( &chart, &out, &song );
|
|
|
|
|
if( !reader.Read() ) return false;
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool BMSLoader::LoadFromDir( const RString &sDir, Song &out )
|
|
|
|
|
{
|
|
|
|
|
LOG->Trace( "Song::LoadFromBMSDir(%s)", sDir.c_str() );
|
|
|
|
|
|
|
|
|
|
ASSERT( out.m_vsKeysoundFile.empty() );
|
|
|
|
|
|
|
|
|
|
vector<RString> arrayBMSFileNames;
|
|
|
|
|
GetApplicableFiles( sDir, arrayBMSFileNames );
|
|
|
|
|
|
|
|
|
|
/* We should have at least one; if we had none, we shouldn't have been
|
|
|
|
|
* called to begin with. */
|
|
|
|
|
ASSERT( arrayBMSFileNames.size() );
|
|
|
|
|
|
|
|
|
|
BMSSongLoader loader( sDir, &out );
|
2011-03-17 01:47:30 -04:00
|
|
|
for( unsigned i=0; i<arrayBMSFileNames.size(); i++ )
|
|
|
|
|
{
|
2011-09-10 01:08:04 +07:00
|
|
|
loader.Load( arrayBMSFileNames[i] );
|
2011-03-17 01:47:30 -04:00
|
|
|
}
|
2011-09-10 01:08:04 +07:00
|
|
|
loader.AddToSong();
|
2011-03-17 01:47:30 -04:00
|
|
|
|
|
|
|
|
return true;
|
2011-09-10 01:08:04 +07:00
|
|
|
|
2011-03-17 01:47:30 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* (c) 2001-2004 Chris Danford, Glenn Maynard
|
|
|
|
|
* 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.
|
|
|
|
|
*/
|