mirror of
https://github.com/Dark98/threeSD.git
synced 2026-07-03 00:38:58 +00:00
Add support for system titles
This commit is contained in:
Vendored
+53
-2
@@ -16,7 +16,10 @@ end
|
|||||||
|
|
||||||
set PREVIEW_MODE "threeSD Dumper\nby zhaowenlan1779\n \nWorking..."
|
set PREVIEW_MODE "threeSD Dumper\nby zhaowenlan1779\n \nWorking..."
|
||||||
|
|
||||||
|
# === movable.sed
|
||||||
cp -w -n "1:/private/movable.sed" $[OUT]/movable.sed
|
cp -w -n "1:/private/movable.sed" $[OUT]/movable.sed
|
||||||
|
|
||||||
|
# === bootrom
|
||||||
if find "M:/boot9.bin" NULL
|
if find "M:/boot9.bin" NULL
|
||||||
cp -w -n "M:/boot9.bin" $[OUT]/boot9.bin
|
cp -w -n "M:/boot9.bin" $[OUT]/boot9.bin
|
||||||
elif find "0:/3DS/boot9.bin" NULL
|
elif find "0:/3DS/boot9.bin" NULL
|
||||||
@@ -26,6 +29,7 @@ else
|
|||||||
goto Exit
|
goto Exit
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# === Safe mode firm
|
||||||
if not find $[OUT]/firm NULL
|
if not find $[OUT]/firm NULL
|
||||||
mkdir $[OUT]/firm
|
mkdir $[OUT]/firm
|
||||||
end
|
end
|
||||||
@@ -47,10 +51,12 @@ else
|
|||||||
decrypt $[APP]
|
decrypt $[APP]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# === Secret sector (N3DS only)
|
||||||
if chk $[ONTYPE] "N3DS"
|
if chk $[ONTYPE] "N3DS"
|
||||||
cp -w -n "S:/sector0x96.bin" $[OUT]/sector0x96.bin
|
cp -w -n "S:/sector0x96.bin" $[OUT]/sector0x96.bin
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# === System Archives
|
||||||
if not find $[OUT]/sysarchives NULL
|
if not find $[OUT]/sysarchives NULL
|
||||||
mkdir $[OUT]/sysarchives
|
mkdir $[OUT]/sysarchives
|
||||||
end
|
end
|
||||||
@@ -98,10 +104,52 @@ find 1:/title/000400db/00010302/content/*.app APP
|
|||||||
cp -w -n $[APP] $[OUT]/sysarchives/000400db/00010302.app
|
cp -w -n $[APP] $[OUT]/sysarchives/000400db/00010302.app
|
||||||
decrypt $[OUT]/sysarchives/000400db/00010302.app
|
decrypt $[OUT]/sysarchives/000400db/00010302.app
|
||||||
|
|
||||||
# Config savegame
|
# === Config savegame
|
||||||
cp -w -n 1:/data/$[SYSID0]/sysdata/00010017/00000000 $[OUT]/config.sav
|
cp -w -n 1:/data/$[SYSID0]/sysdata/00010017/00000000 $[OUT]/config.sav
|
||||||
|
|
||||||
# seeddb.bin
|
# === Other system titles
|
||||||
|
if not find $[OUT]/title NULL
|
||||||
|
mkdir $[OUT]/title
|
||||||
|
end
|
||||||
|
|
||||||
|
# System Applications
|
||||||
|
if not find $[OUT]/title/00040010 NULL
|
||||||
|
mkdir $[OUT]/title/00040010
|
||||||
|
end
|
||||||
|
cp -w -n "1:/title/00040010" $[OUT]/title/00040010
|
||||||
|
|
||||||
|
# System Data Archives
|
||||||
|
if not find $[OUT]/title/0004001b NULL
|
||||||
|
mkdir $[OUT]/title/0004001b
|
||||||
|
end
|
||||||
|
cp -w -n "1:/title/0004001b" $[OUT]/title/0004001b
|
||||||
|
|
||||||
|
# System Applets
|
||||||
|
if not find $[OUT]/title/00040030 NULL
|
||||||
|
mkdir $[OUT]/title/00040030
|
||||||
|
end
|
||||||
|
cp -w -n "1:/title/00040030" $[OUT]/title/00040030
|
||||||
|
|
||||||
|
# 0004009b Shared Data Archives skipped (included in sysarchives)
|
||||||
|
|
||||||
|
# System Data Archives
|
||||||
|
if not find $[OUT]/title/000400db NULL
|
||||||
|
mkdir $[OUT]/title/000400db
|
||||||
|
end
|
||||||
|
cp -w -n "1:/title/000400db" $[OUT]/title/000400db
|
||||||
|
|
||||||
|
# System Modules
|
||||||
|
if not find $[OUT]/title/00040130 NULL
|
||||||
|
mkdir $[OUT]/title/00040130
|
||||||
|
end
|
||||||
|
cp -w -n "1:/title/00040130" $[OUT]/title/00040130
|
||||||
|
|
||||||
|
# 00040138 System Firmware skipped (dumped above)
|
||||||
|
|
||||||
|
# Already included in sysarchives
|
||||||
|
rm $[OUT]/title/000400db/00010302
|
||||||
|
|
||||||
|
# === seeddb.bin
|
||||||
sdump -o -s -w seeddb.bin
|
sdump -o -s -w seeddb.bin
|
||||||
if not find 0:/gm9/out/seeddb.bin NULL
|
if not find 0:/gm9/out/seeddb.bin NULL
|
||||||
echo "WARNING: \nseeddb.bin couldn't be built. \nThis may be because your system \ndoes not have any seeds. \nOtherwise, imported games may fail \nto run if they use seed encryption."
|
echo "WARNING: \nseeddb.bin couldn't be built. \nThis may be because your system \ndoes not have any seeds. \nOtherwise, imported games may fail \nto run if they use seed encryption."
|
||||||
@@ -110,6 +158,9 @@ else
|
|||||||
rm "0:/gm9/out/seeddb.bin"
|
rm "0:/gm9/out/seeddb.bin"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# === Write version
|
||||||
|
dumptxt $[OUT]/version.txt 1
|
||||||
|
|
||||||
set PREVIEW_MODE "threeSD Dumper\nby zhaowenlan1779\n \nSuccess!"
|
set PREVIEW_MODE "threeSD Dumper\nby zhaowenlan1779\n \nSuccess!"
|
||||||
echo "Successfully dumped necessary\nfiles for threeSD."
|
echo "Successfully dumped necessary\nfiles for threeSD."
|
||||||
|
|
||||||
|
|||||||
+206
-45
@@ -81,18 +81,25 @@ bool SDMCImporter::ImportContent(const ContentSpecifier& specifier,
|
|||||||
return ImportSystemArchive(specifier.id, callback);
|
return ImportSystemArchive(specifier.id, callback);
|
||||||
case ContentType::Sysdata:
|
case ContentType::Sysdata:
|
||||||
return ImportSysdata(specifier.id, callback);
|
return ImportSysdata(specifier.id, callback);
|
||||||
|
case ContentType::SystemTitle:
|
||||||
|
return ImportNandTitle(specifier, callback);
|
||||||
default:
|
default:
|
||||||
UNREACHABLE();
|
UNREACHABLE();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool SDMCImporter::ImportTitle(const ContentSpecifier& specifier,
|
namespace {
|
||||||
const ProgressCallback& callback) {
|
|
||||||
decryptor->Reset(specifier.maximum_size);
|
template <typename Dec>
|
||||||
|
bool ImportTitleGeneric(Dec& decryptor, const std::string& base_path,
|
||||||
|
const ContentSpecifier& specifier,
|
||||||
|
const std::function<bool(const std::string&)>& decryption_func) {
|
||||||
|
|
||||||
|
decryptor.Reset(specifier.maximum_size);
|
||||||
const FileUtil::DirectoryEntryCallable DirectoryEntryCallback =
|
const FileUtil::DirectoryEntryCallable DirectoryEntryCallback =
|
||||||
[this, size = config.sdmc_path.size(), callback,
|
[size = base_path.size(), &DirectoryEntryCallback,
|
||||||
&DirectoryEntryCallback](u64* /*num_entries_out*/, const std::string& directory,
|
&decryption_func](u64* /*num_entries_out*/, const std::string& directory,
|
||||||
const std::string& virtual_name) {
|
const std::string& virtual_name) {
|
||||||
if (FileUtil::IsDirectory(directory + virtual_name + "/")) {
|
if (FileUtil::IsDirectory(directory + virtual_name + "/")) {
|
||||||
if (virtual_name == "cmd") {
|
if (virtual_name == "cmd") {
|
||||||
return true; // Skip cmd (not used in Citra)
|
return true; // Skip cmd (not used in Citra)
|
||||||
@@ -102,6 +109,19 @@ bool SDMCImporter::ImportTitle(const ContentSpecifier& specifier,
|
|||||||
DirectoryEntryCallback);
|
DirectoryEntryCallback);
|
||||||
}
|
}
|
||||||
const auto filepath = (directory + virtual_name).substr(size - 1);
|
const auto filepath = (directory + virtual_name).substr(size - 1);
|
||||||
|
return decryption_func(filepath);
|
||||||
|
};
|
||||||
|
const auto path = fmt::format("title/{:08x}/{:08x}/content/", (specifier.id >> 32),
|
||||||
|
(specifier.id & 0xFFFFFFFF));
|
||||||
|
return FileUtil::ForeachDirectoryEntry(nullptr, base_path + path, DirectoryEntryCallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
bool SDMCImporter::ImportTitle(const ContentSpecifier& specifier,
|
||||||
|
const ProgressCallback& callback) {
|
||||||
|
return ImportTitleGeneric(
|
||||||
|
*decryptor, config.sdmc_path, specifier, [this, &callback](const std::string& filepath) {
|
||||||
return decryptor->DecryptAndWriteFile(
|
return decryptor->DecryptAndWriteFile(
|
||||||
filepath,
|
filepath,
|
||||||
FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir) +
|
FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir) +
|
||||||
@@ -109,11 +129,31 @@ bool SDMCImporter::ImportTitle(const ContentSpecifier& specifier,
|
|||||||
"3DS/00000000000000000000000000000000/00000000000000000000000000000000" +
|
"3DS/00000000000000000000000000000000/00000000000000000000000000000000" +
|
||||||
filepath,
|
filepath,
|
||||||
callback);
|
callback);
|
||||||
};
|
});
|
||||||
const auto path = fmt::format("title/{:08x}/{:08x}/content/", (specifier.id >> 32),
|
}
|
||||||
(specifier.id & 0xFFFFFFFF));
|
|
||||||
return FileUtil::ForeachDirectoryEntry(nullptr, config.sdmc_path + path,
|
bool SDMCImporter::ImportNandTitle(const ContentSpecifier& specifier,
|
||||||
DirectoryEntryCallback);
|
const ProgressCallback& callback) {
|
||||||
|
|
||||||
|
const auto base_path =
|
||||||
|
config.system_titles_path.substr(0, config.system_titles_path.size() - 6);
|
||||||
|
QuickDecryptor<> quick_decryptor;
|
||||||
|
return ImportTitleGeneric(
|
||||||
|
quick_decryptor, base_path, specifier,
|
||||||
|
[&base_path, &quick_decryptor, &callback](const std::string& filepath) {
|
||||||
|
const auto physical_path = base_path + filepath.substr(1);
|
||||||
|
const auto citra_path = FileUtil::GetUserPath(FileUtil::UserPath::NANDDir) +
|
||||||
|
"00000000000000000000000000000000" + filepath;
|
||||||
|
if (!FileUtil::CreateFullPath(citra_path)) {
|
||||||
|
LOG_ERROR(Core, "Could not create path {}", citra_path);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Do not specify keys: plain copy with progress.
|
||||||
|
return quick_decryptor.DecryptAndWriteFile(
|
||||||
|
std::make_shared<FileUtil::IOFile>(physical_path, "rb"),
|
||||||
|
FileUtil::GetSize(physical_path),
|
||||||
|
std::make_shared<FileUtil::IOFile>(citra_path, "wb"), callback);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
bool SDMCImporter::ImportSavegame(u64 id, [[maybe_unused]] const ProgressCallback& callback) {
|
bool SDMCImporter::ImportSavegame(u64 id, [[maybe_unused]] const ProgressCallback& callback) {
|
||||||
@@ -310,6 +350,7 @@ bool SDMCImporter::ImportSysdata(u64 id, [[maybe_unused]] const ProgressCallback
|
|||||||
std::vector<ContentSpecifier> SDMCImporter::ListContent() const {
|
std::vector<ContentSpecifier> SDMCImporter::ListContent() const {
|
||||||
std::vector<ContentSpecifier> content_list;
|
std::vector<ContentSpecifier> content_list;
|
||||||
ListTitle(content_list);
|
ListTitle(content_list);
|
||||||
|
ListNandTitle(content_list);
|
||||||
ListExtdata(content_list);
|
ListExtdata(content_list);
|
||||||
ListSystemArchive(content_list);
|
ListSystemArchive(content_list);
|
||||||
ListSysdata(content_list);
|
ListSysdata(content_list);
|
||||||
@@ -319,12 +360,10 @@ std::vector<ContentSpecifier> SDMCImporter::ListContent() const {
|
|||||||
// Regex for half Title IDs
|
// Regex for half Title IDs
|
||||||
static const std::regex title_regex{"[0-9a-f]{8}"};
|
static const std::regex title_regex{"[0-9a-f]{8}"};
|
||||||
|
|
||||||
static bool LoadTMD(const std::string& sdmc_path, const std::string& path, SDMCDecryptor& decryptor,
|
static std::string FindTMD(const std::string& path) {
|
||||||
TitleMetadata& out) {
|
|
||||||
|
|
||||||
std::string title_metadata;
|
std::string title_metadata;
|
||||||
const bool ret = FileUtil::ForeachDirectoryEntry(
|
const bool ret = FileUtil::ForeachDirectoryEntry(
|
||||||
nullptr, sdmc_path + path,
|
nullptr, path,
|
||||||
[&title_metadata](u64* /*num_entries_out*/, const std::string& directory,
|
[&title_metadata](u64* /*num_entries_out*/, const std::string& directory,
|
||||||
const std::string& virtual_name) {
|
const std::string& virtual_name) {
|
||||||
if (FileUtil::IsDirectory(directory + virtual_name)) {
|
if (FileUtil::IsDirectory(directory + virtual_name)) {
|
||||||
@@ -342,35 +381,32 @@ static bool LoadTMD(const std::string& sdmc_path, const std::string& path, SDMCD
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (ret) { // TMD not found
|
if (ret) { // TMD not found
|
||||||
return false;
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!FileUtil::Exists(sdmc_path + path + title_metadata)) {
|
if (!FileUtil::Exists(path + title_metadata)) {
|
||||||
// Probably TMD is not directly inside, aborting.
|
// Probably TMD is not directly inside, aborting.
|
||||||
return false;
|
return {};
|
||||||
}
|
}
|
||||||
|
return path + title_metadata;
|
||||||
return out.Load(decryptor.DecryptFile(path + title_metadata)) == ResultStatus::Success;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::tuple<std::string, u64, EncryptionType, bool, std::vector<u16>> SDMCImporter::LoadTitleData(
|
static bool LoadTMD(const std::string& sdmc_path, const std::string& path, SDMCDecryptor& decryptor,
|
||||||
const std::string& path) const {
|
TitleMetadata& out) {
|
||||||
// Remove trailing '/'
|
|
||||||
const auto sdmc_path = config.sdmc_path.substr(0, config.sdmc_path.size() - 1);
|
|
||||||
|
|
||||||
TitleMetadata tmd;
|
const auto tmd = FindTMD(sdmc_path + path.substr(1));
|
||||||
if (!LoadTMD(sdmc_path, path, *decryptor, tmd)) {
|
if (tmd.empty()) {
|
||||||
return {};
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto boot_content_path = fmt::format("{}{:08x}.app", path, tmd.GetBootContentID());
|
return out.Load(decryptor.DecryptFile(tmd.substr(sdmc_path.size() - 1))) ==
|
||||||
|
ResultStatus::Success;
|
||||||
|
}
|
||||||
|
|
||||||
NCCHContainer ncch(config.sdmc_path, boot_content_path);
|
// English short title name, extdata id, encryption, seed, icon
|
||||||
auto ret2 = ncch.Load();
|
template <typename File>
|
||||||
if (ret2 != ResultStatus::Success) {
|
std::tuple<std::string, u64, EncryptionType, bool, std::vector<u16>> LoadTitleData(
|
||||||
LOG_CRITICAL(Core, "failed to load ncch: {}", ret2);
|
NCCHContainer<File>& ncch) {
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<u8> smdh_buffer;
|
std::vector<u8> smdh_buffer;
|
||||||
if (ncch.LoadSectionExeFS("icon", smdh_buffer) != ResultStatus::Success) {
|
if (ncch.LoadSectionExeFS("icon", smdh_buffer) != ResultStatus::Success) {
|
||||||
@@ -416,7 +452,8 @@ bool SDMCImporter::DumpCXI(const ContentSpecifier& specifier, const std::string&
|
|||||||
|
|
||||||
const auto boot_content_path =
|
const auto boot_content_path =
|
||||||
fmt::format("{}{:08x}.app", content_path, tmd.GetBootContentID());
|
fmt::format("{}{:08x}.app", content_path, tmd.GetBootContentID());
|
||||||
dump_cxi_ncch = std::make_unique<NCCHContainer>(config.sdmc_path, boot_content_path);
|
dump_cxi_ncch = std::make_unique<NCCHContainer<SDMCFile>>(
|
||||||
|
std::make_shared<SDMCFile>(config.sdmc_path, boot_content_path, "rb"));
|
||||||
return dump_cxi_ncch->DecryptToFile(destination, callback) == ResultStatus::Success;
|
return dump_cxi_ncch->DecryptToFile(destination, callback) == ResultStatus::Success;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -429,8 +466,9 @@ void SDMCImporter::ListTitle(std::vector<ContentSpecifier>& out) const {
|
|||||||
u64 high_id) {
|
u64 high_id) {
|
||||||
FileUtil::ForeachDirectoryEntry(
|
FileUtil::ForeachDirectoryEntry(
|
||||||
nullptr, fmt::format("{}title/{:08x}/", sdmc_path, high_id),
|
nullptr, fmt::format("{}title/{:08x}/", sdmc_path, high_id),
|
||||||
[this, type, high_id, &out](u64* /*num_entries_out*/, const std::string& directory,
|
[this, &sdmc_path, type, high_id, &out](u64* /*num_entries_out*/,
|
||||||
const std::string& virtual_name) {
|
const std::string& directory,
|
||||||
|
const std::string& virtual_name) {
|
||||||
if (!FileUtil::IsDirectory(directory + virtual_name + "/")) {
|
if (!FileUtil::IsDirectory(directory + virtual_name + "/")) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -447,14 +485,38 @@ void SDMCImporter::ListTitle(std::vector<ContentSpecifier>& out) const {
|
|||||||
FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir), high_id, virtual_name);
|
FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir), high_id, virtual_name);
|
||||||
|
|
||||||
if (FileUtil::Exists(directory + virtual_name + "/content/")) {
|
if (FileUtil::Exists(directory + virtual_name + "/content/")) {
|
||||||
const auto content_path =
|
do {
|
||||||
fmt::format("/title/{:08x}/{}/content/", high_id, virtual_name);
|
const auto content_path =
|
||||||
const auto& [name, extdata_id, encryption, seed_crypto, icon] =
|
fmt::format("/title/{:08x}/{}/content/", high_id, virtual_name);
|
||||||
LoadTitleData(content_path);
|
|
||||||
out.push_back(
|
TitleMetadata tmd;
|
||||||
{type, id, FileUtil::Exists(citra_path + "content/"),
|
if (!LoadTMD(sdmc_path, content_path, *decryptor, tmd)) {
|
||||||
FileUtil::GetDirectoryTreeSize(directory + virtual_name + "/content/"),
|
LOG_WARNING(Core, "Could not load tmd from {}", content_path);
|
||||||
name, extdata_id, encryption, seed_crypto, icon});
|
out.push_back({type, id, FileUtil::Exists(citra_path + "content/"),
|
||||||
|
FileUtil::GetDirectoryTreeSize(directory + virtual_name +
|
||||||
|
"/content/")});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto boot_content_path =
|
||||||
|
fmt::format("{}{:08x}.app", content_path, tmd.GetBootContentID());
|
||||||
|
NCCHContainer<SDMCFile> ncch(
|
||||||
|
std::make_shared<SDMCFile>(sdmc_path, boot_content_path, "rb"));
|
||||||
|
if (ncch.Load() != ResultStatus::Success) {
|
||||||
|
LOG_WARNING(Core, "Could not load NCCH {}", boot_content_path);
|
||||||
|
out.push_back({type, id, FileUtil::Exists(citra_path + "content/"),
|
||||||
|
FileUtil::GetDirectoryTreeSize(directory + virtual_name +
|
||||||
|
"/content/")});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto& [name, extdata_id, encryption, seed_crypto, icon] =
|
||||||
|
LoadTitleData(ncch);
|
||||||
|
out.push_back(
|
||||||
|
{type, id, FileUtil::Exists(citra_path + "content/"),
|
||||||
|
FileUtil::GetDirectoryTreeSize(directory + virtual_name + "/content/"),
|
||||||
|
name, extdata_id, encryption, seed_crypto, icon});
|
||||||
|
} while (false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type != ContentType::Application) {
|
if (type != ContentType::Application) {
|
||||||
@@ -483,6 +545,96 @@ void SDMCImporter::ListTitle(std::vector<ContentSpecifier>& out) const {
|
|||||||
ProcessDirectory(ContentType::DLC, 0x0004008c);
|
ProcessDirectory(ContentType::DLC, 0x0004008c);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool LoadNandTMD(const std::string& path, TitleMetadata& out) {
|
||||||
|
|
||||||
|
const auto tmd = FindTMD(path);
|
||||||
|
if (tmd.empty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
FileUtil::IOFile file(tmd, "rb");
|
||||||
|
if (!file) {
|
||||||
|
LOG_ERROR(Core, "Could not open file {}", tmd);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (file.GetSize() >= 1024 * 1024) { // Too big
|
||||||
|
LOG_ERROR(Core, "TMD {} too big", tmd);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<u8> data(file.GetSize());
|
||||||
|
if (file.ReadBytes(data.data(), data.size()) != data.size()) {
|
||||||
|
LOG_ERROR(Core, "Could not read from {}", tmd);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return out.Load(std::move(data)) == ResultStatus::Success;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Simplify.
|
||||||
|
void SDMCImporter::ListNandTitle(std::vector<ContentSpecifier>& out) const {
|
||||||
|
const auto ProcessDirectory = [&out,
|
||||||
|
&system_titles_path = config.system_titles_path](u64 high_id) {
|
||||||
|
FileUtil::ForeachDirectoryEntry(
|
||||||
|
nullptr, fmt::format("{}{:08x}/", system_titles_path, high_id),
|
||||||
|
[high_id, &out](u64* /*num_entries_out*/, const std::string& directory,
|
||||||
|
const std::string& virtual_name) {
|
||||||
|
if (!FileUtil::IsDirectory(directory + virtual_name + "/")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!std::regex_match(virtual_name, title_regex)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const u64 id = (high_id << 32) + std::stoull(virtual_name, nullptr, 16);
|
||||||
|
const auto citra_path = fmt::format(
|
||||||
|
"{}00000000000000000000000000000000/title/{:08x}/{}/",
|
||||||
|
FileUtil::GetUserPath(FileUtil::UserPath::NANDDir), high_id, virtual_name);
|
||||||
|
|
||||||
|
const auto content_path = directory + virtual_name + "/content/";
|
||||||
|
if (FileUtil::Exists(content_path)) {
|
||||||
|
do {
|
||||||
|
TitleMetadata tmd;
|
||||||
|
if (!LoadNandTMD(content_path, tmd)) {
|
||||||
|
LOG_WARNING(Core, "Could not load tmd from {}", content_path);
|
||||||
|
out.push_back({ContentType::SystemTitle, id,
|
||||||
|
FileUtil::Exists(citra_path + "content/"),
|
||||||
|
FileUtil::GetDirectoryTreeSize(content_path)});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto boot_content_path =
|
||||||
|
fmt::format("{}{:08x}.app", content_path, tmd.GetBootContentID());
|
||||||
|
NCCHContainer<FileUtil::IOFile> ncch(
|
||||||
|
std::make_shared<FileUtil::IOFile>(boot_content_path, "rb"));
|
||||||
|
if (ncch.Load() != ResultStatus::Success) {
|
||||||
|
LOG_WARNING(Core, "Could not load NCCH {}", boot_content_path);
|
||||||
|
out.push_back({ContentType::SystemTitle, id,
|
||||||
|
FileUtil::Exists(citra_path + "content/"),
|
||||||
|
FileUtil::GetDirectoryTreeSize(content_path)});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto& [name, extdata_id, encryption, seed_crypto, icon] =
|
||||||
|
LoadTitleData(ncch);
|
||||||
|
out.push_back(
|
||||||
|
{ContentType::SystemTitle, id,
|
||||||
|
FileUtil::Exists(citra_path + "content/"),
|
||||||
|
FileUtil::GetDirectoryTreeSize(directory + virtual_name + "/content/"),
|
||||||
|
name, extdata_id, encryption, seed_crypto, icon});
|
||||||
|
} while (false);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
ProcessDirectory(0x00040010);
|
||||||
|
ProcessDirectory(0x0004001b);
|
||||||
|
ProcessDirectory(0x00040030);
|
||||||
|
ProcessDirectory(0x000400db);
|
||||||
|
ProcessDirectory(0x00040130);
|
||||||
|
}
|
||||||
|
|
||||||
void SDMCImporter::ListExtdata(std::vector<ContentSpecifier>& out) const {
|
void SDMCImporter::ListExtdata(std::vector<ContentSpecifier>& out) const {
|
||||||
FileUtil::ForeachDirectoryEntry(
|
FileUtil::ForeachDirectoryEntry(
|
||||||
nullptr, fmt::format("{}extdata/00000000/", config.sdmc_path),
|
nullptr, fmt::format("{}extdata/00000000/", config.sdmc_path),
|
||||||
@@ -627,6 +779,8 @@ void SDMCImporter::DeleteContent(const ContentSpecifier& specifier) {
|
|||||||
return DeleteSystemArchive(specifier.id);
|
return DeleteSystemArchive(specifier.id);
|
||||||
case ContentType::Sysdata:
|
case ContentType::Sysdata:
|
||||||
return DeleteSysdata(specifier.id);
|
return DeleteSysdata(specifier.id);
|
||||||
|
case ContentType::SystemTitle:
|
||||||
|
return DeleteNandTitle(specifier.id);
|
||||||
default:
|
default:
|
||||||
UNREACHABLE();
|
UNREACHABLE();
|
||||||
}
|
}
|
||||||
@@ -640,6 +794,12 @@ void SDMCImporter::DeleteTitle(u64 id) const {
|
|||||||
FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir), (id >> 32), (id & 0xFFFFFFFF)));
|
FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir), (id >> 32), (id & 0xFFFFFFFF)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SDMCImporter::DeleteNandTitle(u64 id) const {
|
||||||
|
FileUtil::DeleteDirRecursively(fmt::format(
|
||||||
|
"{}00000000000000000000000000000000/title/{:08x}/{:08x}/content/",
|
||||||
|
FileUtil::GetUserPath(FileUtil::UserPath::NANDDir), (id >> 32), (id & 0xFFFFFFFF)));
|
||||||
|
}
|
||||||
|
|
||||||
void SDMCImporter::DeleteSavegame(u64 id) const {
|
void SDMCImporter::DeleteSavegame(u64 id) const {
|
||||||
FileUtil::DeleteDirRecursively(fmt::format(
|
FileUtil::DeleteDirRecursively(fmt::format(
|
||||||
"{}Nintendo "
|
"{}Nintendo "
|
||||||
@@ -721,6 +881,7 @@ std::vector<Config> LoadPresetConfig(std::string mount_point) {
|
|||||||
LOAD_DATA(secret_sector_path, SECRET_SECTOR);
|
LOAD_DATA(secret_sector_path, SECRET_SECTOR);
|
||||||
LOAD_DATA(config_savegame_path, "config.sav");
|
LOAD_DATA(config_savegame_path, "config.sav");
|
||||||
LOAD_DATA(system_archives_path, "sysarchives/");
|
LOAD_DATA(system_archives_path, "sysarchives/");
|
||||||
|
LOAD_DATA(system_titles_path, "title/");
|
||||||
#undef LOAD_DATA
|
#undef LOAD_DATA
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+9
-10
@@ -26,6 +26,7 @@ enum class ContentType {
|
|||||||
Extdata,
|
Extdata,
|
||||||
SystemArchive,
|
SystemArchive,
|
||||||
Sysdata,
|
Sysdata,
|
||||||
|
SystemTitle,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -77,8 +78,12 @@ struct Config {
|
|||||||
std::string config_savegame_path; ///< Path to config savegame (Sysdata 5)
|
std::string config_savegame_path; ///< Path to config savegame (Sysdata 5)
|
||||||
|
|
||||||
std::string system_archives_path; ///< Path to system archives.
|
std::string system_archives_path; ///< Path to system archives.
|
||||||
|
std::string system_titles_path; ///< Path to system titles.
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class SDMCFile;
|
||||||
|
|
||||||
|
template <typename File>
|
||||||
class NCCHContainer;
|
class NCCHContainer;
|
||||||
|
|
||||||
class SDMCImporter {
|
class SDMCImporter {
|
||||||
@@ -140,37 +145,31 @@ private:
|
|||||||
bool Init();
|
bool Init();
|
||||||
|
|
||||||
bool ImportTitle(const ContentSpecifier& specifier, const ProgressCallback& callback);
|
bool ImportTitle(const ContentSpecifier& specifier, const ProgressCallback& callback);
|
||||||
|
bool ImportNandTitle(const ContentSpecifier& specifier, const ProgressCallback& callback);
|
||||||
bool ImportSavegame(u64 id, const ProgressCallback& callback);
|
bool ImportSavegame(u64 id, const ProgressCallback& callback);
|
||||||
bool ImportExtdata(u64 id, const ProgressCallback& callback);
|
bool ImportExtdata(u64 id, const ProgressCallback& callback);
|
||||||
bool ImportSystemArchive(u64 id, const ProgressCallback& callback);
|
bool ImportSystemArchive(u64 id, const ProgressCallback& callback);
|
||||||
bool ImportSysdata(u64 id, const ProgressCallback& callback);
|
bool ImportSysdata(u64 id, const ProgressCallback& callback);
|
||||||
|
|
||||||
void ListTitle(std::vector<ContentSpecifier>& out) const;
|
void ListTitle(std::vector<ContentSpecifier>& out) const;
|
||||||
|
void ListNandTitle(std::vector<ContentSpecifier>& out) const;
|
||||||
void ListExtdata(std::vector<ContentSpecifier>& out) const;
|
void ListExtdata(std::vector<ContentSpecifier>& out) const;
|
||||||
void ListSystemArchive(std::vector<ContentSpecifier>& out) const;
|
void ListSystemArchive(std::vector<ContentSpecifier>& out) const;
|
||||||
void ListSysdata(std::vector<ContentSpecifier>& out) const;
|
void ListSysdata(std::vector<ContentSpecifier>& out) const;
|
||||||
|
|
||||||
void DeleteTitle(u64 id) const;
|
void DeleteTitle(u64 id) const;
|
||||||
|
void DeleteNandTitle(u64 id) const;
|
||||||
void DeleteSavegame(u64 id) const;
|
void DeleteSavegame(u64 id) const;
|
||||||
void DeleteExtdata(u64 id) const;
|
void DeleteExtdata(u64 id) const;
|
||||||
void DeleteSystemArchive(u64 id) const;
|
void DeleteSystemArchive(u64 id) const;
|
||||||
void DeleteSysdata(u64 id) const;
|
void DeleteSysdata(u64 id) const;
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads the English short title name, extdata id, encryption and icon of a title.
|
|
||||||
* @param path Path of the 'content' folder relative to the SDMC root folder.
|
|
||||||
* Required to end with '/'.
|
|
||||||
* @return {name, extdata_id, encryption, seed_crypto, icon}
|
|
||||||
*/
|
|
||||||
std::tuple<std::string, u64, EncryptionType, bool, std::vector<u16>> LoadTitleData(
|
|
||||||
const std::string& path) const;
|
|
||||||
|
|
||||||
bool is_good{};
|
bool is_good{};
|
||||||
Config config;
|
Config config;
|
||||||
std::unique_ptr<SDMCDecryptor> decryptor;
|
std::unique_ptr<SDMCDecryptor> decryptor;
|
||||||
|
|
||||||
// The NCCH used to dump CXIs.
|
// The NCCH used to dump CXIs.
|
||||||
std::unique_ptr<NCCHContainer> dump_cxi_ncch;
|
std::unique_ptr<NCCHContainer<SDMCFile>> dump_cxi_ncch;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -29,27 +29,24 @@ constexpr u32 MakeMagic(char a, char b, char c, char d) {
|
|||||||
static const int kMaxSections = 8; ///< Maximum number of sections (files) in an ExeFs
|
static const int kMaxSections = 8; ///< Maximum number of sections (files) in an ExeFs
|
||||||
static const int kBlockSize = 0x200; ///< Size of ExeFS blocks (in bytes)
|
static const int kBlockSize = 0x200; ///< Size of ExeFS blocks (in bytes)
|
||||||
|
|
||||||
NCCHContainer::NCCHContainer(const std::string& root_folder, const std::string& filepath)
|
template <typename File>
|
||||||
: root_folder(root_folder), filepath(filepath) {
|
NCCHContainer<File>::NCCHContainer(std::shared_ptr<File> file_) : file(std::move(file_)) {}
|
||||||
file = std::make_shared<SDMCFile>(root_folder, filepath, "rb");
|
|
||||||
}
|
|
||||||
|
|
||||||
ResultStatus NCCHContainer::OpenFile(const std::string& root_folder, const std::string& filepath) {
|
template <typename File>
|
||||||
this->root_folder = root_folder;
|
ResultStatus NCCHContainer<File>::OpenFile(std::shared_ptr<File> file_) {
|
||||||
this->filepath = filepath;
|
file = std::move(file_);
|
||||||
file = std::make_shared<SDMCFile>(root_folder, filepath, "rb");
|
|
||||||
|
|
||||||
if (!file->IsOpen()) {
|
if (!file->IsOpen()) {
|
||||||
LOG_WARNING(Service_FS, "Failed to open {}", filepath);
|
LOG_WARNING(Service_FS, "Failed to open");
|
||||||
return ResultStatus::Error;
|
return ResultStatus::Error;
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG_DEBUG(Service_FS, "Opened {}", filepath);
|
LOG_DEBUG(Service_FS, "Opened");
|
||||||
return ResultStatus::Success;
|
return ResultStatus::Success;
|
||||||
}
|
}
|
||||||
|
|
||||||
ResultStatus NCCHContainer::Load() {
|
template <typename File>
|
||||||
LOG_INFO(Service_FS, "Loading NCCH from file {}", filepath);
|
ResultStatus NCCHContainer<File>::Load() {
|
||||||
if (is_loaded)
|
if (is_loaded)
|
||||||
return ResultStatus::Success;
|
return ResultStatus::Success;
|
||||||
|
|
||||||
@@ -188,7 +185,7 @@ ResultStatus NCCHContainer::Load() {
|
|||||||
|
|
||||||
// System archives and DLC don't have an extended header but have RomFS
|
// System archives and DLC don't have an extended header but have RomFS
|
||||||
if (ncch_header.extended_header_size) {
|
if (ncch_header.extended_header_size) {
|
||||||
auto read_exheader = [this](SDMCFile& file) {
|
auto read_exheader = [this](File& file) {
|
||||||
const std::size_t size = sizeof(exheader_header);
|
const std::size_t size = sizeof(exheader_header);
|
||||||
return file && file.ReadBytes(&exheader_header, size) == size;
|
return file && file.ReadBytes(&exheader_header, size) == size;
|
||||||
};
|
};
|
||||||
@@ -260,7 +257,7 @@ ResultStatus NCCHContainer::Load() {
|
|||||||
.ProcessData(data, data, sizeof(exefs_header));
|
.ProcessData(data, data, sizeof(exefs_header));
|
||||||
}
|
}
|
||||||
|
|
||||||
exefs_file = std::make_shared<SDMCFile>(root_folder, filepath, "rb");
|
exefs_file = file;
|
||||||
has_exefs = true;
|
has_exefs = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,7 +269,8 @@ ResultStatus NCCHContainer::Load() {
|
|||||||
return ResultStatus::Success;
|
return ResultStatus::Success;
|
||||||
}
|
}
|
||||||
|
|
||||||
ResultStatus NCCHContainer::LoadSectionExeFS(const char* name, std::vector<u8>& buffer) {
|
template <typename File>
|
||||||
|
ResultStatus NCCHContainer<File>::LoadSectionExeFS(const char* name, std::vector<u8>& buffer) {
|
||||||
ResultStatus result = Load();
|
ResultStatus result = Load();
|
||||||
if (result != ResultStatus::Success)
|
if (result != ResultStatus::Success)
|
||||||
return result;
|
return result;
|
||||||
@@ -309,7 +307,8 @@ ResultStatus NCCHContainer::LoadSectionExeFS(const char* name, std::vector<u8>&
|
|||||||
return ResultStatus::ErrorNotUsed;
|
return ResultStatus::ErrorNotUsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
ResultStatus NCCHContainer::ReadProgramId(u64_le& program_id) {
|
template <typename File>
|
||||||
|
ResultStatus NCCHContainer<File>::ReadProgramId(u64_le& program_id) {
|
||||||
ResultStatus result = Load();
|
ResultStatus result = Load();
|
||||||
if (result != ResultStatus::Success)
|
if (result != ResultStatus::Success)
|
||||||
return result;
|
return result;
|
||||||
@@ -321,7 +320,8 @@ ResultStatus NCCHContainer::ReadProgramId(u64_le& program_id) {
|
|||||||
return ResultStatus::Success;
|
return ResultStatus::Success;
|
||||||
}
|
}
|
||||||
|
|
||||||
ResultStatus NCCHContainer::ReadExtdataId(u64& extdata_id) {
|
template <typename File>
|
||||||
|
ResultStatus NCCHContainer<File>::ReadExtdataId(u64& extdata_id) {
|
||||||
ResultStatus result = Load();
|
ResultStatus result = Load();
|
||||||
if (result != ResultStatus::Success)
|
if (result != ResultStatus::Success)
|
||||||
return result;
|
return result;
|
||||||
@@ -356,7 +356,8 @@ ResultStatus NCCHContainer::ReadExtdataId(u64& extdata_id) {
|
|||||||
return ResultStatus::Success;
|
return ResultStatus::Success;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool NCCHContainer::HasExeFS() {
|
template <typename File>
|
||||||
|
bool NCCHContainer<File>::HasExeFS() {
|
||||||
ResultStatus result = Load();
|
ResultStatus result = Load();
|
||||||
if (result != ResultStatus::Success)
|
if (result != ResultStatus::Success)
|
||||||
return false;
|
return false;
|
||||||
@@ -364,7 +365,8 @@ bool NCCHContainer::HasExeFS() {
|
|||||||
return has_exefs;
|
return has_exefs;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool NCCHContainer::HasExHeader() {
|
template <typename File>
|
||||||
|
bool NCCHContainer<File>::HasExHeader() {
|
||||||
ResultStatus result = Load();
|
ResultStatus result = Load();
|
||||||
if (result != ResultStatus::Success)
|
if (result != ResultStatus::Success)
|
||||||
return false;
|
return false;
|
||||||
@@ -372,7 +374,8 @@ bool NCCHContainer::HasExHeader() {
|
|||||||
return has_exheader;
|
return has_exheader;
|
||||||
}
|
}
|
||||||
|
|
||||||
ResultStatus NCCHContainer::ReadEncryptionType(EncryptionType& encryption) {
|
template <typename File>
|
||||||
|
ResultStatus NCCHContainer<File>::ReadEncryptionType(EncryptionType& encryption) {
|
||||||
ResultStatus result = Load();
|
ResultStatus result = Load();
|
||||||
if (result != ResultStatus::Success)
|
if (result != ResultStatus::Success)
|
||||||
return result;
|
return result;
|
||||||
@@ -407,7 +410,8 @@ ResultStatus NCCHContainer::ReadEncryptionType(EncryptionType& encryption) {
|
|||||||
return ResultStatus::Success;
|
return ResultStatus::Success;
|
||||||
}
|
}
|
||||||
|
|
||||||
ResultStatus NCCHContainer::ReadSeedCrypto(bool& used) {
|
template <typename File>
|
||||||
|
ResultStatus NCCHContainer<File>::ReadSeedCrypto(bool& used) {
|
||||||
ResultStatus result = Load();
|
ResultStatus result = Load();
|
||||||
if (result != ResultStatus::Success)
|
if (result != ResultStatus::Success)
|
||||||
return result;
|
return result;
|
||||||
@@ -419,8 +423,9 @@ ResultStatus NCCHContainer::ReadSeedCrypto(bool& used) {
|
|||||||
return ResultStatus::Success;
|
return ResultStatus::Success;
|
||||||
}
|
}
|
||||||
|
|
||||||
ResultStatus NCCHContainer::DecryptToFile(const std::string& destination,
|
template <typename File>
|
||||||
const ProgressCallback& callback) {
|
ResultStatus NCCHContainer<File>::DecryptToFile(const std::string& destination,
|
||||||
|
const ProgressCallback& callback) {
|
||||||
ResultStatus result = Load();
|
ResultStatus result = Load();
|
||||||
if (result != ResultStatus::Success)
|
if (result != ResultStatus::Success)
|
||||||
return result;
|
return result;
|
||||||
@@ -545,11 +550,15 @@ ResultStatus NCCHContainer::DecryptToFile(const std::string& destination,
|
|||||||
return ResultStatus::Success;
|
return ResultStatus::Success;
|
||||||
}
|
}
|
||||||
|
|
||||||
void NCCHContainer::AbortDecryptToFile() {
|
template <typename File>
|
||||||
|
void NCCHContainer<File>::AbortDecryptToFile() {
|
||||||
aborted = true;
|
aborted = true;
|
||||||
decryptor.Abort();
|
decryptor.Abort();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
template class NCCHContainer<SDMCFile>;
|
||||||
|
template class NCCHContainer<FileUtil::IOFile>;
|
||||||
|
|
||||||
#pragma pack(push, 1)
|
#pragma pack(push, 1)
|
||||||
struct RomFSIVFCHeader {
|
struct RomFSIVFCHeader {
|
||||||
u32_le magic;
|
u32_le magic;
|
||||||
|
|||||||
@@ -204,17 +204,13 @@ enum class EncryptionType;
|
|||||||
* Note that this is heavily stripped down and can only read (primary-key
|
* Note that this is heavily stripped down and can only read (primary-key
|
||||||
* encrypted non-code sections of) ExeFS and ExHeader by design.
|
* encrypted non-code sections of) ExeFS and ExHeader by design.
|
||||||
*/
|
*/
|
||||||
|
template <typename File = SDMCFile>
|
||||||
class NCCHContainer {
|
class NCCHContainer {
|
||||||
public:
|
public:
|
||||||
/**
|
NCCHContainer(std::shared_ptr<File> file);
|
||||||
* Constructs the container.
|
|
||||||
* @param root_folder Path to SDMC folder
|
|
||||||
* @param filepath Path relative to SDMC folder, starting with /
|
|
||||||
*/
|
|
||||||
NCCHContainer(const std::string& root_folder, const std::string& filepath);
|
|
||||||
NCCHContainer() {}
|
NCCHContainer() {}
|
||||||
|
|
||||||
ResultStatus OpenFile(const std::string& root_folder, const std::string& filepath);
|
ResultStatus OpenFile(std::shared_ptr<File> file);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensure ExeFS and exheader is loaded and ready for reading sections
|
* Ensure ExeFS and exheader is loaded and ready for reading sections
|
||||||
@@ -302,11 +298,11 @@ private:
|
|||||||
|
|
||||||
std::string root_folder;
|
std::string root_folder;
|
||||||
std::string filepath;
|
std::string filepath;
|
||||||
std::shared_ptr<SDMCFile> file;
|
std::shared_ptr<File> file;
|
||||||
std::shared_ptr<SDMCFile> exefs_file;
|
std::shared_ptr<File> exefs_file;
|
||||||
|
|
||||||
// Used for DecryptToFile
|
// Used for DecryptToFile
|
||||||
QuickDecryptor<SDMCFile, FileUtil::IOFile> decryptor;
|
QuickDecryptor<File, FileUtil::IOFile> decryptor;
|
||||||
std::atomic_bool aborted{false};
|
std::atomic_bool aborted{false};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ QString ReadableByteSize(qulonglong size) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// content type, name, icon name
|
// content type, name, icon name
|
||||||
static constexpr std::array<std::tuple<Core::ContentType, const char*, const char*>, 7>
|
static constexpr std::array<std::tuple<Core::ContentType, const char*, const char*>, 8>
|
||||||
ContentTypeMap{{
|
ContentTypeMap{{
|
||||||
{Core::ContentType::Application, QT_TR_NOOP("Application"), "app"},
|
{Core::ContentType::Application, QT_TR_NOOP("Application"), "app"},
|
||||||
{Core::ContentType::Update, QT_TR_NOOP("Update"), "update"},
|
{Core::ContentType::Update, QT_TR_NOOP("Update"), "update"},
|
||||||
@@ -44,6 +44,7 @@ static constexpr std::array<std::tuple<Core::ContentType, const char*, const cha
|
|||||||
{Core::ContentType::Extdata, QT_TR_NOOP("Extra Data"), "save_data"},
|
{Core::ContentType::Extdata, QT_TR_NOOP("Extra Data"), "save_data"},
|
||||||
{Core::ContentType::SystemArchive, QT_TR_NOOP("System Archive"), "system_archive"},
|
{Core::ContentType::SystemArchive, QT_TR_NOOP("System Archive"), "system_archive"},
|
||||||
{Core::ContentType::Sysdata, QT_TR_NOOP("System Data"), "system_data"},
|
{Core::ContentType::Sysdata, QT_TR_NOOP("System Data"), "system_data"},
|
||||||
|
{Core::ContentType::SystemTitle, QT_TR_NOOP("System Title"), "system_archive"},
|
||||||
}};
|
}};
|
||||||
|
|
||||||
static const std::unordered_map<Core::EncryptionType, const char*> EncryptionTypeMap{{
|
static const std::unordered_map<Core::EncryptionType, const char*> EncryptionTypeMap{{
|
||||||
@@ -206,7 +207,7 @@ void ImportDialog::InsertSecondLevelItem(std::size_t row, const Core::ContentSpe
|
|||||||
name = QStringLiteral("%1 (%2)")
|
name = QStringLiteral("%1 (%2)")
|
||||||
.arg(GetContentName(content))
|
.arg(GetContentName(content))
|
||||||
.arg(GetContentTypeName(content.type));
|
.arg(GetContentTypeName(content.type));
|
||||||
} else if (row <= 2) {
|
} else if (row <= 3) {
|
||||||
name = GetContentName(content);
|
name = GetContentName(content);
|
||||||
} else {
|
} else {
|
||||||
name = GetContentTypeName(content.type);
|
name = GetContentTypeName(content.type);
|
||||||
@@ -225,7 +226,8 @@ void ImportDialog::InsertSecondLevelItem(std::size_t row, const Core::ContentSpe
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (content.type != Core::ContentType::Application &&
|
if (content.type != Core::ContentType::Application &&
|
||||||
content.type != Core::ContentType::Update && content.type != Core::ContentType::DLC) {
|
content.type != Core::ContentType::Update && content.type != Core::ContentType::DLC &&
|
||||||
|
content.type != Core::ContentType::SystemTitle) {
|
||||||
|
|
||||||
// Do not display encryption in this case
|
// Do not display encryption in this case
|
||||||
encryption.clear();
|
encryption.clear();
|
||||||
@@ -237,11 +239,15 @@ void ImportDialog::InsertSecondLevelItem(std::size_t row, const Core::ContentSpe
|
|||||||
|
|
||||||
QPixmap icon;
|
QPixmap icon;
|
||||||
if (replace_icon.isNull()) {
|
if (replace_icon.isNull()) {
|
||||||
// When not in title view, only System Data and System Archive groups use category icons.
|
// Exclude system titles, they are a single group but have own icons.
|
||||||
const bool use_category_icon = content.type == Core::ContentType::Sysdata ||
|
if (use_title_view && content.type != Core::ContentType::SystemTitle) {
|
||||||
content.type == Core::ContentType::SystemArchive;
|
icon = GetContentTypeIcon(content.type);
|
||||||
icon = use_title_view ? GetContentTypeIcon(content.type)
|
} else {
|
||||||
: GetContentIcon(content, use_category_icon);
|
// When not in title view, System Data and System Archive groups use category icons.
|
||||||
|
const bool use_category_icon = content.type == Core::ContentType::Sysdata ||
|
||||||
|
content.type == Core::ContentType::SystemArchive;
|
||||||
|
icon = GetContentIcon(content, use_category_icon);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
icon = replace_icon;
|
icon = replace_icon;
|
||||||
}
|
}
|
||||||
@@ -258,13 +264,14 @@ void ImportDialog::InsertSecondLevelItem(std::size_t row, const Core::ContentSpe
|
|||||||
} else {
|
} else {
|
||||||
if (!warning_shown && !exists &&
|
if (!warning_shown && !exists &&
|
||||||
(type == Core::ContentType::SystemArchive ||
|
(type == Core::ContentType::SystemArchive ||
|
||||||
type == Core::ContentType::Sysdata)) {
|
type == Core::ContentType::Sysdata ||
|
||||||
|
type == Core::ContentType::SystemTitle)) {
|
||||||
|
|
||||||
QMessageBox::warning(
|
QMessageBox::warning(
|
||||||
this, tr("Warning"),
|
this, tr("Warning"),
|
||||||
tr("System Archive and System Data are important files that may "
|
tr("You are de-selecting important files that may be necessary for "
|
||||||
"be necessary for your imported games to run.\nIt is highly "
|
"your imported games to run.\nIt is highly recommended to import "
|
||||||
"recommended to import these contents if they do not exist yet."));
|
"these contents if they do not exist yet."));
|
||||||
warning_shown = true;
|
warning_shown = true;
|
||||||
}
|
}
|
||||||
total_size -= size;
|
total_size -= size;
|
||||||
@@ -302,10 +309,12 @@ void ImportDialog::RepopulateContent() {
|
|||||||
title_name_map.insert_or_assign(0, tr("Ungrouped"));
|
title_name_map.insert_or_assign(0, tr("Ungrouped"));
|
||||||
title_name_map.insert_or_assign(1, tr("System Archive"));
|
title_name_map.insert_or_assign(1, tr("System Archive"));
|
||||||
title_name_map.insert_or_assign(2, tr("System Data"));
|
title_name_map.insert_or_assign(2, tr("System Data"));
|
||||||
|
title_name_map.insert_or_assign(3, tr("System Title"));
|
||||||
|
|
||||||
title_icon_map.insert_or_assign(0, QIcon::fromTheme(QStringLiteral("unknown")).pixmap(24));
|
title_icon_map.insert_or_assign(0, QIcon::fromTheme(QStringLiteral("unknown")).pixmap(24));
|
||||||
title_icon_map.insert_or_assign(1, GetContentTypeIcon(Core::ContentType::SystemArchive));
|
title_icon_map.insert_or_assign(1, GetContentTypeIcon(Core::ContentType::SystemArchive));
|
||||||
title_icon_map.insert_or_assign(2, GetContentTypeIcon(Core::ContentType::Sysdata));
|
title_icon_map.insert_or_assign(2, GetContentTypeIcon(Core::ContentType::Sysdata));
|
||||||
|
title_icon_map.insert_or_assign(3, GetContentTypeIcon(Core::ContentType::SystemTitle));
|
||||||
|
|
||||||
std::unordered_map<u64, u64> title_row_map;
|
std::unordered_map<u64, u64> title_row_map;
|
||||||
for (const auto& [id, name] : title_name_map) {
|
for (const auto& [id, name] : title_name_map) {
|
||||||
@@ -342,6 +351,10 @@ void ImportDialog::RepopulateContent() {
|
|||||||
row = title_row_map.at(2); // System data
|
row = title_row_map.at(2); // System data
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case Core::ContentType::SystemTitle: {
|
||||||
|
row = title_row_map.at(3); // System title
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
InsertSecondLevelItem(row, content, i);
|
InsertSecondLevelItem(row, content, i);
|
||||||
|
|||||||
@@ -42,8 +42,8 @@ private:
|
|||||||
Core::ContentSpecifier SpecifierFromItem(QTreeWidgetItem* item) const;
|
Core::ContentSpecifier SpecifierFromItem(QTreeWidgetItem* item) const;
|
||||||
void OnContextMenu(const QPoint& point);
|
void OnContextMenu(const QPoint& point);
|
||||||
void StartDumpingCXI(const Core::ContentSpecifier& content);
|
void StartDumpingCXI(const Core::ContentSpecifier& content);
|
||||||
Core::NCCHContainer dump_cxi_container; // NCCH container used for dumping CXI
|
Core::NCCHContainer<Core::SDMCFile> dump_cxi_container; // NCCH container used for dumping CXI
|
||||||
QString last_dump_cxi_path; // Used for recording last path in StartDumpingCXI
|
QString last_dump_cxi_path; // Used for recording last path in StartDumpingCXI
|
||||||
|
|
||||||
std::unique_ptr<Ui::ImportDialog> ui;
|
std::unique_ptr<Ui::ImportDialog> ui;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user