245 lines
7.4 KiB
C++
245 lines
7.4 KiB
C++
// Copyright (C) 2024-2026 NXST contributors
|
|
#include <algorithm>
|
|
#include <cstring>
|
|
#include <vector>
|
|
|
|
#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_titles_loaded = false;
|
|
|
|
void Title::init(u8 save_data_type, u64 title_id, AccountUid uid, const std::string& name,
|
|
const std::string& author) {
|
|
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(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 {
|
|
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() 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;
|
|
}
|
|
|
|
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";
|
|
}
|
|
|
|
void Title::refreshDirectories() {
|
|
m_saves.clear();
|
|
m_full_save_paths.clear();
|
|
|
|
Directory savelist(m_path);
|
|
if (savelist.good()) {
|
|
for (size_t i = 0; i < savelist.size(); ++i) {
|
|
if (savelist.folder(i)) {
|
|
m_saves.push_back(savelist.entry(i));
|
|
m_full_save_paths.push_back(m_path + "/" + savelist.entry(i));
|
|
}
|
|
}
|
|
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 {
|
|
nxst::log::error("Could not read save directory for title %s", m_name.c_str());
|
|
}
|
|
}
|
|
|
|
bool areTitlesLoaded() {
|
|
return s_titles_loaded;
|
|
}
|
|
|
|
void loadTitles() {
|
|
if (s_titles_loaded)
|
|
return;
|
|
s_titles_loaded = true;
|
|
titles.clear();
|
|
|
|
FsSaveDataInfoReader reader;
|
|
Result res = fsOpenSaveDataInfoReader(&reader, FsSaveDataSpaceId_User);
|
|
if (R_FAILED(res))
|
|
return;
|
|
|
|
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);
|
|
}
|
|
|
|
auto it = titles.find(uid);
|
|
if (it != titles.end()) {
|
|
it->second.push_back(title);
|
|
} else {
|
|
titles.emplace(uid, std::vector<Title>{title});
|
|
}
|
|
}
|
|
|
|
fsSaveDataInfoReaderClose(&reader);
|
|
sortTitles();
|
|
}
|
|
|
|
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 SortPlayTime:
|
|
return l.playTimeNanoseconds() > r.playTimeNanoseconds();
|
|
case SortAlpha:
|
|
default:
|
|
return l.name() < r.name();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
void rotateSortMode() {
|
|
s_sort_mode = static_cast<sort_t>((s_sort_mode + 1) % SortModesCount);
|
|
sortTitles();
|
|
}
|
|
|
|
void getTitle(Title& dst, AccountUid uid, size_t i) {
|
|
auto it = titles.find(uid);
|
|
if (it != titles.end() && i < it->second.size())
|
|
dst = it->second[i];
|
|
}
|
|
|
|
size_t getTitleCount(AccountUid uid) {
|
|
auto it = titles.find(uid);
|
|
return it != titles.end() ? it->second.size() : 0;
|
|
}
|
|
|
|
void refreshDirectories(u64 id) {
|
|
for (auto& pair : titles) {
|
|
for (auto& title : pair.second) {
|
|
if (title.id() == id)
|
|
title.refreshDirectories();
|
|
}
|
|
}
|
|
}
|
|
|
|
std::unordered_map<std::string, std::string> getCompleteTitleList() {
|
|
std::unordered_map<std::string, std::string> map;
|
|
for (const auto& pair : titles) {
|
|
for (const auto& title : pair.second) {
|
|
map.emplace(StringUtils::format("0x%016llX", title.id()), title.name());
|
|
}
|
|
}
|
|
return map;
|
|
}
|