8b09da8e64
Added delay_save_cache to SongCacheIndex so that it doesn't write the entire cache index file after every song is loaded when loading songs. Added m_SongsByDir to SongManager so that GetSongFromDir doesn't have to walk the entire list of songs. Minor changes to when LoadEnabledSongsFromPref occurs and how SanityCheckGroupDir works to speed up loading. Song::ReloadFromSongDir removes cache file to force an actual reload from the song dir instead of reloading from the cache. ReloadFromSongDir exposed to lua. Reordered Actor::LoadFromNode to put Command first because that case is more common. Course::GetTrailUnsorted reserves entries before starting to save time reallocating. join in RageUtil calculates the final size of the concatenated strings reserves it to save time reallocating. Added time log file to RageLog for profiling.
1177 lines
33 KiB
C++
1177 lines
33 KiB
C++
#include <limits.h>
|
|
#include "global.h"
|
|
#include "Course.h"
|
|
#include "CourseLoaderCRS.h"
|
|
#include "Foreach.h"
|
|
#include "GameManager.h"
|
|
#include "GameState.h"
|
|
#include "LocalizedString.h"
|
|
#include "LuaManager.h"
|
|
#include "Preference.h"
|
|
#include "PrefsManager.h"
|
|
#include "ProfileManager.h"
|
|
#include "RageLog.h"
|
|
#include "Song.h"
|
|
#include "SongCacheIndex.h"
|
|
#include "Steps.h"
|
|
#include "ThemeManager.h"
|
|
#include "UnlockManager.h"
|
|
#include "Game.h"
|
|
#include "Style.h"
|
|
|
|
static Preference<int> MAX_SONGS_IN_EDIT_COURSE( "MaxSongsInEditCourse", -1 );
|
|
|
|
static const char *SongSortNames[] = {
|
|
"Randomize",
|
|
"MostPlays",
|
|
"FewestPlays",
|
|
"TopGrades",
|
|
"LowestGrades",
|
|
};
|
|
XToString( SongSort );
|
|
XToLocalizedString( SongSort );
|
|
|
|
|
|
/* Maximum lower value of ranges when difficult: */
|
|
const int MAX_BOTTOM_RANGE = 10;
|
|
|
|
#define SORT_PREFERRED_COLOR THEME->GetMetricC("Course","SortPreferredColor")
|
|
#define SORT_LEVEL1_COLOR THEME->GetMetricC("Course","SortLevel1Color")
|
|
#define SORT_LEVEL2_COLOR THEME->GetMetricC("Course","SortLevel2Color")
|
|
#define SORT_LEVEL3_COLOR THEME->GetMetricC("Course","SortLevel3Color")
|
|
#define SORT_LEVEL4_COLOR THEME->GetMetricC("Course","SortLevel4Color")
|
|
#define SORT_LEVEL5_COLOR THEME->GetMetricC("Course","SortLevel5Color")
|
|
|
|
//#define INCLUDE_BEGINNER_STEPS THEME->GetMetricB( "Course","IncludeBeginnerSteps" );
|
|
|
|
RString CourseEntry::GetTextDescription() const
|
|
{
|
|
vector<RString> vsEntryDescription;
|
|
Song *pSong = songID.ToSong();
|
|
if( pSong )
|
|
vsEntryDescription.push_back( pSong->GetTranslitFullTitle() );
|
|
else
|
|
vsEntryDescription.push_back( "Random" );
|
|
if( !songCriteria.m_sGroupName.empty() )
|
|
vsEntryDescription.push_back( songCriteria.m_sGroupName );
|
|
if( songCriteria.m_bUseSongGenreAllowedList )
|
|
vsEntryDescription.push_back( join(",",songCriteria.m_vsSongGenreAllowedList) );
|
|
if( stepsCriteria.m_difficulty != Difficulty_Invalid && stepsCriteria.m_difficulty != Difficulty_Medium )
|
|
vsEntryDescription.push_back( CourseDifficultyToLocalizedString(stepsCriteria.m_difficulty) );
|
|
if( stepsCriteria.m_iLowMeter != -1 )
|
|
vsEntryDescription.push_back( ssprintf("Low meter: %d", stepsCriteria.m_iLowMeter) );
|
|
if( stepsCriteria.m_iHighMeter != -1 )
|
|
vsEntryDescription.push_back( ssprintf("High meter: %d", stepsCriteria.m_iHighMeter) );
|
|
if( songSort != SongSort_Randomize )
|
|
vsEntryDescription.push_back( "Sort: %d" + SongSortToLocalizedString(songSort) );
|
|
if( songSort != SongSort_Randomize && iChooseIndex != 0 )
|
|
vsEntryDescription.push_back( "Choose " + FormatNumberAndSuffix(iChooseIndex) + " match" );
|
|
int iNumModChanges = GetNumModChanges();
|
|
if( iNumModChanges != 0 )
|
|
vsEntryDescription.push_back( ssprintf("%d mod changes", iNumModChanges) );
|
|
if( fGainSeconds != 0 )
|
|
vsEntryDescription.push_back( ssprintf("Low meter: %.0f", fGainSeconds) );
|
|
|
|
RString s = join( ",", vsEntryDescription );
|
|
return s;
|
|
}
|
|
|
|
int CourseEntry::GetNumModChanges() const
|
|
{
|
|
int iNumModChanges = 0;
|
|
if( !sModifiers.empty() )
|
|
iNumModChanges++;
|
|
iNumModChanges += attacks.size();
|
|
return iNumModChanges;
|
|
}
|
|
|
|
|
|
Course::Course(): m_bIsAutogen(false), m_sPath(""), m_sMainTitle(""),
|
|
m_sMainTitleTranslit(""), m_sSubTitle(""), m_sSubTitleTranslit(""),
|
|
m_sScripter(""), m_sDescription(""), m_sBannerPath(""), m_sBackgroundPath(""),
|
|
m_sCDTitlePath(""), m_sGroupName(""), m_bRepeat(false), m_fGoalSeconds(0),
|
|
m_bShuffle(false), m_iLives(-1), m_bSortByMeter(false),
|
|
m_bIncomplete(false), m_vEntries(), m_SortOrder_TotalDifficulty(0),
|
|
m_SortOrder_Ranking(0), m_LoadedFromProfile(ProfileSlot_Invalid),
|
|
m_TrailCache(), m_iTrailCacheSeed(0), m_RadarCache(),
|
|
m_setStyles(), m_CachedObject()
|
|
{
|
|
FOREACH_ENUM( Difficulty,dc)
|
|
m_iCustomMeter[dc] = -1;
|
|
}
|
|
|
|
CourseType Course::GetCourseType() const
|
|
{
|
|
if( m_bRepeat )
|
|
return COURSE_TYPE_ENDLESS;
|
|
if( m_iLives > 0 )
|
|
return COURSE_TYPE_ONI;
|
|
if( !m_vEntries.empty() && m_vEntries[0].fGainSeconds > 0 )
|
|
return COURSE_TYPE_SURVIVAL;
|
|
return COURSE_TYPE_NONSTOP;
|
|
}
|
|
|
|
void Course::SetCourseType( CourseType ct )
|
|
{
|
|
if( GetCourseType() == ct )
|
|
return;
|
|
|
|
m_bRepeat = false;
|
|
m_iLives = -1;
|
|
if( !m_vEntries.empty() )
|
|
m_vEntries[0].fGainSeconds = 0;
|
|
|
|
switch( ct )
|
|
{
|
|
default:
|
|
FAIL_M(ssprintf("Invalid course type: %i", ct));
|
|
case COURSE_TYPE_NONSTOP:
|
|
break;
|
|
case COURSE_TYPE_ONI:
|
|
m_iLives = 4;
|
|
break;
|
|
case COURSE_TYPE_ENDLESS:
|
|
m_bRepeat = true;
|
|
break;
|
|
case COURSE_TYPE_SURVIVAL:
|
|
if( !m_vEntries.empty() )
|
|
m_vEntries[0].fGainSeconds = 120;
|
|
break;
|
|
}
|
|
}
|
|
|
|
PlayMode Course::GetPlayMode() const
|
|
{
|
|
CourseType ct = GetCourseType();
|
|
switch( ct )
|
|
{
|
|
case COURSE_TYPE_ENDLESS: return PLAY_MODE_ENDLESS;
|
|
case COURSE_TYPE_ONI: return PLAY_MODE_ONI;
|
|
case COURSE_TYPE_SURVIVAL: return PLAY_MODE_ONI;
|
|
case COURSE_TYPE_NONSTOP: return PLAY_MODE_NONSTOP;
|
|
default:
|
|
FAIL_M(ssprintf("Invalid course type: %i", ct));
|
|
}
|
|
}
|
|
|
|
void Course::RevertFromDisk()
|
|
{
|
|
// trying to catch invalid an Course
|
|
ASSERT( !m_sPath.empty() );
|
|
|
|
CourseLoaderCRS::LoadFromCRSFile( m_sPath, *this );
|
|
}
|
|
|
|
RString Course::GetCacheFilePath() const
|
|
{
|
|
return SongCacheIndex::GetCacheFilePath( "Courses", m_sPath );
|
|
}
|
|
|
|
void Course::Init()
|
|
{
|
|
m_bIsAutogen = false;
|
|
m_sPath = "";
|
|
|
|
m_sMainTitle = "";
|
|
m_sMainTitleTranslit = "";
|
|
m_sSubTitle = "";
|
|
m_sSubTitleTranslit = "";
|
|
m_sScripter = "";
|
|
m_sDescription = "";
|
|
|
|
m_sBannerPath = "";
|
|
m_sBackgroundPath = "";
|
|
m_sCDTitlePath = "";
|
|
m_sGroupName = "";
|
|
|
|
m_bRepeat = false;
|
|
m_fGoalSeconds = 0;
|
|
m_bShuffle = false;
|
|
|
|
m_iLives = -1;
|
|
FOREACH_ENUM( Difficulty,dc)
|
|
m_iCustomMeter[dc] = -1;
|
|
m_bSortByMeter = false;
|
|
|
|
m_vEntries.clear();
|
|
|
|
m_SortOrder_TotalDifficulty = 0;
|
|
m_SortOrder_Ranking = 0;
|
|
|
|
m_LoadedFromProfile = ProfileSlot_Invalid;
|
|
m_bIncomplete = false;
|
|
m_TrailCache.clear();
|
|
m_iTrailCacheSeed = 0;
|
|
m_RadarCache.clear();
|
|
}
|
|
|
|
bool Course::IsPlayableIn( StepsType st ) const
|
|
{
|
|
Trail t;
|
|
FOREACH_ShownCourseDifficulty( cd )
|
|
{
|
|
if( GetTrailUnsorted( st, cd, t ) )
|
|
return true;
|
|
}
|
|
|
|
// No valid trail for this StepsType.
|
|
return false;
|
|
}
|
|
|
|
|
|
struct SortTrailEntry
|
|
{
|
|
TrailEntry entry;
|
|
int SortMeter;
|
|
|
|
SortTrailEntry(): entry(), SortMeter(0) {}
|
|
|
|
bool operator< ( const SortTrailEntry &rhs ) const { return SortMeter < rhs.SortMeter; }
|
|
};
|
|
|
|
RString Course::GetDisplayMainTitle() const
|
|
{
|
|
if( !PREFSMAN->m_bShowNativeLanguage )
|
|
return GetTranslitMainTitle();
|
|
return m_sMainTitle;
|
|
}
|
|
|
|
RString Course::GetDisplaySubTitle() const
|
|
{
|
|
if( !PREFSMAN->m_bShowNativeLanguage )
|
|
return GetTranslitSubTitle();
|
|
return m_sSubTitle;
|
|
}
|
|
|
|
RString Course::GetDisplayFullTitle() const
|
|
{
|
|
RString sTitle = GetDisplayMainTitle();
|
|
RString sSubTitle = GetDisplaySubTitle();
|
|
|
|
if( !sSubTitle.empty() )
|
|
sTitle += " " + sSubTitle;
|
|
return sTitle;
|
|
}
|
|
|
|
RString Course::GetTranslitFullTitle() const
|
|
{
|
|
RString sTitle = GetTranslitMainTitle();
|
|
RString sSubTitle = GetTranslitSubTitle();
|
|
|
|
if( !sSubTitle.empty() )
|
|
sTitle += " " + sSubTitle;
|
|
return sTitle;
|
|
}
|
|
|
|
/* This is called by many simple functions, like Course::GetTotalSeconds, and may
|
|
* be called on all songs to sort. It can take time to execute, so we cache the
|
|
* results. Returned pointers remain valid for the lifetime of the Course. If the
|
|
* course difficulty doesn't exist, NULL is returned. */
|
|
Trail* Course::GetTrail( StepsType st, CourseDifficulty cd ) const
|
|
{
|
|
ASSERT( cd != Difficulty_Invalid );
|
|
|
|
// Check to see if the Trail cache is out of date
|
|
if( m_iTrailCacheSeed != GAMESTATE->m_iStageSeed )
|
|
{
|
|
RegenerateNonFixedTrails();
|
|
m_iTrailCacheSeed = GAMESTATE->m_iStageSeed;
|
|
}
|
|
|
|
// Look in the Trail cache
|
|
{
|
|
TrailCache_t::iterator it = m_TrailCache.find( CacheEntry(st, cd) );
|
|
if( it != m_TrailCache.end() )
|
|
{
|
|
CacheData &cache = it->second;
|
|
if( cache.null )
|
|
return NULL;
|
|
return &cache.trail;
|
|
}
|
|
}
|
|
|
|
return GetTrailForceRegenCache( st, cd );
|
|
}
|
|
|
|
Trail* Course::GetTrailForceRegenCache( StepsType st, CourseDifficulty cd ) const
|
|
{
|
|
// Construct a new Trail, add it to the cache, then return it.
|
|
CacheData &cache = m_TrailCache[ CacheEntry(st, cd) ];
|
|
Trail &trail = cache.trail;
|
|
trail.Init();
|
|
if( !GetTrailSorted(st, cd, trail) )
|
|
{
|
|
// This course difficulty doesn't exist.
|
|
cache.null = true;
|
|
return NULL;
|
|
}
|
|
|
|
// If we have cached RadarValues for this trail, insert them.
|
|
{
|
|
RadarCache_t::const_iterator it = m_RadarCache.find( CacheEntry( st, cd ) );
|
|
if( it != m_RadarCache.end() )
|
|
{
|
|
const RadarValues &rv = it->second;
|
|
trail.SetRadarValues(rv);
|
|
}
|
|
}
|
|
|
|
cache.null = false;
|
|
return &cache.trail;
|
|
}
|
|
|
|
bool Course::GetTrailSorted( StepsType st, CourseDifficulty cd, Trail &trail ) const
|
|
{
|
|
if( !GetTrailUnsorted( st, cd, trail ) )
|
|
return false;
|
|
|
|
if( this->m_bSortByMeter )
|
|
{
|
|
/* Sort according to Difficulty_Medium, since the order of songs
|
|
* must not change across difficulties. */
|
|
Trail SortTrail;
|
|
if( cd == Difficulty_Medium )
|
|
SortTrail = trail;
|
|
else
|
|
{
|
|
bool bOK = GetTrailUnsorted( st, Difficulty_Medium, SortTrail );
|
|
|
|
/* If we have any other difficulty, we must have Difficulty_Medium. */
|
|
ASSERT( bOK );
|
|
}
|
|
ASSERT_M( trail.m_vEntries.size() == SortTrail.m_vEntries.size(),
|
|
ssprintf("%i %i", int(trail.m_vEntries.size()), int(SortTrail.m_vEntries.size())) );
|
|
|
|
vector<SortTrailEntry> entries;
|
|
for( unsigned i = 0; i < trail.m_vEntries.size(); ++i )
|
|
{
|
|
SortTrailEntry ste;
|
|
ste.entry = trail.m_vEntries[i];
|
|
ste.SortMeter = SortTrail.m_vEntries[i].pSteps->GetMeter();
|
|
entries.push_back( ste );
|
|
}
|
|
|
|
stable_sort( entries.begin(), entries.end() );
|
|
for( unsigned i = 0; i < trail.m_vEntries.size(); ++i )
|
|
trail.m_vEntries[i] = entries[i].entry;
|
|
}
|
|
|
|
if( trail.m_vEntries.empty() )
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
// TODO: Move Course initialization after PROFILEMAN is created
|
|
static void CourseSortSongs( SongSort sort, vector<Song*> &vpPossibleSongs, RandomGen &rnd )
|
|
{
|
|
switch( sort )
|
|
{
|
|
DEFAULT_FAIL(sort);
|
|
case SongSort_Randomize:
|
|
random_shuffle( vpPossibleSongs.begin(), vpPossibleSongs.end(), rnd );
|
|
break;
|
|
case SongSort_MostPlays:
|
|
if( PROFILEMAN )
|
|
SongUtil::SortSongPointerArrayByNumPlays( vpPossibleSongs, PROFILEMAN->GetMachineProfile(), true ); // descending
|
|
break;
|
|
case SongSort_FewestPlays:
|
|
if( PROFILEMAN )
|
|
SongUtil::SortSongPointerArrayByNumPlays( vpPossibleSongs, PROFILEMAN->GetMachineProfile(), false ); // ascending
|
|
break;
|
|
case SongSort_TopGrades:
|
|
if( PROFILEMAN )
|
|
SongUtil::SortSongPointerArrayByGrades( vpPossibleSongs, true ); // descending
|
|
break;
|
|
case SongSort_LowestGrades:
|
|
if( PROFILEMAN )
|
|
SongUtil::SortSongPointerArrayByGrades( vpPossibleSongs, false ); // ascending
|
|
break;
|
|
}
|
|
}
|
|
|
|
namespace
|
|
{
|
|
struct SongIsEqual
|
|
{
|
|
const Song *m_pSong;
|
|
SongIsEqual( const Song *pSong ) : m_pSong(pSong) { }
|
|
bool operator()( const SongAndSteps &sas ) const { return sas.pSong == m_pSong; }
|
|
};
|
|
}
|
|
|
|
bool Course::GetTrailUnsorted( StepsType st, CourseDifficulty cd, Trail &trail ) const
|
|
{
|
|
trail.Init();
|
|
|
|
// XXX: Why are beginner and challenge excluded here? -Wolfman2000
|
|
// No idea, probably an obsolete design decision from ITG, removing
|
|
// exclusion here, but there's some other area that prevents it too. -Kyz
|
|
/*
|
|
switch( cd )
|
|
{
|
|
case Difficulty_Beginner:
|
|
return false;
|
|
case Difficulty_Challenge:
|
|
return false;
|
|
default: break;
|
|
}
|
|
*/
|
|
|
|
// Construct a new Trail, add it to the cache, then return it.
|
|
// Different seed for each course, but the same for the whole round:
|
|
RandomGen rnd( GAMESTATE->m_iStageSeed + GetHashForString(m_sMainTitle) );
|
|
|
|
vector<CourseEntry> tmp_entries;
|
|
if( m_bShuffle )
|
|
{
|
|
/* 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. */
|
|
tmp_entries = m_vEntries;
|
|
random_shuffle( tmp_entries.begin(), tmp_entries.end(), rnd );
|
|
}
|
|
|
|
const vector<CourseEntry> &entries = m_bShuffle ? tmp_entries:m_vEntries;
|
|
|
|
// This can take some time, so don't fill it out unless we need it.
|
|
vector<Song*> vSongsByMostPlayed;
|
|
vector<Song*> AllSongsShuffled;
|
|
|
|
trail.m_StepsType = st;
|
|
trail.m_CourseType = GetCourseType();
|
|
trail.m_CourseDifficulty = cd;
|
|
trail.m_vEntries.reserve(entries.size());
|
|
|
|
// Set to true if CourseDifficulty is able to change something.
|
|
bool bCourseDifficultyIsSignificant = (cd == Difficulty_Medium);
|
|
|
|
vector<Song*> vpAllPossibleSongs;
|
|
vector<SongAndSteps> vSongAndSteps;
|
|
|
|
// Resolve each entry to a Song and Steps.
|
|
FOREACH_CONST( CourseEntry, entries, e )
|
|
{
|
|
SongAndSteps resolved; // fill this in
|
|
SongCriteria soc = e->songCriteria;
|
|
|
|
Song *pSong = e->songID.ToSong();
|
|
if( pSong )
|
|
{
|
|
soc.m_bUseSongAllowedList = true;
|
|
soc.m_vpSongAllowedList.push_back( pSong );
|
|
}
|
|
soc.m_Tutorial = SongCriteria::Tutorial_No;
|
|
soc.m_Locked = SongCriteria::Locked_Unlocked;
|
|
if( !soc.m_bUseSongAllowedList )
|
|
soc.m_iMaxStagesForSong = 1;
|
|
|
|
StepsCriteria stc = e->stepsCriteria;
|
|
stc.m_st = st;
|
|
stc.m_Locked = StepsCriteria::Locked_Unlocked;
|
|
|
|
const bool bSameSongCriteria = e != entries.begin() && (e-1)->songCriteria == soc;
|
|
const bool bSameStepsCriteria = e != entries.begin() && (e-1)->stepsCriteria == stc;
|
|
|
|
if( pSong )
|
|
{
|
|
StepsUtil::GetAllMatching( pSong, stc, vSongAndSteps );
|
|
}
|
|
else if( vSongAndSteps.empty() || !(bSameSongCriteria && bSameStepsCriteria) )
|
|
{
|
|
vSongAndSteps.clear();
|
|
StepsUtil::GetAllMatching( soc, stc, vSongAndSteps );
|
|
}
|
|
|
|
// It looks bad to have the same song 2x in a row in a randomly generated course.
|
|
// Don't allow the same song to be played 2x in a row, unless there's only
|
|
// one song in vpPossibleSongs.
|
|
if( trail.m_vEntries.size() > 0 && vSongAndSteps.size() > 1 )
|
|
{
|
|
const TrailEntry &teLast = trail.m_vEntries.back();
|
|
RemoveIf( vSongAndSteps, SongIsEqual(teLast.pSong) );
|
|
}
|
|
|
|
// if there are no songs to choose from, abort this CourseEntry
|
|
if( vSongAndSteps.empty() )
|
|
continue;
|
|
|
|
vector<Song*> vpSongs;
|
|
typedef vector<Steps*> StepsVector;
|
|
map<Song*,StepsVector> mapSongToSteps;
|
|
FOREACH_CONST( SongAndSteps, vSongAndSteps, sas )
|
|
{
|
|
StepsVector &v = mapSongToSteps[sas->pSong];
|
|
|
|
v.push_back( sas->pSteps );
|
|
if( v.size() == 1 )
|
|
vpSongs.push_back( sas->pSong );
|
|
}
|
|
|
|
CourseSortSongs( e->songSort, vpSongs, rnd );
|
|
|
|
ASSERT( e->iChooseIndex >= 0 );
|
|
if( e->iChooseIndex < int(vSongAndSteps.size()) )
|
|
{
|
|
resolved.pSong = vpSongs[e->iChooseIndex];
|
|
const vector<Steps*> &mappedSongs = mapSongToSteps[resolved.pSong];
|
|
resolved.pSteps = mappedSongs[ RandomInt(mappedSongs.size()) ];
|
|
}
|
|
else
|
|
{
|
|
continue;
|
|
}
|
|
|
|
/* If we're not COURSE_DIFFICULTY_REGULAR, then we should be choosing steps that are
|
|
* either easier or harder than the base difficulty. If no such steps exist, then
|
|
* just use the one we already have. */
|
|
Difficulty dc = resolved.pSteps->GetDifficulty();
|
|
int iLowMeter = e->stepsCriteria.m_iLowMeter;
|
|
int iHighMeter = e->stepsCriteria.m_iHighMeter;
|
|
if( cd != Difficulty_Medium && !e->bNoDifficult )
|
|
{
|
|
Difficulty new_dc = (Difficulty)(dc + cd - Difficulty_Medium);
|
|
new_dc = clamp( new_dc, (Difficulty)0, (Difficulty)(Difficulty_Edit-1) );
|
|
/*
|
|
// re-edit this code to work using the metric.
|
|
Difficulty new_dc;
|
|
if( INCLUDE_BEGINNER_STEPS )
|
|
{
|
|
// don't factor in the course difficulty if we're including
|
|
// beginner steps -aj
|
|
new_dc = clamp( dc, Difficulty_Beginner, (Difficulty)(Difficulty_Edit-1) );
|
|
}
|
|
else
|
|
{
|
|
new_dc = (Difficulty)(dc + cd - Difficulty_Medium);
|
|
new_dc = clamp( new_dc, (Difficulty)0, (Difficulty)(Difficulty_Edit-1) );
|
|
}
|
|
*/
|
|
|
|
bool bChangedDifficulty = false;
|
|
if( new_dc != dc )
|
|
{
|
|
Steps* pNewSteps = SongUtil::GetStepsByDifficulty( resolved.pSong, st, new_dc );
|
|
if( pNewSteps )
|
|
{
|
|
dc = new_dc;
|
|
resolved.pSteps = pNewSteps;
|
|
bChangedDifficulty = true;
|
|
bCourseDifficultyIsSignificant = true;
|
|
}
|
|
}
|
|
|
|
/* Hack: We used to adjust low_meter/high_meter above while searching for
|
|
* songs. However, that results in a different song being chosen for
|
|
* difficult courses, which is bad when LockCourseDifficulties is disabled;
|
|
* each player can end up with a different song. Instead, choose based
|
|
* on the original range, bump the steps based on course difficulty, and
|
|
* then retroactively tweak the low_meter/high_meter so course displays
|
|
* line up. */
|
|
if( e->stepsCriteria.m_difficulty == Difficulty_Invalid && bChangedDifficulty )
|
|
{
|
|
/* Minimum and maximum to add to make the meter range contain the actual
|
|
* meter: */
|
|
int iMinDist = resolved.pSteps->GetMeter() - iHighMeter;
|
|
int iMaxDist = resolved.pSteps->GetMeter() - iLowMeter;
|
|
|
|
/* Clamp the possible adjustments to try to avoid going under 1 or over
|
|
* MAX_BOTTOM_RANGE. */
|
|
iMinDist = min( max( iMinDist, -iLowMeter+1 ), iMaxDist );
|
|
iMaxDist = max( min( iMaxDist, MAX_BOTTOM_RANGE-iHighMeter ), iMinDist );
|
|
|
|
int iAdd;
|
|
if( iMaxDist == iMinDist )
|
|
iAdd = iMaxDist;
|
|
else
|
|
iAdd = rnd(iMaxDist-iMinDist) + iMinDist;
|
|
iLowMeter += iAdd;
|
|
iHighMeter += iAdd;
|
|
}
|
|
}
|
|
|
|
TrailEntry te;
|
|
te.pSong = resolved.pSong;
|
|
te.pSteps = resolved.pSteps;
|
|
te.Modifiers = e->sModifiers;
|
|
te.Attacks = e->attacks;
|
|
te.bSecret = e->bSecret;
|
|
te.iLowMeter = iLowMeter;
|
|
te.iHighMeter = iHighMeter;
|
|
|
|
/* If we chose based on meter (not difficulty), then store Difficulty_Invalid, so
|
|
* other classes can tell that we used meter. */
|
|
if( e->stepsCriteria.m_difficulty == Difficulty_Invalid )
|
|
{
|
|
te.dc = Difficulty_Invalid;
|
|
}
|
|
else
|
|
{
|
|
/* Otherwise, store the actual difficulty we got (post-course-difficulty).
|
|
* This may or may not be the same as e.difficulty. */
|
|
te.dc = dc;
|
|
}
|
|
trail.m_vEntries.push_back( te );
|
|
|
|
// LOG->Trace( "Chose: %s, %d", te.pSong->GetSongDir().c_str(), te.pSteps->GetMeter() );
|
|
|
|
if( IsAnEdit() && MAX_SONGS_IN_EDIT_COURSE > 0 &&
|
|
int(trail.m_vEntries.size()) >= MAX_SONGS_IN_EDIT_COURSE )
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* Hack: If any entry was non-FIXED, or m_bShuffle is set, then radar values
|
|
* for this trail will be meaningless as they'll change every time. Pre-cache
|
|
* empty data. XXX: How can we do this cleanly, without propagating lots of
|
|
* otherwise unnecessary data (course entry types, m_bShuffle) to Trail, or
|
|
* storing a Course pointer in Trail (yuck)? */
|
|
if( !AllSongsAreFixed() || m_bShuffle )
|
|
{
|
|
trail.m_bRadarValuesCached = true;
|
|
trail.m_CachedRadarValues = RadarValues();
|
|
}
|
|
|
|
/* If we have a manually-entered meter for this difficulty, use it. */
|
|
if( m_iCustomMeter[cd] != -1 )
|
|
trail.m_iSpecifiedMeter = m_iCustomMeter[cd];
|
|
|
|
/* If the course difficulty never actually changed anything, then this difficulty
|
|
* is equivalent to Difficulty_Medium; it doesn't exist. */
|
|
return bCourseDifficultyIsSignificant && trail.m_vEntries.size() > 0;
|
|
}
|
|
|
|
void Course::GetTrails( vector<Trail*> &AddTo, StepsType st ) const
|
|
{
|
|
FOREACH_ShownCourseDifficulty( cd )
|
|
{
|
|
Trail *pTrail = GetTrail( st, cd );
|
|
if( pTrail == NULL )
|
|
continue;
|
|
AddTo.push_back( pTrail );
|
|
}
|
|
}
|
|
|
|
void Course::GetAllTrails( vector<Trail*> &AddTo ) const
|
|
{
|
|
vector<StepsType> vStepsTypesToShow;
|
|
GAMEMAN->GetStepsTypesForGame( GAMESTATE->m_pCurGame, vStepsTypesToShow );
|
|
FOREACH( StepsType, vStepsTypesToShow, st )
|
|
{
|
|
GetTrails( AddTo, *st );
|
|
}
|
|
}
|
|
|
|
int Course::GetMeter( StepsType st, CourseDifficulty cd ) const
|
|
{
|
|
if( m_iCustomMeter[cd] != -1 )
|
|
return m_iCustomMeter[cd];
|
|
const Trail* pTrail = GetTrail( st );
|
|
if( pTrail != NULL )
|
|
return pTrail->GetMeter();
|
|
return 0;
|
|
}
|
|
|
|
bool Course::HasMods() const
|
|
{
|
|
FOREACH_CONST( CourseEntry, m_vEntries, e )
|
|
{
|
|
if( !e->attacks.empty() )
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool Course::HasTimedMods() const
|
|
{
|
|
// What makes this different from the SM4 implementation is that
|
|
// HasTimedMods now searches for bGlobal in the attacks; if one of
|
|
// them is false, it has timed mods. Also returning false will probably
|
|
// take longer than expected. -aj
|
|
FOREACH_CONST( CourseEntry, m_vEntries, e )
|
|
{
|
|
if( !e->attacks.empty() )
|
|
{
|
|
for( unsigned s=0; s < e->attacks.size(); s++ )
|
|
{
|
|
const Attack attack = e->attacks[s];
|
|
if(!attack.bGlobal)
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool Course::AllSongsAreFixed() const
|
|
{
|
|
FOREACH_CONST( CourseEntry, m_vEntries, e )
|
|
{
|
|
if( !e->IsFixedSong() )
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
const Style *Course::GetCourseStyle( const Game *pGame, int iNumPlayers ) const
|
|
{
|
|
vector<const Style*> vpStyles;
|
|
GAMEMAN->GetCompatibleStyles( pGame, iNumPlayers, vpStyles );
|
|
|
|
for( int s=0; s < (int) vpStyles.size(); ++s )
|
|
{
|
|
const Style *pStyle = vpStyles[s];
|
|
FOREACHS_CONST( RString, m_setStyles, style )
|
|
{
|
|
if( !style->CompareNoCase(pStyle->m_szName) )
|
|
return pStyle;
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
void Course::InvalidateTrailCache()
|
|
{
|
|
m_TrailCache.clear();
|
|
}
|
|
|
|
void Course::Invalidate( const Song *pStaleSong )
|
|
{
|
|
FOREACH_CONST( CourseEntry, m_vEntries, e )
|
|
{
|
|
Song *pSong = e->songID.ToSong();
|
|
if( pSong == pStaleSong ) // a fixed entry that references the stale Song
|
|
{
|
|
RevertFromDisk();
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Invalidate any Trails that contain this song.
|
|
// If we find a Trail that contains this song, then it's part of a
|
|
// non-fixed entry. So, regenerating the Trail will force different
|
|
// songs to be chosen.
|
|
FOREACH_ENUM( StepsType,st )
|
|
{
|
|
FOREACH_ShownCourseDifficulty( cd )
|
|
{
|
|
TrailCache_t::iterator it = m_TrailCache.find( CacheEntry(st, cd) );
|
|
if( it == m_TrailCache.end() )
|
|
continue;
|
|
CacheData &cache = it->second;
|
|
if( !cache.null )
|
|
if( GetTrail( st, cd )->ContainsSong( pStaleSong ) )
|
|
m_TrailCache.erase( it );
|
|
}
|
|
}
|
|
}
|
|
|
|
void Course::RegenerateNonFixedTrails() const
|
|
{
|
|
// Only need to regen Trails if the Course has a random entry.
|
|
// We can create these Trails on demand because we don't
|
|
// calculate RadarValues for Trails with one or more non-fixed
|
|
// entry.
|
|
if( AllSongsAreFixed() )
|
|
return;
|
|
|
|
FOREACHM( CacheEntry, CacheData, m_TrailCache, e )
|
|
{
|
|
const CacheEntry &ce = e->first;
|
|
GetTrailForceRegenCache( ce.first, ce.second );
|
|
}
|
|
}
|
|
|
|
RageColor Course::GetColor() const
|
|
{
|
|
// FIXME: Calculate the meter.
|
|
int iMeter = 5;
|
|
|
|
switch( PREFSMAN->m_CourseSortOrder )
|
|
{
|
|
case COURSE_SORT_PREFERRED:
|
|
return SORT_PREFERRED_COLOR; //This will also be used for autogen'd courses in some cases.
|
|
|
|
case COURSE_SORT_SONGS:
|
|
if( m_vEntries.size() >= 7 ) return SORT_LEVEL2_COLOR;
|
|
else if( m_vEntries.size() >= 4 ) return SORT_LEVEL4_COLOR;
|
|
else return SORT_LEVEL5_COLOR;
|
|
|
|
case COURSE_SORT_METER:
|
|
if( !AllSongsAreFixed() ) return SORT_LEVEL1_COLOR;
|
|
else if( iMeter > 9 ) return SORT_LEVEL2_COLOR;
|
|
else if( iMeter >= 7 ) return SORT_LEVEL3_COLOR;
|
|
else if( iMeter >= 5 ) return SORT_LEVEL4_COLOR;
|
|
else return SORT_LEVEL5_COLOR;
|
|
|
|
case COURSE_SORT_METER_SUM:
|
|
if( !AllSongsAreFixed() ) return SORT_LEVEL1_COLOR;
|
|
if( m_SortOrder_TotalDifficulty >= 40 ) return SORT_LEVEL2_COLOR;
|
|
if( m_SortOrder_TotalDifficulty >= 30 ) return SORT_LEVEL3_COLOR;
|
|
if( m_SortOrder_TotalDifficulty >= 20 ) return SORT_LEVEL4_COLOR;
|
|
else return SORT_LEVEL5_COLOR;
|
|
|
|
case COURSE_SORT_RANK:
|
|
if( m_SortOrder_Ranking == 3 ) return SORT_LEVEL1_COLOR;
|
|
else if( m_SortOrder_Ranking == 2 ) return SORT_LEVEL3_COLOR;
|
|
else if( m_SortOrder_Ranking == 1 ) return SORT_LEVEL5_COLOR;
|
|
else return SORT_LEVEL4_COLOR;
|
|
default:
|
|
FAIL_M( ssprintf("Invalid course sort %d.", int(PREFSMAN->m_CourseSortOrder)) );
|
|
return RageColor(1,1,1,1); // white; should never reach here
|
|
}
|
|
}
|
|
|
|
bool Course::GetTotalSeconds( StepsType st, float& fSecondsOut ) const
|
|
{
|
|
if( !AllSongsAreFixed() )
|
|
return false;
|
|
|
|
Trail* trail = GetTrail( st, Difficulty_Medium );
|
|
if(!trail)
|
|
{
|
|
for(int cd= 0; cd < NUM_CourseDifficulty; ++cd)
|
|
{
|
|
trail= GetTrail(st, (CourseDifficulty)cd);
|
|
if(trail)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
if(!trail)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
fSecondsOut = trail->GetLengthSeconds();
|
|
return true;
|
|
}
|
|
|
|
bool Course::CourseHasBestOrWorst() const
|
|
{
|
|
FOREACH_CONST( CourseEntry, m_vEntries, e )
|
|
{
|
|
if( e->songSort == SongSort_MostPlays && e->iChooseIndex != -1 )
|
|
return true;
|
|
if( e->songSort == SongSort_FewestPlays && e->iChooseIndex != -1 )
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
RString Course::GetBannerPath() const
|
|
{
|
|
if( m_sBannerPath.empty() )
|
|
return RString();
|
|
if( m_sBannerPath[0] == '/' )
|
|
return m_sBannerPath;
|
|
return Dirname(m_sPath) + m_sBannerPath;
|
|
}
|
|
|
|
RString Course::GetBackgroundPath() const
|
|
{
|
|
if( m_sBackgroundPath.empty() )
|
|
return RString();
|
|
if( m_sBackgroundPath[0] == '/' )
|
|
return m_sBackgroundPath;
|
|
return Dirname(m_sPath) + m_sBackgroundPath;
|
|
}
|
|
|
|
bool Course::HasBanner() const
|
|
{
|
|
return GetBannerPath() != "" && IsAFile(GetBannerPath());
|
|
}
|
|
|
|
bool Course::HasBackground() const
|
|
{
|
|
return GetBackgroundPath() != "" && IsAFile(GetBackgroundPath());
|
|
}
|
|
|
|
void Course::UpdateCourseStats( StepsType st )
|
|
{
|
|
m_SortOrder_TotalDifficulty = 0;
|
|
|
|
// courses with random/players best-worst songs should go at the end
|
|
for(unsigned i = 0; i < m_vEntries.size(); i++)
|
|
{
|
|
Song *pSong = m_vEntries[i].songID.ToSong();
|
|
if( pSong != NULL )
|
|
continue;
|
|
|
|
if ( m_SortOrder_Ranking == 2 )
|
|
m_SortOrder_Ranking = 3;
|
|
m_SortOrder_TotalDifficulty = INT_MAX;
|
|
return;
|
|
}
|
|
|
|
const Trail* pTrail = GetTrail( st, Difficulty_Medium );
|
|
|
|
m_SortOrder_TotalDifficulty += pTrail != NULL? pTrail->GetTotalMeter():0;
|
|
|
|
// OPTIMIZATION: Ranking info isn't dependent on style, so call it
|
|
// sparingly. It's handled on startup and when themes change.
|
|
|
|
LOG->Trace("%s: Total feet: %d",
|
|
this->m_sMainTitle.c_str(),
|
|
m_SortOrder_TotalDifficulty );
|
|
}
|
|
|
|
bool Course::IsRanking() const
|
|
{
|
|
vector<RString> rankingsongs;
|
|
|
|
split(THEME->GetMetric("ScreenRanking", "CoursesToShow"), ",", rankingsongs);
|
|
|
|
for(unsigned i=0; i < rankingsongs.size(); i++)
|
|
if (rankingsongs[i].CompareNoCase(m_sPath))
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
const CourseEntry *Course::FindFixedSong( const Song *pSong ) const
|
|
{
|
|
FOREACH_CONST( CourseEntry, m_vEntries, e )
|
|
{
|
|
const CourseEntry &entry = *e;
|
|
Song *lSong = entry.songID.ToSong();
|
|
if( pSong == lSong )
|
|
return &entry;
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
void Course::GetAllCachedTrails( vector<Trail *> &out )
|
|
{
|
|
TrailCache_t::iterator it;
|
|
for( it = m_TrailCache.begin(); it != m_TrailCache.end(); ++it )
|
|
{
|
|
CacheData &cd = it->second;
|
|
if( !cd.null )
|
|
out.push_back( &cd.trail );
|
|
}
|
|
}
|
|
|
|
bool Course::ShowInDemonstrationAndRanking() const
|
|
{
|
|
// Don't show endless courses in Ranking.
|
|
// todo: make this a metric of course types not to show? -aj
|
|
return !IsEndless();
|
|
}
|
|
|
|
void Course::CalculateRadarValues()
|
|
{
|
|
FOREACH_ENUM( StepsType,st )
|
|
{
|
|
FOREACH_ENUM( CourseDifficulty,cd )
|
|
{
|
|
// For courses that aren't fixed, the radar values are meaningless.
|
|
// Makes non-fixed courses have unknown radar values.
|
|
if( AllSongsAreFixed() )
|
|
{
|
|
Trail *pTrail = GetTrail( st, cd );
|
|
if( pTrail == NULL )
|
|
continue;
|
|
RadarValues rv = pTrail->GetRadarValues();
|
|
m_RadarCache[CacheEntry(st, cd)] = rv;
|
|
}
|
|
else
|
|
{
|
|
m_RadarCache[CacheEntry(st, cd)] = RadarValues();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
bool Course::Matches( RString sGroup, RString sCourse ) const
|
|
{
|
|
if( sGroup.size() && sGroup.CompareNoCase(this->m_sGroupName) != 0)
|
|
return false;
|
|
|
|
RString sFile = m_sPath;
|
|
if( !sFile.empty() )
|
|
{
|
|
sFile.Replace("\\","/");
|
|
vector<RString> bits;
|
|
split( sFile, "/", bits );
|
|
const RString &sLastBit = bits[bits.size()-1];
|
|
if( sCourse.EqualsNoCase(sLastBit) )
|
|
return true;
|
|
}
|
|
|
|
if( sCourse.EqualsNoCase(this->GetTranslitFullTitle()) )
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
// lua start
|
|
#include "LuaBinding.h"
|
|
|
|
/** @brief Allow Lua to have access to the CourseEntry. */
|
|
class LunaCourseEntry: public Luna<CourseEntry>
|
|
{
|
|
public:
|
|
static int GetSong( T* p, lua_State *L )
|
|
{
|
|
if( p->songID.ToSong() )
|
|
p->songID.ToSong()->PushSelf(L);
|
|
else
|
|
lua_pushnil(L);
|
|
return 1;
|
|
}
|
|
DEFINE_METHOD( IsSecret, bSecret );
|
|
DEFINE_METHOD( IsFixedSong, IsFixedSong() );
|
|
DEFINE_METHOD( GetGainSeconds, fGainSeconds );
|
|
DEFINE_METHOD( GetGainLives, iGainLives );
|
|
DEFINE_METHOD( GetNormalModifiers, sModifiers );
|
|
// GetTimedModifiers - table
|
|
DEFINE_METHOD( GetNumModChanges, GetNumModChanges() );
|
|
DEFINE_METHOD( GetTextDescription, GetTextDescription() );
|
|
|
|
LunaCourseEntry()
|
|
{
|
|
ADD_METHOD( GetSong );
|
|
// sm-ssc additions:
|
|
ADD_METHOD( IsSecret );
|
|
ADD_METHOD( IsFixedSong );
|
|
ADD_METHOD( GetGainSeconds );
|
|
ADD_METHOD( GetGainLives );
|
|
ADD_METHOD( GetNormalModifiers );
|
|
//ADD_METHOD( GetTimedModifiers );
|
|
ADD_METHOD( GetNumModChanges );
|
|
ADD_METHOD( GetTextDescription );
|
|
}
|
|
};
|
|
|
|
LUA_REGISTER_CLASS( CourseEntry )
|
|
|
|
// Now for the Course bindings:
|
|
/** @brief Allow Lua to have access to the Course. */
|
|
class LunaCourse: public Luna<Course>
|
|
{
|
|
public:
|
|
DEFINE_METHOD( GetPlayMode, GetPlayMode() )
|
|
static int GetDisplayFullTitle( T* p, lua_State *L ) { lua_pushstring(L, p->GetDisplayFullTitle() ); return 1; }
|
|
static int GetTranslitFullTitle( T* p, lua_State *L ) { lua_pushstring(L, p->GetTranslitFullTitle() ); return 1; }
|
|
static int HasMods( T* p, lua_State *L ) { lua_pushboolean(L, p->HasMods() ); return 1; }
|
|
static int HasTimedMods( T* p, lua_State *L ) { lua_pushboolean( L, p->HasTimedMods() ); return 1; }
|
|
DEFINE_METHOD( GetCourseType, GetCourseType() )
|
|
static int GetCourseEntry( T* p, lua_State *L ) { CourseEntry &ce = p->m_vEntries[IArg(1)]; ce.PushSelf(L); return 1; }
|
|
static int GetCourseEntries( T* p, lua_State *L )
|
|
{
|
|
vector<CourseEntry*> v;
|
|
for( unsigned i = 0; i < p->m_vEntries.size(); ++i )
|
|
{
|
|
v.push_back(&p->m_vEntries[i]);
|
|
}
|
|
LuaHelpers::CreateTableFromArray<CourseEntry*>( v, L );
|
|
return 1;
|
|
}
|
|
static int GetAllTrails( T* p, lua_State *L )
|
|
{
|
|
vector<Trail*> v;
|
|
p->GetAllTrails( v );
|
|
LuaHelpers::CreateTableFromArray<Trail*>( v, L );
|
|
return 1;
|
|
}
|
|
static int GetBannerPath( T* p, lua_State *L ) { RString s = p->GetBannerPath(); if( s.empty() ) return 0; LuaHelpers::Push(L, s); return 1; }
|
|
static int GetBackgroundPath( T* p, lua_State *L ) { RString s = p->GetBackgroundPath(); if( s.empty() ) return 0; LuaHelpers::Push(L, s); return 1; }
|
|
static int GetCourseDir( T* p, lua_State *L ) { lua_pushstring(L, p->m_sPath ); return 1; }
|
|
static int GetGroupName( T* p, lua_State *L ) { lua_pushstring(L, p->m_sGroupName ); return 1; }
|
|
static int IsAutogen( T* p, lua_State *L ) { lua_pushboolean(L, p->m_bIsAutogen ); return 1; }
|
|
static int GetEstimatedNumStages( T* p, lua_State *L ) { lua_pushnumber(L, p->GetEstimatedNumStages() ); return 1; }
|
|
static int GetScripter( T* p, lua_State *L ) { lua_pushstring(L, p->m_sScripter ); return 1; }
|
|
static int GetDescription( T* p, lua_State *L ) { lua_pushstring(L, p->m_sDescription ); return 1; }
|
|
static int GetTotalSeconds( T* p, lua_State *L )
|
|
{
|
|
StepsType st = Enum::Check<StepsType>(L, 1);
|
|
float fTotalSeconds;
|
|
if( !p->GetTotalSeconds(st, fTotalSeconds) )
|
|
lua_pushnil( L );
|
|
else
|
|
lua_pushnumber( L, fTotalSeconds );
|
|
return 1;
|
|
}
|
|
DEFINE_METHOD( IsEndless, IsEndless() )
|
|
DEFINE_METHOD( IsNonstop, IsNonstop() )
|
|
DEFINE_METHOD( IsOni, IsOni() )
|
|
DEFINE_METHOD( GetGoalSeconds, m_fGoalSeconds )
|
|
static int HasBanner( T* p, lua_State *L ) { lua_pushboolean(L, p->HasBanner() ); return 1; }
|
|
static int HasBackground( T* p, lua_State *L ) { lua_pushboolean(L, p->HasBackground() ); return 1; }
|
|
DEFINE_METHOD( IsAnEdit, IsAnEdit() )
|
|
static int IsPlayableIn( T* p, lua_State *L )
|
|
{
|
|
StepsType st = Enum::Check<StepsType>(L, 1);
|
|
lua_pushboolean(L, p->IsPlayableIn( st ) );
|
|
return 1;
|
|
}
|
|
DEFINE_METHOD( IsRanking, IsRanking() )
|
|
DEFINE_METHOD( AllSongsAreFixed, AllSongsAreFixed() )
|
|
|
|
LunaCourse()
|
|
{
|
|
ADD_METHOD( GetPlayMode );
|
|
ADD_METHOD( GetDisplayFullTitle );
|
|
ADD_METHOD( GetTranslitFullTitle );
|
|
ADD_METHOD( HasMods );
|
|
ADD_METHOD( HasTimedMods );
|
|
ADD_METHOD( GetCourseType );
|
|
ADD_METHOD( GetCourseEntry );
|
|
ADD_METHOD( GetCourseEntries );
|
|
ADD_METHOD( GetAllTrails );
|
|
ADD_METHOD( GetBannerPath );
|
|
ADD_METHOD( GetBackgroundPath );
|
|
ADD_METHOD( GetCourseDir );
|
|
ADD_METHOD( GetGroupName );
|
|
ADD_METHOD( IsAutogen );
|
|
ADD_METHOD( GetEstimatedNumStages );
|
|
ADD_METHOD( GetScripter );
|
|
ADD_METHOD( GetDescription );
|
|
ADD_METHOD( GetTotalSeconds );
|
|
ADD_METHOD( IsEndless );
|
|
ADD_METHOD( IsNonstop );
|
|
ADD_METHOD( IsOni );
|
|
ADD_METHOD( GetGoalSeconds );
|
|
ADD_METHOD( HasBanner );
|
|
ADD_METHOD( HasBackground );
|
|
ADD_METHOD( IsAnEdit );
|
|
ADD_METHOD( IsPlayableIn );
|
|
ADD_METHOD( IsRanking );
|
|
ADD_METHOD( AllSongsAreFixed );
|
|
}
|
|
};
|
|
|
|
LUA_REGISTER_CLASS( Course )
|
|
// lua end
|
|
|
|
|
|
/*
|
|
* (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.
|
|
*/
|