add save creation. add loading sys-tweek entries. add title cache delete.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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{};
|
||||
|
||||
@@ -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* {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user