mirror of
https://github.com/Dark98/threeSD.git
synced 2026-07-03 00:38:58 +00:00
Add system archives import
This commit is contained in:
@@ -25,7 +25,7 @@ If you are wishing to use threeSD with a portable install of Citra (i.e. that ha
|
||||
* You can install GodMode9 by downloading it and copying the `firm` file to `luma/payloads` on your SD card. You can rename it to begin with `[BUTTON]_` (e.g. `X_GodMode9.firm`) to set a convenicence button to hold during boot to enter GodMode9.
|
||||
* PC compatible with Citra
|
||||
* You will need a graphics card compatible with OpenGL 3.3 and install the latest graphics drivers from your vendor's website.
|
||||
* Operating system requirements: **64-bit** Windows (7+), Linux (flatpak compatible) or macOS (10.13+). Note that Citra on macOS 10.13 is currently broken. It is recommended to update to 10.14.
|
||||
* Operating system requirements: **64-bit** Windows (7+), Linux (flatpak compatible) or macOS (10.13+).
|
||||
* SD / microSD card reader
|
||||
* Make sure it can be well connected to your PC (i.e. do not use a 10-year-old dusty one)
|
||||
|
||||
@@ -36,7 +36,7 @@ You will need to run a GodMode9 script. If you are unsure about the script's saf
|
||||
1. Copy the gm9 script (`threeSDumper.gm9`) in `dist` to the `gm9/scripts` folder on your SD card.
|
||||
1. Launch GodMode9 on your 3DS (you will need to hold a button corresponding to your `firm` file's name, or hold `START` to enter the chainloader menu). Press the `Home` button to bring up GodMode9's `HOME Menu`. Use the d-pad and the `A` button to select `Scripts...`.
|
||||
1. Use the d-pad and the `A` button to select `threeSDumper`. You will be prompted with a question "Execute threeSD Dumper?". Press `A` to confirm.
|
||||
1. After a moment or two you will see the message "Successfully dumped necessary files for threeSD." Your 3DS SD card is now prepared for use with threeSD and Citra. Press `A` to exit the script.
|
||||
1. After a few seconds, you will see the message "Successfully dumped necessary files for threeSD." Your 3DS SD card is now prepared for use with threeSD and Citra. Press `A` to exit the script.
|
||||
1. Power off your 3DS with `R+START`. Remove the SD card from your 3DS and insert it into your PC (with a card reader).
|
||||
|
||||
### On your PC
|
||||
@@ -47,7 +47,7 @@ Make sure the SD card is properly recognized and shows up as a disk.
|
||||
* If it does not show up and the combo box says `None`, you should check if you can really find your SD card in the explorer (aka. `My Computer`), whether the drive for your SD card is accessible, and whether it contains the `Nintendo 3DS` and `threeSD` folders.
|
||||
1. Click `OK`. After a few seconds of loading, you should see the `Select Contents` dialog. Select the contents you would like to import. By default, contents that do not currently exist is selected. Make sure the total size of your selected contents do not exceed the available space on your disk.
|
||||
* You can select between `Title View` which organizes contents by title, and `Group View` which organizes contents by type (application, save data, etc).
|
||||
* `System Data` (which resides in `Ungrouped` when using `Title View`) contains important data that is necessary for your imported games to run. You should definitely import the contents there, if they do not exist yet.
|
||||
* The `System Archive` and `System Data` groups contains important data that is necessary for your imported games to run. You should definitely import the contents there, if they do not exist yet.
|
||||
1. After you've finished your selection, click `OK`. You should now see a progress dialog; wait a while until your contents are imported.
|
||||
* The time will depend on how big your contents are, as well as your CPU processing power and (mainly) disk I/O speeds.
|
||||
|
||||
@@ -55,13 +55,12 @@ Make sure the SD card is properly recognized and shows up as a disk.
|
||||
|
||||
You can now enjoy your games with Citra, at high resolutions, with custom controllers, and the (now in Canary) Custom Textures feature!
|
||||
|
||||
It is recommended that you also [dump your system archives and shared fonts](https://citra-emu.org/wiki/dumping-system-archives-and-the-shared-fonts-from-a-3ds-console) and optionally [dump your config savegame](https://citra-emu.org/wiki/dumping-config-savegame-from-a-3ds-console) (if you come across problems), for the best experience while enjoying Citra.
|
||||
It is recommended that you also optionally [dump your config savegame](https://citra-emu.org/wiki/dumping-config-savegame-from-a-3ds-console) if you come across problems, for the best experience while enjoying Citra.
|
||||
|
||||
If you have any game cartidges, and would like to dump them as well, visit [this tutorial](https://citra-emu.org/wiki/dumping-game-cartridges).
|
||||
|
||||
## TODO
|
||||
|
||||
* System archives
|
||||
* Config savegame
|
||||
* UI improvements
|
||||
* Better error messages
|
||||
|
||||
Vendored
+47
@@ -48,6 +48,53 @@ sdump -w seeddb.bin
|
||||
cp -w -n "0:/gm9/out/seeddb.bin" $[OUT]/seeddb.bin
|
||||
rm "0:/gm9/out/seeddb.bin"
|
||||
|
||||
if not find $[OUT]/sysarchives NULL
|
||||
mkdir $[OUT]/sysarchives
|
||||
end
|
||||
|
||||
if not find $[OUT]/sysarchives/0004009b NULL
|
||||
mkdir $[OUT]/sysarchives/0004009b
|
||||
end
|
||||
|
||||
# Mii Data
|
||||
find 1:/title/0004009b/00010202/content/*.app APP
|
||||
cp -w -n $[APP] $[OUT]/sysarchives/0004009b/00010202.app
|
||||
decrypt $[OUT]/sysarchives/0004009b/00010202.app
|
||||
|
||||
# Region Manifest
|
||||
find 1:/title/0004009b/00010402/content/*.app APP
|
||||
cp -w -n $[APP] $[OUT]/sysarchives/0004009b/00010402.app
|
||||
decrypt $[OUT]/sysarchives/0004009b/00010402.app
|
||||
|
||||
# Shared Font (JPN/EUR/USA)
|
||||
find 1:/title/0004009b/00014002/content/*.app APP
|
||||
cp -w -n $[APP] $[OUT]/sysarchives/0004009b/00014002.app
|
||||
decrypt $[OUT]/sysarchives/0004009b/00014002.app
|
||||
|
||||
# Shared Font (CHN)
|
||||
find 1:/title/0004009b/00014102/content/*.app APP
|
||||
cp -w -n $[APP] $[OUT]/sysarchives/0004009b/00014102.app
|
||||
decrypt $[OUT]/sysarchives/0004009b/00014102.app
|
||||
|
||||
# Shared Font (KOR)
|
||||
find 1:/title/0004009b/00014202/content/*.app APP
|
||||
cp -w -n $[APP] $[OUT]/sysarchives/0004009b/00014202.app
|
||||
decrypt $[OUT]/sysarchives/0004009b/00014202.app
|
||||
|
||||
# Shared Font (TWN)
|
||||
find 1:/title/0004009b/00014302/content/*.app APP
|
||||
cp -w -n $[APP] $[OUT]/sysarchives/0004009b/00014302.app
|
||||
decrypt $[OUT]/sysarchives/0004009b/00014302.app
|
||||
|
||||
if not find $[OUT]/sysarchives/000400db NULL
|
||||
mkdir $[OUT]/sysarchives/000400db
|
||||
end
|
||||
|
||||
# NG Bad word list
|
||||
find 1:/title/000400db/00010302/content/*.app APP
|
||||
cp -w -n $[APP] $[OUT]/sysarchives/000400db/00010302.app
|
||||
decrypt $[OUT]/sysarchives/000400db/00010302.app
|
||||
|
||||
set PREVIEW_MODE "threeSD Dumper\nby zhaowenlan1779\n \nSuccess!"
|
||||
echo "Successfully dumped necessary\nfiles for threeSD."
|
||||
|
||||
|
||||
@@ -72,6 +72,8 @@ bool SDMCImporter::ImportContent(const ContentSpecifier& specifier,
|
||||
return ImportSavegame(specifier.id, callback);
|
||||
case ContentType::Extdata:
|
||||
return ImportExtdata(specifier.id, callback);
|
||||
case ContentType::SystemArchive:
|
||||
return ImportSystemArchive(specifier.id, callback);
|
||||
case ContentType::Sysdata:
|
||||
return ImportSysdata(specifier.id, callback);
|
||||
default:
|
||||
@@ -128,6 +130,45 @@ bool SDMCImporter::ImportExtdata(u64 id, [[maybe_unused]] const ProgressCallback
|
||||
"Nintendo 3DS/00000000000000000000000000000000/00000000000000000000000000000000/" + path);
|
||||
}
|
||||
|
||||
bool SDMCImporter::ImportSystemArchive(u64 id, [[maybe_unused]] const ProgressCallback& callback) {
|
||||
const auto path = fmt::format("{}{:08x}/{:08x}.app", config.system_archives_path, (id >> 32),
|
||||
(id & 0xFFFFFFFF));
|
||||
FileUtil::IOFile file(path, "rb");
|
||||
if (!file) {
|
||||
LOG_ERROR(Core, "Could not open {}", path);
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<u8> data(file.GetSize());
|
||||
if (file.ReadBytes(data.data(), data.size()) != data.size()) {
|
||||
LOG_ERROR(Core, "Failed to read from {}", path);
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto& romfs = LoadSharedRomFS(data);
|
||||
|
||||
const auto target_path = fmt::format(
|
||||
"{}00000000000000000000000000000000/title/{:08x}/{:08x}/content/00000000.app.romfs",
|
||||
FileUtil::GetUserPath(FileUtil::UserPath::NANDDir), (id >> 32), (id & 0xFFFFFFFF));
|
||||
if (!FileUtil::CreateFullPath(target_path)) {
|
||||
LOG_ERROR(Core, "Could not create path {}", target_path);
|
||||
return false;
|
||||
}
|
||||
|
||||
FileUtil::IOFile target(target_path, "wb");
|
||||
if (!target) {
|
||||
LOG_ERROR(Core, "Could not open {}", target_path);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (target.WriteBytes(romfs.data(), romfs.size()) != romfs.size()) {
|
||||
LOG_ERROR(Core, "Failed to write to {}", target_path);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SDMCImporter::ImportSysdata(u64 id, [[maybe_unused]] const ProgressCallback& callback) {
|
||||
switch (id) {
|
||||
case 0: { // boot9.bin
|
||||
@@ -207,6 +248,7 @@ std::vector<ContentSpecifier> SDMCImporter::ListContent() const {
|
||||
std::vector<ContentSpecifier> content_list;
|
||||
ListTitle(content_list);
|
||||
ListExtdata(content_list);
|
||||
ListSystemArchive(content_list);
|
||||
ListSysdata(content_list);
|
||||
return content_list;
|
||||
}
|
||||
@@ -361,6 +403,30 @@ void SDMCImporter::ListExtdata(std::vector<ContentSpecifier>& out) const {
|
||||
});
|
||||
}
|
||||
|
||||
void SDMCImporter::ListSystemArchive(std::vector<ContentSpecifier>& out) const {
|
||||
constexpr std::array<std::pair<u64, const char*>, 8> SystemArchives{{
|
||||
{0x0004009b'00010202, "Mii Data"},
|
||||
{0x0004009b'00010402, "Region Manifest"},
|
||||
{0x0004009b'00014002, "Shared Font (JPN/EUR/USA)"},
|
||||
{0x0004009b'00014102, "Shared Font (CHN)"},
|
||||
{0x0004009b'00014202, "Shared Font (KOR)"},
|
||||
{0x0004009b'00014302, "Shared Font (TWN)"},
|
||||
{0x000400db'00010302, "Bad word list"},
|
||||
}};
|
||||
|
||||
for (const auto& [id, name] : SystemArchives) {
|
||||
const auto path = fmt::format("{}{:08x}/{:08x}.app", config.system_archives_path,
|
||||
(id >> 32), (id & 0xFFFFFFFF));
|
||||
if (FileUtil::Exists(path)) {
|
||||
const auto target_path = fmt::format(
|
||||
"{}00000000000000000000000000000000/title/{:08x}/{:08x}/content/",
|
||||
FileUtil::GetUserPath(FileUtil::UserPath::NANDDir), (id >> 32), (id & 0xFFFFFFFF));
|
||||
out.push_back({ContentType::SystemArchive, id, FileUtil::Exists(target_path),
|
||||
FileUtil::GetSize(path), name});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SDMCImporter::ListSysdata(std::vector<ContentSpecifier>& out) const {
|
||||
#define CHECK_CONTENT(id, var_path, citra_path, display_name) \
|
||||
if (!var_path.empty()) { \
|
||||
@@ -418,6 +484,8 @@ void SDMCImporter::DeleteContent(const ContentSpecifier& specifier) {
|
||||
return DeleteSavegame(specifier.id);
|
||||
case ContentType::Extdata:
|
||||
return DeleteExtdata(specifier.id);
|
||||
case ContentType::SystemArchive:
|
||||
return DeleteSystemArchive(specifier.id);
|
||||
case ContentType::Sysdata:
|
||||
return DeleteSysdata(specifier.id);
|
||||
default:
|
||||
@@ -449,6 +517,12 @@ void SDMCImporter::DeleteExtdata(u64 id) const {
|
||||
FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir), (id >> 32), (id & 0xFFFFFFFF)));
|
||||
}
|
||||
|
||||
void SDMCImporter::DeleteSystemArchive(u64 id) const {
|
||||
FileUtil::DeleteDirRecursively(fmt::format(
|
||||
"{}00000000000000000000000000000000/title/{:08x}/{:08x}/content/",
|
||||
FileUtil::GetUserPath(FileUtil::UserPath::NANDDir), (id >> 32), (id & 0xFFFFFFFF)));
|
||||
}
|
||||
|
||||
void SDMCImporter::DeleteSysdata(u64 id) const {
|
||||
switch (id) {
|
||||
case 0: { // boot9.bin
|
||||
@@ -501,6 +575,7 @@ std::vector<Config> LoadPresetConfig(std::string mount_point) {
|
||||
LOAD_DATA(safe_mode_firm_path, "firm/");
|
||||
LOAD_DATA(seed_db_path, SEED_DB);
|
||||
LOAD_DATA(secret_sector_path, SECRET_SECTOR);
|
||||
LOAD_DATA(system_archives_path, "sysarchives/");
|
||||
#undef LOAD_DATA
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ enum class ContentType {
|
||||
DLC,
|
||||
Savegame,
|
||||
Extdata,
|
||||
SystemArchive,
|
||||
Sysdata,
|
||||
};
|
||||
|
||||
@@ -56,6 +57,7 @@ struct Config {
|
||||
std::string safe_mode_firm_path; ///< Path to safe mode firm (A folder) (Sysdata 1)
|
||||
std::string seed_db_path; ///< Path to seeddb.bin (Sysdata 2)
|
||||
std::string secret_sector_path; ///< Path to secret sector (New3DS only) (Sysdata 3)
|
||||
std::string system_archives_path; ///< Path to system archives.
|
||||
|
||||
// Sysdata 4 is aes_keys.db (slot0x25KeyX)
|
||||
};
|
||||
@@ -108,15 +110,18 @@ private:
|
||||
bool ImportTitle(u64 id, const ProgressCallback& callback);
|
||||
bool ImportSavegame(u64 id, const ProgressCallback& callback);
|
||||
bool ImportExtdata(u64 id, const ProgressCallback& callback);
|
||||
bool ImportSystemArchive(u64 id, const ProgressCallback& callback);
|
||||
bool ImportSysdata(u64 id, const ProgressCallback& callback);
|
||||
|
||||
void ListTitle(std::vector<ContentSpecifier>& out) const;
|
||||
void ListExtdata(std::vector<ContentSpecifier>& out) const;
|
||||
void ListSystemArchive(std::vector<ContentSpecifier>& out) const;
|
||||
void ListSysdata(std::vector<ContentSpecifier>& out) const;
|
||||
|
||||
void DeleteTitle(u64 id) const;
|
||||
void DeleteSavegame(u64 id) const;
|
||||
void DeleteExtdata(u64 id) const;
|
||||
void DeleteSystemArchive(u64 id) const;
|
||||
void DeleteSysdata(u64 id) const;
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,13 +3,18 @@
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#include <cinttypes>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <cryptopp/aes.h>
|
||||
#include <cryptopp/modes.h>
|
||||
#include <cryptopp/sha.h>
|
||||
#include "common/alignment.h"
|
||||
#include "common/assert.h"
|
||||
#include "common/common_funcs.h"
|
||||
#include "common/common_types.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "core/data_container.h"
|
||||
#include "core/key/key.h"
|
||||
#include "core/ncch/ncch_container.h"
|
||||
|
||||
@@ -304,4 +309,40 @@ bool NCCHContainer::HasExHeader() {
|
||||
return has_exheader;
|
||||
}
|
||||
|
||||
#pragma pack(push, 1)
|
||||
struct RomFSIVFCHeader {
|
||||
u32_le magic;
|
||||
u32_le version;
|
||||
u32_le master_hash_size;
|
||||
std::array<LevelDescriptor, 3> levels;
|
||||
INSERT_PADDING_BYTES(0xC);
|
||||
};
|
||||
static_assert(sizeof(RomFSIVFCHeader) == 0x60, "Size of RomFSIVFCHeader is incorrect");
|
||||
#pragma pack(pop)
|
||||
|
||||
std::vector<u8> LoadSharedRomFS(const std::vector<u8>& data) {
|
||||
NCCH_Header header;
|
||||
ASSERT_MSG(data.size() >= sizeof(header), "NCCH size is too small");
|
||||
std::memcpy(&header, data.data(), sizeof(header));
|
||||
|
||||
const std::size_t offset = header.romfs_offset * 0x200; // 0x200: Media unit
|
||||
RomFSIVFCHeader ivfc;
|
||||
ASSERT_MSG(data.size() >= offset + sizeof(ivfc), "NCCH size is too small");
|
||||
std::memcpy(&ivfc, data.data() + offset, sizeof(ivfc));
|
||||
|
||||
ASSERT_MSG(ivfc.magic == MakeMagic('I', 'V', 'F', 'C'), "IVFC magic is incorrect");
|
||||
ASSERT_MSG(ivfc.version == 0x10000, "IVFC version is incorrect");
|
||||
|
||||
std::vector<u8> result(ivfc.levels[2].size);
|
||||
|
||||
// Calculation from ctrtool
|
||||
const std::size_t data_offset =
|
||||
offset + Common::AlignUp(sizeof(ivfc) + ivfc.master_hash_size,
|
||||
std::pow(2, ivfc.levels[2].block_size));
|
||||
ASSERT_MSG(data.size() >= data_offset + ivfc.levels[2].size);
|
||||
std::memcpy(result.data(), data.data() + data_offset, ivfc.levels[2].size);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace Core
|
||||
|
||||
@@ -277,4 +277,10 @@ private:
|
||||
SDMCFile exefs_file;
|
||||
};
|
||||
|
||||
/**
|
||||
* Extracts the shared RomFS from a NCCH image.
|
||||
* Used for handling system archives.
|
||||
*/
|
||||
std::vector<u8> LoadSharedRomFS(const std::vector<u8>& data);
|
||||
|
||||
} // namespace Core
|
||||
|
||||
@@ -31,12 +31,13 @@ QString ReadableByteSize(qulonglong size) {
|
||||
.arg(QObject::tr(units[digit_groups], "ImportDialog"));
|
||||
}
|
||||
|
||||
static constexpr std::array<std::pair<Core::ContentType, const char*>, 6> ContentTypeMap{{
|
||||
static constexpr std::array<std::pair<Core::ContentType, const char*>, 7> ContentTypeMap{{
|
||||
{Core::ContentType::Application, QT_TR_NOOP("Application")},
|
||||
{Core::ContentType::Update, QT_TR_NOOP("Update")},
|
||||
{Core::ContentType::DLC, QT_TR_NOOP("DLC")},
|
||||
{Core::ContentType::Savegame, QT_TR_NOOP("Save Data")},
|
||||
{Core::ContentType::Extdata, QT_TR_NOOP("Extra Data")},
|
||||
{Core::ContentType::SystemArchive, QT_TR_NOOP("System Archive")},
|
||||
{Core::ContentType::Sysdata, QT_TR_NOOP("System Data")},
|
||||
}};
|
||||
|
||||
@@ -158,10 +159,20 @@ void ImportDialog::InsertSecondLevelItem(std::size_t row, const Core::ContentSpe
|
||||
// HACK: The checkbox is used to record ID. Is there a better way?
|
||||
checkBox->setProperty("id", id);
|
||||
|
||||
const QString name = (row == 0 ? QStringLiteral("%1 (%2)")
|
||||
QString name;
|
||||
if (ui->title_view_button->isChecked()) {
|
||||
if (row == 0) {
|
||||
name = QStringLiteral("%1 (%2)")
|
||||
.arg(GetContentName(content))
|
||||
.arg(GetContentTypeName(content.type))
|
||||
: GetContentTypeName(content.type));
|
||||
.arg(GetContentTypeName(content.type));
|
||||
} else if (row <= 2) {
|
||||
name = GetContentName(content);
|
||||
} else {
|
||||
name = GetContentTypeName(content.type);
|
||||
}
|
||||
} else {
|
||||
name = GetContentName(content);
|
||||
}
|
||||
|
||||
auto* item = new QTreeWidgetItem{
|
||||
{QString{}, name, ReadableByteSize(content.maximum_size),
|
||||
@@ -205,6 +216,8 @@ void ImportDialog::RepopulateContent() {
|
||||
}
|
||||
}
|
||||
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(2, tr("System Data"));
|
||||
|
||||
std::unordered_map<u64, u64> title_row_map;
|
||||
for (const auto& [id, name] : title_name_map) {
|
||||
@@ -233,8 +246,12 @@ void ImportDialog::RepopulateContent() {
|
||||
row = title_row_map.at(real_id);
|
||||
break;
|
||||
}
|
||||
case Core::ContentType::SystemArchive: {
|
||||
row = title_row_map.at(1); // System archive
|
||||
break;
|
||||
}
|
||||
case Core::ContentType::Sysdata: {
|
||||
row = title_row_map.at(0);
|
||||
row = title_row_map.at(2); // System data
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user