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;