mirror of
https://git.eden-emu.dev/eden-emu/eden.git
synced 2026-10-21 10:11:16 +00:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ccd5b99634 | |||
| da7d4165ee | |||
| 144b0502c6 | |||
| 1928bbbde0 | |||
| 469aa1e3d6 | |||
| 0b6f102d5d | |||
| 710c927ff8 | |||
| 9bc3503525 | |||
| ea68a544cb | |||
| c6f33de22b | |||
| e77cc89bf5 | |||
| 17091b59f1 |
Vendored
+293
-283
File diff suppressed because it is too large
Load Diff
Vendored
+288
-278
File diff suppressed because it is too large
Load Diff
Vendored
+288
-278
File diff suppressed because it is too large
Load Diff
Vendored
+288
-278
File diff suppressed because it is too large
Load Diff
Vendored
+288
-278
File diff suppressed because it is too large
Load Diff
Vendored
+288
-278
File diff suppressed because it is too large
Load Diff
Vendored
+288
-278
File diff suppressed because it is too large
Load Diff
Vendored
+288
-278
File diff suppressed because it is too large
Load Diff
Vendored
+288
-278
File diff suppressed because it is too large
Load Diff
Vendored
+288
-278
File diff suppressed because it is too large
Load Diff
Vendored
+288
-278
File diff suppressed because it is too large
Load Diff
Vendored
+288
-278
File diff suppressed because it is too large
Load Diff
Vendored
+288
-278
File diff suppressed because it is too large
Load Diff
Vendored
+340
-335
File diff suppressed because it is too large
Load Diff
Vendored
+288
-278
File diff suppressed because it is too large
Load Diff
Vendored
+288
-278
File diff suppressed because it is too large
Load Diff
Vendored
+288
-278
File diff suppressed because it is too large
Load Diff
Vendored
+288
-278
File diff suppressed because it is too large
Load Diff
Vendored
+288
-278
File diff suppressed because it is too large
Load Diff
Vendored
+288
-278
File diff suppressed because it is too large
Load Diff
Vendored
+288
-278
File diff suppressed because it is too large
Load Diff
Vendored
+288
-278
File diff suppressed because it is too large
Load Diff
Vendored
+288
-278
File diff suppressed because it is too large
Load Diff
Vendored
+288
-278
File diff suppressed because it is too large
Load Diff
Vendored
+288
-278
File diff suppressed because it is too large
Load Diff
Vendored
+288
-278
File diff suppressed because it is too large
Load Diff
Vendored
+288
-278
File diff suppressed because it is too large
Load Diff
@@ -100,7 +100,7 @@ class Game(
|
||||
|
||||
companion object {
|
||||
val extensions: Set<String> = HashSet(
|
||||
listOf("xci", "nsp", "nca", "nro")
|
||||
listOf("xci", "nsp", "nca", "nro", "nsz", "xcz")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@ object GameHelper {
|
||||
|
||||
// File extensions considered as external content, buuut should
|
||||
// be done better imo.
|
||||
private val externalContentExtensions = setOf("nsp", "xci")
|
||||
private val externalContentExtensions = setOf("nsp", "xci", "nsz", "xcz")
|
||||
|
||||
private fun scanContentContainersRecursive(
|
||||
files: Array<MinimalDocumentFile>,
|
||||
|
||||
@@ -76,7 +76,9 @@ jboolean Java_org_yuzu_yuzu_1emu_utils_GameMetadata_getIsValid(JNIEnv* env, jobj
|
||||
auto const file_type = loader->GetFileType();
|
||||
if (file_type == Loader::FileType::Unknown || file_type == Loader::FileType::Error)
|
||||
return false;
|
||||
if ((file_type == Loader::FileType::NSP || file_type == Loader::FileType::XCI) && !Loader::IsBootableGameContainer(file, file_type))
|
||||
if ((file_type == Loader::FileType::NSP || file_type == Loader::FileType::XCI ||
|
||||
file_type == Loader::FileType::NSZ || file_type == Loader::FileType::XCZ) &&
|
||||
!Loader::IsBootableGameContainer(file, file_type))
|
||||
return false;
|
||||
u64 program_id = 0;
|
||||
return loader->ReadProgramId(program_id) == Loader::ResultStatus::Success;
|
||||
|
||||
@@ -696,7 +696,7 @@
|
||||
<string name="control_stick">عصا التحكم</string>
|
||||
<string name="right_stick">العصا اليمنى</string>
|
||||
<string name="c_stick">عصا-C</string>
|
||||
<string name="pressed">ضغط العصا</string>
|
||||
<string name="pressed">مضغوط</string>
|
||||
<string name="range">النطاق</string>
|
||||
<string name="deadzone">المنطقة الميتة</string>
|
||||
<string name="modifier">معدل</string>
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
|
||||
<string name="input_overlay_options">虛擬按鍵設定</string>
|
||||
<string name="input_overlay_options_description">調整虛擬按鍵的位置與顯示設定</string>
|
||||
<string name="edit_overlay_layout">調整虛擬按鍵</string>
|
||||
<string name="edit_overlay_layout">編輯虛擬按鍵</string>
|
||||
<string name="edit_overlay_layout_description">調整虛擬按鍵的位置和大小</string>
|
||||
|
||||
|
||||
@@ -253,8 +253,8 @@
|
||||
<string name="install_firmware_warning">跳過安裝韌體?</string>
|
||||
<string name="emulator_data">設定模擬器資料</string>
|
||||
<string name="emulator_data_description">模擬器需要金鑰才能正常執行,同時建議安裝韌體以啟動 QLaunch 小程式</string>
|
||||
<string name="permissions">授予通知權限</string>
|
||||
<string name="permissions_description">授予 Eden 傳送通知的權限以使用模擬器的特定功能</string>
|
||||
<string name="permissions">授予權限</string>
|
||||
<string name="permissions_description">授予權限以使用模擬器的特定功能</string>
|
||||
<string name="install_firmware_warning_description">許多遊戲需要韌體才能正常運作</string>
|
||||
<string name="install_firmware_warning_help">https://yuzu-mirror.github.io/help/quickstart/#guide-introduction</string>
|
||||
<string name="notifications">通知</string>
|
||||
@@ -268,8 +268,8 @@
|
||||
<string name="device_manufacturer">製造商</string>
|
||||
<string name="device_model">型號</string>
|
||||
<string name="product">型號代碼</string>
|
||||
<string name="android_version">Android 版本</string>
|
||||
<string name="android_security_patch">Android 安全性修補程式等級</string>
|
||||
<string name="android_version">安卓版本</string>
|
||||
<string name="android_security_patch">安卓安全性修補程式等級</string>
|
||||
<string name="build_id">版本號碼</string>
|
||||
<string name="general_information">基本資訊</string>
|
||||
<string name="hardware">硬體</string>
|
||||
@@ -287,7 +287,7 @@
|
||||
<string name="warning_skip">跳過</string>
|
||||
<string name="warning_cancel">取消</string>
|
||||
<string name="install_amiibo_keys">安裝 Amiibo 金鑰</string>
|
||||
<string name="install_amiibo_keys_description">安裝 Amiibo 以在日後遊玩時使用</string>
|
||||
<string name="install_amiibo_keys_description">需要先安裝才能在遊戲中使用 Amiibo</string>
|
||||
<string name="gpu_driver_fetcher">GPU 驅動程式下載器</string>
|
||||
<string name="gpu_driver_manager">GPU 驅動程式管理員</string>
|
||||
<string name="install_gpu_driver_description">安裝替代驅動程式以取得潛在的更佳效能或準確度</string>
|
||||
@@ -417,17 +417,17 @@
|
||||
<string name="custom_driver_not_supported">您的裝置不支援自訂驅動程式</string>
|
||||
<string name="custom_driver_not_supported_description">此裝置不支援注入自訂驅動程式\n請以後再來查看是否已新增支援!</string>
|
||||
<string name="manage_yuzu_data">管理 Eden 資料</string>
|
||||
<string name="manage_yuzu_data_description">安裝韌體、金鑰,匯入/匯出使用者資料及安裝其它項目!</string>
|
||||
<string name="manage_yuzu_data_description">安裝韌體、金鑰,導入/導出使用者資料及安裝其它項目!</string>
|
||||
<string name="game_folders">遊戲資料夾</string>
|
||||
<string name="deep_scan">深度掃描</string>
|
||||
<string name="add_game_folder">新增遊戲資料夾</string>
|
||||
<string name="folder_already_added">這個資料夾已經新增過了!</string>
|
||||
<string name="game_folder_properties">遊戲資料夾設定</string>
|
||||
<plurals name="saves_import_failed">
|
||||
<item quantity="other">%d 個存檔匯入失敗</item>
|
||||
<item quantity="other">%d 個存檔導入失敗</item>
|
||||
</plurals>
|
||||
<plurals name="saves_import_success">
|
||||
<item quantity="other">成功匯入 %d 個存檔</item>
|
||||
<item quantity="other">成功導入 %d 個存檔</item>
|
||||
</plurals>
|
||||
<string name="no_save_data_found">找不到儲存資料</string>
|
||||
<string name="verify_installed_content">驗證已安裝內容的完整性</string>
|
||||
@@ -578,9 +578,9 @@
|
||||
<string name="skip_cpu_inner_invalidation">跳過 CPU 內部失效處理</string>
|
||||
<string name="skip_cpu_inner_invalidation_description">在記憶體更新期間跳過某些 CPU 端快取的失效處理,減少 CPU 使用率並提高其性能。可能會導致某些遊戲出現故障或當機</string>
|
||||
<string name="nce_invalidation_gpu_readback">在 NCE 無效化前進行 GPU 回讀</string>
|
||||
<string name="nce_invalidation_gpu_readback_description">(暫時性功能)修復《終結者瑪格諾莉亞:霧中綻放》及其它使用虛幻引擎之遊戲中的當機問題,可能會降低效能。此功能正在改善中</string>
|
||||
<string name="nce_invalidation_gpu_readback_description">(暫時性功能)修復 \"終結者瑪格諾莉亞:霧中綻放\" 及其它使用虛幻引擎之遊戲中的當機問題,可能會降低效能。此功能正在改善中</string>
|
||||
<string name="fix_bloom_effects">修正光暈特效</string>
|
||||
<string name="fix_bloom_effects_description">在《薩爾達傳說:織夢島》和《薩爾達傳說:智慧的再現》中減少光暈模糊的問題並移除《橫衝直撞:狂飆樂園重製版》中的光暈效果(適用於 Adreno A6XX - A7XX/ Turnip)。警告:可能會在其它遊戲中導致圖形異常</string>
|
||||
<string name="fix_bloom_effects_description">在 \"薩爾達傳說:織夢島\" 和 \"薩爾達傳說:智慧的再現\" 中減少光暈模糊的問題並移除 \"橫衝直撞:狂飆樂園重製版\" 中的光暈效果(適用於 Adreno A6XX - A7XX/ Turnip)。警告:可能會在其它遊戲中導致圖形異常</string>
|
||||
<string name="emulate_bgr565">模擬 BGR565</string>
|
||||
<string name="emulate_bgr565_description">修正遊戲中出現的色彩反轉、圖形異常或陰影異常</string>
|
||||
<string name="rescale_hack">啟用舊版重新縮放處理</string>
|
||||
@@ -603,7 +603,7 @@
|
||||
<string name="extensions">擴充功能</string>
|
||||
|
||||
<string name="dyna_state">擴展動態狀態</string>
|
||||
<string name="dyna_state_description">控制 Vulkan 擴展動態狀態中可使用的功能數量,較高的數值可根據驅動程式支援的動態狀態減少管線編譯的次數</string>
|
||||
<string name="dyna_state_description">控制擴展動態狀態中可使用的功能數量,較高的數值可根據驅動程式支援的動態狀態減少管線編譯的次數</string>
|
||||
<string name="disabled">停用</string>
|
||||
<string name="vertex_input_dynamic_state">頂點輸入動態狀態</string>
|
||||
<string name="vertex_input_dynamic_state_description">啟用此功能可提供更彈性的頂點輸入處理方式,並可能減少頂點/緩衝區的管線編譯時間</string>
|
||||
@@ -808,7 +808,7 @@
|
||||
<string name="recommended_driver">推薦驅動程式:</string>
|
||||
<string name="gpu_model">GPU 型號</string>
|
||||
<string name="unsupported_gpu">不支援的 GPU</string>
|
||||
<string name="unsupported_gpu_warning">您的 GPU 不支援驅動程式注入,不建議嘗試安裝自訂驅動程式</string>
|
||||
<string name="unsupported_gpu_warning">您的 GPU 不支援驅動程式注入,不建議嘗試設定自訂驅動程式</string>
|
||||
|
||||
<!-- Preferences Screen -->
|
||||
<string name="preferences_settings">設定</string>
|
||||
@@ -892,7 +892,7 @@
|
||||
<string name="select_content_type">內容類型</string>
|
||||
<string name="updates_and_dlc">更新及 DLC</string>
|
||||
<string name="mods_and_cheats">模組及金手指</string>
|
||||
<string name="addon_notice">注意!</string>
|
||||
<string name="addon_notice">重要的˙附加元件通知</string>
|
||||
<!-- \"cheats/" "romfs/" and \"exefs/ should not be translated -->
|
||||
<string name="addon_notice_description">若要安裝模組及金手指,您必須選擇一個包含 cheats/、romfs/ 或 exefs/ 的目錄。我們無法驗證這些內容是否與您的遊戲相容,所以請謹慎安裝!</string>
|
||||
<string name="invalid_directory">無效的目錄</string>
|
||||
@@ -981,7 +981,7 @@
|
||||
<string name="emulation_pause">暫停模擬</string>
|
||||
<string name="emulation_unpause">繼續模擬</string>
|
||||
<string name="emulation_input_overlay">疊加層/虛擬按鍵設定</string>
|
||||
<string name="load_amiibo">匯入 Amiibo</string>
|
||||
<string name="load_amiibo">導入 Amiibo</string>
|
||||
<string name="touchscreen">觸控螢幕</string>
|
||||
<string name="lock_drawer">鎖定側邊選單</string>
|
||||
<string name="unlock_drawer">解鎖側邊選單</string>
|
||||
@@ -1128,16 +1128,6 @@
|
||||
<string name="freedreno_clear_all">全部清除</string>
|
||||
<string name="freedreno_saved">Freedreno 設定已儲存</string>
|
||||
<string name="freedreno_cleared_all">已清除所有 Freedreno 變數</string>
|
||||
<string name="freedreno_variable_added">變數 %1$s 已新增</string>
|
||||
<string name="freedreno_preset_applied">設定檔 \"%1$s\" 已套用</string>
|
||||
<string name="freedreno_error_empty_name">變數名稱不可空白</string>
|
||||
<string name="freedreno_error_setting_variable">變數設定失敗</string>
|
||||
<string name="freedreno_info_title">關於 Freedreno 設定</string>
|
||||
<string name="freedreno_info_description">透過調整 Freedreno/Turnip GPU 驅動程式的設定來偵錯、分析和最佳化效能,會自動儲存變更。詳情請參閱 https://docs.mesa3d.org/drivers/freedreno.html</string>
|
||||
<string name="freedreno_per_game_title">Freedreno 設定</string>
|
||||
<string name="freedreno_per_game_description">調整此遊戲的 GPU 驅動程式設定</string>
|
||||
<string name="freedreno_per_game_saved">Freedreno 設定已儲存</string>
|
||||
|
||||
<!-- Gamepad Buttons -->
|
||||
<string name="gamepad_d_pad">方向鍵</string>
|
||||
<string name="gamepad_left_stick">左搖桿</string>
|
||||
@@ -1146,17 +1136,14 @@
|
||||
<string name="gamepad_screenshot">螢幕截圖</string>
|
||||
|
||||
<!-- Disk shader cache -->
|
||||
<string name="building_shaders">正在編譯著色器</string>
|
||||
<string name="building_shaders">正在建置著色器</string>
|
||||
|
||||
<!-- App options -->
|
||||
<string name="change_app_theme">變更應用程式主題</string>
|
||||
<string name="theme_default">預設</string>
|
||||
<string name="theme_material_you">Material You</string>
|
||||
<string name="app_settings">Eden 介面設定</string>
|
||||
<string name="theme_material_you">質感設計3</string>
|
||||
<string name="app_settings">Eden介面設定</string>
|
||||
<string name="theme_and_color">主題和顏色</string>
|
||||
<string name="fullscreen_mode">全螢幕模式</string>
|
||||
<string name="fullscreen_mode_description">在使用 Eden 時隱藏 Android 系統的導覽列,在直向時從螢幕底部往上滑以暫時顯示它們;橫向時則從螢幕側邊往內滑動</string>
|
||||
|
||||
<!-- Theme Modes -->
|
||||
<string name="change_theme_mode">變更主題模式</string>
|
||||
<string name="theme_mode_follow_system">跟隨系統</string>
|
||||
@@ -1165,26 +1152,18 @@
|
||||
|
||||
<!-- Black backgrounds theme -->
|
||||
<string name="use_black_backgrounds">黑色背景</string>
|
||||
<string name="use_black_backgrounds_description">使用深色主題時套用黑色背景</string>
|
||||
|
||||
<!-- Buttons -->
|
||||
<string name="enable_folder_button">遊戲資料夾</string>
|
||||
<string name="enable_folder_button_description">在 Eden 首頁顯示新增遊戲資料夾的按鈕</string>
|
||||
<string name="enable_qlaunch_button">QLaunch</string>
|
||||
<string name="enable_qlaunch_button_description">在 Eden 首頁顯示 QLaunch 按鈕</string>
|
||||
<string name="use_black_backgrounds_description">使用深色主題時,套用黑色背景。</string>
|
||||
|
||||
<!-- App Language -->
|
||||
<string name="app_language">語言</string>
|
||||
<string name="app_language_description">選擇 Eden 顯示的語言</string>
|
||||
<string name="app_language_description">選擇Eden顯示的語言</string>
|
||||
<string name="app_language_system">跟隨系統</string>
|
||||
<!-- Static Themes -->
|
||||
<string name="static_theme_color">主題色彩</string>
|
||||
<string name="eden_theme">Eden</string>
|
||||
<string name="violet">紫色</string>
|
||||
<string name="blue">藍色</string>
|
||||
<string name="cyan">青色</string>
|
||||
<string name="red">紅色</string>
|
||||
<string name="green">綠色</string>
|
||||
<string name="yellow">黃色</string>
|
||||
<string name="orange">橘色</string>
|
||||
<string name="pink">粉紅色</string>
|
||||
@@ -1206,28 +1185,14 @@
|
||||
<string name="applets_menu_description">更改小程式前端和設定</string>
|
||||
|
||||
<string name="applet_hle">自訂前端</string>
|
||||
<string name="applet_lle">Switch 小程式</string>
|
||||
<string name="applet_lle">Switch小程式</string>
|
||||
|
||||
<string name="swkbd_applet">軟體鍵盤</string>
|
||||
|
||||
<string name="airplane_mode">飛航模式</string>
|
||||
<string name="airplane_mode_description">將飛航模式傳遞給 Switch 作業系統</string>
|
||||
<string name="airplane_mode_description">將飛航模式傳遞給Switch作業系統</string>
|
||||
|
||||
<string name="enable_overlay">啟用小程式虛擬按鍵</string>
|
||||
<string name="enable_overlay_description">啟用 Horizon OS 內建的小程式虛擬按鍵,長按 Home 鍵 1 秒以顯示虛擬按鍵</string>
|
||||
|
||||
<!-- Profile Management -->
|
||||
<string name="profile_manager">使用者管理</string>
|
||||
<string name="profile_manager_description">更改使用者名稱、圖片及新增或刪除使用者</string>
|
||||
<string name="profile_add_user">新增使用者</string>
|
||||
<string name="profile_new_user">新增使用者</string>
|
||||
<string name="profile_edit_user">編輯使用者資料</string>
|
||||
<string name="profile_edit">編輯</string>
|
||||
<string name="profile_delete">刪除</string>
|
||||
<string name="profile_username">使用者名稱</string>
|
||||
<string name="profile_uuid">使用者 ID (通用唯一辨識碼)</string>
|
||||
<string name="profile_uuid_description">這是此使用者設定檔的唯一識別碼,建立後就無法進行更改</string>
|
||||
<!-- Licenses screen strings -->
|
||||
<string name="licenses">授權</string>
|
||||
<string name="license_fidelityfx_fsr_description">來自 AMD 的升級圖像品質</string>
|
||||
</resources>
|
||||
</resources>
|
||||
@@ -1,3 +1,6 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
@@ -46,4 +49,10 @@ std::vector<u8> DecompressDataZSTD(std::span<const u8> compressed) {
|
||||
return decompressed;
|
||||
}
|
||||
|
||||
std::size_t DecompressDataZSTD(std::span<const u8> compressed, std::span<u8> output) {
|
||||
const std::size_t result =
|
||||
ZSTD_decompress(output.data(), output.size(), compressed.data(), compressed.size());
|
||||
return ZSTD_isError(result) ? 0 : result;
|
||||
}
|
||||
|
||||
} // namespace Common::Compression
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
@@ -42,4 +45,15 @@ namespace Common::Compression {
|
||||
*/
|
||||
[[nodiscard]] std::vector<u8> DecompressDataZSTD(std::span<const u8> compressed);
|
||||
|
||||
/**
|
||||
* Decompresses a single Zstandard frame into a caller-provided buffer of known size.
|
||||
*
|
||||
* @param compressed the compressed source memory region.
|
||||
* @param output the destination buffer. Its size is the maximum number of bytes to write.
|
||||
*
|
||||
* @return the number of bytes written, or 0 if the frame is invalid or doesn't fit. An empty
|
||||
* frame also returns 0.
|
||||
*/
|
||||
[[nodiscard]] std::size_t DecompressDataZSTD(std::span<const u8> compressed, std::span<u8> output);
|
||||
|
||||
} // namespace Common::Compression
|
||||
|
||||
@@ -33,6 +33,8 @@ add_library(core STATIC
|
||||
crypto/encryption_layer.h
|
||||
crypto/key_manager.cpp
|
||||
crypto/key_manager.h
|
||||
crypto/ncz_decompressor.cpp
|
||||
crypto/ncz_decompressor.h
|
||||
crypto/partition_data_manager.cpp
|
||||
crypto/partition_data_manager.h
|
||||
crypto/xts_encryption_layer.cpp
|
||||
|
||||
@@ -0,0 +1,430 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <zstd.h>
|
||||
|
||||
#include "common/common_types.h"
|
||||
#include "common/logging.h"
|
||||
#include "common/zstd_compression.h"
|
||||
#include "core/crypto/aes_util.h"
|
||||
#include "core/crypto/key_manager.h"
|
||||
#include "core/crypto/ncz_decompressor.h"
|
||||
#include "core/file_sys/vfs/vfs.h"
|
||||
|
||||
namespace Core::Crypto {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr u64 HeaderSize = 0x4000;
|
||||
constexpr std::size_t SectionWire = 0x40;
|
||||
constexpr std::size_t BlockFixed = 0x18;
|
||||
constexpr std::size_t Chunk = 0x100000;
|
||||
// Upper bound on NCZ section entries (64 bytes each, so the table is at most 4 MiB). A base NCA
|
||||
// has a handful of sections, but nsz stores an update's patch (BKTR) data one entry per
|
||||
// subsection, which is thousands of them.
|
||||
constexpr u64 MaxSectionCount = 0x10000;
|
||||
constexpr std::array<u8, 8> SectionsMagic{'N', 'C', 'Z', 'S', 'E', 'C', 'T', 'N'};
|
||||
constexpr std::array<u8, 8> BlockMagic{'N', 'C', 'Z', 'B', 'L', 'O', 'C', 'K'};
|
||||
|
||||
struct Section {
|
||||
s64 offset, size;
|
||||
u64 crypto_type;
|
||||
Key128 key, counter;
|
||||
bool Ctr() const { return crypto_type == 3 || crypto_type == 4; }
|
||||
};
|
||||
|
||||
struct BlockHeader {
|
||||
u32 exponent;
|
||||
u64 decompressed_size;
|
||||
std::vector<u32> sizes;
|
||||
};
|
||||
|
||||
u64 Le(const u8* p, std::size_t n) {
|
||||
u64 v = 0;
|
||||
for (std::size_t i = n; i-- > 0;) v = (v << 8) | p[i];
|
||||
return v;
|
||||
}
|
||||
|
||||
// Mirrors nsz AESCTR.seek(): upper 8 bytes = stored counter, lower 8 = absolute NCA offset / 16.
|
||||
void SetIv(Key128& iv, const Key128& base, u64 off) {
|
||||
iv = base;
|
||||
u64 blk = off >> 4;
|
||||
for (std::size_t i = 0; i < 8; ++i) { iv[15 - i] = static_cast<u8>(blk & 0xFF); blk >>= 8; }
|
||||
}
|
||||
|
||||
// Parse header + section table + optional block header. Returns payload_start.
|
||||
std::optional<std::pair<std::vector<Section>, std::optional<BlockHeader>>> ParseHeader(
|
||||
const FileSys::VirtualFile& f, u64& payload_start) {
|
||||
const u64 fsize = f->GetSize();
|
||||
if (fsize < HeaderSize + 16) return std::nullopt;
|
||||
|
||||
const auto head = f->ReadBytes(16, HeaderSize);
|
||||
if (head.size() != 16 || !std::equal(head.begin(), head.begin() + 8, SectionsMagic.begin()))
|
||||
return std::nullopt;
|
||||
const u64 count = Le(head.data() + 8, 8);
|
||||
if (count == 0 || count > MaxSectionCount || count * SectionWire > fsize - HeaderSize - 16)
|
||||
return std::nullopt;
|
||||
|
||||
const auto table = f->ReadBytes(count * SectionWire, HeaderSize + 16);
|
||||
if (table.size() != count * SectionWire) return std::nullopt;
|
||||
|
||||
std::vector<Section> sections;
|
||||
sections.reserve(count + 1);
|
||||
for (u64 i = 0; i < count; ++i) {
|
||||
const u8* e = table.data() + i * SectionWire;
|
||||
Section s{};
|
||||
s.offset = static_cast<s64>(Le(e, 8));
|
||||
s.size = static_cast<s64>(Le(e + 8, 8));
|
||||
s.crypto_type = Le(e + 16, 8);
|
||||
std::memcpy(s.key.data(), e + 32, 16);
|
||||
std::memcpy(s.counter.data(), e + 48, 16);
|
||||
sections.push_back(s);
|
||||
}
|
||||
|
||||
const u64 block_start = HeaderSize + 16 + count * SectionWire;
|
||||
const auto fixed = f->ReadBytes(BlockFixed, block_start);
|
||||
std::optional<BlockHeader> bh;
|
||||
if (fixed.size() == BlockFixed &&
|
||||
std::equal(fixed.begin(), fixed.begin() + 8, BlockMagic.begin())) {
|
||||
BlockHeader b{};
|
||||
b.exponent = fixed[11];
|
||||
const s32 n = static_cast<s32>(Le(fixed.data() + 12, 4));
|
||||
if (n <= 0) return std::nullopt;
|
||||
b.decompressed_size = Le(fixed.data() + 16, 8);
|
||||
const auto sizes = f->ReadBytes(static_cast<u64>(n) * 4, block_start + BlockFixed);
|
||||
if (sizes.size() != static_cast<u64>(n) * 4) return std::nullopt;
|
||||
b.sizes.resize(n);
|
||||
for (s32 i = 0; i < n; ++i) b.sizes[i] = static_cast<u32>(Le(sizes.data() + i * 4, 4));
|
||||
payload_start = block_start + BlockFixed + static_cast<u64>(n) * 4;
|
||||
bh = std::move(b);
|
||||
} else {
|
||||
payload_start = block_start;
|
||||
}
|
||||
|
||||
if (!sections.empty() && sections[0].offset > static_cast<s64>(HeaderSize)) {
|
||||
Section fake{};
|
||||
fake.offset = static_cast<s64>(HeaderSize);
|
||||
fake.size = sections[0].offset - static_cast<s64>(HeaderSize);
|
||||
sections.insert(sections.begin(), fake);
|
||||
}
|
||||
return std::make_pair(std::move(sections), std::move(bh));
|
||||
}
|
||||
|
||||
// Sequential reader over independently-compressed blocks, one block cached (like nsz_rs).
|
||||
class BlockReader {
|
||||
public:
|
||||
BlockReader(FileSys::VirtualFile f, const BlockHeader& bh, u64 payload_start)
|
||||
: source{std::move(f)}, block_size{1ULL << bh.exponent}, sizes{bh.sizes},
|
||||
decompressed_size{bh.decompressed_size} {
|
||||
if (bh.exponent < 14 || bh.exponent > 32 || sizes.empty())
|
||||
throw std::runtime_error("bad NCZBLOCK header");
|
||||
u64 off = payload_start;
|
||||
offsets.reserve(sizes.size());
|
||||
for (u32 s : sizes) { offsets.push_back(off); off += s; }
|
||||
}
|
||||
|
||||
// Fills `out` from `position`; returns bytes written.
|
||||
std::size_t ReadAt(u64 position, std::span<u8> out) {
|
||||
std::size_t done = 0;
|
||||
while (done < out.size() && position < decompressed_size) {
|
||||
const u64 id = position / block_size;
|
||||
if (id >= sizes.size()) break;
|
||||
if (cache_id != id && !Load(id)) break;
|
||||
const u64 in = position - id * block_size;
|
||||
const std::size_t n = static_cast<std::size_t>(
|
||||
std::min<u64>(out.size() - done, cache.size() - in));
|
||||
std::memcpy(out.data() + done, cache.data() + in, n);
|
||||
done += n;
|
||||
position += n;
|
||||
}
|
||||
return done;
|
||||
}
|
||||
|
||||
private:
|
||||
bool Load(u64 id) {
|
||||
u64 want = block_size;
|
||||
if (id + 1 == sizes.size()) {
|
||||
const u64 rem = decompressed_size % block_size;
|
||||
if (rem > 0) want = rem; // avoid the 0-size bug from the original nsz
|
||||
}
|
||||
try { cache.resize(static_cast<std::size_t>(want)); }
|
||||
catch (const std::bad_alloc&) { return false; }
|
||||
const u32 csize = sizes[id];
|
||||
if (csize < want) {
|
||||
const auto comp = source->ReadBytes(csize, offsets[id]);
|
||||
if (comp.size() != csize ||
|
||||
Common::Compression::DecompressDataZSTD(comp, cache) != want) return false;
|
||||
} else if (source->Read(cache.data(), cache.size(), offsets[id]) != cache.size()) {
|
||||
return false;
|
||||
}
|
||||
cache_id = id;
|
||||
return true;
|
||||
}
|
||||
|
||||
FileSys::VirtualFile source;
|
||||
u64 block_size;
|
||||
std::vector<u64> offsets;
|
||||
std::vector<u32> sizes;
|
||||
u64 decompressed_size;
|
||||
u64 cache_id = ~0ULL;
|
||||
std::vector<u8> cache;
|
||||
};
|
||||
|
||||
class BlockNczFile final : public FileSys::VfsFile {
|
||||
public:
|
||||
BlockNczFile(std::string name_, std::vector<u8> hdr_, std::vector<Section> sections_,
|
||||
std::unique_ptr<BlockReader> body_)
|
||||
: name{std::move(name_)}, hdr{std::move(hdr_)}, sections{std::move(sections_)},
|
||||
body{std::move(body_)} {
|
||||
nca_size = HeaderSize;
|
||||
for (const auto& s : sections)
|
||||
nca_size = std::max<u64>(nca_size, static_cast<u64>(s.offset + s.size));
|
||||
ciphers.reserve(sections.size());
|
||||
for (const auto& s : sections)
|
||||
ciphers.push_back(s.Ctr() ? std::make_unique<AESCipher<Key128>>(s.key, Mode::CTR)
|
||||
: nullptr);
|
||||
}
|
||||
|
||||
std::string GetName() const override { return name; }
|
||||
std::size_t GetSize() const override { return static_cast<std::size_t>(nca_size); }
|
||||
bool Resize(std::size_t) override { return false; }
|
||||
FileSys::VirtualDir GetContainingDirectory() const override { return nullptr; }
|
||||
bool IsWritable() const override { return false; }
|
||||
bool IsReadable() const override { return true; }
|
||||
|
||||
std::size_t Read(u8* data, std::size_t len, std::size_t off) const override {
|
||||
if (off >= nca_size) return 0;
|
||||
len = static_cast<std::size_t>(std::min<u64>(len, nca_size - off));
|
||||
const u64 end = off + len;
|
||||
u64 pos = off;
|
||||
if (pos < HeaderSize) {
|
||||
const u64 n = std::min<u64>(end, HeaderSize) - pos;
|
||||
std::memcpy(data, hdr.data() + pos, static_cast<std::size_t>(n));
|
||||
pos += n;
|
||||
}
|
||||
std::scoped_lock lock{mutex};
|
||||
for (std::size_t i = 0; i < sections.size() && pos < end; ++i) {
|
||||
const auto& s = sections[i];
|
||||
const u64 begin = std::max<u64>(static_cast<u64>(s.offset), HeaderSize);
|
||||
const u64 stop = std::max<u64>(static_cast<u64>(s.offset + s.size), begin);
|
||||
if (stop <= pos) continue;
|
||||
if (pos < begin) {
|
||||
const u64 g = std::min(end, begin);
|
||||
std::memset(data + (pos - off), 0, static_cast<std::size_t>(g - pos));
|
||||
pos = g;
|
||||
if (pos >= end) break;
|
||||
}
|
||||
const u64 pe = std::min(end, stop);
|
||||
const std::span<u8> piece(data + (pos - off), static_cast<std::size_t>(pe - pos));
|
||||
if (body->ReadAt(pos - HeaderSize, piece) != piece.size())
|
||||
return static_cast<std::size_t>(pos - off);
|
||||
if (ciphers[i]) {
|
||||
Key128 iv{};
|
||||
SetIv(iv, s.counter, pos);
|
||||
ciphers[i]->SetIV(iv);
|
||||
ciphers[i]->Transcode(piece.data(), piece.size(), piece.data(), Op::Encrypt);
|
||||
}
|
||||
pos = pe;
|
||||
}
|
||||
return len;
|
||||
}
|
||||
|
||||
std::size_t Write(const u8*, std::size_t, std::size_t) override { return 0; }
|
||||
bool Rename(std::string_view) override { return false; }
|
||||
|
||||
private:
|
||||
const std::string name;
|
||||
const std::vector<u8> hdr;
|
||||
const std::vector<Section> sections;
|
||||
const std::unique_ptr<BlockReader> body;
|
||||
std::vector<std::unique_ptr<AESCipher<Key128>>> ciphers;
|
||||
mutable std::mutex mutex;
|
||||
u64 nca_size{};
|
||||
};
|
||||
|
||||
// Solid NCZ: one zstd stream over the whole body. Decoded incrementally into memory, up to the
|
||||
// requested offset, so sequential install reads are O(n) instead of O(n^2).
|
||||
class SolidNczFile final : public FileSys::VfsFile {
|
||||
public:
|
||||
SolidNczFile(std::string name_, FileSys::VirtualFile source_,
|
||||
std::vector<Section> sections_, u64 payload_start_)
|
||||
: name{std::move(name_)}, source{std::move(source_)}, sections{std::move(sections_)},
|
||||
payload_start{payload_start_} {
|
||||
const auto raw_header = source->ReadBytes(HeaderSize, 0);
|
||||
data.assign(raw_header.begin(), raw_header.end());
|
||||
frontier = HeaderSize;
|
||||
}
|
||||
|
||||
~SolidNczFile() override {
|
||||
if (stream != nullptr) ZSTD_freeDStream(stream);
|
||||
}
|
||||
|
||||
std::string GetName() const override { return name; }
|
||||
std::size_t GetSize() const override {
|
||||
u64 size = HeaderSize;
|
||||
for (const auto& s : sections)
|
||||
size = std::max<u64>(size, static_cast<u64>(s.offset + s.size));
|
||||
return static_cast<std::size_t>(size);
|
||||
}
|
||||
bool Resize(std::size_t) override { return false; }
|
||||
FileSys::VirtualDir GetContainingDirectory() const override { return nullptr; }
|
||||
bool IsWritable() const override { return false; }
|
||||
bool IsReadable() const override { return true; }
|
||||
|
||||
std::size_t Read(u8* out, std::size_t len, std::size_t off) const override {
|
||||
std::scoped_lock lock{mutex};
|
||||
const u64 nca_size = GetSize();
|
||||
if (off >= nca_size) return 0;
|
||||
len = static_cast<std::size_t>(std::min<u64>(len, nca_size - off));
|
||||
const u64 needed = off + len;
|
||||
if (needed > frontier && !DecodeTo(needed)) return 0;
|
||||
std::memcpy(out, data.data() + off, len);
|
||||
return len;
|
||||
}
|
||||
|
||||
std::size_t Write(const u8*, std::size_t, std::size_t) override { return 0; }
|
||||
bool Rename(std::string_view) override { return false; }
|
||||
|
||||
private:
|
||||
bool DecodeTo(u64 needed) const {
|
||||
if (stream == nullptr) {
|
||||
stream = ZSTD_createDStream();
|
||||
if (stream == nullptr) return false;
|
||||
ZSTD_DCtx_setParameter(stream, ZSTD_d_windowLogMax, 31);
|
||||
read_pos = payload_start;
|
||||
}
|
||||
|
||||
const u64 file_size = source->GetSize();
|
||||
std::vector<u8> out_buf(Chunk);
|
||||
|
||||
std::size_t index = 0;
|
||||
for (; index < sections.size(); ++index) {
|
||||
const auto& s = sections[index];
|
||||
const u64 begin = std::max<u64>(static_cast<u64>(s.offset), HeaderSize);
|
||||
const u64 end = std::max<u64>(static_cast<u64>(s.offset) + static_cast<u64>(s.size),
|
||||
begin);
|
||||
if (end > frontier) break;
|
||||
}
|
||||
|
||||
for (; index < sections.size() && frontier < needed; ++index) {
|
||||
const auto& s = sections[index];
|
||||
const u64 begin = std::max<u64>(static_cast<u64>(s.offset), HeaderSize);
|
||||
const u64 end = std::max<u64>(static_cast<u64>(s.offset) + static_cast<u64>(s.size),
|
||||
begin);
|
||||
if (end <= frontier) continue;
|
||||
u64 pos = std::max<u64>(begin, frontier);
|
||||
|
||||
std::optional<AESCipher<Key128>> cipher;
|
||||
if (s.Ctr()) cipher.emplace(s.key, Mode::CTR);
|
||||
|
||||
while (pos < end && frontier < needed) {
|
||||
const u64 want = std::min<u64>(end - pos, out_buf.size());
|
||||
std::size_t produced = 0;
|
||||
while (produced < want) {
|
||||
if (in.pos >= in.size) {
|
||||
const u64 remaining = file_size - read_pos;
|
||||
const std::size_t chunk = static_cast<std::size_t>(
|
||||
std::min<u64>(Chunk, remaining));
|
||||
if (chunk == 0) { LOG_ERROR(Crypto, "Solid NCZ: truncated"); return false; }
|
||||
const auto bytes = source->ReadBytes(chunk, read_pos);
|
||||
if (bytes.empty()) { LOG_ERROR(Crypto, "Solid NCZ: truncated"); return false; }
|
||||
read_pos += bytes.size();
|
||||
in_buf.assign(bytes.begin(), bytes.end());
|
||||
in.src = in_buf.data();
|
||||
in.size = in_buf.size();
|
||||
in.pos = 0;
|
||||
}
|
||||
ZSTD_outBuffer outb{out_buf.data() + produced, want - produced, 0};
|
||||
const std::size_t res = ZSTD_decompressStream(stream, &outb, &in);
|
||||
if (ZSTD_isError(res)) {
|
||||
LOG_ERROR(Crypto, "Solid NCZ: zstd error");
|
||||
return false;
|
||||
}
|
||||
produced += outb.pos;
|
||||
}
|
||||
if (cipher) {
|
||||
Key128 iv{};
|
||||
SetIv(iv, s.counter, pos);
|
||||
cipher->SetIV(iv);
|
||||
cipher->Transcode(out_buf.data(), produced, out_buf.data(), Op::Encrypt);
|
||||
}
|
||||
data.insert(data.end(), out_buf.begin(), out_buf.begin() + produced);
|
||||
pos += produced;
|
||||
frontier = pos;
|
||||
}
|
||||
}
|
||||
return frontier >= needed;
|
||||
}
|
||||
|
||||
const std::string name;
|
||||
const FileSys::VirtualFile source;
|
||||
const std::vector<Section> sections;
|
||||
const u64 payload_start;
|
||||
mutable std::mutex mutex;
|
||||
mutable std::vector<u8> data;
|
||||
mutable ZSTD_DStream* stream = nullptr;
|
||||
mutable u64 read_pos = 0;
|
||||
mutable u64 frontier = 0;
|
||||
mutable std::vector<u8> in_buf;
|
||||
mutable ZSTD_inBuffer in{nullptr, 0, 0};
|
||||
};
|
||||
|
||||
// Shared opener: block path always; solid only when allow_solid is true.
|
||||
FileSys::VirtualFile OpenNcz(const FileSys::VirtualFile& f, std::string_view name,
|
||||
bool allow_solid) {
|
||||
if (!f) return nullptr;
|
||||
u64 ps = 0;
|
||||
auto parsed = ParseHeader(f, ps);
|
||||
if (!parsed) return nullptr;
|
||||
auto& [sections, block] = *parsed;
|
||||
|
||||
if (block) {
|
||||
auto hdr = f->ReadBytes(HeaderSize, 0);
|
||||
if (hdr.size() != HeaderSize) return nullptr;
|
||||
std::unique_ptr<BlockReader> body;
|
||||
try { body = std::make_unique<BlockReader>(f, *block, ps); }
|
||||
catch (const std::exception& e) {
|
||||
LOG_ERROR(Crypto, "Failed to open NCZBLOCK: {}", e.what());
|
||||
return nullptr;
|
||||
}
|
||||
return std::make_shared<BlockNczFile>(std::string{name}, std::move(hdr),
|
||||
std::move(sections), std::move(body));
|
||||
}
|
||||
|
||||
if (allow_solid) {
|
||||
return std::make_shared<SolidNczFile>(std::string{name}, f, std::move(sections), ps);
|
||||
}
|
||||
|
||||
LOG_DEBUG(Crypto, "Solid NCZ {} skipped (only NCZBLOCK is supported for play)", name);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
} // Anonymous namespace
|
||||
|
||||
bool IsBlockCompressedNCZ(const FileSys::VirtualFile& f) {
|
||||
if (!f) return false;
|
||||
u64 ps = 0;
|
||||
const auto p = ParseHeader(f, ps);
|
||||
return p && p->second.has_value();
|
||||
}
|
||||
|
||||
FileSys::VirtualFile DecompressNCZ(const FileSys::VirtualFile& f, std::string_view name) {
|
||||
return OpenNcz(f, name, /*allow_solid=*/false);
|
||||
}
|
||||
|
||||
FileSys::VirtualFile DecompressNCZForInstall(const FileSys::VirtualFile& f,
|
||||
std::string_view name) {
|
||||
return OpenNcz(f, name, /*allow_solid=*/true);
|
||||
}
|
||||
|
||||
} // namespace Core::Crypto
|
||||
@@ -0,0 +1,35 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string_view>
|
||||
|
||||
#include "core/file_sys/vfs/vfs_types.h"
|
||||
|
||||
namespace Core::Crypto {
|
||||
|
||||
/**
|
||||
* Opens an NCZBLOCK-compressed NCA (nsz -B / .xcz block format) as a VirtualFile backed by
|
||||
* on-demand block decoding. Nothing is written to disk; each read decodes only the blocks it
|
||||
* covers and re-encrypts CTR sections. Solid NCZ is not supported.
|
||||
*
|
||||
* @param ncz_file The raw NCZ payload (e.g. "<id>.ncz" entry from an NSZ/XCZ PFS0).
|
||||
* @param output_name Name to give the returned VirtualFile (e.g. "<id>.nca").
|
||||
*
|
||||
* @return a VirtualFile for the NCA, or nullptr if the NCZ is malformed or not NCZBLOCK.
|
||||
*/
|
||||
[[nodiscard]] FileSys::VirtualFile DecompressNCZ(const FileSys::VirtualFile& ncz_file,
|
||||
std::string_view output_name);
|
||||
|
||||
/**
|
||||
* Returns true if the given file is an NCZ payload in block-compressed (NCZBLOCK) form,
|
||||
* i.e. one that DecompressNCZ() can open. Solid NCZ returns false.
|
||||
*/
|
||||
[[nodiscard]] bool IsBlockCompressedNCZ(const FileSys::VirtualFile& ncz_file);
|
||||
|
||||
// Install to NAND: same as DecompressNCZ, but solid NCZ is decoded on demand too.
|
||||
[[nodiscard]] FileSys::VirtualFile DecompressNCZForInstall(
|
||||
const FileSys::VirtualFile& ncz_file, std::string_view output_name);
|
||||
|
||||
} // namespace Core::Crypto
|
||||
@@ -16,6 +16,7 @@
|
||||
#include "common/random.h"
|
||||
#include "common/string_util.h"
|
||||
#include "core/crypto/key_manager.h"
|
||||
#include "core/crypto/ncz_decompressor.h"
|
||||
#include "core/file_sys/card_image.h"
|
||||
#include "core/file_sys/common_funcs.h"
|
||||
#include "core/file_sys/content_archive.h"
|
||||
@@ -158,6 +159,25 @@ static std::shared_ptr<NSP> OpenContainerAsNsp(const VirtualFile& file, Loader::
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Checks that every .ncz entry in the container is NCZBLOCK. Solid NCZ has no random access,
|
||||
// and the whole point of this build is to reject it at scan time so such files never appear
|
||||
// in the game list. Containers without any .ncz (plain NSP/XCI) pass through.
|
||||
static bool ContainerIsFullyBlockCompressed(const std::shared_ptr<NSP>& nsp) {
|
||||
if (!nsp) {
|
||||
return false;
|
||||
}
|
||||
for (const auto& entry : nsp->GetFiles()) {
|
||||
const auto name = entry->GetName();
|
||||
if (name.size() < 4 || name.substr(name.size() - 4) != ".ncz") {
|
||||
continue;
|
||||
}
|
||||
if (!Core::Crypto::IsBlockCompressedNCZ(entry)) {
|
||||
return false; // solid NCZ -> reject the whole container
|
||||
}
|
||||
}
|
||||
return true; // no .ncz at all, or all block-compressed
|
||||
}
|
||||
|
||||
template <typename Callback>
|
||||
bool ForEachContainerEntry(const std::shared_ptr<NSP>& nsp, bool only_content,
|
||||
std::optional<u64> base_program_id, Callback&& on_entry) {
|
||||
@@ -811,7 +831,14 @@ std::vector<ContentProviderEntry> RegisteredCache::ListEntriesFilter(
|
||||
}
|
||||
|
||||
static std::shared_ptr<NCA> GetNCAFromNSPForID(const NSP& nsp, const NcaID& id) {
|
||||
auto file = nsp.GetFile(fmt::format("{}.nca", Common::HexToString(id, false)));
|
||||
const auto id_string = Common::HexToString(id, false);
|
||||
auto file = nsp.GetFile(fmt::format("{}.nca", id_string));
|
||||
if (file == nullptr) {
|
||||
if (auto ncz_file = nsp.GetFile(fmt::format("{}.ncz", id_string))) {
|
||||
file = Core::Crypto::DecompressNCZForInstall(
|
||||
ncz_file, fmt::format("{}.nca", id_string));
|
||||
}
|
||||
}
|
||||
if (file == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
@@ -1273,6 +1300,12 @@ bool ManualContentProvider::AddEntriesFromContainer(VirtualFile file, bool only_
|
||||
return false;
|
||||
}
|
||||
|
||||
// Reject solid NCZ (.nsz without -B): only block-compressed NCZ is supported.
|
||||
if (!ContainerIsFullyBlockCompressed(nsp)) {
|
||||
LOG_DEBUG(Service_FS, "Skipping solid-compressed container: {}", file->GetName());
|
||||
return false;
|
||||
}
|
||||
|
||||
return ForEachContainerEntry(
|
||||
nsp, only_content, base_program_id,
|
||||
[this](TitleType title_type, ContentRecordType content_type, u64 title_id,
|
||||
@@ -1416,9 +1449,9 @@ void ExternalContentProvider::ScanDirectory(const VirtualDir& dir) {
|
||||
|
||||
const auto extension = Common::ToLower(filename.substr(dot_pos + 1));
|
||||
|
||||
if (extension == "nsp") {
|
||||
if (extension == "nsp" || extension == "nsz") {
|
||||
ProcessNSP(file);
|
||||
} else if (extension == "xci") {
|
||||
} else if (extension == "xci" || extension == "xcz") {
|
||||
ProcessXCI(file);
|
||||
}
|
||||
}
|
||||
@@ -1434,6 +1467,12 @@ void ExternalContentProvider::ProcessNSP(const VirtualFile& file) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Reject solid NCZ (.nsz without -B): Eden only supports block-compressed NCZ.
|
||||
if (!ContainerIsFullyBlockCompressed(nsp)) {
|
||||
LOG_DEBUG(Service_FS, "Skipping solid-compressed container: {}", file->GetName());
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_DEBUG(Service_FS, "Processing NSP file: {}", file->GetName());
|
||||
AddExternalEntriesFromContainer(nsp, entries, versions, multi_version_entries);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include "common/hex_util.h"
|
||||
#include "common/logging.h"
|
||||
#include "core/crypto/key_manager.h"
|
||||
#include "core/crypto/ncz_decompressor.h"
|
||||
#include "core/file_sys/common_funcs.h"
|
||||
#include "core/file_sys/content_archive.h"
|
||||
#include "core/file_sys/nca_metadata.h"
|
||||
@@ -246,6 +247,17 @@ void NSP::ReadNCAs(const std::vector<VirtualFile>& files) {
|
||||
const auto id_string = Common::HexToString(rec.nca_id, false);
|
||||
auto next_file = pfs->GetFile(fmt::format("{}.nca", id_string));
|
||||
|
||||
if (next_file == nullptr) {
|
||||
auto ncz_file = pfs->GetFile(fmt::format("{}.ncz", id_string));
|
||||
if (ncz_file != nullptr) {
|
||||
next_file = Core::Crypto::DecompressNCZ(ncz_file, fmt::format("{}.nca", id_string));
|
||||
if (next_file == nullptr) {
|
||||
LOG_DEBUG(Service_FS, "Skipping solid or unsupported NCZ content {}.ncz", id_string);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (next_file == nullptr) {
|
||||
if (rec.type != ContentRecordType::DeltaFragment) {
|
||||
LOG_WARNING(Service_FS,
|
||||
|
||||
@@ -57,21 +57,8 @@ u32 HardwareComposer::ComposeLocked(f32* out_speed_scale, Display& display,
|
||||
// Set default speed limit to 100%.
|
||||
*out_speed_scale = 1.0f;
|
||||
|
||||
// If no layers are visible, skip the logic. Acquiring and holding buffers here would
|
||||
// consume the producer's queue without ever presenting.
|
||||
bool any_visible = false;
|
||||
for (auto& layer : display.stack.layers) {
|
||||
if (layer->visible) {
|
||||
any_visible = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!any_visible) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Wait for the previous composite, bounding the number of in-flight composites.
|
||||
nvdisp.WaitForComposite();
|
||||
this->ReleaseFramebuffersLocked(display);
|
||||
|
||||
// Determine the number of vsync periods to wait before composing again.
|
||||
std::optional<s32> swap_interval{};
|
||||
@@ -165,13 +152,6 @@ u32 HardwareComposer::ComposeLocked(f32* out_speed_scale, Display& display,
|
||||
// Advance by 1 frame (60 FPS compositing)
|
||||
m_frame_number += 1;
|
||||
|
||||
// Release framebuffers once the frame has been handed to the compositor. This happens
|
||||
// within the same vsync period as the acquire (release_frame_number is checked against
|
||||
// the advanced counter), so the producer gets its buffers back immediately instead of
|
||||
// losing a queue slot for a whole frame.
|
||||
// TODO: support release fences; releasing before the composite has finished can tear.
|
||||
this->ReleaseFramebuffersLocked(display);
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -82,12 +82,12 @@ std::shared_ptr<FileSys::NSP> OpenContainerAsNsp(FileSys::VirtualFile file, File
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (type == FileType::NSP) {
|
||||
if (type == FileType::NSP || type == FileType::NSZ) {
|
||||
auto nsp = std::make_shared<FileSys::NSP>(file, program_id, program_index);
|
||||
return nsp->GetStatus() == ResultStatus::Success ? nsp : nullptr;
|
||||
}
|
||||
|
||||
if (type == FileType::XCI) {
|
||||
if (type == FileType::XCI || type == FileType::XCZ) {
|
||||
FileSys::XCI xci{file, program_id, program_index};
|
||||
if (xci.GetStatus() != ResultStatus::Success) {
|
||||
return nullptr;
|
||||
@@ -144,7 +144,8 @@ FileType IdentifyFile(FileSys::VirtualFile file) {
|
||||
}
|
||||
|
||||
bool IsContainerType(FileType type) {
|
||||
return type == FileType::NSP || type == FileType::XCI;
|
||||
return type == FileType::NSP || type == FileType::NSZ || type == FileType::XCI ||
|
||||
type == FileType::XCZ;
|
||||
}
|
||||
|
||||
bool IsBootableGameContainer(FileSys::VirtualFile file, FileType type, u64 program_id,
|
||||
@@ -180,8 +181,12 @@ FileType GuessFromFilename(const std::string& name) {
|
||||
return FileType::NCA;
|
||||
else if (extension == "xci")
|
||||
return FileType::XCI;
|
||||
else if (extension == "xcz")
|
||||
return FileType::XCZ;
|
||||
else if (extension == "nsp")
|
||||
return FileType::NSP;
|
||||
else if (extension == "nsz")
|
||||
return FileType::NSZ;
|
||||
else if (extension == "kip")
|
||||
return FileType::KIP;
|
||||
return FileType::Unknown;
|
||||
@@ -197,10 +202,14 @@ std::string GetFileTypeString(FileType type) {
|
||||
return "NCA";
|
||||
case FileType::XCI:
|
||||
return "XCI";
|
||||
case FileType::XCZ:
|
||||
return "XCZ";
|
||||
case FileType::NAX:
|
||||
return "NAX";
|
||||
case FileType::NSP:
|
||||
return "NSP";
|
||||
case FileType::NSZ:
|
||||
return "NSZ";
|
||||
case FileType::KIP:
|
||||
return "KIP";
|
||||
case FileType::DeconstructedRomDirectory:
|
||||
@@ -322,6 +331,7 @@ static std::unique_ptr<AppLoader> GetFileLoader(Core::System& system, FileSys::V
|
||||
|
||||
// NX XCI (nX Card Image) file format.
|
||||
case FileType::XCI:
|
||||
case FileType::XCZ:
|
||||
return std::make_unique<AppLoader_XCI>(std::move(file), system.GetFileSystemController(),
|
||||
system.GetContentProvider(), program_id,
|
||||
program_index);
|
||||
@@ -332,6 +342,7 @@ static std::unique_ptr<AppLoader> GetFileLoader(Core::System& system, FileSys::V
|
||||
|
||||
// NX NSP (Nintendo Submission Package) file format
|
||||
case FileType::NSP:
|
||||
case FileType::NSZ:
|
||||
return std::make_unique<AppLoader_NSP>(std::move(file), system.GetFileSystemController(),
|
||||
system.GetContentProvider(), program_id,
|
||||
program_index);
|
||||
|
||||
@@ -42,7 +42,9 @@ enum class FileType {
|
||||
NRO,
|
||||
NCA,
|
||||
NSP,
|
||||
NSZ,
|
||||
XCI,
|
||||
XCZ,
|
||||
NAX,
|
||||
KIP,
|
||||
DeconstructedRomDirectory,
|
||||
|
||||
@@ -56,6 +56,11 @@ AppLoader_NSP::AppLoader_NSP(FileSys::VirtualFile file_,
|
||||
AppLoader_NSP::~AppLoader_NSP() = default;
|
||||
|
||||
FileType AppLoader_NSP::IdentifyType(const FileSys::VirtualFile& nsp_file) {
|
||||
// .nsz is NSP-compatible; return NSZ so the game list shows the right type.
|
||||
if (nsp_file->GetName().ends_with(".nsz")) {
|
||||
return FileType::NSZ;
|
||||
}
|
||||
|
||||
const FileSys::NSP nsp(nsp_file);
|
||||
|
||||
if (nsp.GetStatus() != ResultStatus::Success) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
|
||||
@@ -163,7 +163,9 @@ inline InstallResult InstallNSP(Core::System& system, FileSys::VfsFilesystem& vf
|
||||
|
||||
std::shared_ptr<FileSys::NSP> nsp;
|
||||
FileSys::VirtualFile file = vfs.OpenFile(filename, FileSys::OpenMode::Read);
|
||||
if (boost::to_lower_copy(file->GetName()).ends_with("nsp")) {
|
||||
const auto lower_name = boost::to_lower_copy(file->GetName());
|
||||
|
||||
if (lower_name.ends_with("nsp") || lower_name.ends_with("nsz")) {
|
||||
nsp = std::make_shared<FileSys::NSP>(file);
|
||||
if (nsp->IsExtractedType()) {
|
||||
return InstallResult::Failure;
|
||||
|
||||
@@ -83,20 +83,21 @@ void Joycons::ScanThread(std::stop_token stop_token) {
|
||||
constexpr u16 nintendo_vendor_id = 0x057e;
|
||||
Common::SetCurrentThreadName("JoyconScanThread");
|
||||
|
||||
auto last_ret = 0u;
|
||||
do {
|
||||
SDL_hid_device_info* devs = SDL_hid_enumerate(nintendo_vendor_id, 0x0);
|
||||
SDL_hid_device_info* cur_dev = devs;
|
||||
|
||||
while (cur_dev) {
|
||||
if (IsDeviceNew(cur_dev)) {
|
||||
LOG_DEBUG(Input, "Device Found,type : {:04X} {:04X}", cur_dev->vendor_id,
|
||||
cur_dev->product_id);
|
||||
RegisterNewDevice(cur_dev);
|
||||
auto ret = SDL_hid_device_change_count();
|
||||
if (last_ret == 0u || last_ret != ret) {
|
||||
last_ret = ret;
|
||||
SDL_hid_device_info* devs = SDL_hid_enumerate(nintendo_vendor_id, 0x0);
|
||||
for (auto* cur_dev = devs; cur_dev; cur_dev = cur_dev->next) {
|
||||
if (IsDeviceNew(cur_dev)) {
|
||||
LOG_DEBUG(Input, "Device Found,type : {:04X} {:04X}", cur_dev->vendor_id, cur_dev->product_id);
|
||||
RegisterNewDevice(cur_dev);
|
||||
}
|
||||
cur_dev = cur_dev->next;
|
||||
}
|
||||
cur_dev = cur_dev->next;
|
||||
SDL_hid_free_enumeration(devs);
|
||||
}
|
||||
|
||||
SDL_hid_free_enumeration(devs);
|
||||
} while (Common::StoppableTimedWait(stop_token, std::chrono::seconds{5}));
|
||||
}
|
||||
|
||||
|
||||
@@ -360,7 +360,8 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa
|
||||
}
|
||||
|
||||
if (target == ScanTarget::PopulateGameList &&
|
||||
(file_type == Loader::FileType::XCI || file_type == Loader::FileType::NSP) &&
|
||||
(file_type == Loader::FileType::XCI || file_type == Loader::FileType::XCZ ||
|
||||
file_type == Loader::FileType::NSP || file_type == Loader::FileType::NSZ) &&
|
||||
!Loader::IsBootableGameContainer(file, file_type)) {
|
||||
return true;
|
||||
}
|
||||
@@ -375,7 +376,9 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa
|
||||
program_id, file);
|
||||
} else if (Settings::values.ext_content_from_game_dirs.GetValue() &&
|
||||
(file_type == Loader::FileType::XCI ||
|
||||
file_type == Loader::FileType::NSP)) {
|
||||
file_type == Loader::FileType::XCZ ||
|
||||
file_type == Loader::FileType::NSP ||
|
||||
file_type == Loader::FileType::NSZ)) {
|
||||
void(provider->AddEntriesFromContainer(file));
|
||||
}
|
||||
} else {
|
||||
@@ -402,7 +405,8 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa
|
||||
};
|
||||
|
||||
if (res2 == Loader::ResultStatus::Success && program_ids.size() > 1 &&
|
||||
(file_type == Loader::FileType::XCI || file_type == Loader::FileType::NSP)) {
|
||||
(file_type == Loader::FileType::XCI || file_type == Loader::FileType::XCZ ||
|
||||
file_type == Loader::FileType::NSP || file_type == Loader::FileType::NSZ)) {
|
||||
for (const auto id : program_ids) {
|
||||
// dravee suggested this, only viable way to
|
||||
// not show sub-games in qlaunch for now.
|
||||
|
||||
@@ -57,7 +57,8 @@ std::unique_ptr<EmuThread> emu_thread = nullptr;
|
||||
|
||||
const QStringList supported_file_extensions = {QStringLiteral("nro"), QStringLiteral("nso"),
|
||||
QStringLiteral("nca"), QStringLiteral("xci"),
|
||||
QStringLiteral("nsp"), QStringLiteral("kip")};
|
||||
QStringLiteral("xcz"), QStringLiteral("nsp"),
|
||||
QStringLiteral("nsz"), QStringLiteral("kip")};
|
||||
|
||||
Core::Frontend::WindowSystemType GetWindowSystemType() {
|
||||
// Determine WSI type based on Qt platform.
|
||||
|
||||
@@ -2797,9 +2797,9 @@ void MainWindow::IncrementInstallProgress() {
|
||||
|
||||
void MainWindow::OnMenuInstallToNAND() {
|
||||
const QString file_filter =
|
||||
tr("Installable Switch File (*.nca *.nsp *.xci);;Nintendo Content Archive "
|
||||
"(*.nca);;Nintendo Submission Package (*.nsp);;NX Cartridge "
|
||||
"Image (*.xci)");
|
||||
tr("Installable Switch File (*.nca *.nsp *.nsz *.xci *.xcz);;Nintendo Content Archive "
|
||||
"(*.nca);;Nintendo Submission Package (*.nsp *.nsz);;NX Cartridge "
|
||||
"Image (*.xci *.xcz)");
|
||||
|
||||
QStringList filenames = QFileDialog::getOpenFileNames(
|
||||
this, tr("Install Files"), QString::fromStdString(UISettings::values.roms_path),
|
||||
@@ -2856,7 +2856,10 @@ void MainWindow::OnMenuInstallToNAND() {
|
||||
QFuture<ContentManager::InstallResult> future;
|
||||
ContentManager::InstallResult result;
|
||||
|
||||
if (file.endsWith(QStringLiteral("nsp"), Qt::CaseInsensitive)) {
|
||||
if (file.endsWith(QStringLiteral("nsp"), Qt::CaseInsensitive) ||
|
||||
file.endsWith(QStringLiteral("nsz"), Qt::CaseInsensitive) ||
|
||||
file.endsWith(QStringLiteral("xci"), Qt::CaseInsensitive) ||
|
||||
file.endsWith(QStringLiteral("xcz"), Qt::CaseInsensitive)) {
|
||||
const auto progress_callback = [this](size_t size, size_t progress) {
|
||||
emit UpdateInstallProgress();
|
||||
if (install_progress->wasCanceled()) {
|
||||
|
||||
Reference in New Issue
Block a user