From ecbeff1856c1a6ac3cf50a10f2e9ca7fc157311a Mon Sep 17 00:00:00 2001 From: J-D-K Date: Sat, 31 May 2025 13:39:01 -0400 Subject: [PATCH] Data loading tweaks, fixes, and finishing touches. --- Makefile | 2 +- include/data/User.hpp | 12 +++- include/data/data.hpp | 7 +- source/JKSV.cpp | 6 +- source/data/User.cpp | 151 +++++++++++++++++++++++------------------- source/data/data.cpp | 109 +++++++++++++++++++++++++----- 6 files changed, 195 insertions(+), 92 deletions(-) diff --git a/Makefile b/Makefile index f5357ec..72d5dee 100644 --- a/Makefile +++ b/Makefile @@ -38,7 +38,7 @@ INCLUDES := include ./Libraries/FsLib/Switch/FsLib/include ./Libraries/SDLLib/SD EXEFS_SRC := exefs_src APP_TITLE := JKSV APP_AUTHOR := JK -APP_VERSION := 12.22.2024 +APP_VERSION := 05.31.2025 ROMFS := romfs ICON := icon.jpg diff --git a/include/data/User.hpp b/include/data/User.hpp index 0fbd231..fd8a1fd 100644 --- a/include/data/User.hpp +++ b/include/data/User.hpp @@ -10,6 +10,9 @@ namespace data /// @brief Type used to store save info and play statistics in the vector. Vector is used to preserve the order since I can't use a map without having to extra heap allocate it. using UserDataEntry = std::pair>; + /// @brief Type definition for the user save info/play stats vector. + using UserSaveInfoList = std::vector; + /// @brief Class that stores data for the user. class User { @@ -34,6 +37,9 @@ namespace data /// @param playStats Play statistics. void add_data(const FsSaveDataInfo &saveInfo, const PdmPlayStatistics &playStats); + /// @brief Clears the user save info vector. + void clear_save_info(void); + /// @brief Erases data at index. /// @param index Index of save data info to erase. void erase_data(int index); @@ -81,6 +87,10 @@ namespace data /// @return Pointer to save info if found. nullptr if not. FsSaveDataInfo *get_save_info_by_id(uint64_t applicationID); + /// @brief Returns a reference to the user save data info vector. + /// @return Reference to the user save info vector. + data::UserSaveInfoList &get_user_save_info_list(void); + /// @brief Returns a pointer to the play statistics of applicationID /// @param applicationID Application ID to search and fetch. /// @return Pointer to play statistics if index is valid. nullptr if it isn't. @@ -111,7 +121,7 @@ namespace data sdl::SharedTexture m_icon = nullptr; /// @brief Vector containing save info and play statistics. - std::vector m_userData; + data::UserSaveInfoList m_userData; /// @brief Loads account structs from system. /// @param profile AccountProfile struct to write to. diff --git a/include/data/data.hpp b/include/data/data.hpp index c5707f9..9dbbb62 100644 --- a/include/data/data.hpp +++ b/include/data/data.hpp @@ -10,9 +10,10 @@ namespace data /// @brief Declaration for user list/user pointer vector. using UserList = std::vector; - /// @brief Loads users, applications, and save info from the system. - /// @return True if everything goes fine. False if something goes horribly wrong. - bool initialize(void); + /// @brief Initializes data. Loads user accounts from system and save data info. + /// @param clearCache Whether or not the current cache file should be deleted from the SD card first. + /// @return True on success. False on failure. + bool initialize(bool clearCache); /// @brief Writes pointers to users to vectorOut /// @param userList List to push the pointers to. diff --git a/source/JKSV.cpp b/source/JKSV.cpp index 4f13309..232da0c 100644 --- a/source/JKSV.cpp +++ b/source/JKSV.cpp @@ -19,8 +19,8 @@ namespace { - constexpr uint8_t BUILD_MON = 1; - constexpr uint8_t BUILD_DAY = 6; + constexpr uint8_t BUILD_MON = 5; + constexpr uint8_t BUILD_DAY = 31; constexpr uint16_t BUILD_YEAR = 2025; } // namespace @@ -82,7 +82,7 @@ JKSV::JKSV(void) // JKSV also has no internal strings anymore. This is FATAL now. ABORT_ON_FAILURE(strings::initialize()); - if (!data::initialize()) + if (!data::initialize(false)) { return; } diff --git a/source/data/User.cpp b/source/data/User.cpp index 77490f0..4432578 100644 --- a/source/data/User.cpp +++ b/source/data/User.cpp @@ -15,74 +15,8 @@ namespace constexpr int SIZE_ICON_FONT = 50; } // namespace -// Function used to sort user data. -static bool sortUserData(const data::UserDataEntry &entryA, const data::UserDataEntry &entryB) -{ - auto &[applicationIDA, dataA] = entryA; - auto &[applicationIDB, dataB] = entryB; - auto &[saveInfoA, playStatsA] = dataA; - auto &[saveInfoB, playStatsB] = dataB; - - // Favorites over all. - if (config::is_favorite(applicationIDA) != config::is_favorite(applicationIDB)) - { - return config::is_favorite(applicationIDA); - } - - data::TitleInfo *titleInfoA = data::get_title_info_by_id(applicationIDA); - data::TitleInfo *titleInfoB = data::get_title_info_by_id(applicationIDB); - switch (config::get_by_key(config::keys::TITLE_SORT_TYPE)) - { - // Alpha - case 0: - { - // Get titles - const char *titleA = titleInfoA->get_title(); - const char *titleB = titleInfoB->get_title(); - - // Get the shortest of the two. - size_t titleALength = std::char_traits::length(titleA); - size_t titleBLength = std::char_traits::length(titleB); - size_t shortestTitle = titleALength < titleBLength ? titleALength : titleBLength; - // Loop and compare codepoints. - for (size_t i = 0, j = 0; i < shortestTitle;) - { - // Decode UTF-8 - uint32_t codepointA = 0; - uint32_t codepointB = 0; - ssize_t unitCountA = decode_utf8(&codepointA, reinterpret_cast(&titleA[i])); - ssize_t unitCountB = decode_utf8(&codepointB, reinterpret_cast(&titleB[j])); - - // Lower so case doesn't screw with it. - int charA = std::tolower(codepointA); - int charB = std::tolower(codepointB); - if (charA != charB) - { - return charA < charB; - } - - i += unitCountA; - j += unitCountB; - } - } - break; - - // Most played. - case 1: - { - return playStatsA.playtime > playStatsB.playtime; - } - break; - - // Last played. - case 2: - { - return playStatsA.last_timestamp_user > playStatsB.last_timestamp_user; - } - break; - } - return false; -} +// Function used to sort user data. Definition at the bottom. +static bool sort_user_data(const data::UserDataEntry &entryA, const data::UserDataEntry &entryB); data::User::User(AccountUid accountID, FsSaveDataType saveType) : m_accountID(accountID), m_saveType(saveType) { @@ -124,6 +58,11 @@ void data::User::add_data(const FsSaveDataInfo &saveInfo, const PdmPlayStatistic m_userData.push_back(std::make_pair(applicationID, std::make_pair(saveInfo, playStats))); } +void data::User::clear_save_info(void) +{ + m_userData.clear(); +} + void data::User::erase_data(int index) { m_userData.erase(m_userData.begin() + index); @@ -131,7 +70,7 @@ void data::User::erase_data(int index) void data::User::sort_data(void) { - std::sort(m_userData.begin(), m_userData.end(), sortUserData); + std::sort(m_userData.begin(), m_userData.end(), sort_user_data); } AccountUid data::User::get_account_id(void) const @@ -199,6 +138,11 @@ FsSaveDataInfo *data::User::get_save_info_by_id(uint64_t applicationID) return &findTitle->second.first; } +data::UserSaveInfoList &data::User::get_user_save_info_list(void) +{ + return m_userData; +} + PdmPlayStatistics *data::User::get_play_stats_by_id(uint64_t applicationID) { auto findTitle = std::find_if(m_userData.begin(), m_userData.end(), [applicationID](data::UserDataEntry &entry) { @@ -268,3 +212,72 @@ void data::User::create_account(void) std::memcpy(m_nickname, accountIDString.c_str(), accountIDString.length()); std::memcpy(m_pathSafeNickname, accountIDString.c_str(), accountIDString.length()); } + +static bool sort_user_data(const data::UserDataEntry &entryA, const data::UserDataEntry &entryB) +{ + // Structured bindings to make this slightly more readable. + auto &[applicationIDA, dataA] = entryA; + auto &[applicationIDB, dataB] = entryB; + auto &[saveInfoA, playStatsA] = dataA; + auto &[saveInfoB, playStatsB] = dataB; + + // Favorites over all. + if (config::is_favorite(applicationIDA) != config::is_favorite(applicationIDB)) + { + return config::is_favorite(applicationIDA); + } + + data::TitleInfo *titleInfoA = data::get_title_info_by_id(applicationIDA); + data::TitleInfo *titleInfoB = data::get_title_info_by_id(applicationIDB); + switch (config::get_by_key(config::keys::TITLE_SORT_TYPE)) + { + // Alpha + case 0: + { + // Get titles + const char *titleA = titleInfoA->get_title(); + const char *titleB = titleInfoB->get_title(); + + // Get the shortest of the two. + size_t titleALength = std::char_traits::length(titleA); + size_t titleBLength = std::char_traits::length(titleB); + size_t shortestTitle = titleALength < titleBLength ? titleALength : titleBLength; + // Loop and compare codepoints. + for (size_t i = 0, j = 0; i < shortestTitle;) + { + // Decode UTF-8 + uint32_t codepointA = 0; + uint32_t codepointB = 0; + ssize_t unitCountA = decode_utf8(&codepointA, reinterpret_cast(&titleA[i])); + ssize_t unitCountB = decode_utf8(&codepointB, reinterpret_cast(&titleB[j])); + + // Lower so case doesn't screw with it. + int charA = std::tolower(codepointA); + int charB = std::tolower(codepointB); + if (charA != charB) + { + return charA < charB; + } + + i += unitCountA; + j += unitCountB; + } + } + break; + + // Most played. + case 1: + { + return playStatsA.playtime > playStatsB.playtime; + } + break; + + // Last played. + case 2: + { + return playStatsA.last_timestamp_user > playStatsB.last_timestamp_user; + } + break; + } + return false; +} diff --git a/source/data/data.cpp b/source/data/data.cpp index 4ebd7fd..457e731 100644 --- a/source/data/data.cpp +++ b/source/data/data.cpp @@ -40,6 +40,12 @@ namespace FsSaveDataSpaceId_SdUser, FsSaveDataSpaceId_ProperSystem, FsSaveDataSpaceId_SafeMode}; + + // These are the ID's used for system type users. + constexpr AccountUid ID_SYSTEM_USER = {FsSaveDataType_System}; + constexpr AccountUid ID_BCAT_USER = {FsSaveDataType_Bcat}; + constexpr AccountUid ID_DEVICE_USER = {FsSaveDataType_Device}; + constexpr AccountUid ID_CACHE_USER = {FsSaveDataType_Cache}; } // namespace // Declarations here. Definitions at bottom. These should appear in the order called. @@ -59,20 +65,45 @@ static bool read_cache_file(void); /// @brief Creates the cache file on the SD card. static void create_cache_file(void); -bool data::initialize(void) +/// @brief Searches for and returns a user according to the AccountUid provided. +/// @param id AccountUid to use to search with. +/// @return Iterator to user on success. s_userVector.end() on failure. +static inline std::vector::iterator find_user_by_id(AccountUid id); + +bool data::initialize(bool clearCache) { - // Load user accounts if not done previously. Bail if the load fails. + // Convert this to an fslib::Path right off the bat so we don't call the path constructor twice. + fslib::Path cachePath = PATH_CACHE_PATH; + + // Nuke the cache file if we're supposed to. + if (clearCache && fslib::file_exists(cachePath) && !fslib::delete_file(cachePath)) + { + // I don't really think this should be fatal. It's not good that it happens, but not fatal. + logger::log("data::initialize failed to remove existing cache file from SD: %s", fslib::get_error_string()); + } + + // Load user accounts if not done previously. Bail if the load fails cause that is fatal. if (s_userVector.empty() && !load_create_user_accounts()) { return false; } - // Attempt to read the cache file from SD. If not, load application records and create the cache. - if (!read_cache_file()) + // If the cacheWas nuked, the map is empty or we fail to read the cache file... + if ((clearCache || s_titleInfoMap.empty()) && !read_cache_file()) { + // Clear the map out first. + s_titleInfoMap.clear(); + // Load the application records. load_application_records(); } + // I'm just going to assume this is implied since we're reloading everything. + // Loop through the users and clear their save data info. + for (auto &[accountID, user] : s_userVector) + { + user.clear_save_info(); + } + // Load the save data. load_save_data_info(); @@ -130,10 +161,10 @@ void data::get_title_info_by_type(FsSaveDataType saveType, std::vector(saveInfo.uid.uid[0])); + + // Create a new user using the id and hex name. + s_userVector.push_back(std::make_pair( + saveInfo.uid, + data::User(accountID, idHex, idHex, static_cast(saveInfo.save_data_type)))); + + // To do: Maybe check this and continue on failure? I hate nested ifs hard. + if ((user = find_user_by_id(accountID)) == s_userVector.end()) + { + continue; + } } // System saves have no application ID. @@ -298,6 +342,33 @@ static void load_save_data_info(void) } } + // If the include device save option is toggled. + if (config::get_by_key(config::keys::INCLUDE_DEVICE_SAVES)) + { + // Grab the device user. + auto deviceUser = find_user_by_id(ID_DEVICE_USER); + // Grab a reference to the vector. + data::UserSaveInfoList &deviceList = deviceUser->second.get_user_save_info_list(); + + for (auto &[accountID, user] : s_userVector) + { + // Break at the device user. It should be the first that's a system type user. + if (accountID == ID_DEVICE_USER) + { + break; + } + + // Loop through the device list and add it to the target user. + for (data::UserDataEntry &entry : deviceList) + { + // To do: Final decision on this. It's easier to read than second.first... + auto &[saveInfo, playStats] = entry.second; + + user.add_data(saveInfo, playStats); + } + } + } + // Sort save info according to config. for (auto &[id, user] : s_userVector) { @@ -388,4 +459,12 @@ static void create_cache_file(void) // Write the count. cache.write(&titleCount, sizeof(unsigned int)); + // fslib::File cleans up on destruction. +} + +static inline std::vector::iterator find_user_by_id(AccountUid id) +{ + return std::find_if(s_userVector.begin(), s_userVector.end(), [id](const UserIDPair &pair) { + return pair.second.get_account_id() == id; + }); }