From 9d4c431eef5ae83f4d754596fcc40173f7c4b7bc Mon Sep 17 00:00:00 2001 From: ITotalJustice <47043333+ITotalJustice@users.noreply.github.com> Date: Sat, 21 Jun 2025 22:25:51 +0100 Subject: [PATCH] add save creation. add loading sys-tweek entries. add title cache delete. --- sphaira/include/title_info.hpp | 33 +-- sphaira/include/ui/menus/game_menu.hpp | 4 +- sphaira/include/ui/menus/gc_menu.hpp | 3 +- sphaira/include/ui/menus/save_menu.hpp | 3 +- sphaira/source/title_info.cpp | 379 ++++++++++++------------- sphaira/source/ui/menus/game_menu.cpp | 149 +++++++--- sphaira/source/ui/menus/gc_menu.cpp | 12 +- sphaira/source/ui/menus/save_menu.cpp | 37 ++- 8 files changed, 321 insertions(+), 299 deletions(-) diff --git a/sphaira/include/title_info.hpp b/sphaira/include/title_info.hpp index 8fe9624..6cab976 100644 --- a/sphaira/include/title_info.hpp +++ b/sphaira/include/title_info.hpp @@ -1,9 +1,5 @@ #pragma once -// #include -// #include -// #include -// #include #include "fs.hpp" #include #include @@ -47,8 +43,7 @@ enum class NacpLoadStatus { struct ThreadResultData { u64 id{}; - std::shared_ptr control{}; - u64 jpeg_size{}; + std::vector icon; NacpLanguageEntry lang{}; NacpLoadStatus status{NacpLoadStatus::None}; }; @@ -60,23 +55,11 @@ 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 - +void PushAsync(u64 app_id); // 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 GetAsync(u64 app_id) -> std::shared_ptr; +// single threaded title info fetch. +auto Get(u64 app_id, bool* cached = nullptr) -> std::shared_ptr; auto GetNcmCs(u8 storage_id) -> NcmContentStorage&; auto GetNcmDb(u8 storage_id) -> NcmContentMetaDatabase&; @@ -87,10 +70,10 @@ 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); +// /atmosphere/contents/xxx +auto GetContentsPath(u64 app_id) -> fs::FsPath; + } // namespace sphaira::title diff --git a/sphaira/include/ui/menus/game_menu.hpp b/sphaira/include/ui/menus/game_menu.hpp index fcc31c6..34c08f2 100644 --- a/sphaira/include/ui/menus/game_menu.hpp +++ b/sphaira/include/ui/menus/game_menu.hpp @@ -17,8 +17,7 @@ struct Entry { int image{}; bool selected{}; - std::shared_ptr control{}; - u64 jpeg_size{}; + std::shared_ptr info{}; title::NacpLoadStatus status{title::NacpLoadStatus::None}; auto GetName() const -> const char* { @@ -83,6 +82,7 @@ private: void DeleteGames(); void DumpGames(u32 flags); + void CreateSaves(AccountUid uid); private: static constexpr inline const char* INI_SECTION = "games"; diff --git a/sphaira/include/ui/menus/gc_menu.hpp b/sphaira/include/ui/menus/gc_menu.hpp index 752844e..04971d2 100644 --- a/sphaira/include/ui/menus/gc_menu.hpp +++ b/sphaira/include/ui/menus/gc_menu.hpp @@ -151,8 +151,7 @@ struct ApplicationEntry { u64 app_id{}; u32 version{}; u8 key_gen{}; - std::shared_ptr control{}; - u64 jpeg_size{}; + std::vector icon; 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 f689346..bbdb598 100644 --- a/sphaira/include/ui/menus/save_menu.hpp +++ b/sphaira/include/ui/menus/save_menu.hpp @@ -17,8 +17,7 @@ struct Entry final : FsSaveDataInfo { int image{}; bool selected{}; - std::shared_ptr control{}; - u64 jpeg_size{}; + std::shared_ptr info{}; title::NacpLoadStatus status{title::NacpLoadStatus::None}; auto GetName() const -> const char* { diff --git a/sphaira/source/title_info.cpp b/sphaira/source/title_info.cpp index 15d2591..c49089a 100644 --- a/sphaira/source/title_info.cpp +++ b/sphaira/source/title_info.cpp @@ -12,6 +12,7 @@ #include #include +#include namespace sphaira::title { namespace { @@ -25,17 +26,9 @@ struct ThreadData { 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); + void PushAsync(u64 id); + auto GetAsync(u64 app_id) -> std::shared_ptr; + auto Get(u64 app_id, bool* cached = nullptr) -> std::shared_ptr; auto IsRunning() const -> bool { return m_running; @@ -46,6 +39,7 @@ struct ThreadData { } private: + fs::FsNativeSd m_fs{}; UEvent m_uevent{}; Mutex m_mutex_id{}; Mutex m_mutex_result{}; @@ -54,7 +48,7 @@ private: // 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::vector> m_result{}; std::atomic_bool m_running{}; }; @@ -113,44 +107,15 @@ auto& GetNcmEntry(u8 storage_id) { 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; +void FakeNacpEntry(std::shared_ptr& e) { + e->status = NacpLoadStatus::Error; // fake the nacp entry - std::strcpy(e.lang.name, "Corrupted"); - std::strcpy(e.lang.author, "Corrupted"); - e.control.reset(); + std::strcpy(e->lang.name, "Corrupted"); + std::strcpy(e->lang.author, "Corrupted"); } -Result LoadControlManual(u64 id, ThreadResultData& data) { +Result LoadControlManual(u64 id, NacpStruct& nacp, std::shared_ptr& data) { TimeStamp ts; MetaEntries entries; @@ -160,14 +125,9 @@ Result LoadControlManual(u64 id, ThreadResultData& data) { u64 program_id; fs::FsPath path; R_TRY(GetControlPathFromStatus(entries.back(), &program_id, &path)); + R_TRY(nca::ParseControl(path, program_id, &nacp, sizeof(nacp), &data->icon)); - 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(); } @@ -179,7 +139,10 @@ ThreadData::ThreadData(bool title_cache) : m_title_cache{title_cache} { } void ThreadData::Run() { + TimeStamp ts{}; + bool cached{true}; const auto waiter = waiterForUEvent(&m_uevent); + while (IsRunning()) { const auto rc = waitSingle(waiter, 3e+9); @@ -204,16 +167,15 @@ void ThreadData::Run() { return; } - bool cached{}; - const auto result = LoadControlEntry(ids[i], &cached); - - if (!cached) { - // sleep after every other entry loaded. - svcSleepThread(2e+6); // 2ms + // sleep after every other entry loaded. + const auto elapsed = (s64)2e+6 - (s64)ts.GetNs(); + if (!cached && elapsed > 0) { + svcSleepThread(elapsed); } - SCOPED_MUTEX(&m_mutex_result); - m_result.emplace_back(result); + // loads new entry into cache. + std::ignore = Get(ids[i], &cached); + ts.Update(); } } } @@ -223,13 +185,13 @@ void ThreadData::Close() { ueventSignal(&m_uevent); } -void ThreadData::Push(u64 id) { +void ThreadData::PushAsync(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; + return id == e->id; }); if (it_id == m_ids.end() && it_result == m_result.end()) { @@ -238,61 +200,150 @@ void ThreadData::Push(u64 id) { } } -void ThreadData::Push(std::span app_ids) { - for (auto& e : app_ids) { - Push(e); - } -} - -#if 0 -auto ThreadData::Pop(u64 app_id) -> std::optional { +auto ThreadData::GetAsync(u64 app_id) -> std::shared_ptr { 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) { + if (app_id == m_result[i]->id) { return m_result[i]; } } - return std::nullopt; + return {}; } -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); +auto ThreadData::Get(u64 app_id, bool* cached) -> std::shared_ptr { + // try and fetch from results first, before manually loading. + if (auto data = GetAsync(app_id)) { + if (cached) { + *cached = true; + } + return data; + } + + TimeStamp ts; + auto result = std::make_shared(app_id); + result->status = NacpLoadStatus::Error; + + if (auto data = nxtcGetApplicationMetadataEntryById(app_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); + result->icon.resize(data->icon_size); + std::memcpy(result->icon.data(), data->icon_data, result->icon.size()); + } else { + if (cached) { + *cached = false; + } + + bool manual_load = true; + u64 actual_size{}; + auto control = std::make_unique(); + + if (hosversionBefore(20,0,0)) { + TimeStamp ts; + if (R_SUCCEEDED(nsGetApplicationControlData(NsApplicationControlSource_CacheOnly, app_id, control.get(), sizeof(NsApplicationControlData), &actual_size))) { + manual_load = false; + log_write("\t\t[ns control cache] time taken: %.2fs %zums\n", ts.GetSecondsD(), ts.GetMs()); + } + } + + if (manual_load) { + manual_load = R_SUCCEEDED(LoadControlManual(app_id, control->nacp, result)); + } + + Result rc{}; + if (!manual_load) { + TimeStamp ts; + if (R_SUCCEEDED(rc = nsGetApplicationControlData(NsApplicationControlSource_Storage, app_id, control.get(), sizeof(NsApplicationControlData), &actual_size))) { + log_write("\t\t[ns control storage] time taken: %.2fs %zums\n", ts.GetSecondsD(), ts.GetMs()); + } + } + + if (R_FAILED(rc)) { + FakeNacpEntry(result); + } else { + bool valid = true; + NacpLanguageEntry* lang; + if (R_SUCCEEDED(nsGetApplicationDesiredLanguage(&control->nacp, &lang))) { + result->lang = *lang; + } else { + FakeNacpEntry(result); + valid = false; + } + + if (!manual_load) { + const auto jpeg_size = actual_size - sizeof(NacpStruct); + result->icon.resize(jpeg_size); + std::memcpy(result->icon.data(), control->icon, result->icon.size()); + } + + // add new entry to cache, if valid. + if (valid) { + nxtcAddEntry(app_id, &control->nacp, result->icon.size(), result->icon.data(), true); + } + + result->status = NacpLoadStatus::Loaded; } } + + // load override from sys-tweek. + if (result->status == NacpLoadStatus::Loaded) { + const auto tweek_path = GetContentsPath(app_id); + if (m_fs.DirExists(tweek_path)) { + log_write("[TITLE] found contents path: %s\n", tweek_path.s); + + std::vector icon; + m_fs.read_entire_file(fs::AppendPath(tweek_path, "icon.jpg"), icon); + + struct Overrides { + std::string name; + std::string author; + } overrides; + + static const auto cb = [](const mTCHAR *Section, const mTCHAR *Key, const mTCHAR *Value, void *UserData) -> int { + auto e = static_cast(UserData); + + if (!std::strcmp(Section, "override_nacp")) { + if (!std::strcmp(Key, "name")) { + e->name = Value; + } else if (!std::strcmp(Key, "author")) { + e->author = Value; + } + } + + return 1; + }; + + ini_browse(cb, &overrides, fs::AppendPath(tweek_path, "config.ini")); + + if (!icon.empty() && icon.size() < sizeof(NsApplicationControlData::icon)) { + log_write("[TITLE] overriding icon: %zu -> %zu\n", result->icon.size(), icon.size()); + result->icon = icon; + } + + if (!overrides.name.empty() && overrides.name.length() < sizeof(result->lang.name)) { + log_write("[TITLE] overriding name: %s -> %s\n", result->lang.name, overrides.name.c_str()); + std::strcpy(result->lang.name, overrides.name.c_str()); + } + + if (!overrides.author.empty() && overrides.author.length() < sizeof(result->lang.author)) { + log_write("[TITLE] overriding author: %s -> %s\n", result->lang.author, overrides.author.c_str()); + std::strcpy(result->lang.author, overrides.author.c_str()); + } + } + } + + SCOPED_MUTEX(&m_mutex_result); + m_result.emplace_back(result); + return result; } void ThreadFunc(void* user) { @@ -314,10 +365,6 @@ void ThreadFunc(void* user) { Result Init() { SCOPED_MUTEX(&g_mutex); - if (g_ref_count) { - R_SUCCEED(); - } - if (!g_ref_count) { R_TRY(nsInitialize()); R_TRY(ncmInitialize()); @@ -347,50 +394,40 @@ void Exit() { 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(); + for (auto& e : ncm_entries) { + e.Close(); + } + nsExit(); ncmExit(); } } -// adds new entry to queue. -void Push(u64 app_id) { +void PushAsync(u64 app_id) { SCOPED_MUTEX(&g_mutex); if (g_thread_data) { - g_thread_data->Push(app_id); + g_thread_data->PushAsync(app_id); } } -// adds array of entries to queue. -void Push(std::span app_ids) { +auto GetAsync(u64 app_id) -> std::shared_ptr { 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 g_thread_data->GetAsync(app_id); } return {}; } -// gets array of entries without removing it from the queue. -void Get(std::span app_ids, std::vector& out) { +auto Get(u64 app_id, bool* cached) -> std::shared_ptr { SCOPED_MUTEX(&g_mutex); if (g_thread_data) { - g_thread_data->Get(app_ids, out); + return g_thread_data->Get(app_id, cached); } + return {}; } auto GetNcmCs(u8 storage_id) -> NcmContentStorage& { @@ -440,80 +477,6 @@ Result GetControlPathFromStatus(const NsApplicationContentMetaStatus& status, u6 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) { @@ -554,4 +517,10 @@ void utilsReplaceIllegalCharacters(char *str, bool ascii_only) *ptr2 = '\0'; } +auto GetContentsPath(u64 app_id) -> fs::FsPath { + fs::FsPath path; + std::snprintf(path, sizeof(path), "/atmosphere/contents/%016lX", app_id); + return path; +} + } // namespace sphaira::title diff --git a/sphaira/source/ui/menus/game_menu.cpp b/sphaira/source/ui/menus/game_menu.cpp index c5e10f9..4a78ec3 100644 --- a/sphaira/source/ui/menus/game_menu.cpp +++ b/sphaira/source/ui/menus/game_menu.cpp @@ -8,6 +8,7 @@ #include "swkbd.hpp" #include "ui/menus/game_menu.hpp" +#include "ui/menus/save_menu.hpp" #include "ui/sidebar.hpp" #include "ui/error_box.hpp" #include "ui/option_box.hpp" @@ -186,11 +187,11 @@ Result GetMetaEntries(const Entry& e, title::MetaEntries& out, u32 flags = title } bool LoadControlImage(Entry& e) { - if (!e.image && e.control) { - ON_SCOPE_EXIT(e.control.reset()); + if (!e.image && e.info && !e.info->icon.empty()) { + ON_SCOPE_EXIT(e.info.reset()); TimeStamp ts; - const auto image = ImageLoadFromMemory({e.control->icon, e.jpeg_size}, ImageFlag_JPEG); + const auto image = ImageLoadFromMemory(e.info->icon, ImageFlag_JPEG); if (!image.data.empty()) { e.image = nvgCreateImageRGBA(App::GetVg(), image.w, image.h, 0, image.data.data()); log_write("\t[image load] time taken: %.2fs %zums\n", ts.GetSecondsD(), ts.GetMs()); @@ -201,18 +202,16 @@ bool LoadControlImage(Entry& e) { return false; } -void LoadResultIntoEntry(Entry& e, const title::ThreadResultData& result) { - e.status = result.status; - e.control = result.control; - e.jpeg_size= result.jpeg_size; - e.lang = result.lang; - e.status = result.status; +void LoadResultIntoEntry(Entry& e, const std::shared_ptr& result) { + e.info = result; + e.status = result->status; + e.lang = result->lang; + e.status = result->status; } void LoadControlEntry(Entry& e, bool force_image_load = false) { if (e.status == title::NacpLoadStatus::None) { - const auto result = title::LoadControlEntry(e.app_id); - LoadResultIntoEntry(e, result); + LoadResultIntoEntry(e, title::Get(e.app_id)); } if (force_image_load && e.status == title::NacpLoadStatus::Loaded) { @@ -411,6 +410,30 @@ void LaunchEntry(const Entry& e) { Notify(rc, "Failed to launch application"); } +Result CreateSave(u64 app_id, AccountUid uid) { + u64 actual_size; + auto data = std::make_unique(); + R_TRY(nsGetApplicationControlData(NsApplicationControlSource_Storage, app_id, data.get(), sizeof(NsApplicationControlData), &actual_size)); + + FsSaveDataAttribute attr{}; + attr.application_id = app_id; + attr.uid = uid; + attr.save_data_type = FsSaveDataType_Account; + + FsSaveDataCreationInfo info{}; + info.save_data_size = data->nacp.user_account_save_data_size; + info.journal_size = data->nacp.user_account_save_data_journal_size; + info.available_size = data->nacp.user_account_save_data_size; // todo: check what this should be. + info.owner_id = data->nacp.save_data_owner_id; + info.save_data_space_id = FsSaveDataSpaceId_User; + + // what is this??? + FsSaveDataMetaInfo meta{}; + R_TRY(fsCreateSaveDataFileSystem(&attr, &info, &meta)); + + R_SUCCEED(); +} + } // namespace Menu::Menu(u32 flags) : grid::Menu{"Games"_i18n, flags} { @@ -586,36 +609,57 @@ Menu::Menu(u32 flags) : grid::Menu{"Games"_i18n, flags} { }, true)); } - options->Add(std::make_shared("Title cache"_i18n, m_title_cache.Get(), [this](bool& v_out){ - m_title_cache.Set(v_out); - })); + options->Add(std::make_shared("Advanced options"_i18n, [this](){ + auto options = std::make_shared("Advanced Options"_i18n, Sidebar::Side::RIGHT); + ON_SCOPE_EXIT(App::Push(options)); - // todo: impl this. - #if 0 - options->Add(std::make_shared("Create save"_i18n, [this](){ - ui::PopupList::Items items{}; - const auto accounts = App::GetAccountList(); - for (auto& p : accounts) { - items.emplace_back(p.nickname); - } + options->Add(std::make_shared("Refresh"_i18n, [this](){ + m_dirty = true; + App::PopToMenu(); + })); - fsCreateSaveDataFileSystem; + options->Add(std::make_shared("Create contents folder"_i18n, [this](){ + const auto rc = fs::FsNativeSd().CreateDirectory(title::GetContentsPath(m_entries[m_index].app_id)); + App::PushErrorBox(rc, "Folder create failed!"_i18n); - App::Push(std::make_shared( - "Select user to create save for"_i18n, items, [accounts](auto op_index){ - if (op_index) { - s64 out; - if (R_SUCCEEDED(swkbd::ShowNumPad(out, "Enter the save size"_i18n.c_str()))) { + if (R_SUCCEEDED(rc)) { + App::Notify("Folder created!"_i18n); + } + })); + + options->Add(std::make_shared("Create save"_i18n, [this](){ + ui::PopupList::Items items{}; + const auto accounts = App::GetAccountList(); + for (auto& p : accounts) { + items.emplace_back(p.nickname); + } + + App::Push(std::make_shared( + "Select user to create save for"_i18n, items, [this, accounts](auto op_index){ + if (op_index) { + CreateSaves(accounts[*op_index].uid); } } - } - )); + )); + })); - // 1. Select user to create save for. - // 2. Enter the save size. - // 3. Enter the journal size (0 for default). + options->Add(std::make_shared("Title cache"_i18n, m_title_cache.Get(), [this](bool& v_out){ + m_title_cache.Set(v_out); + })); + + options->Add(std::make_shared("Delete title cache"_i18n, [this](){ + App::Push(std::make_shared( + "Are you sure you want to delete the title cache?"_i18n, + "Back"_i18n, "Delete"_i18n, 0, [this](auto op_index){ + if (op_index && *op_index) { + m_dirty = true; + nxtcWipeCache(); + App::PopToMenu(); + } + } + )); + })); })); - #endif }}) ); @@ -668,11 +712,11 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) { auto& e = m_entries[pos]; if (e.status == title::NacpLoadStatus::None) { - title::Push(e.app_id); + title::PushAsync(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); + if (const auto data = title::GetAsync(e.app_id)) { + LoadResultIntoEntry(e, data); } } @@ -880,4 +924,35 @@ void Menu::DumpGames(u32 flags) { }); } +void Menu::CreateSaves(AccountUid uid) { + App::Push(std::make_shared(0, "Creating"_i18n, "", [this, uid](auto pbox) -> Result { + auto targets = GetSelectedEntries(); + + for (s64 i = 0; i < std::size(targets); i++) { + auto& e = targets[i]; + + LoadControlEntry(e); + pbox->SetTitle(e.GetName()); + pbox->UpdateTransfer(i + 1, std::size(targets)); + const auto rc = CreateSave(e.app_id, uid); + + // don't error if the save already exists. + if (R_FAILED(rc) && rc != FsError_PathAlreadyExists) { + R_THROW(rc); + } + } + + R_SUCCEED(); + }, [this](Result rc){ + App::PushErrorBox(rc, "Save create failed!"_i18n); + + ClearSelection(); + save::SignalChange(); + + if (R_SUCCEEDED(rc)) { + App::Notify("Save create successfull!"_i18n); + } + })); +} + } // namespace sphaira::ui::menu::game diff --git a/sphaira/source/ui/menus/gc_menu.cpp b/sphaira/source/ui/menus/gc_menu.cpp index 545ccf1..1bfadb1 100644 --- a/sphaira/source/ui/menus/gc_menu.cpp +++ b/sphaira/source/ui/menus/gc_menu.cpp @@ -863,12 +863,11 @@ void Menu::FreeImage() { } Result Menu::LoadControlData(ApplicationEntry& e) { - const auto data = title::LoadControlEntry(e.app_id); - R_UNLESS(data.status == title::NacpLoadStatus::Loaded, 0x1); + const auto data = title::Get(e.app_id); + R_UNLESS(data->status == title::NacpLoadStatus::Loaded, 0x1); - e.control = data.control; - e.jpeg_size = data.jpeg_size; - e.lang_entry = data.lang; + e.icon = data->icon; + e.lang_entry = data->lang; R_SUCCEED(); } @@ -883,10 +882,9 @@ 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.jpeg_size; TimeStamp ts; - const auto image = ImageLoadFromMemory({e.control->icon, jpeg_size}, ImageFlag_JPEG); + const auto image = ImageLoadFromMemory(e.icon, ImageFlag_JPEG); if (!image.data.empty()) { m_icon = nvgCreateImageRGBA(App::GetVg(), image.w, image.h, 0, image.data.data()); log_write("\t[image load] time taken: %.2fs %zums\n", ts.GetSecondsD(), ts.GetMs()); diff --git a/sphaira/source/ui/menus/save_menu.cpp b/sphaira/source/ui/menus/save_menu.cpp index 09aa68e..46b0154 100644 --- a/sphaira/source/ui/menus/save_menu.cpp +++ b/sphaira/source/ui/menus/save_menu.cpp @@ -216,15 +216,15 @@ void FakeNacpEntryForSystem(Entry& e) { // 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); std::strcpy(e.lang.author, "Nintendo"); - e.control.reset(); + e.info.reset(); } bool LoadControlImage(Entry& e) { - if (!e.image && e.control) { - ON_SCOPE_EXIT(e.control.reset()); + if (!e.image && e.info && !e.info->icon.empty()) { + ON_SCOPE_EXIT(e.info.reset()); TimeStamp ts; - const auto image = ImageLoadFromMemory({e.control->icon, e.jpeg_size}, ImageFlag_JPEG); + const auto image = ImageLoadFromMemory(e.info->icon, ImageFlag_JPEG); if (!image.data.empty()) { e.image = nvgCreateImageRGBA(App::GetVg(), image.w, image.h, 0, image.data.data()); log_write("\t[image load] time taken: %.2fs %zums\n", ts.GetSecondsD(), ts.GetMs()); @@ -235,12 +235,11 @@ bool LoadControlImage(Entry& e) { return false; } -void LoadResultIntoEntry(Entry& e, const title::ThreadResultData& result) { - e.status = result.status; - e.control = result.control; - e.jpeg_size= result.jpeg_size; - e.lang = result.lang; - e.status = result.status; +void LoadResultIntoEntry(Entry& e, const std::shared_ptr& result) { + e.info = result; + e.status = result->status; + e.lang = result->lang; + e.status = result->status; } void LoadControlEntry(Entry& e, bool force_image_load = false) { @@ -248,8 +247,7 @@ void LoadControlEntry(Entry& e, bool force_image_load = false) { if (e.save_data_type == FsSaveDataType_System || e.save_data_type == FsSaveDataType_SystemBcat) { FakeNacpEntryForSystem(e); } else { - const auto result = title::LoadControlEntry(e.application_id); - LoadResultIntoEntry(e, result); + LoadResultIntoEntry(e, title::Get(e.application_id)); } } @@ -502,14 +500,14 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) { if (e.status == title::NacpLoadStatus::None) { if (m_data_type != FsSaveDataType_System && m_data_type != FsSaveDataType_SystemBcat) { - title::Push(e.application_id); + title::PushAsync(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); + if (const auto data = title::GetAsync(e.application_id)) { + LoadResultIntoEntry(e, data); } } @@ -577,6 +575,7 @@ void Menu::ScanHomebrew() { FreeEntries(); ClearSelection(); + ueventClear(&g_change_uevent); m_entries.reserve(ENTRY_CHUNK_COUNT); m_is_reversed = false; m_dirty = false; @@ -808,8 +807,8 @@ Result Menu::RestoreSaveInternal(ProgressBox* pbox, const Entry& e, const fs::Fs pbox->SetTitle(e.GetName()); if (e.image) { pbox->SetImage(e.image); - } else if (e.control && e.jpeg_size) { - pbox->SetImageDataConst({e.control->icon, e.jpeg_size}); + } else if (e.info && !e.info->icon.empty()) { + pbox->SetImageDataConst(e.info->icon); } else { pbox->SetImage(0); } @@ -935,8 +934,8 @@ Result Menu::BackupSaveInternal(ProgressBox* pbox, const dump::DumpLocation& loc pbox->SetTitle(e.GetName()); if (e.image) { pbox->SetImage(e.image); - } else if (e.control && e.jpeg_size) { - pbox->SetImageDataConst({e.control->icon, e.jpeg_size}); + } else if (e.info && !e.info->icon.empty()) { + pbox->SetImageDataConst(e.info->icon); } else { pbox->SetImage(0); }