add save creation. add loading sys-tweek entries. add title cache delete.

This commit is contained in:
ITotalJustice
2025-06-21 22:25:51 +01:00
parent 1f7179e941
commit 9d4c431eef
8 changed files with 321 additions and 299 deletions

View File

@@ -1,9 +1,5 @@
#pragma once
// #include <optional>
// #include <variant>
// #include <list>
// #include <string>
#include "fs.hpp"
#include <optional>
#include <span>
@@ -47,8 +43,7 @@ enum class NacpLoadStatus {
struct ThreadResultData {
u64 id{};
std::shared_ptr<NsApplicationControlData> control{};
u64 jpeg_size{};
std::vector<u8> 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<const u64> app_ids);
#if 0
// removes entry from the queue into out.
void Pop(u64 app_id, std::vector<ThreadResultData>& out);
// removes array of entries from the queue into out.
void Pop(std::span<const u64> app_ids, std::vector<ThreadResultData>& out);
// removes all entries from the queue into out.
void Pop(std::vector<ThreadResultData>& out);
#endif
void PushAsync(u64 app_id);
// gets entry without removing it from the queue.
auto Get(u64 app_id) -> std::optional<ThreadResultData>;
// gets array of entries without removing it from the queue.
void Get(std::span<const u64> app_ids, std::vector<ThreadResultData>& out);
auto GetAsync(u64 app_id) -> std::shared_ptr<ThreadResultData>;
// single threaded title info fetch.
auto Get(u64 app_id, bool* cached = nullptr) -> std::shared_ptr<ThreadResultData>;
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

View File

@@ -17,8 +17,7 @@ struct Entry {
int image{};
bool selected{};
std::shared_ptr<NsApplicationControlData> control{};
u64 jpeg_size{};
std::shared_ptr<title::ThreadResultData> 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";

View File

@@ -151,8 +151,7 @@ struct ApplicationEntry {
u64 app_id{};
u32 version{};
u8 key_gen{};
std::shared_ptr<NsApplicationControlData> control{};
u64 jpeg_size{};
std::vector<u8> icon;
NacpLanguageEntry lang_entry{};
std::vector<GcCollections> application{};

View File

@@ -17,8 +17,7 @@ struct Entry final : FsSaveDataInfo {
int image{};
bool selected{};
std::shared_ptr<NsApplicationControlData> control{};
u64 jpeg_size{};
std::shared_ptr<title::ThreadResultData> info{};
title::NacpLoadStatus status{title::NacpLoadStatus::None};
auto GetName() const -> const char* {

View File

@@ -12,6 +12,7 @@
#include <algorithm>
#include <nxtc.h>
#include <minIni.h>
namespace sphaira::title {
namespace {
@@ -25,17 +26,9 @@ struct ThreadData {
void Run();
void Close();
void Push(u64 id);
void Push(std::span<const u64> app_ids);
#if 0
auto Pop(u64 app_id) -> std::optional<ThreadResultData>;
void Pop(std::span<const u64> app_ids, std::vector<ThreadResultData>& out);
void PopAll(std::vector<ThreadResultData>& out);
#endif
auto Get(u64 app_id) -> std::optional<ThreadResultData>;
void Get(std::span<const u64> app_ids, std::vector<ThreadResultData>& out);
void PushAsync(u64 id);
auto GetAsync(u64 app_id) -> std::shared_ptr<ThreadResultData>;
auto Get(u64 app_id, bool* cached = nullptr) -> std::shared_ptr<ThreadResultData>;
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<u64> m_ids{};
// control data pushed to the queue.
std::vector<ThreadResultData> m_result{};
std::vector<std::shared_ptr<ThreadResultData>> 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<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();
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<ThreadResultData>& 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<u8> 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<const u64> app_ids) {
for (auto& e : app_ids) {
Push(e);
}
}
#if 0
auto ThreadData::Pop(u64 app_id) -> std::optional<ThreadResultData> {
auto ThreadData::GetAsync(u64 app_id) -> std::shared_ptr<ThreadResultData> {
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<const u64> app_ids, std::vector<ThreadResultData>& out) {
for (auto& e : app_ids) {
if (const auto result = Pop(e)) {
out.emplace_back(*result);
}
}
}
void ThreadData::PopAll(std::vector<ThreadResultData>& out) {
SCOPED_MUTEX(&m_mutex_result);
std::swap(out, m_result);
m_result.clear();
}
#endif
auto ThreadData::Get(u64 app_id) -> std::optional<ThreadResultData> {
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<const u64> app_ids, std::vector<ThreadResultData>& 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<ThreadResultData> {
// 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<ThreadResultData>(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<NsApplicationControlData>();
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<u8> 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<Overrides*>(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<const u64> app_ids) {
auto GetAsync(u64 app_id) -> std::shared_ptr<ThreadResultData> {
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<ThreadResultData> {
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<const u64> app_ids, std::vector<ThreadResultData>& out) {
auto Get(u64 app_id, bool* cached) -> std::shared_ptr<ThreadResultData> {
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<NsApplicationControlData>();
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

View File

@@ -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<title::ThreadResultData>& 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<NsApplicationControlData>();
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<SidebarEntryBool>("Title cache"_i18n, m_title_cache.Get(), [this](bool& v_out){
m_title_cache.Set(v_out);
}));
options->Add(std::make_shared<SidebarEntryCallback>("Advanced options"_i18n, [this](){
auto options = std::make_shared<Sidebar>("Advanced Options"_i18n, Sidebar::Side::RIGHT);
ON_SCOPE_EXIT(App::Push(options));
// todo: impl this.
#if 0
options->Add(std::make_shared<SidebarEntryCallback>("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<SidebarEntryCallback>("Refresh"_i18n, [this](){
m_dirty = true;
App::PopToMenu();
}));
fsCreateSaveDataFileSystem;
options->Add(std::make_shared<SidebarEntryCallback>("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<ui::PopupList>(
"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<SidebarEntryCallback>("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<ui::PopupList>(
"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<SidebarEntryBool>("Title cache"_i18n, m_title_cache.Get(), [this](bool& v_out){
m_title_cache.Set(v_out);
}));
options->Add(std::make_shared<SidebarEntryCallback>("Delete title cache"_i18n, [this](){
App::Push(std::make_shared<OptionBox>(
"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<ProgressBox>(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

View File

@@ -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());

View File

@@ -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<title::ThreadResultData>& 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);
}