diff --git a/3ds/assets/romfs/config.json b/3ds/assets/romfs/config.json index 361ee7bb..18b85634 100644 --- a/3ds/assets/romfs/config.json +++ b/3ds/assets/romfs/config.json @@ -11,7 +11,8 @@ "additional_extdata_folders": { }, + "dsiware_saves": true, "nand_saves": false, "scan_cart": false, - "version": 3 + "version": 4 } \ No newline at end of file diff --git a/3ds/include/archive.hpp b/3ds/include/archive.hpp index 02469826..d3194e27 100644 --- a/3ds/include/archive.hpp +++ b/3ds/include/archive.hpp @@ -36,11 +36,13 @@ typedef enum { MODE_SAVE, MODE_EXTDATA } Mode_t; namespace Archive { Result init(void); + Result initTWLN(void); void exit(void); Mode_t mode(void); void mode(Mode_t v); FS_Archive sdmc(void); + FS_Archive twln(void); Result save(FS_Archive* archive, FS_MediaType mediatype, u32 lowid, u32 highid); Result extdata(FS_Archive* archive, u32 extdata); diff --git a/3ds/include/configuration.hpp b/3ds/include/configuration.hpp index f6da9afb..79af91bc 100644 --- a/3ds/include/configuration.hpp +++ b/3ds/include/configuration.hpp @@ -34,7 +34,7 @@ #include #include -#define CONFIG_VERSION 3 +#define CONFIG_VERSION 4 class Configuration { public: @@ -46,8 +46,10 @@ class Configuration { bool filter(u64 id); bool favorite(u64 id); + bool dsiwareSaves(void); bool nandSaves(void); bool shouldScanCard(void); + void disableDSiWareSaves(); std::vector additionalSaveFolders(u64 id); std::vector additionalExtdataFolders(u64 id); @@ -55,6 +57,8 @@ class Configuration { Configuration(void); ~Configuration() = default; + void editJsonElement(const char* element, nlohmann::json value); + void store(void); nlohmann::json loadJson(const std::string& path); void storeJson(nlohmann::json& json, const std::string& path); @@ -65,7 +69,7 @@ class Configuration { nlohmann::json mJson; std::unordered_set mFilterIds, mFavoriteIds; std::unordered_map> mAdditionalSaveFolders, mAdditionalExtdataFolders; - bool mNandSaves, mScanCard; + bool mDSiWareSaves, mNandSaves, mScanCard; std::string BASEPATH = "/3ds/Checkpoint/config.json"; }; diff --git a/3ds/include/io.hpp b/3ds/include/io.hpp index bfc8004e..c785b867 100644 --- a/3ds/include/io.hpp +++ b/3ds/include/io.hpp @@ -47,6 +47,7 @@ namespace io { void copyFile(FS_Archive srcArch, FS_Archive dstArch, const std::u16string& srcPath, const std::u16string& dstPath); Result createDirectory(FS_Archive archive, const std::u16string& path); void deleteBackupFolder(const std::u16string& path); + Result deleteFolderContentsRecursively(FS_Archive arch, const std::u16string& path); Result deleteFolderRecursively(FS_Archive arch, const std::u16string& path); bool directoryExists(FS_Archive archive, const std::u16string& path); bool fileExists(FS_Archive archive, const std::u16string& path); diff --git a/3ds/source/archive.cpp b/3ds/source/archive.cpp index 321dd0fa..7a8f6076 100644 --- a/3ds/source/archive.cpp +++ b/3ds/source/archive.cpp @@ -27,6 +27,7 @@ #include "archive.hpp" static FS_Archive mSdmc; +static FS_Archive mTwln; static Mode_t mMode = MODE_SAVE; Mode_t Archive::mode(void) @@ -44,9 +45,15 @@ Result Archive::init(void) return FSUSER_OpenArchive(&mSdmc, ARCHIVE_SDMC, fsMakePath(PATH_EMPTY, "")); } +Result Archive::initTWLN(void) +{ + return FSUSER_OpenArchive(&mTwln, ARCHIVE_NAND_TWL_FS, fsMakePath(PATH_EMPTY, "")); +} + void Archive::exit(void) { FSUSER_CloseArchive(mSdmc); + FSUSER_CloseArchive(mTwln); } FS_Archive Archive::sdmc(void) @@ -54,6 +61,11 @@ FS_Archive Archive::sdmc(void) return mSdmc; } +FS_Archive Archive::twln(void) +{ + return mTwln; +} + Result Archive::save(FS_Archive* archive, FS_MediaType mediatype, u32 lowid, u32 highid) { if (mediatype == MEDIATYPE_NAND) { diff --git a/3ds/source/configuration.cpp b/3ds/source/configuration.cpp index 413ceb14..8378a58d 100644 --- a/3ds/source/configuration.cpp +++ b/3ds/source/configuration.cpp @@ -50,6 +50,10 @@ Configuration::Configuration(void) mJson["version"] = CONFIG_VERSION; updateJson = true; } + if (!(mJson.contains("dsiware_saves") && mJson["dsiware_saves"].is_boolean())) { + mJson["dsiware_saves"] = true; + updateJson = true; + } if (!(mJson.contains("nand_saves") && mJson["nand_saves"].is_boolean())) { mJson["nand_saves"] = false; updateJson = true; @@ -122,8 +126,9 @@ Configuration::Configuration(void) mFavoriteIds.emplace(strtoull(id.c_str(), NULL, 16)); } - mNandSaves = mJson["nand_saves"]; - mScanCard = mJson["scan_cart"]; + mDSiWareSaves = mJson["dsiware_saves"]; + mNandSaves = mJson["nand_saves"]; + mScanCard = mJson["scan_cart"]; // parse additional save folders auto js = mJson["additional_save_folders"]; @@ -148,6 +153,11 @@ Configuration::Configuration(void) } } +void Configuration::editJsonElement(const char* element, nlohmann::json value) +{ + mJson[element] = value; +} + nlohmann::json Configuration::loadJson(const std::string& path) { nlohmann::json json; @@ -188,6 +198,11 @@ bool Configuration::favorite(u64 id) return mFavoriteIds.find(id) != mFavoriteIds.end(); } +bool Configuration::dsiwareSaves(void) +{ + return mDSiWareSaves; +} + bool Configuration::nandSaves(void) { return mNandSaves; @@ -211,3 +226,10 @@ bool Configuration::shouldScanCard(void) { return mScanCard; } + +void Configuration::disableDSiWareSaves(void) +{ + mDSiWareSaves = false; + editJsonElement("dsiware_saves", false); + storeJson(mJson, BASEPATH); +} diff --git a/3ds/source/io.cpp b/3ds/source/io.cpp index 68d13a41..6967a0e7 100644 --- a/3ds/source/io.cpp +++ b/3ds/source/io.cpp @@ -141,7 +141,7 @@ bool io::directoryExists(FS_Archive archive, const std::u16string& path) return true; } -Result io::deleteFolderRecursively(FS_Archive arch, const std::u16string& path) +Result io::deleteFolderContentsRecursively(FS_Archive arch, const std::u16string& path) { Directory dir(arch, path); if (!dir.good()) { @@ -161,6 +161,16 @@ Result io::deleteFolderRecursively(FS_Archive arch, const std::u16string& path) } } + return 0; +} + +Result io::deleteFolderRecursively(FS_Archive arch, const std::u16string& path) +{ + Result res = deleteFolderContentsRecursively(arch, path); + if (R_FAILED(res)) + { + return res; + } FSUSER_DeleteDirectory(arch, fsMakePath(PATH_UTF16, path.data())); return 0; } @@ -242,6 +252,58 @@ std::tuple io::backup(size_t index, size_t cellIndex) FSUSER_CloseArchive(archive); } + else if (title.mediaType() == MEDIATYPE_NAND) { + // DSiWare + std::string suggestion = DateTime::dateTimeStr(); + + std::u16string customPath; + if (MS::multipleSelectionEnabled()) { + customPath = isNewFolder ? StringUtils::UTF8toUTF16(suggestion.c_str()) : StringUtils::UTF8toUTF16(""); + } + else { + customPath = isNewFolder ? KeyboardManager::get().keyboard(suggestion) : StringUtils::UTF8toUTF16(""); + } + + std::u16string dstPath; + if (!isNewFolder) { + // we're overriding an existing folder + dstPath = title.fullSavePath(cellIndex); + } + else { + dstPath = title.savePath(); + dstPath += StringUtils::UTF8toUTF16("/") + customPath; + } + + if (!isNewFolder || io::directoryExists(Archive::sdmc(), dstPath)) { + res = FSUSER_DeleteDirectoryRecursively(Archive::sdmc(), fsMakePath(PATH_UTF16, dstPath.data())); + if (R_FAILED(res)) { + Logger::getInstance().log(Logger::ERROR, "Failed to delete the existing backup directory recursively with result 0x%08lX.", res); + return std::make_tuple(false, res, "Failed to delete the existing backup\ndirectory recursively."); + } + } + + res = io::createDirectory(Archive::sdmc(), dstPath); + if (R_FAILED(res)) { + Logger::getInstance().log(Logger::ERROR, "Failed to create destination directory."); + return std::make_tuple(false, res, "Failed to create destination directory."); + } + + std::u16string copyPath = dstPath + StringUtils::UTF8toUTF16("/"); + + char* saveDir = new char[31]; + sprintf(saveDir, "/title/%08lx/%08lx/data/", (title.highId() & 0x00000FFF) | 0x00030000, title.lowId()); + res = io::copyDirectory(Archive::twln(), Archive::sdmc(), StringUtils::UTF8toUTF16(saveDir), copyPath); + delete[] saveDir; + + if (R_FAILED(res)) { + std::string message = "Failed to backup save."; + FSUSER_DeleteDirectoryRecursively(Archive::sdmc(), fsMakePath(PATH_UTF16, dstPath.data())); + Logger::getInstance().log(Logger::ERROR, message + " Result 0x%08lX.", res); + return std::make_tuple(false, res, message); + } + + refreshDirectories(title.id()); + } else { CardType cardType = title.SPICardType(); u32 saveSize = SPIGetCapacity(cardType); @@ -386,6 +448,24 @@ std::tuple io::restore(size_t index, size_t cellIndex FSUSER_CloseArchive(archive); } + else if (title.mediaType() == MEDIATYPE_NAND) { + std::u16string srcPath = title.fullSavePath(cellIndex); + srcPath += StringUtils::UTF8toUTF16("/"); + + char* saveDir = new char[31]; + sprintf(saveDir, "/title/%08lx/%08lx/data/", (title.highId() & 0x00000FFF) | 0x00030000, title.lowId()); + std::u16string dstPath = StringUtils::UTF8toUTF16(saveDir); + delete[] saveDir; + + deleteFolderContentsRecursively(Archive::twln(), dstPath); + + res = io::copyDirectory(Archive::sdmc(), Archive::twln(), srcPath, dstPath); + if (R_FAILED(res)) { + std::string message = "Failed to restore save."; + Logger::getInstance().log(Logger::ERROR, message + ". Result 0x%08lX.", res); + return std::make_tuple(false, res, message); + } + } else { CardType cardType = title.SPICardType(); u32 saveSize = SPIGetCapacity(cardType); diff --git a/3ds/source/title.cpp b/3ds/source/title.cpp index 3102bff8..90b87966 100644 --- a/3ds/source/title.cpp +++ b/3ds/source/title.cpp @@ -36,16 +36,14 @@ static void exportTitleListCache(std::vector& list, const std::u16string& static void importTitleListCache(void); static constexpr Tex3DS_SubTexture dsIconSubt3x = {32, 32, 0.0f, 1.0f, 1.0f, 0.0f}; -static C2D_Image dsIcon = {nullptr, &dsIconSubt3x}; -static void loadDSIcon(u8* banner) +static C2D_Image loadDSIcon(u8* banner) { static constexpr int WIDTH_POW2 = 32; static constexpr int HEIGHT_POW2 = 32; - if (!dsIcon.tex) { - dsIcon.tex = new C3D_Tex; - C3D_TexInit(dsIcon.tex, WIDTH_POW2, HEIGHT_POW2, GPU_RGB565); - } + + C2D_Image iconds = {new C3D_Tex, &dsIconSubt3x}; + C3D_TexInit(iconds.tex, WIDTH_POW2, HEIGHT_POW2, GPU_RGB565); struct bannerData { u16 version; @@ -56,7 +54,7 @@ static void loadDSIcon(u8* banner) }; bannerData* iconData = (bannerData*)banner; - u16* output = (u16*)dsIcon.tex->data; + u16* output = (u16*)iconds.tex->data; for (size_t x = 0; x < 32; x++) { for (size_t y = 0; y < 32; y++) { u32 srcOff = (((y >> 3) * 4 + (x >> 3)) * 8 + (y & 7)) * 4 + ((x & 7) >> 1); @@ -76,6 +74,8 @@ static void loadDSIcon(u8* banner) output[dst] = color; } } + + return iconds; } void Title::load(void) @@ -173,10 +173,10 @@ bool Title::load(u64 _id, FS_MediaType _media, FS_CardType _card) } else { u8* headerData = new u8[0x3B4]; - Result res = FSUSER_GetLegacyRomHeader(mMedia, 0LL, headerData); + Result res = FSUSER_GetLegacyRomHeader(mMedia, mMedia == MEDIATYPE_GAME_CARD ? 0LL : mId, headerData); if (R_FAILED(res)) { delete[] headerData; - Logger::getInstance().log(Logger::ERROR, "Failed get legacy rom header with result 0x%08lX.", res); + Logger::getInstance().log(Logger::ERROR, "Failed get legacy rom header for title 0x%016llX with result 0x%08lX.", mMedia == MEDIATYPE_GAME_CARD ? 0LL : mId, res); return false; } @@ -190,15 +190,16 @@ bool Title::load(u64 _id, FS_MediaType _media, FS_CardType _card) delete[] headerData; headerData = new u8[0x23C0]; - FSUSER_GetLegacyBannerData(mMedia, 0LL, headerData); - loadDSIcon(headerData); - mIcon = dsIcon; + FSUSER_GetLegacyBannerData(mMedia, mMedia == MEDIATYPE_GAME_CARD ? 0LL : mId, headerData); + mIcon = loadDSIcon(headerData); delete[] headerData; - res = SPIGetCardType(&mCardType, (_gameCode[0] == 'I') ? 1 : 0); - if (R_FAILED(res)) { - Logger::getInstance().log(Logger::ERROR, "Failed get SPI Card Type with result 0x%08lX.", res); - return false; + if (mMedia == MEDIATYPE_GAME_CARD) { + res = SPIGetCardType(&mCardType, (_gameCode[0] == 'I') ? 1 : 0); + if (R_FAILED(res)) { + Logger::getInstance().log(Logger::ERROR, "Failed get SPI Card Type with result 0x%08lX.", res); + return false; + } } mShortDescription = StringUtils::removeForbiddenCharacters(StringUtils::UTF8toUTF16(_cardTitle)); @@ -208,15 +209,23 @@ bool Title::load(u64 _id, FS_MediaType _media, FS_CardType _card) mExtdataPath = mSavePath; memset(productCode, 0, 16); - mAccessibleSave = true; + if (mMedia == MEDIATYPE_GAME_CARD) mAccessibleSave = true; + else { + char* saveDir = new char[30]; + sprintf(saveDir, "/title/%08lx/%08lx/data", (highId() & 0x00000FFF) | 0x00030000, lowId()); + mAccessibleSave = Directory(Archive::twln(), StringUtils::UTF8toUTF16(saveDir)).good(); + delete[] saveDir; + } mAccessibleExtdata = false; - loadTitle = true; - if (!io::directoryExists(Archive::sdmc(), mSavePath)) { - res = io::createDirectory(Archive::sdmc(), mSavePath); - if (R_FAILED(res)) { - loadTitle = false; - Logger::getInstance().log(Logger::ERROR, "Failed to create backup directory with result 0x%08lX.", res); + if (mAccessibleSave) { + loadTitle = true; + if (!io::directoryExists(Archive::sdmc(), mSavePath)) { + res = io::createDirectory(Archive::sdmc(), mSavePath); + if (R_FAILED(res)) { + loadTitle = false; + Logger::getInstance().log(Logger::ERROR, "Failed to create backup directory with result 0x%08lX.", res); + } } } } @@ -486,6 +495,11 @@ static bool validId(u64 id) return false; } + // check for DSi non-executable data files + if (high == 0x0004800F) { + return false; + } + return !Configuration::getInstance().filter(id); } @@ -547,7 +561,7 @@ void loadTitles(bool forceRefresh) else { u32 count = 0; - if (Configuration::getInstance().nandSaves()) { + if (Configuration::getInstance().nandSaves() || Configuration::getInstance().dsiwareSaves()) { AM_GetTitleCount(MEDIATYPE_NAND, &count); std::unique_ptr<u64[]> ids_nand = std::unique_ptr<u64[]>(new u64[count]); AM_GetTitleList(NULL, MEDIATYPE_NAND, count, ids_nand.get()); @@ -555,11 +569,23 @@ void loadTitles(bool forceRefresh) for (u32 i = 0; i < count; i++) { if (validId(ids_nand[i])) { Title title; - if (title.load(ids_nand[i], MEDIATYPE_NAND, CARD_CTR)) { - if (title.accessibleSave()) { - titleSaves.push_back(title); + if (Configuration::getInstance().dsiwareSaves()) { + if (((ids_nand[i] >> 44) & 0x0000F) == 8) { + if (title.load(ids_nand[i], MEDIATYPE_NAND, CARD_TWL)) { + if (title.accessibleSave()) { + titleSaves.push_back(title); + } + } + continue; + } + } + if (Configuration::getInstance().nandSaves()) { + if (title.load(ids_nand[i], MEDIATYPE_NAND, CARD_CTR)) { + if (title.accessibleSave()) { + titleSaves.push_back(title); + } + // TODO: extdata? } - // TODO: extdata? } } } @@ -689,6 +715,19 @@ bool favorite(int i) return Configuration::getInstance().favorite(id); } +static C2D_Image loadDSIconFromBytes(u16* iconData) +{ + static constexpr int WIDTH_POW2 = 32; + static constexpr int HEIGHT_POW2 = 32; + + C2D_Image iconds = {new C3D_Tex, &dsIconSubt3x}; + C3D_TexInit(iconds.tex, WIDTH_POW2, HEIGHT_POW2, GPU_RGB565); + + memcpy(iconds.tex->data, iconData, 0x400 * 2); + + return iconds; +} + static C2D_Image loadTextureFromBytes(u16* bigIconData) { C3D_Tex* tex = (C3D_Tex*)malloc(sizeof(C3D_Tex)); @@ -772,6 +811,10 @@ static void exportTitleListCache(std::vector<Title>& list, const std::u16string& } delete smdh; } + else if (media == MEDIATYPE_NAND) { + // wasting 2.5 KB of space because we're that rich + memcpy(cache.get() + i * ENTRYSIZE + 733, list.at(i).icon().tex->data, 0x400 * 2); + } memcpy(cache.get() + i * ENTRYSIZE + 0, &id, sizeof(u64)); memcpy(cache.get() + i * ENTRYSIZE + 8, list.at(i).productCode, 16); @@ -858,8 +901,10 @@ static void importTitleListCache(void) title.setIcon(smallIcon); } else { - // this cannot happen - title.setIcon(Gui::noIcon()); + u16 iconData[0x400]; + memcpy(iconData, cachesaves + i * ENTRYSIZE + 733, 0x400 * 2); + C2D_Image iconds = loadDSIconFromBytes(iconData); + title.setIcon(iconds); } titleSaves.at(i) = title; diff --git a/3ds/source/util.cpp b/3ds/source/util.cpp index e8120a27..ddc26570 100644 --- a/3ds/source/util.cpp +++ b/3ds/source/util.cpp @@ -71,6 +71,15 @@ Result servicesInit(void) romfsInit(); ATEXIT(romfsExit); + // this conditionally depends on ROMFS + if (Configuration::getInstance().dsiwareSaves()) { + if (R_FAILED(res = Archive::initTWLN())) { + Logger::getInstance().log(Logger::ERROR, "Failed to init TWLN with result 0x%08lX", res); + Configuration::getInstance().disableDSiWareSaves(); + return consoleDisplayError("Archive::initTWLN failed. DSiWare loading disabled.", res); + } + } + srvInit(); ATEXIT(srvExit); @@ -95,14 +104,15 @@ void calculateTitleDBHash(u8* hash) { u32 titleCount, nandCount, titlesRead, nandTitlesRead; AM_GetTitleCount(MEDIATYPE_SD, &titleCount); - if (Configuration::getInstance().nandSaves()) { + if (Configuration::getInstance().nandSaves() || Configuration::getInstance().dsiwareSaves()) { AM_GetTitleCount(MEDIATYPE_NAND, &nandCount); std::vector<u64> ordered; - ordered.reserve(titleCount + nandCount); + ordered.reserve(titleCount + nandCount + Configuration::getInstance().dsiwareSaves()); AM_GetTitleList(&titlesRead, MEDIATYPE_SD, titleCount, ordered.data()); AM_GetTitleList(&nandTitlesRead, MEDIATYPE_NAND, nandCount, ordered.data() + titlesRead * sizeof(u64)); + if (Configuration::getInstance().dsiwareSaves()) ordered.push_back(0x0123456789ABCDEF); // hack to make hash different sort(ordered.begin(), ordered.end()); - sha256(hash, (u8*)ordered.data(), (titleCount + nandCount) * sizeof(u64)); + sha256(hash, (u8*)ordered.data(), (titleCount + nandCount + Configuration::getInstance().dsiwareSaves()) * sizeof(u64)); } else { std::vector<u64> ordered;