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
+30 -72
View File
@@ -1,38 +1,16 @@
/*
* 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 <cstdio>
#include <cstring>
#include <map>
#include <sys/stat.h>
#include <vector>
#include <nxst/domain/account.hpp>
#include <nxst/infra/fs/handles.hpp>
static std::map<AccountUid, User> mUsers;
static std::map<AccountUid, User> s_users;
Result Account::init(void) {
Result Account::init() {
Result res = accountInitialize(AccountServiceType_Application);
if (R_FAILED(res))
return res;
@@ -40,48 +18,47 @@ Result Account::init(void) {
AccountUid uids[8];
s32 count = 0;
accountListAllUsers(uids, 8, &count);
for (s32 i = 0; i < count; i++) {
Account::username(uids[i]); // populates mUsers as side effect
for (s32 i = 0; i < count; ++i) {
username(uids[i]); // populate cache
}
return 0;
}
void Account::exit(void) {
void Account::exit() {
accountExit();
}
std::vector<AccountUid> Account::ids(void) {
std::vector<AccountUid> v;
for (auto& value : mUsers) {
v.push_back(value.second.id);
std::vector<AccountUid> Account::ids() {
std::vector<AccountUid> result;
result.reserve(s_users.size());
for (const auto& pair : s_users) {
result.push_back(pair.second.id);
}
return v;
return result;
}
static User getUser(AccountUid id) {
static User fetchUser(AccountUid id) {
User user{id, ""};
nxst::AccountProfileHandle profile;
AccountProfileBase profilebase;
memset(&profilebase, 0, sizeof(profilebase));
AccountProfileBase base{};
if (R_SUCCEEDED(accountGetProfile(profile.get(), id))) {
profile.valid = true;
if (R_SUCCEEDED(accountProfileGet(profile.get(), NULL, &profilebase))) {
user.name = std::string(profilebase.nickname);
if (R_SUCCEEDED(accountProfileGet(profile.get(), nullptr, &base))) {
user.name = std::string(base.nickname);
}
}
return user;
}
std::string Account::username(AccountUid id) {
std::map<AccountUid, User>::const_iterator got = mUsers.find(id);
if (got == mUsers.end()) {
User user = getUser(id);
mUsers.insert({id, user});
auto it = s_users.find(id);
if (it == s_users.end()) {
User user = fetchUser(id);
s_users.emplace(id, user);
return user.name;
}
return got->second.name;
return it->second.name;
}
std::string Account::iconPath(AccountUid id) {
@@ -101,37 +78,18 @@ std::string Account::iconPath(AccountUid id) {
return "";
profile.valid = true;
u32 imgSize = 0;
if (R_FAILED(accountProfileGetImageSize(profile.get(), &imgSize)) || imgSize == 0)
u32 img_size = 0;
if (R_FAILED(accountProfileGetImageSize(profile.get(), &img_size)) || img_size == 0)
return "";
std::vector<u8> buf(imgSize);
u32 outSize = 0;
if (R_FAILED(accountProfileLoadImage(profile.get(), buf.data(), imgSize, &outSize)) || outSize == 0)
std::vector<u8> buf(img_size);
u32 out_size = 0;
if (R_FAILED(accountProfileLoadImage(profile.get(), buf.data(), img_size, &out_size)) || out_size == 0)
return "";
nxst::FileHandle f(fopen(path, "wb"));
if (!f)
return "";
fwrite(buf.data(), 1, outSize, f.get());
fwrite(buf.data(), 1, out_size, f.get());
return std::string(path);
}
AccountUid Account::selectAccount(void) {
LibAppletArgs args;
libappletArgsCreate(&args, 0x10000);
u8 st_in[0xA0] = {0};
u8 st_out[0x18] = {0};
size_t repsz;
Result res =
libappletLaunch(AppletId_LibraryAppletPlayerSelect, &args, st_in, 0xA0, st_out, 0x18, &repsz);
if (R_SUCCEEDED(res)) {
u64 lres = *(u64*)st_out;
AccountUid uid = *(AccountUid*)&st_out[8];
if (lres == 0)
return uid;
}
return AccountUid{};
}
-121
View File
@@ -1,121 +0,0 @@
/*
* 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.
*/
#include <nxst/domain/common.hpp>
std::string DateTime::timeStr(void) {
time_t unixTime;
struct tm timeStruct;
time(&unixTime);
localtime_r(&unixTime, &timeStruct);
return StringUtils::format("%02i:%02i:%02i", timeStruct.tm_hour, timeStruct.tm_min, timeStruct.tm_sec);
}
std::string DateTime::dateTimeStr(void) {
time_t unixTime;
struct tm timeStruct;
time(&unixTime);
localtime_r(&unixTime, &timeStruct);
return StringUtils::format("%04i%02i%02i-%02i%02i%02i", timeStruct.tm_year + 1900, timeStruct.tm_mon + 1,
timeStruct.tm_mday, timeStruct.tm_hour, timeStruct.tm_min, timeStruct.tm_sec);
}
std::string DateTime::logDateTime(void) {
time_t unixTime;
struct tm timeStruct;
time(&unixTime);
localtime_r(&unixTime, &timeStruct);
return StringUtils::format("%04i-%02i-%02i %02i:%02i:%02i", timeStruct.tm_year + 1900,
timeStruct.tm_mon + 1, timeStruct.tm_mday, timeStruct.tm_hour,
timeStruct.tm_min, timeStruct.tm_sec);
}
std::string StringUtils::UTF16toUTF8(const std::u16string& src) {
static std::wstring_convert<std::codecvt_utf8_utf16<char16_t>, char16_t> convert;
std::string dst = convert.to_bytes(src);
return dst;
}
std::string StringUtils::removeForbiddenCharacters(std::string src) {
static const std::string illegalChars = ".,!\\/:?*\"<>|";
for (size_t i = 0, sz = src.length(); i < sz; i++) {
if (illegalChars.find(src[i]) != std::string::npos) {
src[i] = ' ';
}
}
size_t i;
for (i = src.length() - 1; i > 0 && src[i] == ' '; i--)
;
src.erase(i + 1, src.length() - i);
return src;
}
std::string StringUtils::format(const std::string fmt_str, ...) {
va_list ap;
char* fp = NULL;
va_start(ap, fmt_str);
vasprintf(&fp, fmt_str.c_str(), ap);
va_end(ap);
std::unique_ptr<char[]> formatted(fp);
return std::string(formatted.get());
}
bool StringUtils::containsInvalidChar(const std::string& str) {
for (size_t i = 0, sz = str.length(); i < sz; i++) {
if (!isascii(str[i])) {
return true;
}
}
return false;
}
void StringUtils::ltrim(std::string& s) {
s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](int ch) {
return !std::isspace(ch);
}));
}
void StringUtils::rtrim(std::string& s) {
s.erase(std::find_if(s.rbegin(), s.rend(),
[](int ch) {
return !std::isspace(ch);
})
.base(),
s.end());
}
void StringUtils::trim(std::string& s) {
ltrim(s);
rtrim(s);
}
char* getConsoleIP(void) {
struct in_addr in;
in.s_addr = gethostid();
return inet_ntoa(in);
}
+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;
+190 -99
View File
@@ -1,159 +1,250 @@
/*
* 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 <cctype>
#include <cstdarg>
#include <cstdio>
#include <string>
#include <unordered_map>
#include <nxst/app/main.hpp>
#include <nxst/app/main_application.hpp>
#include <nxst/domain/account.hpp>
#include <nxst/domain/util.hpp>
#include <nxst/infra/fs/io.hpp>
#include <nxst/infra/sys/logger.hpp>
void servicesExit(void) {
Logger::getInstance().flush();
static bool s_notification_led_available = false;
void servicesExit() {
Account::exit();
plExit();
romfsExit();
}
Result servicesInit(void) {
Result servicesInit() {
io::createDirectory("sdmc:/switch");
io::createDirectory("sdmc:/switch/NXST");
io::createDirectory("sdmc:/switch/NXST/saves");
if (appletGetAppletType() != AppletType_Application) {
Logger::getInstance().log(Logger::WARN, "Please do not run NXST in applet mode.");
nxst::log::warn("Please do not run NXST in applet mode.");
}
Result res = 0;
romfsInit();
padConfigureInput(1, HidNpadStyleSet_NpadStandard);
hidInitializeTouchScreen();
if (R_FAILED(res = plInitialize(PlServiceType_User))) {
Logger::getInstance().log(Logger::ERROR, "plInitialize failed. Result code 0x%08lX.", res);
nxst::log::error("plInitialize failed. Result code 0x%08X.", res);
return res;
}
if (R_FAILED(res = Account::init())) {
Logger::getInstance().log(Logger::ERROR, "Account::init failed. Result code 0x%08lX.", res);
nxst::log::error("Account::init failed. Result code 0x%08X.", res);
return res;
}
if (R_FAILED(res = nsInitialize())) {
Logger::getInstance().log(Logger::ERROR, "nsInitialize failed. Result code 0x{:08X}.", res);
nxst::log::error("nsInitialize failed. Result code 0x%08X.", res);
return res;
}
if (R_SUCCEEDED(res = hidsysInitialize())) {
g_notificationLedAvailable = true;
if (R_SUCCEEDED(hidsysInitialize())) {
s_notification_led_available = true;
} else {
Logger::getInstance().log(Logger::INFO, "Notification led not available. Result code 0x{:08X}.", res);
nxst::log::info("Notification LED not available.");
}
Logger::getInstance().log(Logger::INFO, "NXST loading completed!");
nxst::log::info("NXST loading completed.");
return 0;
}
std::u16string StringUtils::UTF8toUTF16(const char* src) {
char16_t tmp[256] = {0};
utf8_to_utf16((uint16_t*)tmp, (uint8_t*)src, 256);
return std::u16string(tmp);
bool StringUtils::containsInvalidChar(const std::string& str) {
for (unsigned char c : str) {
if (!isascii(c))
return true;
}
return false;
}
// https://stackoverflow.com/questions/14094621/change-all-accented-letters-to-normal-letters-in-c
std::string StringUtils::removeAccents(std::string str) {
std::u16string src = UTF8toUTF16(str.c_str());
const std::u16string illegal =
UTF8toUTF16("ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüūýþÿ");
const std::u16string fixed =
UTF8toUTF16("AAAAAAECEEEEIIIIDNOOOOOx0UUUUYPsaaaaaaeceeeeiiiiOnooooo/0uuuuuypy");
std::string StringUtils::format(const char* fmt, ...) {
va_list a1, a2;
va_start(a1, fmt);
va_copy(a2, a1);
int n = vsnprintf(nullptr, 0, fmt, a1);
va_end(a1);
if (n < 0) {
va_end(a2);
return {};
}
std::string buf(static_cast<size_t>(n), '\0');
vsnprintf(buf.data(), static_cast<size_t>(n) + 1, fmt, a2);
va_end(a2);
return buf;
}
for (size_t i = 0, sz = src.length(); i < sz; i++) {
size_t index = illegal.find(src[i]);
if (index != std::string::npos) {
src[i] = fixed[index];
std::string StringUtils::removeForbiddenCharacters(std::string src) {
static constexpr std::string_view kForbidden = ".,!\\/:?*\"<>|";
for (char& c : src) {
if (kForbidden.find(c) != std::string_view::npos)
c = ' ';
}
auto last = src.find_last_not_of(' ');
if (last != std::string::npos)
src.erase(last + 1);
return src;
}
static size_t encodeUtf8(char* out, char32_t cp) {
if (cp < 0x80) {
out[0] = static_cast<char>(cp);
return 1;
}
if (cp < 0x800) {
out[0] = static_cast<char>(0xC0 | (cp >> 6));
out[1] = static_cast<char>(0x80 | (cp & 0x3F));
return 2;
}
if (cp < 0x10000) {
out[0] = static_cast<char>(0xE0 | (cp >> 12));
out[1] = static_cast<char>(0x80 | ((cp >> 6) & 0x3F));
out[2] = static_cast<char>(0x80 | (cp & 0x3F));
return 3;
}
out[0] = static_cast<char>(0xF0 | (cp >> 18));
out[1] = static_cast<char>(0x80 | ((cp >> 12) & 0x3F));
out[2] = static_cast<char>(0x80 | ((cp >> 6) & 0x3F));
out[3] = static_cast<char>(0x80 | (cp & 0x3F));
return 4;
}
std::string StringUtils::UTF16toUTF8(const std::u16string& src) {
std::string result;
result.reserve(src.size() * 2);
for (size_t i = 0; i < src.size(); ++i) {
char32_t cp = src[i];
if (cp >= 0xD800 && cp <= 0xDBFF && i + 1 < src.size()) {
cp = 0x10000 + ((cp - 0xD800) << 10) + (src[++i] - 0xDC00);
}
char buf[4];
result.append(buf, encodeUtf8(buf, cp));
}
return result;
}
void StringUtils::ltrim(std::string& s) {
s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](unsigned char c) {
return !std::isspace(c);
}));
}
void StringUtils::rtrim(std::string& s) {
s.erase(std::find_if(s.rbegin(), s.rend(),
[](unsigned char c) {
return !std::isspace(c);
})
.base(),
s.end());
}
void StringUtils::trim(std::string& s) {
ltrim(s);
rtrim(s);
}
// Decodes a UTF-8 string to UTF-16, handling surrogate pairs for codepoints > U+FFFF.
std::u16string StringUtils::UTF8toUTF16(const char* src) {
std::u16string result;
while (*src != '\0') {
char32_t cp = 0;
unsigned char c = static_cast<unsigned char>(*src);
if (c < 0x80) {
cp = c;
++src;
} else if (c < 0xE0) {
cp = static_cast<char32_t>(c & 0x1F) << 6 | (src[1] & 0x3F);
src += 2;
} else if (c < 0xF0) {
cp = static_cast<char32_t>(c & 0x0F) << 12 | static_cast<char32_t>(src[1] & 0x3F) << 6 |
(src[2] & 0x3F);
src += 3;
} else {
cp = static_cast<char32_t>(c & 0x07) << 18 | static_cast<char32_t>(src[1] & 0x3F) << 12 |
static_cast<char32_t>(src[2] & 0x3F) << 6 | (src[3] & 0x3F);
src += 4;
}
if (cp < 0x10000) {
result += static_cast<char16_t>(cp);
} else {
cp -= 0x10000;
result += static_cast<char16_t>(0xD800 | (cp >> 10));
result += static_cast<char16_t>(0xDC00 | (cp & 0x3FF));
}
}
return result;
}
return UTF16toUTF8(src);
// Replaces Latin characters with diacritics with their ASCII base equivalents.
std::string StringUtils::removeAccents(std::string str) {
static const std::unordered_map<char16_t, char16_t> kMap = {
{u'À', u'A'}, {u'Á', u'A'}, {u'Â', u'A'}, {u'Ã', u'A'}, {u'Ä', u'A'}, {u'Å', u'A'}, {u'Æ', u'E'},
{u'Ç', u'C'}, {u'È', u'E'}, {u'É', u'E'}, {u'Ê', u'E'}, {u'Ë', u'E'}, {u'Ì', u'I'}, {u'Í', u'I'},
{u'Î', u'I'}, {u'Ï', u'I'}, {u'Ð', u'D'}, {u'Ñ', u'N'}, {u'Ò', u'O'}, {u'Ó', u'O'}, {u'Ô', u'O'},
{u'Õ', u'O'}, {u'Ö', u'O'}, {u'Ø', u'O'}, {u'Ù', u'U'}, {u'Ú', u'U'}, {u'Û', u'U'}, {u'Ü', u'U'},
{u'Ý', u'Y'}, {u'ß', u's'}, {u'à', u'a'}, {u'á', u'a'}, {u'â', u'a'}, {u'ã', u'a'}, {u'ä', u'a'},
{u'å', u'a'}, {u'æ', u'e'}, {u'ç', u'c'}, {u'è', u'e'}, {u'é', u'e'}, {u'ê', u'e'}, {u'ë', u'e'},
{u'ì', u'i'}, {u'í', u'i'}, {u'î', u'i'}, {u'ï', u'i'}, {u'ñ', u'n'}, {u'ò', u'o'}, {u'ó', u'o'},
{u'ô', u'o'}, {u'õ', u'o'}, {u'ö', u'o'}, {u'ø', u'o'}, {u'ù', u'u'}, {u'ú', u'u'}, {u'û', u'u'},
{u'ü', u'u'}, {u'ū', u'u'}, {u'ý', u'y'}, {u'ÿ', u'y'},
};
std::u16string wide = UTF8toUTF16(str.c_str());
for (char16_t& ch : wide) {
auto it = kMap.find(ch);
if (it != kMap.end())
ch = it->second;
}
return StringUtils::UTF16toUTF8(wide);
}
std::string StringUtils::removeNotAscii(std::string str) {
for (size_t i = 0, sz = str.length(); i < sz; i++) {
if (!isascii(str[i])) {
str[i] = ' ';
}
for (char& c : str) {
if (!isascii(static_cast<unsigned char>(c)))
c = ' ';
}
return str;
}
std::string StringUtils::elide(const std::string& s, size_t maxChars) {
if (s.size() <= maxChars || maxChars < 6)
std::string StringUtils::elide(const std::string& s, size_t max_chars) {
if (s.size() <= max_chars || max_chars < 6)
return s;
constexpr const char* dots = "...";
size_t budget = maxChars - 3;
size_t budget = max_chars - 3;
size_t head = (budget + 1) / 2;
size_t tail = budget - head;
return s.substr(0, head) + dots + s.substr(s.size() - tail);
return s.substr(0, head) + "..." + s.substr(s.size() - tail);
}
HidsysNotificationLedPattern blinkLedPattern(u8 times) {
HidsysNotificationLedPattern pattern;
memset(&pattern, 0, sizeof(pattern));
pattern.baseMiniCycleDuration = 0x1; // 12.5ms.
pattern.totalMiniCycles = 0x2; // 2 mini cycles.
pattern.totalFullCycles = times; // Repeat n times.
pattern.startIntensity = 0x0; // 0%.
pattern.miniCycles[0].ledIntensity = 0xF; // 100%.
pattern.miniCycles[0].transitionSteps = 0xF; // 15 steps. Total 187.5ms.
pattern.miniCycles[0].finalStepDuration = 0x0; // Forced 12.5ms.
pattern.miniCycles[1].ledIntensity = 0x0; // 0%.
pattern.miniCycles[1].transitionSteps = 0xF; // 15 steps. Total 187.5ms.
pattern.miniCycles[1].finalStepDuration = 0x0; // Forced 12.5ms.
return pattern;
static HidsysNotificationLedPattern makeLedPattern(u8 times) {
HidsysNotificationLedPattern p{};
p.baseMiniCycleDuration = 0x1;
p.totalMiniCycles = 0x2;
p.totalFullCycles = times;
p.startIntensity = 0x0;
p.miniCycles[0] = {0xF, 0xF, 0x0};
p.miniCycles[1] = {0x0, 0xF, 0x0};
return p;
}
void blinkLed(u8 times) {
if (g_notificationLedAvailable) {
PadState pad;
padInitializeDefault(&pad);
s32 n;
HidsysUniquePadId uniquePadIds[2] = {0};
HidsysNotificationLedPattern pattern = blinkLedPattern(times);
memset(uniquePadIds, 0, sizeof(uniquePadIds));
Result res = hidsysGetUniquePadsFromNpad(
padIsHandheld(&pad) ? HidNpadIdType_Handheld : HidNpadIdType_No1, uniquePadIds, 2, &n);
if (R_SUCCEEDED(res)) {
for (s32 i = 0; i < n; i++) {
hidsysSetNotificationLedPattern(&pattern, uniquePadIds[i]);
}
if (!s_notification_led_available)
return;
PadState pad;
padInitializeDefault(&pad);
s32 n = 0;
HidsysUniquePadId pads[2]{};
HidsysNotificationLedPattern pattern = makeLedPattern(times);
Result res = hidsysGetUniquePadsFromNpad(padIsHandheld(&pad) ? HidNpadIdType_Handheld : HidNpadIdType_No1,
pads, 2, &n);
if (R_SUCCEEDED(res)) {
for (s32 i = 0; i < n; ++i) {
hidsysSetNotificationLedPattern(&pattern, pads[i]);
}
}
}