#include "data/User.hpp" #include "colors.hpp" #include "config.hpp" #include "data/data.hpp" #include "fs/save_mount.hpp" #include "gfxutil.hpp" #include "logger.hpp" #include "sdl.hpp" #include "stringutil.hpp" #include #include namespace { /// @brief Font size for rendering text to icons. constexpr int SIZE_ICON_FONT = 50; /// @brief This is the number of FsSaveDataInfo entries to allocate and try to read. constexpr size_t SIZE_SAVE_INFO_BUFFER = 128; // Array of SaveDataSpaceIDs - SaveDataSpaceAll doesn't seem to work as it should... constexpr std::array SAVE_DATA_SPACE_ORDER = {FsSaveDataSpaceId_System, FsSaveDataSpaceId_User, FsSaveDataSpaceId_SdSystem, FsSaveDataSpaceId_Temporary, FsSaveDataSpaceId_SdUser, FsSaveDataSpaceId_SafeMode}; } // namespace // 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) { AccountProfile profile; AccountProfileBase profileBase = {0}; // Whoever named these needs some help. What the hell? Result profileError = accountGetProfile(&profile, m_accountID); Result profileBaseError = accountProfileGet(&profile, NULL, &profileBase); if (R_FAILED(profileError) || R_FAILED(profileBaseError)) { User::create_account(); } else { User::load_account(profile, profileBase); } accountProfileClose(&profile); } data::User::User(AccountUid accountID, std::string_view nickname, std::string_view pathSafeNickname, FsSaveDataType saveType) : m_accountID(accountID), m_saveType(saveType) { // Generate icon. m_icon = gfxutil::create_generic_icon(nickname, 48, colors::DIALOG_BOX, colors::WHITE); // We're just gonna use this for both. std::memcpy(m_nickname, nickname.data(), nickname.length()); std::memcpy(m_pathSafeNickname, pathSafeNickname.data(), pathSafeNickname.length()); } void data::User::add_data(const FsSaveDataInfo *saveInfo, const PdmPlayStatistics *playStats) { uint64_t applicationID = saveInfo->application_id == 0 ? saveInfo->system_save_data_id : saveInfo->application_id; m_userData.push_back(std::make_pair(applicationID, std::make_pair(*saveInfo, *playStats))); } void data::User::clear_data_entries() { m_userData.clear(); } void data::User::erase_data(int index) { m_userData.erase(m_userData.begin() + index); } void data::User::sort_data() { std::sort(m_userData.begin(), m_userData.end(), sort_user_data); } AccountUid data::User::get_account_id() const { return m_accountID; } FsSaveDataType data::User::get_account_save_type() const { return m_saveType; } const char *data::User::get_nickname() const { return m_nickname; } const char *data::User::get_path_safe_nickname() const { return m_pathSafeNickname; } size_t data::User::get_total_data_entries() const { return m_userData.size(); } uint64_t data::User::get_application_id_at(int index) const { if (index < 0 || index >= static_cast(m_userData.size())) { return 0; } return m_userData.at(index).first; } FsSaveDataInfo *data::User::get_save_info_at(int index) { if (index < 0 || index >= static_cast(m_userData.size())) { return nullptr; } return &m_userData.at(index).second.first; } PdmPlayStatistics *data::User::get_play_stats_at(int index) { if (index < 0 || index >= static_cast(m_userData.size())) { return nullptr; } return &m_userData.at(index).second.second; } FsSaveDataInfo *data::User::get_save_info_by_id(uint64_t applicationID) { auto findTitle = std::find_if(m_userData.begin(), m_userData.end(), [applicationID](data::UserDataEntry &entry) { return entry.first == applicationID; }); if (findTitle == m_userData.end()) { return nullptr; } return &findTitle->second.first; } data::UserSaveInfoList &data::User::get_user_save_info_list() { 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) { return entry.first == applicationID; }); if (findTitle == m_userData.end()) { return nullptr; } return &findTitle->second.second; } SDL_Texture *data::User::get_icon() { return m_icon->get(); } sdl::SharedTexture data::User::get_shared_icon() { return m_icon; } void data::User::erase_save_info_by_id(uint64_t applicationID) { auto targetEntry = std::find_if(m_userData.begin(), m_userData.end(), [applicationID](const data::UserDataEntry &entry) { return entry.second.first.application_id == applicationID; }); if (targetEntry == m_userData.end()) { // Do not pass go. Do not collect $200. return; } m_userData.erase(targetEntry); } void data::User::load_user_data() { // Pull these from config quick. bool accountSystemSaves = config::get_by_key(config::keys::LIST_ACCOUNT_SYS_SAVES); bool enforceMountable = config::get_by_key(config::keys::ONLY_LIST_MOUNTABLE); // Clear the vector if there's anything there first. if (!m_userData.empty()) { m_userData.clear(); } // Loop through the save data space IDs. for (int i = 0; i < 6; i++) { // Open the save reader according to the save type of the account. fslib::SaveInfoReader infoReader; if (!User::open_save_info_reader(SAVE_DATA_SPACE_ORDER[i], infoReader)) { continue; } // Loop until the reader can't be read anymore. while (infoReader.read()) { // Grab the count cause I don't remember if the compiler optimizes this away or not. int64_t readCount = infoReader.get_read_count(); // Loop through that too. Loopy loop loop. for (int64_t i = 0; i < readCount; i++) { // Grab the target save data info struct. FsSaveDataInfo &saveInfo = infoReader[i]; // Since system saves have no application ID... uint64_t applicationID = saveInfo.application_id == 0 ? saveInfo.system_save_data_id : saveInfo.application_id; // Make sure we shouldn't filter it. if ((!accountSystemSaves && saveInfo.save_data_type == FsSaveDataType_System && saveInfo.uid != 0) || config::is_blacklisted(applicationID) || (enforceMountable && !fslib::open_save_data_with_save_info(fs::DEFAULT_SAVE_MOUNT, saveInfo))) { // Just skip this stuff. continue; } // Make sure to clean up the mounting. fslib::close_file_system(fs::DEFAULT_SAVE_MOUNT); if (!data::title_exists_in_map(applicationID)) { data::load_title_to_map(applicationID); } // Try to read the play stats. I don't really care if this fails. PdmPlayStatistics playStats = {0}; pdmqryQueryPlayStatisticsByApplicationIdAndUserAccountId(saveInfo.application_id, m_accountID, false, &playStats); User::add_data(&saveInfo, &playStats); } } } // Sort User::sort_data(); } void data::User::load_account(AccountProfile &profile, AccountProfileBase &profileBase) { // Try to load icon. uint32_t iconSize = 0; Result accError = accountProfileGetImageSize(&profile, &iconSize); if (R_FAILED(accError)) { logger::log("Error getting user icon size: 0x%X.", accError); User::create_account(); return; } std::unique_ptr iconBuffer(new unsigned char[iconSize]); accError = accountProfileLoadImage(&profile, iconBuffer.get(), iconSize, &iconSize); if (R_FAILED(accError)) { logger::log("Error loading user icon: 0x%08X.", accError); User::create_account(); return; } // We should be good at this point. m_icon = sdl::TextureManager::create_load_texture(profileBase.nickname, iconBuffer.get(), iconSize); // Memcpy the nickname. std::memcpy(m_nickname, &profileBase.nickname, 0x20); if (!stringutil::sanitize_string_for_path(m_nickname, m_pathSafeNickname, 0x20)) { std::string accountIDString = stringutil::get_formatted_string("Account_%08X", m_accountID.uid[0] & 0xFFFFFFFF); std::memcpy(m_pathSafeNickname, accountIDString.c_str(), accountIDString.length()); } } void data::User::create_account() { // This is needed a lot here. std::string accountIDString = stringutil::get_formatted_string("Acc_%08X", m_accountID.uid[0] & 0xFFFFFFFF); // Create icon m_icon = gfxutil::create_generic_icon(accountIDString, SIZE_ICON_FONT, colors::DIALOG_BOX, colors::WHITE); // Memcpy the id string for both nicknames std::memcpy(m_nickname, accountIDString.c_str(), accountIDString.length()); std::memcpy(m_pathSafeNickname, accountIDString.c_str(), accountIDString.length()); } bool data::User::open_save_info_reader(FsSaveDataSpaceId spaceID, fslib::SaveInfoReader &reader) { switch (m_saveType) { case FsSaveDataType_System: { reader.open(spaceID, m_saveType, SIZE_SAVE_INFO_BUFFER); } break; case FsSaveDataType_Account: { reader.open(spaceID, m_accountID, SIZE_SAVE_INFO_BUFFER); } break; case FsSaveDataType_Bcat: { reader.open(spaceID, m_saveType, SIZE_SAVE_INFO_BUFFER); } break; case FsSaveDataType_Device: { reader.open(spaceID, m_saveType, SIZE_SAVE_INFO_BUFFER); } break; case FsSaveDataType_Cache: { reader.open(spaceID, m_saveType, SIZE_SAVE_INFO_BUFFER); } break; default: { } break; } return reader.is_open(); } 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; }