mirror of
https://github.com/J-D-K/JKSV.git
synced 2026-08-28 21:44:10 -05:00
Data loading tweaks, fixes, and finishing touches.
This commit is contained in:
2
Makefile
2
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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user