simplify title cache loading.
This commit is contained in:
@@ -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
|
||||
|
||||
96
sphaira/include/title_info.hpp
Normal file
96
sphaira/include/title_info.hpp
Normal file
@@ -0,0 +1,96 @@
|
||||
#pragma once
|
||||
|
||||
// #include <optional>
|
||||
// #include <variant>
|
||||
// #include <list>
|
||||
// #include <string>
|
||||
#include "fs.hpp"
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include <functional>
|
||||
#include <switch.h>
|
||||
|
||||
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<NsApplicationControlData> control{};
|
||||
u64 jpeg_size{};
|
||||
NacpLanguageEntry lang{};
|
||||
NacpLoadStatus status{NacpLoadStatus::None};
|
||||
};
|
||||
|
||||
using MetaEntries = std::vector<NsApplicationContentMetaStatus>;
|
||||
|
||||
// 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<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
|
||||
|
||||
// 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 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
|
||||
@@ -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 <memory>
|
||||
@@ -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<NsApplicationControlData> 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<NsApplicationControlData> 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<const Entry> entries);
|
||||
void Pop(std::vector<ThreadResultData>& 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<u64> m_ids{};
|
||||
// control data pushed to the queue.
|
||||
std::vector<ThreadResultData> 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<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};
|
||||
|
||||
@@ -151,8 +151,8 @@ struct ApplicationEntry {
|
||||
u64 app_id{};
|
||||
u32 version{};
|
||||
u8 key_gen{};
|
||||
std::unique_ptr<NsApplicationControlData> control{};
|
||||
u64 control_size{};
|
||||
std::shared_ptr<NsApplicationControlData> control{};
|
||||
u64 jpeg_size{};
|
||||
NacpLanguageEntry lang_entry{};
|
||||
|
||||
std::vector<GcCollections> application{};
|
||||
|
||||
@@ -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<NsApplicationControlData> 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<NsApplicationControlData> 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<const Entry> entries);
|
||||
void Pop(std::vector<ThreadResultData>& 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<u64> m_ids{};
|
||||
// control data pushed to the queue.
|
||||
std::vector<ThreadResultData> 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};
|
||||
|
||||
557
sphaira/source/title_info.cpp
Normal file
557
sphaira/source/title_info.cpp
Normal file
@@ -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 <cstring>
|
||||
#include <atomic>
|
||||
#include <ranges>
|
||||
#include <algorithm>
|
||||
|
||||
#include <nxtc.h>
|
||||
|
||||
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<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);
|
||||
|
||||
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<u64> m_ids{};
|
||||
// control data pushed to the queue.
|
||||
std::vector<ThreadResultData> m_result{};
|
||||
|
||||
std::atomic_bool m_running{};
|
||||
};
|
||||
|
||||
Mutex g_mutex{};
|
||||
Thread g_thread{};
|
||||
u32 g_ref_count{};
|
||||
std::unique_ptr<ThreadData> 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<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();
|
||||
}
|
||||
|
||||
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<u64> 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<const u64> app_ids) {
|
||||
for (auto& e : app_ids) {
|
||||
Push(e);
|
||||
}
|
||||
}
|
||||
|
||||
#if 0
|
||||
auto ThreadData::Pop(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) {
|
||||
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) {
|
||||
return m_result[i];
|
||||
}
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ThreadFunc(void* user) {
|
||||
auto data = static_cast<ThreadData*>(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<ThreadData>(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<const u64> 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<ThreadResultData> {
|
||||
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<const u64> app_ids, std::vector<ThreadResultData>& 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<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)
|
||||
{
|
||||
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
|
||||
@@ -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<NsApplicationContentMetaStatus>;
|
||||
|
||||
struct ContentInfoEntry {
|
||||
NsApplicationContentMetaStatus status{};
|
||||
std::vector<NcmContentInfo> 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<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();
|
||||
}
|
||||
|
||||
auto LoadControlEntry(u64 id) -> ThreadResultData {
|
||||
ThreadResultData data{};
|
||||
data.id = id;
|
||||
data.control = std::make_shared<NsApplicationControlData>();
|
||||
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<NspEntry>& 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<ThreadData*>(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<u64> 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<NsApplicationControlData>();
|
||||
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<const Entry> entries) {
|
||||
for (auto& e : entries) {
|
||||
Push(e.app_id);
|
||||
}
|
||||
}
|
||||
|
||||
void ThreadData::Pop(std::vector<ThreadResultData>& 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<SidebarEntryCallback>("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<ui::ErrorBox>(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<SidebarEntryCallback>("Dump All"_i18n, [this](){
|
||||
DumpGames(ContentFlag_All);
|
||||
DumpGames(title::ContentFlag_All);
|
||||
}, true));
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Dump Application"_i18n, [this](){
|
||||
DumpGames(ContentFlag_Application);
|
||||
DumpGames(title::ContentFlag_Application);
|
||||
}, true));
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Dump Patch"_i18n, [this](){
|
||||
DumpGames(ContentFlag_Patch);
|
||||
DumpGames(title::ContentFlag_Patch);
|
||||
}, true));
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Dump AddOnContent"_i18n, [this](){
|
||||
DumpGames(ContentFlag_AddOnContent);
|
||||
DumpGames(title::ContentFlag_AddOnContent);
|
||||
}, true));
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("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<ThreadData>(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<ThreadResultData> 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
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include "download.hpp"
|
||||
#include "dumper.hpp"
|
||||
#include "image.hpp"
|
||||
#include "title_info.hpp"
|
||||
|
||||
#include <cstring>
|
||||
#include <algorithm>
|
||||
@@ -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<List>(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<NsApplicationControlData>();
|
||||
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<u8> 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);
|
||||
|
||||
@@ -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<NsApplicationContentMetaStatus>;
|
||||
|
||||
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<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();
|
||||
}
|
||||
|
||||
auto LoadControlEntry(u64 id) -> ThreadResultData {
|
||||
ThreadResultData data{};
|
||||
data.id = id;
|
||||
data.control = std::make_shared<NsApplicationControlData>();
|
||||
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<ThreadData*>(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<u64> 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<NsApplicationControlData>();
|
||||
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<const Entry> entries) {
|
||||
for (auto& e : entries) {
|
||||
Push(e.application_id);
|
||||
}
|
||||
}
|
||||
|
||||
void ThreadData::Pop(std::vector<ThreadResultData>& 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<ThreadResultData> 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);
|
||||
|
||||
Reference in New Issue
Block a user