diff --git a/dist/threeSDumper.gm9 b/dist/threeSDumper.gm9 index d84348c..9ef0e9b 100644 --- a/dist/threeSDumper.gm9 +++ b/dist/threeSDumper.gm9 @@ -5,124 +5,109 @@ # GM9 Script for dumping necessary files automatically. set PREVIEW_MODE "threeSD Dumper\nby zhaowenlan1779" -set OUT "0:/threeSD" -if not find $[OUT] NULL - mkdir $[OUT] -end - -if not ask "Execute threeSD Dumper?" +if not ask "Execute threeSD Dumper?\n \nRequires GodMode9 v2.0.0\nYou are on $[GM9VER]\n \nRequired Space: ~400MB for each NAND" goto Exit end set PREVIEW_MODE "threeSD Dumper\nby zhaowenlan1779\n \nWorking..." -# === movable.sed -cp -w -n "1:/private/movable.sed" $[OUT]/movable.sed +set OUT "0:/threeSD" +if exist $[OUT] + rm $[OUT] +end +mkdir $[OUT] -# === bootrom -if find "M:/boot9.bin" NULL +# === General data (independent of NANDs) + +# Version +dumptxt $[OUT]/version.txt 4 + +# bootrom +if exist "M:/boot9.bin" cp -w -n "M:/boot9.bin" $[OUT]/boot9.bin -elif find "0:/3DS/boot9.bin" NULL +elif exist "0:/3DS/boot9.bin" cp -w -n "0:/3DS/boot9.bin" $[OUT]/boot9.bin else echo "ERROR: \nboot9.bin not found. \nIf you use fastboot3ds, hold HOME while booting, \nand go to Miscellaneous... > Dump bootroms & OTP. \nWhen finished, simply execute this script again." goto Exit end -# === certs.db -if chk $[RDTYPE] "devkit" - echo "WARNING: \nDev kit detected. \nCIA building will not be usable." -else - cp -w -n "1:/dbs/certs.db" $[OUT]/certs.db -end - -# === ticket.db -cp -w -n "1:/dbs/ticket.db" $[OUT]/ticket.db - -# === title.db -cp -w -n "1:/dbs/title.db" $[OUT]/title.db - -# === Secret sector (N3DS only) +# Secret sector (N3DS only) if chk $[ONTYPE] "N3DS" cp -w -n "S:/sector0x96.bin" $[OUT]/sector0x96.bin end -# === NAND data -if not find $[OUT]/data NULL - mkdir $[OUT]/data -end +# === NANDs -if not find $[OUT]/data/extdata NULL - mkdir $[OUT]/data/extdata -end -cp -w -n "1:/data/$[SYSID0]/extdata" $[OUT]/data/extdata +# Start with SysNAND +set NAND "1:" +set NAND_NAME "Sys" +set ID0 $[SYSID0] -if not find $[OUT]/data/sysdata NULL - mkdir $[OUT]/data/sysdata -end -cp -w -n "1:/data/$[SYSID0]/sysdata" $[OUT]/data/sysdata +@Loop +set PREVIEW_MODE "threeSD Dumper\nby zhaowenlan1779\n \nWorking ($[NAND_NAME])..." +set OUT "0:/threeSD/$[NAND_NAME]" +mkdir $[OUT] -# === Other system titles -if find $[OUT]/title NULL - rm $[OUT]/title -end -mkdir $[OUT]/title - -# 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 +# movable.sed +cp -w -n $[NAND]/private/movable.sed $[OUT]/movable.sed -# System Applets -if not find $[OUT]/title/00040030 NULL - mkdir $[OUT]/title/00040030 -end -cp -w -n "1:/title/00040030" $[OUT]/title/00040030 - -# Shared Data Archives -if not find $[OUT]/title/0004009b NULL - mkdir $[OUT]/title/0004009b +# certs.db +if chk $[RDTYPE] "devkit" + echo "WARNING: \nDev kit detected. \nCIA building will not be usable." +else + cp -w -n $[NAND]/dbs/certs.db $[OUT]/certs.db end -cp -w -n "1:/title/0004009b" $[OUT]/title/0004009b -# System Data Archives -if not find $[OUT]/title/000400db NULL - mkdir $[OUT]/title/000400db -end -cp -w -n "1:/title/000400db" $[OUT]/title/000400db +# ticket.db +cp -w -n $[NAND]/dbs/ticket.db $[OUT]/ticket.db -# System Modules -if not find $[OUT]/title/00040130 NULL - mkdir $[OUT]/title/00040130 -end -cp -w -n "1:/title/00040130" $[OUT]/title/00040130 +# title.db +cp -w -n $[NAND]/dbs/title.db $[OUT]/title.db -# System Firmware -if not find $[OUT]/title/00040138 NULL - mkdir $[OUT]/title/00040138 +# seeddb.bin +# Note: this contains both SysNAND and EmuNAND seeds when built, but only the current EmuNAND +if exist 0:/gm9/out/seedd.bin + rm 0:/gm9/out/seeddb.bin end -cp -w -n "1:/title/00040138" $[OUT]/title/00040138 - -# === seeddb.bin sdump -o -s -w seeddb.bin -if not find 0:/gm9/out/seeddb.bin NULL +if not exist 0:/gm9/out/seeddb.bin 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." else cp -w -n "0:/gm9/out/seeddb.bin" $[OUT]/seeddb.bin rm "0:/gm9/out/seeddb.bin" end -# === Write version -dumptxt $[OUT]/version.txt 4 +# data +cp -w -n $[NAND]/data/$[ID0] $[OUT]/data + +# title +cp -w -n $[NAND]/title $[OUT]/title +# Loop Control +if chk $[NAND] "1:" + # Start EmuNAND + if not exist "4:/title" + goto Finish + end + set NAND "4:" +else + # Next EmuNAND + set LASTEMU $[EMUBASE] + nextemu + if chk $[EMUBASE] $[LASTEMU] + # All EmuNANDs done + goto Finish + end +end +set NAND_NAME "Emu$[EMUBASE]" +set ID0 $[EMUID0] +goto Loop + +@Finish set PREVIEW_MODE "threeSD Dumper\nby zhaowenlan1779\n \nSuccess!" -echo "Successfully dumped necessary\nfiles for threeSD." +if ask "Successfully dumped necessary\nfiles for threeSD.\n \nPower off now?" + poweroff +end @Exit diff --git a/src/core/importer.cpp b/src/core/importer.cpp index be155aa..6cee38e 100644 --- a/src/core/importer.cpp +++ b/src/core/importer.cpp @@ -2,6 +2,7 @@ // Licensed under GPLv2 or any later version // Refer to the license.txt file included. +#include #include #include #include "common/assert.h" @@ -32,13 +33,12 @@ SDMCImporter::SDMCImporter(const Config& config_) : config(config_) { SDMCImporter::~SDMCImporter() { // Unload global DBs Certs::Clear(); - Seeds::Clear(); + g_seed_db.seeds.clear(); } bool SDMCImporter::Init() { - ASSERT_MSG(!config.sdmc_path.empty() && !config.user_path.empty() && - !config.bootrom_path.empty() && !config.movable_sed_path.empty(), - "Config is not good"); + ASSERT_MSG(IsConfigGood(config), "Config is not good"); + nand_config = config.nands[0]; // Fix paths if (config.sdmc_path.back() != '/' && config.sdmc_path.back() != '\\') { @@ -51,28 +51,29 @@ bool SDMCImporter::Init() { Key::ClearKeys(); Key::LoadBootromKeys(config.bootrom_path); - Key::LoadMovableSedKeys(config.movable_sed_path); + Key::LoadMovableSedKeys(nand_config.movable_sed_path); if (!Key::IsNormalKeyAvailable(Key::SDKey)) { LOG_ERROR(Core, "SDKey is not available"); return false; } - // Load global DBs - if (!config.seed_db_path.empty()) { - Seeds::Load(config.seed_db_path); - } - if (!config.certs_db_path.empty()) { - Certs::Load(config.certs_db_path); - } - - // Load Ticket DB - if (!config.ticket_db_path.empty()) { - ticket_db = std::make_shared(config.ticket_db_path); - } - if (!ticket_db || !ticket_db->IsGood()) { - LOG_WARNING(Core, "ticket.db not present or is invalid"); - ticket_db.reset(); + // Load and merge global DBs + ticket_db = std::make_shared(); + nand_title_db = std::make_unique(); + for (const auto& nand : config.nands) { + if (!nand.certs_db_path.empty()) { + TRY(Certs::Load(nand.certs_db_path)); + } + if (!nand.ticket_db_path.empty()) { + TRY(ticket_db->AddFromFile(nand.ticket_db_path)); + } + if (!nand.title_db_path.empty()) { + TRY(nand_title_db->AddFromFile(nand.title_db_path)); + } + if (!nand.seed_db_path.empty()) { + TRY(g_seed_db.AddFromFile(nand.seed_db_path)); + } } // Create children @@ -84,22 +85,10 @@ bool SDMCImporter::Init() { DataContainer container(sdmc_decryptor->DecryptFile("/dbs/title.db")); std::vector> data; if (container.IsGood() && container.GetIVFCLevel4Data(data)) { - sdmc_title_db = std::make_unique(std::move(data[0])); + sdmc_title_db = std::make_unique(); + TRY(sdmc_title_db->AddFromData(std::move(data[0]))); } } - if (!sdmc_title_db || !sdmc_title_db->IsGood()) { - LOG_WARNING(Core, "SDMC title.db invalid"); - sdmc_title_db.reset(); - } - - // Load NAND Title DB - if (!config.nand_title_db_path.empty()) { - nand_title_db = std::make_unique(config.nand_title_db_path); - } - if (!nand_title_db || !nand_title_db->IsGood()) { - LOG_WARNING(Core, "NAND title.db invalid"); - nand_title_db.reset(); - } FileUtil::SetUserPath(config.user_path); return true; @@ -193,8 +182,7 @@ bool SDMCImporter::ImportTitle(const ContentSpecifier& specifier, bool SDMCImporter::ImportNandTitle(const ContentSpecifier& specifier, const Common::ProgressCallback& callback) { - const auto base_path = - config.system_titles_path.substr(0, config.system_titles_path.size() - 6); + const auto base_path = nand_config.title_path.substr(0, nand_config.title_path.size() - 6); return ImportTitleGeneric( base_path, specifier, callback, [this, &base_path](const std::string& filepath, @@ -242,7 +230,7 @@ bool SDMCImporter::ImportNandSavegame(u64 id, [[maybe_unused]] const Common::ProgressCallback& callback) { const auto path = fmt::format("sysdata/{:08x}/00000000", (id & 0xFFFFFFFF)); - FileUtil::IOFile file(config.nand_data_path + path, "rb"); + FileUtil::IOFile file(nand_config.data_path + path, "rb"); std::vector data = file.GetData(); if (data.empty()) { LOG_ERROR(Core, "Failed to read from {}", path); @@ -281,7 +269,7 @@ bool SDMCImporter::ImportExtdata(u64 id, bool SDMCImporter::ImportNandExtdata(u64 id, [[maybe_unused]] const Common::ProgressCallback& callback) { const auto path = fmt::format("extdata/{:08x}/{:08x}/", (id >> 32), (id & 0xFFFFFFFF)); - Extdata extdata(config.nand_data_path + path); + Extdata extdata(nand_config.data_path + path); if (!extdata.IsGood()) { return false; } @@ -303,27 +291,11 @@ bool SDMCImporter::ImportSysdata(u64 id, } case 1: { // seed db const auto target_path = FileUtil::GetUserPath(FileUtil::UserPath::SysDataDir) + SEED_DB; - LOG_INFO(Core, "Dumping SeedDB from {} to {}", SEED_DB, config.seed_db_path, target_path); + LOG_INFO(Core, "Dumping SeedDB to {}", SEED_DB, target_path); - SeedDB target; - if (!target.Load(target_path)) { - LOG_ERROR(Core, "Could not load seeddb from {}", target_path); - return false; - } - - SeedDB source; - if (!source.Load(config.seed_db_path)) { - LOG_ERROR(Core, "Could not load seeddb from {}", config.seed_db_path); - return false; - } - - for (const auto& seed : source) { - if (!target.Get(seed.title_id)) { - LOG_INFO(Core, "Adding seed for {:16X}", seed.title_id); - target.Add(seed); - } - } - return target.Save(target_path); + SeedDB merged_seed_db{g_seed_db}; + merged_seed_db.AddFromFile(target_path); + return merged_seed_db.Save(target_path); } case 2: { // secret sector const auto target_path = @@ -407,11 +379,11 @@ bool SDMCImporter::LoadTMD(ContentType type, u64 id, TitleMetadata& out) const { const bool is_nand = type == ContentType::NandTitle; auto& title_db = is_nand ? nand_title_db : sdmc_title_db; - const auto physical_path = - is_nand ? fmt::format("{}{:08x}/{:08x}/content/", config.system_titles_path, (id >> 32), - (id & 0xFFFFFFFF)) - : fmt::format("{}title/{:08x}/{:08x}/content/", config.sdmc_path, (id >> 32), - (id & 0xFFFFFFFF)); + const auto physical_path = is_nand + ? fmt::format("{}{:08x}/{:08x}/content/", nand_config.title_path, + (id >> 32), (id & 0xFFFFFFFF)) + : fmt::format("{}title/{:08x}/{:08x}/content/", config.sdmc_path, + (id >> 32), (id & 0xFFFFFFFF)); std::string tmd_path; if (title_db && title_db->titles.count(id)) { @@ -445,7 +417,7 @@ std::shared_ptr SDMCImporter::OpenContent(const ContentSpecifi u32 content_id) const { if (specifier.type == ContentType::NandTitle) { const auto path = - fmt::format("{}{:08x}/{:08x}/content/{:08x}.app", config.system_titles_path, + fmt::format("{}{:08x}/{:08x}/content/{:08x}.app", nand_config.title_path, (specifier.id >> 32), (specifier.id & 0xFFFFFFFF), content_id); return std::make_shared(path, "rb"); } else { @@ -868,10 +840,9 @@ void SDMCImporter::ListTitle(std::vector& out) const { // TODO: Simplify. void SDMCImporter::ListNandTitle(std::vector& out) const { - const auto ProcessDirectory = [this, &out, - &system_titles_path = config.system_titles_path](u64 high_id) { + const auto ProcessDirectory = [this, &out, &title_path = nand_config.title_path](u64 high_id) { FileUtil::ForeachDirectoryEntry( - nullptr, fmt::format("{}{:08x}/", system_titles_path, high_id), + nullptr, fmt::format("{}{:08x}/", title_path, high_id), [this, high_id, &out](u64* /*num_entries_out*/, const std::string& directory, const std::string& virtual_name) { if (!FileUtil::IsDirectory(directory + virtual_name + "/")) { @@ -931,7 +902,7 @@ void SDMCImporter::ListNandTitle(std::vector& out) const { void SDMCImporter::ListNandSavegame(std::vector& out) const { FileUtil::ForeachDirectoryEntry( - nullptr, fmt::format("{}sysdata/", config.nand_data_path), + nullptr, fmt::format("{}sysdata/", nand_config.data_path), [&out](u64* /*num_entries_out*/, const std::string& directory, const std::string& virtual_name) { if (!FileUtil::IsDirectory(directory + virtual_name + "/")) { @@ -996,7 +967,7 @@ void SDMCImporter::ListExtdata(std::vector& out) const { "3DS/00000000000000000000000000000000/00000000000000000000000000000000/" "extdata/00000000/{}"); ProcessDirectory(0x00048000, ContentType::NandExtdata, - fmt::format("{}extdata/00048000/", config.nand_data_path), + fmt::format("{}extdata/00048000/", nand_config.data_path), FileUtil::GetUserPath(FileUtil::UserPath::NANDDir) + "data/00000000000000000000000000000000/extdata/00048000/{}"); } @@ -1026,32 +997,25 @@ void SDMCImporter::ListSysdata(std::vector& out) const { } // Check for seeddb - if (config.seed_db_path.empty()) { + if (g_seed_db.seeds.empty()) { return; } const auto target_path = FileUtil::GetUserPath(FileUtil::UserPath::SysDataDir) + SEED_DB; SeedDB target; - if (!target.Load(target_path)) { + if (!target.AddFromFile(target_path)) { LOG_ERROR(Core, "Could not load seeddb from {}", target_path); return; } - SeedDB source; - if (!source.Load(config.seed_db_path)) { - LOG_ERROR(Core, "Could not load seeddb from {}", config.seed_db_path); - return; - } - bool exists = true; // Whether the DB already 'exists', i.e. no new seeds can be found - for (const auto& seed : source) { - if (!target.Get(seed.title_id)) { + for (const auto& [title_id, seed] : g_seed_db.seeds) { + if (!target.seeds.count(title_id)) { exists = false; break; } } - out.push_back( - {ContentType::Sysdata, 1, exists, FileUtil::GetSize(config.seed_db_path), SEED_DB}); + out.push_back({ContentType::Sysdata, 1, exists, g_seed_db.GetSize(), SEED_DB}); } void SDMCImporter::DeleteContent(const ContentSpecifier& specifier) const { @@ -1136,6 +1100,67 @@ void SDMCImporter::DeleteSysdata(u64 id) const { } } +static std::string GetID0(const std::string& movable_sed_path) { + FileUtil::IOFile file(movable_sed_path, "rb"); + if (!file || file.GetSize() < 0x120) { + LOG_ERROR(Core, "Couldn't open file {}, or file too small", movable_sed_path); + return {}; + } + + // Check magic + u32_le magic{}; + if (file.ReadBytes(&magic, sizeof(magic)) != sizeof(magic) || + magic != MakeMagic('S', 'E', 'E', 'D')) { + + LOG_ERROR(Core, "File {} is invalid", movable_sed_path); + return {}; + } + + // Calculate ID0 + file.Seek(0x110, SEEK_SET); // KeyY offset + std::array keyY; + if (file.ReadBytes(keyY.data(), keyY.size()) != keyY.size()) { + LOG_ERROR(Core, "Could not read keyY from {}", movable_sed_path); + return {}; + } + + CryptoPP::SHA256 sha; + sha.Update(keyY.data(), keyY.size()); + + std::array hash; + sha.Final(reinterpret_cast(hash.data())); + + // ID0 is generated from the first half of the hash, with the four u32s byte-flipped + return fmt::format("{:08x}{:08x}{:08x}{:08x}", hash[0], hash[1], hash[2], hash[3]); +} + +// Gets SDMC path (Nintendo 3DS//) from ID0 folder. Basically just takes the first +// folder contained within, that matches the ID regex. +static std::string GetSDMCPath(const std::string& id0_folder) { + static const std::regex IdRegex{"[0-9a-f]{32}"}; + + std::string result; + FileUtil::ForeachDirectoryEntry( + nullptr, id0_folder, + [&result](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, IdRegex)) { + return true; + } + result = virtual_name; + return false; // halt searching + }); + + if (result.empty()) { + LOG_ERROR(Core, "Could not find ID1 folder in {}", id0_folder); + return {}; + } + return id0_folder + result + "/"; +} + std::vector LoadPresetConfig(std::string mount_point) { if (mount_point.back() != '/' && mount_point.back() != '\\') { mount_point += '/'; @@ -1148,80 +1173,89 @@ std::vector LoadPresetConfig(std::string mount_point) { Config config_template{}; config_template.user_path = FileUtil::GetUserPath(FileUtil::UserPath::UserDir); + if (!FileUtil::Exists(mount_point + "threeSD/")) { + // Still return the config for display in frontend to notify the user + return {config_template}; + } + + // Check version first + if (FileUtil::Exists(mount_point + "threeSD/version.txt")) { + std::ifstream stream; + OpenFStream(stream, mount_point + "threeSD/version.txt", std::ios::in); + stream >> config_template.version; + } + if (config_template.version != CurrentDumperVersion) { + return {config_template}; // Notify the user + } - // Load dumped data paths if using our dumper - if (FileUtil::Exists(mount_point + "threeSD/")) { #define LOAD_DATA(var, path) \ - if (FileUtil::Exists(mount_point + "threeSD/" + path)) { \ - config_template.var = mount_point + "threeSD/" + path; \ + if (FileUtil::Exists(mount_point + path)) { \ + config_template.var = mount_point + path; \ } + // General data + LOAD_DATA(bootrom_path, "threeSD/" BOOTROM9); + LOAD_DATA(secret_sector_path, "threeSD/" SECRET_SECTOR); + LOAD_DATA(enc_title_keys_bin_path, "gm9/support/" ENC_TITLE_KEYS_BIN); +#undef LOAD_DATA + + // Load NANDs + std::vector nands; + const auto Callback = [&nands](u64* /*num_entries_out*/, const std::string& directory, + const std::string& virtual_name) { + const std::string nand_dir = directory + virtual_name + "/"; + if (!FileUtil::IsDirectory(nand_dir)) { + return true; + } + + Config::NandConfig config; + config.nand_name = virtual_name; + +#define LOAD_DATA(var, path) \ + if (FileUtil::Exists(nand_dir + path)) { \ + config.var = nand_dir + path; \ + } LOAD_DATA(movable_sed_path, MOVABLE_SED); - LOAD_DATA(bootrom_path, BOOTROM9); LOAD_DATA(certs_db_path, CERTS_DB); - LOAD_DATA(nand_title_db_path, TITLE_DB); LOAD_DATA(ticket_db_path, TICKET_DB); + LOAD_DATA(title_db_path, TITLE_DB); LOAD_DATA(seed_db_path, SEED_DB); - LOAD_DATA(secret_sector_path, SECRET_SECTOR); - LOAD_DATA(system_titles_path, "title/"); - LOAD_DATA(nand_data_path, "data/"); + LOAD_DATA(title_path, "title/"); + LOAD_DATA(data_path, "data/"); #undef LOAD_DATA - // encTitleKeys.bin - if (FileUtil::Exists(mount_point + "gm9/support/" ENC_TITLE_KEYS_BIN)) { - config_template.enc_title_keys_bin_path = - mount_point + "gm9/support/" ENC_TITLE_KEYS_BIN; - } + nands.emplace_back(std::move(config)); + return true; + }; + FileUtil::ForeachDirectoryEntry(nullptr, mount_point + "threeSD/", Callback); - // Load version - if (FileUtil::Exists(mount_point + "threeSD/version.txt")) { - std::ifstream stream; - OpenFStream(stream, mount_point + "threeSD/version.txt", std::ios::in); - stream >> config_template.version; + // Group NAND configs by ID0 to generate configs + std::map config_map; // id0 -> config + for (const auto& nand : nands) { + const auto id0 = GetID0(nand.movable_sed_path); + if (id0.empty()) { + continue; } - } - // Regex for 3DS ID0 and ID1 - const std::regex id_regex{"[0-9a-f]{32}"}; + if (!config_map.count(id0)) { + // Create config for this ID0 + config_map[id0] = config_template; + config_map[id0].id0 = id0; - // Load SDMC dir - std::vector out; - const auto ProcessDirectory = [&id_regex, &config_template, &out](const std::string& path) { - return FileUtil::ForeachDirectoryEntry( - nullptr, path, - [&id_regex, &config_template, &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, id_regex)) { - return true; - } - - Config config = config_template; - config.sdmc_path = directory + virtual_name + "/"; - out.push_back(config); - return true; - }); - }; - - FileUtil::ForeachDirectoryEntry( - nullptr, mount_point + "Nintendo 3DS/", - [&id_regex, &ProcessDirectory](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, id_regex)) { - return true; + auto sdmc_path = GetSDMCPath(fmt::format("{}Nintendo 3DS/{}/", mount_point, id0)); + if (sdmc_path.empty()) { + continue; } + config_map[id0].sdmc_path = std::move(sdmc_path); + } + config_map[id0].nands.emplace_back(nand); + } - return ProcessDirectory(directory + virtual_name + "/"); - }); - + // Convert to a vector + std::vector out; + for (auto& [id0, config] : config_map) { + out.emplace_back(std::move(config)); + } return out; } diff --git a/src/core/importer.h b/src/core/importer.h index 5670ba6..315a896 100644 --- a/src/core/importer.h +++ b/src/core/importer.h @@ -58,35 +58,50 @@ struct ContentSpecifier { * All paths to directories shall end with a '/' (will be automatically added when not present) */ struct Config { - std::string sdmc_path; ///< SDMC root path ("Nintendo 3DS//") - std::string user_path; ///< Target user path of Citra + int version = 0; ///< Version of the dumper used. - // Necessary system files keys are loaded from. - std::string movable_sed_path; ///< Path to movable.sed - std::string bootrom_path; ///< Path to bootrom (boot9.bin) (Sysdata 0) - std::string certs_db_path; ///< Path to certs.db. Used while building CIA. + std::string user_path; ///< Target user path of Citra + std::string sdmc_path; ///< SDMC root path ("Nintendo 3DS//") + std::string id0; ///< ID0 of the SDMC used in this configuration. - // Optional, used while building CIA, but usually missing these files won't hinder CIA building. - std::string nand_title_db_path; ///< Path to NAND title.db. Entirely optional. - std::string ticket_db_path; ///< Path to ticket.db. Entirely optional. + // Necessary system files + std::string bootrom_path; ///< Path to bootrom (boot9.bin) (Sysdata 0) + std::string secret_sector_path; ///< Path to secret sector (New3DS only) (Sysdata 2) std::string enc_title_keys_bin_path; ///< Path to encTitleKeys.bin. Entirely optional. - // The following system files are optional for importing and are only copied so that Citra - // will be able to decrypt imported encrypted ROMs. - - std::string seed_db_path; ///< Path to seeddb.bin (Sysdata 1) - std::string secret_sector_path; ///< Path to secret sector (New3DS only) (Sysdata 2) - // Note: Sysdata 3 is aes_keys.txt (slot0x25KeyX) - - std::string system_titles_path; ///< Path to system titles. - std::string nand_data_path; ///< Path to NAND data. (Extdata and savedata) - - int version = 0; ///< Version of the dumper used. + struct NandConfig { + std::string nand_name; ///< Name of the NAND used in this configuration. + std::string movable_sed_path; ///< Path to movable.sed + + std::string certs_db_path; ///< Path to certs.db. Used while building CIA. + std::string ticket_db_path; ///< Path to ticket.db. Entirely optional. + std::string title_db_path; ///< Path to NAND title.db. Entirely optional. + std::string seed_db_path; ///< Path to seeddb.bin + + std::string title_path; ///< Path to system titles. + std::string data_path; ///< Path to NAND data. (Extdata and savedata) + }; + /// A list of NandConfigs with the same ID0 (linked NANDs). + /// The order of the NANDs matter: the importer will merge the dbs, but will only load NAND + /// titles and data from the *first* NAND. + std::vector nands; }; // Version of the current dumper. constexpr int CurrentDumperVersion = 4; +constexpr bool IsConfigGood(const Config& config) { + return config.version == CurrentDumperVersion && !config.user_path.empty() && + !config.sdmc_path.empty() && !config.bootrom_path.empty() && !config.nands.empty() && + !config.nands[0].movable_sed_path.empty(); +} + +constexpr bool IsConfigComplete(const Config& config) { + // We are skipping the DBs here. Need more work regarding that + return IsConfigGood(config) && !config.nands[0].title_path.empty() && + !config.nands[0].data_path.empty(); +} + class SDMCFile; class NCCHContainer; @@ -94,7 +109,6 @@ class SDMCImporter { public: /** * Initializes the importer. - * @param root_folder Path to the "Nintendo 3DS//" folder. */ explicit SDMCImporter(const Config& config); @@ -211,6 +225,7 @@ class SDMCImporter { bool is_good{}; Config config; + Config::NandConfig nand_config; // Main NAND config std::unique_ptr sdmc_decryptor; FileDecryptor file_decryptor; @@ -227,6 +242,9 @@ class SDMCImporter { /** * Look for and load preset config for a SD card mounted at mount_point. + * Note: This returns only one config per ID0. + * The frontend should allow the user to change the order of the NANDs. + * * @return a list of preset config available. can be empty */ std::vector LoadPresetConfig(std::string mount_point); diff --git a/src/frontend/main.cpp b/src/frontend/main.cpp index eb4a8b3..b62120a 100644 --- a/src/frontend/main.cpp +++ b/src/frontend/main.cpp @@ -31,17 +31,6 @@ Q_IMPORT_PLUGIN(QWindowsVistaStylePlugin) #endif #endif -bool IsConfigGood(const Core::Config& config) { - return !config.sdmc_path.empty() && !config.user_path.empty() && - !config.movable_sed_path.empty() && !config.bootrom_path.empty(); -} - -bool IsConfigComplete(const Core::Config& config) { - return IsConfigGood(config) && !config.certs_db_path.empty() && - !config.nand_title_db_path.empty() && !config.ticket_db_path.empty() && - !config.system_titles_path.empty() && !config.nand_data_path.empty(); -} - MainDialog::MainDialog(QWidget* parent) : DPIAwareDialog(parent, 640, 256), ui(std::make_unique()) { @@ -116,8 +105,6 @@ void MainDialog::SetContentSizes(int previous_width, int previous_height) { } } -static const std::regex sdmc_path_regex{"(.+)([/\\\\])Nintendo 3DS/([0-9a-f]{32})/([0-9a-f]{32})/"}; - void MainDialog::LoadPresetConfig() { ui->main->clear(); preset_config_list.clear(); @@ -137,24 +124,16 @@ void MainDialog::LoadPresetConfig() { } // Get ID0 - QString id0 = tr("Unknown"); - std::smatch match; - if (std::regex_match(list[i].sdmc_path, match, sdmc_path_regex)) { - if (match.size() >= 5) { - id0 = QString::fromStdString(match[3].str()); - } - } + QString id0 = QString::fromStdString(list[i].id0); // Get status QString status = tr("Good"); - if (!IsConfigGood(list[i])) { - status = tr("No Configuration Found"); - } else if (list[i].version != Core::CurrentDumperVersion) { + if (list[i].version != Core::CurrentDumperVersion) { status = tr("Version Dismatch"); + } else if (!IsConfigGood(list[i])) { + status = tr("No Configuration Found"); } else if (!IsConfigComplete(list[i])) { status = tr("Missing System Files"); - } else if (list[i].seed_db_path.empty()) { - status = tr("Good, Missing Seeds"); } auto* item = new QTreeWidgetItem{{path, id0, status}}; @@ -201,7 +180,14 @@ void MainDialog::LaunchImportDialog() { config = preset_config_list.at(index); } - // Check config integrity + // Display info regarding status + if (config.version != Core::CurrentDumperVersion) { + QMessageBox::critical(this, tr("Version Dismatch"), + tr("You are using an unsupported version of threeSDumper.
Please " + "ensure that you are using the most recent version of both " + "threeSD and threeSDumper and try again.")); + return; + } if (!IsConfigGood(config)) { QMessageBox::critical( this, tr("Error"), @@ -211,15 +197,6 @@ void MainDialog::LaunchImportDialog() { "guide correctly.")); return; } - - if (config.version != Core::CurrentDumperVersion) { - QMessageBox::critical(this, tr("Version Dismatch"), - tr("You are using an unsupported version of threeSDumper.
Please " - "ensure that you are using the most recent version of both " - "threeSD and threeSDumper and try again.")); - return; - } - if (!IsConfigComplete(config)) { QMessageBox::warning( this, tr("Warning"), @@ -227,11 +204,6 @@ void MainDialog::LaunchImportDialog() { "may not be importable, or may not run.
Please check if you have followed the guide " "correctly.")); - } else if (config.seed_db_path.empty()) { - QMessageBox::warning(this, tr("Warning"), - tr("Seed database is missing from your configuration.
Your system " - "likely does not have any seeds.
However, if it does have any, " - "imported games using seed encryption may not work.")); } ImportDialog dialog(this, config);