diff --git a/Makefile b/Makefile index d5c1e89..1a229a7 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 := 08.08.2025 +APP_VERSION := 08.13.2025 ROMFS := romfs ICON := icon.jpg diff --git a/include/StateManager.hpp b/include/StateManager.hpp index ea14b31..40efc6d 100644 --- a/include/StateManager.hpp +++ b/include/StateManager.hpp @@ -35,5 +35,5 @@ class StateManager static StateManager &get_instance(); /// @brief This is the vector that holds the pointers to the states. - static inline std::vector> sm_stateVector; + std::vector> m_stateVector; }; diff --git a/include/appstates/BaseTask.hpp b/include/appstates/BaseTask.hpp index b9be706..49dd9c3 100644 --- a/include/appstates/BaseTask.hpp +++ b/include/appstates/BaseTask.hpp @@ -31,6 +31,9 @@ class BaseTask : public BaseState /// @brief Underlying system task. This needs to be allocated by the derived classes. std::unique_ptr m_task{}; + /// @brief Updates the loading glyph animation. + void update_loading_glyph(); + private: /// @brief This is the current frame of the loading glyph animation. int m_currentFrame{}; @@ -45,6 +48,6 @@ class BaseTask : public BaseState const char *m_popUnableExit{}; /// @brief This array holds the glyphs of the loading sequence. I think it's from the Wii? - static inline std::array sm_glyphArray = + static inline constexpr std::array sm_glyphArray = {"\ue020", "\ue021", "\ue022", "\ue023", "\ue024", "\ue025", "\ue026", "\ue027"}; }; diff --git a/include/appstates/DataLoadingState.hpp b/include/appstates/DataLoadingState.hpp new file mode 100644 index 0000000..4749799 --- /dev/null +++ b/include/appstates/DataLoadingState.hpp @@ -0,0 +1,64 @@ +#pragma once +#include "StateManager.hpp" +#include "appstates/BaseTask.hpp" +#include "sdl.hpp" + +#include +#include + +class DataLoadingState final : public BaseTask +{ + public: + /// @brief This is a definition for functions that are called at destruction. + using DestructFunction = std::function; + + template + DataLoadingState(void (*function)(sys::Task *, Args...), Args... args) + : BaseTask() + { + DataLoadingState::initialize_static_members(); + m_task = std::make_unique(function, std::forward(args)...); + } + + template + static std::shared_ptr create(void (*function)(sys::Task *, Args...), Args... args) + { + return std::make_shared(function, std::forward(args)...); + } + + template + static std::shared_ptr create_and_push(void (*function)(sys::Task *, Args...), Args... args) + { + auto newState = DataLoadingState::create(function, std::forward(args)...); + StateManager::push_state(newState); + return newState; + } + + /// @brief Destructor. Runs all DestructFunctions in the vector. + ~DataLoadingState(); + + /// @brief Update override. + void update() override; + + /// @brief Render override. + void render() override; + + /// @brief Executes the destruct functions passed. + void execute_destructs(); + + /// @brief Adds a function to the vector to be executed upon destruction. + void add_destruct_function(DataLoadingState::DestructFunction function); + + private: + /// @brief X coord of the status text. + int m_statusX{}; + + /// @brief The functions called upon destruction. + std::vector m_destructFunctions{}; + + /// @brief Icon displayed in the center of the screen. + static inline sdl::SharedTexture sm_jksvIcon{}; + + /// @brief Loads the icon if it hasn't been already. + void initialize_static_members(); +}; diff --git a/include/appstates/TaskCallbackState.hpp b/include/appstates/TaskCallbackState.hpp deleted file mode 100644 index 5768552..0000000 --- a/include/appstates/TaskCallbackState.hpp +++ /dev/null @@ -1,36 +0,0 @@ -#pragma once -#include "appstates/BaseTask.hpp" - -#include - -/// @brief This is basically a task state that allows a callback and custom render function to be set. -class TaskCallbackState final : public BaseTask -{ - public: - /// @brief - using CallbackFunction = std::function; - - template - TaskCallbackState(void (*taskFunction)(sys::Task *, Args...), - CallbackFunction callbackFunction, - CallbackFunction renderFunction, - Args... args) - : m_callbackFunction(callbackFunction) - , m_renderFunction(renderFunction) - { - m_task = std::make_unique(taskFunction, std::forward(args)...); - } - - /// @brief Update override. Calls m_callbackFunction. - void update() override; - - /// @brief Render override. Calls m_renderFunction. - void render() override; - - private: - /// @brief Callback that is called on update. - CallbackFunction m_callbackFunction{}; - - /// @brief Callback that is called on render. - CallbackFunction m_renderFunction{}; -}; diff --git a/include/data/DataCommon.hpp b/include/data/DataCommon.hpp new file mode 100644 index 0000000..8b7c242 --- /dev/null +++ b/include/data/DataCommon.hpp @@ -0,0 +1,25 @@ +#pragma once +#include "sdl.hpp" + +namespace data +{ + class DataCommon + { + public: + /// @brief Default + DataCommon() = default; + + /// @brief Function to load the icon to a texture. + virtual void load_icon() = 0; + + /// @brief Returns the icon. + sdl::SharedTexture get_icon() { return m_icon; }; + + /// @brief Sets the icon. + void set_icon(sdl::SharedTexture &icon) { m_icon = icon; }; + + protected: + /// @brief Shared texture of the icon. + sdl::SharedTexture m_icon{}; + }; +} diff --git a/include/data/TitleInfo.hpp b/include/data/TitleInfo.hpp index 73dbada..b868a52 100644 --- a/include/data/TitleInfo.hpp +++ b/include/data/TitleInfo.hpp @@ -1,4 +1,5 @@ #pragma once +#include "data/DataCommon.hpp" #include "sdl.hpp" #include @@ -8,7 +9,7 @@ namespace data { /// @brief Class that holds data related to titles loaded from the system. - class TitleInfo + class TitleInfo final : public data::DataCommon { public: /// @brief Constructs a TitleInfo instance. Loads control data, icon. @@ -18,7 +19,7 @@ namespace data /// @brief Initializes a TitleInfo instance using external (cached) NsApplicationControlData /// @param applicationID Application ID of the title loaded from cache. /// @param controlData Reference to the control data to init from. - TitleInfo(uint64_t applicationID, NsApplicationControlData &controlData); + TitleInfo(uint64_t applicationID, std::unique_ptr &controlData); /// @brief Move constructor and operator. TitleInfo(TitleInfo &&titleInfo); @@ -87,6 +88,9 @@ namespace data /// @param newPathSafe Buffer containing the new safe path to use. void set_path_safe_title(const char *newPathSafe); + /// @brief Loads the icon from the nacp. + void load_icon() override; + private: /// @brief This defines how long the buffer is for the path safe version of the title. static inline constexpr size_t SIZE_PATH_SAFE = 0x200; diff --git a/include/data/User.hpp b/include/data/User.hpp index 8f4f6f6..7fd1142 100644 --- a/include/data/User.hpp +++ b/include/data/User.hpp @@ -1,4 +1,5 @@ #pragma once +#include "data/DataCommon.hpp" #include "fslib.hpp" #include "sdl.hpp" @@ -17,7 +18,7 @@ namespace data using UserSaveInfoList = std::vector; /// @brief Class that stores data for the user. - class User + class User final : public data::DataCommon { public: /// @brief Constructs a new user with accountID @@ -55,14 +56,19 @@ namespace data /// @brief Runs the sort algo on the vector. void sort_data(); + /// @brief Returns the account ID of the user AccountUid get_account_id() const; + /// @brief Returns the primary save data type o FsSaveDataType get_account_save_type() const; + /// @brief Returns the user's full UTF-8 nickname. const char *get_nickname() const; + /// @brief Returns the path safe version of the user's nickname. const char *get_path_safe_nickname() const; + /// @brief Returns the total data entries. size_t get_total_data_entries() const; /// @brief Returns the application ID of the title at index. @@ -77,15 +83,12 @@ namespace data /// @brief Returns a pointer to the save info of applicationID. FsSaveDataInfo *get_save_info_by_id(uint64_t applicationID); + /// @brief Returns a reference to the internal map for range based loops. data::UserSaveInfoList &get_user_save_info_list(); /// @brief Returns a pointer to the play statistics of applicationID PdmPlayStatistics *get_play_stats_by_id(uint64_t applicationID); - SDL_Texture *get_icon(); - - sdl::SharedTexture get_shared_icon(); - /// @brief Erases a UserDataEntry according to the application ID passed. /// @param applicationID ID of the save to erase. void erase_save_info_by_id(uint64_t applicationID); @@ -94,6 +97,9 @@ namespace data /// constructor. void load_user_data(); + /// @brief Loads the icon from the system and converts it to a texture. + void load_icon() override; + private: /// @brief Account's ID AccountUid m_accountID{}; @@ -107,9 +113,6 @@ namespace data /// @brief Path safe version of nickname. char m_pathSafeNickname[0x20]{}; - /// @brief User's icon. - sdl::SharedTexture m_icon{}; - /// @brief Vector containing save info and play statistics. data::UserSaveInfoList m_userData{}; diff --git a/include/data/data.hpp b/include/data/data.hpp index e9a9864..3a0942e 100644 --- a/include/data/data.hpp +++ b/include/data/data.hpp @@ -2,6 +2,7 @@ #include "data/TitleInfo.hpp" #include "data/User.hpp" #include "data/accountUID.hpp" +#include "sys/sys.hpp" #include #include @@ -12,10 +13,10 @@ namespace data using UserList = std::vector; using TitleInfoList = std::vector; - /// @brief Initializes data. Loads user accounts from system and save data info. - /// @param clearCache Whether or not the current cache file should be deleted from the SD card first. - /// @return True on success. False on failure. - bool initialize(bool clearCache); + /// @brief Launches the data loading/initialization state. + /// @param clear Whether or not the cache should be cleared. + /// @param onDestruction Function that is executed upon destruction of the data loading screen. + void launch_initialization(bool clear, std::function onDestruction); /// @brief Writes pointers to users to vectorOut /// @param userList List to push the pointers to. diff --git a/include/sys/CallbackTask.hpp b/include/sys/CallbackTask.hpp deleted file mode 100644 index 1216e02..0000000 --- a/include/sys/CallbackTask.hpp +++ /dev/null @@ -1,33 +0,0 @@ -#pragma once -#include "sys/Task.hpp" - -#include - -namespace sys -{ - class CallbackTask final : public sys::Task - { - public: - template - CallbackTask(void (*function)(sys::CallbackTask *, Args...), Args... args) - { - m_thread = std::thread(function, this, std::forward(args)...); - } - - /// @brief Signals that the main task is finished. - void task_complete(); - - /// @brief - void callback_complete(); - - /// @brief Returns whether both the main task and callback have finished. - bool is_running() const; - - private: - /// @brief Bool to signal whether or not the task thread is completed. - std::atomic m_taskFinished{}; - - /// @brief This signals whether or not the callback is finished.s - bool m_callbackFinished{}; - }; -} diff --git a/include/ui/PopMessage.hpp b/include/ui/PopMessage.hpp index 081ef70..34c7bf8 100644 --- a/include/ui/PopMessage.hpp +++ b/include/ui/PopMessage.hpp @@ -21,6 +21,9 @@ namespace ui /// @brief Returns whether or not the message can be purged. bool finished() const; + /// @brief Returns the text of the message. + std::string_view get_message() const; + private: // Every message begins off screen. static inline constexpr int START_X = 624; diff --git a/romfs/Textures/LoadingIcon.png b/romfs/Textures/LoadingIcon.png new file mode 100644 index 0000000..610d99d Binary files /dev/null and b/romfs/Textures/LoadingIcon.png differ diff --git a/source/JKSV.cpp b/source/JKSV.cpp index 99bc5e8..63e9df9 100644 --- a/source/JKSV.cpp +++ b/source/JKSV.cpp @@ -7,6 +7,7 @@ #include "config.hpp" #include "curl/curl.hpp" #include "data/data.hpp" +#include "error.hpp" #include "fslib.hpp" #include "input.hpp" #include "logger.hpp" @@ -29,7 +30,7 @@ namespace /// @brief Build month. constexpr uint8_t BUILD_MON = 8; /// @brief Build day. - constexpr uint8_t BUILD_DAY = 8; + constexpr uint8_t BUILD_DAY = 13; /// @brief Year. constexpr uint16_t BUILD_YEAR = 2025; } // namespace @@ -46,11 +47,12 @@ static bool initialize_service(Result (*function)(Args...), const char *serviceN return true; } +// Definition at bottom. +static void finish_initialization(); + // This can't really have an initializer list since it sets everything up. JKSV::JKSV() { - const std::time_t beginTime = std::time(nullptr); - appletSetCpuBoostMode(ApmCpuBoostMode_FastLoad); ABORT_ON_FAILURE(JKSV::initialize_services()); ABORT_ON_FAILURE(JKSV::initialize_filesystem()); @@ -72,20 +74,7 @@ JKSV::JKSV() // This needs the config init'd or read to work. JKSV::create_directories(); - // Data loading depends on the config being read or init'd. - ABORT_ON_FAILURE(data::initialize(false)); - - // Push initial main menu state. - auto mainMenu = MainMenuState::create(); - StateManager::push_state(mainMenu); - - // Init drive or webdav. - if (fslib::file_exists(remote::PATH_GOOGLE_DRIVE_CONFIG)) { remote::initialize_google_drive(); } - else if (fslib::file_exists(remote::PATH_WEBDAV_CONFIG)) { remote::initialize_webdav(); } - - const std::time_t endTime = std::time(nullptr); - const double diff = std::difftime(endTime, beginTime); - logger::log("Boot time: %.02f seconds.", diff); + data::launch_initialization(false, finish_initialization); m_isRunning = true; } @@ -214,3 +203,11 @@ void JKSV::exit_services() nsExit(); accountExit(); } + +static void finish_initialization() +{ + MainMenuState::create_and_push(); + + if (fslib::file_exists(remote::PATH_GOOGLE_DRIVE_CONFIG)) { remote::initialize_google_drive(); } + else if (fslib::file_exists(remote::PATH_WEBDAV_CONFIG)) { remote::initialize_webdav(); } +} diff --git a/source/StateManager.cpp b/source/StateManager.cpp index 8fdb940..f7ee0bd 100644 --- a/source/StateManager.cpp +++ b/source/StateManager.cpp @@ -1,69 +1,65 @@ #include "StateManager.hpp" +#include "logger.hpp" + void StateManager::update() { // Grab the instance. StateManager &instance = StateManager::get_instance(); + auto &stateVector = instance.m_stateVector; - if (instance.sm_stateVector.empty()) - { - // Just return. - return; - } + if (stateVector.empty()) { return; } // Purge uneeded states. - for (size_t i = instance.sm_stateVector.size() - 1; i > 0; i--) + for (auto current = stateVector.begin(); current != stateVector.end();) { - // Grab a raw pointer to avoid reference count increase. - BaseState *appState = instance.sm_stateVector.at(i).get(); + BaseState *state = current->get(); - if (!appState->is_active()) + if (!state->is_active()) { - // Take focus first. - appState->take_focus(); - instance.sm_stateVector.erase(instance.sm_stateVector.begin() + i); + state->take_focus(); + current = stateVector.erase(current); + continue; } + ++current; } - // Check if the back has focus. It should always have it. - if (!instance.sm_stateVector.back()->has_focus()) { instance.sm_stateVector.back()->give_focus(); } + if (stateVector.empty()) { return; } - // Only call update on the back. - instance.sm_stateVector.back()->update(); + std::shared_ptr &back = stateVector.back(); + if (!back->has_focus()) { back->give_focus(); } + back->update(); } void StateManager::render() { - // Instance. StateManager &instance = StateManager::get_instance(); + auto &stateVector = instance.m_stateVector; - // Loop and render all states. - for (std::shared_ptr &appState : instance.sm_stateVector) { appState->render(); } + for (std::shared_ptr &state : stateVector) { state->render(); } } bool StateManager::back_is_closable() { - // Instance. StateManager &instance = StateManager::get_instance(); + auto &stateVector = instance.m_stateVector; - // Not too sure how to handle this yet. - if (instance.sm_stateVector.empty()) { return false; } + if (stateVector.empty()) { return true; } - // Just return this. - return instance.sm_stateVector.back()->is_closable(); + std::shared_ptr &state = stateVector.back(); + return state->is_closable(); } void StateManager::push_state(std::shared_ptr newState) { - // Instance. StateManager &instance = StateManager::get_instance(); + auto &stateVector = instance.m_stateVector; - // Take focus from the current back() - if (!instance.sm_stateVector.empty()) { instance.sm_stateVector.back()->take_focus(); } + if (!stateVector.empty()) { stateVector.back()->take_focus(); } // Give the incoming state focus and then push it. newState->give_focus(); - instance.sm_stateVector.push_back(newState); + stateVector.push_back(newState); } StateManager &StateManager::get_instance() diff --git a/source/appstates/BaseTask.cpp b/source/appstates/BaseTask.cpp index ab1841d..dbc68fe 100644 --- a/source/appstates/BaseTask.cpp +++ b/source/appstates/BaseTask.cpp @@ -29,14 +29,18 @@ void BaseTask::update() } else if (plusPressed) { ui::PopMessageManager::push_message(ui::PopMessageManager::DEFAULT_TICKS, m_popUnableExit); } - m_colorMod.update(); - - // Just bail if the timer wasn't triggered yet. - if (!m_frameTimer.is_triggered()) { return; } - if (++m_currentFrame >= 8) { m_currentFrame = 0; } + BaseTask::update_loading_glyph(); } void BaseTask::render_loading_glyph() { sdl::text::render(sdl::Texture::Null, 56, 673, 32, sdl::text::NO_WRAP, m_colorMod, sm_glyphArray[m_currentFrame]); } + +void BaseTask::update_loading_glyph() +{ + m_colorMod.update(); + + if (!m_frameTimer.is_triggered()) { return; } + if (++m_currentFrame % 8 == 0) { m_currentFrame = 0; } +} diff --git a/source/appstates/DataLoadingState.cpp b/source/appstates/DataLoadingState.cpp new file mode 100644 index 0000000..f6f7fe7 --- /dev/null +++ b/source/appstates/DataLoadingState.cpp @@ -0,0 +1,50 @@ +#include "appstates/DataLoadingState.hpp" + +#include "appstates/FadeState.hpp" +#include "colors.hpp" + +namespace +{ + constexpr int SCREEN_CENTER = 640; +} + +DataLoadingState::~DataLoadingState() { DataLoadingState::execute_destructs(); } + +void DataLoadingState::update() +{ + static constexpr int SCREEN_CENTER = 640; + + BaseTask::update(); + const std::string status = m_task->get_status(); + const int statusWidth = sdl::text::get_width(22, status); + m_statusX = SCREEN_CENTER - (statusWidth / 2); +} + +void DataLoadingState::render() +{ + static constexpr int ICON_X_COORD = SCREEN_CENTER - 128; + static constexpr int ICON_Y_COORD = 226; + const std::string status = m_task->get_status(); + + sdl::render_rect_fill(sdl::Texture::Null, 0, 0, 1280, 720, colors::CLEAR_COLOR); + sm_jksvIcon->render(sdl::Texture::Null, ICON_X_COORD, ICON_Y_COORD); + sdl::text::render(sdl::Texture::Null, m_statusX, 673, 22, sdl::text::NO_WRAP, colors::WHITE, status); + BaseTask::render_loading_glyph(); +} + +void DataLoadingState::execute_destructs() +{ + for (auto &function : m_destructFunctions) { function(); } +} + +void DataLoadingState::add_destruct_function(DataLoadingState::DestructFunction function) +{ + m_destructFunctions.push_back(function); +} + +void DataLoadingState::initialize_static_members() +{ + if (sm_jksvIcon) { return; } + + sm_jksvIcon = sdl::TextureManager::create_load_texture("LoadingIcon", "romfs:/Textures/LoadingIcon.png"); +} diff --git a/source/appstates/ExtrasMenuState.cpp b/source/appstates/ExtrasMenuState.cpp index d804641..59d64ef 100644 --- a/source/appstates/ExtrasMenuState.cpp +++ b/source/appstates/ExtrasMenuState.cpp @@ -28,6 +28,9 @@ namespace }; } // namespace +// Definition at bottom. +static void finish_reinitialization(); + ExtrasMenuState::ExtrasMenuState() : m_extrasMenu(32, 8, 1000, 24, 555) , m_renderTarget(sdl::TextureManager::create_load_texture(SECONDARY_TARGET, 1080, 555, SDL_TEXTUREACCESS_TARGET)) @@ -72,19 +75,12 @@ void ExtrasMenuState::initialize_menu() } } -void ExtrasMenuState::reinitialize_data() +void ExtrasMenuState::reinitialize_data() { data::launch_initialization(true, finish_reinitialization); } + +static void finish_reinitialization() { const int popTicks = ui::PopMessageManager::DEFAULT_TICKS; const char *popSuccess = strings::get_by_name(strings::names::EXTRASMENU_POPS, 0); - const char *popFailure = strings::get_by_name(strings::names::EXTRASMENU_POPS, 1); - - // Call data and make in reinit and delete the cache first. - const bool initSuccess = data::initialize(true); - if (!initSuccess) - { - ui::PopMessageManager::push_message(popTicks, popFailure); - return; - } MainMenuState::refresh_view_states(); ui::PopMessageManager::push_message(popTicks, popSuccess); diff --git a/source/appstates/MainMenuState.cpp b/source/appstates/MainMenuState.cpp index 4e2dc75..22a2663 100644 --- a/source/appstates/MainMenuState.cpp +++ b/source/appstates/MainMenuState.cpp @@ -122,7 +122,7 @@ void MainMenuState::initialize_menu() { data::get_users(sm_users); sm_userCount = sm_users.size(); - for (data::User *user : sm_users) { m_mainMenu.add_option(user->get_shared_icon()); } + for (data::User *user : sm_users) { m_mainMenu.add_option(user->get_icon()); } m_mainMenu.add_option(m_settingsIcon); m_mainMenu.add_option(m_extrasIcon); } diff --git a/source/appstates/TaskCallbackState.cpp b/source/appstates/TaskCallbackState.cpp deleted file mode 100644 index f790789..0000000 --- a/source/appstates/TaskCallbackState.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include "appstates/TaskCallbackState.hpp" - -void TaskCallbackState::update() { m_callbackFunction(m_task.get()); } - -void TaskCallbackState::render() { m_renderFunction(m_task.get()); } diff --git a/source/data/TitleInfo.cpp b/source/data/TitleInfo.cpp index 68b0321..1bf213f 100644 --- a/source/data/TitleInfo.cpp +++ b/source/data/TitleInfo.cpp @@ -35,7 +35,6 @@ data::TitleInfo::TitleInfo(uint64_t applicationID) std::memset(data, 0x00, SIZE_CTRL_DATA); std::snprintf(name, TitleInfo::SIZE_PATH_SAFE, "%016lX", m_applicationID); - m_icon = gfxutil::create_generic_icon(appIDHex, 48, colors::DIALOG_DARK, colors::WHITE); TitleInfo::get_create_path_safe_title(); } else if (!getError && !entryError) @@ -44,25 +43,21 @@ data::TitleInfo::TitleInfo(uint64_t applicationID) m_hasData = true; TitleInfo::get_create_path_safe_title(); - m_icon = sdl::TextureManager::create_load_texture(entry->name, data->icon, iconSize); } } // To do: Make this safer... -data::TitleInfo::TitleInfo(uint64_t applicationID, NsApplicationControlData &controlData) +data::TitleInfo::TitleInfo(uint64_t applicationID, std::unique_ptr &controlData) : m_applicationID(applicationID) - , m_data(std::make_unique()) + , m_data(std::move(controlData)) { - NsApplicationControlData *data = m_data.get(); - - std::memcpy(data, &controlData, sizeof(NsApplicationControlData)); + m_hasData = true; NacpLanguageEntry *entry{}; - const bool entryError = error::libnx(nacpGetLanguageEntry(&data->nacp, &entry)); + const bool entryError = error::libnx(nacpGetLanguageEntry(&m_data->nacp, &entry)); if (entryError) { std::snprintf(entry->name, TitleInfo::SIZE_PATH_SAFE, "%016lX", m_applicationID); } TitleInfo::get_create_path_safe_title(); - m_icon = sdl::TextureManager::create_load_texture(entry->name, m_data->icon, sizeof(m_data->icon)); } data::TitleInfo::TitleInfo(data::TitleInfo &&titleInfo) { *this = std::move(titleInfo); } @@ -213,3 +208,20 @@ void data::TitleInfo::get_create_path_safe_title() std::snprintf(m_pathSafeTitle, TitleInfo::SIZE_PATH_SAFE, "%016lX", m_applicationID); } } + +void data::TitleInfo::load_icon() +{ + // This is taken from the NacpStruct. + static constexpr size_t SIZE_ICON = 0x20000; + + if (m_hasData) + { + const std::string textureName = stringutil::get_formatted_string("%016llX", m_applicationID); + m_icon = sdl::TextureManager::create_load_texture(textureName, m_data->icon, SIZE_ICON); + } + else + { + const std::string text = stringutil::get_formatted_string("%04X", m_applicationID & 0xFFFF); + m_icon = gfxutil::create_generic_icon(text, 48, colors::DIALOG_DARK, colors::WHITE); + } +} diff --git a/source/data/User.cpp b/source/data/User.cpp index d34a23a..0b0b1da 100644 --- a/source/data/User.cpp +++ b/source/data/User.cpp @@ -51,7 +51,6 @@ data::User::User(AccountUid accountID, std::string_view nickname, std::string_vi : m_accountID{accountID} , m_saveType{saveType} { - m_icon = gfxutil::create_generic_icon(nickname, 48, colors::DIALOG_DARK, colors::WHITE); std::memcpy(m_nickname, nickname.data(), nickname.length()); std::memcpy(m_pathSafeNickname, pathSafeNickname.data(), pathSafeNickname.length()); } @@ -139,10 +138,6 @@ PdmPlayStatistics *data::User::get_play_stats_by_id(uint64_t applicationID) return &target->second.second; } -SDL_Texture *data::User::get_icon() { return m_icon->get(); } - -sdl::SharedTexture data::User::get_shared_icon() { return m_icon; } - void data::User::erase_save_info_by_id(uint64_t applicationID) { auto target = User::find_title_by_id(applicationID); @@ -207,23 +202,35 @@ void data::User::load_user_data() User::sort_data(); } +void data::User::load_icon() +{ + if (m_saveType == FsSaveDataType_Account) + { + uint32_t iconSize{}; + AccountProfile profile{}; + const bool profileError = error::libnx(accountGetProfile(&profile, m_accountID)); + const bool sizeError = !profileError && error::libnx(accountProfileGetImageSize(&profile, &iconSize)); + if (profileError || sizeError) + { + m_icon = gfxutil::create_generic_icon(m_nickname, SIZE_ICON_FONT, colors::DIALOG_DARK, colors::WHITE); + return; + } + + auto iconBuffer = std::make_unique(iconSize); + const bool loadError = error::libnx(accountProfileLoadImage(&profile, iconBuffer.get(), iconSize, &iconSize)); + if (loadError) { return; } + + accountProfileClose(&profile); + m_icon = sdl::TextureManager::create_load_texture(m_nickname, iconBuffer.get(), iconSize); + } + else { m_icon = gfxutil::create_generic_icon(m_nickname, SIZE_ICON_FONT, colors::DIALOG_DARK, colors::WHITE); } +} + void data::User::load_account(AccountProfile &profile, AccountProfileBase &profileBase) { - // We're going to use this for the icon buffer size since it should be pretty safe to use. - static constexpr size_t SIZE_ICON = sizeof(NsApplicationControlData::icon); static constexpr size_t NICKNAME_BUFFER = 0x20; - uint32_t iconSize{}; - auto iconBuffer = std::make_unique(SIZE_ICON); - const bool loadError = error::libnx(accountProfileLoadImage(&profile, iconBuffer.get(), SIZE_ICON, &iconSize)); - if (loadError) - { - User::create_account(); - return; - } - std::strncpy(m_nickname, profileBase.nickname, NICKNAME_BUFFER); - m_icon = sdl::TextureManager::create_load_texture(profileBase.nickname, iconBuffer.get(), iconSize); const bool sanitizeError = !stringutil::sanitize_string_for_path(m_nickname, m_pathSafeNickname, NICKNAME_BUFFER); if (sanitizeError) @@ -236,7 +243,6 @@ void data::User::load_account(AccountProfile &profile, AccountProfileBase &profi void data::User::create_account() { const std::string idString = stringutil::get_formatted_string("Acc_%04X", m_accountID.uid[0] & 0xFFFF); - m_icon = gfxutil::create_generic_icon(idString, SIZE_ICON_FONT, colors::DIALOG_DARK, colors::WHITE); std::memcpy(m_nickname, idString.c_str(), idString.length()); std::memcpy(m_pathSafeNickname, idString.c_str(), idString.length()); } diff --git a/source/data/data.cpp b/source/data/data.cpp index d184044..d828ea7 100644 --- a/source/data/data.cpp +++ b/source/data/data.cpp @@ -1,37 +1,34 @@ #include "data/data.hpp" +#include "appstates/DataLoadingState.hpp" +#include "appstates/FadeState.hpp" +#include "colors.hpp" #include "config.hpp" #include "error.hpp" #include "fs/fs.hpp" #include "logger.hpp" #include "strings.hpp" #include "stringutil.hpp" +#include "sys/sys.hpp" #include #include -#include #include #include #include #include #include -#include namespace { - // clang-format off - struct CacheEntry - { - uint64_t applicationID{}; - NsApplicationControlData controlData{}; - }; - // clang-format on - /// @brief This contains the user accounts on the system. std::vector s_users{}; // Map of Title info paired with its title/application - std::unordered_map s_titleinfo; + std::unordered_map s_titleinfo{}; + + /// @brief This vector holds pointers to everything that needs icons and processes it all on the main thread at the end. + std::vector s_iconQueue; /// @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.zip"; @@ -43,51 +40,39 @@ namespace constexpr AccountUid ID_CACHE_USER = {FsSaveDataType_Cache}; /// @brief This is for loading the cache. - constexpr size_t SIZE_CTRL_DATA = sizeof(NsApplicationControlData); - constexpr size_t SIZE_CACHE_ENTRY = sizeof(CacheEntry); - constexpr size_t SIZE_SAVE_INFO = sizeof(FsSaveDataInfo); + constexpr size_t SIZE_CTRL_DATA = sizeof(NsApplicationControlData); + constexpr size_t SIZE_SAVE_INFO = sizeof(FsSaveDataInfo); } // namespace -// Declarations here. Definitions at bottom. These should appear in the order called. +/// @brief The main routine for the task to load data. +static void data_initialize_task(sys::Task *task, bool clearCache); + /// @brief Loads users from the system and creates the system users. -static bool load_create_user_accounts(); +static bool load_create_user_accounts(sys::Task *task); /// @brief Loads the application records from NS. -static void load_application_records(); +static void load_application_records(sys::Task *task); /// @brief Imports external SVI(Control Data) files. -static void import_svi_files(); +static void import_svi_files(sys::Task *task); /// @brief Attempts to read the cache file from the SD. /// @return True on success. False on failure. -static bool read_cache_file(); +static bool read_cache_file(sys::Task *task); /// @brief Creates the cache file on the SD card. static void create_cache_file(); -bool data::initialize(bool clearCache) +/// @brief Processes the queues. +static void process_queue(); + +void data::launch_initialization(bool clearCache, std::function onDestruction) { - const fslib::Path cachePath{PATH_CACHE_PATH}; - const bool cacheExists = fslib::file_exists(cachePath); - const bool cacheClearFailed = clearCache && cacheExists && error::fslib(fslib::delete_file(cachePath)); - if (clearCache && cacheClearFailed) { return false; } - - const bool usersLoaded = s_users.empty() && load_create_user_accounts(); - if (!usersLoaded && s_users.empty()) { return false; } - - if (!read_cache_file()) - { - s_titleinfo.clear(); - load_application_records(); - import_svi_files(); - } - - for (data::User &user : s_users) { user.load_user_data(); } - - const bool needsCache = !fslib::file_exists(cachePath); - if (needsCache) { create_cache_file(); } - - return true; + auto loadingState = DataLoadingState::create(data_initialize_task, clearCache); + loadingState->add_destruct_function(process_queue); + loadingState->add_destruct_function(create_cache_file); + loadingState->add_destruct_function(onDestruction); + StateManager::push_state(loadingState); } void data::get_users(data::UserList &userList) @@ -119,12 +104,65 @@ void data::get_title_info_by_type(FsSaveDataType saveType, data::TitleInfoList & } } -static bool load_create_user_accounts() +static void data_initialize_task(sys::Task *task, bool clearCache) { - const char *systemName = strings::get_by_name(strings::names::SAVE_DATA_TYPES, 0); - const char *bcatName = strings::get_by_name(strings::names::SAVE_DATA_TYPES, 2); - const char *deviceName = strings::get_by_name(strings::names::SAVE_DATA_TYPES, 3); - const char *cacheName = strings::get_by_name(strings::names::SAVE_DATA_TYPES, 5); + if (error::is_null(task)) { return; } + const char *statusLoadingUserInfo = "Loading save data information for `%s`..."; + const char *statusFinalizing = "Finalizing. Please be patient..."; + + const fslib::Path &cachePath{PATH_CACHE_PATH}; + bool cacheExists = fslib::file_exists(cachePath); + if (clearCache && cacheExists) + { + error::fslib(fslib::delete_file(cachePath)); + cacheExists = false; + } + + if (s_users.empty()) { load_create_user_accounts(task); } + + const bool cacheRead = cacheExists && read_cache_file(task); + if (!cacheRead) + { + s_titleinfo.clear(); + load_application_records(task); + import_svi_files(task); + } + + { + for (data::User &user : s_users) + { + { + const char *nickname = user.get_nickname(); + const std::string status = stringutil::get_formatted_string(statusLoadingUserInfo, nickname); + task->set_status(status); + } + user.load_user_data(); + } + } + + task->set_status(statusFinalizing); + for (data::User &user : s_users) { s_iconQueue.push_back(&user); } + for (auto &[applicationID, titleInfo] : s_titleinfo) { s_iconQueue.push_back(&titleInfo); } + task->complete(); +} + +static bool load_create_user_accounts(sys::Task *task) +{ + static constexpr const char *systemSafe = "System"; + static constexpr const char *bcatSafe = "BCAT"; + static constexpr const char *deviceSafe = "Device"; + static constexpr const char *cacheSafe = "Cache"; + + if (error::is_null(task)) { return false; } + + const char *statusLoading = "Loading user accounts from system..."; + const char *statusCreating = "Creating system type accounts..."; + const char *systemName = strings::get_by_name(strings::names::SAVE_DATA_TYPES, 0); + const char *bcatName = strings::get_by_name(strings::names::SAVE_DATA_TYPES, 2); + const char *deviceName = strings::get_by_name(strings::names::SAVE_DATA_TYPES, 3); + const char *cacheName = strings::get_by_name(strings::names::SAVE_DATA_TYPES, 5); + + task->set_status(statusLoading); int total{}; AccountUid accounts[8]{}; @@ -132,39 +170,54 @@ static bool load_create_user_accounts() const bool noAccounts = total <= 0; if (accountError || noAccounts) { return false; } - // Loop and load the account data. for (int i = 0; i < total; i++) { s_users.emplace_back(accounts[i], FsSaveDataType_Account); } - s_users.emplace_back(ID_DEVICE_USER, deviceName, "Device", FsSaveDataType_Device); - s_users.emplace_back(ID_BCAT_USER, bcatName, "BCAT", FsSaveDataType_Bcat); - s_users.emplace_back(ID_CACHE_USER, cacheName, "Cache", FsSaveDataType_Cache); - s_users.emplace_back(ID_SYSTEM_USER, systemName, "System", FsSaveDataType_System); + + task->set_status(statusCreating); + s_users.emplace_back(ID_DEVICE_USER, deviceName, deviceSafe, FsSaveDataType_Device); + s_users.emplace_back(ID_BCAT_USER, bcatName, bcatSafe, FsSaveDataType_Bcat); + s_users.emplace_back(ID_CACHE_USER, cacheName, cacheSafe, FsSaveDataType_Cache); + s_users.emplace_back(ID_SYSTEM_USER, systemName, systemSafe, FsSaveDataType_System); return true; } -static void load_application_records() +static void load_application_records(sys::Task *task) { + if (error::is_null(task)) { return; } + int offset{}; int count{}; NsApplicationRecord record{}; + const char *statusLoadingRecords = "Loading %016llX..."; bool listError{}; do { listError = error::libnx(nsListApplicationRecord(&record, 1, offset++, &count)) || count <= 0; if (listError) { break; } + + { + const std::string status = stringutil::get_formatted_string(statusLoadingRecords, record.application_id); + task->set_status(status); + } + s_titleinfo.emplace(record.application_id, record.application_id); } while (!listError); } -static void import_svi_files() +static void import_svi_files(sys::Task *task) { static constexpr size_t SIZE_UINT64 = sizeof(uint64_t); static constexpr size_t SIZE_CTRL_DATA = sizeof(NsApplicationControlData); static constexpr size_t SIZE_SVI = SIZE_UINT64 + SIZE_CTRL_DATA; - const fslib::Path sviPath = config::get_working_directory() / "svi"; + if (error::is_null(task)) { return; } + + const char *statusLoadingSvi = "Loading SVI files from SD..."; + const fslib::Path sviPath = config::get_working_directory() / "svi"; const fslib::Directory sviDir{sviPath}; - if (error::fslib(sviDir)) { return; } + if (error::fslib(sviDir.is_open())) { return; } + + task->set_status(statusLoadingSvi); const int64_t sviCount = sviDir.get_count(); for (int64_t i = 0; i < sviCount; i++) @@ -176,34 +229,40 @@ static void import_svi_files() if (!goodFile) { continue; } uint64_t applicationID{}; - NsApplicationControlData controlData{}; + auto controlData = std::make_unique(); const bool idReadGood = sviFile.read(&applicationID, SIZE_UINT64) == SIZE_UINT64; - const bool dataReadGood = sviFile.read(&controlData, SIZE_CTRL_DATA) == SIZE_CTRL_DATA; + const bool dataReadGood = sviFile.read(controlData.get(), SIZE_CTRL_DATA) == SIZE_CTRL_DATA; if (!idReadGood || !dataReadGood) { continue; } - data::TitleInfo newInfo{applicationID, controlData}; - s_titleinfo.emplace(applicationID, std::move(newInfo)); + data::TitleInfo newTitle{applicationID, controlData}; + s_titleinfo.emplace(applicationID, std::move(newTitle)); } } -static bool read_cache_file() +static bool read_cache_file(sys::Task *task) { + if (error::is_null(task)) { return false; } + + const char *statusLoadingCache = "Loading cache..."; + task->set_status(statusLoadingCache); + fs::MiniUnzip cacheZip{PATH_CACHE_PATH}; if (!cacheZip.is_open()) { return false; } - NsApplicationControlData controlBuffer{}; do { - const bool read = cacheZip.read(&controlBuffer, SIZE_CTRL_DATA) == SIZE_CTRL_DATA; + auto controlBuffer = std::make_unique(); + const bool read = cacheZip.read(controlBuffer.get(), SIZE_CTRL_DATA) == SIZE_CTRL_DATA; if (!read) { continue; } - std::string filename{cacheZip.get_filename()}; + std::string_view filename{cacheZip.get_filename()}; const size_t nameBegin = filename.find_first_not_of('/'); if (nameBegin == filename.npos) { continue; } // This is required in order to get the application ID. filename = filename.substr(nameBegin); const uint64_t applicationID = std::strtoull(filename.data(), nullptr, 16); - data::TitleInfo newInfo{applicationID, controlBuffer}; - s_titleinfo.emplace(applicationID, std::move(newInfo)); + + data::TitleInfo newTitle{applicationID, controlBuffer}; + s_titleinfo.emplace(applicationID, std::move(newTitle)); } while (cacheZip.next_file()); return true; @@ -211,6 +270,10 @@ static bool read_cache_file() static void create_cache_file() { + const fslib::Path cachePath{PATH_CACHE_PATH}; + const bool cacheExists = fslib::file_exists(cachePath); + if (cacheExists) { return; } + fs::MiniZip cacheZip{PATH_CACHE_PATH}; if (!cacheZip.is_open()) { return; } @@ -228,3 +291,9 @@ static void create_cache_file() cacheZip.close_current_file(); } } + +static void process_queue() +{ + for (data::DataCommon *dataCommon : s_iconQueue) { dataCommon->load_icon(); } + s_iconQueue.clear(); +} diff --git a/source/error.cpp b/source/error.cpp index 1442c2f..26c315a 100644 --- a/source/error.cpp +++ b/source/error.cpp @@ -16,7 +16,7 @@ bool error::libnx(Result code, const std::source_location &location) std::string_view file{}, function{}; prep_locations(file, function, location); - logger::log("%s::%s::%u::%u:%X", file.data(), function.data(), location.line(), location.column()); + logger::log("%s::%s::%u::%u:%X", file.data(), function.data(), location.line(), location.column(), code); return true; } diff --git a/source/sys/CallbackTask.cpp b/source/sys/CallbackTask.cpp deleted file mode 100644 index 798abed..0000000 --- a/source/sys/CallbackTask.cpp +++ /dev/null @@ -1,7 +0,0 @@ -#include "sys/CallbackTask.hpp" - -void sys::CallbackTask::task_complete() { m_taskFinished = true; } - -void sys::CallbackTask::callback_complete() { m_callbackFinished = true; } - -bool sys::CallbackTask::is_running() const { return m_taskFinished && m_callbackFinished; } diff --git a/source/sys/Task.cpp b/source/sys/Task.cpp index 3bd94b5..3fe44de 100644 --- a/source/sys/Task.cpp +++ b/source/sys/Task.cpp @@ -1,18 +1,12 @@ #include "sys/Task.hpp" -#include - -namespace -{ - /// @brief Size of buffer for formatting the status string. - constexpr size_t VA_BUFFER_SIZE = 0x1000; -} // namespace +#include "logger.hpp" sys::Task::~Task() { m_thread.join(); } -bool sys::Task::is_running() const { return m_isRunning.load(); } +bool sys::Task::is_running() const { return m_isRunning; } -void sys::Task::complete() { m_isRunning.store(false); } +void sys::Task::complete() { m_isRunning = false; } void sys::Task::set_status(std::string_view status) { diff --git a/source/tasks/savecreate.cpp b/source/tasks/savecreate.cpp index b55de00..c7095a7 100644 --- a/source/tasks/savecreate.cpp +++ b/source/tasks/savecreate.cpp @@ -30,8 +30,8 @@ void tasks::savecreate::create_save_data_for(sys::Task *task, { const std::string popMessage = stringutil::get_formatted_string(popSuccess, title); ui::PopMessageManager::push_message(popTicks, popMessage); + spawningState->refresh_required(); } - spawningState->refresh_required(); task->complete(); } diff --git a/source/ui/PopMessage.cpp b/source/ui/PopMessage.cpp index 403d0b0..39a856e 100644 --- a/source/ui/PopMessage.cpp +++ b/source/ui/PopMessage.cpp @@ -32,6 +32,8 @@ void ui::PopMessage::render() sdl::text::render(sdl::Texture::Null, m_textX, m_y + 5, 22, sdl::text::NO_WRAP, colors::BLACK, message); } +std::string_view ui::PopMessage::get_message() const { return m_message; } + bool ui::PopMessage::finished() const { return m_finished; } void ui::PopMessage::update_y(double targetY) diff --git a/source/ui/PopMessageManager.cpp b/source/ui/PopMessageManager.cpp index a2a3e58..26d11f1 100644 --- a/source/ui/PopMessageManager.cpp +++ b/source/ui/PopMessageManager.cpp @@ -68,9 +68,21 @@ void ui::PopMessageManager::push_message(int displayTicks, std::string_view mess { PopMessageManager &manager = PopMessageManager::get_instance(); std::mutex &queueMutex = manager.m_queueMutex; + std::mutex &messageMutex = manager.m_messageMutex; auto &messageQueue = manager.m_messageQueue; + auto &messages = manager.m_messages; - std::lock_guard queueGuard(queueMutex); + { + std::lock_guard messageGuard{messageMutex}; + if (!messages.empty()) + { + ui::PopMessage &back = messages.back(); + const std::string_view lastMessage = back.get_message(); + if (lastMessage == message) { return; } + } + } + + std::lock_guard queueGuard(queueMutex); auto queuePair = std::make_pair(displayTicks, std::string{message}); messageQueue.push_back(std::move(queuePair)); }