2018-05-14 06:52:41 +00:00
|
|
|
/*
|
|
|
|
* This file is part of Checkpoint
|
|
|
|
* Copyright (C) 2017-2018 Bernardo Giordano
|
|
|
|
*
|
|
|
|
* This program is free software: you can redistribute it and/or modify
|
|
|
|
* it under the terms of the GNU General Public License as published by
|
|
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
|
|
* (at your option) any later version.
|
|
|
|
*
|
|
|
|
* This program is distributed in the hope that it will be useful,
|
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
* GNU General Public License for more details.
|
|
|
|
*
|
|
|
|
* You should have received a copy of the GNU General Public License
|
|
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
*
|
|
|
|
* Additional Terms 7.b and 7.c of GPLv3 apply to this file:
|
|
|
|
* * Requiring preservation of specified reasonable legal notices or
|
|
|
|
* author attributions in that material or in the Appropriate Legal
|
|
|
|
* Notices displayed by works containing it.
|
|
|
|
* * Prohibiting misrepresentation of the origin of that material,
|
|
|
|
* or requiring that modified versions of such material be marked in
|
|
|
|
* reasonable ways as different from the original version.
|
2017-09-29 07:45:56 +00:00
|
|
|
*/
|
|
|
|
|
2018-05-14 06:52:41 +00:00
|
|
|
#include "title.hpp"
|
2017-09-29 07:45:56 +00:00
|
|
|
|
2018-05-26 20:25:48 +00:00
|
|
|
static bool validId(u64 id);
|
2018-05-14 06:52:41 +00:00
|
|
|
static C2D_Image loadTextureIcon(smdh_s *smdh);
|
2017-09-29 07:45:56 +00:00
|
|
|
|
2017-09-30 10:06:33 +00:00
|
|
|
static std::vector<Title> titleSaves;
|
|
|
|
static std::vector<Title> titleExtdatas;
|
2017-09-29 07:45:56 +00:00
|
|
|
|
2017-12-25 18:10:41 +00:00
|
|
|
static void exportTitleListCache(std::vector<Title> list, const std::u16string path);
|
|
|
|
static void importTitleListCache(void);
|
|
|
|
|
|
|
|
void Title::load(void)
|
|
|
|
{
|
2018-05-14 06:52:41 +00:00
|
|
|
mId = 0xFFFFFFFFFFFFFFFF;
|
|
|
|
mMedia = MEDIATYPE_SD;
|
|
|
|
mCard = CARD_CTR;
|
|
|
|
memset(productCode, 0, 16);
|
|
|
|
mShortDescription = StringUtils::UTF8toUTF16(" ");
|
|
|
|
mLongDescription = StringUtils::UTF8toUTF16(" ");
|
|
|
|
mSavePath = StringUtils::UTF8toUTF16(" ");
|
|
|
|
mExtdataPath = StringUtils::UTF8toUTF16(" ");
|
|
|
|
mIcon = Gui::noIcon();
|
|
|
|
mAccessibleSave = false;
|
|
|
|
mAccessibleExtdata = false;
|
|
|
|
mSaves.clear();
|
|
|
|
mExtdata.clear();
|
2017-12-25 18:10:41 +00:00
|
|
|
}
|
|
|
|
|
2017-10-04 11:18:41 +00:00
|
|
|
bool Title::load(u64 _id, FS_MediaType _media, FS_CardType _card)
|
2017-09-29 07:45:56 +00:00
|
|
|
{
|
2018-05-14 06:52:41 +00:00
|
|
|
bool loadTitle = false;
|
|
|
|
mId = _id;
|
|
|
|
mMedia = _media;
|
|
|
|
mCard = _card;
|
|
|
|
|
|
|
|
if (mCard == CARD_CTR)
|
|
|
|
{
|
|
|
|
smdh_s *smdh = loadSMDH(lowId(), highId(), mMedia);
|
|
|
|
if (smdh == NULL)
|
|
|
|
{
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
char unique[12] = {0};
|
|
|
|
sprintf(unique, "0x%05X ", (unsigned int)uniqueId());
|
|
|
|
|
|
|
|
mShortDescription = StringUtils::removeForbiddenCharacters((char16_t*)smdh->applicationTitles[1].shortDescription);
|
|
|
|
mLongDescription = (char16_t*)smdh->applicationTitles[1].longDescription;
|
|
|
|
mSavePath = StringUtils::UTF8toUTF16("/3ds/Checkpoint/saves/") + StringUtils::UTF8toUTF16(unique) + mShortDescription;
|
|
|
|
mExtdataPath = StringUtils::UTF8toUTF16("/3ds/Checkpoint/extdata/") + StringUtils::UTF8toUTF16(unique) + mShortDescription;
|
|
|
|
AM_GetTitleProductCode(mMedia, mId, productCode);
|
2018-05-27 20:59:33 +00:00
|
|
|
|
2018-05-14 06:52:41 +00:00
|
|
|
mAccessibleSave = Archive::accessible(mediaType(), lowId(), highId());
|
|
|
|
mAccessibleExtdata = Archive::accessible(extdataId());
|
|
|
|
|
|
|
|
if (mAccessibleSave)
|
|
|
|
{
|
|
|
|
loadTitle = true;
|
|
|
|
if (!io::directoryExists(Archive::sdmc(), mSavePath))
|
|
|
|
{
|
|
|
|
Result res = io::createDirectory(Archive::sdmc(), mSavePath);
|
|
|
|
if (R_FAILED(res))
|
|
|
|
{
|
|
|
|
Gui::createError(res, "Failed to create backup directory.");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (mAccessibleExtdata)
|
|
|
|
{
|
|
|
|
loadTitle = true;
|
|
|
|
if (!io::directoryExists(Archive::sdmc(), mExtdataPath))
|
|
|
|
{
|
|
|
|
Result res = io::createDirectory(Archive::sdmc(), mExtdataPath);
|
|
|
|
if (R_FAILED(res))
|
|
|
|
{
|
|
|
|
Gui::createError(res, "Failed to create backup directory.");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (loadTitle)
|
|
|
|
{
|
|
|
|
mIcon = loadTextureIcon(smdh);
|
|
|
|
}
|
|
|
|
|
|
|
|
delete smdh;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
u8* headerData = new u8[0x3B4];
|
|
|
|
Result res = FSUSER_GetLegacyRomHeader(mMedia, 0LL, headerData);
|
|
|
|
if (R_FAILED(res))
|
|
|
|
{
|
|
|
|
delete[] headerData;
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
char _cardTitle[14] = {0};
|
|
|
|
char _gameCode[6] = {0};
|
|
|
|
|
|
|
|
std::copy(headerData, headerData + 12, _cardTitle);
|
|
|
|
std::copy(headerData + 12, headerData + 16, _gameCode);
|
|
|
|
_cardTitle[13] = '\0';
|
|
|
|
_gameCode[5] = '\0';
|
|
|
|
|
|
|
|
res = SPIGetCardType(&mCardType, (_gameCode[0] == 'I') ? 1 : 0);
|
|
|
|
if (R_FAILED(res))
|
|
|
|
{
|
|
|
|
delete[] headerData;
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
delete[] headerData;
|
|
|
|
|
|
|
|
mShortDescription = StringUtils::removeForbiddenCharacters(StringUtils::UTF8toUTF16(_cardTitle));
|
|
|
|
mLongDescription = mShortDescription;
|
|
|
|
mSavePath = StringUtils::UTF8toUTF16("/3ds/Checkpoint/saves/") + StringUtils::UTF8toUTF16(_gameCode) + StringUtils::UTF8toUTF16(" ") + mShortDescription;
|
|
|
|
mExtdataPath = mSavePath;
|
|
|
|
memset(productCode, 0, 16);
|
|
|
|
|
|
|
|
mAccessibleSave = true;
|
|
|
|
mAccessibleExtdata = false;
|
|
|
|
|
|
|
|
loadTitle = true;
|
|
|
|
if (!io::directoryExists(Archive::sdmc(), mSavePath))
|
|
|
|
{
|
|
|
|
Result res = io::createDirectory(Archive::sdmc(), mSavePath);
|
|
|
|
if (R_FAILED(res))
|
|
|
|
{
|
|
|
|
Gui::createError(res, "Failed to create backup directory.");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
mIcon = Gui::TWLIcon();
|
|
|
|
}
|
|
|
|
|
|
|
|
refreshDirectories();
|
|
|
|
return loadTitle;
|
2017-09-30 10:51:15 +00:00
|
|
|
}
|
|
|
|
|
2018-05-14 06:52:41 +00:00
|
|
|
bool Title::accessibleSave(void)
|
2017-09-30 10:51:15 +00:00
|
|
|
{
|
2018-05-14 06:52:41 +00:00
|
|
|
return mAccessibleSave;
|
2017-09-30 10:51:15 +00:00
|
|
|
}
|
|
|
|
|
2018-05-14 06:52:41 +00:00
|
|
|
bool Title::accessibleExtdata(void)
|
2017-09-30 10:51:15 +00:00
|
|
|
{
|
2018-05-14 06:52:41 +00:00
|
|
|
return mAccessibleExtdata;
|
2017-09-29 07:45:56 +00:00
|
|
|
}
|
|
|
|
|
2018-05-14 06:52:41 +00:00
|
|
|
std::string Title::mediaTypeString(void)
|
2017-09-29 07:45:56 +00:00
|
|
|
{
|
2018-05-14 06:52:41 +00:00
|
|
|
switch(mMedia)
|
|
|
|
{
|
|
|
|
case MEDIATYPE_SD: return "SD Card";
|
|
|
|
case MEDIATYPE_GAME_CARD: return "Cartridge";
|
|
|
|
case MEDIATYPE_NAND: return "NAND";
|
|
|
|
default: return " ";
|
|
|
|
}
|
|
|
|
|
|
|
|
return " ";
|
2017-09-29 07:45:56 +00:00
|
|
|
}
|
|
|
|
|
2018-05-14 06:52:41 +00:00
|
|
|
std::string Title::shortDescription(void)
|
2017-09-29 07:45:56 +00:00
|
|
|
{
|
2018-05-14 06:52:41 +00:00
|
|
|
return StringUtils::UTF16toUTF8(mShortDescription);
|
2017-09-29 07:45:56 +00:00
|
|
|
}
|
|
|
|
|
2018-05-14 06:52:41 +00:00
|
|
|
std::string Title::longDescription(void)
|
2017-09-29 07:45:56 +00:00
|
|
|
{
|
2018-05-14 06:52:41 +00:00
|
|
|
return StringUtils::UTF16toUTF8(mLongDescription);
|
2017-09-29 07:45:56 +00:00
|
|
|
}
|
|
|
|
|
2018-05-14 06:52:41 +00:00
|
|
|
std::u16string Title::savePath(void)
|
2017-09-29 07:45:56 +00:00
|
|
|
{
|
2018-05-14 06:52:41 +00:00
|
|
|
return mSavePath;
|
2017-09-29 07:45:56 +00:00
|
|
|
}
|
|
|
|
|
2018-05-14 06:52:41 +00:00
|
|
|
std::u16string Title::extdataPath(void)
|
2017-09-29 20:58:55 +00:00
|
|
|
{
|
2018-05-14 06:52:41 +00:00
|
|
|
return mExtdataPath;
|
2017-09-29 20:58:55 +00:00
|
|
|
}
|
|
|
|
|
2018-05-28 18:24:11 +00:00
|
|
|
std::u16string Title::fullSavePath(size_t index)
|
|
|
|
{
|
|
|
|
return mFullSavePaths.at(index);
|
|
|
|
}
|
|
|
|
|
|
|
|
std::u16string Title::fullExtdataPath(size_t index)
|
|
|
|
{
|
|
|
|
return mFullExtdataPaths.at(index);
|
|
|
|
}
|
|
|
|
|
2018-05-14 06:52:41 +00:00
|
|
|
std::vector<std::u16string> Title::saves(void)
|
2017-09-29 07:45:56 +00:00
|
|
|
{
|
2018-05-14 06:52:41 +00:00
|
|
|
return mSaves;
|
2017-09-29 07:45:56 +00:00
|
|
|
}
|
|
|
|
|
2018-05-14 06:52:41 +00:00
|
|
|
std::vector<std::u16string> Title::extdata(void)
|
2017-09-29 20:58:55 +00:00
|
|
|
{
|
2018-05-14 06:52:41 +00:00
|
|
|
return mExtdata;
|
2017-09-29 20:58:55 +00:00
|
|
|
}
|
|
|
|
|
2017-09-29 07:45:56 +00:00
|
|
|
void Title::refreshDirectories(void)
|
|
|
|
{
|
2018-05-14 06:52:41 +00:00
|
|
|
mSaves.clear();
|
|
|
|
mExtdata.clear();
|
|
|
|
|
|
|
|
if (accessibleSave())
|
|
|
|
{
|
2018-05-28 18:24:11 +00:00
|
|
|
// standard save backups
|
2018-05-14 06:52:41 +00:00
|
|
|
Directory savelist(Archive::sdmc(), mSavePath);
|
|
|
|
if (savelist.good())
|
|
|
|
{
|
|
|
|
for (size_t i = 0, sz = savelist.size(); i < sz; i++)
|
|
|
|
{
|
|
|
|
if (savelist.folder(i))
|
|
|
|
{
|
|
|
|
mSaves.push_back(savelist.entry(i));
|
2018-05-28 18:24:11 +00:00
|
|
|
mFullSavePaths.push_back(mSavePath + StringUtils::UTF8toUTF16("/") + savelist.entry(i));
|
2018-05-14 06:52:41 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
std::sort(mSaves.rbegin(), mSaves.rend());
|
|
|
|
mSaves.insert(mSaves.begin(), StringUtils::UTF8toUTF16("New..."));
|
2018-05-28 18:24:11 +00:00
|
|
|
mFullSavePaths.insert(mFullSavePaths.begin(), StringUtils::UTF8toUTF16("New..."));
|
2018-05-14 06:52:41 +00:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
Gui::createError(savelist.error(), "Couldn't retrieve the directory list for the title " + shortDescription());
|
|
|
|
}
|
2018-05-28 18:24:11 +00:00
|
|
|
|
|
|
|
// save backups from configuration
|
|
|
|
std::vector<std::u16string> additionalFolders = Configuration::getInstance().additionalSaveFolders(mId);
|
|
|
|
for (std::vector<std::u16string>::const_iterator it = additionalFolders.begin(); it != additionalFolders.end(); it++)
|
|
|
|
{
|
|
|
|
// we have other folders to parse
|
|
|
|
Directory list(Archive::sdmc(), *it);
|
|
|
|
if (list.good())
|
|
|
|
{
|
|
|
|
for (size_t i = 0, sz = list.size(); i < sz; i++)
|
|
|
|
{
|
|
|
|
if (list.folder(i))
|
|
|
|
{
|
|
|
|
mSaves.push_back(list.entry(i));
|
|
|
|
mFullSavePaths.push_back(mSavePath + StringUtils::UTF8toUTF16("/") + list.entry(i));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2018-05-14 06:52:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (accessibleExtdata())
|
|
|
|
{
|
|
|
|
// extdata backups
|
|
|
|
Directory extlist(Archive::sdmc(), mExtdataPath);
|
|
|
|
if (extlist.good())
|
|
|
|
{
|
|
|
|
for (size_t i = 0, sz = extlist.size(); i < sz; i++)
|
|
|
|
{
|
|
|
|
if (extlist.folder(i))
|
|
|
|
{
|
|
|
|
mExtdata.push_back(extlist.entry(i));
|
2018-05-28 18:24:11 +00:00
|
|
|
mFullExtdataPaths.push_back(mExtdataPath + StringUtils::UTF8toUTF16("/") + extlist.entry(i));
|
2018-05-14 06:52:41 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
std::sort(mExtdata.begin(), mExtdata.end());
|
|
|
|
mExtdata.insert(mExtdata.begin(), StringUtils::UTF8toUTF16("New..."));
|
2018-05-28 18:24:11 +00:00
|
|
|
mFullExtdataPaths.insert(mFullExtdataPaths.begin(), StringUtils::UTF8toUTF16("New..."));
|
2018-05-14 06:52:41 +00:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
Gui::createError(extlist.error(), "Couldn't retrieve the extdata list for the title " + shortDescription());
|
|
|
|
}
|
2018-05-28 18:24:11 +00:00
|
|
|
|
|
|
|
// extdata backups from configuration
|
|
|
|
std::vector<std::u16string> additionalFolders = Configuration::getInstance().additionalExtdataFolders(mId);
|
|
|
|
for (std::vector<std::u16string>::const_iterator it = additionalFolders.begin(); it != additionalFolders.end(); it++)
|
|
|
|
{
|
|
|
|
// we have other folders to parse
|
|
|
|
Directory list(Archive::sdmc(), *it);
|
|
|
|
if (list.good())
|
|
|
|
{
|
|
|
|
for (size_t i = 0, sz = list.size(); i < sz; i++)
|
|
|
|
{
|
|
|
|
if (list.folder(i))
|
|
|
|
{
|
|
|
|
mExtdata.push_back(list.entry(i));
|
|
|
|
mFullExtdataPaths.push_back(mExtdataPath + StringUtils::UTF8toUTF16("/") + list.entry(i));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2018-05-14 06:52:41 +00:00
|
|
|
}
|
2017-09-29 07:45:56 +00:00
|
|
|
}
|
|
|
|
|
2018-05-14 06:52:41 +00:00
|
|
|
u32 Title::highId(void)
|
2017-09-29 07:45:56 +00:00
|
|
|
{
|
2018-05-14 06:52:41 +00:00
|
|
|
return (u32)(mId >> 32);
|
2017-09-29 07:45:56 +00:00
|
|
|
}
|
|
|
|
|
2018-05-14 06:52:41 +00:00
|
|
|
u32 Title::lowId(void)
|
2017-09-29 07:45:56 +00:00
|
|
|
{
|
2018-05-14 06:52:41 +00:00
|
|
|
return (u32)mId;
|
2017-09-29 07:45:56 +00:00
|
|
|
}
|
|
|
|
|
2018-05-14 06:52:41 +00:00
|
|
|
u32 Title::uniqueId(void)
|
2017-09-29 07:45:56 +00:00
|
|
|
{
|
2018-05-14 06:52:41 +00:00
|
|
|
return (lowId() >> 8);
|
2017-09-29 07:45:56 +00:00
|
|
|
}
|
|
|
|
|
2018-05-14 06:52:41 +00:00
|
|
|
u64 Title::id(void)
|
2017-09-29 07:45:56 +00:00
|
|
|
{
|
2018-05-14 06:52:41 +00:00
|
|
|
return mId;
|
2017-09-29 07:45:56 +00:00
|
|
|
}
|
|
|
|
|
2018-05-14 06:52:41 +00:00
|
|
|
u32 Title::extdataId(void)
|
2017-09-29 20:58:55 +00:00
|
|
|
{
|
2018-05-14 06:52:41 +00:00
|
|
|
u32 low = lowId();
|
|
|
|
switch(low)
|
|
|
|
{
|
|
|
|
case 0x00055E00: return 0x055D; // Pokémon Y
|
|
|
|
case 0x0011C400: return 0x11C5; // Pokémon Omega Ruby
|
|
|
|
case 0x00175E00: return 0x1648; // Pokémon Moon
|
|
|
|
case 0x00179600:
|
|
|
|
case 0x00179800: return 0x1794; // Fire Emblem Conquest SE NA
|
|
|
|
case 0x00179700:
|
|
|
|
case 0x0017A800: return 0x1795; // Fire Emblem Conquest SE EU
|
|
|
|
case 0x0012DD00:
|
|
|
|
case 0x0012DE00: return 0x12DC; // Fire Emblem If JP
|
|
|
|
case 0x001B5100: return 0x1B50; // Pokémon Ultramoon
|
|
|
|
}
|
|
|
|
|
|
|
|
return low >> 8;
|
2017-09-29 20:58:55 +00:00
|
|
|
}
|
|
|
|
|
2018-05-14 06:52:41 +00:00
|
|
|
FS_MediaType Title::mediaType(void)
|
2017-09-29 07:45:56 +00:00
|
|
|
{
|
2018-05-14 06:52:41 +00:00
|
|
|
return mMedia;
|
2017-09-29 07:45:56 +00:00
|
|
|
}
|
|
|
|
|
2018-05-14 06:52:41 +00:00
|
|
|
FS_CardType Title::cardType(void)
|
2017-10-04 11:18:41 +00:00
|
|
|
{
|
2018-05-14 06:52:41 +00:00
|
|
|
return mCard;
|
2017-10-04 11:18:41 +00:00
|
|
|
}
|
|
|
|
|
2018-05-14 06:52:41 +00:00
|
|
|
CardType Title::SPICardType(void)
|
2017-10-04 11:18:41 +00:00
|
|
|
{
|
2018-05-14 06:52:41 +00:00
|
|
|
return mCardType;
|
2017-10-04 11:18:41 +00:00
|
|
|
}
|
|
|
|
|
2018-05-14 06:52:41 +00:00
|
|
|
C2D_Image Title::icon(void)
|
2017-09-29 07:45:56 +00:00
|
|
|
{
|
2018-05-14 06:52:41 +00:00
|
|
|
return mIcon;
|
2017-09-29 07:45:56 +00:00
|
|
|
}
|
|
|
|
|
2018-05-26 20:25:48 +00:00
|
|
|
static bool validId(u64 id)
|
2017-09-29 07:45:56 +00:00
|
|
|
{
|
2018-05-27 20:59:33 +00:00
|
|
|
// check for invalid titles
|
|
|
|
switch ((u32)id)
|
|
|
|
{
|
|
|
|
// Instruction Manual
|
|
|
|
case 0x00008602:
|
|
|
|
case 0x00009202:
|
|
|
|
case 0x00009B02:
|
|
|
|
case 0x0000A402:
|
|
|
|
case 0x0000AC02:
|
|
|
|
case 0x0000B402:
|
|
|
|
// Internet Browser
|
|
|
|
case 0x00008802:
|
|
|
|
case 0x00009402:
|
|
|
|
case 0x00009D02:
|
|
|
|
case 0x0000A602:
|
|
|
|
case 0x0000AE02:
|
|
|
|
case 0x0000B602:
|
|
|
|
case 0x20008802:
|
|
|
|
case 0x20009402:
|
|
|
|
case 0x20009D02:
|
|
|
|
case 0x2000AE02:
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// check for updates
|
2018-05-14 06:52:41 +00:00
|
|
|
u32 high = id >> 32;
|
2018-05-27 20:59:33 +00:00
|
|
|
if (high == 0x0004000E)
|
|
|
|
{
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return !Configuration::getInstance().filter(id);
|
2017-09-29 07:45:56 +00:00
|
|
|
}
|
|
|
|
|
2017-12-25 18:10:41 +00:00
|
|
|
void loadTitles(bool forceRefresh)
|
2017-09-29 07:45:56 +00:00
|
|
|
{
|
2018-05-14 06:52:41 +00:00
|
|
|
std::u16string savecachePath = StringUtils::UTF8toUTF16("/3ds/Checkpoint/savecache");
|
|
|
|
std::u16string extdatacachePath = StringUtils::UTF8toUTF16("/3ds/Checkpoint/extdatacache");
|
|
|
|
|
|
|
|
// on refreshing
|
|
|
|
titleSaves.clear();
|
|
|
|
titleExtdatas.clear();
|
|
|
|
|
|
|
|
bool optimizedLoad = false;
|
|
|
|
|
|
|
|
u8 hash[SHA256_BLOCK_SIZE];
|
|
|
|
calculateTitleDBHash(hash);
|
|
|
|
|
|
|
|
std::u16string titlesHashPath = StringUtils::UTF8toUTF16("/3ds/Checkpoint/titles.sha");
|
|
|
|
if (!io::fileExists(Archive::sdmc(), titlesHashPath) || !io::fileExists(Archive::sdmc(), savecachePath) || !io::fileExists(Archive::sdmc(), extdatacachePath))
|
|
|
|
{
|
|
|
|
// create title list sha256 hash file if it doesn't exist in the working directory
|
|
|
|
FSStream output(Archive::sdmc(), titlesHashPath, FS_OPEN_WRITE, SHA256_BLOCK_SIZE);
|
|
|
|
output.write(hash, SHA256_BLOCK_SIZE);
|
|
|
|
output.close();
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
// compare current hash with the previous hash
|
|
|
|
FSStream input(Archive::sdmc(), titlesHashPath, FS_OPEN_READ);
|
|
|
|
if (input.good() && input.size() == SHA256_BLOCK_SIZE)
|
|
|
|
{
|
|
|
|
u8* buf = new u8[input.size()];
|
|
|
|
input.read(buf, input.size());
|
|
|
|
input.close();
|
|
|
|
|
|
|
|
if (memcmp(hash, buf, SHA256_BLOCK_SIZE) == 0)
|
|
|
|
{
|
|
|
|
// hash matches
|
|
|
|
optimizedLoad = true;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
FSUSER_DeleteFile(Archive::sdmc(), fsMakePath(PATH_UTF16, titlesHashPath.data()));
|
|
|
|
FSStream output(Archive::sdmc(), titlesHashPath, FS_OPEN_WRITE, SHA256_BLOCK_SIZE);
|
|
|
|
output.write(hash, SHA256_BLOCK_SIZE);
|
|
|
|
output.close();
|
|
|
|
}
|
|
|
|
|
|
|
|
delete[] buf;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (optimizedLoad && !forceRefresh)
|
|
|
|
{
|
|
|
|
// deserialize data
|
|
|
|
importTitleListCache();
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
u32 count = 0;
|
|
|
|
|
2018-05-27 20:59:33 +00:00
|
|
|
if (Configuration::getInstance().nandSaves())
|
|
|
|
{
|
|
|
|
AM_GetTitleCount(MEDIATYPE_NAND, &count);
|
|
|
|
u64 ids_nand[count];
|
|
|
|
AM_GetTitleList(NULL, MEDIATYPE_NAND, count, ids_nand);
|
|
|
|
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
// TODO: extdata?
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
count = 0;
|
|
|
|
AM_GetTitleCount(MEDIATYPE_SD, &count);
|
2018-05-14 06:52:41 +00:00
|
|
|
u64 ids[count];
|
|
|
|
AM_GetTitleList(NULL, MEDIATYPE_SD, count, ids);
|
|
|
|
|
|
|
|
for (u32 i = 0; i < count; i++)
|
|
|
|
{
|
2018-05-26 20:25:48 +00:00
|
|
|
if (validId(ids[i]))
|
2018-05-14 06:52:41 +00:00
|
|
|
{
|
|
|
|
Title title;
|
|
|
|
if (title.load(ids[i], MEDIATYPE_SD, CARD_CTR))
|
|
|
|
{
|
|
|
|
if (title.accessibleSave())
|
|
|
|
{
|
|
|
|
titleSaves.push_back(title);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (title.accessibleExtdata())
|
|
|
|
{
|
|
|
|
titleExtdatas.push_back(title);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
std::sort(titleSaves.begin(), titleSaves.end(), [](Title& l, Title& r) {
|
|
|
|
return l.shortDescription() < r.shortDescription();
|
|
|
|
});
|
|
|
|
|
|
|
|
std::sort(titleExtdatas.begin(), titleExtdatas.end(), [](Title& l, Title& r) {
|
|
|
|
return l.shortDescription() < r.shortDescription();
|
|
|
|
});
|
|
|
|
|
|
|
|
FS_CardType cardType;
|
|
|
|
Result res = FSUSER_GetCardType(&cardType);
|
|
|
|
if (R_SUCCEEDED(res))
|
|
|
|
{
|
|
|
|
if (cardType == CARD_CTR)
|
|
|
|
{
|
|
|
|
AM_GetTitleCount(MEDIATYPE_GAME_CARD, &count);
|
|
|
|
if (count > 0)
|
|
|
|
{
|
|
|
|
AM_GetTitleList(NULL, MEDIATYPE_GAME_CARD, count, ids);
|
2018-05-26 20:25:48 +00:00
|
|
|
if (validId(ids[0]))
|
2018-05-14 06:52:41 +00:00
|
|
|
{
|
|
|
|
Title title;
|
|
|
|
if (title.load(ids[0], MEDIATYPE_GAME_CARD, cardType))
|
|
|
|
{
|
|
|
|
if (title.accessibleSave())
|
|
|
|
{
|
|
|
|
titleSaves.insert(titleSaves.begin(), title);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (title.accessibleExtdata())
|
|
|
|
{
|
|
|
|
titleExtdatas.insert(titleExtdatas.begin(), title);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
Title title;
|
|
|
|
if (title.load(0, MEDIATYPE_GAME_CARD, cardType))
|
|
|
|
{
|
|
|
|
titleSaves.insert(titleSaves.begin(), title);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// serialize data
|
|
|
|
exportTitleListCache(titleSaves, savecachePath);
|
|
|
|
exportTitleListCache(titleExtdatas, extdatacachePath);
|
2017-09-29 07:45:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void getTitle(Title &dst, int i)
|
|
|
|
{
|
2018-05-14 06:52:41 +00:00
|
|
|
const Mode_t mode = Archive::mode();
|
|
|
|
if (i < getTitleCount())
|
|
|
|
{
|
|
|
|
dst = mode == MODE_SAVE ? titleSaves.at(i) : titleExtdatas.at(i);
|
|
|
|
}
|
2017-09-29 07:45:56 +00:00
|
|
|
}
|
|
|
|
|
2018-05-14 06:52:41 +00:00
|
|
|
int getTitleCount(void)
|
2017-09-29 07:45:56 +00:00
|
|
|
{
|
2018-05-14 06:52:41 +00:00
|
|
|
const Mode_t mode = Archive::mode();
|
|
|
|
return mode == MODE_SAVE ? titleSaves.size() : titleExtdatas.size();
|
2017-09-29 07:45:56 +00:00
|
|
|
}
|
|
|
|
|
2018-05-14 06:52:41 +00:00
|
|
|
C2D_Image icon(int i)
|
2017-09-29 07:45:56 +00:00
|
|
|
{
|
2018-05-14 06:52:41 +00:00
|
|
|
const Mode_t mode = Archive::mode();
|
|
|
|
return mode == MODE_SAVE ? titleSaves.at(i).icon() : titleExtdatas.at(i).icon();
|
2017-09-29 07:45:56 +00:00
|
|
|
}
|
|
|
|
|
2018-05-14 06:52:41 +00:00
|
|
|
static C2D_Image loadTextureIcon(smdh_s *smdh)
|
|
|
|
{
|
|
|
|
C3D_Tex* tex = (C3D_Tex*)malloc(sizeof(C3D_Tex));
|
|
|
|
static const Tex3DS_SubTexture subt3x = { 48, 48, 0.0f, 48/64.0f, 48/64.0f, 0.0f };
|
|
|
|
C2D_Image image = (C2D_Image){ tex, &subt3x };
|
|
|
|
C3D_TexInit(image.tex, 64, 64, GPU_RGB565);
|
|
|
|
|
|
|
|
u16* dest = (u16*)image.tex->data + (64-48)*64;
|
|
|
|
u16* src = (u16*)smdh->bigIconData;
|
|
|
|
for (int j = 0; j < 48; j += 8)
|
|
|
|
{
|
|
|
|
memcpy(dest, src, 48*8*sizeof(u16));
|
|
|
|
src += 48*8;
|
|
|
|
dest += 64*8;
|
|
|
|
}
|
|
|
|
|
|
|
|
return image;
|
2017-09-29 07:45:56 +00:00
|
|
|
}
|
|
|
|
|
2017-12-23 13:06:15 +00:00
|
|
|
void refreshDirectories(u64 id)
|
2017-09-29 07:45:56 +00:00
|
|
|
{
|
2018-05-14 06:52:41 +00:00
|
|
|
const Mode_t mode = Archive::mode();
|
|
|
|
if (mode == MODE_SAVE)
|
|
|
|
{
|
|
|
|
for (size_t i = 0; i < titleSaves.size(); i++)
|
|
|
|
{
|
|
|
|
if (titleSaves.at(i).id() == id)
|
|
|
|
{
|
|
|
|
titleSaves.at(i).refreshDirectories();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
for (size_t i = 0; i < titleExtdatas.size(); i++)
|
|
|
|
{
|
|
|
|
if (titleExtdatas.at(i).id() == id)
|
|
|
|
{
|
|
|
|
titleExtdatas.at(i).refreshDirectories();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2017-12-25 18:10:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
static void exportTitleListCache(std::vector<Title> list, const std::u16string path)
|
|
|
|
{
|
2018-05-14 06:52:41 +00:00
|
|
|
u8* cache = new u8[list.size() * 10];
|
|
|
|
for (size_t i = 0; i < list.size(); i++)
|
|
|
|
{
|
|
|
|
u64 id = list.at(i).id();
|
|
|
|
FS_MediaType media = list.at(i).mediaType();
|
|
|
|
FS_CardType card = list.at(i).cardType();
|
|
|
|
memcpy(cache + i*10 + 0, &id, sizeof(u64));
|
|
|
|
memcpy(cache + i*10 + 8, &media, sizeof(u8));
|
|
|
|
memcpy(cache + i*10 + 9, &card, sizeof(u8));
|
|
|
|
}
|
|
|
|
FSUSER_DeleteFile(Archive::sdmc(), fsMakePath(PATH_UTF16, path.data()));
|
|
|
|
FSStream output(Archive::sdmc(), path, FS_OPEN_WRITE, list.size() * 10);
|
|
|
|
output.write(cache, list.size() * 10);
|
|
|
|
output.close();
|
|
|
|
delete[] cache;
|
2017-12-25 18:10:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
static void importTitleListCache(void)
|
|
|
|
{
|
2018-05-14 06:52:41 +00:00
|
|
|
FSStream inputsaves(Archive::sdmc(), StringUtils::UTF8toUTF16("/3ds/Checkpoint/savecache"), FS_OPEN_READ);
|
|
|
|
u32 sizesaves = inputsaves.size() / 10;
|
|
|
|
u8* cachesaves = new u8[inputsaves.size()];
|
|
|
|
inputsaves.read(cachesaves, inputsaves.size());
|
|
|
|
inputsaves.close();
|
|
|
|
|
|
|
|
FSStream inputextdatas(Archive::sdmc(), StringUtils::UTF8toUTF16("/3ds/Checkpoint/extdatacache"), FS_OPEN_READ);
|
|
|
|
u32 sizeextdatas = inputextdatas.size() / 10;
|
|
|
|
u8* cacheextdatas = new u8[inputextdatas.size()];
|
|
|
|
inputextdatas.read(cacheextdatas, inputextdatas.size());
|
|
|
|
inputextdatas.close();
|
|
|
|
|
|
|
|
// fill the lists with blank titles firsts
|
|
|
|
for (size_t i = 0, sz = std::max(sizesaves, sizeextdatas); i < sz; i++)
|
|
|
|
{
|
|
|
|
Title title;
|
|
|
|
title.load();
|
|
|
|
if (i < sizesaves)
|
|
|
|
{
|
|
|
|
titleSaves.push_back(title);
|
|
|
|
}
|
|
|
|
if (i < sizeextdatas)
|
|
|
|
{
|
|
|
|
titleExtdatas.push_back(title);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// store already loaded ids
|
|
|
|
std::vector<u64> alreadystored;
|
|
|
|
|
|
|
|
for (size_t i = 0; i < sizesaves; i++)
|
|
|
|
{
|
|
|
|
u64 id;
|
|
|
|
FS_MediaType media;
|
|
|
|
FS_CardType card;
|
|
|
|
memcpy(&id, cachesaves + i*10, sizeof(u64));
|
|
|
|
memcpy(&media, cachesaves + i*10 + 8, sizeof(u8));
|
|
|
|
memcpy(&card, cachesaves + i*10 + 9, sizeof(u8));
|
|
|
|
Title title;
|
|
|
|
title.load(id, media, card);
|
|
|
|
titleSaves.at(i) = title;
|
|
|
|
alreadystored.push_back(id);
|
|
|
|
}
|
|
|
|
|
|
|
|
for (size_t i = 0; i < sizeextdatas; i++)
|
|
|
|
{
|
|
|
|
u64 id;
|
|
|
|
memcpy(&id, cacheextdatas + i*10, sizeof(u64));
|
|
|
|
std::vector<u64>::iterator it = find(alreadystored.begin(), alreadystored.end(), id);
|
|
|
|
if (it == alreadystored.end())
|
|
|
|
{
|
|
|
|
FS_MediaType media;
|
|
|
|
FS_CardType card;
|
|
|
|
memcpy(&media, cacheextdatas + i*10 + 8, sizeof(u8));
|
|
|
|
memcpy(&card, cacheextdatas + i*10 + 9, sizeof(u8));
|
|
|
|
Title title;
|
|
|
|
title.load(id, media, card);
|
|
|
|
titleExtdatas.at(i) = title;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
auto pos = it - alreadystored.begin();
|
|
|
|
|
|
|
|
// avoid to copy a cartridge title into the extdata list twice
|
|
|
|
if (i != 0 && pos == 0)
|
|
|
|
{
|
|
|
|
auto newpos = find(alreadystored.rbegin(), alreadystored.rend(), id);
|
|
|
|
titleExtdatas.at(i) = titleSaves.at(alreadystored.rend() - newpos);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
titleExtdatas.at(i) = titleSaves.at(pos);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
delete[] cachesaves;
|
|
|
|
delete[] cacheextdatas;
|
2017-09-30 10:51:15 +00:00
|
|
|
}
|