Files
NXST/src/domain/title.cpp
T
DragonSpirit d410c4355d
CI / Build NRO (push) Has been cancelled
CI / Format check (push) Has been cancelled
CI / Layering check (push) Has been cancelled
another stage of refactoring
2026-05-12 09:59:43 +03:00

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;
}