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 0444086..0000000 Binary files a/romfs/Textures/BCAT.png and /dev/null differ diff --git a/romfs/Textures/Cache.png b/romfs/Textures/Cache.png deleted file mode 100644 index f7075a3..0000000 Binary files a/romfs/Textures/Cache.png and /dev/null differ diff --git a/romfs/Textures/SystemSaves.png b/romfs/Textures/SystemSaves.png deleted file mode 100644 index 53daa8d..0000000 Binary files a/romfs/Textures/SystemSaves.png and /dev/null differ diff --git a/source/JKSV.cpp b/source/JKSV.cpp index 3dcd6d9..c943c0f 100644 --- a/source/JKSV.cpp +++ b/source/JKSV.cpp @@ -22,9 +22,9 @@ namespace { /// @brief Build month. - constexpr uint8_t BUILD_MON = 5; + constexpr uint8_t BUILD_MON = 6; /// @brief Build day. - constexpr uint8_t BUILD_DAY = 31; + constexpr uint8_t BUILD_DAY = 13; /// @brief Year. constexpr uint16_t BUILD_YEAR = 2025; } // namespace @@ -43,6 +43,9 @@ static bool initialize_service(Result (*function)(Args...), const char *serviceN JKSV::JKSV(void) { + // Start with this. + appletSetCpuBoostMode(ApmCpuBoostMode_FastLoad); + // FsLib ABORT_ON_FAILURE(fslib::initialize()); @@ -139,6 +142,7 @@ JKSV::~JKSV() sdl::text::exit(); sdl::exit(); fslib::exit(); + appletSetCpuBoostMode(ApmCpuBoostMode_Normal); } bool JKSV::is_running(void) const @@ -164,13 +168,17 @@ void JKSV::update(void) void JKSV::render(void) { sdl::frame_begin(colors::CLEAR_COLOR); + // Top and bottom divider lines. sdl::render_line(NULL, 30, 88, 1250, 88, colors::WHITE); sdl::render_line(NULL, 30, 648, 1250, 648, colors::WHITE); + // Icon m_headerIcon->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)