simplify title cache loading.
This commit is contained in:
@@ -83,6 +83,7 @@ add_executable(sphaira
|
|||||||
source/ftpsrv_helper.cpp
|
source/ftpsrv_helper.cpp
|
||||||
source/haze_helper.cpp
|
source/haze_helper.cpp
|
||||||
source/threaded_file_transfer.cpp
|
source/threaded_file_transfer.cpp
|
||||||
|
source/title_info.cpp
|
||||||
source/minizip_helper.cpp
|
source/minizip_helper.cpp
|
||||||
|
|
||||||
source/usb/base.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/menus/grid_menu_base.hpp"
|
||||||
#include "ui/list.hpp"
|
#include "ui/list.hpp"
|
||||||
|
#include "title_info.hpp"
|
||||||
#include "fs.hpp"
|
#include "fs.hpp"
|
||||||
#include "option.hpp"
|
#include "option.hpp"
|
||||||
#include <memory>
|
#include <memory>
|
||||||
@@ -10,17 +11,6 @@
|
|||||||
|
|
||||||
namespace sphaira::ui::menu::game {
|
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 {
|
struct Entry {
|
||||||
u64 app_id{};
|
u64 app_id{};
|
||||||
NacpLanguageEntry lang{};
|
NacpLanguageEntry lang{};
|
||||||
@@ -29,7 +19,7 @@ struct Entry {
|
|||||||
|
|
||||||
std::shared_ptr<NsApplicationControlData> control{};
|
std::shared_ptr<NsApplicationControlData> control{};
|
||||||
u64 jpeg_size{};
|
u64 jpeg_size{};
|
||||||
NacpLoadStatus status{NacpLoadStatus::None};
|
title::NacpLoadStatus status{title::NacpLoadStatus::None};
|
||||||
|
|
||||||
auto GetName() const -> const char* {
|
auto GetName() const -> const char* {
|
||||||
return lang.name;
|
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 {
|
enum SortType {
|
||||||
SortType_Updated,
|
SortType_Updated,
|
||||||
};
|
};
|
||||||
@@ -144,9 +95,6 @@ private:
|
|||||||
bool m_is_reversed{};
|
bool m_is_reversed{};
|
||||||
bool m_dirty{};
|
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_sort{INI_SECTION, "sort", SortType::SortType_Updated};
|
||||||
option::OptionLong m_order{INI_SECTION, "order", OrderType::OrderType_Descending};
|
option::OptionLong m_order{INI_SECTION, "order", OrderType::OrderType_Descending};
|
||||||
option::OptionLong m_layout{INI_SECTION, "layout", LayoutType::LayoutType_Grid};
|
option::OptionLong m_layout{INI_SECTION, "layout", LayoutType::LayoutType_Grid};
|
||||||
|
|||||||
@@ -151,8 +151,8 @@ struct ApplicationEntry {
|
|||||||
u64 app_id{};
|
u64 app_id{};
|
||||||
u32 version{};
|
u32 version{};
|
||||||
u8 key_gen{};
|
u8 key_gen{};
|
||||||
std::unique_ptr<NsApplicationControlData> control{};
|
std::shared_ptr<NsApplicationControlData> control{};
|
||||||
u64 control_size{};
|
u64 jpeg_size{};
|
||||||
NacpLanguageEntry lang_entry{};
|
NacpLanguageEntry lang_entry{};
|
||||||
|
|
||||||
std::vector<GcCollections> application{};
|
std::vector<GcCollections> application{};
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include "ui/menus/grid_menu_base.hpp"
|
#include "ui/menus/grid_menu_base.hpp"
|
||||||
#include "ui/list.hpp"
|
#include "ui/list.hpp"
|
||||||
|
#include "title_info.hpp"
|
||||||
#include "fs.hpp"
|
#include "fs.hpp"
|
||||||
#include "option.hpp"
|
#include "option.hpp"
|
||||||
#include "dumper.hpp"
|
#include "dumper.hpp"
|
||||||
@@ -11,17 +12,6 @@
|
|||||||
|
|
||||||
namespace sphaira::ui::menu::save {
|
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 {
|
struct Entry final : FsSaveDataInfo {
|
||||||
NacpLanguageEntry lang{};
|
NacpLanguageEntry lang{};
|
||||||
int image{};
|
int image{};
|
||||||
@@ -29,7 +19,7 @@ struct Entry final : FsSaveDataInfo {
|
|||||||
|
|
||||||
std::shared_ptr<NsApplicationControlData> control{};
|
std::shared_ptr<NsApplicationControlData> control{};
|
||||||
u64 jpeg_size{};
|
u64 jpeg_size{};
|
||||||
NacpLoadStatus status{NacpLoadStatus::None};
|
title::NacpLoadStatus status{title::NacpLoadStatus::None};
|
||||||
|
|
||||||
auto GetName() const -> const char* {
|
auto GetName() const -> const char* {
|
||||||
return lang.name;
|
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 {
|
enum SortType {
|
||||||
SortType_Updated,
|
SortType_Updated,
|
||||||
};
|
};
|
||||||
@@ -145,9 +104,6 @@ private:
|
|||||||
s64 m_account_index{};
|
s64 m_account_index{};
|
||||||
u8 m_data_type{FsSaveDataType_Account};
|
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_sort{INI_SECTION, "sort", SortType::SortType_Updated};
|
||||||
option::OptionLong m_order{INI_SECTION, "order", OrderType::OrderType_Descending};
|
option::OptionLong m_order{INI_SECTION, "order", OrderType::OrderType_Descending};
|
||||||
option::OptionLong m_layout{INI_SECTION, "layout", LayoutType::LayoutType_Grid};
|
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 sphaira::ui::menu::game {
|
||||||
namespace {
|
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 {
|
struct ContentInfoEntry {
|
||||||
NsApplicationContentMetaStatus status{};
|
NsApplicationContentMetaStatus status{};
|
||||||
std::vector<NcmContentInfo> content_infos{};
|
std::vector<NcmContentInfo> content_infos{};
|
||||||
@@ -287,36 +181,8 @@ Result Notify(Result rc, const std::string& error_message) {
|
|||||||
|
|
||||||
return rc;
|
return rc;
|
||||||
}
|
}
|
||||||
|
Result GetMetaEntries(const Entry& e, title::MetaEntries& out, u32 flags = title::ContentFlag_All) {
|
||||||
Result GetMetaEntries(u64 id, MetaEntries& out, u32 flags = ContentFlag_All) {
|
return title::GetMetaEntries(e.app_id, out, 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 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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool LoadControlImage(Entry& e) {
|
bool LoadControlImage(Entry& e) {
|
||||||
@@ -335,92 +201,7 @@ bool LoadControlImage(Entry& e) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Result GetControlPathFromStatus(const NsApplicationContentMetaStatus& status, u64* out_program_id, fs::FsPath* out_path) {
|
void LoadResultIntoEntry(Entry& e, const title::ThreadResultData& result) {
|
||||||
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) {
|
|
||||||
e.status = result.status;
|
e.status = result.status;
|
||||||
e.control = result.control;
|
e.control = result.control;
|
||||||
e.jpeg_size= result.jpeg_size;
|
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) {
|
void LoadControlEntry(Entry& e, bool force_image_load = false) {
|
||||||
if (e.status == NacpLoadStatus::None) {
|
if (e.status == title::NacpLoadStatus::None) {
|
||||||
const auto result = LoadControlEntry(e.app_id);
|
const auto result = title::LoadControlEntry(e.app_id);
|
||||||
LoadResultIntoEntry(e, result);
|
LoadResultIntoEntry(e, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (force_image_load && e.status == NacpLoadStatus::Loaded) {
|
if (force_image_load && e.status == title::NacpLoadStatus::Loaded) {
|
||||||
LoadControlImage(e);
|
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 {
|
auto isRightsIdValid(FsRightsId id) -> bool {
|
||||||
FsRightsId empty_id{};
|
FsRightsId empty_id{};
|
||||||
return 0 != std::memcmp(std::addressof(id), std::addressof(empty_id), sizeof(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 {
|
auto BuildNspPath(const Entry& e, const NsApplicationContentMetaStatus& status) -> fs::FsPath {
|
||||||
fs::FsPath name_buf = e.GetName();
|
fs::FsPath name_buf = e.GetName();
|
||||||
utilsReplaceIllegalCharacters(name_buf, true);
|
title::utilsReplaceIllegalCharacters(name_buf, true);
|
||||||
|
|
||||||
char version[sizeof(NacpStruct::display_version) + 1]{};
|
char version[sizeof(NacpStruct::display_version) + 1]{};
|
||||||
if (status.meta_type == NcmContentMetaType_Patch) {
|
if (status.meta_type == NcmContentMetaType_Patch) {
|
||||||
u64 program_id;
|
u64 program_id;
|
||||||
fs::FsPath path;
|
fs::FsPath path;
|
||||||
if (R_SUCCEEDED(GetControlPathFromStatus(status, &program_id, &path))) {
|
if (R_SUCCEEDED(title::GetControlPathFromStatus(status, &program_id, &path))) {
|
||||||
char display_version[0x10];
|
char display_version[0x10];
|
||||||
if (R_SUCCEEDED(nca::ParseControl(path, program_id, display_version, sizeof(display_version), nullptr, offsetof(NacpStruct, display_version)))) {
|
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);
|
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) {
|
Result BuildContentEntry(const NsApplicationContentMetaStatus& status, ContentInfoEntry& out) {
|
||||||
auto& cs = GetNcmCs(status.storageID);
|
auto& cs = title::GetNcmCs(status.storageID);
|
||||||
auto& db = GetNcmDb(status.storageID);
|
auto& db = title::GetNcmDb(status.storageID);
|
||||||
const auto app_id = ncm::GetAppId(status.meta_type, status.application_id);
|
const auto app_id = ncm::GetAppId(status.meta_type, status.application_id);
|
||||||
|
|
||||||
auto id_min = 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.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();
|
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) {
|
Result BuildNspEntries(Entry& e, u32 flags, std::vector<NspEntry>& out) {
|
||||||
LoadControlEntry(e);
|
LoadControlEntry(e);
|
||||||
|
|
||||||
MetaEntries meta_entries;
|
title::MetaEntries meta_entries;
|
||||||
R_TRY(GetMetaEntries(e, meta_entries, flags));
|
R_TRY(GetMetaEntries(e, meta_entries, flags));
|
||||||
|
|
||||||
keys::Keys keys;
|
keys::Keys keys;
|
||||||
@@ -670,114 +411,8 @@ void LaunchEntry(const Entry& e) {
|
|||||||
Notify(rc, "Failed to launch application");
|
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
|
} // 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} {
|
Menu::Menu(u32 flags) : grid::Menu{"Games"_i18n, flags} {
|
||||||
this->SetActions(
|
this->SetActions(
|
||||||
std::make_pair(Button::L3, Action{[this](){
|
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](){
|
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);
|
const auto rc = GetMetaEntries(m_entries[m_index], meta_entries);
|
||||||
if (R_FAILED(rc)) {
|
if (R_FAILED(rc)) {
|
||||||
App::Push(std::make_shared<ui::ErrorBox>(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));
|
ON_SCOPE_EXIT(App::Push(options));
|
||||||
|
|
||||||
options->Add(std::make_shared<SidebarEntryCallback>("Dump All"_i18n, [this](){
|
options->Add(std::make_shared<SidebarEntryCallback>("Dump All"_i18n, [this](){
|
||||||
DumpGames(ContentFlag_All);
|
DumpGames(title::ContentFlag_All);
|
||||||
}, true));
|
}, true));
|
||||||
options->Add(std::make_shared<SidebarEntryCallback>("Dump Application"_i18n, [this](){
|
options->Add(std::make_shared<SidebarEntryCallback>("Dump Application"_i18n, [this](){
|
||||||
DumpGames(ContentFlag_Application);
|
DumpGames(title::ContentFlag_Application);
|
||||||
}, true));
|
}, true));
|
||||||
options->Add(std::make_shared<SidebarEntryCallback>("Dump Patch"_i18n, [this](){
|
options->Add(std::make_shared<SidebarEntryCallback>("Dump Patch"_i18n, [this](){
|
||||||
DumpGames(ContentFlag_Patch);
|
DumpGames(title::ContentFlag_Patch);
|
||||||
}, true));
|
}, true));
|
||||||
options->Add(std::make_shared<SidebarEntryCallback>("Dump AddOnContent"_i18n, [this](){
|
options->Add(std::make_shared<SidebarEntryCallback>("Dump AddOnContent"_i18n, [this](){
|
||||||
DumpGames(ContentFlag_AddOnContent);
|
DumpGames(title::ContentFlag_AddOnContent);
|
||||||
}, true));
|
}, true));
|
||||||
options->Add(std::make_shared<SidebarEntryCallback>("Dump DataPatch"_i18n, [this](){
|
options->Add(std::make_shared<SidebarEntryCallback>("Dump DataPatch"_i18n, [this](){
|
||||||
DumpGames(ContentFlag_DataPatch);
|
DumpGames(title::ContentFlag_DataPatch);
|
||||||
}, true));
|
}, true));
|
||||||
}, true));
|
}, true));
|
||||||
|
|
||||||
@@ -988,30 +623,15 @@ Menu::Menu(u32 flags) : grid::Menu{"Games"_i18n, flags} {
|
|||||||
|
|
||||||
nsInitialize();
|
nsInitialize();
|
||||||
es::Initialize();
|
es::Initialize();
|
||||||
|
title::Init();
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Menu::~Menu() {
|
Menu::~Menu() {
|
||||||
m_thread_data->Close();
|
title::Exit();
|
||||||
|
|
||||||
for (auto& e : ncm_entries) {
|
|
||||||
e.Close();
|
|
||||||
}
|
|
||||||
|
|
||||||
FreeEntries();
|
FreeEntries();
|
||||||
nsExit();
|
nsExit();
|
||||||
es::Exit();
|
es::Exit();
|
||||||
|
|
||||||
threadWaitForExit(&m_thread);
|
|
||||||
threadClose(&m_thread);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Menu::Update(Controller* controller, TouchInfo* touch) {
|
void Menu::Update(Controller* controller, TouchInfo* touch) {
|
||||||
@@ -1043,26 +663,17 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
|||||||
const int image_load_max = 2;
|
const int image_load_max = 2;
|
||||||
int image_load_count = 0;
|
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) {
|
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;
|
const auto& [x, y, w, h] = v;
|
||||||
auto& e = m_entries[pos];
|
auto& e = m_entries[pos];
|
||||||
|
|
||||||
if (e.status == NacpLoadStatus::None) {
|
if (e.status == title::NacpLoadStatus::None) {
|
||||||
m_thread_data->Push(e.app_id);
|
title::Push(e.app_id);
|
||||||
e.status = NacpLoadStatus::Progress;
|
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
|
// lazy load image
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
#include "download.hpp"
|
#include "download.hpp"
|
||||||
#include "dumper.hpp"
|
#include "dumper.hpp"
|
||||||
#include "image.hpp"
|
#include "image.hpp"
|
||||||
|
#include "title_info.hpp"
|
||||||
|
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
@@ -62,46 +63,6 @@ auto GetXciSizeFromRomSize(u8 rom_size) -> s64 {
|
|||||||
return 0;
|
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 {
|
struct DebugEventInfo {
|
||||||
u32 event_type;
|
u32 event_type;
|
||||||
u32 flags;
|
u32 flags;
|
||||||
@@ -132,7 +93,7 @@ auto GetDumpTypeStr(u8 type) -> const char* {
|
|||||||
|
|
||||||
auto BuildXciName(const ApplicationEntry& e) -> fs::FsPath {
|
auto BuildXciName(const ApplicationEntry& e) -> fs::FsPath {
|
||||||
fs::FsPath name_buf = e.lang_entry.name;
|
fs::FsPath name_buf = e.lang_entry.name;
|
||||||
utilsReplaceIllegalCharacters(name_buf, true);
|
title::utilsReplaceIllegalCharacters(name_buf, true);
|
||||||
|
|
||||||
fs::FsPath path;
|
fs::FsPath path;
|
||||||
std::snprintf(path, sizeof(path), "%s [%016lX][v%u]", name_buf.s, e.app_id, e.version);
|
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};
|
const Vec2 pad{0, 125 - v.h};
|
||||||
m_list = std::make_unique<List>(1, 3, m_pos, v, pad);
|
m_list = std::make_unique<List>(1, 3, m_pos, v, pad);
|
||||||
|
|
||||||
nsInitialize();
|
|
||||||
fsOpenDeviceOperator(std::addressof(m_dev_op));
|
fsOpenDeviceOperator(std::addressof(m_dev_op));
|
||||||
fsOpenGameCardDetectionEventNotifier(std::addressof(m_event_notifier));
|
fsOpenGameCardDetectionEventNotifier(std::addressof(m_event_notifier));
|
||||||
fsEventNotifierGetEventHandle(std::addressof(m_event_notifier), std::addressof(m_event), true);
|
fsEventNotifierGetEventHandle(std::addressof(m_event_notifier), std::addressof(m_event), true);
|
||||||
|
title::Init();
|
||||||
}
|
}
|
||||||
|
|
||||||
Menu::~Menu() {
|
Menu::~Menu() {
|
||||||
|
title::Exit();
|
||||||
GcUnmount();
|
GcUnmount();
|
||||||
eventClose(std::addressof(m_event));
|
eventClose(std::addressof(m_event));
|
||||||
fsEventNotifierClose(std::addressof(m_event_notifier));
|
fsEventNotifierClose(std::addressof(m_event_notifier));
|
||||||
fsDeviceOperatorClose(std::addressof(m_dev_op));
|
fsDeviceOperatorClose(std::addressof(m_dev_op));
|
||||||
nsExit();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Menu::Update(Controller* controller, TouchInfo* touch) {
|
void Menu::Update(Controller* controller, TouchInfo* touch) {
|
||||||
@@ -683,13 +644,6 @@ Result Menu::GcMount() {
|
|||||||
// load all control data, icons are loaded when displayed.
|
// load all control data, icons are loaded when displayed.
|
||||||
for (auto& e : m_entries) {
|
for (auto& e : m_entries) {
|
||||||
R_TRY(LoadControlData(e));
|
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) {
|
if (m_entries.size() > 1) {
|
||||||
@@ -909,47 +863,13 @@ void Menu::FreeImage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Result Menu::LoadControlData(ApplicationEntry& e) {
|
Result Menu::LoadControlData(ApplicationEntry& e) {
|
||||||
const auto id = e.app_id;
|
const auto data = title::LoadControlEntry(e.app_id);
|
||||||
e.control = std::make_unique<NsApplicationControlData>();
|
R_UNLESS(data.status == title::NacpLoadStatus::Loaded, 0x1);
|
||||||
|
|
||||||
if (hosversionBefore(20,0,0)) {
|
e.control = data.control;
|
||||||
TimeStamp ts;
|
e.jpeg_size = data.jpeg_size;
|
||||||
if (R_SUCCEEDED(nsGetApplicationControlData(NsApplicationControlSource_CacheOnly, id, e.control.get(), sizeof(NsApplicationControlData), &e.control_size))) {
|
e.lang_entry = data.lang;
|
||||||
log_write("\t\t[ns control cache] time taken: %.2fs %zums\n", ts.GetSecondsD(), ts.GetMs());
|
|
||||||
R_SUCCEED();
|
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Menu::OnChangeIndex(s64 new_index) {
|
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()));
|
this->SetSubHeading(std::to_string(index) + " / " + std::to_string(m_entries.size()));
|
||||||
|
|
||||||
const auto& e = m_entries[m_entry_index];
|
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;
|
TimeStamp ts;
|
||||||
const auto image = ImageLoadFromMemory({e.control->icon, jpeg_size}, ImageFlag_JPEG);
|
const auto image = ImageLoadFromMemory({e.control->icon, jpeg_size}, ImageFlag_JPEG);
|
||||||
|
|||||||
@@ -34,9 +34,6 @@
|
|||||||
namespace sphaira::ui::menu::save {
|
namespace sphaira::ui::menu::save {
|
||||||
namespace {
|
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_MAGIC = 0x4A4B5356; // JKSV
|
||||||
constexpr u32 NX_SAVE_META_VERSION = 1;
|
constexpr u32 NX_SAVE_META_VERSION = 1;
|
||||||
constexpr const char* NX_SAVE_META_NAME = ".nx_save_meta.bin";
|
constexpr const char* NX_SAVE_META_NAME = ".nx_save_meta.bin";
|
||||||
@@ -59,34 +56,6 @@ struct NXSaveMeta {
|
|||||||
};
|
};
|
||||||
static_assert(sizeof(NXSaveMeta) == 128);
|
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) {
|
void GetFsSaveAttr(const AccountProfileBase& acc, u8 data_type, FsSaveDataSpaceId& space_id, FsSaveDataFilter& filter) {
|
||||||
std::memset(&filter, 0, sizeof(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 {
|
auto GetSaveFolder(u8 data_type) -> fs::FsPath {
|
||||||
switch (data_type) {
|
switch (data_type) {
|
||||||
case FsSaveDataType_System: return "Save System";
|
case FsSaveDataType_System: return "Save System";
|
||||||
@@ -344,7 +211,7 @@ auto GetSystemSaveName(u64 system_save_data_id) -> const char* {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void FakeNacpEntryForSystem(Entry& e) {
|
void FakeNacpEntryForSystem(Entry& e) {
|
||||||
e.status = NacpLoadStatus::Loaded;
|
e.status = title::NacpLoadStatus::Loaded;
|
||||||
|
|
||||||
// fake the nacp entry
|
// 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::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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Result GetControlPathFromStatus(const NsApplicationContentMetaStatus& status, u64* out_program_id, fs::FsPath* out_path) {
|
void LoadResultIntoEntry(Entry& e, const title::ThreadResultData& result) {
|
||||||
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) {
|
|
||||||
e.status = result.status;
|
e.status = result.status;
|
||||||
e.control = result.control;
|
e.control = result.control;
|
||||||
e.jpeg_size= result.jpeg_size;
|
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) {
|
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) {
|
if (e.save_data_type == FsSaveDataType_System || e.save_data_type == FsSaveDataType_SystemBcat) {
|
||||||
FakeNacpEntryForSystem(e);
|
FakeNacpEntryForSystem(e);
|
||||||
} else {
|
} else {
|
||||||
const auto result = LoadControlEntry(e.application_id);
|
const auto result = title::LoadControlEntry(e.application_id);
|
||||||
LoadResultIntoEntry(e, result);
|
LoadResultIntoEntry(e, result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (force_image_load && e.status == NacpLoadStatus::Loaded) {
|
if (force_image_load && e.status == title::NacpLoadStatus::Loaded) {
|
||||||
LoadControlImage(e);
|
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 {
|
struct HashStr {
|
||||||
char str[0x21];
|
char str[0x21];
|
||||||
};
|
};
|
||||||
@@ -530,7 +272,7 @@ HashStr hexIdToStr(auto id) {
|
|||||||
|
|
||||||
auto BuildSaveName(const Entry& e) -> fs::FsPath {
|
auto BuildSaveName(const Entry& e) -> fs::FsPath {
|
||||||
fs::FsPath name_buf = e.GetName();
|
fs::FsPath name_buf = e.GetName();
|
||||||
utilsReplaceIllegalCharacters(name_buf, true);
|
title::utilsReplaceIllegalCharacters(name_buf, true);
|
||||||
return name_buf;
|
return name_buf;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -550,122 +292,12 @@ void FreeEntry(NVGcontext* vg, Entry& e) {
|
|||||||
e.image = 0;
|
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
|
} // namespace
|
||||||
|
|
||||||
void SignalChange() {
|
void SignalChange() {
|
||||||
ueventSignal(&g_change_uevent);
|
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} {
|
Menu::Menu(u32 flags) : grid::Menu{"Saves"_i18n, flags} {
|
||||||
this->SetActions(
|
this->SetActions(
|
||||||
std::make_pair(Button::L3, Action{[this](){
|
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]);
|
log_write("[SAVE] account uid is not found: 0x%016lX%016lX\n", uid.uid[0], uid.uid[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (auto& e : ncm_entries) {
|
title::Init();
|
||||||
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);
|
|
||||||
ueventCreate(&g_change_uevent, true);
|
ueventCreate(&g_change_uevent, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
Menu::~Menu() {
|
Menu::~Menu() {
|
||||||
m_thread_data.Close();
|
title::Exit();
|
||||||
|
|
||||||
for (auto& e : ncm_entries) {
|
|
||||||
e.Close();
|
|
||||||
}
|
|
||||||
|
|
||||||
FreeEntries();
|
FreeEntries();
|
||||||
nsExit();
|
nsExit();
|
||||||
|
|
||||||
threadWaitForExit(&m_thread);
|
|
||||||
threadClose(&m_thread);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Menu::Update(Controller* controller, TouchInfo* touch) {
|
void Menu::Update(Controller* controller, TouchInfo* touch) {
|
||||||
@@ -877,30 +496,21 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
|||||||
const int image_load_max = 2;
|
const int image_load_max = 2;
|
||||||
int image_load_count = 0;
|
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) {
|
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;
|
const auto& [x, y, w, h] = v;
|
||||||
auto& e = m_entries[pos];
|
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) {
|
if (m_data_type != FsSaveDataType_System && m_data_type != FsSaveDataType_SystemBcat) {
|
||||||
m_thread_data.Push(e.application_id);
|
title::Push(e.application_id);
|
||||||
e.status = NacpLoadStatus::Progress;
|
e.status = title::NacpLoadStatus::Progress;
|
||||||
} else {
|
} else {
|
||||||
FakeNacpEntryForSystem(e);
|
FakeNacpEntryForSystem(e);
|
||||||
}
|
}
|
||||||
|
} else if (e.status == title::NacpLoadStatus::Progress) {
|
||||||
|
if (const auto data = title::Get(e.application_id)) {
|
||||||
|
LoadResultIntoEntry(e, *data);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// lazy load image
|
// 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);
|
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);
|
std::snprintf(path, sizeof(path), "%s/%s - %s.zip", base.s, name_buf.s, time);
|
||||||
} else {
|
} else {
|
||||||
std::snprintf(path, sizeof(path), "%s/%s.zip", base.s, time);
|
std::snprintf(path, sizeof(path), "%s/%s.zip", base.s, time);
|
||||||
|
|||||||
Reference in New Issue
Block a user