Files
itgmania212121/stepmania/src/Course.cpp
T
Andrew Wong d3dadcc2e2 (hopefully) bugfix for following bug:
"Also, sorting courses by Average feet has some problems. At on point the order went Trip 5, World Tour, Road of Slow, Road of 2MB. The averages for these are 8.4, 7.89, 7.6, and 8.6. A bit out of order, eh?"

NOTE: it has to compare floats, NOT ints.  getmeter() doesn't work because it returns an integer, causing loss of decimal places
2003-08-09 22:08:17 +00:00

965 lines
27 KiB
C++

#include "global.h"
/*
-----------------------------------------------------------------------------
Class: Course
Desc: See header.
Copyright (c) 2001-2002 by the person(s) listed below. All rights reserved.
Chris Danford
-----------------------------------------------------------------------------
*/
#include "Course.h"
#include "PrefsManager.h"
#include "song.h"
#include "GameManager.h"
#include "SongManager.h"
#include "RageException.h"
#include "RageLog.h"
#include "MsdFile.h"
#include "PlayerOptions.h"
#include "SongOptions.h"
#include "RageUtil.h"
#include "TitleSubstitution.h"
#include "Steps.h"
#include "GameState.h"
#include "BannerCache.h"
#include "RageFile.h"
#include "arch/arch.h"
#include "ThemeManager.h"
/* Amount to increase meter ranges to make them difficult: */
const int DIFFICULT_METER_CHANGE = 2;
/* Maximum lower value of ranges when difficult: */
const int MAX_BOTTOM_RANGE = 10;
/* -1 is the default parameter of a few Course calls; leaving it out indicates
* to use GAMESTATE->m_bDifficultCourses. */
static bool IsDifficult( int Difficult )
{
if( Difficult == -1 )
return GAMESTATE->m_bDifficultCourses;
else
return !!Difficult;
}
Course::Course()
{
m_bIsAutogen = false;
m_bRepeat = false;
m_bRandomize = false;
m_bDifficult = false;
m_iLives = -1;
m_iMeter = -1;
SetDefaultScore();
}
void Course::SetDefaultScore()
{
//
// Init high scores
//
for( unsigned i=0; i<NUM_STEPS_TYPES; i++ )
for( int j=0; j<NUM_RANKING_LINES; j++ )
{
m_RankingScores[i][j].iScore = IsOni()? 573:573000;
m_RankingScores[i][j].fSurviveTime = 57.3f;
m_RankingScores[i][j].sName = DEFAULT_RANKING_NAME;
}
for( unsigned m=0; m<NUM_MEMORY_CARDS; m++ )
for( unsigned i=0; i<NUM_STEPS_TYPES; i++ )
{
m_MemCardScores[m][i].iNumTimesPlayed = 0;
m_MemCardScores[m][i].iScore = 0;
m_MemCardScores[m][i].fSurviveTime = 0;
}
}
PlayMode Course::GetPlayMode() const
{
if( m_bRepeat )
return PLAY_MODE_ENDLESS;
return m_iLives > 0? PLAY_MODE_ONI:PLAY_MODE_NONSTOP;
}
const int DifficultMeterRamp = 3;
int Course::GetMeter( int Difficult ) const
{
if( m_iMeter != -1 )
return m_iMeter + IsDifficult(Difficult)? DifficultMeterRamp:0;
/*LOG->Trace( "Course file '%s' contains a song '%s%s%s' that is not present",
m_sPath.c_str(), sGroup.c_str(), sGroup.size()? SLASH:"", sSong.c_str());*/
vector<Info> ci;
GetCourseInfo( GAMESTATE->GetCurrentStyleDef()->m_StepsType, ci, Difficult );
/* Take the average meter. */
float fTotalMeter = 0;
for( unsigned c = 0; c < ci.size(); ++c )
{
if( ci[c].Mystery )
{
switch( GetDifficulty(ci[c]) )
{
case DIFFICULTY_INVALID:
{
int iMeterLow, iMeterHigh;
GetMeterRange(ci[c], iMeterLow, iMeterHigh );
fTotalMeter += int( (iMeterLow + iMeterHigh) / 2.0f );
break;
}
case DIFFICULTY_BEGINNER: fTotalMeter += 1; break;
case DIFFICULTY_EASY: fTotalMeter += 2; break;
case DIFFICULTY_MEDIUM: fTotalMeter += 5; break;
case DIFFICULTY_HARD: fTotalMeter += 7; break;
case DIFFICULTY_CHALLENGE: fTotalMeter += 9; break;
}
}
else
fTotalMeter += ci[c].pNotes->GetMeter();
}
return (int)roundf( fTotalMeter / ci.size() );
}
void Course::LoadFromCRSFile( CString sPath )
{
m_sPath = sPath; // save path
MsdFile msd;
if( !msd.ReadFile(sPath) )
RageException::Throw( "Error opening CRS file '%s'.", sPath.c_str() );
CString sDir, sFName, sExt;
splitrelpath( sPath, sDir, sFName, sExt );
sFName = sDir + sFName;
CStringArray arrayPossibleBanners;
GetDirListing( sFName + ".png", arrayPossibleBanners, false, true );
GetDirListing( sFName + ".jpg", arrayPossibleBanners, false, true );
GetDirListing( sFName + ".bmp", arrayPossibleBanners, false, true );
GetDirListing( sFName + ".gif", arrayPossibleBanners, false, true );
if( !arrayPossibleBanners.empty() )
{
m_sBannerPath = arrayPossibleBanners[0];
/* Cache and load the course banner. */
BANNERCACHE->CacheBanner( m_sBannerPath );
}
for( unsigned i=0; i<msd.GetNumValues(); i++ )
{
CString sValueName = msd.GetParam(i, 0);
const MsdFile::value_t &sParams = msd.GetValue(i);
// handle the data
if( 0 == stricmp(sValueName, "COURSE") )
{
m_sName = sParams[1];
m_sTranslitName = m_sName;
}
else if( 0 == stricmp(sValueName, "REPEAT") )
{
CString str = sParams[1];
str.MakeLower();
if( str.Find("yes") != -1 )
m_bRepeat = true;
}
else if( 0 == stricmp(sValueName, "LIVES") )
m_iLives = atoi( sParams[1] );
else if( 0 == stricmp(sValueName, "METER") )
m_iMeter = atoi( sParams[1] );
else if( 0 == stricmp(sValueName, "SONG") )
{
Entry new_entry;
// infer entry::Type from the first param
if( sParams[1].Left(strlen("BEST")) == "BEST" )
{
new_entry.type = Entry::best;
new_entry.players_index = atoi( sParams[1].Right(sParams[1].size()-strlen("BEST")) ) - 1;
CLAMP( new_entry.players_index, 0, 500 );
}
else if( sParams[1].Left(strlen("WORST")) == "WORST" )
{
new_entry.type = Entry::worst;
new_entry.players_index = atoi( sParams[1].Right(sParams[1].size()-strlen("WORST")) ) - 1;
CLAMP( new_entry.players_index, 0, 500 );
}
else if( sParams[1] == "*" )
{
new_entry.mystery = true;
new_entry.type = Entry::random;
}
else if( sParams[1].Right(1) == "*" )
{
new_entry.mystery = true;
new_entry.type = Entry::random_within_group;
CString sSong = sParams[1];
sSong.Replace( "\\", "/" );
CStringArray bits;
split( sSong, "/", bits );
if( bits.size() == 2 )
new_entry.group_name = bits[0];
else
LOG->Warn( "Course file '%s' contains a random_within_group entry '%s' that is invalid. "
"Song should be in the format '<group>/*'.",
m_sPath.c_str(), sSong.c_str());
if( !SONGMAN->DoesGroupExist(new_entry.group_name) )
{
/* XXX: We need a place to put "user warnings". This is too loud for info.txt--
* it obscures important warnings--and regular users never look there, anyway. */
//LOG->Warn( "Course file '%s' random_within_group entry '%s' specifies a group that doesn't exist. "
// "This entry will be ignored.",
// m_sPath.c_str(), sSong.c_str());
continue; // skip this #SONG
}
}
else
{
new_entry.type = Entry::fixed;
CString sSong = sParams[1];
new_entry.pSong = SONGMAN->FindSong( sSong );
if( new_entry.pSong == NULL )
{
/* XXX: We need a place to put "user warnings". This is too loud for info.txt--
* it obscures important warnings--and regular users never look there, anyway. */
LOG->Trace( "Course file '%s' contains a fixed song entry '%s' that does not exist. "
"This entry will be ignored.",
m_sPath.c_str(), sSong.c_str());
continue; // skip this #SONG
}
}
new_entry.difficulty = StringToDifficulty( sParams[2] );
if( new_entry.difficulty == DIFFICULTY_INVALID )
{
int retval = sscanf( sParams[2], "%d..%d", &new_entry.low_meter, &new_entry.high_meter );
if( retval == 1 )
new_entry.high_meter = new_entry.low_meter;
else if( retval != 2 )
{
LOG->Warn("Course file '%s' contains an invalid difficulty setting: \"%s\", 3..6 used instead",
m_sPath.c_str(), sParams[2].c_str());
new_entry.low_meter = 3;
new_entry.high_meter = 6;
}
}
{
/* If "showcourse" or "noshowcourse" is in the list, force new_entry.mystery
* on or off. */
CStringArray mods;
split( sParams[3], ",", mods, true );
for( int j = (int) mods.size()-1; j >= 0 ; --j )
{
if( !mods[j].CompareNoCase("showcourse") )
new_entry.mystery = false;
else if( !mods[j].CompareNoCase("noshowcourse") )
new_entry.mystery = true;
else
continue;
mods.erase(mods.begin() + j);
}
new_entry.modifiers = join( ",", mods );
}
m_entries.push_back( new_entry );
}
else
LOG->Trace( "Unexpected value named '%s'", sValueName.c_str() );
}
static TitleSubst tsub("courses");
CString ignore;
tsub.Subst(m_sName, ignore, ignore,
ignore, ignore, ignore);
SetDefaultScore();
}
void Course::Save()
{
ASSERT( !m_bIsAutogen );
FILE* fp = Ragefopen( m_sPath, "w" );
if( fp == NULL )
{
LOG->Warn( "Could not write course file '%s'.", m_sPath.c_str() );
return;
}
fprintf( fp, "#COURSE:%s;\n", m_sName.c_str() );
fprintf( fp, "#REPEAT:%s;\n", m_bRepeat ? "YES" : "NO" );
fprintf( fp, "#LIVES:%i;\n", m_iLives );
fprintf( fp, "#METER:%i;\n", m_iMeter );
for( unsigned i=0; i<m_entries.size(); i++ )
{
const Entry& entry = m_entries[i];
switch( entry.type )
{
case Entry::fixed:
fprintf( fp, "#SONG:%s", entry.pSong->GetSongDir().c_str() );
break;
case Entry::random:
fprintf( fp, "#SONG:*" );
break;
case Entry::random_within_group:
fprintf( fp, "#SONG:%s/*", entry.group_name.c_str() );
break;
case Entry::best:
fprintf( fp, "#SONG:BEST%d", entry.players_index+1 );
break;
case Entry::worst:
fprintf( fp, "#SONG:WORST%d", entry.players_index+1 );
break;
default:
ASSERT(0);
}
fprintf( fp, ":" );
if( entry.difficulty != DIFFICULTY_INVALID )
fprintf( fp, "%s", DifficultyToString(entry.difficulty).c_str() );
fprintf( fp, ":" );
if( entry.low_meter != -1 && entry.high_meter != -1 )
fprintf( fp, "%d..%d", entry.low_meter, entry.high_meter );
fprintf( fp, ":%s", entry.modifiers.c_str() );
bool default_mystery = !(entry.type == Entry::random || entry.type == Entry::random_within_group);
if( default_mystery != entry.mystery )
{
if( entry.modifiers != "" )
fprintf( fp, "," );
fprintf( fp, entry.mystery? "noshowcourse":"showcourse" );
}
fprintf( fp, ";\n" );
}
fclose( fp );
}
void Course::AutogenEndlessFromGroup( CString sGroupName, vector<Song*> &apSongsInGroup )
{
m_bIsAutogen = true;
m_bRepeat = true;
m_bRandomize = true;
m_iLives = -1;
m_iMeter = -1;
m_sName = SONGMAN->ShortenGroupName( sGroupName );
m_sBannerPath = SONGMAN->GetGroupBannerPath( sGroupName );
// We want multiple songs, so we can try to prevent repeats during
// gameplay. (We might still get a repeat at the repeat boundary,
// but that'd be rare.) -glenn
Entry e;
e.type = Entry::random_within_group;
e.group_name = sGroupName;
e.difficulty = DIFFICULTY_MEDIUM;
e.mystery = true;
vector<Song*> vSongs;
SONGMAN->GetSongs( vSongs, e.group_name );
for( unsigned i = 0; i < vSongs.size(); ++i)
m_entries.push_back( e );
SetDefaultScore();
}
void Course::AutogenNonstopFromGroup( CString sGroupName, vector<Song*> &apSongsInGroup )
{
AutogenEndlessFromGroup( sGroupName, apSongsInGroup );
m_bRepeat = false;
m_sName += " Random";
// resize to 4
while( m_entries.size() < 4 )
m_entries.push_back( m_entries[0] );
while( m_entries.size() > 4 )
m_entries.pop_back();
SetDefaultScore();
}
/*
* Difficult courses do the following:
*
* For entries with a meter range, bump it up by DIFFICULT_METER_CHANGE;
* eg. 3..6 -> 5..8, with a minimum no higher than MAX_BOTTOM_RANGE.
*
* For entries with a difficulty class, use notes one class harder, if they
* exist. This way, if a static song entry points to a difficulty, we'll always
* play that song, even if we're on difficult and harder notes don't exist. (The
* exception is a static song entry with a meter range, but that's not very useful.)
*/
bool Course::HasDifficult( StepsType nt ) const
{
/* Check to see if any songs would change if difficult. */
vector<Info> Normal, Hard;
GetCourseInfo( nt, Normal, false );
GetCourseInfo( nt, Hard, true );
if( Normal.size() != Hard.size() )
return true; /* it changed */
for( unsigned i=0; i<Normal.size(); i++ )
{
if( Normal[i].CourseIndex != Hard[i].CourseIndex )
return true; /* it changed */
if( Normal[i].Mystery )
{
const Entry &e = m_entries[ Normal[i].CourseIndex ];
/* Difficulties under CHALLENGE change by getting harder. */
if( e.difficulty < DIFFICULTY_CHALLENGE )
return true;
/* Meters under MAX_BOTTOM_RANGE..MAX_BOTTOM_RANGE change by getting harder. */
if( e.difficulty != DIFFICULTY_INVALID &&
e.low_meter < MAX_BOTTOM_RANGE &&
e.high_meter < MAX_BOTTOM_RANGE )
return true;
continue;
}
if( Normal[i].pSong != Hard[i].pSong || Normal[i].pNotes != Hard[i].pNotes )
return true;
}
return false;
}
bool Course::IsPlayableIn( StepsType nt ) const
{
vector<Info> ci;
GetCourseInfo( nt, ci );
return ci.size() > 0;
}
static vector<Song*> GetFilteredBestSongs( StepsType nt )
{
vector<Song*> vSongsByMostPlayed = SONGMAN->GetBestSongs();
// filter out songs that don't have both medium and hard steps and long ver sons
for( int j=vSongsByMostPlayed.size()-1; j>=0; j-- )
{
Song* pSong = vSongsByMostPlayed[j];
if( pSong->m_fMusicLengthSeconds > PREFSMAN->m_fLongVerSongSeconds ||
pSong->m_fMusicLengthSeconds > PREFSMAN->m_fMarathonVerSongSeconds ||
!pSong->GetNotes(nt, DIFFICULTY_MEDIUM, PREFSMAN->m_bAutogenMissingTypes) ||
!pSong->GetNotes(nt, DIFFICULTY_HARD, PREFSMAN->m_bAutogenMissingTypes) )
vSongsByMostPlayed.erase( vSongsByMostPlayed.begin()+j );
}
return vSongsByMostPlayed;
}
void Course::GetCourseInfo( StepsType nt, vector<Course::Info> &ci, int Difficult ) const
{
vector<Entry> entries = m_entries;
/* Different seed for each course, but the same for the whole round: */
RandomGen rnd( GAMESTATE->m_iRoundSeed + GetHashForString(m_sName) );
if( m_bRandomize )
{
/* Always randomize the same way per round. Otherwise, the displayed course
* will change every time it's viewed, and the displayed order will have no
* bearing on what you'll actually play. */
random_shuffle( entries.begin(), entries.end(), rnd );
}
/* This can take some time, so don't fill it out unless we need it. */
bool bMostPlayedSet = false;
vector<Song*> vSongsByMostPlayed;
vector<Song*> AllSongsShuffled = SONGMAN->GetAllSongs();
random_shuffle( AllSongsShuffled.begin(), AllSongsShuffled.end(), rnd );
int CurSong = 0; /* Current offset into AllSongsShuffled */
ci.clear();
for( unsigned i=0; i<entries.size(); i++ )
{
const Entry &e = entries[i];
Song* pSong = NULL; // fill this in
Steps* pNotes = NULL; // fill this in
/* This applies difficult mode for meter ranges. (If it's a difficulty
* class, we'll do it below.) */
int low_meter, high_meter;
GetMeterRange( i, low_meter, high_meter, Difficult );
switch( e.type )
{
case Entry::fixed:
pSong = e.pSong;
if( pSong )
{
if( e.difficulty == DIFFICULTY_INVALID )
pNotes = pSong->GetNotes( nt, low_meter, high_meter, PREFSMAN->m_bAutogenMissingTypes );
else
pNotes = pSong->GetNotes( nt, e.difficulty, PREFSMAN->m_bAutogenMissingTypes );
}
break;
case Entry::random:
case Entry::random_within_group:
{
// find a song with the notes we want
for( unsigned j=0; j<AllSongsShuffled.size(); j++ )
{
/* See if the first song matches what we want. */
ASSERT( unsigned(CurSong) < AllSongsShuffled.size() );
pSong = AllSongsShuffled[CurSong];
ASSERT( pSong );
CurSong = (CurSong+1) % AllSongsShuffled.size();
if(e.type == Entry::random_within_group &&
pSong->m_sGroupName.CompareNoCase(e.group_name))
continue; /* wrong group */
if( e.difficulty == DIFFICULTY_INVALID )
pNotes = pSong->GetNotes( nt, low_meter, high_meter, PREFSMAN->m_bAutogenMissingTypes );
else
pNotes = pSong->GetNotes( nt, e.difficulty, PREFSMAN->m_bAutogenMissingTypes );
if( pNotes ) // found a match
break; // stop searching
pSong = NULL;
pNotes = NULL;
}
}
break;
case Entry::best:
case Entry::worst:
{
if( !bMostPlayedSet )
{
bMostPlayedSet = true;
vSongsByMostPlayed = GetFilteredBestSongs( nt );
}
if( e.players_index >= (int)vSongsByMostPlayed.size() )
break;
switch( e.type )
{
case Entry::best:
pSong = vSongsByMostPlayed[e.players_index];
break;
case Entry::worst:
pSong = vSongsByMostPlayed[vSongsByMostPlayed.size()-1-e.players_index];
break;
default:
ASSERT(0);
}
if( e.difficulty == DIFFICULTY_INVALID )
pNotes = pSong->GetNotes( nt, low_meter, high_meter, PREFSMAN->m_bAutogenMissingTypes );
else
pNotes = pSong->GetNotes( nt, e.difficulty, PREFSMAN->m_bAutogenMissingTypes );
if( pNotes == NULL )
pNotes = pSong->GetClosestNotes( nt, DIFFICULTY_MEDIUM );
}
break;
default:
ASSERT(0);
}
if( !pSong || !pNotes )
continue; // this song entry isn't playable. Skip.
/* If e.difficulty == DIFFICULTY_INVALID, then we already increased difficulty
* based on meter. */
if( IsDifficult(Difficult) && e.difficulty != DIFFICULTY_INVALID )
{
/* See if we can find a NoteData that's one notch more difficult than
* the one we found above. */
Difficulty dc = pNotes->GetDifficulty();
if(dc < DIFFICULTY_CHALLENGE)
{
dc = Difficulty(dc + 1);
Steps* pNewNotes = pSong->GetNotes( nt, dc, PREFSMAN->m_bAutogenMissingTypes );
if(pNewNotes)
pNotes = pNewNotes;
}
}
Info cinfo;
cinfo.pSong = pSong;
cinfo.pNotes = pNotes;
cinfo.Modifiers = e.modifiers;
cinfo.Random = ( e.type == Entry::random || e.type == Entry::random_within_group );
cinfo.Mystery = e.mystery;
cinfo.CourseIndex = i;
cinfo.Difficult = IsDifficult(Difficult);
ci.push_back( cinfo );
}
}
RageColor Course::GetColor() const
{
switch (PREFSMAN->m_iCourseSortOrder)
{
case PrefsManager::COURSE_SORT_SONGS:
if( m_entries.size() >= 7 )
return RageColor(1,0,0,1); // red
else if( m_entries.size() >= 4 )
return RageColor(1,1,0,1); // yellow
else
return RageColor(0,1,0,1); // green
// never should get here
break;
case PrefsManager::COURSE_SORT_METER:
if (SortOrder_AvgDifficulty > 100)
return RageColor(0,0,1,1); // blue
if (SortOrder_AvgDifficulty > 8.5)
return RageColor(1,0,0,1); // red
if (SortOrder_AvgDifficulty >= 7)
return RageColor(1,0.5f,0,1); // orange
if (SortOrder_AvgDifficulty >= 5)
return RageColor(1,1,0,1); // yellow
return RageColor(0,1,0,1); // green
case PrefsManager::COURSE_SORT_METER_SUM:
if (SortOrder_TotalDifficulty > 100000)
return RageColor(0,0,1,1); // blue
if (SortOrder_TotalDifficulty >= 40)
return RageColor(1,0,0,1); // red
if (SortOrder_TotalDifficulty >= 30)
return RageColor(1,0.5f,0,1); // orange
if (SortOrder_TotalDifficulty >= 20)
return RageColor(1,1,0,1); // yellow
return RageColor(0,1,0,1); // green
case PrefsManager::COURSE_SORT_RANK:
if (SortOrder_Ranking == 3)
return RageColor(0,0,1,1); // blue
if (SortOrder_Ranking == 2)
return RageColor(1,0.5f,0,1); // orange
if (SortOrder_Ranking == 1)
return RageColor(0,1,0,1); // green
return RageColor(1,1,0,1); // yellow, never should get here
default:
ASSERT(0);
return RageColor(1,1,1,1); // white, never should reach here
}
}
Difficulty Course::GetDifficulty( const Info &stage ) const
{
Difficulty dc = m_entries[stage.CourseIndex].difficulty;
if( stage.Difficult && dc < DIFFICULTY_CHALLENGE )
dc = Difficulty(dc + 1);
return dc;
}
void Course::GetMeterRange( int stage, int& iMeterLowOut, int& iMeterHighOut, int Difficult ) const
{
iMeterLowOut = m_entries[stage].low_meter;
iMeterHighOut = m_entries[stage].high_meter;
if( m_entries[stage].difficulty == DIFFICULTY_INVALID && IsDifficult(Difficult) )
{
iMeterHighOut += DIFFICULT_METER_CHANGE;
iMeterLowOut += DIFFICULT_METER_CHANGE;
iMeterLowOut = min( iMeterLowOut, MAX_BOTTOM_RANGE );
}
}
void Course::GetMeterRange( const Info &stage, int& iMeterLowOut, int& iMeterHighOut ) const
{
GetMeterRange( stage.CourseIndex, iMeterLowOut, iMeterHighOut, stage.Difficult );
}
bool Course::GetTotalSeconds( float& fSecondsOut ) const
{
vector<Info> ci;
GetCourseInfo( STEPS_TYPE_DANCE_SINGLE, ci );
fSecondsOut = 0;
for( unsigned i=0; i<ci.size(); i++ )
{
if( ci[i].Mystery )
return false;
fSecondsOut += ci[i].pSong->m_fMusicLengthSeconds;
}
return true;
}
struct RankingToInsert
{
PlayerNumber pn;
int iScore;
float fSurviveTime;
static bool CompareDescending( const RankingToInsert &hs1, const RankingToInsert &hs2 )
{
return hs1.iScore > hs2.iScore;
}
static void SortDescending( vector<RankingToInsert>& vHSout )
{
sort( vHSout.begin(), vHSout.end(), CompareDescending );
}
};
void Course::AddScores( StepsType nt, bool bPlayerEnabled[NUM_PLAYERS], int iScore[NUM_PLAYERS], float fSurviveTime[NUM_PLAYERS], int iRankingIndexOut[NUM_PLAYERS], bool bNewRecordOut[NUM_PLAYERS] )
{
vector<RankingToInsert> vHS;
for( int p=0; p<NUM_PLAYERS; p++ )
{
iRankingIndexOut[p] = -1;
bNewRecordOut[p] = false;
if( !bPlayerEnabled[p] )
continue; // skip
// Update memory card
m_MemCardScores[p][nt].iNumTimesPlayed++;
m_MemCardScores[MEMORY_CARD_MACHINE][nt].iNumTimesPlayed++;
if( iScore[p] > m_MemCardScores[p][nt].iScore )
{
m_MemCardScores[p][nt].iScore = iScore[p];
m_MemCardScores[p][nt].fSurviveTime = fSurviveTime[p];
bNewRecordOut[p] = true;
}
if( iScore[p] > m_MemCardScores[MEMORY_CARD_MACHINE][nt].iScore )
{
m_MemCardScores[MEMORY_CARD_MACHINE][nt].iScore = iScore[p];
m_MemCardScores[MEMORY_CARD_MACHINE][nt].fSurviveTime = fSurviveTime[p];
}
// Update Ranking
RankingToInsert hs;
hs.iScore = iScore[p];
hs.fSurviveTime = fSurviveTime[p];
hs.pn = (PlayerNumber)p;
vHS.push_back( hs );
}
// Sort descending before inserting.
// This guarantees that a high score will not switch poitions on us when we later insert scores for the other player
RankingToInsert::SortDescending( vHS );
for( unsigned i=0; i<vHS.size(); i++ )
{
RankingToInsert& newHS = vHS[i];
RankingScore* rankingScores = m_RankingScores[nt];
for( int i=0; i<NUM_RANKING_LINES; i++ )
{
if( newHS.iScore > rankingScores[i].iScore )
{
// We found the insert point. Shift down.
for( int j=NUM_RANKING_LINES-1; j>i; j-- )
rankingScores[j] = rankingScores[j-1];
// insert
rankingScores[i].fSurviveTime = newHS.fSurviveTime;
rankingScores[i].iScore = newHS.iScore;
rankingScores[i].sName = DEFAULT_RANKING_NAME;
iRankingIndexOut[newHS.pn] = i;
break;
}
}
}
}
//
// Sorting stuff
//
static bool CompareCoursePointersByName(const Course* pCourse1, const Course* pCourse2)
{
// HACK: strcmp and other string comparators appear to eat whitespace.
// For example, the string "Players Best 13-16" is sorted between
// "Players Best 1-4" and "Players Best 5-8". Replace the string " "
// with " 0" for comparison only.
// XXX: That doesn't happen to me, and it shouldn't (strcmp is strictly
// a byte sort, though CompareNoCase doesn't use strcmp). Are you sure
// you didn't have only one space before? -glenn
CString sName1 = pCourse1->m_sName;
CString sName2 = pCourse2->m_sName;
sName1.Replace( " " , " 0" );
sName2.Replace( " " , " 0" );
return sName1.CompareNoCase( sName2 ) == -1;
}
static bool CompareCoursePointersByAutogen(const Course* pCourse1, const Course* pCourse2)
{
int b1 = pCourse1->m_bIsAutogen;
int b2 = pCourse2->m_bIsAutogen;
if( b1 < b2 )
return true;
else if( b1 > b2 )
return false;
else
return CompareCoursePointersByName(pCourse1,pCourse2);
}
static bool CompareCoursePointersByDifficulty(const Course* pCourse1, const Course* pCourse2)
{
int iNum1 = pCourse1->GetEstimatedNumStages();
int iNum2 = pCourse2->GetEstimatedNumStages();
if( iNum1 < iNum2 )
return true;
else if( iNum1 > iNum2 )
return false;
else // iNum1 == iNum2
return CompareCoursePointersByAutogen( pCourse1, pCourse2 );
}
static bool CompareCoursePointersByAvgDifficulty(const Course* pCourse1, const Course* pCourse2)
{
// GLENN: we must use the member variable because getmeter
// returns an int, and a loss of precision occurs when that
// happens. Anyways, we already have this variable
// from updatecoursestats.
// Plus players best/courses with randomo courses should
// go at the end.
float fNum1 = pCourse1->SortOrder_AvgDifficulty;
float fNum2 = pCourse2->SortOrder_AvgDifficulty;
if( fNum1 < fNum2 )
return true;
else if( fNum1 > fNum2 )
return false;
else // fNum1 == fNum2
return ( pCourse1->m_sName < pCourse2->m_sName );
}
static bool CompareCoursePointersByTotalDifficulty(const Course* pCourse1, const Course* pCourse2)
{
int iNum1 = pCourse1->SortOrder_TotalDifficulty;
int iNum2 = pCourse2->SortOrder_TotalDifficulty;
if( iNum1 < iNum2 )
return true;
else if( iNum1 > iNum2 )
return false;
else // iNum1 == iNum2
return CompareCoursePointersByAutogen( pCourse1, pCourse2 );
}
static bool CompareCoursePointersByRanking(const Course* pCourse1, const Course* pCourse2)
{
int iNum1 = pCourse1->SortOrder_Ranking;
int iNum2 = pCourse2->SortOrder_Ranking;
if( iNum1 < iNum2 )
return true;
else if( iNum1 > iNum2 )
return false;
else // iNum1 == iNum2
return CompareCoursePointersByAutogen( pCourse1, pCourse2 );
}
void SortCoursePointerArrayByDifficulty( vector<Course*> &apCourses )
{
sort( apCourses.begin(), apCourses.end(), CompareCoursePointersByDifficulty );
}
void SortCoursePointerArrayByRanking( vector<Course*> &apCourses )
{
for(unsigned i=0; i<apCourses.size(); i++)
apCourses[i]->UpdateCourseStats();
sort( apCourses.begin(), apCourses.end(), CompareCoursePointersByRanking );
}
void SortCoursePointerArrayByAvgDifficulty( vector<Course*> &apCourses )
{
sort( apCourses.begin(), apCourses.end(), CompareCoursePointersByAvgDifficulty );
}
void SortCoursePointerArrayByTotalDifficulty( vector<Course*> &apCourses )
{
for(unsigned i=0; i<apCourses.size(); i++)
apCourses[i]->UpdateCourseStats();
sort( apCourses.begin(), apCourses.end(), CompareCoursePointersByTotalDifficulty );
}
static bool CompareCoursePointersByType(const Course* pCourse1, const Course* pCourse2)
{
return pCourse1->GetPlayMode() < pCourse2->GetPlayMode();
}
void SortCoursePointerArrayByType( vector<Course*> &apCourses )
{
stable_sort( apCourses.begin(), apCourses.end(), CompareCoursePointersByType );
}
bool Course::HasBanner() const
{
return m_sBannerPath != "" && IsAFile(m_sBannerPath);
}
void Course::UpdateCourseStats()
{
SortOrder_AvgDifficulty = 0;
SortOrder_TotalDifficulty = 0;
unsigned i;
// courses with random/players best-worst songs should go at the end
for(i = 0; i < m_entries.size(); i++)
{
if ( m_entries[i].type == Entry::fixed )
continue;
SortOrder_AvgDifficulty = 9999999; // large number
if ( SortOrder_Ranking == 2 )
SortOrder_Ranking = 3;
SortOrder_TotalDifficulty = 999999; // large number
return;
}
vector<Info> ci;
GetCourseInfo( GAMESTATE->GetCurrentStyleDef()->m_StepsType, ci );
for( i = 0; i < ci.size(); i++ )
SortOrder_TotalDifficulty += ci[i].pNotes->GetMeter();
SortOrder_AvgDifficulty = (float)SortOrder_TotalDifficulty/GetEstimatedNumStages();
// OPTIMIZATION: Ranking info isn't dependant on style, so
// call it sparingly. Its handled on startup and when
// themes change..
LOG->Trace("%s: Total feet: %d, Average Difficulty: %f",
this->m_sName.c_str(),
SortOrder_TotalDifficulty,
SortOrder_AvgDifficulty);
}