5 Commits

Author SHA1 Message Date
ITotalJustice
4d27bf5492 bump version for new release 0.13.2 -> 0.13.3 2025-06-19 11:40:42 +01:00
ITotalJustice
6b85d2cef1 fix account save listing where the profile uid differs from the account uid.
i am unsure how this happens, as i thought the profile uid was the same as the account uid, but apparently this can differ.
on the same switch in sysmmc, the uid's match, so everything works.
again on the same switch, created in emummc, same account, the uid differs...

i performed the same test but on another 2 switches, and the uid's all match, so i am not sure what causes them to change.

in any case, using the uid from the account is the intended behaviour anyway, so this commit fixes that.
2025-06-19 11:37:08 +01:00
xHR
aae9930f5e Updated UK translation (#180)
* fixed long strings, translate untranslated

* Updated UK translation
2025-06-19 11:36:42 +01:00
ITotalJustice
eca19aa4bf fix hdd createDirWithPath, improve flashcart detection in gc menu, bump version for new release 0.13.1 -> 0.13.2 2025-06-18 23:17:58 +01:00
ITotalJustice
8e02538405 fix nvjpg crashing when trying to load a non-jpg image, fix building with nvjpg disable, optimise invalid nro asset loading, bump version for new release 0.13.0 -> 0.13.1 2025-06-18 21:10:15 +01:00
8 changed files with 74 additions and 36 deletions

View File

@@ -72,7 +72,7 @@
"Append folder with .xci": "Додати до теки .xci", "Append folder with .xci": "Додати до теки .xci",
"Trim XCI": "Обрізати XCI", "Trim XCI": "Обрізати XCI",
"Label trimmed XCI": "Позначити обрізаний XCI", "Label trimmed XCI": "Позначити обрізаний XCI",
"Multi-threaded USB transfer": "Многопоточ. передача USB", "Multi-threaded USB transfer": "Багатопотокова передача USB",
"Dump All Bins": "Дамп всіх BIN-файлів", "Dump All Bins": "Дамп всіх BIN-файлів",
"Dump XCI": "Дамп XCI", "Dump XCI": "Дамп XCI",
"Dump Card ID Set": "Дамп набору ID картриджа", "Dump Card ID Set": "Дамп набору ID картриджа",
@@ -116,8 +116,8 @@
"SSID:": "SSID:", "SSID:": "SSID:",
"Passphrase:": "Кодова фраза:", "Passphrase:": "Кодова фраза:",
"Failed to install, press B to exit...": "Не вдалося встановити через FTP, натисніть B для виходу...", "Failed to install, press B to exit...": "Не вдалося встановити через FTP, натисніть B для виходу...",
"Install success!": "Встановлення через FTP успішно завершено.", "Install success!": "Успішно встановлено",
"Install failed!": "Встановлення через FTP не вдалося.", "Install failed!": "Встановлення не вдалося.",
"USB Install": "Встановлення через USB", "USB Install": "Встановлення через USB",
"USB": "USB", "USB": "USB",
"Connected, waiting for file list...": "Підключено, очікування списку файлів...", "Connected, waiting for file list...": "Підключено, очікування списку файлів...",
@@ -206,8 +206,8 @@
"Slow": "Повільно", "Slow": "Повільно",
"Normal": "Середнє", "Normal": "Середнє",
"Fast": "Швидко", "Fast": "Швидко",
"Set left-side menu": "Ліве меню", "Set left-side menu": "Ліве меню (L)",
"Set right-side menu": "Праве меню", "Set right-side menu": "Праве меню (R)",
"Install options": "Опції встановлення", "Install options": "Опції встановлення",
"Install Options": "Опції встановлення", "Install Options": "Опції встановлення",
"Enable sysmmc": "Дозволити в sysMMC", "Enable sysmmc": "Дозволити в sysMMC",

View File

@@ -1,6 +1,6 @@
cmake_minimum_required(VERSION 3.13) cmake_minimum_required(VERSION 3.13)
set(sphaira_VERSION 0.13.0) set(sphaira_VERSION 0.13.3)
project(sphaira project(sphaira
VERSION ${sphaira_VERSION} VERSION ${sphaira_VERSION}
@@ -208,8 +208,8 @@ FetchContent_Declare(libnxtc
) )
FetchContent_Declare(nvjpg FetchContent_Declare(nvjpg
GIT_REPOSITORY https://github.com/averne/oss-nvjpg.git GIT_REPOSITORY https://github.com/ITotalJustice/oss-nvjpg.git
GIT_TAG 16c10a3 GIT_TAG 45680e7
) )
set(USE_NEW_ZSTD ON) set(USE_NEW_ZSTD ON)

View File

@@ -227,6 +227,10 @@ public:
AccountProfileBase base; AccountProfileBase base;
if (R_SUCCEEDED(accountProfileGet(&profile, nullptr, &base))) { if (R_SUCCEEDED(accountProfileGet(&profile, nullptr, &base))) {
// sometimes the uid for the acc can differ to the base.
base.uid = uids[i];
log_write("[ACC] found uid: 0x%016lX%016lX\n", uids[i].uid[0], uids[i].uid[1]);
log_write("[ACC] base uid: 0x%016lX%016lX\n", base.uid.uid[0], base.uid.uid[1]);
out.emplace_back(base); out.emplace_back(base);
} }
} }

View File

@@ -185,7 +185,6 @@ Result CreateDirectoryRecursivelyWithPath(FsFileSystem* fs, const FsPath& _path,
FsPath new_path{}; FsPath new_path{};
std::snprintf(new_path, sizeof(new_path), "%.*s", (int)(last_slash - _path.s), _path.s); std::snprintf(new_path, sizeof(new_path), "%.*s", (int)(last_slash - _path.s), _path.s);
R_TRY(CreateDirectoryRecursively(fs, new_path, ignore_read_only)); R_TRY(CreateDirectoryRecursively(fs, new_path, ignore_read_only));
fsFsCommit(fs);
R_SUCCEED(); R_SUCCEED();
} }

View File

@@ -2,6 +2,7 @@
#include "app.hpp" #include "app.hpp"
#include "threaded_file_transfer.hpp" #include "threaded_file_transfer.hpp"
#include <mbedtls/md5.h> #include <mbedtls/md5.h>
#include <utility>
namespace sphaira::hash { namespace sphaira::hash {
namespace { namespace {
@@ -16,11 +17,11 @@ struct FileSource final : BaseSource {
m_is_file_based_emummc = App::IsFileBaseEmummc(); m_is_file_based_emummc = App::IsFileBaseEmummc();
} }
Result Size(s64* out) { Result Size(s64* out) override {
return m_file.GetSize(out); return m_file.GetSize(out);
} }
Result Read(void* buf, s64 off, s64 size, u64* bytes_read) { Result Read(void* buf, s64 off, s64 size, u64* bytes_read) override {
const auto rc = m_file.Read(off, buf, size, 0, bytes_read); const auto rc = m_file.Read(off, buf, size, 0, bytes_read);
if (m_fs->IsNative() && m_is_file_based_emummc) { if (m_fs->IsNative() && m_is_file_based_emummc) {
svcSleepThread(2e+6); // 2ms svcSleepThread(2e+6); // 2ms
@@ -38,12 +39,12 @@ private:
struct MemSource final : BaseSource { struct MemSource final : BaseSource {
MemSource(std::span<const u8> data) : m_data{data} { } MemSource(std::span<const u8> data) : m_data{data} { }
Result Size(s64* out) { Result Size(s64* out) override {
*out = m_data.size(); *out = m_data.size();
R_SUCCEED(); R_SUCCEED();
} }
Result Read(void* buf, s64 off, s64 size, u64* bytes_read) { Result Read(void* buf, s64 off, s64 size, u64* bytes_read) override {
size = std::min<s64>(size, m_data.size() - off); size = std::min<s64>(size, m_data.size() - off);
std::memcpy(buf, m_data.data() + off, size); std::memcpy(buf, m_data.data() + off, size);
*bytes_read = size; *bytes_read = size;
@@ -61,11 +62,11 @@ struct HashSource {
}; };
struct HashCrc32 final : HashSource { struct HashCrc32 final : HashSource {
void Update(const void* buf, s64 size) { void Update(const void* buf, s64 size) override {
m_seed = crc32CalculateWithSeed(m_seed, buf, size); m_seed = crc32CalculateWithSeed(m_seed, buf, size);
} }
void Get(std::string& out) { void Get(std::string& out) override {
char str[CalculateHashStrLen(sizeof(m_seed))]; char str[CalculateHashStrLen(sizeof(m_seed))];
std::snprintf(str, sizeof(str), "%08x", m_seed); std::snprintf(str, sizeof(str), "%08x", m_seed);
out = str; out = str;
@@ -85,11 +86,11 @@ struct HashMd5 final : HashSource {
mbedtls_md5_free(&m_ctx); mbedtls_md5_free(&m_ctx);
} }
void Update(const void* buf, s64 size) { void Update(const void* buf, s64 size) override {
mbedtls_md5_update_ret(&m_ctx, (const u8*)buf, size); mbedtls_md5_update_ret(&m_ctx, (const u8*)buf, size);
} }
void Get(std::string& out) { void Get(std::string& out) override {
u8 hash[16]; u8 hash[16];
mbedtls_md5_finish_ret(&m_ctx, hash); mbedtls_md5_finish_ret(&m_ctx, hash);
@@ -110,11 +111,11 @@ struct HashSha1 final : HashSource {
sha1ContextCreate(&m_ctx); sha1ContextCreate(&m_ctx);
} }
void Update(const void* buf, s64 size) { void Update(const void* buf, s64 size) override {
sha1ContextUpdate(&m_ctx, buf, size); sha1ContextUpdate(&m_ctx, buf, size);
} }
void Get(std::string& out) { void Get(std::string& out) override {
u8 hash[SHA1_HASH_SIZE]; u8 hash[SHA1_HASH_SIZE];
sha1ContextGetHash(&m_ctx, hash); sha1ContextGetHash(&m_ctx, hash);
@@ -135,11 +136,11 @@ struct HashSha256 final : HashSource {
sha256ContextCreate(&m_ctx); sha256ContextCreate(&m_ctx);
} }
void Update(const void* buf, s64 size) { void Update(const void* buf, s64 size) override {
sha256ContextUpdate(&m_ctx, buf, size); sha256ContextUpdate(&m_ctx, buf, size);
} }
void Get(std::string& out) { void Get(std::string& out) override {
u8 hash[SHA256_HASH_SIZE]; u8 hash[SHA256_HASH_SIZE];
sha256ContextGetHash(&m_ctx, hash); sha256ContextGetHash(&m_ctx, hash);

View File

@@ -59,18 +59,20 @@ auto nro_parse_internal(fs::Fs* fs, const fs::FsPath& path, NroEntry& entry) ->
std::strncpy(nacp.lang.name, file_name, file_name_len - 4); std::strncpy(nacp.lang.name, file_name, file_name_len - 4);
std::strcpy(nacp.lang.author, "Unknown"); std::strcpy(nacp.lang.author, "Unknown");
std::strcpy(nacp.display_version, "Unknown"); std::strcpy(nacp.display_version, "Unknown");
entry.icon_offset = entry.icon_size = 0;
entry.is_nacp_valid = false; entry.is_nacp_valid = false;
} else { } else {
entry.size += sizeof(asset) + asset.icon.size + asset.nacp.size + asset.romfs.size; entry.size += sizeof(asset) + asset.icon.size + asset.nacp.size + asset.romfs.size;
R_TRY(f.Read(data.header.size + asset.nacp.offset, &nacp.lang, sizeof(nacp.lang), FsReadOption_None, &bytes_read)); R_TRY(f.Read(data.header.size + asset.nacp.offset, &nacp.lang, sizeof(nacp.lang), FsReadOption_None, &bytes_read));
R_TRY(f.Read(data.header.size + asset.nacp.offset + offsetof(NacpStruct, display_version), nacp.display_version, sizeof(nacp.display_version), FsReadOption_None, &bytes_read)); R_TRY(f.Read(data.header.size + asset.nacp.offset + offsetof(NacpStruct, display_version), nacp.display_version, sizeof(nacp.display_version), FsReadOption_None, &bytes_read));
// lazy load the icons
entry.icon_size = asset.icon.size;
entry.icon_offset = data.header.size + asset.icon.offset;
entry.is_nacp_valid = true; entry.is_nacp_valid = true;
} }
// lazy load the icons
entry.icon_size = asset.icon.size;
entry.icon_offset = data.header.size + asset.icon.offset;
R_SUCCEED(); R_SUCCEED();
} }

View File

@@ -1045,25 +1045,35 @@ Result Menu::DumpGames(u32 flags) {
bool is_trimmed = false; bool is_trimmed = false;
Result trim_rc = 0; Result trim_rc = 0;
if ((flags & DumpFileFlag_XCI) && m_storage_trimmed_size < m_storage_total_size) { if ((flags & DumpFileFlag_XCI) && m_storage_trimmed_size < m_storage_total_size) {
u8 temp{}; const auto start_offset = std::min<s64>(0, m_storage_trimmed_size - 0x4000);
if (R_FAILED(trim_rc = GcStorageRead(&temp, m_storage_trimmed_size, sizeof(temp)))) { // works on fw 1.2.0 and below.
log_write("[GC] WARNING! GameCard is already trimmed: 0x%X FlashError: %u\n", trim_rc, trim_rc == 0x13D002); std::vector<u8> temp(1024*1024*1);
if (R_FAILED(trim_rc = GcStorageRead(temp.data(), m_storage_trimmed_size, std::min<s64>(temp.size(), m_storage_total_size - start_offset)))) {
log_write("[GC] WARNING1! GameCard is already trimmed: 0x%X FlashError: %u\n", trim_rc, trim_rc == 0x13D002);
is_trimmed = true; is_trimmed = true;
} }
if (!is_trimmed) {
// works on fw 1.2.0 and below.
if (R_FAILED(trim_rc = GcStorageRead(temp.data(), m_storage_total_size - temp.size(), temp.size()))) {
log_write("[GC] WARNING2! GameCard is already trimmed: 0x%X FlashError: %u\n", trim_rc, trim_rc == 0x13D002);
is_trimmed = true;
}
}
} }
// if trimmed and the user wants to dump the full xci, error. // if trimmed and the user wants to dump the full xci, error.
if ((flags & DumpFileFlag_XCI) && is_trimmed && App::GetApp()->m_dump_trim_xci.Get()) { if ((flags & DumpFileFlag_XCI) && is_trimmed && App::GetApp()->m_dump_trim_xci.Get()) {
App::PushErrorBox(trim_rc, "GameCard is already trimmed!"_i18n);
} else if ((flags & DumpFileFlag_XCI) && is_trimmed) {
App::Push(std::make_shared<ui::OptionBox>( App::Push(std::make_shared<ui::OptionBox>(
"WARNING: GameCard is already trimmed!"_i18n, "WARNING: GameCard is already trimmed!"_i18n,
"Back"_i18n, "Continue"_i18n, 0, [&](auto op_index){ "Back"_i18n, "Continue"_i18n, 0, [&](auto op_index){
if (op_index && *op_index) { if (op_index && *op_index) {
do_dump(flags); do_dump(flags);
} }
} }, m_icon
)); ));
} else if ((flags & DumpFileFlag_XCI) && is_trimmed) {
App::PushErrorBox(trim_rc, "GameCard is trimmed, full dump is not possible!"_i18n);
} else { } else {
do_dump(flags); do_dump(flags);
} }

View File

@@ -815,6 +815,9 @@ Menu::Menu(u32 flags) : grid::Menu{"Saves"_i18n, flags} {
if (it != m_accounts.end()) { if (it != m_accounts.end()) {
m_account_index = std::distance(m_accounts.begin(), it); m_account_index = std::distance(m_accounts.begin(), it);
log_write("[SAVE] found account uid at: %zu\n", m_account_index);
} else {
log_write("[SAVE] account uid is not found: 0x%016lX%016lX\n", uid.uid[0], uid.uid[1]);
} }
for (auto& e : ncm_entries) { for (auto& e : ncm_entries) {
@@ -936,10 +939,26 @@ void Menu::SetIndex(s64 index) {
m_list->SetYoff(0); m_list->SetYoff(0);
} }
if (m_accounts.empty()) {
return;
}
u64 id{};
if (!m_entries.empty()) {
if (m_data_type == FsSaveDataType_System || m_data_type == FsSaveDataType_SystemBcat) {
id = m_entries[m_index].system_save_data_id;
} else {
id = m_entries[m_index].application_id;
}
this->SetSubHeading(std::to_string(m_index + 1) + " / " + std::to_string(m_entries.size()));
} else {
this->SetSubHeading("0 / 0");
}
char title[0x40]; char title[0x40];
std::snprintf(title, sizeof(title), "%s | %016lX", m_accounts[m_account_index].nickname, m_entries[m_index].application_id); std::snprintf(title, sizeof(title), "%s | %016lX", m_accounts[m_account_index].nickname, id);
SetTitleSubHeading(title); SetTitleSubHeading(title);
this->SetSubHeading(std::to_string(m_index + 1) + " / " + std::to_string(m_entries.size()));
} }
void Menu::ScanHomebrew() { void Menu::ScanHomebrew() {
@@ -947,7 +966,10 @@ void Menu::ScanHomebrew() {
TimeStamp ts; TimeStamp ts;
FreeEntries(); FreeEntries();
ClearSelection();
m_entries.reserve(ENTRY_CHUNK_COUNT); m_entries.reserve(ENTRY_CHUNK_COUNT);
m_is_reversed = false;
m_dirty = false;
if (m_accounts.empty()) { if (m_accounts.empty()) {
return; return;
@@ -958,7 +980,9 @@ void Menu::ScanHomebrew() {
GetFsSaveAttr(m_accounts[m_account_index], m_data_type, space_id, filter); GetFsSaveAttr(m_accounts[m_account_index], m_data_type, space_id, filter);
FsSaveDataInfoReader reader; FsSaveDataInfoReader reader;
fsOpenSaveDataInfoReaderWithFilter(&reader, space_id, &filter); if (R_FAILED(fsOpenSaveDataInfoReaderWithFilter(&reader, space_id, &filter))) {
log_write("[SAVE] failed to open reader\n");
}
ON_SCOPE_EXIT(fsSaveDataInfoReaderClose(&reader)); ON_SCOPE_EXIT(fsSaveDataInfoReaderClose(&reader));
std::vector<FsSaveDataInfo> info_list(ENTRY_CHUNK_COUNT); std::vector<FsSaveDataInfo> info_list(ENTRY_CHUNK_COUNT);
@@ -979,8 +1003,6 @@ void Menu::ScanHomebrew() {
} }
} }
m_is_reversed = false;
m_dirty = false;
log_write("games found: %zu time_taken: %.2f seconds %zu ms %zu ns\n", m_entries.size(), ts.GetSecondsD(), ts.GetMs(), ts.GetNs()); log_write("games found: %zu time_taken: %.2f seconds %zu ms %zu ns\n", m_entries.size(), ts.GetSecondsD(), ts.GetMs(), ts.GetNs());
this->Sort(); this->Sort();
SetIndex(0); SetIndex(0);