diff --git a/Docs/Changelog_sm5.txt b/Docs/Changelog_sm5.txt index 240277c0f6..d32e5e2bd9 100644 --- a/Docs/Changelog_sm5.txt +++ b/Docs/Changelog_sm5.txt @@ -4,6 +4,18 @@ The StepMania 5 Changelog covers all post-sm-ssc changes. For a list of changes from StepMania 4 alpha 5 to sm-ssc v1.2.5, see Changelog_sm-ssc.txt. ________________________________________________________________________________ + +2014/11/28 +---------- +* [Profile] Guest and Test profile types added. All existing and new + profiles are Normal. If a profile is changed to Guest through the profile + management screen, it will always show at the top of the list. A profile + set to Test will always be at the bottom of the list. Profiles can be + moved around in the list. + GetType and GetPriority lua functions added to Profile. + Bug that caused a crash when a profile dir was renamed to a non-number and + a new profile was created fixed. [kyzentun] + 2014/11/22 ---------- * [Preferences] MinTNSToHideNotes preference added. [kyzentun] diff --git a/Docs/Luadoc/Lua.xml b/Docs/Luadoc/Lua.xml index d90c1215eb..425841c22f 100644 --- a/Docs/Luadoc/Lua.xml +++ b/Docs/Luadoc/Lua.xml @@ -1201,6 +1201,7 @@ + @@ -1220,6 +1221,7 @@ + @@ -2271,6 +2273,11 @@ + + + + + diff --git a/Docs/Luadoc/LuaDocumentation.xml b/Docs/Luadoc/LuaDocumentation.xml index 24a1dacae9..9d2280fc91 100644 --- a/Docs/Luadoc/LuaDocumentation.xml +++ b/Docs/Luadoc/LuaDocumentation.xml @@ -3463,6 +3463,9 @@ save yourself some time, copy this for undocumented things: Returns the total number of songs played with the profile. + + Returns the position the profile should have in its category in the list. + Returns the number of times song s has been played with the profile. @@ -3511,6 +3514,9 @@ save yourself some time, copy this for undocumented things: Returns the number of trails with the specified StepsType and Difficulty that you've scored a certain Grade g on. + + Returns the type of the profile. The type of the profile is only used to determine where the profile shows up in the list of profiles, and that problem is already handled by ProfileManager, so if you're reading this, this function only exists so you can make your theme color this profile's list entry based on the type or something like that. + Returns the user table for this Profile. diff --git a/Themes/_fallback/Languages/en.ini b/Themes/_fallback/Languages/en.ini index 599b732db2..0f9b427d19 100644 --- a/Themes/_fallback/Languages/en.ini +++ b/Themes/_fallback/Languages/en.ini @@ -1229,6 +1229,15 @@ Edit=Edit Rename=Rename SetDefaultP1=Set P1 SetDefaultP2=Set P2 +MergeToMachine=Merge into Machine Profile +MergeToMachineSkipTotal=Merge into Machine Profile Without Totals +MergeToP1=Merge into P1 Profile +MergeToP2=Merge into P2 Profile +ChangeToGuest=Change To Guest +ChangeToNormal=Change To Normal +ChangeToTest=Change To Test +MoveUp=Move Up +MoveDown=Move Down [RadarCategory] Air=Air diff --git a/src/HighScore.cpp b/src/HighScore.cpp index 3c5f682d8b..e70bf6a701 100644 --- a/src/HighScore.cpp +++ b/src/HighScore.cpp @@ -8,6 +8,8 @@ #include "Foreach.h" #include "RadarValues.h" +#include + ThemeMetric EMPTY_NAME("HighScore","EmptyName"); @@ -231,22 +233,37 @@ void HighScore::SetDisqualified( bool b ) { m_Impl->bDisqualified = b; } * is used. */ RString *HighScore::GetNameMutable() { return &m_Impl->sName; } -bool HighScore::operator>=( const HighScore& other ) const +bool HighScore::operator<(HighScore const& other) const { /* Make sure we treat AAAA as higher than AAA, even though the score * is the same. */ if( PREFSMAN->m_bPercentageScoring ) { if( GetPercentDP() != other.GetPercentDP() ) - return GetPercentDP() >= other.GetPercentDP(); + return GetPercentDP() < other.GetPercentDP(); } else { if( GetScore() != other.GetScore() ) - return GetScore() >= other.GetScore(); + return GetScore() < other.GetScore(); } - return GetGrade() >= other.GetGrade(); + return GetGrade() < other.GetGrade(); +} + +bool HighScore::operator>(HighScore const& other) const +{ + return other.operator<(*this); +} + +bool HighScore::operator<=( const HighScore& other ) const +{ + return !operator>(other); +} + +bool HighScore::operator>=( const HighScore& other ) const +{ + return !operator<(other); } bool HighScore::operator==( const HighScore& other ) const @@ -254,6 +271,11 @@ bool HighScore::operator==( const HighScore& other ) const return *m_Impl == *other.m_Impl; } +bool HighScore::operator!=( const HighScore& other ) const +{ + return !operator==(other); +} + XNode* HighScore::CreateNode() const { return m_Impl->CreateNode(); @@ -406,6 +428,24 @@ void HighScoreList::ClampSize( bool bIsMachine ) vHighScores.erase( vHighScores.begin()+iMaxScores, vHighScores.end() ); } +void HighScoreList::MergeFromOtherHSL(HighScoreList& other, bool is_machine) +{ + iNumTimesPlayed+= other.iNumTimesPlayed; + if(other.dtLastPlayed > dtLastPlayed) { dtLastPlayed= other.dtLastPlayed; } + if(other.HighGrade > HighGrade) { HighGrade= other.HighGrade; } + vHighScores.insert(vHighScores.end(), other.vHighScores.begin(), + other.vHighScores.end()); + std::sort(vHighScores.begin(), vHighScores.end()); + // Remove non-unique scores because they probably come from an accidental + // repeated merge. -Kyz + vector::iterator unique_end= + std::unique(vHighScores.begin(), vHighScores.end()); + vHighScores.erase(unique_end, vHighScores.end()); + // Reverse it because sort moved the lesser scores to the top. + std::reverse(vHighScores.begin(), vHighScores.end()); + ClampSize(is_machine); +} + XNode* Screenshot::CreateNode() const { XNode* pNode = new XNode( "Screenshot" ); diff --git a/src/HighScore.h b/src/HighScore.h index 2b473db0dd..c3e8ed86bc 100644 --- a/src/HighScore.h +++ b/src/HighScore.h @@ -88,8 +88,12 @@ struct HighScore void Unset(); - bool operator>=( const HighScore& other ) const; - bool operator==( const HighScore& other ) const; + bool operator<(HighScore const& other) const; + bool operator>(HighScore const& other) const; + bool operator<=(HighScore const& other) const; + bool operator>=(HighScore const& other) const; + bool operator==(HighScore const& other) const; + bool operator!=(HighScore const& other) const; XNode* CreateNode() const; void LoadFromNode( const XNode* pNode ); @@ -131,6 +135,8 @@ public: void RemoveAllButOneOfEachName(); void ClampSize( bool bIsMachine ); + void MergeFromOtherHSL(HighScoreList& other, bool is_machine); + XNode* CreateNode() const; void LoadFromNode( const XNode* pNode ); @@ -158,6 +164,15 @@ struct Screenshot XNode* CreateNode() const; void LoadFromNode( const XNode* pNode ); + bool operator<(Screenshot const& rhs) const + { + return highScore.GetDateTime() < rhs.highScore.GetDateTime(); + } + + bool operator==(Screenshot const& rhs) const + { + return sFileName == rhs.sFileName; + } }; #endif diff --git a/src/Profile.cpp b/src/Profile.cpp index de2c10f9c2..20a8ea37f9 100644 --- a/src/Profile.cpp +++ b/src/Profile.cpp @@ -26,10 +26,14 @@ #include "CharacterManager.h" #include "Character.h" +#include + const RString STATS_XML = "Stats.xml"; const RString STATS_XML_GZ = "Stats.xml.gz"; /** @brief The filename for where one can edit their personal profile information. */ const RString EDITABLE_INI = "Editable.ini"; +/** @brief A tiny file containing the type and list priority. */ +const RString TYPE_INI = "Type.ini"; /** @brief The filename containing the signature for STATS_XML's signature. */ const RString DONT_SHARE_SIG = "DontShare.sig"; const RString PUBLIC_KEY_FILE = "public.key"; @@ -58,6 +62,15 @@ const float DEFAULT_BIRTH_YEAR= 1995; #pragma warning (disable : 4706) // assignment within conditional expression #endif +static const char* ProfileTypeNames[] = { + "Guest", + "Normal", + "Test", +}; +XToString(ProfileType); +StringToX(ProfileType); +LuaXType(ProfileType); + int Profile::HighScoresForASong::GetNumTimesPlayed() const { @@ -547,7 +560,7 @@ Song *Profile::GetMostPopularSong() const FOREACHM_CONST( SongID, HighScoresForASong, m_SongHighScores, i ) { int iNumTimesPlayed = i->second.GetNumTimesPlayed(); - if( iNumTimesPlayed > iMaxNumTimesPlayed ) + if(i->first.ToSong() != NULL && iNumTimesPlayed > iMaxNumTimesPlayed) { id = i->first; iMaxNumTimesPlayed = iNumTimesPlayed; @@ -564,7 +577,7 @@ Course *Profile::GetMostPopularCourse() const FOREACHM_CONST( CourseID, HighScoresForACourse, m_CourseHighScores, i ) { int iNumTimesPlayed = i->second.GetNumTimesPlayed(); - if( iNumTimesPlayed > iMaxNumTimesPlayed ) + if(i->first.ToCourse() != NULL && iNumTimesPlayed > iMaxNumTimesPlayed) { id = i->first; iMaxNumTimesPlayed = iNumTimesPlayed; @@ -785,6 +798,198 @@ void Profile::GetAllUsedHighScoreNames(std::set& names) #undef GET_NAMES_FROM_MAP } +// MergeScoresFromOtherProfile has three intended use cases: +// 1. Restoring scores to the machine profile that were deleted because the +// songs were not loaded. +// 2. Migrating a profile from an older version of Stepmania, and adding its +// scores to the machine profile. +// 3. Merging two profiles that were separate together. +// In case 1, the various total numbers are still correct, so they should be +// skipped. This is why the skip_totals arg exists. +// -Kyz +void Profile::MergeScoresFromOtherProfile(Profile* other, bool skip_totals, + RString const& from_dir, RString const& to_dir) +{ + if(!skip_totals) + { +#define MERGE_FIELD(field_name) field_name+= other->field_name; + MERGE_FIELD(m_iTotalSessions); + MERGE_FIELD(m_iTotalSessionSeconds); + MERGE_FIELD(m_iTotalGameplaySeconds); + MERGE_FIELD(m_fTotalCaloriesBurned); + MERGE_FIELD(m_iTotalDancePoints); + MERGE_FIELD(m_iNumExtraStagesPassed); + MERGE_FIELD(m_iNumExtraStagesFailed); + MERGE_FIELD(m_iNumToasties); + MERGE_FIELD(m_iTotalTapsAndHolds); + MERGE_FIELD(m_iTotalJumps); + MERGE_FIELD(m_iTotalHolds); + MERGE_FIELD(m_iTotalRolls); + MERGE_FIELD(m_iTotalMines); + MERGE_FIELD(m_iTotalHands); + MERGE_FIELD(m_iTotalLifts); + FOREACH_ENUM(PlayMode, i) + { + MERGE_FIELD(m_iNumSongsPlayedByPlayMode[i]); + MERGE_FIELD(m_iNumStagesPassedByPlayMode[i]); + } + FOREACH_ENUM(Difficulty, i) + { + MERGE_FIELD(m_iNumSongsPlayedByDifficulty[i]); + } + for(int i= 0; i < MAX_METER; ++i) + { + MERGE_FIELD(m_iNumSongsPlayedByMeter[i]); + } + MERGE_FIELD(m_iNumTotalSongsPlayed); + FOREACH_ENUM(Grade, i) + { + MERGE_FIELD(m_iNumStagesPassedByGrade[i]); + } +#undef MERGE_FIELD + for(map::iterator other_cal= + other->m_mapDayToCaloriesBurned.begin(); + other_cal != other->m_mapDayToCaloriesBurned.end(); ++other_cal) + { + map::iterator this_cal= + m_mapDayToCaloriesBurned.find(other_cal->first); + if(this_cal == m_mapDayToCaloriesBurned.end()) + { + m_mapDayToCaloriesBurned[other_cal->first]= other_cal->second; + } + else + { + this_cal->second.fCals+= other_cal->second.fCals; + } + } + } +#define MERGE_SCORES_IN_MEMBER(main_member, main_key_type, main_value_type, sub_member, sub_key_type, sub_value_type) \ + for(std::map::iterator main_entry= \ + other->main_member.begin(); main_entry != other->main_member.end(); \ + ++main_entry) \ + { \ + std::map::iterator this_entry= \ + main_member.find(main_entry->first); \ + if(this_entry == main_member.end()) \ + { \ + main_member[main_entry->first]= main_entry->second; \ + } \ + else \ + { \ + for(std::map::iterator sub_entry= \ + main_entry->second.sub_member.begin(); \ + sub_entry != main_entry->second.sub_member.end(); ++sub_entry) \ + { \ + std::map::iterator this_sub= \ + this_entry->second.sub_member.find(sub_entry->first); \ + if(this_sub == this_entry->second.sub_member.end()) \ + { \ + this_entry->second.sub_member[sub_entry->first]= sub_entry->second; \ + } \ + else \ + { \ + this_sub->second.hsl.MergeFromOtherHSL(sub_entry->second.hsl, IsMachine()); \ + } \ + } \ + } \ + } + MERGE_SCORES_IN_MEMBER(m_SongHighScores, SongID, HighScoresForASong, m_StepsHighScores, StepsID, HighScoresForASteps); + MERGE_SCORES_IN_MEMBER(m_CourseHighScores, CourseID, HighScoresForACourse, m_TrailHighScores, TrailID, HighScoresForATrail); +#undef MERGE_SCORES_IN_MEMBER + // I think the machine profile should not have screenshots merged into it + // because the intended use case is someone whose profile scores were + // deleted off the machine by mishap, or a profile being migrated from an + // older version of Stepmania. Either way, the screenshots should stay + // with the profile they came from. + // In the case where two local profiles are being merged together, the user + // is probably planning to delete the old profile after the merge, so we + // want to copy the screenshots over. -Kyz + if(!IsMachine()) + { + // The old screenshot count is stored so we know where to start in the + // list when copying the screenshot images. + size_t old_count= m_vScreenshots.size(); + m_vScreenshots.insert(m_vScreenshots.end(), + other->m_vScreenshots.begin(), other->m_vScreenshots.end()); + for(size_t sid= old_count; sid < m_vScreenshots.size(); ++sid) + { + RString old_path= from_dir + "Screenshots/" + m_vScreenshots[sid].sFileName; + RString new_path= to_dir + "Screenshots/" + m_vScreenshots[sid].sFileName; + // Only move the old screenshot over if it exists and won't stomp an + // existing screenshot. + if(FILEMAN->DoesFileExist(old_path) && (!FILEMAN->DoesFileExist(new_path))) + { + FILEMAN->Move(old_path, new_path); + } + } + // The screenshots are kept sorted by date for ease of use, and + // duplicates are removed because they come from the user mistakenly + // merging a second time. -Kyz + std::sort(m_vScreenshots.begin(), m_vScreenshots.end()); + vector::iterator unique_end= + std::unique(m_vScreenshots.begin(), m_vScreenshots.end()); + m_vScreenshots.erase(unique_end, m_vScreenshots.end()); + } +} + +void Profile::swap(Profile& other) +{ + // Type is skipped because this is meant to be used only on matching types, + // to move profiles after the priorities have been assigned. -Kyz + // A bit of a misnomer, since it actually works on any type that has its + // own swap function, which includes the standard containers. +#define SWAP_STR_MEMBER(member_name) member_name.swap(other.member_name) +#define SWAP_GENERAL(member_name) std::swap(member_name, other.member_name) + SWAP_GENERAL(m_ListPriority); + SWAP_STR_MEMBER(m_sDisplayName); + SWAP_STR_MEMBER(m_sCharacterID); + SWAP_STR_MEMBER(m_sLastUsedHighScoreName); + SWAP_GENERAL(m_iWeightPounds); + SWAP_GENERAL(m_Voomax); + SWAP_GENERAL(m_BirthYear); + SWAP_GENERAL(m_IgnoreStepCountCalories); + SWAP_GENERAL(m_IsMale); + SWAP_STR_MEMBER(m_sGuid); + SWAP_GENERAL(m_iCurrentCombo); + SWAP_GENERAL(m_iTotalSessions); + SWAP_GENERAL(m_iTotalSessionSeconds); + SWAP_GENERAL(m_iTotalGameplaySeconds); + SWAP_GENERAL(m_fTotalCaloriesBurned); + SWAP_GENERAL(m_GoalType); + SWAP_GENERAL(m_iGoalCalories); + SWAP_GENERAL(m_iGoalSeconds); + SWAP_GENERAL(m_iTotalDancePoints); + SWAP_GENERAL(m_iNumExtraStagesPassed); + SWAP_GENERAL(m_iNumExtraStagesFailed); + SWAP_GENERAL(m_iNumToasties); + SWAP_GENERAL(m_iTotalTapsAndHolds); + SWAP_GENERAL(m_iTotalJumps); + SWAP_GENERAL(m_iTotalHolds); + SWAP_GENERAL(m_iTotalRolls); + SWAP_GENERAL(m_iTotalMines); + SWAP_GENERAL(m_iTotalHands); + SWAP_GENERAL(m_iTotalLifts); + SWAP_GENERAL(m_bNewProfile); + SWAP_STR_MEMBER(m_UnlockedEntryIDs); + SWAP_STR_MEMBER(m_sLastPlayedMachineGuid); + SWAP_GENERAL(m_LastPlayedDate); + SWAP_GENERAL(m_iNumSongsPlayedByPlayMode); + SWAP_STR_MEMBER(m_iNumSongsPlayedByStyle); + SWAP_GENERAL(m_iNumSongsPlayedByDifficulty); + SWAP_GENERAL(m_iNumSongsPlayedByMeter); + SWAP_GENERAL(m_iNumTotalSongsPlayed); + SWAP_GENERAL(m_iNumStagesPassedByPlayMode); + SWAP_GENERAL(m_iNumStagesPassedByGrade); + SWAP_GENERAL(m_UserTable); + SWAP_STR_MEMBER(m_SongHighScores); + SWAP_STR_MEMBER(m_CourseHighScores); + SWAP_GENERAL(m_CategoryHighScores); + SWAP_STR_MEMBER(m_vScreenshots); + SWAP_STR_MEMBER(m_mapDayToCaloriesBurned); +#undef SWAP_STR_MEMBER +#undef SWAP_GENERAL +} + // Category high scores void Profile::AddCategoryHighScore( StepsType st, RankingCategory rc, HighScore hs, int &iIndexOut ) { @@ -862,6 +1067,7 @@ ProfileLoadResult Profile::LoadAllFromDir( RString sDir, bool bRequireSignature InitAll(); + LoadTypeFromDir(sDir); // Not critical if this fails LoadEditableDataFromDir( sDir ); @@ -949,6 +1155,34 @@ ProfileLoadResult Profile::LoadAllFromDir( RString sDir, bool bRequireSignature return ProfileLoadResult_Success; } +void Profile::LoadTypeFromDir(RString dir) +{ + m_Type= ProfileType_Normal; + m_ListPriority= 0; + RString fn= dir + TYPE_INI; + if(FILEMAN->DoesFileExist(fn)) + { + IniFile ini; + if(ini.ReadFile(fn)) + { + XNode const* data= ini.GetChild("ListPosition"); + if(data != NULL) + { + RString type_str; + if(data->GetAttrValue("Type", type_str)) + { + m_Type= StringToProfileType(type_str); + if(m_Type >= NUM_ProfileType) + { + m_Type= ProfileType_Normal; + } + } + data->GetAttrValue("Priority", m_ListPriority); + } + } + } +} + ProfileLoadResult Profile::LoadStatsXmlFromNode( const XNode *xml, bool bIgnoreEditable ) { /* The placeholder stats.xml file has an tag. Don't load it, @@ -999,6 +1233,7 @@ bool Profile::SaveAllToDir( RString sDir, bool bSignData ) const m_sLastPlayedMachineGuid = PROFILEMAN->GetMachineProfile()->m_sGuid; m_LastPlayedDate = DateTime::GetNowDate(); + SaveTypeToDir(sDir); // Save editable.ini SaveEditableDataToDir( sDir ); @@ -1107,6 +1342,14 @@ bool Profile::SaveStatsXmlToDir( RString sDir, bool bSignData ) const return true; } +void Profile::SaveTypeToDir(RString dir) const +{ + IniFile ini; + ini.SetValue("ListPosition", "Type", ProfileTypeToString(m_Type)); + ini.SetValue("ListPosition", "Priority", m_ListPriority); + ini.WriteFile(dir + TYPE_INI); +} + void Profile::SaveEditableDataToDir( RString sDir ) const { IniFile ini; @@ -1634,8 +1877,10 @@ void Profile::LoadSongScoresFromNode( const XNode* pSongScores ) SongID songID; songID.LoadFromNode( pSong ); - if( !songID.IsValid() ) - continue; + // Allow invalid songs so that scores aren't deleted for people that use + // AdditionalSongsFolders and change it frequently. -Kyz + //if( !songID.IsValid() ) + // continue; FOREACH_CONST_Child( pSong, pSteps ) { @@ -1714,8 +1959,10 @@ void Profile::LoadCourseScoresFromNode( const XNode* pCourseScores ) CourseID courseID; courseID.LoadFromNode( pCourse ); - if( !courseID.IsValid() ) - WARN_AND_CONTINUE; + // Allow invalid courses so that scores aren't deleted for people that use + // AdditionalCoursesFolders and change it frequently. -Kyz + //if( !courseID.IsValid() ) + // WARN_AND_CONTINUE; // Backward compatability hack to fix importing scores of old style @@ -2149,6 +2396,8 @@ public: p->AddScreenshot(screenshot); return 0; } + DEFINE_METHOD(GetType, m_Type); + DEFINE_METHOD(GetPriority, m_ListPriority); static int GetDisplayName( T* p, lua_State *L ) { lua_pushstring(L, p->m_sDisplayName ); return 1; } static int SetDisplayName( T* p, lua_State *L ) @@ -2371,6 +2620,8 @@ public: LunaProfile() { ADD_METHOD( AddScreenshot ); + ADD_METHOD( GetType ); + ADD_METHOD( GetPriority ); ADD_METHOD( GetDisplayName ); ADD_METHOD( SetDisplayName ); ADD_METHOD( GetLastUsedHighScoreName ); diff --git a/src/Profile.h b/src/Profile.h index bc3d55932f..d89975d175 100644 --- a/src/Profile.h +++ b/src/Profile.h @@ -60,6 +60,18 @@ class Song; class Steps; class Course; struct Game; + +// Profile types exist for sorting the list of profiles. +// Guest profiles at the top, test at the bottom. +enum ProfileType +{ + ProfileType_Guest, + ProfileType_Normal, + ProfileType_Test, + NUM_ProfileType, + ProfileType_Invalid +}; + /** * @brief Player data that persists between sessions. * @@ -71,7 +83,14 @@ public: * @brief Set up the Profile with default values. * * Note: there are probably a lot of variables. */ - Profile(): m_sDisplayName(""), m_sCharacterID(""), + // When adding new score related data, add logic for handling it to + // MergeScoresFromOtherProfile. -Kyz + // When adding any new fields, add them to SwapExceptPriority. Anything not + // added to SwapExceptPriority won't be swapped correctly when the user + // changes the list priority of a profile. -Kyz + Profile(): + m_Type(ProfileType_Normal), m_ListPriority(0), + m_sDisplayName(""), m_sCharacterID(""), m_sLastUsedHighScoreName(""), m_iWeightPounds(0), m_Voomax(0), m_BirthYear(0), m_IgnoreStepCountCalories(false), m_IsMale(true), @@ -147,6 +166,10 @@ public: bool IsMachine() const; + ProfileType m_Type; + // Profiles of the same type and priority are sorted by dir name. + int m_ListPriority; + // Editable data RString m_sDisplayName; RString m_sCharacterID; @@ -277,6 +300,9 @@ public: void GetAllUsedHighScoreNames(std::set& names); + void MergeScoresFromOtherProfile(Profile* other, bool skip_totals, + RString const& from_dir, RString const& to_dir); + // Category high scores HighScoreList m_CategoryHighScores[NUM_StepsType][NUM_RankingCategory]; @@ -361,8 +387,11 @@ public: void InitCalorieData(); void ClearStats(); + void swap(Profile& other); + // Loading and saving ProfileLoadResult LoadAllFromDir( RString sDir, bool bRequireSignature ); + void LoadTypeFromDir(RString dir); void LoadCustomFunction( RString sDir ); bool SaveAllToDir( RString sDir, bool bSignData ) const; @@ -375,6 +404,7 @@ public: void LoadScreenshotDataFromNode( const XNode* pNode ); void LoadCalorieDataFromNode( const XNode* pNode ); + void SaveTypeToDir(RString dir) const; void SaveEditableDataToDir( RString sDir ) const; bool SaveStatsXmlToDir( RString sDir, bool bSignData ) const; XNode* SaveStatsXmlCreateNode() const; diff --git a/src/ProfileManager.cpp b/src/ProfileManager.cpp index f00ab076ad..cbdc5dbcd2 100644 --- a/src/ProfileManager.cpp +++ b/src/ProfileManager.cpp @@ -27,6 +27,10 @@ ProfileManager* PROFILEMAN = NULL; // global and accessible from anywhere in our program +#define ID_DIGITS 8 +#define ID_DIGITS_STR "8" +#define MAX_ID 99999999 + static void DefaultLocalProfileIDInit( size_t /*PlayerNumber*/ i, RString &sNameOut, RString &defaultValueOut ) { sNameOut = ssprintf( "DefaultLocalProfileIDP%d", int(i+1) ); @@ -54,6 +58,11 @@ struct DirAndProfile { RString sDir; Profile profile; + void swap(DirAndProfile& other) + { + sDir.swap(other.sDir); + profile.swap(other.profile); + } }; static vector g_vLocalProfile; @@ -399,20 +408,67 @@ void ProfileManager::UnloadAllLocalProfiles() g_vLocalProfile.clear(); } +static void add_category_to_global_list(vector& cat) +{ + g_vLocalProfile.insert(g_vLocalProfile.end(), cat.begin(), cat.end()); +} + void ProfileManager::RefreshLocalProfilesFromDisk() { UnloadAllLocalProfiles(); - vector vsProfileID; - GetDirListing( USER_PROFILES_DIR + "*", vsProfileID, true, true ); - FOREACH_CONST( RString, vsProfileID, p ) + vector profile_ids; + GetDirListing(USER_PROFILES_DIR + "*", profile_ids, true, true); + // Profiles have 3 types: + // 1. Guest profiles: + // Meant for use by guests, always at the top of the list. + // 2. Normal profiles: + // Meant for normal use, listed after guests. + // e. Test profiles: + // Meant for use when testing things, listed last. + // If the user renames a profile directory manually, that should not be a + // problem. -Kyz + map > categorized_profiles; + // The type data for a profile is in its own file so that loading isn't + // slowed down by copying temporary profiles around to make sure the list + // is sorted. The profiles are loaded at the end. -Kyz + FOREACH_CONST(RString, profile_ids, id) { - g_vLocalProfile.push_back( DirAndProfile() ); - DirAndProfile &dap = g_vLocalProfile.back(); - dap.sDir = *p + "/"; - dap.profile.LoadAllFromDir( dap.sDir, PREFSMAN->m_bSignProfileData ); + DirAndProfile derp; + derp.sDir= *id + "/"; + derp.profile.LoadTypeFromDir(derp.sDir); + map >::iterator category= + categorized_profiles.find(derp.profile.m_Type); + if(category == categorized_profiles.end()) + { + categorized_profiles[derp.profile.m_Type].push_back(derp); + } + else + { + bool inserted= false; + FOREACH(DirAndProfile, category->second, curr) + { + if(curr->profile.m_ListPriority > derp.profile.m_ListPriority) + { + category->second.insert(curr, derp); + inserted= true; + break; + } + } + if(!inserted) + { + category->second.push_back(derp); + } + } } -} + add_category_to_global_list(categorized_profiles[ProfileType_Guest]); + add_category_to_global_list(categorized_profiles[ProfileType_Normal]); + add_category_to_global_list(categorized_profiles[ProfileType_Test]); + FOREACH(DirAndProfile, g_vLocalProfile, curr) + { + curr->profile.LoadAllFromDir(curr->sDir, PREFSMAN->m_bSignProfileData); + } + } const Profile *ProfileManager::GetLocalProfile( const RString &sProfileID ) const { @@ -433,14 +489,44 @@ bool ProfileManager::CreateLocalProfile( RString sName, RString &sProfileIDOut ) // Find a directory directory name that's a number greater than all // existing numbers. This preserves the "order by create date". - int iMaxProfileNumber = -1; - vector vs; - GetLocalProfileIDs( vs ); - FOREACH_CONST( RString, vs, s ) - iMaxProfileNumber = StringToInt( *s ); + // Profile IDs are actually the directory names, so they can be any string, + // and we have to handle the case where the user renames one. + // Since the user can rename them, they might have any number, wrapping our + // counter or setting it to a ridiculous value. That case must also be + // handled. -Kyz + int max_profile_number= -1; + int first_free_number= 0; + vector profile_ids; + GetLocalProfileIDs(profile_ids); + FOREACH_CONST(RString, profile_ids, id) + { + int tmp= 0; + if((*id) >> tmp) + { + // The profile ids are already in order, so we don't have to handle the + // case where 5 is encountered before 3. + if(tmp == first_free_number) + { + ++first_free_number; + } + max_profile_number= max(tmp, max_profile_number); + } + } - int iProfileNumber = iMaxProfileNumber + 1; - RString sProfileID = ssprintf( "%08d", iProfileNumber ); + int profile_number = max_profile_number + 1; + // Prevent profiles from going over the 8 digit limit. + if(profile_number > MAX_ID || profile_number < 0) + { + profile_number= first_free_number; + } + ASSERT_M(profile_number >= 0 && profile_number <= MAX_ID, + "Too many profiles, cannot assign ID to new profile."); + RString profile_id = ssprintf( "%0" ID_DIGITS_STR "d", profile_number ); + + // make sure this id doesn't already exist + ASSERT_M(GetLocalProfile(profile_id) == NULL, + ssprintf("creating profile with ID \"%s\" that already exists", + profile_id.c_str())); // Create the new profile. Profile *pProfile = new Profile; @@ -448,7 +534,7 @@ bool ProfileManager::CreateLocalProfile( RString sName, RString &sProfileIDOut ) pProfile->m_sCharacterID = CHARMAN->GetRandomCharacter()->m_sCharacterID; // Save it to disk. - RString sProfileDir = LocalProfileIDToDir( sProfileID ); + RString sProfileDir = LocalProfileIDToDir(profile_id); if( !pProfile->SaveAllToDir(sProfileDir, PREFSMAN->m_bSignProfileData) ) { delete pProfile; @@ -456,12 +542,37 @@ bool ProfileManager::CreateLocalProfile( RString sName, RString &sProfileIDOut ) return false; } - AddLocalProfileByID( pProfile, sProfileID ); + AddLocalProfileByID(pProfile, profile_id); - sProfileIDOut = sProfileID; + sProfileIDOut = profile_id; return true; } +static void InsertProfileIntoList(DirAndProfile& derp) +{ + bool inserted= false; + derp.profile.m_ListPriority= 0; + FOREACH(DirAndProfile, g_vLocalProfile, curr) + { + if(curr->profile.m_Type > derp.profile.m_Type) + { + derp.profile.SaveTypeToDir(derp.sDir); + g_vLocalProfile.insert(curr, derp); + inserted= true; + break; + } + else if(curr->profile.m_Type == derp.profile.m_Type) + { + ++derp.profile.m_ListPriority; + } + } + if(!inserted) + { + derp.profile.SaveTypeToDir(derp.sDir); + g_vLocalProfile.push_back(derp); + } +} + void ProfileManager::AddLocalProfileByID( Profile *pProfile, RString sProfileID ) { // make sure this id doesn't already exist @@ -469,11 +580,10 @@ void ProfileManager::AddLocalProfileByID( Profile *pProfile, RString sProfileID ssprintf("creating \"%s\" \"%s\" that already exists", pProfile->m_sDisplayName.c_str(), sProfileID.c_str()) ); - // insert - g_vLocalProfile.push_back( DirAndProfile() ); - DirAndProfile &dap = g_vLocalProfile.back(); - dap.sDir = LocalProfileIDToDir( sProfileID ); - dap.profile = *pProfile; + DirAndProfile derp; + derp.sDir= LocalProfileIDToDir(sProfileID); + derp.profile= *pProfile; + InsertProfileIntoList(derp); } bool ProfileManager::RenameLocalProfile( RString sProfileID, RString sNewName ) @@ -620,6 +730,81 @@ const Profile* ProfileManager::GetProfile( ProfileSlot slot ) const } } +void ProfileManager::MergeLocalProfiles(RString const& from_id, RString const& to_id) +{ + Profile* from= GetLocalProfile(from_id); + Profile* to= GetLocalProfile(to_id); + if(from == NULL || to == NULL) + { + return; + } + to->MergeScoresFromOtherProfile(from, false, + LocalProfileIDToDir(from_id), LocalProfileIDToDir(to_id)); +} + +void ProfileManager::MergeLocalProfileIntoMachine(RString const& from_id, bool skip_totals) +{ + Profile* from= GetLocalProfile(from_id); + if(from == NULL) + { + return; + } + GetMachineProfile()->MergeScoresFromOtherProfile(from, skip_totals, + LocalProfileIDToDir(from_id), MACHINE_PROFILE_DIR); +} + +void ProfileManager::ChangeProfileType(int index, ProfileType new_type) +{ + if(index < 0 || static_cast(index) >= g_vLocalProfile.size()) + { return; } + if(new_type == g_vLocalProfile[index].profile.m_Type) + { return; } + DirAndProfile derp= g_vLocalProfile[index]; + g_vLocalProfile.erase(g_vLocalProfile.begin() + index); + derp.profile.m_Type= new_type; + InsertProfileIntoList(derp); +} + +void ProfileManager::MoveProfilePriority(int index, bool up) +{ + if(index < 0 || static_cast(index) >= g_vLocalProfile.size()) + { return; } + // Changing the priority is complicated a bit because the profiles might + // all have the same priority. So this function has to assign priorities + // to all the profiles of the same type. + // bools are numbers, true evaluatues to 1. + int swindex= index + ((up * -2) + 1); + ProfileType type= g_vLocalProfile[index].profile.m_Type; + int priority= 0; + for(size_t i= 0; i < g_vLocalProfile.size(); ++i) + { + DirAndProfile* curr= &g_vLocalProfile[i]; + if(curr->profile.m_Type == type) + { + if(curr->profile.m_ListPriority != priority) + { + curr->profile.m_ListPriority= priority; + if(i != index && i != swindex) + { + curr->profile.SaveTypeToDir(curr->sDir); + } + } + ++priority; + } + else if(curr->profile.m_Type > type) + { + break; + } + } + // Only swap if both indices are valid and the types match. + if(swindex >= 0 && static_cast(swindex) < g_vLocalProfile.size() && + g_vLocalProfile[swindex].profile.m_Type == + g_vLocalProfile[index].profile.m_Type) + { + g_vLocalProfile[index].swap(g_vLocalProfile[swindex]); + } +} + // // General // @@ -839,8 +1024,31 @@ public: lua_pushnil(L); return 1; } - static int GetLocalProfileFromIndex( T* p, lua_State *L ) { Profile *pProfile = p->GetLocalProfileFromIndex(IArg(1)); ASSERT(pProfile != NULL); pProfile->PushSelf(L); return 1; } - static int GetLocalProfileIDFromIndex( T* p, lua_State *L ) { lua_pushstring(L, p->GetLocalProfileIDFromIndex(IArg(1)) ); return 1; } + static int GetLocalProfileFromIndex( T* p, lua_State *L ) + { + int index= IArg(1); + if(index >= p->GetNumLocalProfiles()) + { + luaL_error(L, "Profile index %d out of range.", index); + } + Profile *pProfile = p->GetLocalProfileFromIndex(index); + if(pProfile == NULL) + { + luaL_error(L, "No profile at index %d.", index); + } + pProfile->PushSelf(L); + return 1; + } + static int GetLocalProfileIDFromIndex( T* p, lua_State *L ) + { + int index= IArg(1); + if(index >= p->GetNumLocalProfiles()) + { + luaL_error(L, "Profile index %d out of range.", index); + } + lua_pushstring(L, p->GetLocalProfileIDFromIndex(index) ); + return 1; + } static int GetLocalProfileIndexFromID( T* p, lua_State *L ) { lua_pushnumber(L, p->GetLocalProfileIndexFromID(SArg(1)) ); return 1; } static int GetNumLocalProfiles( T* p, lua_State *L ) { lua_pushnumber(L, p->GetNumLocalProfiles() ); return 1; } static int GetProfileDir( T* p, lua_State *L ) { lua_pushstring(L, p->GetProfileDir(Enum::Check(L, 1)) ); return 1; } diff --git a/src/ProfileManager.h b/src/ProfileManager.h index 33d9ba43d7..dd457292f6 100644 --- a/src/ProfileManager.h +++ b/src/ProfileManager.h @@ -6,8 +6,8 @@ #include "Difficulty.h" #include "Preference.h" #include "Grade.h" +#include "Profile.h" -class Profile; class Song; class Steps; class Style; @@ -52,6 +52,11 @@ public: bool SaveLocalProfile( RString sProfileID ); void UnloadProfile( PlayerNumber pn ); + void MergeLocalProfiles(RString const& from_id, RString const& to_id); + void MergeLocalProfileIntoMachine(RString const& from_id, bool skip_totals); + void ChangeProfileType(int index, ProfileType new_type); + void MoveProfilePriority(int index, bool up); + // General data void IncrementToastiesCount( PlayerNumber pn ); void AddStepTotals( PlayerNumber pn, int iNumTapsAndHolds, int iNumJumps, int iNumHolds, int iNumRolls, int iNumMines, int iNumHands, int iNumLifts, float fCaloriesBurned ); diff --git a/src/RageTimer.h b/src/RageTimer.h index bb549c0b8e..da863c11d5 100644 --- a/src/RageTimer.h +++ b/src/RageTimer.h @@ -51,6 +51,10 @@ private: extern const RageTimer RageZeroTimer; +// For profiling how long some chunk of code takes. -Kyz +#define START_TIME(name) float name##_start_time= RageTimer::GetTimeSinceStartFast(); +#define END_TIME(name) float name##_end_time= RageTimer::GetTimeSinceStartFast(); LOG->Warn(#name " time: %f", name##_end_time - name##_start_time); + #endif /* diff --git a/src/ScreenOptionsManageProfiles.cpp b/src/ScreenOptionsManageProfiles.cpp index dae1d916e0..f80665135b 100644 --- a/src/ScreenOptionsManageProfiles.cpp +++ b/src/ScreenOptionsManageProfiles.cpp @@ -30,6 +30,15 @@ enum ProfileAction ProfileAction_Rename, ProfileAction_Delete, ProfileAction_Clear, + ProfileAction_MergeToMachine, + ProfileAction_MergeToMachineSkipTotal, + ProfileAction_MergeToP1, + ProfileAction_MergeToP2, + ProfileAction_ChangeToGuest, + ProfileAction_ChangeToNormal, + ProfileAction_ChangeToTest, + ProfileAction_MoveUp, + ProfileAction_MoveDown, NUM_ProfileAction }; static const char *ProfileActionNames[] = { @@ -39,6 +48,15 @@ static const char *ProfileActionNames[] = { "Rename", "Delete", "Clear", + "MergeToMachine", + "MergeToMachineSkipTotal", + "MergeToP1", + "MergeToP2", + "ChangeToGuest", + "ChangeToNormal", + "ChangeToTest", + "MoveUp", + "MoveDown", }; XToString( ProfileAction ); XToLocalizedString( ProfileAction ); @@ -309,6 +327,45 @@ void ScreenOptionsManageProfiles::HandleScreenMessage( const ScreenMessage SM ) ScreenPrompt::Prompt( SM_BackFromClearConfirm, sMessage, PROMPT_YES_NO ); } break; + case ProfileAction_MergeToMachine: + PROFILEMAN->MergeLocalProfileIntoMachine( + GetLocalProfileIDWithFocus(), false); + break; + case ProfileAction_MergeToMachineSkipTotal: + PROFILEMAN->MergeLocalProfileIntoMachine( + GetLocalProfileIDWithFocus(), true); + break; + case ProfileAction_MergeToP1: + PROFILEMAN->MergeLocalProfiles(GetLocalProfileIDWithFocus(), + ProfileManager::m_sDefaultLocalProfileID[PLAYER_1].Get()); + break; + case ProfileAction_MergeToP2: + PROFILEMAN->MergeLocalProfiles(GetLocalProfileIDWithFocus(), + ProfileManager::m_sDefaultLocalProfileID[PLAYER_2].Get()); + break; + case ProfileAction_ChangeToGuest: + PROFILEMAN->ChangeProfileType(GetLocalProfileIndexWithFocus(), + ProfileType_Guest); + SCREENMAN->SetNewScreen(this->m_sName); // reload + break; + case ProfileAction_ChangeToNormal: + PROFILEMAN->ChangeProfileType(GetLocalProfileIndexWithFocus(), + ProfileType_Normal); + SCREENMAN->SetNewScreen(this->m_sName); // reload + break; + case ProfileAction_ChangeToTest: + PROFILEMAN->ChangeProfileType(GetLocalProfileIndexWithFocus(), + ProfileType_Test); + SCREENMAN->SetNewScreen(this->m_sName); // reload + break; + case ProfileAction_MoveUp: + PROFILEMAN->MoveProfilePriority(GetLocalProfileIndexWithFocus(), true); + SCREENMAN->SetNewScreen(this->m_sName); // reload + break; + case ProfileAction_MoveDown: + PROFILEMAN->MoveProfilePriority(GetLocalProfileIndexWithFocus(), false); + SCREENMAN->SetNewScreen(this->m_sName); // reload + break; } } } @@ -383,6 +440,15 @@ void ScreenOptionsManageProfiles::ProcessMenuStart( const InputEventPlus & ) ADD_ACTION( ProfileAction_Edit ); ADD_ACTION( ProfileAction_Rename ); ADD_ACTION( ProfileAction_Delete ); + ADD_ACTION( ProfileAction_MergeToMachine ); + ADD_ACTION( ProfileAction_MergeToMachineSkipTotal ); + ADD_ACTION( ProfileAction_MergeToP1 ); + ADD_ACTION( ProfileAction_MergeToP2 ); + ADD_ACTION( ProfileAction_ChangeToGuest ); + ADD_ACTION( ProfileAction_ChangeToNormal ); + ADD_ACTION( ProfileAction_ChangeToTest ); + ADD_ACTION( ProfileAction_MoveUp ); + ADD_ACTION( ProfileAction_MoveDown ); } int iWidth, iX, iY;