another stage of refactoring
CI / Build NRO (push) Has been cancelled
CI / Format check (push) Has been cancelled
CI / Layering check (push) Has been cancelled

This commit is contained in:
2026-05-12 09:59:43 +03:00
parent 6f8ede035f
commit d410c4355d
45 changed files with 1958 additions and 2639 deletions
+169 -225
View File
@@ -1,260 +1,206 @@
/*
* This file is part of Checkpoint
* Copyright (C) 2017-2021 Bernardo Giordano, FlagBrew
*
* 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.
*/
// Copyright (C) 2024-2026 NXST contributors
#include <algorithm>
#include <cstring>
#include <vector>
#include <nxst/app/main.hpp>
#include "nxst/domain/account.hpp"
#include <nxst/domain/util.hpp>
#include <nxst/domain/title.hpp>
#include <nxst/domain/util.hpp>
#include <nxst/infra/fs/directory.hpp>
#include <nxst/infra/sys/logger.hpp>
using sort_t = enum { SortAlpha, SortLastPlayed, SortPlayTime, SortModesCount };
static constexpr const char* kEmptySave = "New...";
static sort_t s_sort_mode = SortAlpha;
static std::unordered_map<AccountUid, std::vector<Title>> titles;
static bool s_titlesLoaded = false;
static bool s_titles_loaded = false;
bool areTitlesLoaded(void) {
return s_titlesLoaded;
}
void Title::init(u8 saveDataType, u64 id, AccountUid userID, const std::string& name,
void Title::init(u8 save_data_type, u64 title_id, AccountUid uid, const std::string& name,
const std::string& author) {
mId = id;
mUserId = userID;
mSaveDataType = saveDataType;
mUserName = Account::username(userID);
mAuthor = author;
mName = name;
mSafeName = StringUtils::containsInvalidChar(name) ? StringUtils::format("0x%016llX", mId)
: StringUtils::removeForbiddenCharacters(name);
mPath = "sdmc:/switch/NXST/saves/" + StringUtils::format("0x%016llX", mId) + " " + mSafeName;
m_id = title_id;
m_uid = uid;
m_save_data_type = save_data_type;
m_user_name = Account::username(uid);
m_author = author;
m_name = name;
m_safe_name = StringUtils::containsInvalidChar(name) ? StringUtils::format("0x%016llX", m_id)
: StringUtils::removeForbiddenCharacters(name);
m_path = "sdmc:/switch/NXST/saves/" + StringUtils::format("0x%016llX", m_id) + " " + m_safe_name;
std::string aname = StringUtils::removeAccents(mName);
size_t pos = aname.rfind(":");
mDisplayName = std::make_pair(aname, "");
if (pos != std::string::npos) {
std::string name1 = aname.substr(0, pos);
std::string name2 = aname.substr(pos + 1);
StringUtils::trim(name1);
StringUtils::trim(name2);
mDisplayName.first = name1;
mDisplayName.second = name2;
std::string aname = StringUtils::removeAccents(m_name);
m_display_name = {aname, ""};
size_t colon = aname.rfind(':');
if (colon != std::string::npos) {
std::string head = aname.substr(0, colon);
std::string tail = aname.substr(colon + 1);
StringUtils::trim(head);
StringUtils::trim(tail);
m_display_name = {head, tail};
} else {
// check for parenthesis
size_t pos1 = aname.rfind("(");
size_t pos2 = aname.rfind(")");
if (pos1 != std::string::npos && pos2 != std::string::npos) {
std::string name1 = aname.substr(0, pos1);
std::string name2 = aname.substr(pos1 + 1, pos2 - 1 - pos1);
StringUtils::trim(name1);
StringUtils::trim(name2);
mDisplayName.first = name1;
mDisplayName.second = name2;
size_t open = aname.rfind('(');
size_t close = aname.rfind(')');
if (open != std::string::npos && close != std::string::npos && close > open) {
std::string head = aname.substr(0, open);
std::string paren = aname.substr(open + 1, close - open - 1);
StringUtils::trim(head);
StringUtils::trim(paren);
m_display_name = {head, paren};
}
}
refreshDirectories();
}
u8 Title::saveDataType(void) {
return mSaveDataType;
u8 Title::saveDataType() const {
return m_save_data_type;
}
u64 Title::id() const {
return m_id;
}
u64 Title::saveId() const {
return m_save_id;
}
void Title::saveId(u64 id) {
m_save_id = id;
}
AccountUid Title::userId() const {
return m_uid;
}
std::string Title::userName() const {
return m_user_name;
}
std::string Title::author() const {
return m_author;
}
std::string Title::name() const {
return m_name;
}
std::pair<std::string, std::string> Title::displayName() const {
return m_display_name;
}
std::string Title::path() const {
return m_path;
}
std::string Title::fullPath(size_t index) const {
return m_full_save_paths.at(index);
}
std::vector<std::string> Title::saves() const {
return m_saves;
}
u64 Title::playTimeNanoseconds() const {
return m_play_time_ns;
}
void Title::playTimeNanoseconds(u64 ns) {
m_play_time_ns = ns;
}
u32 Title::lastPlayedTimestamp() const {
return m_last_played_ts;
}
void Title::lastPlayedTimestamp(u32 ts) {
m_last_played_ts = ts;
}
u64 Title::id(void) {
return mId;
std::string Title::playTime() const {
const u64 minutes = m_play_time_ns / 60000000000ULL;
return StringUtils::format("%d", minutes / 60) + ":" + StringUtils::format("%02d", minutes % 60) +
" hours";
}
u64 Title::saveId(void) {
return mSaveId;
}
void Title::refreshDirectories() {
m_saves.clear();
m_full_save_paths.clear();
void Title::saveId(u64 saveId) {
mSaveId = saveId;
}
AccountUid Title::userId(void) {
return mUserId;
}
std::string Title::userName(void) {
return mUserName;
}
std::string Title::author(void) {
return mAuthor;
}
std::string Title::name(void) {
return mName;
}
std::pair<std::string, std::string> Title::displayName(void) {
return mDisplayName;
}
std::string Title::path(void) {
return mPath;
}
std::string Title::fullPath(size_t index) {
return mFullSavePaths.at(index);
}
std::vector<std::string> Title::saves() {
return mSaves;
}
u64 Title::playTimeNanoseconds(void) {
return mPlayTimeNanoseconds;
}
std::string Title::playTime(void) {
const u64 playTimeMinutes = mPlayTimeNanoseconds / 60000000000;
return StringUtils::format("%d", playTimeMinutes / 60) + ":" +
StringUtils::format("%02d", playTimeMinutes % 60) + " hours";
}
void Title::playTimeNanoseconds(u64 playTimeNanoseconds) {
mPlayTimeNanoseconds = playTimeNanoseconds;
}
u32 Title::lastPlayedTimestamp(void) {
return mLastPlayedTimestamp;
}
void Title::lastPlayedTimestamp(u32 lastPlayedTimestamp) {
mLastPlayedTimestamp = lastPlayedTimestamp;
}
void Title::refreshDirectories(void) {
mSaves.clear();
mFullSavePaths.clear();
Directory savelist(mPath);
Directory savelist(m_path);
if (savelist.good()) {
for (size_t i = 0, sz = savelist.size(); i < sz; i++) {
for (size_t i = 0; i < savelist.size(); ++i) {
if (savelist.folder(i)) {
mSaves.push_back(savelist.entry(i));
mFullSavePaths.push_back(mPath + "/" + savelist.entry(i));
m_saves.push_back(savelist.entry(i));
m_full_save_paths.push_back(m_path + "/" + savelist.entry(i));
}
}
std::sort(mSaves.rbegin(), mSaves.rend());
std::sort(mFullSavePaths.rbegin(), mFullSavePaths.rend());
mSaves.insert(mSaves.begin(), g_emptySave);
mFullSavePaths.insert(mFullSavePaths.begin(), g_emptySave);
std::sort(m_saves.rbegin(), m_saves.rend());
std::sort(m_full_save_paths.rbegin(), m_full_save_paths.rend());
m_saves.insert(m_saves.begin(), kEmptySave);
m_full_save_paths.insert(m_full_save_paths.begin(), kEmptySave);
} else {
Logger::getInstance().log(Logger::ERROR,
"Couldn't retrieve the extdata directory list for the title " + name());
nxst::log::error("Could not read save directory for title %s", m_name.c_str());
}
}
void loadTitles(void) {
if (s_titlesLoaded)
return;
s_titlesLoaded = true;
bool areTitlesLoaded() {
return s_titles_loaded;
}
void loadTitles() {
if (s_titles_loaded)
return;
s_titles_loaded = true;
titles.clear();
FsSaveDataInfoReader reader;
FsSaveDataInfo info;
s64 total_entries = 0;
size_t outsize = 0;
NacpLanguageEntry* nle = NULL;
NsApplicationControlData* nsacd = (NsApplicationControlData*)malloc(sizeof(NsApplicationControlData));
if (nsacd == NULL) {
return;
}
memset(nsacd, 0, sizeof(NsApplicationControlData));
Result res = fsOpenSaveDataInfoReader(&reader, FsSaveDataSpaceId_User);
if (R_FAILED(res)) {
free(nsacd);
if (R_FAILED(res))
return;
}
while (1) {
res = fsSaveDataInfoReaderRead(&reader, &info, 1, &total_entries);
if (R_FAILED(res) || total_entries == 0) {
std::vector<u8> nacp_buf(sizeof(NsApplicationControlData), 0);
auto* nsacd = reinterpret_cast<NsApplicationControlData*>(nacp_buf.data());
FsSaveDataInfo info{};
s64 count = 0;
while (true) {
res = fsSaveDataInfoReaderRead(&reader, &info, 1, &count);
if (R_FAILED(res) || count == 0)
break;
if (info.save_data_type != FsSaveDataType_Account)
continue;
u64 tid = info.application_id;
AccountUid uid = info.uid;
size_t outsize = 0;
NacpLanguageEntry* nle = nullptr;
memset(nsacd, 0, sizeof(NsApplicationControlData));
res = nsGetApplicationControlData(NsApplicationControlSource_Storage, tid, nsacd,
sizeof(NsApplicationControlData), &outsize);
if (R_FAILED(res) || outsize < sizeof(nsacd->nacp))
continue;
if (R_FAILED(nacpGetLanguageEntry(&nsacd->nacp, &nle)) || !nle)
continue;
Title title;
title.init(info.save_data_type, tid, uid, std::string(nle->name), std::string(nle->author));
title.saveId(info.save_data_id);
PdmPlayStatistics stats{};
if (R_SUCCEEDED(pdmqryQueryPlayStatisticsByApplicationIdAndUserAccountId(tid, uid, false, &stats))) {
title.playTimeNanoseconds(stats.playtime);
title.lastPlayedTimestamp(stats.last_timestamp_user);
}
if (info.save_data_type == FsSaveDataType_Account) {
u64 tid = info.application_id;
u64 sid = info.save_data_id;
AccountUid uid = info.uid;
// if (mFilterIds.find(tid) == mFilterIds.end()) {
res = nsGetApplicationControlData(NsApplicationControlSource_Storage, tid, nsacd,
sizeof(NsApplicationControlData), &outsize);
if (R_SUCCEEDED(res) && !(outsize < sizeof(nsacd->nacp))) {
res = nacpGetLanguageEntry(&nsacd->nacp, &nle);
if (R_SUCCEEDED(res) && nle != NULL) {
Title title;
title.init(info.save_data_type, tid, uid, std::string(nle->name),
std::string(nle->author));
title.saveId(sid);
// load play statistics
PdmPlayStatistics stats;
res = pdmqryQueryPlayStatisticsByApplicationIdAndUserAccountId(tid, uid, false, &stats);
if (R_SUCCEEDED(res)) {
title.playTimeNanoseconds(stats.playtime);
title.lastPlayedTimestamp(stats.last_timestamp_user);
}
// loadIcon(tid, nsacd, outsize - sizeof(nsacd->nacp));
// check if the vector is already created
std::unordered_map<AccountUid, std::vector<Title>>::iterator it = titles.find(uid);
if (it != titles.end()) {
// found
it->second.push_back(title);
} else {
// not found, insert into map
std::vector<Title> v;
v.push_back(title);
titles.emplace(uid, v);
}
}
}
nle = NULL;
// }
auto it = titles.find(uid);
if (it != titles.end()) {
it->second.push_back(title);
} else {
titles.emplace(uid, std::vector<Title>{title});
}
}
free(nsacd);
fsSaveDataInfoReaderClose(&reader);
sortTitles();
}
void sortTitles(void) {
for (auto& vect : titles) {
std::sort(vect.second.begin(), vect.second.end(), [](Title& l, Title& r) {
switch (g_sortMode) {
case SORT_LAST_PLAYED:
void sortTitles() {
for (auto& pair : titles) {
std::sort(pair.second.begin(), pair.second.end(), [](const Title& l, const Title& r) {
switch (s_sort_mode) {
case SortLastPlayed:
return l.lastPlayedTimestamp() > r.lastPlayedTimestamp();
case SORT_PLAY_TIME:
case SortPlayTime:
return l.playTimeNanoseconds() > r.playTimeNanoseconds();
case SORT_ALPHA:
case SortAlpha:
default:
return l.name() < r.name();
}
@@ -262,38 +208,36 @@ void sortTitles(void) {
}
}
void rotateSortMode(void) {
g_sortMode = static_cast<sort_t>((g_sortMode + 1) % SORT_MODES_COUNT);
void rotateSortMode() {
s_sort_mode = static_cast<sort_t>((s_sort_mode + 1) % SortModesCount);
sortTitles();
}
void getTitle(Title& dst, AccountUid uid, size_t i) {
std::unordered_map<AccountUid, std::vector<Title>>::iterator it = titles.find(uid);
if (it != titles.end() && i < getTitleCount(uid)) {
dst = it->second.at(i);
}
auto it = titles.find(uid);
if (it != titles.end() && i < it->second.size())
dst = it->second[i];
}
size_t getTitleCount(AccountUid uid) {
std::unordered_map<AccountUid, std::vector<Title>>::iterator it = titles.find(uid);
auto it = titles.find(uid);
return it != titles.end() ? it->second.size() : 0;
}
void refreshDirectories(u64 id) {
for (auto& pair : titles) {
for (size_t i = 0; i < pair.second.size(); i++) {
if (pair.second.at(i).id() == id) {
pair.second.at(i).refreshDirectories();
}
for (auto& title : pair.second) {
if (title.id() == id)
title.refreshDirectories();
}
}
}
std::unordered_map<std::string, std::string> getCompleteTitleList(void) {
std::unordered_map<std::string, std::string> getCompleteTitleList() {
std::unordered_map<std::string, std::string> map;
for (const auto& pair : titles) {
for (auto value : pair.second) {
map.insert({StringUtils::format("0x%016llX", value.id()), value.name()});
for (const auto& title : pair.second) {
map.emplace(StringUtils::format("0x%016llX", title.id()), title.name());
}
}
return map;