Data loading tweaks, fixes, and finishing touches.

This commit is contained in:
J-D-K
2025-05-31 13:39:01 -04:00
parent 34e4cdff87
commit ecbeff1856
6 changed files with 195 additions and 92 deletions

View File

@@ -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

View File

@@ -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<uint64_t, std::pair<FsSaveDataInfo, PdmPlayStatistics>>;
/// @brief Type definition for the user save info/play stats vector.
using UserSaveInfoList = std::vector<UserDataEntry>;
/// @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<UserDataEntry> m_userData;
data::UserSaveInfoList m_userData;
/// @brief Loads account structs from system.
/// @param profile AccountProfile struct to write to.

View File

@@ -10,9 +10,10 @@ namespace data
/// @brief Declaration for user list/user pointer vector.
using UserList = std::vector<data::User *>;
/// @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.

View File

@@ -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;
}

View File

@@ -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<char>::length(titleA);
size_t titleBLength = std::char_traits<char>::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<const uint8_t *>(&titleA[i]));
ssize_t unitCountB = decode_utf8(&codepointB, reinterpret_cast<const uint8_t *>(&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<char>::length(titleA);
size_t titleBLength = std::char_traits<char>::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<const uint8_t *>(&titleA[i]));
ssize_t unitCountB = decode_utf8(&codepointB, reinterpret_cast<const uint8_t *>(&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;
}

View File

@@ -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<UserIDPair>::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<data::Tit
static bool load_create_user_accounts(void)
{
// These are the IDs used for system type account users.
constexpr AccountUid deviceID = {FsSaveDataType_Device};
constexpr AccountUid bcatID = {FsSaveDataType_Bcat};
constexpr AccountUid cacheID = {FsSaveDataType_Cache};
constexpr AccountUid systemID = {FsSaveDataType_System};
static constexpr AccountUid deviceID = {FsSaveDataType_Device};
static constexpr AccountUid bcatID = {FsSaveDataType_Bcat};
static constexpr AccountUid cacheID = {FsSaveDataType_Cache};
static constexpr AccountUid systemID = {FsSaveDataType_System};
// For saving total accounts found.
int total = 0;
@@ -233,38 +264,51 @@ static void load_save_data_info(void)
{
case FsSaveDataType_Bcat:
{
accountID = {FsSaveDataType_Bcat};
accountID = ID_BCAT_USER;
}
break;
case FsSaveDataType_Device:
{
accountID = {FsSaveDataType_Device};
accountID = ID_DEVICE_USER;
}
break;
case FsSaveDataType_Cache:
{
accountID = {FsSaveDataType_Cache};
accountID = ID_CACHE_USER;
}
break;
default:
{
// Default is just the ID in the save info struct.
accountID = saveInfo.uid;
}
break;
}
// Find the user with the ID we have now.
auto user = std::find_if(s_userVector.begin(), s_userVector.end(), [accountID](const UserIDPair &pair) {
return accountID == pair.second.get_account_id();
});
auto user = find_user_by_id(accountID);
// To do: Handle this like old JKSV did. Here we're just being quitters.
if (user == s_userVector.end())
{
continue;
// We're going to do this since we don't have much of a choice here.
// Just use the lowest 16 bits here.
char idHex[32] = {0};
std::snprintf(idHex, 32, "%04X", static_cast<uint16_t>(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<FsSaveDataType>(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<UserIDPair>::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;
});
}