diff --git a/sphaira/CMakeLists.txt b/sphaira/CMakeLists.txt index a115a62..6cb1188 100644 --- a/sphaira/CMakeLists.txt +++ b/sphaira/CMakeLists.txt @@ -83,6 +83,7 @@ add_executable(sphaira source/ftpsrv_helper.cpp source/haze_helper.cpp source/threaded_file_transfer.cpp + source/title_info.cpp source/minizip_helper.cpp source/usb/base.cpp diff --git a/sphaira/include/title_info.hpp b/sphaira/include/title_info.hpp new file mode 100644 index 0000000..8fe9624 --- /dev/null +++ b/sphaira/include/title_info.hpp @@ -0,0 +1,96 @@ +#pragma once + +// #include +// #include +// #include +// #include +#include "fs.hpp" +#include +#include +#include +#include +#include +#include + +namespace sphaira::title { + +constexpr u32 ContentMetaTypeToContentFlag(u8 meta_type) { + if (meta_type & 0x80) { + return 1 << (meta_type - 0x80); + } + + return 0; +} + +enum ContentFlag { + ContentFlag_Application = ContentMetaTypeToContentFlag(NcmContentMetaType_Application), + ContentFlag_Patch = ContentMetaTypeToContentFlag(NcmContentMetaType_Patch), + ContentFlag_AddOnContent = ContentMetaTypeToContentFlag(NcmContentMetaType_AddOnContent), + ContentFlag_DataPatch = ContentMetaTypeToContentFlag(NcmContentMetaType_DataPatch), + + // nca locations where a control.nacp can exist. + ContentFlag_Nacp = ContentFlag_Application | ContentFlag_Patch, + // all of the above. + ContentFlag_All = ContentFlag_Application | ContentFlag_Patch | ContentFlag_AddOnContent | ContentFlag_DataPatch, +}; + +enum class NacpLoadStatus { + // not yet attempted to be loaded. + None, + // started loading. + Progress, + // loaded, ready to parse. + Loaded, + // failed to load, do not attempt to load again! + Error, +}; + +struct ThreadResultData { + u64 id{}; + std::shared_ptr control{}; + u64 jpeg_size{}; + NacpLanguageEntry lang{}; + NacpLoadStatus status{NacpLoadStatus::None}; +}; + +using MetaEntries = std::vector; + +// starts background thread. +Result Init(); +void Exit(); + +// adds new entry to queue. +void Push(u64 app_id); +// adds array of entries to queue. +void Push(std::span app_ids); + +#if 0 +// removes entry from the queue into out. +void Pop(u64 app_id, std::vector& out); +// removes array of entries from the queue into out. +void Pop(std::span app_ids, std::vector& out); +// removes all entries from the queue into out. +void Pop(std::vector& out); +#endif + +// gets entry without removing it from the queue. +auto Get(u64 app_id) -> std::optional; +// gets array of entries without removing it from the queue. +void Get(std::span app_ids, std::vector& out); + +auto GetNcmCs(u8 storage_id) -> NcmContentStorage&; +auto GetNcmDb(u8 storage_id) -> NcmContentMetaDatabase&; + +// gets all meta entries for an id. +Result GetMetaEntries(u64 id, MetaEntries& out, u32 flags = ContentFlag_All); + +// returns the nca path of a control nca. +Result GetControlPathFromStatus(const NsApplicationContentMetaStatus& status, u64* out_program_id, fs::FsPath* out_path); + +// single threaded title info fetch. +auto LoadControlEntry(u64 id, bool* cached = nullptr) -> ThreadResultData; + +// taken from nxdumptool. +void utilsReplaceIllegalCharacters(char *str, bool ascii_only); + +} // namespace sphaira::title diff --git a/sphaira/include/ui/menus/game_menu.hpp b/sphaira/include/ui/menus/game_menu.hpp index 81c0ea5..fcc31c6 100644 --- a/sphaira/include/ui/menus/game_menu.hpp +++ b/sphaira/include/ui/menus/game_menu.hpp @@ -2,6 +2,7 @@ #include "ui/menus/grid_menu_base.hpp" #include "ui/list.hpp" +#include "title_info.hpp" #include "fs.hpp" #include "option.hpp" #include @@ -10,17 +11,6 @@ namespace sphaira::ui::menu::game { -enum class NacpLoadStatus { - // not yet attempted to be loaded. - None, - // started loading. - Progress, - // loaded, ready to parse. - Loaded, - // failed to load, do not attempt to load again! - Error, -}; - struct Entry { u64 app_id{}; NacpLanguageEntry lang{}; @@ -29,7 +19,7 @@ struct Entry { std::shared_ptr control{}; u64 jpeg_size{}; - NacpLoadStatus status{NacpLoadStatus::None}; + title::NacpLoadStatus status{title::NacpLoadStatus::None}; auto GetName() const -> const char* { return lang.name; @@ -40,45 +30,6 @@ struct Entry { } }; -struct ThreadResultData { - u64 id{}; - std::shared_ptr control{}; - u64 jpeg_size{}; - NacpLanguageEntry lang{}; - NacpLoadStatus status{NacpLoadStatus::None}; -}; - -struct ThreadData { - ThreadData(bool title_cache); - - void Run(); - void Close(); - void Push(u64 id); - void Push(std::span entries); - void Pop(std::vector& out); - - auto IsRunning() const -> bool { - return m_running; - } - - auto IsTitleCacheEnabled() const { - return m_title_cache; - } - -private: - UEvent m_uevent{}; - Mutex m_mutex_id{}; - Mutex m_mutex_result{}; - bool m_title_cache{}; - - // app_ids pushed to the queue, signal uevent when pushed. - std::vector m_ids{}; - // control data pushed to the queue. - std::vector m_result{}; - - std::atomic_bool m_running{}; -}; - enum SortType { SortType_Updated, }; @@ -144,9 +95,6 @@ private: bool m_is_reversed{}; bool m_dirty{}; - std::unique_ptr m_thread_data{}; - Thread m_thread{}; - option::OptionLong m_sort{INI_SECTION, "sort", SortType::SortType_Updated}; option::OptionLong m_order{INI_SECTION, "order", OrderType::OrderType_Descending}; option::OptionLong m_layout{INI_SECTION, "layout", LayoutType::LayoutType_Grid}; diff --git a/sphaira/include/ui/menus/gc_menu.hpp b/sphaira/include/ui/menus/gc_menu.hpp index 6ff5a92..752844e 100644 --- a/sphaira/include/ui/menus/gc_menu.hpp +++ b/sphaira/include/ui/menus/gc_menu.hpp @@ -151,8 +151,8 @@ struct ApplicationEntry { u64 app_id{}; u32 version{}; u8 key_gen{}; - std::unique_ptr control{}; - u64 control_size{}; + std::shared_ptr control{}; + u64 jpeg_size{}; NacpLanguageEntry lang_entry{}; std::vector application{}; diff --git a/sphaira/include/ui/menus/save_menu.hpp b/sphaira/include/ui/menus/save_menu.hpp index a9e7afd..f689346 100644 --- a/sphaira/include/ui/menus/save_menu.hpp +++ b/sphaira/include/ui/menus/save_menu.hpp @@ -2,6 +2,7 @@ #include "ui/menus/grid_menu_base.hpp" #include "ui/list.hpp" +#include "title_info.hpp" #include "fs.hpp" #include "option.hpp" #include "dumper.hpp" @@ -11,17 +12,6 @@ namespace sphaira::ui::menu::save { -enum class NacpLoadStatus { - // not yet attempted to be loaded. - None, - // started loading. - Progress, - // loaded, ready to parse. - Loaded, - // failed to load, do not attempt to load again! - Error, -}; - struct Entry final : FsSaveDataInfo { NacpLanguageEntry lang{}; int image{}; @@ -29,7 +19,7 @@ struct Entry final : FsSaveDataInfo { std::shared_ptr control{}; u64 jpeg_size{}; - NacpLoadStatus status{NacpLoadStatus::None}; + title::NacpLoadStatus status{title::NacpLoadStatus::None}; auto GetName() const -> const char* { return lang.name; @@ -40,37 +30,6 @@ struct Entry final : FsSaveDataInfo { } }; -struct ThreadResultData { - u64 id{}; - std::shared_ptr control{}; - u64 jpeg_size{}; - NacpLanguageEntry lang{}; - NacpLoadStatus status{NacpLoadStatus::None}; -}; - -struct ThreadData { - ThreadData(); - - auto IsRunning() const -> bool; - void Run(); - void Close(); - void Push(u64 id); - void Push(std::span entries); - void Pop(std::vector& out); - -private: - UEvent m_uevent{}; - Mutex m_mutex_id{}; - Mutex m_mutex_result{}; - - // app_ids pushed to the queue, signal uevent when pushed. - std::vector m_ids{}; - // control data pushed to the queue. - std::vector m_result{}; - - std::atomic_bool m_running{}; -}; - enum SortType { SortType_Updated, }; @@ -145,9 +104,6 @@ private: s64 m_account_index{}; u8 m_data_type{FsSaveDataType_Account}; - ThreadData m_thread_data{}; - Thread m_thread{}; - option::OptionLong m_sort{INI_SECTION, "sort", SortType::SortType_Updated}; option::OptionLong m_order{INI_SECTION, "order", OrderType::OrderType_Descending}; option::OptionLong m_layout{INI_SECTION, "layout", LayoutType::LayoutType_Grid}; diff --git a/sphaira/source/title_info.cpp b/sphaira/source/title_info.cpp new file mode 100644 index 0000000..15d2591 --- /dev/null +++ b/sphaira/source/title_info.cpp @@ -0,0 +1,557 @@ +#include "title_info.hpp" +#include "defines.hpp" +#include "ui/types.hpp" +#include "log.hpp" + +#include "yati/nx/nca.hpp" +#include "yati/nx/ncm.hpp" + +#include +#include +#include +#include + +#include + +namespace sphaira::title { +namespace { + +constexpr int THREAD_PRIO = PRIO_PREEMPTIVE; +constexpr int THREAD_CORE = 1; + +struct ThreadData { + ThreadData(bool title_cache); + + void Run(); + void Close(); + + void Push(u64 id); + void Push(std::span app_ids); + + #if 0 + auto Pop(u64 app_id) -> std::optional; + void Pop(std::span app_ids, std::vector& out); + void PopAll(std::vector& out); + #endif + + auto Get(u64 app_id) -> std::optional; + void Get(std::span app_ids, std::vector& out); + + auto IsRunning() const -> bool { + return m_running; + } + + auto IsTitleCacheEnabled() const { + return m_title_cache; + } + +private: + UEvent m_uevent{}; + Mutex m_mutex_id{}; + Mutex m_mutex_result{}; + bool m_title_cache{}; + + // app_ids pushed to the queue, signal uevent when pushed. + std::vector m_ids{}; + // control data pushed to the queue. + std::vector m_result{}; + + std::atomic_bool m_running{}; +}; + +Mutex g_mutex{}; +Thread g_thread{}; +u32 g_ref_count{}; +std::unique_ptr g_thread_data{}; + +struct NcmEntry { + const NcmStorageId storage_id; + NcmContentStorage cs{}; + NcmContentMetaDatabase db{}; + + void Open() { + if (R_FAILED(ncmOpenContentMetaDatabase(std::addressof(db), storage_id))) { + log_write("\tncmOpenContentMetaDatabase() failed. storage_id: %u\n", storage_id); + } else { + log_write("\tncmOpenContentMetaDatabase() success. storage_id: %u\n", storage_id); + } + + if (R_FAILED(ncmOpenContentStorage(std::addressof(cs), storage_id))) { + log_write("\tncmOpenContentStorage() failed. storage_id: %u\n", storage_id); + } else { + log_write("\tncmOpenContentStorage() success. storage_id: %u\n", storage_id); + } + } + + void Close() { + ncmContentMetaDatabaseClose(std::addressof(db)); + ncmContentStorageClose(std::addressof(cs)); + + db = {}; + cs = {}; + } +}; + +constinit NcmEntry ncm_entries[] = { + // on memory, will become invalid on the gamecard being inserted / removed. + { NcmStorageId_GameCard }, + // normal (save), will remain valid. + { NcmStorageId_BuiltInUser }, + { NcmStorageId_SdCard }, +}; + +auto& GetNcmEntry(u8 storage_id) { + auto it = std::ranges::find_if(ncm_entries, [storage_id](auto& e){ + return storage_id == e.storage_id; + }); + + if (it == std::end(ncm_entries)) { + log_write("unable to find valid ncm entry: %u\n", storage_id); + return ncm_entries[0]; + } + + return *it; +} + +// taken from nxtc +constexpr u8 g_nacpLangTable[SetLanguage_Total] = { + [SetLanguage_JA] = 2, + [SetLanguage_ENUS] = 0, + [SetLanguage_FR] = 3, + [SetLanguage_DE] = 4, + [SetLanguage_IT] = 7, + [SetLanguage_ES] = 6, + [SetLanguage_ZHCN] = 14, + [SetLanguage_KO] = 12, + [SetLanguage_NL] = 8, + [SetLanguage_PT] = 10, + [SetLanguage_RU] = 11, + [SetLanguage_ZHTW] = 13, + [SetLanguage_ENGB] = 1, + [SetLanguage_FRCA] = 9, + [SetLanguage_ES419] = 5, + [SetLanguage_ZHHANS] = 14, + [SetLanguage_ZHHANT] = 13, + [SetLanguage_PTBR] = 15 +}; + +auto GetNacpLangEntryIndex() -> u8 { + SetLanguage lang{SetLanguage_ENUS}; + nxtcGetCacheLanguage(&lang); + return g_nacpLangTable[lang]; +} + +// also sets the status to error. +void FakeNacpEntry(ThreadResultData& e) { + e.status = NacpLoadStatus::Error; + // fake the nacp entry + std::strcpy(e.lang.name, "Corrupted"); + std::strcpy(e.lang.author, "Corrupted"); + e.control.reset(); +} + +Result LoadControlManual(u64 id, ThreadResultData& data) { + TimeStamp ts; + + MetaEntries entries; + R_TRY(GetMetaEntries(id, entries, ContentFlag_Nacp)); + R_UNLESS(!entries.empty(), Result_GameEmptyMetaEntries); + + u64 program_id; + fs::FsPath path; + R_TRY(GetControlPathFromStatus(entries.back(), &program_id, &path)); + + std::vector icon; + R_TRY(nca::ParseControl(path, program_id, &data.control->nacp.lang[GetNacpLangEntryIndex()], sizeof(NacpLanguageEntry), &icon)); + std::memcpy(data.control->icon, icon.data(), icon.size()); + + data.jpeg_size = icon.size(); + log_write("\t\t[manual control] time taken: %.2fs %zums\n", ts.GetSecondsD(), ts.GetMs()); + + R_SUCCEED(); +} + +ThreadData::ThreadData(bool title_cache) : m_title_cache{title_cache} { + ueventCreate(&m_uevent, true); + mutexInit(&m_mutex_id); + mutexInit(&m_mutex_result); + m_running = true; +} + +void ThreadData::Run() { + const auto waiter = waiterForUEvent(&m_uevent); + while (IsRunning()) { + const auto rc = waitSingle(waiter, 3e+9); + + // if we timed out, flush the cache and poll again. + if (R_FAILED(rc)) { + nxtcFlushCacheFile(); + continue; + } + + if (!IsRunning()) { + return; + } + + std::vector ids; + { + SCOPED_MUTEX(&m_mutex_id); + std::swap(ids, m_ids); + } + + for (u64 i = 0; i < std::size(ids); i++) { + if (!IsRunning()) { + return; + } + + bool cached{}; + const auto result = LoadControlEntry(ids[i], &cached); + + if (!cached) { + // sleep after every other entry loaded. + svcSleepThread(2e+6); // 2ms + } + + SCOPED_MUTEX(&m_mutex_result); + m_result.emplace_back(result); + } + } +} + +void ThreadData::Close() { + m_running = false; + ueventSignal(&m_uevent); +} + +void ThreadData::Push(u64 id) { + SCOPED_MUTEX(&m_mutex_id); + SCOPED_MUTEX(&m_mutex_result); + + const auto it_id = std::ranges::find(m_ids, id); + const auto it_result = std::ranges::find_if(m_result, [id](auto& e){ + return id == e.id; + }); + + if (it_id == m_ids.end() && it_result == m_result.end()) { + m_ids.emplace_back(id); + ueventSignal(&m_uevent); + } +} + +void ThreadData::Push(std::span app_ids) { + for (auto& e : app_ids) { + Push(e); + } +} + +#if 0 +auto ThreadData::Pop(u64 app_id) -> std::optional { + SCOPED_MUTEX(&m_mutex_result); + + for (s64 i = 0; i < std::size(m_result); i++) { + if (app_id == m_result[i].id) { + const auto result = m_result[i]; + m_result.erase(m_result.begin() + i); + return result; + } + } + + return std::nullopt; +} + +void ThreadData::Pop(std::span app_ids, std::vector& out) { + for (auto& e : app_ids) { + if (const auto result = Pop(e)) { + out.emplace_back(*result); + } + } +} + +void ThreadData::PopAll(std::vector& out) { + SCOPED_MUTEX(&m_mutex_result); + + std::swap(out, m_result); + m_result.clear(); +} +#endif + +auto ThreadData::Get(u64 app_id) -> std::optional { + SCOPED_MUTEX(&m_mutex_result); + + for (s64 i = 0; i < std::size(m_result); i++) { + if (app_id == m_result[i].id) { + return m_result[i]; + } + } + + return std::nullopt; +} + +void ThreadData::Get(std::span app_ids, std::vector& out) { + for (auto& e : app_ids) { + if (const auto result = Get(e)) { + out.emplace_back(*result); + } + } +} + +void ThreadFunc(void* user) { + auto data = static_cast(user); + + if (data->IsTitleCacheEnabled() && !nxtcInitialize()) { + log_write("[NXTC] failed to init cache\n"); + } + ON_SCOPE_EXIT(nxtcExit()); + + while (data->IsRunning()) { + data->Run(); + } +} + +} // namespace + +// starts background thread. +Result Init() { + SCOPED_MUTEX(&g_mutex); + + if (g_ref_count) { + R_SUCCEED(); + } + + if (!g_ref_count) { + R_TRY(nsInitialize()); + R_TRY(ncmInitialize()); + + for (auto& e : ncm_entries) { + e.Open(); + } + + g_thread_data = std::make_unique(true); + R_TRY(threadCreate(&g_thread, ThreadFunc, g_thread_data.get(), nullptr, 1024*32, THREAD_PRIO, THREAD_CORE)); + svcSetThreadCoreMask(g_thread.handle, THREAD_CORE, THREAD_AFFINITY_DEFAULT(THREAD_CORE)); + R_TRY(threadStart(&g_thread)); + } + + g_ref_count++; + R_SUCCEED(); +} + +void Exit() { + SCOPED_MUTEX(&g_mutex); + + if (!g_ref_count) { + return; + } + + g_ref_count--; + if (!g_ref_count) { + g_thread_data->Close(); + + for (auto& e : ncm_entries) { + e.Close(); + } + + threadWaitForExit(&g_thread); + threadClose(&g_thread); + g_thread_data.reset(); + + nsExit(); + ncmExit(); + } +} + +// adds new entry to queue. +void Push(u64 app_id) { + SCOPED_MUTEX(&g_mutex); + if (g_thread_data) { + g_thread_data->Push(app_id); + } +} + +// adds array of entries to queue. +void Push(std::span app_ids) { + SCOPED_MUTEX(&g_mutex); + if (g_thread_data) { + g_thread_data->Push(app_ids); + } +} + +// gets entry without removing it from the queue. +auto Get(u64 app_id) -> std::optional { + SCOPED_MUTEX(&g_mutex); + if (g_thread_data) { + return g_thread_data->Get(app_id); + } + return {}; +} + +// gets array of entries without removing it from the queue. +void Get(std::span app_ids, std::vector& out) { + SCOPED_MUTEX(&g_mutex); + if (g_thread_data) { + g_thread_data->Get(app_ids, out); + } +} + +auto GetNcmCs(u8 storage_id) -> NcmContentStorage& { + return GetNcmEntry(storage_id).cs; +} + +auto GetNcmDb(u8 storage_id) -> NcmContentMetaDatabase& { + return GetNcmEntry(storage_id).db; +} + +Result GetMetaEntries(u64 id, MetaEntries& out, u32 flags) { + for (s32 i = 0; ; i++) { + s32 count; + NsApplicationContentMetaStatus status; + R_TRY(nsListApplicationContentMetaStatus(id, i, &status, 1, &count)); + + if (!count) { + break; + } + + if (flags & ContentMetaTypeToContentFlag(status.meta_type)) { + out.emplace_back(status); + } + } + + R_SUCCEED(); +} + +Result GetControlPathFromStatus(const NsApplicationContentMetaStatus& status, u64* out_program_id, fs::FsPath* out_path) { + const auto& ee = status; + if (ee.storageID != NcmStorageId_SdCard && ee.storageID != NcmStorageId_BuiltInUser && ee.storageID != NcmStorageId_GameCard) { + return 0x1; + } + + auto& db = GetNcmDb(ee.storageID); + auto& cs = GetNcmCs(ee.storageID); + + NcmContentMetaKey key; + R_TRY(ncmContentMetaDatabaseGetLatestContentMetaKey(&db, &key, ee.application_id)); + + NcmContentId content_id; + R_TRY(ncmContentMetaDatabaseGetContentIdByType(&db, &content_id, &key, NcmContentType_Control)); + + R_TRY(ncmContentStorageGetProgramId(&cs, out_program_id, &content_id, FsContentAttributes_All)); + + R_TRY(ncmContentStorageGetPath(&cs, out_path->s, sizeof(*out_path), &content_id)); + R_SUCCEED(); +} + +auto LoadControlEntry(u64 id, bool* cached) -> ThreadResultData { + // try and fetch from results first, before manually loading. + if (auto data = Get(id)) { + return *data; + } + + TimeStamp ts; + ThreadResultData result{id}; + result.control = std::make_shared(); + result.status = NacpLoadStatus::Error; + + if (auto data = nxtcGetApplicationMetadataEntryById(id)) { + log_write("[NXTC] loaded from cache time taken: %.2fs %zums %zuns\n", ts.GetSecondsD(), ts.GetMs(), ts.GetNs()); + ON_SCOPE_EXIT(nxtcFreeApplicationMetadata(&data)); + + if (cached) { + *cached = true; + } + + result.status = NacpLoadStatus::Loaded; + std::strcpy(result.lang.name, data->name); + std::strcpy(result.lang.author, data->publisher); + std::memcpy(result.control->icon, data->icon_data, data->icon_size); + result.jpeg_size = data->icon_size; + } else { + if (cached) { + *cached = false; + } + + bool manual_load = true; + if (hosversionBefore(20,0,0)) { + TimeStamp ts; + u64 actual_size; + if (R_SUCCEEDED(nsGetApplicationControlData(NsApplicationControlSource_CacheOnly, id, result.control.get(), sizeof(NsApplicationControlData), &actual_size))) { + manual_load = false; + result.jpeg_size = actual_size - sizeof(NacpStruct); + log_write("\t\t[ns control cache] time taken: %.2fs %zums\n", ts.GetSecondsD(), ts.GetMs()); + } + } + + if (manual_load) { + manual_load = R_SUCCEEDED(LoadControlManual(id, result)); + } + + Result rc{}; + if (!manual_load) { + TimeStamp ts; + u64 actual_size; + if (R_SUCCEEDED(rc = nsGetApplicationControlData(NsApplicationControlSource_Storage, id, result.control.get(), sizeof(NsApplicationControlData), &actual_size))) { + result.jpeg_size = actual_size - sizeof(NacpStruct); + log_write("\t\t[ns control storage] time taken: %.2fs %zums\n", ts.GetSecondsD(), ts.GetMs()); + } + } + + if (R_FAILED(rc)) { + FakeNacpEntry(result); + } else { + if (!manual_load) { + NacpLanguageEntry* lang; + if (R_SUCCEEDED(nsGetApplicationDesiredLanguage(&result.control->nacp, &lang))) { + result.lang = *lang; + } + } else { + result.lang = result.control->nacp.lang[GetNacpLangEntryIndex()]; + } + + nxtcAddEntry(id, &result.control->nacp, result.jpeg_size, result.control->icon, true); + result.status = NacpLoadStatus::Loaded; + } + } + + return result; +} + +// taken from nxdumptool. +void utilsReplaceIllegalCharacters(char *str, bool ascii_only) +{ + static const char g_illegalFileSystemChars[] = "\\/:*?\"<>|"; + + size_t str_size = 0, cur_pos = 0; + + if (!str || !(str_size = strlen(str))) return; + + u8 *ptr1 = (u8*)str, *ptr2 = ptr1; + ssize_t units = 0; + u32 code = 0; + bool repl = false; + + while(cur_pos < str_size) + { + units = decode_utf8(&code, ptr1); + if (units < 0) break; + + if (code < 0x20 || (!ascii_only && code == 0x7F) || (ascii_only && code >= 0x7F) || \ + (units == 1 && memchr(g_illegalFileSystemChars, (int)code, std::size(g_illegalFileSystemChars)))) + { + if (!repl) + { + *ptr2++ = '_'; + repl = true; + } + } else { + if (ptr2 != ptr1) memmove(ptr2, ptr1, (size_t)units); + ptr2 += units; + repl = false; + } + + ptr1 += units; + cur_pos += (size_t)units; + } + + *ptr2 = '\0'; +} + +} // namespace sphaira::title diff --git a/sphaira/source/ui/menus/game_menu.cpp b/sphaira/source/ui/menus/game_menu.cpp index 69b842f..c5e10f9 100644 --- a/sphaira/source/ui/menus/game_menu.cpp +++ b/sphaira/source/ui/menus/game_menu.cpp @@ -30,112 +30,6 @@ namespace sphaira::ui::menu::game { namespace { -constexpr int THREAD_PRIO = PRIO_PREEMPTIVE; -constexpr int THREAD_CORE = 1; - -// taken from nxtc -constexpr u8 g_nacpLangTable[SetLanguage_Total] = { - [SetLanguage_JA] = 2, - [SetLanguage_ENUS] = 0, - [SetLanguage_FR] = 3, - [SetLanguage_DE] = 4, - [SetLanguage_IT] = 7, - [SetLanguage_ES] = 6, - [SetLanguage_ZHCN] = 14, - [SetLanguage_KO] = 12, - [SetLanguage_NL] = 8, - [SetLanguage_PT] = 10, - [SetLanguage_RU] = 11, - [SetLanguage_ZHTW] = 13, - [SetLanguage_ENGB] = 1, - [SetLanguage_FRCA] = 9, - [SetLanguage_ES419] = 5, - [SetLanguage_ZHHANS] = 14, - [SetLanguage_ZHHANT] = 13, - [SetLanguage_PTBR] = 15 -}; - -auto GetNacpLangEntryIndex() -> u8 { - SetLanguage lang{SetLanguage_ENUS}; - nxtcGetCacheLanguage(&lang); - return g_nacpLangTable[lang]; -} - -constexpr u32 ContentMetaTypeToContentFlag(u8 meta_type) { - if (meta_type & 0x80) { - return 1 << (meta_type - 0x80); - } - - return 0; -} - -enum ContentFlag { - ContentFlag_Application = ContentMetaTypeToContentFlag(NcmContentMetaType_Application), - ContentFlag_Patch = ContentMetaTypeToContentFlag(NcmContentMetaType_Patch), - ContentFlag_AddOnContent = ContentMetaTypeToContentFlag(NcmContentMetaType_AddOnContent), - ContentFlag_DataPatch = ContentMetaTypeToContentFlag(NcmContentMetaType_DataPatch), - ContentFlag_All = ContentFlag_Application | ContentFlag_Patch | ContentFlag_AddOnContent | ContentFlag_DataPatch, -}; - -struct NcmEntry { - const NcmStorageId storage_id; - NcmContentStorage cs{}; - NcmContentMetaDatabase db{}; - - void Open() { - if (R_FAILED(ncmOpenContentMetaDatabase(std::addressof(db), storage_id))) { - log_write("\tncmOpenContentMetaDatabase() failed. storage_id: %u\n", storage_id); - } else { - log_write("\tncmOpenContentMetaDatabase() success. storage_id: %u\n", storage_id); - } - - if (R_FAILED(ncmOpenContentStorage(std::addressof(cs), storage_id))) { - log_write("\tncmOpenContentStorage() failed. storage_id: %u\n", storage_id); - } else { - log_write("\tncmOpenContentStorage() success. storage_id: %u\n", storage_id); - } - } - - void Close() { - ncmContentMetaDatabaseClose(std::addressof(db)); - ncmContentStorageClose(std::addressof(cs)); - - db = {}; - cs = {}; - } -}; - -constinit NcmEntry ncm_entries[] = { - // on memory, will become invalid on the gamecard being inserted / removed. - { NcmStorageId_GameCard }, - // normal (save), will remain valid. - { NcmStorageId_BuiltInUser }, - { NcmStorageId_SdCard }, -}; - -auto& GetNcmEntry(u8 storage_id) { - auto it = std::ranges::find_if(ncm_entries, [storage_id](auto& e){ - return storage_id == e.storage_id; - }); - - if (it == std::end(ncm_entries)) { - log_write("unable to find valid ncm entry: %u\n", storage_id); - return ncm_entries[0]; - } - - return *it; -} - -auto& GetNcmCs(u8 storage_id) { - return GetNcmEntry(storage_id).cs; -} - -auto& GetNcmDb(u8 storage_id) { - return GetNcmEntry(storage_id).db; -} - -using MetaEntries = std::vector; - struct ContentInfoEntry { NsApplicationContentMetaStatus status{}; std::vector content_infos{}; @@ -287,36 +181,8 @@ Result Notify(Result rc, const std::string& error_message) { return rc; } - -Result GetMetaEntries(u64 id, MetaEntries& out, u32 flags = ContentFlag_All) { - for (s32 i = 0; ; i++) { - s32 count; - NsApplicationContentMetaStatus status; - R_TRY(nsListApplicationContentMetaStatus(id, i, &status, 1, &count)); - - if (!count) { - break; - } - - if (flags & ContentMetaTypeToContentFlag(status.meta_type)) { - out.emplace_back(status); - } - } - - R_SUCCEED(); -} - -Result GetMetaEntries(const Entry& e, MetaEntries& out, u32 flags = ContentFlag_All) { - return GetMetaEntries(e.app_id, out, flags); -} - -// also sets the status to error. -void FakeNacpEntry(ThreadResultData& e) { - e.status = NacpLoadStatus::Error; - // fake the nacp entry - std::strcpy(e.lang.name, "Corrupted"); - std::strcpy(e.lang.author, "Corrupted"); - e.control.reset(); +Result GetMetaEntries(const Entry& e, title::MetaEntries& out, u32 flags = title::ContentFlag_All) { + return title::GetMetaEntries(e.app_id, out, flags); } bool LoadControlImage(Entry& e) { @@ -335,92 +201,7 @@ bool LoadControlImage(Entry& e) { return false; } -Result GetControlPathFromStatus(const NsApplicationContentMetaStatus& status, u64* out_program_id, fs::FsPath* out_path) { - const auto& ee = status; - if (ee.storageID != NcmStorageId_SdCard && ee.storageID != NcmStorageId_BuiltInUser && ee.storageID != NcmStorageId_GameCard) { - return 0x1; - } - - auto& db = GetNcmDb(ee.storageID); - auto& cs = GetNcmCs(ee.storageID); - - NcmContentMetaKey key; - R_TRY(ncmContentMetaDatabaseGetLatestContentMetaKey(&db, &key, ee.application_id)); - - NcmContentId content_id; - R_TRY(ncmContentMetaDatabaseGetContentIdByType(&db, &content_id, &key, NcmContentType_Control)); - - R_TRY(ncmContentStorageGetProgramId(&cs, out_program_id, &content_id, FsContentAttributes_All)); - - R_TRY(ncmContentStorageGetPath(&cs, out_path->s, sizeof(*out_path), &content_id)); - R_SUCCEED(); -} - -Result LoadControlManual(u64 id, ThreadResultData& data) { - TimeStamp ts; - - MetaEntries entries; - R_TRY(GetMetaEntries(id, entries)); - R_UNLESS(!entries.empty(), Result_GameEmptyMetaEntries); - - u64 program_id; - fs::FsPath path; - R_TRY(GetControlPathFromStatus(entries.back(), &program_id, &path)); - - std::vector icon; - R_TRY(nca::ParseControl(path, program_id, &data.control->nacp.lang[GetNacpLangEntryIndex()], sizeof(NacpLanguageEntry), &icon)); - std::memcpy(data.control->icon, icon.data(), icon.size()); - - data.jpeg_size = icon.size(); - log_write("\t\t[manual control] time taken: %.2fs %zums\n", ts.GetSecondsD(), ts.GetMs()); - - R_SUCCEED(); -} - -auto LoadControlEntry(u64 id) -> ThreadResultData { - ThreadResultData data{}; - data.id = id; - data.control = std::make_shared(); - data.status = NacpLoadStatus::Error; - - bool manual_load = true; - if (hosversionBefore(20,0,0)) { - TimeStamp ts; - u64 actual_size; - if (R_SUCCEEDED(nsGetApplicationControlData(NsApplicationControlSource_CacheOnly, id, data.control.get(), sizeof(NsApplicationControlData), &actual_size))) { - manual_load = false; - data.jpeg_size = actual_size - sizeof(NacpStruct); - log_write("\t\t[ns control cache] time taken: %.2fs %zums\n", ts.GetSecondsD(), ts.GetMs()); - } - } - - if (manual_load) { - manual_load = R_SUCCEEDED(LoadControlManual(id, data)); - } - - Result rc{}; - if (!manual_load) { - TimeStamp ts; - u64 actual_size; - if (R_SUCCEEDED(rc = nsGetApplicationControlData(NsApplicationControlSource_Storage, id, data.control.get(), sizeof(NsApplicationControlData), &actual_size))) { - data.jpeg_size = actual_size - sizeof(NacpStruct); - log_write("\t\t[ns control storage] time taken: %.2fs %zums\n", ts.GetSecondsD(), ts.GetMs()); - } - } - - if (R_SUCCEEDED(rc)) { - data.lang = data.control->nacp.lang[GetNacpLangEntryIndex()]; - data.status = NacpLoadStatus::Loaded; - } - - if (R_FAILED(rc)) { - FakeNacpEntry(data); - } - - return data; -} - -void LoadResultIntoEntry(Entry& e, const ThreadResultData& result) { +void LoadResultIntoEntry(Entry& e, const title::ThreadResultData& result) { e.status = result.status; e.control = result.control; e.jpeg_size= result.jpeg_size; @@ -429,56 +210,16 @@ void LoadResultIntoEntry(Entry& e, const ThreadResultData& result) { } void LoadControlEntry(Entry& e, bool force_image_load = false) { - if (e.status == NacpLoadStatus::None) { - const auto result = LoadControlEntry(e.app_id); + if (e.status == title::NacpLoadStatus::None) { + const auto result = title::LoadControlEntry(e.app_id); LoadResultIntoEntry(e, result); } - if (force_image_load && e.status == NacpLoadStatus::Loaded) { + if (force_image_load && e.status == title::NacpLoadStatus::Loaded) { LoadControlImage(e); } } -// taken from nxdumptool. -void utilsReplaceIllegalCharacters(char *str, bool ascii_only) -{ - static const char g_illegalFileSystemChars[] = "\\/:*?\"<>|"; - - size_t str_size = 0, cur_pos = 0; - - if (!str || !(str_size = strlen(str))) return; - - u8 *ptr1 = (u8*)str, *ptr2 = ptr1; - ssize_t units = 0; - u32 code = 0; - bool repl = false; - - while(cur_pos < str_size) - { - units = decode_utf8(&code, ptr1); - if (units < 0) break; - - if (code < 0x20 || (!ascii_only && code == 0x7F) || (ascii_only && code >= 0x7F) || \ - (units == 1 && memchr(g_illegalFileSystemChars, (int)code, std::size(g_illegalFileSystemChars)))) - { - if (!repl) - { - *ptr2++ = '_'; - repl = true; - } - } else { - if (ptr2 != ptr1) memmove(ptr2, ptr1, (size_t)units); - ptr2 += units; - repl = false; - } - - ptr1 += units; - cur_pos += (size_t)units; - } - - *ptr2 = '\0'; -} - auto isRightsIdValid(FsRightsId id) -> bool { FsRightsId empty_id{}; return 0 != std::memcmp(std::addressof(id), std::addressof(empty_id), sizeof(id)); @@ -498,13 +239,13 @@ HashStr hexIdToStr(auto id) { auto BuildNspPath(const Entry& e, const NsApplicationContentMetaStatus& status) -> fs::FsPath { fs::FsPath name_buf = e.GetName(); - utilsReplaceIllegalCharacters(name_buf, true); + title::utilsReplaceIllegalCharacters(name_buf, true); char version[sizeof(NacpStruct::display_version) + 1]{}; if (status.meta_type == NcmContentMetaType_Patch) { u64 program_id; fs::FsPath path; - if (R_SUCCEEDED(GetControlPathFromStatus(status, &program_id, &path))) { + if (R_SUCCEEDED(title::GetControlPathFromStatus(status, &program_id, &path))) { char display_version[0x10]; if (R_SUCCEEDED(nca::ParseControl(path, program_id, display_version, sizeof(display_version), nullptr, offsetof(NacpStruct, display_version)))) { std::snprintf(version, sizeof(version), "%s ", display_version); @@ -523,8 +264,8 @@ auto BuildNspPath(const Entry& e, const NsApplicationContentMetaStatus& status) } Result BuildContentEntry(const NsApplicationContentMetaStatus& status, ContentInfoEntry& out) { - auto& cs = GetNcmCs(status.storageID); - auto& db = GetNcmDb(status.storageID); + auto& cs = title::GetNcmCs(status.storageID); + auto& db = title::GetNcmDb(status.storageID); const auto app_id = ncm::GetAppId(status.meta_type, status.application_id); auto id_min = status.application_id; @@ -633,7 +374,7 @@ Result BuildNspEntry(const Entry& e, const ContentInfoEntry& info, const keys::K } out.nsp_data = yati::container::Nsp::Build(out.collections, out.nsp_size); - out.cs = GetNcmCs(info.status.storageID); + out.cs = title::GetNcmCs(info.status.storageID); R_SUCCEED(); } @@ -641,7 +382,7 @@ Result BuildNspEntry(const Entry& e, const ContentInfoEntry& info, const keys::K Result BuildNspEntries(Entry& e, u32 flags, std::vector& out) { LoadControlEntry(e); - MetaEntries meta_entries; + title::MetaEntries meta_entries; R_TRY(GetMetaEntries(e, meta_entries, flags)); keys::Keys keys; @@ -670,114 +411,8 @@ void LaunchEntry(const Entry& e) { Notify(rc, "Failed to launch application"); } -void ThreadFunc(void* user) { - auto data = static_cast(user); - - if (data->IsTitleCacheEnabled() && !nxtcInitialize()) { - log_write("[NXTC] failed to init cache\n"); - } - ON_SCOPE_EXIT(nxtcExit()); - - while (data->IsRunning()) { - data->Run(); - } -} - } // namespace -ThreadData::ThreadData(bool title_cache) : m_title_cache{title_cache} { - ueventCreate(&m_uevent, true); - mutexInit(&m_mutex_id); - mutexInit(&m_mutex_result); - m_running = true; -} - -void ThreadData::Run() { - const auto waiter = waiterForUEvent(&m_uevent); - while (IsRunning()) { - const auto rc = waitSingle(waiter, 3e+9); - - // if we timed out, flush the cache and poll again. - if (R_FAILED(rc)) { - nxtcFlushCacheFile(); - continue; - } - - if (!IsRunning()) { - return; - } - - std::vector ids; - { - mutexLock(&m_mutex_id); - ON_SCOPE_EXIT(mutexUnlock(&m_mutex_id)); - std::swap(ids, m_ids); - } - - for (u64 i = 0; i < std::size(ids); i++) { - if (!IsRunning()) { - return; - } - - ThreadResultData result{ids[i]}; - TimeStamp ts; - if (auto data = nxtcGetApplicationMetadataEntryById(ids[i])) { - log_write("[NXTC] loaded from cache time taken: %.2fs %zums %zuns\n", ts.GetSecondsD(), ts.GetMs(), ts.GetNs()); - ON_SCOPE_EXIT(nxtcFreeApplicationMetadata(&data)); - - result.control = std::make_unique(); - result.status = NacpLoadStatus::Loaded; - std::strcpy(result.lang.name, data->name); - std::strcpy(result.lang.author, data->publisher); - std::memcpy(result.control->icon, data->icon_data, data->icon_size); - result.jpeg_size = data->icon_size; - } else { - // sleep after every other entry loaded. - svcSleepThread(2e+6); // 2ms - - result = LoadControlEntry(ids[i]); - if (result.status == NacpLoadStatus::Loaded) { - nxtcAddEntry(ids[i], &result.control->nacp, result.jpeg_size, result.control->icon, true); - } - } - - mutexLock(&m_mutex_result); - ON_SCOPE_EXIT(mutexUnlock(&m_mutex_result)); - m_result.emplace_back(result); - } - } -} - -void ThreadData::Close() { - m_running = false; - ueventSignal(&m_uevent); -} - -void ThreadData::Push(u64 id) { - mutexLock(&m_mutex_id); - ON_SCOPE_EXIT(mutexUnlock(&m_mutex_id)); - - const auto it = std::ranges::find(m_ids, id); - if (it == m_ids.end()) { - m_ids.emplace_back(id); - ueventSignal(&m_uevent); - } -} - -void ThreadData::Push(std::span entries) { - for (auto& e : entries) { - Push(e.app_id); - } -} - -void ThreadData::Pop(std::vector& out) { - mutexLock(&m_mutex_result); - ON_SCOPE_EXIT(mutexUnlock(&m_mutex_result)); - - std::swap(out, m_result); - m_result.clear(); -} - Menu::Menu(u32 flags) : grid::Menu{"Games"_i18n, flags} { this->SetActions( std::make_pair(Button::L3, Action{[this](){ @@ -880,7 +515,7 @@ Menu::Menu(u32 flags) : grid::Menu{"Games"_i18n, flags} { })); options->Add(std::make_shared("List meta records"_i18n, [this](){ - MetaEntries meta_entries; + title::MetaEntries meta_entries; const auto rc = GetMetaEntries(m_entries[m_index], meta_entries); if (R_FAILED(rc)) { App::Push(std::make_shared(rc, @@ -917,19 +552,19 @@ Menu::Menu(u32 flags) : grid::Menu{"Games"_i18n, flags} { ON_SCOPE_EXIT(App::Push(options)); options->Add(std::make_shared("Dump All"_i18n, [this](){ - DumpGames(ContentFlag_All); + DumpGames(title::ContentFlag_All); }, true)); options->Add(std::make_shared("Dump Application"_i18n, [this](){ - DumpGames(ContentFlag_Application); + DumpGames(title::ContentFlag_Application); }, true)); options->Add(std::make_shared("Dump Patch"_i18n, [this](){ - DumpGames(ContentFlag_Patch); + DumpGames(title::ContentFlag_Patch); }, true)); options->Add(std::make_shared("Dump AddOnContent"_i18n, [this](){ - DumpGames(ContentFlag_AddOnContent); + DumpGames(title::ContentFlag_AddOnContent); }, true)); options->Add(std::make_shared("Dump DataPatch"_i18n, [this](){ - DumpGames(ContentFlag_DataPatch); + DumpGames(title::ContentFlag_DataPatch); }, true)); }, true)); @@ -988,30 +623,15 @@ Menu::Menu(u32 flags) : grid::Menu{"Games"_i18n, flags} { nsInitialize(); es::Initialize(); - - for (auto& e : ncm_entries) { - e.Open(); - } - - m_thread_data = std::make_unique(m_title_cache.Get()); - threadCreate(&m_thread, ThreadFunc, m_thread_data.get(), nullptr, 1024*32, THREAD_PRIO, THREAD_CORE); - svcSetThreadCoreMask(m_thread.handle, THREAD_CORE, THREAD_AFFINITY_DEFAULT(THREAD_CORE)); - threadStart(&m_thread); + title::Init(); } Menu::~Menu() { - m_thread_data->Close(); - - for (auto& e : ncm_entries) { - e.Close(); - } + title::Exit(); FreeEntries(); nsExit(); es::Exit(); - - threadWaitForExit(&m_thread); - threadClose(&m_thread); } void Menu::Update(Controller* controller, TouchInfo* touch) { @@ -1043,26 +663,17 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) { const int image_load_max = 2; int image_load_count = 0; - std::vector data; - m_thread_data->Pop(data); - - for (const auto& d : data) { - const auto it = std::ranges::find_if(m_entries, [&d](auto& e) { - return e.app_id == d.id; - }); - - if (it != m_entries.end()) { - LoadResultIntoEntry(*it, d); - } - } - m_list->Draw(vg, theme, m_entries.size(), [this, &image_load_count](auto* vg, auto* theme, auto v, auto pos) { const auto& [x, y, w, h] = v; auto& e = m_entries[pos]; - if (e.status == NacpLoadStatus::None) { - m_thread_data->Push(e.app_id); - e.status = NacpLoadStatus::Progress; + if (e.status == title::NacpLoadStatus::None) { + title::Push(e.app_id); + e.status = title::NacpLoadStatus::Progress; + } else if (e.status == title::NacpLoadStatus::Progress) { + if (const auto data = title::Get(e.app_id)) { + LoadResultIntoEntry(e, *data); + } } // lazy load image diff --git a/sphaira/source/ui/menus/gc_menu.cpp b/sphaira/source/ui/menus/gc_menu.cpp index ff026fe..545ccf1 100644 --- a/sphaira/source/ui/menus/gc_menu.cpp +++ b/sphaira/source/ui/menus/gc_menu.cpp @@ -14,6 +14,7 @@ #include "download.hpp" #include "dumper.hpp" #include "image.hpp" +#include "title_info.hpp" #include #include @@ -62,46 +63,6 @@ auto GetXciSizeFromRomSize(u8 rom_size) -> s64 { return 0; } -// taken from nxdumptool. -void utilsReplaceIllegalCharacters(char *str, bool ascii_only) -{ - static const char g_illegalFileSystemChars[] = "\\/:*?\"<>|"; - - size_t str_size = 0, cur_pos = 0; - - if (!str || !(str_size = strlen(str))) return; - - u8 *ptr1 = (u8*)str, *ptr2 = ptr1; - ssize_t units = 0; - u32 code = 0; - bool repl = false; - - while(cur_pos < str_size) - { - units = decode_utf8(&code, ptr1); - if (units < 0) break; - - if (code < 0x20 || (!ascii_only && code == 0x7F) || (ascii_only && code >= 0x7F) || \ - (units == 1 && memchr(g_illegalFileSystemChars, (int)code, std::size(g_illegalFileSystemChars)))) - { - if (!repl) - { - *ptr2++ = '_'; - repl = true; - } - } else { - if (ptr2 != ptr1) memmove(ptr2, ptr1, (size_t)units); - ptr2 += units; - repl = false; - } - - ptr1 += units; - cur_pos += (size_t)units; - } - - *ptr2 = '\0'; -} - struct DebugEventInfo { u32 event_type; u32 flags; @@ -132,7 +93,7 @@ auto GetDumpTypeStr(u8 type) -> const char* { auto BuildXciName(const ApplicationEntry& e) -> fs::FsPath { fs::FsPath name_buf = e.lang_entry.name; - utilsReplaceIllegalCharacters(name_buf, true); + title::utilsReplaceIllegalCharacters(name_buf, true); fs::FsPath path; std::snprintf(path, sizeof(path), "%s [%016lX][v%u]", name_buf.s, e.app_id, e.version); @@ -475,18 +436,18 @@ Menu::Menu(u32 flags) : MenuBase{"GameCard"_i18n, flags} { const Vec2 pad{0, 125 - v.h}; m_list = std::make_unique(1, 3, m_pos, v, pad); - nsInitialize(); fsOpenDeviceOperator(std::addressof(m_dev_op)); fsOpenGameCardDetectionEventNotifier(std::addressof(m_event_notifier)); fsEventNotifierGetEventHandle(std::addressof(m_event_notifier), std::addressof(m_event), true); + title::Init(); } Menu::~Menu() { + title::Exit(); GcUnmount(); eventClose(std::addressof(m_event)); fsEventNotifierClose(std::addressof(m_event_notifier)); fsDeviceOperatorClose(std::addressof(m_dev_op)); - nsExit(); } void Menu::Update(Controller* controller, TouchInfo* touch) { @@ -683,13 +644,6 @@ Result Menu::GcMount() { // load all control data, icons are loaded when displayed. for (auto& e : m_entries) { R_TRY(LoadControlData(e)); - - NacpLanguageEntry* lang_entry{}; - R_TRY(nacpGetLanguageEntry(&e.control->nacp, &lang_entry)); - - if (lang_entry) { - e.lang_entry = *lang_entry; - } } if (m_entries.size() > 1) { @@ -909,47 +863,13 @@ void Menu::FreeImage() { } Result Menu::LoadControlData(ApplicationEntry& e) { - const auto id = e.app_id; - e.control = std::make_unique(); + const auto data = title::LoadControlEntry(e.app_id); + R_UNLESS(data.status == title::NacpLoadStatus::Loaded, 0x1); - if (hosversionBefore(20,0,0)) { - TimeStamp ts; - if (R_SUCCEEDED(nsGetApplicationControlData(NsApplicationControlSource_CacheOnly, id, e.control.get(), sizeof(NsApplicationControlData), &e.control_size))) { - log_write("\t\t[ns control cache] time taken: %.2fs %zums\n", ts.GetSecondsD(), ts.GetMs()); - R_SUCCEED(); - } - } - - // nsGetApplicationControlData() will fail if it's the first time - // mounting a gamecard if the image is not already cached. - // waiting 1-2s after mount, then calling seems to work. - // however, we can just manually parse the nca to get the data we need, - // which always works and *is* faster too ;) - for (const auto& app : e.application) { - for (const auto& collection : app) { - if (collection.type == NcmContentType_Control) { - const auto path = BuildGcPath(collection.name.c_str(), &m_handle); - - u64 program_id = id | collection.id_offset; - if (hosversionAtLeast(17, 0, 0)) { - fsGetProgramId(&program_id, path, FsContentAttributes_All); - } - - TimeStamp ts; - std::vector icon; - if (R_SUCCEEDED(nca::ParseControl(path, program_id, &e.control->nacp, sizeof(e.control->nacp), &icon))) { - std::memcpy(e.control->icon, icon.data(), icon.size()); - e.control_size = sizeof(e.control->nacp) + icon.size(); - log_write("\t\tnca::ParseControl(): %.2fs %zums\n", ts.GetSecondsD(), ts.GetMs()); - R_SUCCEED(); - } else { - log_write("\tFAILED to parse control nca %s\n", path.s); - } - } - } - } - - return 0x1; + e.control = data.control; + e.jpeg_size = data.jpeg_size; + e.lang_entry = data.lang; + R_SUCCEED(); } void Menu::OnChangeIndex(s64 new_index) { @@ -963,7 +883,7 @@ void Menu::OnChangeIndex(s64 new_index) { this->SetSubHeading(std::to_string(index) + " / " + std::to_string(m_entries.size())); const auto& e = m_entries[m_entry_index]; - const auto jpeg_size = e.control_size - sizeof(NacpStruct); + const auto jpeg_size = e.jpeg_size; TimeStamp ts; const auto image = ImageLoadFromMemory({e.control->icon, jpeg_size}, ImageFlag_JPEG); diff --git a/sphaira/source/ui/menus/save_menu.cpp b/sphaira/source/ui/menus/save_menu.cpp index ca1c2ca..09aa68e 100644 --- a/sphaira/source/ui/menus/save_menu.cpp +++ b/sphaira/source/ui/menus/save_menu.cpp @@ -34,9 +34,6 @@ namespace sphaira::ui::menu::save { namespace { -constexpr int THREAD_PRIO = PRIO_PREEMPTIVE; -constexpr int THREAD_CORE = 1; - constexpr u32 NX_SAVE_META_MAGIC = 0x4A4B5356; // JKSV constexpr u32 NX_SAVE_META_VERSION = 1; constexpr const char* NX_SAVE_META_NAME = ".nx_save_meta.bin"; @@ -59,34 +56,6 @@ struct NXSaveMeta { }; static_assert(sizeof(NXSaveMeta) == 128); -// taken from nxtc -constexpr u8 g_nacpLangTable[SetLanguage_Total] = { - [SetLanguage_JA] = 2, - [SetLanguage_ENUS] = 0, - [SetLanguage_FR] = 3, - [SetLanguage_DE] = 4, - [SetLanguage_IT] = 7, - [SetLanguage_ES] = 6, - [SetLanguage_ZHCN] = 14, - [SetLanguage_KO] = 12, - [SetLanguage_NL] = 8, - [SetLanguage_PT] = 10, - [SetLanguage_RU] = 11, - [SetLanguage_ZHTW] = 13, - [SetLanguage_ENGB] = 1, - [SetLanguage_FRCA] = 9, - [SetLanguage_ES419] = 5, - [SetLanguage_ZHHANS] = 14, - [SetLanguage_ZHHANT] = 13, - [SetLanguage_PTBR] = 15 -}; - -auto GetNacpLangEntryIndex() -> u8 { - SetLanguage lang{SetLanguage_ENUS}; - nxtcGetCacheLanguage(&lang); - return g_nacpLangTable[lang]; -} - void GetFsSaveAttr(const AccountProfileBase& acc, u8 data_type, FsSaveDataSpaceId& space_id, FsSaveDataFilter& filter) { std::memset(&filter, 0, sizeof(filter)); @@ -117,108 +86,6 @@ void GetFsSaveAttr(const AccountProfileBase& acc, u8 data_type, FsSaveDataSpaceI } } -constexpr u32 ContentMetaTypeToContentFlag(u8 meta_type) { - if (meta_type & 0x80) { - return 1 << (meta_type - 0x80); - } - - return 0; -} - -enum ContentFlag { - ContentFlag_Application = ContentMetaTypeToContentFlag(NcmContentMetaType_Application), - ContentFlag_Patch = ContentMetaTypeToContentFlag(NcmContentMetaType_Patch), - ContentFlag_AddOnContent = ContentMetaTypeToContentFlag(NcmContentMetaType_AddOnContent), - ContentFlag_DataPatch = ContentMetaTypeToContentFlag(NcmContentMetaType_DataPatch), - ContentFlag_All = ContentFlag_Application | ContentFlag_Patch | ContentFlag_AddOnContent | ContentFlag_DataPatch, -}; - -struct NcmEntry { - const NcmStorageId storage_id; - NcmContentStorage cs{}; - NcmContentMetaDatabase db{}; - - void Open() { - if (R_FAILED(ncmOpenContentMetaDatabase(std::addressof(db), storage_id))) { - log_write("\tncmOpenContentMetaDatabase() failed. storage_id: %u\n", storage_id); - } else { - log_write("\tncmOpenContentMetaDatabase() success. storage_id: %u\n", storage_id); - } - - if (R_FAILED(ncmOpenContentStorage(std::addressof(cs), storage_id))) { - log_write("\tncmOpenContentStorage() failed. storage_id: %u\n", storage_id); - } else { - log_write("\tncmOpenContentStorage() success. storage_id: %u\n", storage_id); - } - } - - void Close() { - ncmContentMetaDatabaseClose(std::addressof(db)); - ncmContentStorageClose(std::addressof(cs)); - - db = {}; - cs = {}; - } -}; - -constinit NcmEntry ncm_entries[] = { - // on memory, will become invalid on the gamecard being inserted / removed. - { NcmStorageId_GameCard }, - // normal (save), will remain valid. - { NcmStorageId_BuiltInUser }, - { NcmStorageId_SdCard }, -}; - -auto& GetNcmEntry(u8 storage_id) { - auto it = std::ranges::find_if(ncm_entries, [storage_id](auto& e){ - return storage_id == e.storage_id; - }); - - if (it == std::end(ncm_entries)) { - log_write("unable to find valid ncm entry: %u\n", storage_id); - return ncm_entries[0]; - } - - return *it; -} - -auto& GetNcmCs(u8 storage_id) { - return GetNcmEntry(storage_id).cs; -} - -auto& GetNcmDb(u8 storage_id) { - return GetNcmEntry(storage_id).db; -} - -using MetaEntries = std::vector; - -Result GetMetaEntries(u64 id, MetaEntries& out, u32 flags = ContentFlag_All) { - for (s32 i = 0; ; i++) { - s32 count; - NsApplicationContentMetaStatus status; - R_TRY(nsListApplicationContentMetaStatus(id, i, &status, 1, &count)); - - if (!count) { - break; - } - - if (flags & ContentMetaTypeToContentFlag(status.meta_type)) { - out.emplace_back(status); - } - } - - R_SUCCEED(); -} - -// also sets the status to error. -void FakeNacpEntry(ThreadResultData& e) { - e.status = NacpLoadStatus::Error; - // fake the nacp entry - std::strcpy(e.lang.name, "Corrupted"); - std::strcpy(e.lang.author, "Corrupted"); - e.control.reset(); -} - auto GetSaveFolder(u8 data_type) -> fs::FsPath { switch (data_type) { case FsSaveDataType_System: return "Save System"; @@ -344,7 +211,7 @@ auto GetSystemSaveName(u64 system_save_data_id) -> const char* { } void FakeNacpEntryForSystem(Entry& e) { - e.status = NacpLoadStatus::Loaded; + e.status = title::NacpLoadStatus::Loaded; // fake the nacp entry std::snprintf(e.lang.name, sizeof(e.lang.name), "%s | %016lX", GetSystemSaveName(e.system_save_data_id), e.system_save_data_id); @@ -368,92 +235,7 @@ bool LoadControlImage(Entry& e) { return false; } -Result GetControlPathFromStatus(const NsApplicationContentMetaStatus& status, u64* out_program_id, fs::FsPath* out_path) { - const auto& ee = status; - if (ee.storageID != NcmStorageId_SdCard && ee.storageID != NcmStorageId_BuiltInUser && ee.storageID != NcmStorageId_GameCard) { - return 0x1; - } - - auto& db = GetNcmDb(ee.storageID); - auto& cs = GetNcmCs(ee.storageID); - - NcmContentMetaKey key; - R_TRY(ncmContentMetaDatabaseGetLatestContentMetaKey(&db, &key, ee.application_id)); - - NcmContentId content_id; - R_TRY(ncmContentMetaDatabaseGetContentIdByType(&db, &content_id, &key, NcmContentType_Control)); - - R_TRY(ncmContentStorageGetProgramId(&cs, out_program_id, &content_id, FsContentAttributes_All)); - - R_TRY(ncmContentStorageGetPath(&cs, out_path->s, sizeof(*out_path), &content_id)); - R_SUCCEED(); -} - -Result LoadControlManual(u64 id, ThreadResultData& data) { - TimeStamp ts; - - MetaEntries entries; - R_TRY(GetMetaEntries(id, entries)); - R_UNLESS(!entries.empty(), Result_GameEmptyMetaEntries); - - u64 program_id; - fs::FsPath path; - R_TRY(GetControlPathFromStatus(entries.back(), &program_id, &path)); - - std::vector icon; - R_TRY(nca::ParseControl(path, program_id, &data.control->nacp.lang[GetNacpLangEntryIndex()], sizeof(NacpLanguageEntry), &icon)); - std::memcpy(data.control->icon, icon.data(), icon.size()); - - data.jpeg_size = icon.size(); - log_write("\t\t[manual control] time taken: %.2fs %zums\n", ts.GetSecondsD(), ts.GetMs()); - - R_SUCCEED(); -} - -auto LoadControlEntry(u64 id) -> ThreadResultData { - ThreadResultData data{}; - data.id = id; - data.control = std::make_shared(); - data.status = NacpLoadStatus::Error; - - bool manual_load = true; - if (hosversionBefore(20,0,0)) { - TimeStamp ts; - u64 actual_size; - if (R_SUCCEEDED(nsGetApplicationControlData(NsApplicationControlSource_CacheOnly, id, data.control.get(), sizeof(NsApplicationControlData), &actual_size))) { - manual_load = false; - data.jpeg_size = actual_size - sizeof(NacpStruct); - log_write("\t\t[ns control cache] time taken: %.2fs %zums\n", ts.GetSecondsD(), ts.GetMs()); - } - } - - if (manual_load) { - manual_load = R_SUCCEEDED(LoadControlManual(id, data)); - } - - Result rc{}; - if (!manual_load) { - TimeStamp ts; - u64 actual_size; - if (R_SUCCEEDED(rc = nsGetApplicationControlData(NsApplicationControlSource_Storage, id, data.control.get(), sizeof(NsApplicationControlData), &actual_size))) { - data.jpeg_size = actual_size - sizeof(NacpStruct); - log_write("\t\t[ns control storage] time taken: %.2fs %zums\n", ts.GetSecondsD(), ts.GetMs()); - } - } - - if (R_SUCCEEDED(rc)) { - data.lang = data.control->nacp.lang[GetNacpLangEntryIndex()]; - data.status = NacpLoadStatus::Loaded; - } - - if (R_FAILED(rc)) { - FakeNacpEntry(data); - } - - return data; -} - -void LoadResultIntoEntry(Entry& e, const ThreadResultData& result) { +void LoadResultIntoEntry(Entry& e, const title::ThreadResultData& result) { e.status = result.status; e.control = result.control; e.jpeg_size= result.jpeg_size; @@ -462,60 +244,20 @@ void LoadResultIntoEntry(Entry& e, const ThreadResultData& result) { } void LoadControlEntry(Entry& e, bool force_image_load = false) { - if (e.status == NacpLoadStatus::None) { + if (e.status == title::NacpLoadStatus::None) { if (e.save_data_type == FsSaveDataType_System || e.save_data_type == FsSaveDataType_SystemBcat) { FakeNacpEntryForSystem(e); } else { - const auto result = LoadControlEntry(e.application_id); + const auto result = title::LoadControlEntry(e.application_id); LoadResultIntoEntry(e, result); } } - if (force_image_load && e.status == NacpLoadStatus::Loaded) { + if (force_image_load && e.status == title::NacpLoadStatus::Loaded) { LoadControlImage(e); } } -// taken from nxdumptool. -void utilsReplaceIllegalCharacters(char *str, bool ascii_only) -{ - static const char g_illegalFileSystemChars[] = "\\/:*?\"<>|"; - - size_t str_size = 0, cur_pos = 0; - - if (!str || !(str_size = strlen(str))) return; - - u8 *ptr1 = (u8*)str, *ptr2 = ptr1; - ssize_t units = 0; - u32 code = 0; - bool repl = false; - - while(cur_pos < str_size) - { - units = decode_utf8(&code, ptr1); - if (units < 0) break; - - if (code < 0x20 || (!ascii_only && code == 0x7F) || (ascii_only && code >= 0x7F) || \ - (units == 1 && memchr(g_illegalFileSystemChars, (int)code, std::size(g_illegalFileSystemChars)))) - { - if (!repl) - { - *ptr2++ = '_'; - repl = true; - } - } else { - if (ptr2 != ptr1) memmove(ptr2, ptr1, (size_t)units); - ptr2 += units; - repl = false; - } - - ptr1 += units; - cur_pos += (size_t)units; - } - - *ptr2 = '\0'; -} - struct HashStr { char str[0x21]; }; @@ -530,7 +272,7 @@ HashStr hexIdToStr(auto id) { auto BuildSaveName(const Entry& e) -> fs::FsPath { fs::FsPath name_buf = e.GetName(); - utilsReplaceIllegalCharacters(name_buf, true); + title::utilsReplaceIllegalCharacters(name_buf, true); return name_buf; } @@ -550,122 +292,12 @@ void FreeEntry(NVGcontext* vg, Entry& e) { e.image = 0; } -void ThreadFunc(void* user) { - auto data = static_cast(user); - - if (!nxtcInitialize()) { - log_write("[NXTC] failed to init cache\n"); - } - ON_SCOPE_EXIT(nxtcExit()); - - while (data->IsRunning()) { - data->Run(); - } -} - } // namespace void SignalChange() { ueventSignal(&g_change_uevent); } -ThreadData::ThreadData() { - ueventCreate(&m_uevent, true); - mutexInit(&m_mutex_id); - mutexInit(&m_mutex_result); - m_running = true; -} - -auto ThreadData::IsRunning() const -> bool { - return m_running; -} - -void ThreadData::Run() { - const auto waiter = waiterForUEvent(&m_uevent); - while (IsRunning()) { - const auto rc = waitSingle(waiter, 3e+9); - - // if we timed out, flush the cache and poll again. - if (R_FAILED(rc)) { - nxtcFlushCacheFile(); - continue; - } - - if (!IsRunning()) { - return; - } - - std::vector ids; - { - mutexLock(&m_mutex_id); - ON_SCOPE_EXIT(mutexUnlock(&m_mutex_id)); - std::swap(ids, m_ids); - } - - for (u64 i = 0; i < std::size(ids); i++) { - if (!IsRunning()) { - return; - } - - ThreadResultData result{ids[i]}; - TimeStamp ts; - if (auto data = nxtcGetApplicationMetadataEntryById(ids[i])) { - log_write("[NXTC] loaded from cache time taken: %.2fs %zums %zuns\n", ts.GetSecondsD(), ts.GetMs(), ts.GetNs()); - ON_SCOPE_EXIT(nxtcFreeApplicationMetadata(&data)); - - result.control = std::make_unique(); - result.status = NacpLoadStatus::Loaded; - std::strcpy(result.lang.name, data->name); - std::strcpy(result.lang.author, data->publisher); - std::memcpy(result.control->icon, data->icon_data, data->icon_size); - result.jpeg_size = data->icon_size; - } else { - // sleep after every other entry loaded. - svcSleepThread(2e+6); // 2ms - - result = LoadControlEntry(ids[i]); - if (result.status == NacpLoadStatus::Loaded) { - nxtcAddEntry(ids[i], &result.control->nacp, result.jpeg_size, result.control->icon, true); - } - } - - mutexLock(&m_mutex_result); - ON_SCOPE_EXIT(mutexUnlock(&m_mutex_result)); - m_result.emplace_back(result); - } - } -} - -void ThreadData::Close() { - m_running = false; - ueventSignal(&m_uevent); -} - -void ThreadData::Push(u64 id) { - mutexLock(&m_mutex_id); - ON_SCOPE_EXIT(mutexUnlock(&m_mutex_id)); - - const auto it = std::ranges::find(m_ids, id); - if (it == m_ids.end()) { - m_ids.emplace_back(id); - ueventSignal(&m_uevent); - } -} - -void ThreadData::Push(std::span entries) { - for (auto& e : entries) { - Push(e.application_id); - } -} - -void ThreadData::Pop(std::vector& out) { - mutexLock(&m_mutex_result); - ON_SCOPE_EXIT(mutexUnlock(&m_mutex_result)); - - std::swap(out, m_result); - m_result.clear(); -} - Menu::Menu(u32 flags) : grid::Menu{"Saves"_i18n, flags} { this->SetActions( std::make_pair(Button::L3, Action{[this](){ @@ -820,28 +452,15 @@ Menu::Menu(u32 flags) : grid::Menu{"Saves"_i18n, flags} { log_write("[SAVE] account uid is not found: 0x%016lX%016lX\n", uid.uid[0], uid.uid[1]); } - for (auto& e : ncm_entries) { - e.Open(); - } - - threadCreate(&m_thread, ThreadFunc, &m_thread_data, nullptr, 1024*32, THREAD_PRIO, THREAD_CORE); - svcSetThreadCoreMask(m_thread.handle, THREAD_CORE, THREAD_AFFINITY_DEFAULT(THREAD_CORE)); - threadStart(&m_thread); + title::Init(); ueventCreate(&g_change_uevent, true); } Menu::~Menu() { - m_thread_data.Close(); - - for (auto& e : ncm_entries) { - e.Close(); - } + title::Exit(); FreeEntries(); nsExit(); - - threadWaitForExit(&m_thread); - threadClose(&m_thread); } void Menu::Update(Controller* controller, TouchInfo* touch) { @@ -877,30 +496,21 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) { const int image_load_max = 2; int image_load_count = 0; - std::vector data; - m_thread_data.Pop(data); - - for (const auto& d : data) { - for (auto& e : m_entries) { - if (e.application_id == d.id) { - // don't break out of loop as multiple entries may use - // the same tid, such as cached saves. - LoadResultIntoEntry(e, d); - } - } - } - m_list->Draw(vg, theme, m_entries.size(), [this, &image_load_count](auto* vg, auto* theme, auto v, auto pos) { const auto& [x, y, w, h] = v; auto& e = m_entries[pos]; - if (e.status == NacpLoadStatus::None) { + if (e.status == title::NacpLoadStatus::None) { if (m_data_type != FsSaveDataType_System && m_data_type != FsSaveDataType_SystemBcat) { - m_thread_data.Push(e.application_id); - e.status = NacpLoadStatus::Progress; + title::Push(e.application_id); + e.status = title::NacpLoadStatus::Progress; } else { FakeNacpEntryForSystem(e); } + } else if (e.status == title::NacpLoadStatus::Progress) { + if (const auto data = title::Get(e.application_id)) { + LoadResultIntoEntry(e, *data); + } } // lazy load image @@ -1185,7 +795,7 @@ auto Menu::BuildSavePath(const Entry& e, bool is_auto) const -> fs::FsPath { std::snprintf(name_buf, sizeof(name_buf), "%s", acc.nickname); } - utilsReplaceIllegalCharacters(name_buf, true); + title::utilsReplaceIllegalCharacters(name_buf, true); std::snprintf(path, sizeof(path), "%s/%s - %s.zip", base.s, name_buf.s, time); } else { std::snprintf(path, sizeof(path), "%s/%s.zip", base.s, time);