From 62c5097d6d24670a1a59ba10440ab0f7ebf96a9e Mon Sep 17 00:00:00 2001 From: J-D-K Date: Wed, 18 Jun 2025 13:40:26 -0400 Subject: [PATCH] Commit since everything seems like it's working. --- Libraries/FsLib | 2 +- Makefile | 2 +- include/JKSV.hpp | 4 + include/appstates/BackupMenuState.hpp | 22 +++ include/appstates/MainMenuState.hpp | 21 ++- include/appstates/SaveCreateState.hpp | 10 +- include/appstates/TitleInfoState.hpp | 6 + include/appstates/TitleOptionState.hpp | 37 +++- include/appstates/UserOptionState.hpp | 20 +++ include/data/User.hpp | 12 +- include/data/data.hpp | 9 + include/fs/SaveMetaData.hpp | 52 +++--- romfs/Text/ENUS.json | 10 +- romfs/Textures/BCAT.png | Bin 6390 -> 0 bytes romfs/Textures/Cache.png | Bin 5299 -> 0 bytes romfs/Textures/SystemSaves.png | Bin 3737 -> 0 bytes source/JKSV.cpp | 13 +- source/appstates/BackupMenuState.cpp | 172 ++++++++++--------- source/appstates/MainMenuState.cpp | 72 +++++--- source/appstates/SaveCreateState.cpp | 174 +++++++++++-------- source/appstates/SettingsState.cpp | 93 ++++++---- source/appstates/TextTitleSelectState.cpp | 2 +- source/appstates/TitleInfoState.cpp | 59 ++++--- source/appstates/TitleOptionState.cpp | 170 +++++++++++-------- source/appstates/TitleSelectState.cpp | 17 +- source/appstates/UserOptionState.cpp | 113 ++++++++----- source/config.cpp | 14 +- source/data/TitleInfo.cpp | 1 + source/data/User.cpp | 125 +++++++++++++- source/data/data.cpp | 196 ++-------------------- source/fs/SaveMetaData.cpp | 86 +++++----- source/logger.cpp | 11 +- source/ui/TextScroll.cpp | 11 +- source/ui/TitleView.cpp | 16 +- 34 files changed, 928 insertions(+), 624 deletions(-) delete mode 100644 romfs/Textures/BCAT.png delete mode 100644 romfs/Textures/Cache.png delete mode 100644 romfs/Textures/SystemSaves.png diff --git a/Libraries/FsLib b/Libraries/FsLib index fdb613f..bc0bf99 160000 --- a/Libraries/FsLib +++ b/Libraries/FsLib @@ -1 +1 @@ -Subproject commit fdb613fce0a104699a172f59f15c1b45c0b9fa77 +Subproject commit bc0bf99b32ef89a7892093737407977b3e649e79 diff --git a/Makefile b/Makefile index 91c63bf..9df71b3 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 := 05.31.2025 +APP_VERSION := 06.13.2025 ROMFS := romfs ICON := icon.jpg diff --git a/include/JKSV.hpp b/include/JKSV.hpp index e2d5407..9ed6f40 100644 --- a/include/JKSV.hpp +++ b/include/JKSV.hpp @@ -31,12 +31,16 @@ class JKSV private: /// @brief Whether or not initialization was successful and JKSV is still running. bool m_isRunning = false; + /// @brief Whether or not to print the translation credits. bool m_showTranslationInfo = false; + /// @brief JKSV icon in upper left corner. sdl::SharedTexture m_headerIcon = nullptr; + /// @brief Vector of states to update and render. static inline std::vector> sm_stateVector; + /// @brief Purges and updates states in sm_stateVector. static void update_state_vector(void); }; diff --git a/include/appstates/BackupMenuState.hpp b/include/appstates/BackupMenuState.hpp index 4292815..799d2d6 100644 --- a/include/appstates/BackupMenuState.hpp +++ b/include/appstates/BackupMenuState.hpp @@ -34,6 +34,25 @@ class BackupMenuState : public AppState /// @brief Allows a spawned task to tell this class that it wrote save data to the system. void save_data_written(void); + /// @brief Struct used for passing data to functions. + typedef struct + { + /// @brief Pointer to the target user. + data::User *m_user; + + /// @brief Data for the target title. + data::TitleInfo *m_titleInfo; + + /// @brief Path of the target. + fslib::Path m_targetPath; + + /// @brief Journal size for when a commit is needed. + uint64_t m_journalSize; + + /// @brief Pointer to >this spawning state. + BackupMenuState *m_spawningState; + } DataStruct; + private: /// @brief Pointer to current user. data::User *m_user = nullptr; @@ -56,6 +75,9 @@ class BackupMenuState : public AppState /// @brief Variable that saves whether or not the filesystem has data in it. bool m_saveHasData = false; + /// @brief Data struct passed to functions. + std::shared_ptr m_dataStruct; + /// @brief Whether or not anything beyond this point needs to be init'd. Everything here is static and shared by all instances. static inline bool sm_isInitialized = false; diff --git a/include/appstates/MainMenuState.hpp b/include/appstates/MainMenuState.hpp index f094282..ecfc6f8 100644 --- a/include/appstates/MainMenuState.hpp +++ b/include/appstates/MainMenuState.hpp @@ -3,6 +3,7 @@ #include "data/data.hpp" #include "sdl.hpp" #include "ui/IconMenu.hpp" +#include /// @brief The main class MainMenuState : public AppState @@ -20,7 +21,10 @@ class MainMenuState : public AppState /// @brief Renders menu to screen. void render(void) override; - /// @brief This function allows other states to signal to this one to refresh the views on next call to update(); + /// @brief Signals to + static void initialize_view_states(void); + + /// @brief Calls refresh on on view states in the vector. static void refresh_view_states(void); private: @@ -45,12 +49,15 @@ class MainMenuState : public AppState /// @brief X coordinate of the control guide in the bottom right corner. int m_controlGuideX; - /// @brief Vector of pointers to users. - static inline std::vector sm_users; + /// @brief This is the list of user pointers from data. + static inline data::UserList sm_users; - /// @brief Vector of views for each user, settings, and extras. + /// @brief This is the pointer to the settings state. + static inline std::shared_ptr sm_settingsState = nullptr; + + /// @brief This is the pointer to the extras state. + static inline std::shared_ptr sm_extrasState = nullptr; + + /// @brief This is the vector of title selection states. static inline std::vector> sm_states; - - /// @brief For signaling refreshes are needed. - static inline bool sm_refreshNeeded = false; }; diff --git a/include/appstates/SaveCreateState.hpp b/include/appstates/SaveCreateState.hpp index a18a684..be96a72 100644 --- a/include/appstates/SaveCreateState.hpp +++ b/include/appstates/SaveCreateState.hpp @@ -11,9 +11,9 @@ class SaveCreateState : public AppState { public: /// @brief Constructs a new SaveCreateState. - /// @param targetUser The target user to create save data for. + /// @param user The target user to create save data for. /// @param titleSelect The selection view for the user for refreshing and rendering. - SaveCreateState(data::User *targetUser, TitleSelectCommon *titleSelect); + SaveCreateState(data::User *user, TitleSelectCommon *titleSelect); /// @brief Required destructor. ~SaveCreateState() {}; @@ -24,6 +24,9 @@ class SaveCreateState : public AppState /// @brief Runs the render routine. void render(void) override; + /// @brief This signals so data and the view can be refreshed on the next update() to avoid threading shenanigans. + void data_and_view_refresh_required(void); + private: /// @brief Pointer to target user. data::User *m_user; @@ -37,6 +40,9 @@ class SaveCreateState : public AppState /// @brief Vector of pointers to the title info. This allows sorting them alphabetically and other things. std::vector m_titleInfoVector; + /// @brief Whether or not a refresh is required on the next update() call. + bool m_refreshRequired = false; + /// @brief Shared slide panel all instances use. There's no point in allocating a new one every time. static inline std::unique_ptr sm_slidePanel = nullptr; }; diff --git a/include/appstates/TitleInfoState.hpp b/include/appstates/TitleInfoState.hpp index a150ef3..f1296d8 100644 --- a/include/appstates/TitleInfoState.hpp +++ b/include/appstates/TitleInfoState.hpp @@ -46,6 +46,12 @@ class TitleInfoState : public AppState /// @brief This holds the hex save data id of the file on nand. std::string m_saveDataID; + /// @brief This holds the time the game was first played. + std::string m_firstPlayed; + + /// @brief This holds the last played timestamp. + std::string m_lastPlayed; + /// @brief This holds the play time string. std::string m_playTime; diff --git a/include/appstates/TitleOptionState.hpp b/include/appstates/TitleOptionState.hpp index b7c8f9e..f37693c 100644 --- a/include/appstates/TitleOptionState.hpp +++ b/include/appstates/TitleOptionState.hpp @@ -1,5 +1,6 @@ #pragma once #include "appstates/AppState.hpp" +#include "appstates/TitleSelectCommon.hpp" #include "data/data.hpp" #include "ui/Menu.hpp" #include "ui/SlideOutPanel.hpp" @@ -11,7 +12,7 @@ class TitleOptionState : public AppState /// @brief Constructs a new title option state. /// @param user Target user. /// @param titleInfo Target title. - TitleOptionState(data::User *user, data::TitleInfo *titleInfo); + TitleOptionState(data::User *user, data::TitleInfo *titleInfo, TitleSelectCommon *titleSelect); /// @brief Required destructor. ~TitleOptionState() {}; @@ -22,6 +23,28 @@ class TitleOptionState : public AppState /// @brief Runs the render routine. void render(void) override; + /// @brief This function allows tasks to signal to the spawning state to close itself on the next update() call. + void close_on_update(void); + + /// @brief Signals to the main thread that a view refresh is required on the next update() call. + void refresh_required(void); + + /// @brief This is the struct used to pass data to the thread functions. + typedef struct + { + /// @brief Pointer to the target user. + data::User *m_user = nullptr; + + /// @brief The target title's data. + data::TitleInfo *m_titleInfo = nullptr; + + /// @brief Allows tasks to signal deactivation. + TitleOptionState *m_spawningState = nullptr; + + /// @brief The target title select. This is used for updating it. + TitleSelectCommon *m_titleSelect = nullptr; + } DataStruct; + private: /// @brief This is just in case the option should only apply to the current user. data::User *m_user = nullptr; @@ -29,6 +52,18 @@ class TitleOptionState : public AppState /// @brief This is the target title. data::TitleInfo *m_titleInfo = nullptr; + /// @brief Pointer to the title selection being used for updating. + TitleSelectCommon *m_titleSelect; + + /// @brief The struct passed to functions. + std::shared_ptr m_dataStruct; + + /// @brief This holds whether or not the state should deactivate itself on the next update loop. + bool m_exitRequired = false; + + /// @brief This stores whether or a not a refresh is required on the next update(). + bool m_refreshRequired = false; + /// @brief This is so it's known whether or not to initialize the static members of this class. static inline bool sm_initialized = false; diff --git a/include/appstates/UserOptionState.hpp b/include/appstates/UserOptionState.hpp index 3bf4986..1385658 100644 --- a/include/appstates/UserOptionState.hpp +++ b/include/appstates/UserOptionState.hpp @@ -24,6 +24,20 @@ class UserOptionState : public AppState /// @brief Runs the render routine. void render(void) override; + /// @brief Signals to the main update() function that a refresh is needed. + /// @note Like this to prevent threading headaches. + void data_and_view_refresh_required(void); + + /// @brief Struct used for passing data to functions/tasks. + typedef struct + { + /// @brief Pointer to the target user. + data::User *m_user; + + /// @brief Pointer to >this spawning state. + UserOptionState *m_spawningState; + } DataStruct; + private: /// @brief Pointer to the target user. data::User *m_user; @@ -34,6 +48,12 @@ class UserOptionState : public AppState /// @brief Menu that displays the options available. ui::Menu m_userOptionMenu; + /// @brief Shared pointer to pass data to tasks and functions. + std::shared_ptr m_dataStruct; + + /// @brief This allows spawned tasks to signal to the main thread to update the view. + bool m_refreshRequired = false; + /// @brief Slide panel all instances shared. static inline std::unique_ptr m_menuPanel = nullptr; }; diff --git a/include/data/User.hpp b/include/data/User.hpp index b16328b..f92e639 100644 --- a/include/data/User.hpp +++ b/include/data/User.hpp @@ -1,4 +1,5 @@ #pragma once +#include "fslib.hpp" #include "sdl.hpp" #include #include @@ -38,7 +39,7 @@ namespace data void add_data(const FsSaveDataInfo *saveInfo, const PdmPlayStatistics *playStats); /// @brief Clears the user save info vector. - void clear_save_info(void); + void clear_data_entries(void); /// @brief Erases data at index. /// @param index Index of save data info to erase. @@ -108,6 +109,9 @@ namespace data /// @param applicationID ID of the save to erase. void erase_save_info_by_id(uint64_t applicationID); + /// @brief Loads the save data info and play statistics for the current user using the information passed to the constructor. + void load_user_data(void); + private: /// @brief Account's ID AccountUid m_accountID; @@ -134,5 +138,11 @@ namespace data /// @brief Creates a placeholder since something went wrong. void create_account(void); + + /// @brief Opens a save data info reader according to the data passed to the user. + /// @param spaceID The FsSaveDataSpaceId to use when opening the reader. + /// @param reader Reference to the reader to use to open. + /// @note I added this so the load_user_data function would be easier to follow. + bool open_save_info_reader(FsSaveDataSpaceId spaceID, fslib::SaveInfoReader &reader); }; } // namespace data diff --git a/include/data/data.hpp b/include/data/data.hpp index 9dbbb62..997eeac 100644 --- a/include/data/data.hpp +++ b/include/data/data.hpp @@ -28,6 +28,15 @@ namespace data /// @return Reference to TitleInfoMap. std::unordered_map &get_title_info_map(void); + /// @brief Uses the application ID passed to add/load a title to the map. + /// @param applicationID Application/System save data ID to add. + void load_title_to_map(uint64_t applicationID); + + /// @brief Returns if the title with applicationID is already loaded to the map. + /// @param applicationID Application ID of the title to search for. + /// @return True if it has been. False if it hasn't. + bool title_exists_in_map(uint64_t applicationID); + /// @brief Gets a vector of pointers with all titles with saveType. /// @param saveType Save data type to check for. /// @param vectorOut Vector to push pointers to. diff --git a/include/fs/SaveMetaData.hpp b/include/fs/SaveMetaData.hpp index 5e71974..8121f18 100644 --- a/include/fs/SaveMetaData.hpp +++ b/include/fs/SaveMetaData.hpp @@ -9,43 +9,51 @@ namespace fs constexpr uint32_t SAVE_META_MAGIC = 0x56534B4A; /// @brief This is the filename used for the save data meta info. - static constexpr std::string_view NAME_SAVE_META = ".jksv_save_meta.bin"; + static constexpr std::string_view NAME_SAVE_META = ".nx_save_meta.bin"; - /// @brief This struct is for storing the data necessary to restore saves to a different console. - /// @note Some of this data isn't really needed. Just rather be safe than sorry. + /// @brief Save data meta data struct. typedef struct __attribute__((packed)) { - /// @brief Magic. + /// @brief Meta file magic. uint32_t m_magic; - /// @brief Application ID. + /// @brief Meta revision. + uint8_t m_revision; + /// @brief Application ID of the game. uint64_t m_applicationID; + /// @brief User account ID. + AccountUid m_accountID; + /// @brief System save data ID. + uint64_t m_systemSaveID; /// @brief Save data type. - uint8_t m_saveType; - /// @brief Save data rank. - uint8_t m_saveRank; - /// @brief Save data space id. - uint8_t m_saveSpaceID; - /// @brief Base save data size. + uint8_t m_saveDataType; + /// @brief Save data rank + uint8_t m_saveDataRank; + /// @brief Save data index. This only really used for cache saves. + uint16_t m_saveDataIndex; + // The rest of the attribute struct is useless, empty padding that is always 0? + /// @brief Save data owner ID. + uint64_t m_ownerID; + /// @brief Just says timestamp. Not sure what time stamp. + uint64_t m_timestamp; + /// @brief Save Data flags. + uint32_t m_flags; + /// @brief Size of the save data. int64_t m_saveDataSize; - /// @brief Maximum save size "allowed". - int64_t m_saveDataSizeMax; - /// @brief Base journaling size. + /// @brief Save data's journal size. int64_t m_journalSize; - /// @brief Maximum journaling size. - int64_t m_journalSizeMax; - /// @brief Total size of the container upon backup. - int64_t m_totalSaveSize; + /// @brief Commit ID. + uint64_t m_commitID; + // The rest of the struct is useless garbage padding. } SaveMetaData; /// @brief Didn't feel like a whole new file just for this. Fills an fs::SaveMetaData struct using the passed TitleInfo pointer. - /// @param titleInfo TitleInfo instance to use to create the meta. - /// @param info Reference to FsSaveDataInfo struct to use to fill out the meta struct. + /// @param info Pointer to FsSaveDataInfo struct to use to fill out the meta struct. /// @param meta Struct to fill. - void create_save_meta_data(data::TitleInfo *titleInfo, const FsSaveDataInfo *saveInfo, SaveMetaData &meta); + bool fill_save_meta_data(const FsSaveDataInfo *saveInfo, SaveMetaData &meta); /// @brief Processes the save meta data and applies it to the passed saveInfo pointer. /// @param saveInfo FsSaveDataInfo to apply the meta to. /// @param meta Save meta data to apply. - bool process_save_meta_data(const FsSaveDataInfo *saveInfo, SaveMetaData &meta); + bool process_save_meta_data(const FsSaveDataInfo *saveInfo, const SaveMetaData &meta); } // namespace fs diff --git a/romfs/Text/ENUS.json b/romfs/Text/ENUS.json index f58fb49..60c89ef 100644 --- a/romfs/Text/ENUS.json +++ b/romfs/Text/ENUS.json @@ -127,8 +127,8 @@ "Are you sure you want to delete all of the save data for `%s`? This is *PERMANENT* and can't be undone." ], "UserOptionStatus": [ - "Creating save data for `%s`...", - "Deleting save data for `%s`..." + "Creating save data for #%s#...", + "Deleting save data for #%s#..." ], "TitleOptions": [ "Information", @@ -158,13 +158,15 @@ ], "TitleOptionConfirmations": [ "Are you sure you want to add #%s# to your blacklist? Once you do this, it will no longer appear on any title list or selection.", - "Are you sure you would like to delete all of the current save backups for #%s#? This cannot be undone!", + "Are you sure you would like to delete all of the current save backups for #%s#? *This cannot be undone!*", "Are you sure you would like to reset the save data for #%s#? *This will delete the current save data for the title as if it were never run!*", - "Are you sure you want to delete `%s`'s save data for #%s#? This will permanently delete it from the system." + "Are you sure you want to delete `%s`'s save data for #%s#? *This will permanently delete it from the system.*" ], "TitleInfo": [ "App ID: %016lX", "Save ID: %016lx", + "First Played: %x - %X", + "Last Played: %x - %X", "Play Time: %02d:%02d:%02d", "Launches: %i", "Save Type: %s" diff --git a/romfs/Textures/BCAT.png b/romfs/Textures/BCAT.png deleted file mode 100644 index 0444086ed6e9a57dacff1e47b1d37eec3b4295c7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6390 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 diff --git a/romfs/Textures/Cache.png b/romfs/Textures/Cache.png deleted file mode 100644 index f7075a348dffb9bd7da9680d11d40cd7ec25076f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5299 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 BsmTBU diff --git a/romfs/Textures/SystemSaves.png b/romfs/Textures/SystemSaves.png deleted file mode 100644 index 53daa8d980c46cad4099215b6a77ef1d14f7eead..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3737 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 ZOqp-sU*render(NULL, 66, 27); + // "JKSV" sdl::text::render(NULL, 130, 32, 34, sdl::text::NO_TEXT_WRAP, colors::WHITE, "JKSV"); + // Translation info in bottom left. if (m_showTranslationInfo) { @@ -183,6 +191,7 @@ void JKSV::render(void) strings::get_by_name(strings::names::TRANSLATION_INFO, 0), strings::get_by_name(strings::names::TRANSLATION_INFO, 1)); } + // Build date sdl::text::render(NULL, 8, diff --git a/source/appstates/BackupMenuState.cpp b/source/appstates/BackupMenuState.cpp index f60ab5a..1ddae9e 100644 --- a/source/appstates/BackupMenuState.cpp +++ b/source/appstates/BackupMenuState.cpp @@ -23,21 +23,6 @@ namespace constexpr size_t SIZE_BACKUP_NAME_LENGTH = 0x80; } // namespace -// This struct is used to pass data to Restore, Delete, and upload. -struct TargetStruct -{ - // Path of target. - fslib::Path m_targetPath; - // Journal size if commit is needed. - uint64_t m_journalSize; - // User - data::User *m_user; - // Title info - data::TitleInfo *m_titleInfo; - // Spawning state so refresh can be called. - BackupMenuState *m_spawningState = nullptr; -}; - // Declarations here. Definitions after class. // Create new backup in targetPath static void create_new_backup(sys::ProgressTask *task, @@ -46,15 +31,16 @@ static void create_new_backup(sys::ProgressTask *task, fslib::Path targetPath, BackupMenuState *spawningState); // Overwrites and existing backup. -static void overwrite_backup(sys::ProgressTask *task, std::shared_ptr dataStruct); +static void overwrite_backup(sys::ProgressTask *task, std::shared_ptr dataStruct); // Restores a backup and requires confirmation to do so. Takes a shared_ptr to a TargetStruct. -static void restore_backup(sys::ProgressTask *task, std::shared_ptr dataStruct); +static void restore_backup(sys::ProgressTask *task, std::shared_ptr dataStruct); // Deletes a backup and requires confirmation to do so. Takes a shared_ptr to a TargetStruct. -static void delete_backup(sys::Task *task, std::shared_ptr dataStruct); +static void delete_backup(sys::Task *task, std::shared_ptr dataStruct); BackupMenuState::BackupMenuState(data::User *user, data::TitleInfo *titleInfo, FsSaveDataType saveType) : m_user(user), m_titleInfo(titleInfo), m_saveType(saveType), - m_directoryPath(config::get_working_directory() / m_titleInfo->get_path_safe_title()) + m_directoryPath(config::get_working_directory() / m_titleInfo->get_path_safe_title()), + m_dataStruct(std::make_shared()) { if (!sm_isInitialized) { @@ -70,6 +56,12 @@ BackupMenuState::BackupMenuState(data::User *user, data::TitleInfo *titleInfo, F sm_isInitialized = true; } + // Fill this out. Target path is not set here. + m_dataStruct->m_user = m_user; + m_dataStruct->m_titleInfo = m_titleInfo; + m_dataStruct->m_journalSize = m_titleInfo->get_journal_size(m_saveType); + m_dataStruct->m_spawningState = this; + // String for the top of the panel. std::string panelString = stringutil::get_formatted_string("`%s` - %s", m_user->get_nickname(), m_titleInfo->get_title()); @@ -77,9 +69,11 @@ BackupMenuState::BackupMenuState(data::User *user, data::TitleInfo *titleInfo, F // This needs sm_panelWidth or it'd be in the initializer list. m_titleScroll.create(panelString, 22, sm_panelWidth, 8, true, colors::WHITE); + // This is a quick check to make sure the save has something in it before creating empty backups. fslib::Directory saveCheck(fs::DEFAULT_SAVE_ROOT); m_saveHasData = saveCheck.get_count() > 0; + // This just fills out the menu. BackupMenuState::refresh(); } @@ -125,6 +119,7 @@ void BackupMenuState::update(void) { return; } + // Push the task. JKSV::push_state(std::make_shared(create_new_backup, m_user, @@ -146,14 +141,16 @@ void BackupMenuState::update(void) stringutil::get_formatted_string(strings::get_by_name(strings::names::BACKUPMENU_CONFIRMATIONS, 0), m_directoryListing[selected]); - std::shared_ptr dataStruct(new TargetStruct); - dataStruct->m_targetPath = m_directoryPath / m_directoryListing[selected]; + // Set the target path quick. + m_dataStruct->m_targetPath = m_directoryPath / m_directoryListing[selected]; - JKSV::push_state(std::make_shared>( + auto confirm = std::make_shared>( queryString, config::get_by_key(config::keys::HOLD_FOR_OVERWRITE), overwrite_backup, - dataStruct)); + m_dataStruct); + + JKSV::push_state(confirm); } else if (input::button_pressed(HidNpadButton_A) && !m_saveHasData && sm_backupMenu->get_selected() > 0) { @@ -184,44 +181,42 @@ void BackupMenuState::update(void) return; } - std::shared_ptr dataStruct(new TargetStruct); - dataStruct->m_user = m_user; - dataStruct->m_titleInfo = m_titleInfo; - dataStruct->m_targetPath = m_directoryPath / m_directoryListing[selected]; - dataStruct->m_journalSize = m_titleInfo->get_journal_size(m_saveType); - dataStruct->m_spawningState = this; + // Set target path + m_dataStruct->m_targetPath = m_directoryPath / m_directoryListing[selected]; std::string queryString = stringutil::get_formatted_string(strings::get_by_name(strings::names::BACKUPMENU_CONFIRMATIONS, 1), m_directoryListing[selected]); - JKSV::push_state(std::make_shared>( + auto confirm = std::make_shared>( queryString, config::get_by_key(config::keys::HOLD_FOR_RESTORATION), restore_backup, - dataStruct)); + m_dataStruct); + + JKSV::push_state(confirm); } else if (input::button_pressed(HidNpadButton_X) && sm_backupMenu->get_selected() > 0) { // Selected needs to be offset by one to account for New int selected = sm_backupMenu->get_selected() - 1; - // Create struct to pass. - std::shared_ptr dataStruct(new TargetStruct); - dataStruct->m_targetPath = m_directoryPath / m_directoryListing[selected]; - dataStruct->m_spawningState = this; + // Set path quick. + m_dataStruct->m_targetPath = m_directoryPath / m_directoryListing[selected]; // get the string. std::string queryString = stringutil::get_formatted_string(strings::get_by_name(strings::names::BACKUPMENU_CONFIRMATIONS, 2), m_directoryListing[selected]); - // Create/push new state. - JKSV::push_state(std::make_shared>( + auto confirm = std::make_shared>( queryString, config::get_by_key(config::keys::HOLD_FOR_DELETION), delete_backup, - dataStruct)); + m_dataStruct); + + // Create/push new state. + JKSV::push_state(confirm); } else if (input::button_pressed(HidNpadButton_B)) { @@ -318,7 +313,7 @@ static void create_new_backup(sys::ProgressTask *task, // I got tired of typing out the cast. fs::SaveMetaData saveMeta; - fs::create_save_meta_data(titleInfo, saveInfo, saveMeta); + bool hasMeta = fs::fill_save_meta_data(saveInfo, saveMeta); // This extension search is lazy and needs to be revised. if (config::get_by_key(config::keys::EXPORT_TO_ZIP) || std::strstr(targetPath.c_string(), "zip")) @@ -328,31 +323,34 @@ static void create_new_backup(sys::ProgressTask *task, { // To do: Pop up. logger::log("Error opening zip for backup."); + task->finished(); return; } - // Data for save meta. - zip_fileinfo saveMetaInfo; - fs::create_zip_fileinfo(saveMetaInfo); - - // Write meta to zip. - int zipError = zipOpenNewFileInZip64(newBackup, - fs::NAME_SAVE_META.data(), - &saveMetaInfo, - NULL, - 0, - NULL, - 0, - NULL, - Z_DEFLATED, - config::get_by_key(config::keys::ZIP_COMPRESSION_LEVEL), - 0); - if (zipError == ZIP_OK) + if (hasMeta) { - zipWriteInFileInZip(newBackup, &saveMeta, sizeof(fs::SaveMetaData)); - zipCloseFileInZip(newBackup); - } + // Data for save meta. + zip_fileinfo saveMetaInfo; + fs::create_zip_fileinfo(saveMetaInfo); + // Write meta to zip. + int zipError = zipOpenNewFileInZip64(newBackup, + fs::NAME_SAVE_META.data(), + &saveMetaInfo, + NULL, + 0, + NULL, + 0, + NULL, + Z_DEFLATED, + config::get_by_key(config::keys::ZIP_COMPRESSION_LEVEL), + 0); + if (zipError == ZIP_OK) + { + zipWriteInFileInZip(newBackup, &saveMeta, sizeof(fs::SaveMetaData)); + zipCloseFileInZip(newBackup); + } + } fs::copy_directory_to_zip(fs::DEFAULT_SAVE_ROOT, newBackup, task); zipClose(newBackup, NULL); } @@ -361,7 +359,7 @@ static void create_new_backup(sys::ProgressTask *task, { fslib::Path saveMetaPath = targetPath / fs::NAME_SAVE_META; fslib::File saveMetaOut(saveMetaPath, FsOpenMode_Create | FsOpenMode_Write, sizeof(fs::SaveMetaData)); - if (saveMetaOut) + if (saveMetaOut && hasMeta) { saveMetaOut.write(&saveMeta, sizeof(fs::SaveMetaData)); } @@ -375,7 +373,7 @@ static void create_new_backup(sys::ProgressTask *task, task->finished(); } -static void overwrite_backup(sys::ProgressTask *task, std::shared_ptr dataStruct) +static void overwrite_backup(sys::ProgressTask *task, std::shared_ptr dataStruct) { // Might need this later. FsSaveDataInfo *saveInfo = dataStruct->m_user->get_save_info_by_id(dataStruct->m_titleInfo->get_application_id()); @@ -399,31 +397,41 @@ static void overwrite_backup(sys::ProgressTask *task, std::shared_ptrm_titleInfo, saveInfo, meta); + bool hasMeta = fs::fill_save_meta_data(saveInfo, meta); if (std::strcmp("zip", dataStruct->m_targetPath.get_extension())) { zipFile backupZip = zipOpen64(dataStruct->m_targetPath.c_string(), APPEND_STATUS_CREATE); + if (!backupZip) + { + logger::log("Error overwriting backup: Couldn't create new zip!"); + task->finished(); + return; + } + // Need the zip info for the meta. - zip_fileinfo saveMetaInfo; - fs::create_zip_fileinfo(saveMetaInfo); - - int zipError = zipOpenNewFileInZip64(backupZip, - fs::NAME_SAVE_META.data(), - &saveMetaInfo, - NULL, - 0, - NULL, - 0, - NULL, - Z_DEFLATED, - config::get_by_key(config::keys::ZIP_COMPRESSION_LEVEL), - 0); - if (zipError == ZIP_OK) + if (hasMeta) { - zipWriteInFileInZip(backupZip, &meta, sizeof(fs::SaveMetaData)); - zipCloseFileInZip(backupZip); + zip_fileinfo saveMetaInfo; + fs::create_zip_fileinfo(saveMetaInfo); + + int zipError = zipOpenNewFileInZip64(backupZip, + fs::NAME_SAVE_META.data(), + &saveMetaInfo, + NULL, + 0, + NULL, + 0, + NULL, + Z_DEFLATED, + config::get_by_key(config::keys::ZIP_COMPRESSION_LEVEL), + 0); + if (zipError == ZIP_OK) + { + zipWriteInFileInZip(backupZip, &meta, sizeof(fs::SaveMetaData)); + zipCloseFileInZip(backupZip); + } } fs::copy_directory_to_zip(fs::DEFAULT_SAVE_ROOT, backupZip, task); @@ -435,7 +443,7 @@ static void overwrite_backup(sys::ProgressTask *task, std::shared_ptrm_targetPath / fs::NAME_SAVE_META; fslib::File metaFile(metaPath, FsOpenMode_Create | FsOpenMode_Write, sizeof(fs::SaveMetaData)); - if (metaFile) + if (metaFile && hasMeta) { metaFile.write(&meta, sizeof(fs::SaveMetaData)); } @@ -446,7 +454,7 @@ static void overwrite_backup(sys::ProgressTask *task, std::shared_ptrfinished(); } -static void restore_backup(sys::ProgressTask *task, std::shared_ptr dataStruct) +static void restore_backup(sys::ProgressTask *task, std::shared_ptr dataStruct) { // Going to need this later. FsSaveDataInfo *saveInfo = dataStruct->m_user->get_save_info_by_id(dataStruct->m_titleInfo->get_application_id()); @@ -535,7 +543,7 @@ static void restore_backup(sys::ProgressTask *task, std::shared_ptrfinished(); } -static void delete_backup(sys::Task *task, std::shared_ptr dataStruct) +static void delete_backup(sys::Task *task, std::shared_ptr dataStruct) { if (task) { diff --git a/source/appstates/MainMenuState.cpp b/source/appstates/MainMenuState.cpp index 7cf2029..4fff916 100644 --- a/source/appstates/MainMenuState.cpp +++ b/source/appstates/MainMenuState.cpp @@ -14,47 +14,41 @@ #include "strings.hpp" MainMenuState::MainMenuState(void) - : m_renderTarget(sdl::TextureManager::create_load_texture("MainMenuTarget", + : m_renderTarget(sdl::TextureManager::create_load_texture("mainMenuTarget", 200, 555, SDL_TEXTUREACCESS_STATIC | SDL_TEXTUREACCESS_TARGET)), - m_background( - sdl::TextureManager::create_load_texture("MainMenuBackground", "romfs:/Textures/MenuBackground.png")), + m_background(sdl::TextureManager::create_load_texture("mainBackground", "romfs:/Textures/MenuBackground.png")), + m_settingsIcon(sdl::TextureManager::create_load_texture("settingsIcon", "romfs:/Textures/SettingsIcon.png")), + m_extrasIcon(sdl::TextureManager::create_load_texture("extrasIcon", "romfs:/Textures/ExtrasIcon.png")), m_mainMenu(50, 15, 555), m_controlGuide(strings::get_by_name(strings::names::CONTROL_GUIDES, 0)), m_controlGuideX(1220 - sdl::text::get_width(22, m_controlGuide)) { - // Fetch user list. + if (!sm_settingsState || !sm_extrasState) + { + sm_settingsState = std::make_shared(); + sm_extrasState = std::make_shared(); + } + + // Grab the users and setup the main menu. Users shouldn't change. data::get_users(sm_users); - // Loop through add user's icon to menu and create states. - for (size_t i = 0; i < sm_users.size(); i++) + for (data::User *user : sm_users) { - m_mainMenu.add_option(sm_users.at(i)->get_shared_icon()); - - if (config::get_by_key(config::keys::JKSM_TEXT_MODE)) - { - sm_states.push_back(std::make_shared(sm_users.at(i))); - } - else - { - sm_states.push_back(std::make_shared(sm_users.at(i))); - } + m_mainMenu.add_option(user->get_shared_icon()); } - // Add the settings and extras. - sm_states.push_back(std::make_shared()); - sm_states.push_back(std::make_shared()); - // Create icons for the other two. - m_settingsIcon = sdl::TextureManager::create_load_texture("SettingsIcon", "romfs:/Textures/SettingsIcon.png"); - m_extrasIcon = sdl::TextureManager::create_load_texture("ExtrasIcon", "romfs:/Textures/ExtrasIcon.png"); - - // Finally add them to the end. + // Add the last two. m_mainMenu.add_option(m_settingsIcon); m_mainMenu.add_option(m_extrasIcon); + + // Just call this. + MainMenuState::initialize_view_states(); } void MainMenuState::update(void) { + // Update the main menu. m_mainMenu.update(AppState::has_focus()); int selected = m_mainMenu.get_selected(); @@ -97,11 +91,37 @@ void MainMenuState::render(void) } } +void MainMenuState::initialize_view_states(void) +{ + // Constructor should have taken care of the menu and user list. + // Start by clearing the vector. + sm_states.clear(); + + // Grab this from config instead of calling it every loop. + bool textMode = config::get_by_key(config::keys::JKSM_TEXT_MODE); + + // Loop through users. + for (data::User *user : sm_users) + { + if (textMode) + { + sm_states.push_back(std::make_shared(user)); + } + else + { + sm_states.push_back(std::make_shared(user)); + } + } + sm_states.push_back(sm_settingsState); + sm_states.push_back(sm_extrasState); +} + void MainMenuState::refresh_view_states(void) { - for (size_t i = 0; i < sm_users.size(); i++) + // For this, we're only looping through the user states to be extra careful because the last two don't have the refresh() function. + int userCount = sm_users.size(); + for (int i = 0; i < userCount; i++) { - sm_users.at(i)->sort_data(); std::static_pointer_cast(sm_states.at(i))->refresh(); } } diff --git a/source/appstates/SaveCreateState.cpp b/source/appstates/SaveCreateState.cpp index 5373f36..458852c 100644 --- a/source/appstates/SaveCreateState.cpp +++ b/source/appstates/SaveCreateState.cpp @@ -13,7 +13,106 @@ #include #include -// This sorts the vector alphabetically so stuff is easier to find +// Declarations here. Definitions under class. +static void create_save_data(sys::Task *task, + data::User *targetUser, + data::TitleInfo *titleInfo, + SaveCreateState *spawningState); + +// This is the sorting function. +static bool compare_info(data::TitleInfo *infoA, data::TitleInfo *infoB); + +SaveCreateState::SaveCreateState(data::User *user, TitleSelectCommon *titleSelect) + : m_user(user), m_titleSelect(titleSelect), m_saveMenu(8, 8, 624, 22, 720) +{ + // If the panel is null, create it. + if (!sm_slidePanel) + { + // Create panel and menu. + sm_slidePanel = std::make_unique(640, ui::SlideOutPanel::Side::Right); + } + + // Get title info vector and copy titles to menu. + data::get_title_info_by_type(m_user->get_account_save_type(), m_titleInfoVector); + + // Sort it by alpha + std::sort(m_titleInfoVector.begin(), m_titleInfoVector.end(), compare_info); + + for (size_t i = 0; i < m_titleInfoVector.size(); i++) + { + m_saveMenu.add_option(m_titleInfoVector.at(i)->get_title()); + } +} + +void SaveCreateState::update(void) +{ + if (m_refreshRequired) + { + // There's no other way to get the save info so... + m_user->load_user_data(); + // Refresh the view. + m_titleSelect->refresh(); + // No more refresh needed. + m_refreshRequired = false; + } + + m_saveMenu.update(AppState::has_focus()); + sm_slidePanel->update(AppState::has_focus()); + + if (input::button_pressed(HidNpadButton_A)) + { + data::TitleInfo *targetTitle = m_titleInfoVector.at(m_saveMenu.get_selected()); + JKSV::push_state(std::make_shared(create_save_data, m_user, targetTitle, this)); + } + else if (input::button_pressed(HidNpadButton_B)) + { + sm_slidePanel->close(); + } + else if (sm_slidePanel->is_closed()) + { + sm_slidePanel->reset(); + AppState::deactivate(); + } +} + +void SaveCreateState::render(void) +{ + // Clear slide target, render menu, render slide to frame buffer. + sm_slidePanel->clear_target(); + m_saveMenu.render(sm_slidePanel->get_target(), AppState::has_focus()); + sm_slidePanel->render(NULL, AppState::has_focus()); +} + +void SaveCreateState::data_and_view_refresh_required(void) +{ + m_refreshRequired = true; +} + +static void create_save_data(sys::Task *task, + data::User *targetUser, + data::TitleInfo *titleInfo, + SaveCreateState *spawningState) +{ + // Set status. We'll just borrow the string from the other group. + task->set_status(strings::get_by_name(strings::names::USER_OPTION_STATUS, 0), titleInfo->get_title()); + + if (fs::create_save_data_for(targetUser, titleInfo)) + { + ui::PopMessageManager::push_message(ui::PopMessageManager::DEFAULT_MESSAGE_TICKS, + strings::get_by_name(strings::names::POP_MESSAGES_SAVE_CREATE, 0), + titleInfo->get_title()); + } + else + { + ui::PopMessageManager::push_message(ui::PopMessageManager::DEFAULT_MESSAGE_TICKS, + strings::get_by_name(strings::names::POP_MESSAGES_SAVE_CREATE, 1)); + } + + spawningState->data_and_view_refresh_required(); + + task->finished(); +} + static bool compare_info(data::TitleInfo *infoA, data::TitleInfo *infoB) { // Get pointers to both titles. @@ -47,76 +146,3 @@ static bool compare_info(data::TitleInfo *infoA, data::TitleInfo *infoB) } return false; } - -// Declarations here. Definitions under class. -static void create_save_data(sys::Task *task, data::User *targetUser, data::TitleInfo *titleInfo); - -SaveCreateState::SaveCreateState(data::User *targetUser, TitleSelectCommon *titleSelect) - : m_user(targetUser), m_titleSelect(titleSelect), m_saveMenu(8, 8, 624, 22, 720) -{ - // If the panel is null, create it. - if (!sm_slidePanel) - { - // Create panel and menu. - sm_slidePanel = std::make_unique(640, ui::SlideOutPanel::Side::Right); - } - - // Get title info vector and copy titles to menu. - data::get_title_info_by_type(m_user->get_account_save_type(), m_titleInfoVector); - - // Sort it by alpha - std::sort(m_titleInfoVector.begin(), m_titleInfoVector.end(), compare_info); - - for (size_t i = 0; i < m_titleInfoVector.size(); i++) - { - m_saveMenu.add_option(m_titleInfoVector.at(i)->get_title()); - } -} - -void SaveCreateState::update(void) -{ - m_saveMenu.update(AppState::has_focus()); - sm_slidePanel->update(AppState::has_focus()); - - if (input::button_pressed(HidNpadButton_A)) - { - data::TitleInfo *targetTitle = m_titleInfoVector.at(m_saveMenu.get_selected()); - JKSV::push_state(std::make_shared(create_save_data, m_user, targetTitle)); - } - else if (input::button_pressed(HidNpadButton_B)) - { - sm_slidePanel->close(); - } - else if (sm_slidePanel->is_closed()) - { - sm_slidePanel->reset(); - AppState::deactivate(); - } -} - -void SaveCreateState::render(void) -{ - // Clear slide target, render menu, render slide to frame buffer. - sm_slidePanel->clear_target(); - m_saveMenu.render(sm_slidePanel->get_target(), AppState::has_focus()); - sm_slidePanel->render(NULL, AppState::has_focus()); -} - -static void create_save_data(sys::Task *task, data::User *targetUser, data::TitleInfo *titleInfo) -{ - // Set status. We'll just borrow the string from the other group. - task->set_status(strings::get_by_name(strings::names::USER_OPTION_STATUS, 0), titleInfo->get_title()); - - if (fs::create_save_data_for(targetUser, titleInfo)) - { - ui::PopMessageManager::push_message(ui::PopMessageManager::DEFAULT_MESSAGE_TICKS, - strings::get_by_name(strings::names::POP_MESSAGES_SAVE_CREATE, 0), - titleInfo->get_title()); - } - else - { - ui::PopMessageManager::push_message(ui::PopMessageManager::DEFAULT_MESSAGE_TICKS, - strings::get_by_name(strings::names::POP_MESSAGES_SAVE_CREATE, 1)); - } - task->finished(); -} diff --git a/source/appstates/SettingsState.cpp b/source/appstates/SettingsState.cpp index c64c26c..67b73eb 100644 --- a/source/appstates/SettingsState.cpp +++ b/source/appstates/SettingsState.cpp @@ -1,6 +1,8 @@ #include "appstates/SettingsState.hpp" +#include "appstates/MainMenuState.hpp" #include "colors.hpp" #include "config.hpp" +#include "data/data.hpp" #include "fslib.hpp" #include "input.hpp" #include "keyboard.hpp" @@ -26,7 +28,7 @@ SettingsState::SettingsState(void) SDL_TEXTUREACCESS_STATIC | SDL_TEXTUREACCESS_TARGET)), m_controlGuideX(1220 - sdl::text::get_width(22, strings::get_by_name(strings::names::CONTROL_GUIDES, 3))) { - // Loop and allocation the strings and menu options. + // Loop and allocate the strings and menu options. int currentString = 0; const char *settingsString = nullptr; while ((settingsString = strings::get_by_name(strings::names::SETTINGS_MENU, currentString++)) != nullptr) @@ -109,41 +111,76 @@ void SettingsState::update_menu_options(void) void SettingsState::toggle_options(void) { int selected = m_settingsMenu.get_selected(); - // These are just true or false more or less. - if ((selected >= 2 && selected <= 12) || (selected >= 14 && selected <= 17)) + + + switch (selected) { - config::toggle_by_index(selected - 2); - } - else if (selected == 13) - { - // This is the zip compression level. - uint8_t zipLevel = config::get_by_key(config::keys::ZIP_COMPRESSION_LEVEL); - if (++zipLevel > 9) + // Zip level. + case 13: { - zipLevel = 0; + uint8_t zipLevel = config::get_by_key(config::keys::ZIP_COMPRESSION_LEVEL); + if (++zipLevel > 9) + { + zipLevel = 0; + } + config::set_by_key(config::keys::ZIP_COMPRESSION_LEVEL, zipLevel); } - config::set_by_key(config::keys::ZIP_COMPRESSION_LEVEL, zipLevel); - } - else if (selected == 14) - { - // This is the title sorting type. - uint8_t sortType = config::get_by_key(config::keys::TITLE_SORT_TYPE); - if (++sortType > 2) + break; + + // Title sorting. + case 14: { - sortType = 0; + // Change the sorting type. + uint8_t sortType = config::get_by_key(config::keys::TITLE_SORT_TYPE); + if (++sortType >= 3) + { + sortType = 0; + } + config::set_by_key(config::keys::TITLE_SORT_TYPE, sortType); + + // Grab the users and resort their data. + data::UserList list; + data::get_users(list); + for (data::User *user : list) + { + user->sort_data(); + } + + // Main the main menu refresh everything. + MainMenuState::refresh_view_states(); } - config::set_by_key(config::keys::TITLE_SORT_TYPE, sortType); - } - else if (selected == 18) - { - // This is the animation scaling. - double scaling = config::get_animation_scaling(); - if ((scaling += 0.25f) > 4.0f) + break; + + // Text mode. This is handled beyond a toggle. + case 15: { - scaling = 1.0f; + // We're gonna use this to toggle first. + config::toggle_by_index(selected - 2); + + // Need the main state to reinit all the views. + MainMenuState::initialize_view_states(); } - config::set_animation_scaling(scaling); + break; + + // UI transition scaling. + case 18: + { + double scaling = config::get_animation_scaling(); + if ((scaling += 0.25f) > 4.0f) + { + scaling = 1.0f; + } + config::set_animation_scaling(scaling); + } + break; + + default: + { + config::toggle_by_index(selected - 2); + } + break; } + // Toggle the update routine. SettingsState::update_menu_options(); } diff --git a/source/appstates/TextTitleSelectState.cpp b/source/appstates/TextTitleSelectState.cpp index d878ca7..f28d865 100644 --- a/source/appstates/TextTitleSelectState.cpp +++ b/source/appstates/TextTitleSelectState.cpp @@ -29,7 +29,7 @@ void TextTitleSelectState::update(void) if (input::button_pressed(HidNpadButton_Y)) { config::add_remove_favorite(m_user->get_application_id_at(m_titleSelectMenu.get_selected())); - MainMenuState::refresh_view_states(); + TextTitleSelectState::refresh(); } else if (input::button_pressed(HidNpadButton_B)) { diff --git a/source/appstates/TitleInfoState.cpp b/source/appstates/TitleInfoState.cpp index dbb772a..9641cf3 100644 --- a/source/appstates/TitleInfoState.cpp +++ b/source/appstates/TitleInfoState.cpp @@ -4,6 +4,7 @@ #include "sdl.hpp" #include "strings.hpp" #include "stringutil.hpp" +#include namespace { @@ -65,19 +66,29 @@ TitleInfoState::TitleInfoState(data::User *user, data::TitleInfo *titleInfo) m_saveDataID = stringutil::get_formatted_string(strings::get_by_name(strings::names::TITLE_INFO_STRINGS, 1), saveInfo->save_data_id); // This is simple. - m_totalLaunches = stringutil::get_formatted_string(strings::get_by_name(strings::names::TITLE_INFO_STRINGS, 3), + m_totalLaunches = stringutil::get_formatted_string(strings::get_by_name(strings::names::TITLE_INFO_STRINGS, 5), playStats->total_launches); // This should be semi-simple. m_saveDataType = stringutil::get_formatted_string( - strings::get_by_name(strings::names::TITLE_INFO_STRINGS, 4), + strings::get_by_name(strings::names::TITLE_INFO_STRINGS, 6), strings::get_by_name(strings::names::SAVE_DATA_TYPES, saveInfo->save_data_type)); + // Going to "cheat" with the next two. This works, so screw it. + char playBuffer[0x40] = {0}; + std::tm *firstPlayed = std::localtime(reinterpret_cast(&playStats->first_timestamp_user)); + std::strftime(playBuffer, 0x40, strings::get_by_name(strings::names::TITLE_INFO_STRINGS, 2), firstPlayed); + m_firstPlayed.assign(playBuffer); + + std::tm *lastPlayed = std::localtime(reinterpret_cast(&playStats->last_timestamp_user)); + std::strftime(playBuffer, 0x40, strings::get_by_name(strings::names::TITLE_INFO_STRINGS, 3), lastPlayed); + m_lastPlayed.assign(playBuffer); + // Calculate play time. We're going to use non-floating point to truncate the remainders. int64_t seconds = playStats->playtime / static_cast(1e+9); int64_t hours = seconds / 3600; int64_t minutes = (seconds % 3600) / 60; seconds %= 60; - m_playTime = stringutil::get_formatted_string(strings::get_by_name(strings::names::TITLE_INFO_STRINGS, 2), + m_playTime = stringutil::get_formatted_string(strings::get_by_name(strings::names::TITLE_INFO_STRINGS, 4), hours, minutes, seconds); @@ -134,10 +145,10 @@ void TitleInfoState::render(void) // This is our Y rendering coordinate. It's easier to adjust this as needed than change everything. // This starts under the icon. - int y = 304; + int y = 280; // Start by rendering the icon. - m_icon->render_stretched(panelTarget, 80, 8, 320, 320); + m_icon->render_stretched(panelTarget, 88, 8, 304, 304); // Render the textscroll to the target, then target to the panel target. m_titleScroll.render(sm_titleTarget->get(), hasFocus); @@ -147,14 +158,13 @@ void TitleInfoState::render(void) m_publisherScroll.render(sm_publisherTarget->get(), hasFocus); sm_publisherTarget->render(panelTarget, 8, (y += SIZE_VERT_GAP)); - // These are different since the probably won't go outside the bounds of the rectangle. + // Application ID. sdl::render_rect_fill(panelTarget, 8, (y += SIZE_VERT_GAP), SIZE_RECT_WIDTH, SIZE_TEXT_TARGET_HEIGHT, colors::DIALOG_BOX); - // Text needs to be aligned like the scrolling text. sdl::text::render(panelTarget, 16, y + 6, @@ -163,8 +173,7 @@ void TitleInfoState::render(void) colors::WHITE, m_applicationID.c_str()); - - // These are different since the probably won't go outside the bounds of the rectangle. + // Save data ID. sdl::render_rect_fill(panelTarget, 8, (y += SIZE_VERT_GAP), @@ -174,26 +183,40 @@ void TitleInfoState::render(void) // Text needs to be aligned like the scrolling text. sdl::text::render(panelTarget, 16, y + 6, SIZE_FONT, sdl::text::NO_TEXT_WRAP, colors::WHITE, m_saveDataID.c_str()); - - // These are different since the probably won't go outside the bounds of the rectangle. + // First played. sdl::render_rect_fill(panelTarget, 8, (y += SIZE_VERT_GAP), SIZE_RECT_WIDTH, SIZE_TEXT_TARGET_HEIGHT, colors::DIALOG_BOX); - // Text needs to be aligned like the scrolling text. - sdl::text::render(panelTarget, 16, y + 6, SIZE_FONT, sdl::text::NO_TEXT_WRAP, colors::WHITE, m_playTime.c_str()); + sdl::text::render(panelTarget, 16, y + 6, SIZE_FONT, sdl::text::NO_TEXT_WRAP, colors::WHITE, m_firstPlayed.c_str()); - - // These are different since the probably won't go outside the bounds of the rectangle. + // Last played. + sdl::render_rect_fill(panelTarget, + 8, + (y += SIZE_VERT_GAP), + SIZE_RECT_WIDTH, + SIZE_TEXT_TARGET_HEIGHT, + colors::CLEAR_COLOR); + sdl::text::render(panelTarget, 16, y + 6, SIZE_FONT, sdl::text::NO_TEXT_WRAP, colors::WHITE, m_lastPlayed.c_str()); + + // Play time. + sdl::render_rect_fill(panelTarget, + 8, + (y += SIZE_VERT_GAP), + SIZE_RECT_WIDTH, + SIZE_TEXT_TARGET_HEIGHT, + colors::DIALOG_BOX); + sdl::text::render(panelTarget, 16, y + 6, SIZE_FONT, sdl::text::NO_TEXT_WRAP, colors::WHITE, m_playTime.c_str()); + + // Total launches. sdl::render_rect_fill(panelTarget, 8, (y += SIZE_VERT_GAP), SIZE_RECT_WIDTH, SIZE_TEXT_TARGET_HEIGHT, colors::CLEAR_COLOR); - // Text needs to be aligned like the scrolling text. sdl::text::render(panelTarget, 16, y + 6, @@ -202,15 +225,13 @@ void TitleInfoState::render(void) colors::WHITE, m_totalLaunches.c_str()); - - // These are different since the probably won't go outside the bounds of the rectangle. + // Save data type. sdl::render_rect_fill(panelTarget, 8, (y += SIZE_VERT_GAP), SIZE_RECT_WIDTH, SIZE_TEXT_TARGET_HEIGHT, colors::DIALOG_BOX); - // Text needs to be aligned like the scrolling text. sdl::text::render(panelTarget, 16, y + 6, diff --git a/source/appstates/TitleOptionState.cpp b/source/appstates/TitleOptionState.cpp index 4d40907..dc737b6 100644 --- a/source/appstates/TitleOptionState.cpp +++ b/source/appstates/TitleOptionState.cpp @@ -1,5 +1,6 @@ #include "appstates/TitleOptionState.hpp" #include "appstates/ConfirmState.hpp" +#include "appstates/MainMenuState.hpp" #include "appstates/TitleInfoState.hpp" #include "colors.hpp" #include "config.hpp" @@ -34,23 +35,18 @@ namespace static const char *ERROR_RESETTING_SAVE = "Error resetting save data: %s"; } // namespace -// Struct to send data to functions that require confirmation. -typedef struct -{ - data::User *m_user; - data::TitleInfo *m_targetTitle; -} TargetStruct; - // Declarations. Definitions after class. Some of these are only here to be compatible with confirmations. -static void blacklist_title(sys::Task *task, std::shared_ptr dataStruct); +static void blacklist_title(sys::Task *task, std::shared_ptr dataStruct); static void change_output_path(data::TitleInfo *targetTitle); -static void delete_all_backups_for_title(sys::Task *task, std::shared_ptr dataStruct); -static void reset_save_data(sys::Task *task, std::shared_ptr dataStruct); -static void delete_save_data_from_system(sys::Task *task, std::shared_ptr dataStruct); -static void extend_save_data(sys::Task *task, std::shared_ptr dataStruct); +static void delete_all_backups_for_title(sys::Task *task, std::shared_ptr dataStruct); +static void reset_save_data(sys::Task *task, std::shared_ptr dataStruct); +static void delete_save_data_from_system(sys::Task *task, std::shared_ptr dataStruct); +static void extend_save_data(sys::Task *task, std::shared_ptr dataStruct); static void export_svi_file(data::TitleInfo *titleInfo); -TitleOptionState::TitleOptionState(data::User *user, data::TitleInfo *titleInfo) : m_user(user), m_titleInfo(titleInfo) +TitleOptionState::TitleOptionState(data::User *user, data::TitleInfo *titleInfo, TitleSelectCommon *titleSelect) + : m_user(user), m_titleInfo(titleInfo), m_titleSelect(titleSelect), + m_dataStruct(std::make_shared()) { // Create panel if needed. if (!sm_initialized) @@ -70,10 +66,30 @@ TitleOptionState::TitleOptionState(data::User *user, data::TitleInfo *titleInfo) // Only do this once. sm_initialized = true; } + + // Fill this out. + m_dataStruct->m_user = m_user; + m_dataStruct->m_titleInfo = m_titleInfo; + m_dataStruct->m_spawningState = this; + m_dataStruct->m_titleSelect = m_titleSelect; } void TitleOptionState::update(void) { + // This is kind of tricky to handle, because the blacklist function uses both. + if (m_refreshRequired) + { + // Refresh the views. + MainMenuState::refresh_view_states(); + m_refreshRequired = false; + // Return so nothing else happens. Not sure I like this, but w/e. + return; + } + if (m_exitRequired) + { + sm_slidePanel->close(); + } + // Update panel and menu. sm_slidePanel->update(AppState::has_focus()); sm_titleOptionMenu->update(AppState::has_focus()); @@ -96,16 +112,12 @@ void TitleOptionState::update(void) strings::get_by_name(strings::names::TITLE_OPTION_CONFIRMATIONS, 0), m_titleInfo->get_title()); - // Data to send - std::shared_ptr data = std::make_shared(); - data->m_targetTitle = m_titleInfo; - // The actual state. - std::shared_ptr> confirm = - std::make_shared>(confirmString, - false, - blacklist_title, - data); + auto confirm = + std::make_shared>(confirmString, + false, + blacklist_title, + m_dataStruct); // Push JKSV::push_state(confirm); @@ -130,16 +142,12 @@ void TitleOptionState::update(void) strings::get_by_name(strings::names::TITLE_OPTION_CONFIRMATIONS, 1), m_titleInfo->get_title()); - // Data - std::shared_ptr data = std::make_shared(); - data->m_targetTitle = m_titleInfo; - // State. This always requires holding because I hate people complaining to me about how it's my fault they don't read things first. - std::shared_ptr> confirm = - std::make_shared>(confirmString, - true, - delete_all_backups_for_title, - data); + auto confirm = std::make_shared>( + confirmString, + true, + delete_all_backups_for_title, + m_dataStruct); JKSV::push_state(confirm); } @@ -161,16 +169,11 @@ void TitleOptionState::update(void) strings::get_by_name(strings::names::TITLE_OPTION_CONFIRMATIONS, 2), m_titleInfo->get_title()); - // Data - std::shared_ptr data = std::make_shared(); - data->m_user = m_user; - data->m_targetTitle = m_titleInfo; - - std::shared_ptr> confirm = - std::make_shared>(confirmString, - true, - reset_save_data, - data); + auto confirm = + std::make_shared>(confirmString, + true, + reset_save_data, + m_dataStruct); JKSV::push_state(confirm); } @@ -192,17 +195,12 @@ void TitleOptionState::update(void) m_user->get_nickname(), m_titleInfo->get_title()); - // Data - std::shared_ptr data = std::make_shared(); - data->m_user = m_user; - data->m_targetTitle = m_titleInfo; - // Confirmation. - std::shared_ptr> confirm = - std::make_shared>(confirmString, - true, - delete_save_data_from_system, - data); + auto confirm = std::make_shared>( + confirmString, + true, + delete_save_data_from_system, + m_dataStruct); JKSV::push_state(confirm); } @@ -218,13 +216,8 @@ void TitleOptionState::update(void) return; } - // Data - std::shared_ptr data = std::make_shared(); - data->m_user = m_user; - data->m_targetTitle = m_titleInfo; - // State. - JKSV::push_state(std::make_shared(extend_save_data, data)); + JKSV::push_state(std::make_shared(extend_save_data, m_dataStruct)); } break; @@ -250,6 +243,7 @@ void TitleOptionState::update(void) // Reset static members. sm_slidePanel->reset(); sm_titleOptionMenu->set_selected(0); + // Deactivate and allow state to be purged. AppState::deactivate(); } } @@ -261,10 +255,36 @@ void TitleOptionState::render(void) sm_slidePanel->render(NULL, AppState::has_focus()); } -static void blacklist_title(sys::Task *task, std::shared_ptr dataStruct) +void TitleOptionState::close_on_update(void) { + m_exitRequired = true; +} + +void TitleOptionState::refresh_required(void) +{ + m_refreshRequired = true; +} + +static void blacklist_title(sys::Task *task, std::shared_ptr dataStruct) +{ + // Gonna need this a lot. + uint64_t applicationID = dataStruct->m_titleInfo->get_application_id(); + // We're not gonna bother with a status for this. It'll flicker, but be barely noticeable. - config::add_remove_blacklist(dataStruct->m_targetTitle->get_application_id()); + config::add_remove_blacklist(applicationID); + + // Now we need to remove it from all of the users. This doesn't just apply to the active one. + data::UserList userList; + data::get_users(userList); + for (data::User *user : userList) + { + user->erase_save_info_by_id(applicationID); + } + + // This will tell the main thread a refresh is required on the next update call. + dataStruct->m_spawningState->refresh_required(); + dataStruct->m_spawningState->close_on_update(); + task->finished(); } @@ -312,14 +332,14 @@ static void change_output_path(data::TitleInfo *targetTitle) pathBuffer); } -static void delete_all_backups_for_title(sys::Task *task, std::shared_ptr dataStruct) +static void delete_all_backups_for_title(sys::Task *task, std::shared_ptr dataStruct) { // Get the path. - fslib::Path titlePath = config::get_working_directory() / dataStruct->m_targetTitle->get_path_safe_title(); + fslib::Path titlePath = config::get_working_directory() / dataStruct->m_titleInfo->get_path_safe_title(); // Set the status. task->set_status(strings::get_by_name(strings::names::TITLE_OPTION_STATUS, 0), - dataStruct->m_targetTitle->get_title()); + dataStruct->m_titleInfo->get_title()); // Just call this and nuke the folder. if (!fslib::delete_directory_recursively(titlePath)) @@ -331,18 +351,18 @@ static void delete_all_backups_for_title(sys::Task *task, std::shared_ptrm_targetTitle->get_title()); + dataStruct->m_titleInfo->get_title()); } task->finished(); } -static void reset_save_data(sys::Task *task, std::shared_ptr dataStruct) +static void reset_save_data(sys::Task *task, std::shared_ptr dataStruct) { // To do: Make this not as hard to read. // Attempt to mount save. if (!fslib::open_save_data_with_save_info( fs::DEFAULT_SAVE_MOUNT, - *dataStruct->m_user->get_save_info_by_id(dataStruct->m_targetTitle->get_application_id()))) + *dataStruct->m_user->get_save_info_by_id(dataStruct->m_titleInfo->get_application_id()))) { logger::log(ERROR_RESETTING_SAVE, fslib::get_error_string()); ui::PopMessageManager::push_message(ui::PopMessageManager::DEFAULT_MESSAGE_TICKS, @@ -380,15 +400,15 @@ static void reset_save_data(sys::Task *task, std::shared_ptr dataS task->finished(); } -static void delete_save_data_from_system(sys::Task *task, std::shared_ptr dataStruct) +static void delete_save_data_from_system(sys::Task *task, std::shared_ptr dataStruct) { // Set the status in case this takes a little while. task->set_status(strings::get_by_name(strings::names::TITLE_OPTION_STATUS, 2), dataStruct->m_user->get_nickname(), - dataStruct->m_targetTitle->get_title()); + dataStruct->m_titleInfo->get_title()); // Grab the save data info pointer. - uint64_t applicationID = dataStruct->m_targetTitle->get_application_id(); + uint64_t applicationID = dataStruct->m_titleInfo->get_application_id(); FsSaveDataInfo *saveInfo = dataStruct->m_user->get_save_info_by_id(applicationID); if (saveInfo == nullptr) { @@ -407,14 +427,22 @@ static void delete_save_data_from_system(sys::Task *task, std::shared_ptrm_user->erase_save_info_by_id(applicationID); + // Refresh + dataStruct->m_titleSelect->refresh(); + + // Signal to close, because this save is no long valid. + dataStruct->m_spawningState->close_on_update(); + // Done? task->finished(); } -static void extend_save_data(sys::Task *task, std::shared_ptr dataStruct) +static void extend_save_data(sys::Task *task, std::shared_ptr dataStruct) { // This is just to make stuff easier to read. - data::TitleInfo *titleInfo = dataStruct->m_targetTitle; + data::TitleInfo *titleInfo = dataStruct->m_titleInfo; + + // Grab this quick. FsSaveDataInfo *saveInfo = dataStruct->m_user->get_save_info_by_id(titleInfo->get_application_id()); if (!saveInfo) { @@ -427,7 +455,7 @@ static void extend_save_data(sys::Task *task, std::shared_ptr data // Set the status. task->set_status(strings::get_by_name(strings::names::TITLE_OPTION_STATUS, 3), dataStruct->m_user->get_nickname(), - dataStruct->m_targetTitle->get_title()); + dataStruct->m_titleInfo->get_title()); // This is the header string. std::string_view keyboardString = strings::get_by_name(strings::names::KEYBOARD_STRINGS, 8); diff --git a/source/appstates/TitleSelectState.cpp b/source/appstates/TitleSelectState.cpp index d16c60a..9d93566 100644 --- a/source/appstates/TitleSelectState.cpp +++ b/source/appstates/TitleSelectState.cpp @@ -29,6 +29,12 @@ TitleSelectState::TitleSelectState(data::User *user) void TitleSelectState::update(void) { + if (m_user->get_total_data_entries() <= 0) + { + AppState::deactivate(); + return; + } + m_titleView.update(AppState::has_focus()); if (input::button_pressed(HidNpadButton_A)) @@ -58,7 +64,7 @@ void TitleSelectState::update(void) uint64_t applicationID = m_user->get_application_id_at(m_titleView.get_selected()); data::TitleInfo *titleInfo = data::get_title_info_by_id(applicationID); - JKSV::push_state(std::make_shared(m_user, titleInfo)); + JKSV::push_state(std::make_shared(m_user, titleInfo, this)); } else if (input::button_pressed(HidNpadButton_B)) { @@ -68,9 +74,14 @@ void TitleSelectState::update(void) } else if (input::button_pressed(HidNpadButton_Y)) { + // Add/remove favorite flag. config::add_remove_favorite(m_user->get_application_id_at(m_titleView.get_selected())); - // MainMenuState has all the Users and views, so have it refresh. - MainMenuState::refresh_view_states(); + + // Resort the data. + m_user->sort_data(); + + // Refresh the view. + TitleSelectState::refresh(); } } diff --git a/source/appstates/UserOptionState.cpp b/source/appstates/UserOptionState.cpp index abad333..20500a5 100644 --- a/source/appstates/UserOptionState.cpp +++ b/source/appstates/UserOptionState.cpp @@ -1,6 +1,7 @@ #include "appstates/UserOptionState.hpp" #include "JKSV.hpp" #include "appstates/ConfirmState.hpp" +#include "appstates/MainMenuState.hpp" #include "appstates/ProgressState.hpp" #include "appstates/SaveCreateState.hpp" #include "appstates/TaskState.hpp" @@ -27,22 +28,17 @@ namespace }; } // namespace -// Struct to pass data to functions that require it. -typedef struct -{ - data::User *m_user; -} UserStruct; - // Declarations here. Defintions after class. // Backs up all save data for the target user. -static void backup_all_for_user(sys::ProgressTask *task, std::shared_ptr dataStruct); +static void backup_all_for_user(sys::ProgressTask *task, std::shared_ptr dataStruct); // // Creates all save data for the current user. -static void create_all_save_data_for_user(sys::Task *task, std::shared_ptr dataStruct); +static void create_all_save_data_for_user(sys::Task *task, std::shared_ptr dataStruct); // // Deletes all save data from the system for the target user. -static void delete_all_save_data_for_user(sys::Task *task, std::shared_ptr dataStruct); +static void delete_all_save_data_for_user(sys::Task *task, std::shared_ptr dataStruct); UserOptionState::UserOptionState(data::User *user, TitleSelectCommon *titleSelect) - : m_user(user), m_titleSelect(titleSelect), m_userOptionMenu(8, 8, 460, 22, 720) + : m_user(user), m_titleSelect(titleSelect), m_userOptionMenu(8, 8, 460, 22, 720), + m_dataStruct(std::make_shared()) { // Check if panel needs to be created. It's shared by all instances. if (!m_menuPanel) @@ -56,12 +52,25 @@ UserOptionState::UserOptionState(data::User *user, TitleSelectCommon *titleSelec { m_userOptionMenu.add_option(stringutil::get_formatted_string(currentString, m_user->get_nickname())); } + + // Fill this is. + m_dataStruct->m_user = m_user; + m_dataStruct->m_spawningState = this; } void UserOptionState::update(void) { + // Update the main panel. m_menuPanel->update(AppState::has_focus()); + // See if this needs to be done. + if (m_refreshRequired) + { + m_user->load_user_data(); + m_titleSelect->refresh(); + m_refreshRequired = false; + } + if (input::button_pressed(HidNpadButton_A) && m_user->get_account_save_type() != FsSaveDataType_System) { switch (m_userOptionMenu.get_selected()) @@ -73,16 +82,13 @@ void UserOptionState::update(void) stringutil::get_formatted_string(strings::get_by_name(strings::names::USER_OPTION_CONFIRMATIONS, 0), m_user->get_nickname()); - // Data to send if confirmed. - std::shared_ptr dataStruct(new UserStruct); - dataStruct->m_user = m_user; - // State to push auto confirmBackupAll = - std::make_shared>(queryString, - false, - backup_all_for_user, - dataStruct); + std::make_shared>( + queryString, + false, + backup_all_for_user, + m_dataStruct); JKSV::push_state(confirmBackupAll); } @@ -101,14 +107,12 @@ void UserOptionState::update(void) stringutil::get_formatted_string(strings::get_by_name(strings::names::USER_OPTION_CONFIRMATIONS, 1), m_user->get_nickname()); - std::shared_ptr dataStruct(new UserStruct); - dataStruct->m_user = m_user; - auto confirmCreateAll = - std::make_shared>(queryString, - true, - create_all_save_data_for_user, - dataStruct); + std::make_shared>( + queryString, + true, + create_all_save_data_for_user, + m_dataStruct); // Done? JKSV::push_state(confirmCreateAll); @@ -121,14 +125,12 @@ void UserOptionState::update(void) stringutil::get_formatted_string(strings::get_by_name(strings::names::USER_OPTION_CONFIRMATIONS, 2), m_user->get_nickname()); - std::shared_ptr dataStruct(new UserStruct); - dataStruct->m_user = m_user; - auto confirmDeleteAll = - std::make_shared>(queryString, - true, - delete_all_save_data_for_user, - dataStruct); + std::make_shared>( + queryString, + true, + delete_all_save_data_for_user, + m_dataStruct); JKSV::push_state(confirmDeleteAll); } @@ -159,7 +161,12 @@ void UserOptionState::render(void) m_menuPanel->render(NULL, AppState::has_focus()); } -static void backup_all_for_user(sys::ProgressTask *task, std::shared_ptr dataStruct) +void UserOptionState::data_and_view_refresh_required(void) +{ + m_refreshRequired = true; +} + +static void backup_all_for_user(sys::ProgressTask *task, std::shared_ptr dataStruct) { data::User *targetUser = dataStruct->m_user; @@ -235,7 +242,7 @@ static void backup_all_for_user(sys::ProgressTask *task, std::shared_ptrfinished(); } -static void create_all_save_data_for_user(sys::Task *task, std::shared_ptr dataStruct) +static void create_all_save_data_for_user(sys::Task *task, std::shared_ptr dataStruct) { data::User *targetUser = dataStruct->m_user; @@ -255,31 +262,57 @@ static void create_all_save_data_for_user(sys::Task *task, std::shared_ptrm_spawningState->data_and_view_refresh_required(); + task->finished(); } -static void delete_all_save_data_for_user(sys::Task *task, std::shared_ptr dataStruct) +static void delete_all_save_data_for_user(sys::Task *task, std::shared_ptr dataStruct) { + // This just makes things easier to type. data::User *targetUser = dataStruct->m_user; + // This is to keep track of what's deleted. Erasing on every loop throws the vector out of whack. + std::vector applicationIDs; + for (size_t i = 0; i < targetUser->get_total_data_entries(); i++) { // Grab title for title. - const char *target_title = data::get_title_info_by_id(targetUser->get_application_id_at(i))->get_title(); + const char *targetTitle = data::get_title_info_by_id(targetUser->get_application_id_at(i))->get_title(); // Update thread task. - task->set_status(strings::get_by_name(strings::names::USER_OPTION_STATUS, 1), target_title); + task->set_status(strings::get_by_name(strings::names::USER_OPTION_STATUS, 1), targetTitle); - if (!fs::delete_save_data(targetUser->get_save_info_at(i))) + // Grab a pointer quick. + FsSaveDataInfo *saveInfo = targetUser->get_save_info_at(i); + + // We don't want to let people nuke their entire system, basically. + if (saveInfo->save_data_type != FsSaveDataType_System && !fs::delete_save_data(targetUser->get_save_info_at(i))) { ui::PopMessageManager::push_message(ui::PopMessageManager::DEFAULT_MESSAGE_TICKS, strings::get_by_name(strings::names::POP_MESSAGES_SAVE_CREATE, 2)); + continue; } + + // Push the application ID back. + applicationIDs.push_back(saveInfo->application_id); } + + // Loop through the IDs and purge them all. + for (uint64_t &applicationID : applicationIDs) + { + targetUser->erase_save_info_by_id(applicationID); + } + + // Signal the main thread to update~ + dataStruct->m_spawningState->data_and_view_refresh_required(); + task->finished(); } diff --git a/source/config.cpp b/source/config.cpp index 6405ee1..6fb9dbd 100644 --- a/source/config.cpp +++ b/source/config.cpp @@ -137,15 +137,16 @@ void config::reset_to_default(void) s_workingDirectory = PATH_DEFAULT_WORK_DIR; s_configVector.push_back(std::make_pair(config::keys::INCLUDE_DEVICE_SAVES.data(), 0)); s_configVector.push_back(std::make_pair(config::keys::AUTO_BACKUP_ON_RESTORE.data(), 1)); - s_configVector.push_back(std::make_pair(config::keys::AUTO_NAME_BACKUPS.data(), 1)); + s_configVector.push_back(std::make_pair(config::keys::AUTO_NAME_BACKUPS.data(), 0)); s_configVector.push_back(std::make_pair(config::keys::AUTO_UPLOAD.data(), 1)); - s_configVector.push_back(std::make_pair(config::keys::HOLD_FOR_DELETION.data(), 0)); - s_configVector.push_back(std::make_pair(config::keys::HOLD_FOR_RESTORATION.data(), 0)); - s_configVector.push_back(std::make_pair(config::keys::HOLD_FOR_OVERWRITE.data(), 0)); - s_configVector.push_back(std::make_pair(config::keys::ONLY_LIST_MOUNTABLE.data(), 0)); + s_configVector.push_back(std::make_pair(config::keys::HOLD_FOR_DELETION.data(), 1)); + s_configVector.push_back(std::make_pair(config::keys::HOLD_FOR_RESTORATION.data(), 1)); + s_configVector.push_back(std::make_pair(config::keys::HOLD_FOR_OVERWRITE.data(), 1)); + s_configVector.push_back(std::make_pair(config::keys::ONLY_LIST_MOUNTABLE.data(), 1)); s_configVector.push_back(std::make_pair(config::keys::LIST_ACCOUNT_SYS_SAVES.data(), 0)); s_configVector.push_back(std::make_pair(config::keys::ALLOW_WRITING_TO_SYSTEM.data(), 0)); - s_configVector.push_back(std::make_pair(config::keys::EXPORT_TO_ZIP.data(), 0)); + s_configVector.push_back(std::make_pair(config::keys::EXPORT_TO_ZIP.data(), 1)); + s_configVector.push_back(std::make_pair(config::keys::ZIP_COMPRESSION_LEVEL.data(), 6)); s_configVector.push_back(std::make_pair(config::keys::TITLE_SORT_TYPE.data(), 0)); s_configVector.push_back(std::make_pair(config::keys::JKSM_TEXT_MODE.data(), 0)); s_configVector.push_back(std::make_pair(config::keys::FORCE_ENGLISH.data(), 0)); @@ -291,7 +292,6 @@ void config::toggle_by_index(int index) { return; } - s_configVector[index].second = s_configVector[index].second ? 0 : 1; } diff --git a/source/data/TitleInfo.cpp b/source/data/TitleInfo.cpp index dc80d17..bd4c17b 100644 --- a/source/data/TitleInfo.cpp +++ b/source/data/TitleInfo.cpp @@ -316,6 +316,7 @@ bool data::TitleInfo::has_save_data_type(uint8_t saveType) { NacpStruct *nacp = &m_data.nacp; + // I'm not 100% sure this is the best way to test for this. switch (saveType) { case FsSaveDataType_Account: diff --git a/source/data/User.cpp b/source/data/User.cpp index b233d0c..5148ce3 100644 --- a/source/data/User.cpp +++ b/source/data/User.cpp @@ -2,6 +2,7 @@ #include "colors.hpp" #include "config.hpp" #include "data/data.hpp" +#include "fs/save_mount.hpp" #include "gfxutil.hpp" #include "logger.hpp" #include "sdl.hpp" @@ -13,6 +14,17 @@ 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. @@ -58,7 +70,7 @@ 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) +void data::User::clear_data_entries(void) { m_userData.clear(); } @@ -182,6 +194,75 @@ void data::User::erase_save_info_by_id(uint64_t applicationID) m_userData.erase(targetEntry); } +void data::User::load_user_data(void) +{ + // 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. @@ -229,6 +310,48 @@ void data::User::create_account(void) 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. diff --git a/source/data/data.cpp b/source/data/data.cpp index 4140249..55f453b 100644 --- a/source/data/data.cpp +++ b/source/data/data.cpp @@ -32,15 +32,6 @@ namespace /// @brief Path used for cacheing title information since NS got slow on 20.0+ constexpr std::string_view PATH_CACHE_PATH = "sdmc:/config/JKSV/cache.bin"; - // 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_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}; @@ -58,9 +49,6 @@ static void load_application_records(void); /// @brief Imports external SVI(Control Data) files. static void import_svi_files(void); -/// @brief Loads the save data info available from the system. -static void load_save_data_info(void); - /// @brief Attempts to read the cache file from the SD. /// @return True on success. False on failure. static bool read_cache_file(void); @@ -68,11 +56,6 @@ static bool read_cache_file(void); /// @brief Creates the cache file on the SD card. static void create_cache_file(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) { // Convert this to an fslib::Path right off the bat so we don't call the path constructor twice. @@ -103,15 +86,14 @@ bool data::initialize(bool clearCache) // Load these now if needed. import_svi_files(); - // I'm just going to assume this is implied since we're reloading everything. - // Loop through the users and clear their save data info. + // Load the save data. + // data::load_save_data_info(); + // Loop users and make them load their data. for (auto &[accountID, user] : s_userVector) { - user.clear_save_info(); + user.load_user_data(); } - // Load the save data. - load_save_data_info(); // If the cache file doesn't exist at this point, create it. if (!fslib::file_exists(PATH_CACHE_PATH)) @@ -141,9 +123,20 @@ data::TitleInfo *data::get_title_info_by_id(uint64_t applicationID) { return nullptr; } + return &s_titleInfoMap.at(applicationID); } +void data::load_title_to_map(uint64_t applicationID) +{ + s_titleInfoMap.emplace(applicationID, applicationID); +} + +bool data::title_exists_in_map(uint64_t applicationID) +{ + return s_titleInfoMap.find(applicationID) != s_titleInfoMap.end(); +} + std::unordered_map &data::get_title_info_map(void) { return s_titleInfoMap; @@ -168,6 +161,7 @@ static bool load_create_user_accounts(void) { // For saving total accounts found. int total = 0; + // The Switch can only have up to eight user accounts. AccountUid accounts[8] = {0}; @@ -277,157 +271,6 @@ static void import_svi_files(void) } } -static void load_save_data_info(void) -{ - // Grab these here so I don't call the config functions every loop. Config uses a vector instead of map so the calls - // can add up quickly. - bool showAccountSystemSaves = config::get_by_key(config::keys::LIST_ACCOUNT_SYS_SAVES); - bool onlyListMountable = config::get_by_key(config::keys::ONLY_LIST_MOUNTABLE); - - // Outer loop iterates through the save data spaces. - for (int i = 0; i < 7; i++) - { - // Using fslib's save info reader wrapper. - fslib::SaveInfoReader saveReader(SAVE_DATA_SPACE_ORDER[i]); - // If it fails, log and continue the loop. Do not pass go or get 200 Monopoly fun bux. - if (!saveReader) - { - logger::log(fslib::get_error_string()); - continue; - } - - // Inner loop iterates save data info. - while (saveReader.read()) - { - // Grab a reference to the current SaveDataInfo struct. - FsSaveDataInfo &saveInfo = saveReader.get(); - - // System saves have no application ID. - uint64_t applicationID = (saveInfo.save_data_type == FsSaveDataType_System || - saveInfo.save_data_type == FsSaveDataType_SystemBcat) - ? saveInfo.system_save_data_id - : saveInfo.application_id; - - // This will filter out account system saves if desired and unmountable titles. - if ((!showAccountSystemSaves && saveInfo.save_data_type == FsSaveDataType_System && saveInfo.uid != 0) || - (onlyListMountable && !fslib::open_save_data_with_save_info(fs::DEFAULT_SAVE_MOUNT, saveInfo)) || - config::is_blacklisted(applicationID)) - { - continue; - } - // If it made it here, we need to close the file system before we, I mean I forget. - fslib::close_file_system(fs::DEFAULT_SAVE_MOUNT); - - // JKSV uses fake placeholder accounts for system type saves. - AccountUid accountID = {0}; - switch (saveInfo.save_data_type) - { - case FsSaveDataType_Bcat: - { - accountID = ID_BCAT_USER; - } - break; - - case FsSaveDataType_Device: - { - accountID = ID_DEVICE_USER; - } - break; - - case FsSaveDataType_Cache: - { - accountID = ID_CACHE_USER; - } - break; - - default: - { - // Default is just the ID in the save info struct. This should be fine for system saves too. - accountID = saveInfo.uid; - } - break; - } - - // Find the user with the ID we have now. - 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()) - { - // 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(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; - } - } - - // Search the map just to be sure it was loaded previously. This can happen. - if (s_titleInfoMap.find(applicationID) == s_titleInfoMap.end()) - { - s_titleInfoMap.emplace(applicationID, applicationID); - } - - // I feel weird allcating space for this even if it's not used, but whatever. - PdmPlayStatistics stats = {0}; - // This should be an OKish way to filter out system titles... - if (R_FAILED(pdmqryQueryPlayStatisticsByApplicationIdAndUserAccountId(applicationID, - saveInfo.uid, - false, - &stats))) - { - // This isn't fatal. - logger::log("Error getting play stats for title %016llX!", applicationID); - } - - // Finally push it over to the user. - user->second.add_data(&saveInfo, &stats); - } - } - - // 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) - { - user.sort_data(); - } -} - static bool read_cache_file(void) { // Try opening the cache file. @@ -512,10 +355,3 @@ static void create_cache_file(void) 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; - }); -} diff --git a/source/fs/SaveMetaData.cpp b/source/fs/SaveMetaData.cpp index d020c67..63a4da0 100644 --- a/source/fs/SaveMetaData.cpp +++ b/source/fs/SaveMetaData.cpp @@ -11,71 +11,73 @@ namespace constexpr std::string_view STRING_ERROR_TEMPLATE = "Error processing save meta for %016llX: %s"; } // namespace -void fs::create_save_meta_data(data::TitleInfo *titleInfo, const FsSaveDataInfo *saveInfo, fs::SaveMetaData &meta) +bool fs::fill_save_meta_data(const FsSaveDataInfo *saveInfo, fs::SaveMetaData &meta) { - // I'm assuming this is opened and good because we're making a backup. - int64_t containerSize = 0; - if (!fslib::get_device_total_space(fs::DEFAULT_SAVE_ROOT, containerSize)) + // This struct will allow us to fill this all out in one shot. + FsSaveDataExtraData extraData; + if (R_FAILED(fsReadSaveDataFileSystemExtraDataBySaveDataSpaceId( + &extraData, + sizeof(FsSaveDataExtraData), + static_cast(saveInfo->save_data_space_id), + saveInfo->save_data_id))) { - // Log and fall back to file size count. - logger::log("Error getting save container's total size. Defaulting to file size count."); - containerSize = fs::get_directory_total_size(fs::DEFAULT_SAVE_ROOT); + logger::log("Error generating save meta: Failed to read save extra data!"); + return false; } - // Fill out the meta struct. + // Fill the struct. meta = {.m_magic = fs::SAVE_META_MAGIC, - .m_applicationID = titleInfo->get_application_id(), - .m_saveType = saveInfo->save_data_type, - .m_saveRank = saveInfo->save_data_rank, - .m_saveSpaceID = saveInfo->save_data_space_id, - .m_saveDataSize = titleInfo->get_save_data_size(saveInfo->save_data_type), - .m_saveDataSizeMax = titleInfo->get_journal_size_max(saveInfo->save_data_type), - .m_journalSize = titleInfo->get_journal_size(saveInfo->save_data_type), - .m_journalSizeMax = titleInfo->get_journal_size_max(saveInfo->save_data_type), - .m_totalSaveSize = containerSize}; + .m_revision = 0x00, + .m_applicationID = extraData.attr.application_id, + .m_accountID = extraData.attr.uid, + .m_systemSaveID = extraData.attr.system_save_data_id, + .m_saveDataType = extraData.attr.save_data_type, + .m_saveDataRank = extraData.attr.save_data_rank, + .m_saveDataIndex = extraData.attr.save_data_index, + .m_ownerID = extraData.owner_id, + .m_timestamp = extraData.timestamp, + .m_flags = extraData.flags, + .m_saveDataSize = extraData.data_size, + .m_journalSize = extraData.journal_size, + .m_commitID = extraData.commit_id}; + + // Should be good. + return true; } -bool fs::process_save_meta_data(const FsSaveDataInfo *saveInfo, SaveMetaData &meta) +bool fs::process_save_meta_data(const FsSaveDataInfo *saveInfo, const SaveMetaData &meta) { - if (meta.m_magic != SAVE_META_MAGIC || saveInfo->application_id != meta.m_applicationID) + // We're going to grab this quick and use this to compare. + FsSaveDataExtraData extraData = {0}; + if (R_FAILED(fsReadSaveDataFileSystemExtraDataBySaveDataSpaceId( + &extraData, + sizeof(FsSaveDataExtraData), + static_cast(saveInfo->save_data_space_id), + saveInfo->save_data_id))) { - logger::log(STRING_ERROR_TEMPLATE.data(), meta.m_applicationID, "Invalid magic or mismatched application ID."); + logger::log(STRING_ERROR_TEMPLATE.data(), saveInfo->application_id, fslib::get_error_string()); return false; } - // To do: I'm assuming this function will only be called once the save container is already opened here... - int64_t totalSpace = 0; - if (!fslib::get_device_total_space(fs::DEFAULT_SAVE_ROOT, totalSpace)) - { - logger::log(STRING_ERROR_TEMPLATE.data(), meta.m_applicationID, "get_device_total_space"); - return false; - } - - if (totalSpace >= meta.m_totalSaveSize) - { - // Gonna return true here, because there's no need to do anything to the container. - return true; - } - - // First we need to temporarily close the save. + // We need to temporarily close the file system. if (!fslib::close_file_system(fs::DEFAULT_SAVE_MOUNT)) { - logger::log(STRING_ERROR_TEMPLATE.data(), meta.m_applicationID, "close_file_system"); - // We can't go any further at this point. You can't extend the save while it's open. + logger::log(STRING_ERROR_TEMPLATE.data(), saveInfo->application_id, fslib::get_error_string()); return false; } - // This is where we finally extend the container. Using the large of the two journal sizes - if (!fs::extend_save_data(saveInfo, meta.m_totalSaveSize, meta.m_journalSizeMax)) + // To do: Other checks. + if (extraData.data_size < meta.m_saveDataSize && + !fs::extend_save_data(saveInfo, meta.m_saveDataSize, meta.m_journalSize)) { - logger::log(STRING_ERROR_TEMPLATE.data(), meta.m_applicationID, "Error extending save data container."); + // The fs::extend_save_data function should log the error that occurred. return false; } - // If this fails, we're super screwed. + // Now reopen it. if (!fslib::open_save_data_with_save_info(fs::DEFAULT_SAVE_MOUNT, *saveInfo)) { - logger::log(STRING_ERROR_TEMPLATE.data(), meta.m_applicationID, "open_save_data"); + logger::log(STRING_ERROR_TEMPLATE.data(), saveInfo->application_id, fslib::get_error_string()); return false; } diff --git a/source/logger.cpp b/source/logger.cpp index aa02ae1..a0beba2 100644 --- a/source/logger.cpp +++ b/source/logger.cpp @@ -5,16 +5,19 @@ namespace { - // Path to log file. + /// @brief This is the path to the log file. fslib::Path s_logFilePath; - // Size of va buffer for log. + + /// @brief This is the buffer size for log strings. constexpr size_t VA_BUFFER_SIZE = 0x1000; } // namespace void logger::initialize(void) { // Create log path and empty the log for this run. - s_logFilePath = "sdmc:/switch/JKSV.log"; + s_logFilePath = "sdmc:/config/JKSV/JKSV.log"; + + // Just opening it like this to nuke and restart. fslib::File LogFile(s_logFilePath, FsOpenMode_Create | FsOpenMode_Write); } @@ -29,5 +32,7 @@ void logger::log(const char *format, ...) fslib::File logFile(s_logFilePath, FsOpenMode_Append); logFile << vaBuffer << "\n"; + + // Always flush to guarantee output. logFile.flush(); } diff --git a/source/ui/TextScroll.cpp b/source/ui/TextScroll.cpp index b965664..18cc5b6 100644 --- a/source/ui/TextScroll.cpp +++ b/source/ui/TextScroll.cpp @@ -1,12 +1,13 @@ #include "ui/TextScroll.hpp" #include "sdl.hpp" -#include "logger.hpp" - namespace { /// @brief This is the number of ticks needed before the text starts scrolling. constexpr uint64_t TICKS_SCROLL_TRIGGER = 3000; + + /// @brief This is the number of pixels between the two renderings of the text. + constexpr int SIZE_TEXT_GAP = 0; } // namespace ui::TextScroll::TextScroll(std::string_view text, @@ -61,11 +62,11 @@ void ui::TextScroll::update(bool hasFocus) m_x -= 2; m_textScrollTriggered = true; } - else if (m_textScrollTriggered && m_x > -(m_textWidth + 16)) + else if (m_textScrollTriggered && m_x > -(m_textWidth + SIZE_TEXT_GAP)) { m_x -= 2; } - else if (m_textScrollTriggered && m_x <= -(m_textWidth + 16)) + else if (m_textScrollTriggered && m_x <= -(m_textWidth + SIZE_TEXT_GAP)) { // This will snap the text back to where it was, but the user won't even notice it. It just looks like it's scrolling. m_x = 8; @@ -86,7 +87,7 @@ void ui::TextScroll::render(SDL_Texture *target, bool hasFocus) // We're going to render text twice so it looks like it's scrolling and doesn't end. Ever. sdl::text::render(target, m_x, m_y, m_fontSize, sdl::text::NO_TEXT_WRAP, m_textColor, m_text.c_str()); sdl::text::render(target, - m_x + m_textWidth + 24, + m_x + m_textWidth + 8, m_y, m_fontSize, sdl::text::NO_TEXT_WRAP, diff --git a/source/ui/TitleView.cpp b/source/ui/TitleView.cpp index e1facb8..ce89f1e 100644 --- a/source/ui/TitleView.cpp +++ b/source/ui/TitleView.cpp @@ -100,12 +100,14 @@ void ui::TitleView::render(SDL_Texture *target, bool hasFocus) m_titleTiles.at(i).render(target, tempX, tempY); } } + // Now render the selected title. if (hasFocus) { sdl::render_rect_fill(target, m_selectedX - 23, m_selectedY - 23, 174, 174, colors::CLEAR_COLOR); ui::render_bounding_box(target, m_selectedX - 24, m_selectedY - 24, 176, 176, m_colorMod); } + m_titleTiles.at(m_selected).render(target, m_selectedX, m_selectedY); } @@ -116,14 +118,26 @@ int ui::TitleView::get_selected(void) const void ui::TitleView::refresh(void) { + // Clear the current tiles. m_titleTiles.clear(); - for (size_t i = 0; i < m_user->get_total_data_entries(); i++) + + // Loop through the user's data entries. + int userEntryCount = m_user->get_total_data_entries(); + + for (int i = 0; i < userEntryCount; i++) { // Get pointer to data from user save index I. data::TitleInfo *currentTitleInfo = data::get_title_info_by_id(m_user->get_application_id_at(i)); + // Emplace is faster than push m_titleTiles.emplace_back(config::is_favorite(m_user->get_application_id_at(i)), currentTitleInfo->get_icon()); } + + // Just to be sure. + if (m_selected > 0 && m_selected >= static_cast(m_titleTiles.size())) + { + m_selected = m_titleTiles.size() - 1; + } } void ui::TitleView::reset(void)