From a580d54662f09f3cd68c1365043361705a5b0ff2 Mon Sep 17 00:00:00 2001 From: Kyzentun Date: Tue, 25 Nov 2014 21:37:15 -0700 Subject: [PATCH] Profile loading no longer clears out scores for songs that can't be found. Profile merging functions added to ProfileMan and ScreenManageProfiles for profile migration. HighScore operators rewritten to follow good practice. --- Themes/_fallback/Languages/en.ini | 4 + src/HighScore.cpp | 48 ++++++++- src/HighScore.h | 19 +++- src/Profile.cpp | 152 ++++++++++++++++++++++++++-- src/Profile.h | 5 + src/ProfileManager.cpp | 23 +++++ src/ProfileManager.h | 3 + src/RageTimer.h | 4 + src/ScreenOptionsManageProfiles.cpp | 28 +++++ 9 files changed, 274 insertions(+), 12 deletions(-) diff --git a/Themes/_fallback/Languages/en.ini b/Themes/_fallback/Languages/en.ini index c36778babb..505e1c7929 100644 --- a/Themes/_fallback/Languages/en.ini +++ b/Themes/_fallback/Languages/en.ini @@ -1216,6 +1216,10 @@ 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 [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..83cb481c47 100644 --- a/src/Profile.cpp +++ b/src/Profile.cpp @@ -26,6 +26,8 @@ #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. */ @@ -547,7 +549,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 +566,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 +787,140 @@ 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()); + } +} + // Category high scores void Profile::AddCategoryHighScore( StepsType st, RankingCategory rc, HighScore hs, int &iIndexOut ) { @@ -1634,8 +1770,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 +1852,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 diff --git a/src/Profile.h b/src/Profile.h index bc3d55932f..a7932bdedc 100644 --- a/src/Profile.h +++ b/src/Profile.h @@ -71,6 +71,8 @@ public: * @brief Set up the Profile with default values. * * Note: there are probably a lot of variables. */ + // When adding new score related data, add logic for handling it to + // MergeScoresFromOtherProfile. -Kyz Profile(): m_sDisplayName(""), m_sCharacterID(""), m_sLastUsedHighScoreName(""), m_iWeightPounds(0), m_Voomax(0), m_BirthYear(0), m_IgnoreStepCountCalories(false), @@ -277,6 +279,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]; diff --git a/src/ProfileManager.cpp b/src/ProfileManager.cpp index f00ab076ad..85591b9751 100644 --- a/src/ProfileManager.cpp +++ b/src/ProfileManager.cpp @@ -620,6 +620,29 @@ 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); +} + // // General // diff --git a/src/ProfileManager.h b/src/ProfileManager.h index 33d9ba43d7..eb41ff4bb0 100644 --- a/src/ProfileManager.h +++ b/src/ProfileManager.h @@ -52,6 +52,9 @@ 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); + // 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..0b1d450dde 100644 --- a/src/ScreenOptionsManageProfiles.cpp +++ b/src/ScreenOptionsManageProfiles.cpp @@ -30,6 +30,10 @@ enum ProfileAction ProfileAction_Rename, ProfileAction_Delete, ProfileAction_Clear, + ProfileAction_MergeToMachine, + ProfileAction_MergeToMachineSkipTotal, + ProfileAction_MergeToP1, + ProfileAction_MergeToP2, NUM_ProfileAction }; static const char *ProfileActionNames[] = { @@ -39,6 +43,10 @@ static const char *ProfileActionNames[] = { "Rename", "Delete", "Clear", + "MergeToMachine", + "MergeToMachineSkipTotal", + "MergeToP1", + "MergeToP2", }; XToString( ProfileAction ); XToLocalizedString( ProfileAction ); @@ -309,6 +317,22 @@ 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; } } } @@ -383,6 +407,10 @@ 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 ); } int iWidth, iX, iY;