diff --git a/dist/languages/da_DK.ts b/dist/languages/da_DK.ts
index 861b59046..3c3125c1d 100644
--- a/dist/languages/da_DK.ts
+++ b/dist/languages/da_DK.ts
@@ -3583,6 +3583,24 @@ Drag points to change position, or double-click table cells to edit values.Engelsk
+
+ ConfigureWeb
+
+
+ Form
+ Form
+
+
+
+ Discord Presence
+ Discord-presence
+
+
+
+ Show Current Game in your Discord Status
+ Vis kørende spil som din Discord-status
+
+
DirectConnect
@@ -6861,4 +6879,4 @@ They may have left the room.
Afventningstræ
-
+
\ No newline at end of file
diff --git a/dist/languages/de.ts b/dist/languages/de.ts
index 64f526171..da1f1053d 100644
--- a/dist/languages/de.ts
+++ b/dist/languages/de.ts
@@ -3585,6 +3585,24 @@ Ziehe Punkte, um ihre Position zu verändern, oder doppelklicke auf Zellen in de
Englisch
+
+ ConfigureWeb
+
+
+ Form
+ Form
+
+
+
+ Discord Presence
+ Discord Presence
+
+
+
+ Show Current Game in your Discord Status
+ Aktuelles Spiel in Ihrem Discordstatus anzeigen
+
+
DirectConnect
@@ -6868,4 +6886,4 @@ They may have left the room.
Wait Tree
-
+
\ No newline at end of file
diff --git a/dist/languages/el.ts b/dist/languages/el.ts
index 2731abe21..5aa88a814 100644
--- a/dist/languages/el.ts
+++ b/dist/languages/el.ts
@@ -3584,6 +3584,24 @@ Drag points to change position, or double-click table cells to edit values.Αγγλικά
+
+ ConfigureWeb
+
+
+ Form
+ Φόρμα
+
+
+
+ Discord Presence
+ Παρουσία Discord
+
+
+
+ Show Current Game in your Discord Status
+ Εμφάνιση τρέχοντος παιχνιδιού στην κατάσταση Discord σας
+
+
DirectConnect
@@ -6865,4 +6883,4 @@ They may have left the room.
Δένδρο αναμονής
-
+
\ No newline at end of file
diff --git a/dist/languages/es_ES.ts b/dist/languages/es_ES.ts
index ce5e326d4..090e7a4cc 100644
--- a/dist/languages/es_ES.ts
+++ b/dist/languages/es_ES.ts
@@ -3585,6 +3585,24 @@ Mueve los puntos para cambiar la posición, o haz doble click en las celdas de l
Inglés (English)
+
+ ConfigureWeb
+
+
+ Form
+ Formulario
+
+
+
+ Discord Presence
+ Presencia en Discord
+
+
+
+ Show Current Game in your Discord Status
+ Mostrar Juego Actual en el Estado de Discord
+
+
DirectConnect
@@ -6878,4 +6896,4 @@ Puede que haya dejado la sala.
Árbol de Espera
-
+
\ No newline at end of file
diff --git a/dist/languages/fi.ts b/dist/languages/fi.ts
index b01398ff4..52f9b2b6e 100644
--- a/dist/languages/fi.ts
+++ b/dist/languages/fi.ts
@@ -3583,6 +3583,24 @@ Drag points to change position, or double-click table cells to edit values.Englanti
+
+ ConfigureWeb
+
+
+ Form
+ Muot
+
+
+
+ Discord Presence
+ Discord läsnäolo
+
+
+
+ Show Current Game in your Discord Status
+ Näytä nykyinen peli Discord tilassa
+
+
DirectConnect
@@ -6845,4 +6863,4 @@ They may have left the room.
-
+
\ No newline at end of file
diff --git a/dist/languages/fr.ts b/dist/languages/fr.ts
index 656611455..8dfbf7261 100644
--- a/dist/languages/fr.ts
+++ b/dist/languages/fr.ts
@@ -3585,6 +3585,24 @@ Glissez les points pour modifier la position, ou double-cliquez les cellules pou
Anglais
+
+ ConfigureWeb
+
+
+ Form
+ Forme
+
+
+
+ Discord Presence
+ Présence sur Discord
+
+
+
+ Show Current Game in your Discord Status
+ Afficher votre jeu en cours dans votre statut Discord
+
+
DirectConnect
@@ -6873,4 +6891,4 @@ Il a peut-être quitté la salon.
Arbre d'instructions
-
+
\ No newline at end of file
diff --git a/dist/languages/hu_HU.ts b/dist/languages/hu_HU.ts
index 6abf3b4df..2d5545538 100644
--- a/dist/languages/hu_HU.ts
+++ b/dist/languages/hu_HU.ts
@@ -3582,6 +3582,72 @@ Drag points to change position, or double-click table cells to edit values.Angol
+
+ ConfigureWeb
+
+
+ Form
+ Forma
+
+
+
+ Discord Presence
+ Discord jelenlét
+
+
+
+ Show Current Game in your Discord Status
+ Jelenlegi játék megjelenítése a Discord állapotodban
+
+
+
+ DirectConnect
+
+
+ Direct Connect
+ Közvetlen Kapcsolódás
+
+
+
+ Server Address
+
+
+
+
+ <html><head/><body><p>Server address of the host</p></body></html>
+
+
+
+
+ Port
+ Port
+
+
+
+ <html><head/><body><p>Port number the host is listening on</p></body></html>
+ <html><head/><body><p>Annak a portnak a száma, amire a gazda figyel</p></body></html>
+
+
+
+ 24872
+ 24872
+
+
+
+ Nickname
+ Becenév
+
+
+
+ Password
+ Jelszó
+
+
+
+ Connect
+ Kapcsolás
+
+
DirectConnectWindow
@@ -6807,4 +6873,4 @@ They may have left the room.
Várakozási Fa
-
+
\ No newline at end of file
diff --git a/dist/languages/id.ts b/dist/languages/id.ts
index 4bb737e4f..a6c6cff68 100644
--- a/dist/languages/id.ts
+++ b/dist/languages/id.ts
@@ -3584,6 +3584,24 @@ Drag points to change position, or double-click table cells to edit values.Inggris
+
+ ConfigureWeb
+
+
+ Form
+ Formulir
+
+
+
+ Discord Presence
+ Status Discord
+
+
+
+ Show Current Game in your Discord Status
+ Tampilkan Game Saat Ini ke Status Discord Anda
+
+
DirectConnect
@@ -6861,4 +6879,4 @@ They may have left the room.
Tunggu Tree
-
+
\ No newline at end of file
diff --git a/dist/languages/it.ts b/dist/languages/it.ts
index d4bd4ccb8..bde755430 100644
--- a/dist/languages/it.ts
+++ b/dist/languages/it.ts
@@ -3585,6 +3585,24 @@ Trascina i punti per cambiarne la posizione, o fai doppio clic sulla tabella per
Inglese
+
+ ConfigureWeb
+
+
+ Form
+ Modulo
+
+
+
+ Discord Presence
+ Discord Presence
+
+
+
+ Show Current Game in your Discord Status
+ Mostra il gioco attuale nel tuo stato di Discord
+
+
DirectConnect
@@ -6874,4 +6892,4 @@ Potrebbe aver lasciato la stanza.
Wait Tree
-
+
\ No newline at end of file
diff --git a/dist/languages/ja_JP.ts b/dist/languages/ja_JP.ts
index 95ec541b4..3cc021d2a 100644
--- a/dist/languages/ja_JP.ts
+++ b/dist/languages/ja_JP.ts
@@ -3587,6 +3587,24 @@ Drag points to change position, or double-click table cells to edit values.英語
+
+ ConfigureWeb
+
+
+ Form
+ フォーム
+
+
+
+ Discord Presence
+ Discord Presence
+
+
+
+ Show Current Game in your Discord Status
+ プレイ中のゲームをDiscordに表示
+
+
DirectConnect
@@ -6866,4 +6884,4 @@ They may have left the room.
Wait Tree
-
+
\ No newline at end of file
diff --git a/dist/languages/ko_KR.ts b/dist/languages/ko_KR.ts
index fba684f70..351783d05 100644
--- a/dist/languages/ko_KR.ts
+++ b/dist/languages/ko_KR.ts
@@ -3585,6 +3585,24 @@ Drag points to change position, or double-click table cells to edit values.English
+
+ ConfigureWeb
+
+
+ Form
+ 종류
+
+
+
+ Discord Presence
+ 디스코드 있음
+
+
+
+ Show Current Game in your Discord Status
+ 사용자의 디스코드 상태에 현재 게임 표시하기
+
+
DirectConnect
@@ -6868,4 +6886,4 @@ They may have left the room.
Wait Tree
-
+
\ No newline at end of file
diff --git a/dist/languages/lt_LT.ts b/dist/languages/lt_LT.ts
index 176e6cc01..1c7b45213 100644
--- a/dist/languages/lt_LT.ts
+++ b/dist/languages/lt_LT.ts
@@ -3581,6 +3581,24 @@ Drag points to change position, or double-click table cells to edit values.Anglų k.
+
+ ConfigureWeb
+
+
+ Form
+ Forma
+
+
+
+ Discord Presence
+ Discord nustatymai
+
+
+
+ Show Current Game in your Discord Status
+ Rodyti jūsų žaidžiamą žaidimą Discord'e
+
+
DirectConnect
@@ -6853,4 +6871,4 @@ They may have left the room.
Laukimo gijų medis
-
+
\ No newline at end of file
diff --git a/dist/languages/nb.ts b/dist/languages/nb.ts
index a3c44b557..30ee9deac 100644
--- a/dist/languages/nb.ts
+++ b/dist/languages/nb.ts
@@ -3584,6 +3584,24 @@ Dra punkter for å endre posisjon, eller dobbeltklikk på tabellceller for å re
Engelsk
+
+ ConfigureWeb
+
+
+ Form
+ Form
+
+
+
+ Discord Presence
+ Discord tilstedeværelse
+
+
+
+ Show Current Game in your Discord Status
+ Vis Gjeldende Spill i Discord Statusen din.
+
+
DirectConnect
@@ -6865,4 +6883,4 @@ They may have left the room.
Wait Tree
-
+
\ No newline at end of file
diff --git a/dist/languages/nl.ts b/dist/languages/nl.ts
index a5b218d06..9bedcf20a 100644
--- a/dist/languages/nl.ts
+++ b/dist/languages/nl.ts
@@ -3585,6 +3585,24 @@ Sleep punten om de positie te wijzigen of dubbelklik op tabelcellen om waarden t
Engels
+
+ ConfigureWeb
+
+
+ Form
+ Formulier
+
+
+
+ Discord Presence
+ Discord Presence
+
+
+
+ Show Current Game in your Discord Status
+ Toon Huidige Spel in je Discord Status
+
+
DirectConnect
@@ -6876,4 +6894,4 @@ Misschien hebben ze de kamer verlaten.
Wait Tree
-
+
\ No newline at end of file
diff --git a/dist/languages/pl_PL.ts b/dist/languages/pl_PL.ts
index 64be5be5d..ad25cfd1f 100644
--- a/dist/languages/pl_PL.ts
+++ b/dist/languages/pl_PL.ts
@@ -3583,6 +3583,24 @@ Drag points to change position, or double-click table cells to edit values.Angielski (English)
+
+ ConfigureWeb
+
+
+ Form
+ Formularz
+
+
+
+ Discord Presence
+ Widoczność na Discordzie
+
+
+
+ Show Current Game in your Discord Status
+ Pokaż obecnie włączoną grę w statusie na Discrodzie
+
+
DirectConnect
@@ -6856,4 +6874,4 @@ They may have left the room.
Kolejka Oczekiwania
-
+
\ No newline at end of file
diff --git a/dist/languages/pt_BR.ts b/dist/languages/pt_BR.ts
index 8c7da57f5..de10a88a8 100644
--- a/dist/languages/pt_BR.ts
+++ b/dist/languages/pt_BR.ts
@@ -3585,6 +3585,24 @@ Arraste os pontos para alterar a posição ou clique duas vezes nas células da
Inglês (English)
+
+ ConfigureWeb
+
+
+ Form
+ Formulário
+
+
+
+ Discord Presence
+ Presença no Discord
+
+
+
+ Show Current Game in your Discord Status
+ Mostrar o jogo atual no seu perfil do Discord
+
+
DirectConnect
@@ -6869,4 +6887,4 @@ They may have left the room.
Árvore de espera
-
+
\ No newline at end of file
diff --git a/dist/languages/ro_RO.ts b/dist/languages/ro_RO.ts
index 141b171a6..ca8f8091e 100644
--- a/dist/languages/ro_RO.ts
+++ b/dist/languages/ro_RO.ts
@@ -3584,6 +3584,24 @@ Drag points to change position, or double-click table cells to edit values.Engleză
+
+ ConfigureWeb
+
+
+ Form
+ Model
+
+
+
+ Discord Presence
+ Prezență pe Discord
+
+
+
+ Show Current Game in your Discord Status
+ Afișează Jocul Prezent pe Statusul Discord
+
+
DirectConnect
@@ -6863,4 +6881,4 @@ They may have left the room.
Copac de Așteptare
-
+
\ No newline at end of file
diff --git a/dist/languages/ru_RU.ts b/dist/languages/ru_RU.ts
index 992939f49..1e7304d26 100644
--- a/dist/languages/ru_RU.ts
+++ b/dist/languages/ru_RU.ts
@@ -3587,6 +3587,24 @@ Drag points to change position, or double-click table cells to edit values.Английский
+
+ ConfigureWeb
+
+
+ Form
+ Форма
+
+
+
+ Discord Presence
+ Интеграция с Discord
+
+
+
+ Show Current Game in your Discord Status
+ Показывать текущую игру в статусе Discord
+
+
DirectConnect
@@ -6868,4 +6886,4 @@ They may have left the room.
Дерево цепочки ожидания
-
+
\ No newline at end of file
diff --git a/dist/languages/tr_TR.ts b/dist/languages/tr_TR.ts
index 025759bdf..1a7a2e17c 100644
--- a/dist/languages/tr_TR.ts
+++ b/dist/languages/tr_TR.ts
@@ -3583,6 +3583,24 @@ Drag points to change position, or double-click table cells to edit values.İngilizce
+
+ ConfigureWeb
+
+
+ Form
+ Form
+
+
+
+ Discord Presence
+ Discord Görünümü
+
+
+
+ Show Current Game in your Discord Status
+ Şu Anki Oyunu Discord Durumunda Göster
+
+
DirectConnect
@@ -6865,4 +6883,4 @@ They may have left the room.
Wait Tree
-
+
\ No newline at end of file
diff --git a/dist/languages/vi_VN.ts b/dist/languages/vi_VN.ts
index 617efbbe2..0b8866d55 100644
--- a/dist/languages/vi_VN.ts
+++ b/dist/languages/vi_VN.ts
@@ -3583,6 +3583,24 @@ Drag points to change position, or double-click table cells to edit values.Tiếng Anh
+
+ ConfigureWeb
+
+
+ Form
+ Định dạng
+
+
+
+ Discord Presence
+
+
+
+
+ Show Current Game in your Discord Status
+ Hiển thị game đang chơi trên trạng thái Discord
+
+
DirectConnect
@@ -6861,4 +6879,4 @@ They may have left the room.
Wait Tree
-
+
\ No newline at end of file
diff --git a/dist/languages/zh_CN.ts b/dist/languages/zh_CN.ts
index 402eafcb4..fbc6b80f8 100644
--- a/dist/languages/zh_CN.ts
+++ b/dist/languages/zh_CN.ts
@@ -3585,6 +3585,24 @@ Drag points to change position, or double-click table cells to edit values.英语
+
+ ConfigureWeb
+
+
+ Form
+ 格式
+
+
+
+ Discord Presence
+ Discord 状态
+
+
+
+ Show Current Game in your Discord Status
+ 在您的 Discord 状态中显示当前游戏
+
+
DirectConnect
@@ -6873,4 +6891,4 @@ They may have left the room.
等待树
-
+
\ No newline at end of file
diff --git a/dist/languages/zh_TW.ts b/dist/languages/zh_TW.ts
index 55eb19ad9..220cdf95c 100644
--- a/dist/languages/zh_TW.ts
+++ b/dist/languages/zh_TW.ts
@@ -3584,6 +3584,24 @@ Drag points to change position, or double-click table cells to edit values.English
+
+ ConfigureWeb
+
+
+ Form
+ Form
+
+
+
+ Discord Presence
+ Discord 狀態
+
+
+
+ Show Current Game in your Discord Status
+ 在 Discord 狀態中顯示正在玩的遊戲
+
+
DirectConnect
@@ -6865,4 +6883,4 @@ They may have left the room.
樹狀等待
-
+
\ No newline at end of file
diff --git a/externals/dynarmic b/externals/dynarmic
index 30f1a3c62..a41c38024 160000
--- a/externals/dynarmic
+++ b/externals/dynarmic
@@ -1 +1 @@
-Subproject commit 30f1a3c6289075ef4af08f5ec502be2fc8627a0c
+Subproject commit a41c380246d3d9f9874f0f792d234dc0cc17c180
diff --git a/externals/fmt b/externals/fmt
index 2dd4fa874..fcd3e1e19 160000
--- a/externals/fmt
+++ b/externals/fmt
@@ -1 +1 @@
-Subproject commit 2dd4fa8742fdac36468f8d8ea3e06e78215551f8
+Subproject commit fcd3e1e19c8d2df94bb6cb40d7f1c97a9872cf2b
diff --git a/src/android/app/build.gradle.kts b/src/android/app/build.gradle.kts
index b1289ec3d..9ffd5055d 100644
--- a/src/android/app/build.gradle.kts
+++ b/src/android/app/build.gradle.kts
@@ -29,7 +29,7 @@ android {
namespace = "org.citra.citra_emu"
compileSdkVersion = "android-34"
- ndkVersion = "26.1.10909125"
+ ndkVersion = "26.3.11579264"
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.kt b/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.kt
index bfbe658f8..135794d65 100644
--- a/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.kt
+++ b/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.kt
@@ -183,13 +183,13 @@ object NativeLibrary {
private var coreErrorAlertResult = false
private val coreErrorAlertLock = Object()
- private fun onCoreErrorImpl(title: String, message: String) {
+ private fun onCoreErrorImpl(title: String, message: String, canContinue: Boolean) {
val emulationActivity = sEmulationActivity.get()
if (emulationActivity == null) {
Log.error("[NativeLibrary] EmulationActivity not present")
return
}
- val fragment = CoreErrorDialogFragment.newInstance(title, message)
+ val fragment = CoreErrorDialogFragment.newInstance(title, message, canContinue)
fragment.show(emulationActivity.supportFragmentManager, CoreErrorDialogFragment.TAG)
}
@@ -207,6 +207,7 @@ object NativeLibrary {
}
val title: String
val message: String
+ val canContinue: Boolean
when (error) {
CoreError.ErrorSystemFiles -> {
title = emulationActivity.getString(R.string.system_archive_not_found)
@@ -214,16 +215,25 @@ object NativeLibrary {
R.string.system_archive_not_found_message,
details.ifEmpty { emulationActivity.getString(R.string.system_archive_general) }
)
+ canContinue = true
}
CoreError.ErrorSavestate -> {
title = emulationActivity.getString(R.string.save_load_error)
message = details
+ canContinue = true
+ }
+
+ CoreError.ErrorArticDisconnected -> {
+ title = emulationActivity.getString(R.string.artic_base)
+ message = emulationActivity.getString(R.string.artic_server_comm_error)
+ canContinue = false
}
CoreError.ErrorUnknown -> {
title = emulationActivity.getString(R.string.fatal_error)
message = emulationActivity.getString(R.string.fatal_error_message)
+ canContinue = true
}
else -> {
@@ -232,7 +242,7 @@ object NativeLibrary {
}
// Show the AlertDialog on the main thread.
- emulationActivity.runOnUiThread(Runnable { onCoreErrorImpl(title, message) })
+ emulationActivity.runOnUiThread(Runnable { onCoreErrorImpl(title, message, canContinue) })
// Wait for the lock to notify that it is complete.
synchronized(coreErrorAlertLock) {
@@ -346,6 +356,11 @@ object NativeLibrary {
return
}
+ if (resultCode == EmulationErrorDialogFragment.ShutdownRequested) {
+ emulationActivity.finish()
+ return
+ }
+
emulationActivity.runOnUiThread {
EmulationErrorDialogFragment.newInstance(resultCode).showNow(
emulationActivity.supportFragmentManager,
@@ -361,16 +376,23 @@ object NativeLibrary {
emulationActivity = requireActivity() as EmulationActivity
var captionId = R.string.loader_error_invalid_format
- if (requireArguments().getInt(RESULT_CODE) == ErrorLoader_ErrorEncrypted) {
+ val result = requireArguments().getInt(RESULT_CODE)
+ if (result == ErrorLoader_ErrorEncrypted) {
captionId = R.string.loader_error_encrypted
}
+ if (result == ErrorArticDisconnected) {
+ captionId = R.string.artic_base
+ }
val alert = MaterialAlertDialogBuilder(requireContext())
.setTitle(captionId)
.setMessage(
Html.fromHtml(
- CitraApplication.appContext.resources.getString(R.string.redump_games),
- Html.FROM_HTML_MODE_LEGACY
+ if (result == ErrorArticDisconnected)
+ CitraApplication.appContext.resources.getString(R.string.artic_server_comm_error)
+ else
+ CitraApplication.appContext.resources.getString(R.string.redump_games),
+ Html.FROM_HTML_MODE_LEGACY
)
)
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
@@ -398,7 +420,10 @@ object NativeLibrary {
const val ErrorLoader = 4
const val ErrorLoader_ErrorEncrypted = 5
const val ErrorLoader_ErrorInvalidFormat = 6
- const val ErrorSystemFiles = 7
+ const val ErrorLoader_ErrorGBATitle = 7
+ const val ErrorSystemFiles = 8
+ const val ErrorSavestate = 9
+ const val ErrorArticDisconnected = 10
const val ShutdownRequested = 11
const val ErrorUnknown = 12
@@ -502,12 +527,28 @@ object NativeLibrary {
external fun removeAmiibo()
- const val SAVESTATE_SLOT_COUNT = 10
+ const val SAVESTATE_SLOT_COUNT = 11
+ const val QUICKSAVE_SLOT = 0
external fun getSavestateInfo(): Array?
external fun saveState(slot: Int)
+ fun loadStateIfAvailable(slot: Int): Boolean {
+ var available = false
+ getSavestateInfo()?.forEach {
+ if (it.slot == slot){
+ available = true
+ return@forEach
+ }
+ }
+ if (available) {
+ loadState(slot)
+ return true
+ }
+ return false
+ }
+
external fun loadState(slot: Int)
/**
@@ -619,6 +660,7 @@ object NativeLibrary {
enum class CoreError {
ErrorSystemFiles,
ErrorSavestate,
+ ErrorArticDisconnected,
ErrorUnknown
}
@@ -633,23 +675,33 @@ object NativeLibrary {
}
class CoreErrorDialogFragment : DialogFragment() {
+ private var userChosen = false
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val title = requireArguments().getString(TITLE)
val message = requireArguments().getString(MESSAGE)
- return MaterialAlertDialogBuilder(requireContext())
+ val canContinue = requireArguments().getBoolean(CAN_CONTINUE)
+ val dialog = MaterialAlertDialogBuilder(requireContext())
.setTitle(title)
.setMessage(message)
- .setPositiveButton(R.string.continue_button) { _: DialogInterface?, _: Int ->
+ if (canContinue) {
+ dialog.setPositiveButton(R.string.continue_button) { _: DialogInterface?, _: Int ->
coreErrorAlertResult = true
+ userChosen = true
}
- .setNegativeButton(R.string.abort_button) { _: DialogInterface?, _: Int ->
- coreErrorAlertResult = false
- }.show()
+ }
+ dialog.setNegativeButton(R.string.abort_button) { _: DialogInterface?, _: Int ->
+ coreErrorAlertResult = false
+ userChosen = true
+ }
+ return dialog.show()
}
override fun onDismiss(dialog: DialogInterface) {
super.onDismiss(dialog)
- coreErrorAlertResult = true
+ val canContinue = requireArguments().getBoolean(CAN_CONTINUE)
+ if (!userChosen) {
+ coreErrorAlertResult = canContinue
+ }
synchronized(coreErrorAlertLock) { coreErrorAlertLock.notify() }
}
@@ -658,12 +710,14 @@ object NativeLibrary {
const val TITLE = "title"
const val MESSAGE = "message"
+ const val CAN_CONTINUE = "canContinue"
- fun newInstance(title: String, message: String): CoreErrorDialogFragment {
+ fun newInstance(title: String, message: String, canContinue: Boolean): CoreErrorDialogFragment {
val frag = CoreErrorDialogFragment()
val args = Bundle()
args.putString(TITLE, title)
args.putString(MESSAGE, message)
+ args.putBoolean(CAN_CONTINUE, canContinue)
frag.arguments = args
return frag
}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.kt b/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.kt
index a911b0c31..4681bd319 100644
--- a/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.kt
+++ b/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.kt
@@ -38,7 +38,6 @@ import org.citra.citra_emu.features.settings.model.view.InputBindingSetting
import org.citra.citra_emu.fragments.MessageDialogFragment
import org.citra.citra_emu.utils.ControllerMappingHelper
import org.citra.citra_emu.utils.FileBrowserHelper
-import org.citra.citra_emu.utils.ForegroundService
import org.citra.citra_emu.utils.EmulationLifecycleUtil
import org.citra.citra_emu.utils.EmulationMenuSettings
import org.citra.citra_emu.utils.ThemeUtil
@@ -47,7 +46,6 @@ import org.citra.citra_emu.viewmodel.EmulationViewModel
class EmulationActivity : AppCompatActivity() {
private val preferences: SharedPreferences
get() = PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext)
- private var foregroundService: Intent? = null
var isActivityRecreated = false
private val settingsViewModel: SettingsViewModel by viewModels()
@@ -66,7 +64,7 @@ class EmulationActivity : AppCompatActivity() {
binding = ActivityEmulationBinding.inflate(layoutInflater)
screenAdjustmentUtil = ScreenAdjustmentUtil(windowManager, settingsViewModel.settings)
- hotkeyUtility = HotkeyUtility(screenAdjustmentUtil)
+ hotkeyUtility = HotkeyUtility(screenAdjustmentUtil, this)
setContentView(binding.root)
val navHostFragment =
@@ -85,10 +83,6 @@ class EmulationActivity : AppCompatActivity() {
windowManager.defaultDisplay.rotation
)
- // Start a foreground service to prevent the app from getting killed in the background
- foregroundService = Intent(this, ForegroundService::class.java)
- startForegroundService(foregroundService)
-
EmulationLifecycleUtil.addShutdownHook(hook = { this.finish() })
}
@@ -112,7 +106,6 @@ class EmulationActivity : AppCompatActivity() {
override fun onDestroy() {
EmulationLifecycleUtil.clear()
- stopForegroundService(this)
super.onDestroy()
}
@@ -186,8 +179,7 @@ class EmulationActivity : AppCompatActivity() {
return false
}
- val button =
- preferences.getInt(InputBindingSetting.getInputButtonKey(event.keyCode), event.keyCode)
+ val button = preferences.getInt(InputBindingSetting.getInputButtonKey(event), event.scanCode)
val action: Int = when (event.action) {
KeyEvent.ACTION_DOWN -> {
// On some devices, the back gesture / button press is not intercepted by androidx
@@ -453,12 +445,4 @@ class EmulationActivity : AppCompatActivity() {
OnFilePickerResult(result.toString())
}
-
- companion object {
- fun stopForegroundService(activity: Activity) {
- val startIntent = Intent(activity, ForegroundService::class.java)
- startIntent.action = ForegroundService.ACTION_STOP
- activity.startForegroundService(startIntent)
- }
- }
}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/hotkeys/Hotkey.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/hotkeys/Hotkey.kt
index b19cb03da..db99abf67 100644
--- a/src/android/app/src/main/java/org/citra/citra_emu/features/hotkeys/Hotkey.kt
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/hotkeys/Hotkey.kt
@@ -8,5 +8,7 @@ enum class Hotkey(val button: Int) {
SWAP_SCREEN(10001),
CYCLE_LAYOUT(10002),
CLOSE_GAME(10003),
- PAUSE_OR_RESUME(10004);
+ PAUSE_OR_RESUME(10004),
+ QUICKSAVE(10005),
+ QUICKLOAD(10006);
}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/hotkeys/HotkeyUtility.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/hotkeys/HotkeyUtility.kt
index 830b57b29..25f6a493b 100644
--- a/src/android/app/src/main/java/org/citra/citra_emu/features/hotkeys/HotkeyUtility.kt
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/hotkeys/HotkeyUtility.kt
@@ -4,10 +4,14 @@
package org.citra.citra_emu.features.hotkeys
+import android.content.Context
+import android.widget.Toast
+import org.citra.citra_emu.NativeLibrary
+import org.citra.citra_emu.R
import org.citra.citra_emu.utils.EmulationLifecycleUtil
import org.citra.citra_emu.display.ScreenAdjustmentUtil
-class HotkeyUtility(private val screenAdjustmentUtil: ScreenAdjustmentUtil) {
+class HotkeyUtility(private val screenAdjustmentUtil: ScreenAdjustmentUtil, private val context: Context) {
val hotkeyButtons = Hotkey.entries.map { it.button }
@@ -18,6 +22,23 @@ class HotkeyUtility(private val screenAdjustmentUtil: ScreenAdjustmentUtil) {
Hotkey.CYCLE_LAYOUT.button -> screenAdjustmentUtil.cycleLayouts()
Hotkey.CLOSE_GAME.button -> EmulationLifecycleUtil.closeGame()
Hotkey.PAUSE_OR_RESUME.button -> EmulationLifecycleUtil.pauseOrResume()
+ Hotkey.QUICKSAVE.button -> {
+ NativeLibrary.saveState(NativeLibrary.QUICKSAVE_SLOT)
+ Toast.makeText(context,
+ context.getString(R.string.quicksave_saving),
+ Toast.LENGTH_SHORT).show()
+ }
+ Hotkey.QUICKLOAD.button -> {
+ val wasLoaded = NativeLibrary.loadStateIfAvailable(NativeLibrary.QUICKSAVE_SLOT)
+ val stringRes = if(wasLoaded) {
+ R.string.quickload_loading
+ } else {
+ R.string.quickload_not_found
+ }
+ Toast.makeText(context,
+ context.getString(stringRes),
+ Toast.LENGTH_SHORT).show()
+ }
else -> {}
}
return true
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/IntSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/IntSetting.kt
index b2c378397..f0726f665 100644
--- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/IntSetting.kt
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/IntSetting.kt
@@ -40,7 +40,9 @@ enum class IntSetting(
VSYNC("use_vsync_new", Settings.SECTION_RENDERER, 1),
DEBUG_RENDERER("renderer_debug", Settings.SECTION_DEBUG, 0),
TEXTURE_FILTER("texture_filter", Settings.SECTION_RENDERER, 0),
- USE_FRAME_LIMIT("use_frame_limit", Settings.SECTION_RENDERER, 1);
+ USE_FRAME_LIMIT("use_frame_limit", Settings.SECTION_RENDERER, 1),
+ DELAY_RENDER_THREAD_US("delay_game_render_thread_us", Settings.SECTION_RENDERER, 0),
+ USE_ARTIC_BASE_CONTROLLER("use_artic_base_controller", Settings.SECTION_CONTROLS, 0);
override var int: Int = defaultValue
@@ -68,7 +70,8 @@ enum class IntSetting(
DEBUG_RENDERER,
CPU_JIT,
ASYNC_CUSTOM_LOADING,
- AUDIO_INPUT_TYPE
+ AUDIO_INPUT_TYPE,
+ USE_ARTIC_BASE_CONTROLLER
)
fun from(key: String): IntSetting? = IntSetting.values().firstOrNull { it.key == key }
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/Settings.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/Settings.kt
index d94489021..0e92138cb 100644
--- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/Settings.kt
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/Settings.kt
@@ -136,6 +136,8 @@ class Settings {
const val HOTKEY_CYCLE_LAYOUT = "hotkey_toggle_layout"
const val HOTKEY_CLOSE_GAME = "hotkey_close_game"
const val HOTKEY_PAUSE_OR_RESUME = "hotkey_pause_or_resume_game"
+ const val HOTKEY_QUICKSAVE = "hotkey_quickload"
+ const val HOTKEY_QUICKlOAD = "hotkey_quickpause"
val buttonKeys = listOf(
KEY_BUTTON_A,
@@ -187,13 +189,17 @@ class Settings {
HOTKEY_SCREEN_SWAP,
HOTKEY_CYCLE_LAYOUT,
HOTKEY_CLOSE_GAME,
- HOTKEY_PAUSE_OR_RESUME
+ HOTKEY_PAUSE_OR_RESUME,
+ HOTKEY_QUICKSAVE,
+ HOTKEY_QUICKlOAD
)
val hotkeyTitles = listOf(
R.string.emulation_swap_screens,
R.string.emulation_cycle_landscape_layouts,
R.string.emulation_close_game,
- R.string.emulation_toggle_pause
+ R.string.emulation_toggle_pause,
+ R.string.emulation_quicksave,
+ R.string.emulation_quickload,
)
const val PREF_FIRST_APP_LAUNCH = "FirstApplicationLaunch"
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/InputBindingSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/InputBindingSetting.kt
index 55b454093..c3756abf7 100644
--- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/InputBindingSetting.kt
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/InputBindingSetting.kt
@@ -133,6 +133,8 @@ class InputBindingSetting(
Settings.HOTKEY_CYCLE_LAYOUT -> Hotkey.CYCLE_LAYOUT.button
Settings.HOTKEY_CLOSE_GAME -> Hotkey.CLOSE_GAME.button
Settings.HOTKEY_PAUSE_OR_RESUME -> Hotkey.PAUSE_OR_RESUME.button
+ Settings.HOTKEY_QUICKSAVE -> Hotkey.QUICKSAVE.button
+ Settings.HOTKEY_QUICKlOAD -> Hotkey.QUICKLOAD.button
else -> -1
}
@@ -222,8 +224,10 @@ class InputBindingSetting(
Toast.makeText(context, R.string.input_message_analog_only, Toast.LENGTH_LONG).show()
return
}
- writeButtonMapping(getInputButtonKey(keyEvent.keyCode))
- val uiString = "${keyEvent.device.name}: Button ${keyEvent.keyCode}"
+
+ val code = translateEventToKeyId(keyEvent)
+ writeButtonMapping(getInputButtonKey(code))
+ val uiString = "${keyEvent.device.name}: Button $code"
value = uiString
}
@@ -283,9 +287,17 @@ class InputBindingSetting(
/**
* Helper function to get the settings key for an gamepad button.
+ *
*/
+ @Deprecated("Use the new getInputButtonKey(keyEvent) method to handle unknown keys")
fun getInputButtonKey(keyCode: Int): String = "${INPUT_MAPPING_PREFIX}_HostAxis_${keyCode}"
+ /**
+ * Helper function to get the settings key for an gamepad button.
+ *
+ */
+ fun getInputButtonKey(event: KeyEvent): String = "${INPUT_MAPPING_PREFIX}_HostAxis_${translateEventToKeyId(event)}"
+
/**
* Helper function to get the settings key for an gamepad axis.
*/
@@ -301,5 +313,23 @@ class InputBindingSetting(
*/
fun getInputAxisOrientationKey(axis: Int): String =
"${getInputAxisKey(axis)}_GuestOrientation"
+
+
+ /**
+ * This function translates a keyEvent into an "keyid"
+ * This key id is either the keyCode from the event, or
+ * the raw scanCode.
+ * Only when the keyCode itself is 0, (so it is an unknown key)
+ * we fall back to the raw scan code.
+ * This handles keys like the media-keys on google statia-controllers
+ * that don't have a conventional "mapping" and report as "unknown"
+ */
+ fun translateEventToKeyId(event: KeyEvent): Int {
+ return if (event.keyCode == 0) {
+ event.scanCode
+ } else {
+ event.keyCode
+ }
+ }
}
}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt
index 9f504e603..8f104f0b5 100644
--- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt
@@ -626,6 +626,16 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
val button = getInputObject(key)
add(InputBindingSetting(button, Settings.hotkeyTitles[i]))
}
+ add(HeaderSetting(R.string.miscellaneous))
+ add(
+ SwitchSetting(
+ IntSetting.USE_ARTIC_BASE_CONTROLLER,
+ R.string.use_artic_base_controller,
+ R.string.use_artic_base_controller_desc,
+ IntSetting.USE_ARTIC_BASE_CONTROLLER.key,
+ IntSetting.USE_ARTIC_BASE_CONTROLLER.defaultValue
+ )
+ )
}
}
@@ -729,6 +739,18 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
IntSetting.TEXTURE_FILTER.defaultValue
)
)
+ add(
+ SliderSetting(
+ IntSetting.DELAY_RENDER_THREAD_US,
+ R.string.delay_render_thread,
+ R.string.delay_render_thread_description,
+ 0,
+ 16000,
+ " μs",
+ IntSetting.DELAY_RENDER_THREAD_US.key,
+ IntSetting.DELAY_RENDER_THREAD_US.defaultValue.toFloat()
+ )
+ )
add(HeaderSetting(R.string.stereoscopy))
add(
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt b/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt
index eeff4ff1b..7aa8ce7b7 100644
--- a/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt
+++ b/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt
@@ -481,12 +481,12 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
popupMenu.setOnMenuItemClickListener {
when (it.itemId) {
R.id.menu_emulation_save_state -> {
- showSaveStateSubmenu()
+ showStateSubmenu(true)
true
}
R.id.menu_emulation_load_state -> {
- showLoadStateSubmenu()
+ showStateSubmenu(false)
true
}
@@ -497,7 +497,8 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
popupMenu.show()
}
- private fun showSaveStateSubmenu() {
+ private fun showStateSubmenu(isSaving: Boolean) {
+
val savestates = NativeLibrary.getSavestateInfo()
val popupMenu = PopupMenu(
@@ -507,19 +508,40 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
popupMenu.menu.apply {
for (i in 0 until NativeLibrary.SAVESTATE_SLOT_COUNT) {
- val slot = i + 1
- val text = getString(R.string.emulation_empty_state_slot, slot)
- add(text).setEnabled(true).setOnMenuItemClickListener {
- displaySavestateWarning()
- NativeLibrary.saveState(slot)
+ val slot = i
+ var enableClick = isSaving
+ val text = if (slot == NativeLibrary.QUICKSAVE_SLOT) {
+ enableClick = false
+ getString(R.string.emulation_quicksave_slot)
+ } else {
+ getString(R.string.emulation_empty_state_slot, slot)
+ }
+
+ add(text).setEnabled(enableClick).setOnMenuItemClickListener {
+ if(isSaving) {
+ NativeLibrary.saveState(slot)
+ } else {
+ NativeLibrary.loadState(slot)
+ binding.drawerLayout.close()
+ Toast.makeText(context,
+ getString(R.string.quickload_loading),
+ Toast.LENGTH_SHORT).show()
+ }
true
}
}
}
savestates?.forEach {
- val text = getString(R.string.emulation_occupied_state_slot, it.slot, it.time)
- popupMenu.menu.getItem(it.slot - 1).setTitle(text)
+ var enableClick = true
+ val text = if(it.slot == NativeLibrary.QUICKSAVE_SLOT) {
+ // do not allow saving in quicksave slot
+ enableClick = !isSaving
+ getString(R.string.emulation_occupied_quicksave_slot, it.time)
+ } else{
+ getString(R.string.emulation_occupied_state_slot, it.slot, it.time)
+ }
+ popupMenu.menu.getItem(it.slot).setTitle(text).setEnabled(enableClick)
}
popupMenu.show()
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/fragments/HomeSettingsFragment.kt b/src/android/app/src/main/java/org/citra/citra_emu/fragments/HomeSettingsFragment.kt
index b19fdd7b9..fd026a32b 100644
--- a/src/android/app/src/main/java/org/citra/citra_emu/fragments/HomeSettingsFragment.kt
+++ b/src/android/app/src/main/java/org/citra/citra_emu/fragments/HomeSettingsFragment.kt
@@ -16,6 +16,7 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding
+import androidx.core.widget.doOnTextChanged
import androidx.documentfile.provider.DocumentFile
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
@@ -23,14 +24,19 @@ import androidx.navigation.findNavController
import androidx.navigation.fragment.findNavController
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.GridLayoutManager
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.transition.MaterialSharedAxis
import org.citra.citra_emu.CitraApplication
+import org.citra.citra_emu.HomeNavigationDirections
import org.citra.citra_emu.R
import org.citra.citra_emu.adapters.HomeSettingAdapter
+import org.citra.citra_emu.databinding.DialogSoftwareKeyboardBinding
import org.citra.citra_emu.databinding.FragmentHomeSettingsBinding
import org.citra.citra_emu.features.settings.model.Settings
+import org.citra.citra_emu.features.settings.model.StringSetting
import org.citra.citra_emu.features.settings.ui.SettingsActivity
import org.citra.citra_emu.features.settings.utils.SettingsFile
+import org.citra.citra_emu.model.Game
import org.citra.citra_emu.model.HomeSetting
import org.citra.citra_emu.ui.main.MainActivity
import org.citra.citra_emu.utils.GameHelper
@@ -76,6 +82,44 @@ class HomeSettingsFragment : Fragment() {
R.drawable.ic_settings,
{ SettingsActivity.launch(requireContext(), SettingsFile.FILE_NAME_CONFIG, "") }
),
+ HomeSetting(
+ R.string.artic_base_connect,
+ R.string.artic_base_connect_description,
+ R.drawable.ic_network,
+ {
+ val inflater = LayoutInflater.from(context)
+ val inputBinding = DialogSoftwareKeyboardBinding.inflate(inflater)
+ var textInputValue: String = preferences.getString("last_artic_base_addr", "")!!
+
+ inputBinding.editTextInput.setText(textInputValue)
+ inputBinding.editTextInput.doOnTextChanged { text, _, _, _ ->
+ textInputValue = text.toString()
+ }
+
+ val dialog = context?.let {
+ MaterialAlertDialogBuilder(it)
+ .setView(inputBinding.root)
+ .setTitle(getString(R.string.artic_base_enter_address))
+ .setPositiveButton(android.R.string.ok) { _, _ ->
+ if (textInputValue.isNotEmpty()) {
+ preferences.edit()
+ .putString("last_artic_base_addr", textInputValue)
+ .apply()
+ val menu = Game(
+ title = getString(R.string.artic_base),
+ path = "articbase://$textInputValue",
+ filename = ""
+ )
+ val action =
+ HomeNavigationDirections.actionGlobalEmulationActivity(menu)
+ binding.root.findNavController().navigate(action)
+ }
+ }
+ .setNegativeButton(android.R.string.cancel) {_, _ -> }
+ .show()
+ }
+ }
+ ),
HomeSetting(
R.string.system_files,
R.string.system_files_description,
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/ui/main/MainActivity.kt b/src/android/app/src/main/java/org/citra/citra_emu/ui/main/MainActivity.kt
index c2aa87de4..3e9b48f7c 100644
--- a/src/android/app/src/main/java/org/citra/citra_emu/ui/main/MainActivity.kt
+++ b/src/android/app/src/main/java/org/citra/citra_emu/ui/main/MainActivity.kt
@@ -156,9 +156,6 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
}
}
- // Dismiss previous notifications (should not happen unless a crash occurred)
- EmulationActivity.stopForegroundService(this)
-
setInsets()
}
@@ -170,7 +167,6 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
}
override fun onDestroy() {
- EmulationActivity.stopForegroundService(this)
super.onDestroy()
}
diff --git a/src/android/app/src/main/jni/config.cpp b/src/android/app/src/main/jni/config.cpp
index 4e862f3a0..e565c8ec8 100644
--- a/src/android/app/src/main/jni/config.cpp
+++ b/src/android/app/src/main/jni/config.cpp
@@ -128,6 +128,8 @@ void Config::ReadValues() {
static_cast(sdl2_config->GetInteger("Controls", "udp_input_port",
InputCommon::CemuhookUDP::DEFAULT_PORT));
+ ReadSetting("Controls", Settings::values.use_artic_base_controller);
+
// Core
ReadSetting("Core", Settings::values.use_cpu_jit);
ReadSetting("Core", Settings::values.cpu_clock_percentage);
@@ -169,6 +171,7 @@ void Config::ReadValues() {
ReadSetting("Renderer", Settings::values.bg_red);
ReadSetting("Renderer", Settings::values.bg_green);
ReadSetting("Renderer", Settings::values.bg_blue);
+ ReadSetting("Renderer", Settings::values.delay_game_render_thread_us);
// Layout
Settings::values.layout_option = static_cast(sdl2_config->GetInteger(
diff --git a/src/android/app/src/main/jni/default_ini.h b/src/android/app/src/main/jni/default_ini.h
index c46395fea..acf42a73f 100644
--- a/src/android/app/src/main/jni/default_ini.h
+++ b/src/android/app/src/main/jni/default_ini.h
@@ -86,6 +86,9 @@ udp_input_port=
# The pad to request data on. Should be between 0 (Pad 1) and 3 (Pad 4). (Default 0)
udp_pad_index=
+# Use Artic Controller when connected to Artic Base Server. (Default 0)
+use_artic_base_controller=
+
[Core]
# Whether to use the Just-In-Time (JIT) compiler for CPU emulation
# 0: Interpreter (slow), 1 (default): JIT (fast)
@@ -175,6 +178,10 @@ anaglyph_shader_name =
# 0: Nearest, 1 (default): Linear
filter_mode =
+# Delays the game render thread by the specified amount of microseconds
+# Set to 0 for no delay, only useful in dynamic-fps games to simulate GPU delay.
+delay_game_render_thread_us =
+
[Layout]
# Layout for the screen inside the render window.
# 0 (default): Default Top Bottom Screen, 1: Single Screen Only, 2: Large Screen Small Screen, 3: Side by Side
diff --git a/src/android/app/src/main/jni/emu_window/emu_window.cpp b/src/android/app/src/main/jni/emu_window/emu_window.cpp
index 436442acd..8e1c40cd7 100644
--- a/src/android/app/src/main/jni/emu_window/emu_window.cpp
+++ b/src/android/app/src/main/jni/emu_window/emu_window.cpp
@@ -27,14 +27,18 @@ static void UpdateLandscapeScreenLayout() {
IDCache::GetNativeLibraryClass(), IDCache::GetLandscapeScreenLayout()));
}
-void EmuWindow_Android::OnSurfaceChanged(ANativeWindow* surface) {
- render_window = surface;
+bool EmuWindow_Android::OnSurfaceChanged(ANativeWindow* surface) {
+ if (render_window == surface) {
+ return false;
+ }
+ render_window = surface;
window_info.type = Frontend::WindowSystemType::Android;
window_info.render_surface = surface;
StopPresenting();
OnFramebufferSizeChanged();
+ return true;
}
bool EmuWindow_Android::OnTouchEvent(int x, int y, bool pressed) {
diff --git a/src/android/app/src/main/jni/emu_window/emu_window.h b/src/android/app/src/main/jni/emu_window/emu_window.h
index 64ead5c7f..4266fd1bb 100644
--- a/src/android/app/src/main/jni/emu_window/emu_window.h
+++ b/src/android/app/src/main/jni/emu_window/emu_window.h
@@ -17,7 +17,7 @@ public:
~EmuWindow_Android();
/// Called by the onSurfaceChanges() method to change the surface
- void OnSurfaceChanged(ANativeWindow* surface);
+ bool OnSurfaceChanged(ANativeWindow* surface);
/// Handles touch event that occur.(Touched or released)
bool OnTouchEvent(int x, int y, bool pressed);
diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp
index 13dbc0f2c..393ce906d 100644
--- a/src/android/app/src/main/jni/native.cpp
+++ b/src/android/app/src/main/jni/native.cpp
@@ -82,6 +82,7 @@ static jobject ToJavaCoreError(Core::System::ResultStatus result) {
static const std::map CoreErrorNameMap{
{Core::System::ResultStatus::ErrorSystemFiles, "ErrorSystemFiles"},
{Core::System::ResultStatus::ErrorSavestate, "ErrorSavestate"},
+ {Core::System::ResultStatus::ErrorArticDisconnected, "ErrorArticDisconnected"},
{Core::System::ResultStatus::ErrorUnknown, "ErrorUnknown"},
};
@@ -178,6 +179,7 @@ static Core::System::ResultStatus RunCitra(const std::string& filepath) {
auto app_loader = Loader::GetLoader(filepath);
if (app_loader) {
app_loader->ReadProgramId(program_id);
+ system.RegisterAppLoaderEarly(app_loader);
GameSettings::LoadOverrides(program_id);
}
system.ApplySettings();
@@ -231,6 +233,10 @@ static Core::System::ResultStatus RunCitra(const std::string& filepath) {
InputManager::NDKMotionHandler()->DisableSensors();
if (!HandleCoreError(result, system.GetStatusDetails())) {
// Frontend requests us to abort
+ // If the error was an Artic disconnect, return shutdown request.
+ if (result == Core::System::ResultStatus::ErrorArticDisconnected) {
+ return Core::System::ResultStatus::ShutdownRequested;
+ }
return result;
}
InputManager::NDKMotionHandler()->EnableSensors();
@@ -288,12 +294,13 @@ void Java_org_citra_citra_1emu_NativeLibrary_surfaceChanged(JNIEnv* env,
jobject surf) {
s_surf = ANativeWindow_fromSurface(env, surf);
+ bool notify = false;
if (window) {
- window->OnSurfaceChanged(s_surf);
+ notify = window->OnSurfaceChanged(s_surf);
}
auto& system = Core::System::GetInstance();
- if (system.IsPoweredOn()) {
+ if (notify && system.IsPoweredOn()) {
system.GPU().Renderer().NotifySurfaceChanged();
}
@@ -314,7 +321,9 @@ void Java_org_citra_citra_1emu_NativeLibrary_doFrame([[maybe_unused]] JNIEnv* en
if (stop_run || pause_emulation) {
return;
}
- window->TryPresenting();
+ if (window) {
+ window->TryPresenting();
+ }
}
void JNICALL Java_org_citra_citra_1emu_NativeLibrary_initializeGpuDriver(
diff --git a/src/android/app/src/main/res/drawable/ic_network.xml b/src/android/app/src/main/res/drawable/ic_network.xml
new file mode 100644
index 000000000..91559b988
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_network.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/src/android/app/src/main/res/values-es/strings.xml b/src/android/app/src/main/res/values-es/strings.xml
index 7755c81cc..2b83bc36d 100644
--- a/src/android/app/src/main/res/values-es/strings.xml
+++ b/src/android/app/src/main/res/values-es/strings.xml
@@ -657,4 +657,16 @@ Se esperan fallos gráficos temporales cuando ésta esté activado.
Noviembre
Diciembre
+
+ Fallo de comunicación con el servidor Artic Base. La emulación se detendrá.
+ Artic Base
+ Conectar con Artic Base
+ Conectar con una consola real que esté ejecutando un servidor Artic Base
+ Introduce la dirección del servidor Artic Base
+ Retrasa el hilo de dibujado del juego
+ Retrasa el hilo de dibujado del juego cuando envía datos a la GPU. Ayuda con problemas de rendimiento en los (muy pocos) juegos de fps dinámicos.
+ Misceláneo
+ Usar Artic Controller cuando se está conectado a Artic Base Server
+ Usa los controles proporcionados por Artic Base Server cuando esté conectado a él en lugar del dispositivo de entrada configurado.
+
diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml
index 781e78da1..aad33db15 100644
--- a/src/android/app/src/main/res/values/strings.xml
+++ b/src/android/app/src/main/res/values/strings.xml
@@ -683,4 +683,25 @@
November
December
+
+ Failed to communicate with the Artic Base server. Emulation will stop.
+ Artic Base
+ Connect to a real console that is running an Artic Base server
+ Connect to Artic Base
+ Enter Artic Base server address
+ Delay game render thread
+ Delay the game render thread when it submits data to the GPU. Helps with performance issues in the (very few) dynamic-fps games.
+
+
+ Quicksave
+ Quicksave
+ Quickload
+ Quicksave - %1$tF %1$tR
+ Saving…
+ Loading…
+ No Quicksave available.
+ Miscellaneous
+ Use Artic Controller when connected to Artic Base Server
+ Use the controls provided by Artic Base Server when connected to it instead of the configured input device.
+
diff --git a/src/android/gradle/wrapper/gradle-wrapper.properties b/src/android/gradle/wrapper/gradle-wrapper.properties
index 10cba3572..03ef85ab3 100644
--- a/src/android/gradle/wrapper/gradle-wrapper.properties
+++ b/src/android/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
diff --git a/src/citra/config.cpp b/src/citra/config.cpp
index e47421102..bd60d4764 100644
--- a/src/citra/config.cpp
+++ b/src/citra/config.cpp
@@ -147,6 +147,7 @@ void Config::ReadValues() {
ReadSetting("Renderer", Settings::values.use_vsync_new);
ReadSetting("Renderer", Settings::values.texture_filter);
ReadSetting("Renderer", Settings::values.texture_sampling);
+ ReadSetting("Renderer", Settings::values.delay_game_render_thread_us);
ReadSetting("Renderer", Settings::values.mono_render_option);
ReadSetting("Renderer", Settings::values.render_3d);
diff --git a/src/citra_qt/CMakeLists.txt b/src/citra_qt/CMakeLists.txt
index 4e67d84c4..1cb3cb4d5 100644
--- a/src/citra_qt/CMakeLists.txt
+++ b/src/citra_qt/CMakeLists.txt
@@ -81,6 +81,9 @@ add_executable(citra-qt
configuration/configure_ui.cpp
configuration/configure_ui.h
configuration/configure_ui.ui
+ configuration/configure_web.cpp
+ configuration/configure_web.h
+ configuration/configure_web.ui
configuration/configure_cheats.cpp
configuration/configure_cheats.h
configuration/configure_cheats.ui
diff --git a/src/citra_qt/configuration/config.cpp b/src/citra_qt/configuration/config.cpp
index 7ddbc7eb9..886b66aa2 100644
--- a/src/citra_qt/configuration/config.cpp
+++ b/src/citra_qt/configuration/config.cpp
@@ -327,6 +327,8 @@ void Config::ReadCameraValues() {
void Config::ReadControlValues() {
qt_config->beginGroup(QStringLiteral("Controls"));
+ ReadBasicSetting(Settings::values.use_artic_base_controller);
+
int num_touch_from_button_maps =
qt_config->beginReadArray(QStringLiteral("touch_from_button_maps"));
@@ -636,6 +638,8 @@ void Config::ReadPathValues() {
UISettings::values.game_dirs.append(game_dir);
}
}
+ UISettings::values.last_artic_base_addr =
+ ReadSetting(QStringLiteral("last_artic_base_addr"), QString{}).toString();
UISettings::values.recent_files = ReadSetting(QStringLiteral("recentFiles")).toStringList();
UISettings::values.language = ReadSetting(QStringLiteral("language"), QString{}).toString();
}
@@ -665,6 +669,8 @@ void Config::ReadRendererValues() {
ReadGlobalSetting(Settings::values.texture_filter);
ReadGlobalSetting(Settings::values.texture_sampling);
+ ReadGlobalSetting(Settings::values.delay_game_render_thread_us);
+
if (global) {
ReadBasicSetting(Settings::values.use_shader_jit);
}
@@ -920,6 +926,8 @@ void Config::SaveCameraValues() {
void Config::SaveControlValues() {
qt_config->beginGroup(QStringLiteral("Controls"));
+ WriteBasicSetting(Settings::values.use_artic_base_controller);
+
WriteSetting(QStringLiteral("profile"), Settings::values.current_input_profile_index, 0);
qt_config->beginWriteArray(QStringLiteral("profiles"));
for (std::size_t p = 0; p < Settings::values.input_profiles.size(); ++p) {
@@ -1135,6 +1143,8 @@ void Config::SavePathValues() {
WriteSetting(QStringLiteral("expanded"), game_dir.expanded, true);
}
qt_config->endArray();
+ WriteSetting(QStringLiteral("last_artic_base_addr"),
+ UISettings::values.last_artic_base_addr, QString{});
WriteSetting(QStringLiteral("recentFiles"), UISettings::values.recent_files);
WriteSetting(QStringLiteral("language"), UISettings::values.language, QString{});
}
@@ -1164,6 +1174,8 @@ void Config::SaveRendererValues() {
WriteGlobalSetting(Settings::values.texture_filter);
WriteGlobalSetting(Settings::values.texture_sampling);
+ WriteGlobalSetting(Settings::values.delay_game_render_thread_us);
+
if (global) {
WriteSetting(QStringLiteral("use_shader_jit"), Settings::values.use_shader_jit.GetValue(),
true);
diff --git a/src/citra_qt/configuration/configure.ui b/src/citra_qt/configuration/configure.ui
index e94cbbc9c..f6f5a517a 100644
--- a/src/citra_qt/configuration/configure.ui
+++ b/src/citra_qt/configuration/configure.ui
@@ -97,6 +97,12 @@
configuration/configure_enhancements.h
1
+
+ ConfigureWeb
+ QWidget
+ configuration/configure_web.h
+ 1
+
ConfigureUi
QWidget
diff --git a/src/citra_qt/configuration/configure_debug.ui b/src/citra_qt/configuration/configure_debug.ui
index eacf85be9..860df0ffe 100644
--- a/src/citra_qt/configuration/configure_debug.ui
+++ b/src/citra_qt/configuration/configure_debug.ui
@@ -86,7 +86,7 @@
-
-
+
-
@@ -100,7 +100,7 @@
-
-
+
-
@@ -125,7 +125,7 @@
CPU
-
+
-
diff --git a/src/citra_qt/configuration/configure_dialog.cpp b/src/citra_qt/configuration/configure_dialog.cpp
index 4dd5635c5..dd00e932b 100644
--- a/src/citra_qt/configuration/configure_dialog.cpp
+++ b/src/citra_qt/configuration/configure_dialog.cpp
@@ -16,6 +16,7 @@
#include "citra_qt/configuration/configure_storage.h"
#include "citra_qt/configuration/configure_system.h"
#include "citra_qt/configuration/configure_ui.h"
+#include "citra_qt/configuration/configure_web.h"
#include "citra_qt/hotkeys.h"
#include "common/settings.h"
#include "core/core.h"
@@ -28,7 +29,7 @@ ConfigureDialog::ConfigureDialog(QWidget* parent, HotkeyRegistry& registry_, Cor
system{system_}, is_powered_on{system.IsPoweredOn()},
general_tab{std::make_unique(this)},
system_tab{std::make_unique(system, this)},
- input_tab{std::make_unique(this)},
+ input_tab{std::make_unique(system, this)},
hotkeys_tab{std::make_unique(this)},
graphics_tab{
std::make_unique(gl_renderer, physical_devices, is_powered_on, this)},
@@ -37,7 +38,7 @@ ConfigureDialog::ConfigureDialog(QWidget* parent, HotkeyRegistry& registry_, Cor
camera_tab{std::make_unique(this)},
debug_tab{std::make_unique(is_powered_on, this)},
storage_tab{std::make_unique(is_powered_on, this)},
- ui_tab{std::make_unique(this)} {
+ web_tab{std::make_unique(this)}, ui_tab{std::make_unique(this)} {
Settings::SetConfiguringGlobal(true);
ui->setupUi(this);
@@ -52,6 +53,7 @@ ConfigureDialog::ConfigureDialog(QWidget* parent, HotkeyRegistry& registry_, Cor
ui->tabWidget->addTab(camera_tab.get(), tr("Camera"));
ui->tabWidget->addTab(debug_tab.get(), tr("Debug"));
ui->tabWidget->addTab(storage_tab.get(), tr("Storage"));
+ ui->tabWidget->addTab(web_tab.get(), tr("Web"));
ui->tabWidget->addTab(ui_tab.get(), tr("UI"));
hotkeys_tab->Populate(registry);
@@ -87,6 +89,7 @@ void ConfigureDialog::SetConfiguration() {
audio_tab->SetConfiguration();
camera_tab->SetConfiguration();
debug_tab->SetConfiguration();
+ web_tab->SetConfiguration();
ui_tab->SetConfiguration();
storage_tab->SetConfiguration();
}
@@ -102,6 +105,7 @@ void ConfigureDialog::ApplyConfiguration() {
audio_tab->ApplyConfiguration();
camera_tab->ApplyConfiguration();
debug_tab->ApplyConfiguration();
+ web_tab->ApplyConfiguration();
ui_tab->ApplyConfiguration();
storage_tab->ApplyConfiguration();
system.ApplySettings();
@@ -114,7 +118,7 @@ void ConfigureDialog::PopulateSelectionList() {
ui->selectorList->clear();
const std::array>, 5> items{
- {{tr("General"), {general_tab.get(), debug_tab.get(), ui_tab.get()}},
+ {{tr("General"), {general_tab.get(), web_tab.get(), debug_tab.get(), ui_tab.get()}},
{tr("System"), {system_tab.get(), camera_tab.get(), storage_tab.get()}},
{tr("Graphics"), {enhancements_tab.get(), graphics_tab.get()}},
{tr("Audio"), {audio_tab.get()}},
@@ -154,6 +158,7 @@ void ConfigureDialog::RetranslateUI() {
audio_tab->RetranslateUI();
camera_tab->RetranslateUI();
debug_tab->RetranslateUI();
+ web_tab->RetranslateUI();
ui_tab->RetranslateUI();
storage_tab->RetranslateUI();
}
@@ -173,6 +178,7 @@ void ConfigureDialog::UpdateVisibleTabs() {
{camera_tab.get(), tr("Camera")},
{debug_tab.get(), tr("Debug")},
{storage_tab.get(), tr("Storage")},
+ {web_tab.get(), tr("Web")},
{ui_tab.get(), tr("UI")}};
ui->tabWidget->clear();
diff --git a/src/citra_qt/configuration/configure_dialog.h b/src/citra_qt/configuration/configure_dialog.h
index ac500224e..db2549fcb 100644
--- a/src/citra_qt/configuration/configure_dialog.h
+++ b/src/citra_qt/configuration/configure_dialog.h
@@ -29,6 +29,7 @@ class ConfigureAudio;
class ConfigureCamera;
class ConfigureDebug;
class ConfigureStorage;
+class ConfigureWeb;
class ConfigureUi;
class ConfigureDialog : public QDialog {
@@ -69,5 +70,6 @@ private:
std::unique_ptr camera_tab;
std::unique_ptr debug_tab;
std::unique_ptr storage_tab;
+ std::unique_ptr web_tab;
std::unique_ptr ui_tab;
};
diff --git a/src/citra_qt/configuration/configure_graphics.cpp b/src/citra_qt/configuration/configure_graphics.cpp
index 80c2d138d..2e244b33a 100644
--- a/src/citra_qt/configuration/configure_graphics.cpp
+++ b/src/citra_qt/configuration/configure_graphics.cpp
@@ -26,6 +26,10 @@ ConfigureGraphics::ConfigureGraphics(QString gl_renderer, std::spangraphics_api_combo->setCurrentIndex(-1);
+ const auto width = static_cast(QString::fromStdString("000000000").size() * 6);
+ ui->delay_render_display_label->setMinimumWidth(width);
+ ui->delay_render_combo->setVisible(!Settings::IsConfiguringGlobal());
+
auto graphics_api_combo_model =
qobject_cast(ui->graphics_api_combo->model());
#ifndef ENABLE_SOFTWARE_RENDERER
@@ -82,12 +86,25 @@ ConfigureGraphics::ConfigureGraphics(QString gl_renderer, std::spangraphics_api_combo, qOverload(&QComboBox::currentIndexChanged), this,
&ConfigureGraphics::SetPhysicalDeviceComboVisibility);
+ connect(ui->delay_render_slider, &QSlider::valueChanged, this, [&](int value) {
+ ui->delay_render_display_label->setText(
+ QStringLiteral("%1 ms")
+ .arg(((double)value) / 1000.f, 0, 'f', 3)
+ .rightJustified(QString::fromStdString("000000000").size()));
+ });
+
SetConfiguration();
}
ConfigureGraphics::~ConfigureGraphics() = default;
void ConfigureGraphics::SetConfiguration() {
+ ui->delay_render_slider->setValue(Settings::values.delay_game_render_thread_us.GetValue());
+ ui->delay_render_display_label->setText(
+ QStringLiteral("%1 ms")
+ .arg(((double)ui->delay_render_slider->value()) / 1000, 0, 'f', 3)
+ .rightJustified(QString::fromStdString("000000000").size()));
+
if (!Settings::IsConfiguringGlobal()) {
ConfigurationShared::SetHighlight(ui->graphics_api_group,
!Settings::values.graphics_api.UsingGlobal());
@@ -101,6 +118,16 @@ void ConfigureGraphics::SetConfiguration() {
&Settings::values.texture_sampling);
ConfigurationShared::SetHighlight(ui->widget_texture_sampling,
!Settings::values.texture_sampling.UsingGlobal());
+ ConfigurationShared::SetHighlight(
+ ui->delay_render_layout, !Settings::values.delay_game_render_thread_us.UsingGlobal());
+
+ if (Settings::values.delay_game_render_thread_us.UsingGlobal()) {
+ ui->delay_render_combo->setCurrentIndex(0);
+ ui->delay_render_slider->setEnabled(false);
+ } else {
+ ui->delay_render_combo->setCurrentIndex(1);
+ ui->delay_render_slider->setEnabled(true);
+ }
} else {
ui->graphics_api_combo->setCurrentIndex(
static_cast(Settings::values.graphics_api.GetValue()));
@@ -144,6 +171,9 @@ void ConfigureGraphics::ApplyConfiguration() {
ui->toggle_disk_shader_cache, use_disk_shader_cache);
ConfigurationShared::ApplyPerGameSetting(&Settings::values.use_vsync_new, ui->toggle_vsync_new,
use_vsync_new);
+ ConfigurationShared::ApplyPerGameSetting(
+ &Settings::values.delay_game_render_thread_us, ui->delay_render_combo,
+ [this](s32) { return ui->delay_render_slider->value(); });
if (Settings::IsConfiguringGlobal()) {
Settings::values.use_shader_jit = ui->toggle_shader_jit->isChecked();
@@ -170,9 +200,16 @@ void ConfigureGraphics::SetupPerGameUI() {
ui->toggle_async_present->setEnabled(Settings::values.async_presentation.UsingGlobal());
ui->graphics_api_combo->setEnabled(Settings::values.graphics_api.UsingGlobal());
ui->physical_device_combo->setEnabled(Settings::values.physical_device.UsingGlobal());
+ ui->delay_render_combo->setEnabled(
+ Settings::values.delay_game_render_thread_us.UsingGlobal());
return;
}
+ connect(ui->delay_render_combo, qOverload(&QComboBox::activated), this, [this](int index) {
+ ui->delay_render_slider->setEnabled(index == 1);
+ ConfigurationShared::SetHighlight(ui->delay_render_layout, index == 1);
+ });
+
ui->toggle_shader_jit->setVisible(false);
ConfigurationShared::SetColoredComboBox(
diff --git a/src/citra_qt/configuration/configure_graphics.ui b/src/citra_qt/configuration/configure_graphics.ui
index a052186cd..122fdddcd 100644
--- a/src/citra_qt/configuration/configure_graphics.ui
+++ b/src/citra_qt/configuration/configure_graphics.ui
@@ -307,6 +307,83 @@
+ -
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
-
+
+ Use global
+
+
+ -
+
+ Use per-game
+
+
+
+
+ -
+
+
+ Delay game render thread:
+
+
+ <html><head/><body><p>Delays the emulated game render thread the specified amount of milliseconds every time it submits render commands to the GPU.</p><p>Adjust this feature in the (very few) dynamic-fps games to fix performance issues.</p></body></html>
+
+
+
+ -
+
+
+ 0
+
+
+ 16000
+
+
+ 100
+
+
+ 250
+
+
+ 0
+
+
+ Qt::Horizontal
+
+
+ QSlider::TicksBelow
+
+
+
+ -
+
+
+
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+
+
+
+
diff --git a/src/citra_qt/configuration/configure_input.cpp b/src/citra_qt/configuration/configure_input.cpp
index da98da0d0..158885ca5 100644
--- a/src/citra_qt/configuration/configure_input.cpp
+++ b/src/citra_qt/configuration/configure_input.cpp
@@ -16,6 +16,7 @@
#include "citra_qt/configuration/configure_input.h"
#include "citra_qt/configuration/configure_motion_touch.h"
#include "common/param_package.h"
+#include "core/core.h"
#include "ui_configure_input.h"
const std::array
@@ -145,8 +146,8 @@ static QString AnalogToText(const Common::ParamPackage& param, const std::string
return QObject::tr("[unknown]");
}
-ConfigureInput::ConfigureInput(QWidget* parent)
- : QWidget(parent), ui(std::make_unique()),
+ConfigureInput::ConfigureInput(Core::System& _system, QWidget* parent)
+ : QWidget(parent), system(_system), ui(std::make_unique()),
timeout_timer(std::make_unique()), poll_timer(std::make_unique()) {
ui->setupUi(this);
setFocusPolicy(Qt::ClickFocus);
@@ -400,6 +401,9 @@ ConfigureInput::ConfigureInput(QWidget* parent)
ConfigureInput::~ConfigureInput() = default;
void ConfigureInput::ApplyConfiguration() {
+
+ Settings::values.use_artic_base_controller = ui->use_artic_controller->isChecked();
+
std::transform(buttons_param.begin(), buttons_param.end(),
Settings::values.current_input_profile.buttons.begin(),
[](const Common::ParamPackage& param) { return param.Serialize(); });
@@ -444,6 +448,10 @@ QList ConfigureInput::GetUsedKeyboardKeys() {
}
void ConfigureInput::LoadConfiguration() {
+
+ ui->use_artic_controller->setChecked(Settings::values.use_artic_base_controller.GetValue());
+ ui->use_artic_controller->setEnabled(!system.IsPoweredOn());
+
std::transform(Settings::values.current_input_profile.buttons.begin(),
Settings::values.current_input_profile.buttons.end(), buttons_param.begin(),
[](const std::string& str) { return Common::ParamPackage(str); });
diff --git a/src/citra_qt/configuration/configure_input.h b/src/citra_qt/configuration/configure_input.h
index fb00444c6..45a7a8329 100644
--- a/src/citra_qt/configuration/configure_input.h
+++ b/src/citra_qt/configuration/configure_input.h
@@ -30,7 +30,7 @@ class ConfigureInput : public QWidget {
Q_OBJECT
public:
- explicit ConfigureInput(QWidget* parent = nullptr);
+ explicit ConfigureInput(Core::System& system, QWidget* parent = nullptr);
~ConfigureInput() override;
/// Save all button configurations to settings file
@@ -50,6 +50,7 @@ signals:
void InputKeysChanged(QList new_key_list);
private:
+ Core::System& system;
std::unique_ptr ui;
std::unique_ptr timeout_timer;
diff --git a/src/citra_qt/configuration/configure_input.ui b/src/citra_qt/configuration/configure_input.ui
index 2d199e667..7168b7190 100644
--- a/src/citra_qt/configuration/configure_input.ui
+++ b/src/citra_qt/configuration/configure_input.ui
@@ -841,6 +841,13 @@
+ -
+
+
+ Use Artic Controller when connected to Artic Base Server
+
+
+
diff --git a/src/citra_qt/configuration/configure_per_game.cpp b/src/citra_qt/configuration/configure_per_game.cpp
index c11847a0d..f9c249911 100644
--- a/src/citra_qt/configuration/configure_per_game.cpp
+++ b/src/citra_qt/configuration/configure_per_game.cpp
@@ -151,7 +151,14 @@ void ConfigurePerGame::LoadConfiguration() {
ui->display_title_id->setText(
QStringLiteral("%1").arg(title_id, 16, 16, QLatin1Char{'0'}).toUpper());
- const auto loader = Loader::GetLoader(filename);
+ std::unique_ptr loader_ptr;
+ Loader::AppLoader* loader;
+ if (system.IsPoweredOn()) {
+ loader = &system.GetAppLoader();
+ } else {
+ loader_ptr = Loader::GetLoader(filename);
+ loader = loader_ptr.get();
+ }
std::string title;
if (loader->ReadTitle(title) == Loader::ResultStatus::Success)
diff --git a/src/citra_qt/configuration/configure_system.ui b/src/citra_qt/configuration/configure_system.ui
index 20f585637..71819029e 100644
--- a/src/citra_qt/configuration/configure_system.ui
+++ b/src/citra_qt/configuration/configure_system.ui
@@ -13,640 +13,671 @@
Form
-
+
-
-
-
-
-
-
- System Settings
-
-
-
-
-
-
- Enable New 3DS mode
-
-
-
- -
-
-
- Use LLE applets (if installed)
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- 10
-
-
-
- -
-
-
- Username
-
-
-
- -
-
-
- Birthday
-
-
-
- -
-
-
-
-
-
-
-
- January
-
-
- -
-
- February
-
-
- -
-
- March
-
-
- -
-
- April
-
-
- -
-
- May
-
-
- -
-
- June
-
-
- -
-
- July
-
-
- -
-
- August
-
-
- -
-
- September
-
-
- -
-
- October
-
-
- -
-
- November
-
-
- -
-
- December
-
-
-
-
- -
-
-
-
-
- -
-
-
- Language
-
-
-
- -
-
-
- Note: this can be overridden when region setting is auto-select
-
-
-
-
- Japanese (日本語)
+
+
+
+ 0
+ 480
+
+
+
+ QFrame::NoFrame
+
+
+ 1
+
+
+ true
+
+
+
+
+ 0
+ 0
+ 422
+ 500
+
+
+
+
-
+
+
-
+
+
+ System Settings
-
- -
-
- English
-
-
- -
-
- French (français)
-
-
- -
-
- German (Deutsch)
-
-
- -
-
- Italian (italiano)
-
-
- -
-
- Spanish (español)
-
-
- -
-
- Simplified Chinese (简体中文)
-
-
- -
-
- Korean (한국어)
-
-
- -
-
- Dutch (Nederlands)
-
-
- -
-
- Portuguese (português)
-
-
- -
-
- Russian (Русский)
-
-
- -
-
- Traditional Chinese (正體中文)
-
-
-
-
- -
-
-
- Sound output mode
-
-
-
- -
-
-
-
-
- Mono
-
-
- -
-
- Stereo
-
-
- -
-
- Surround
-
-
-
-
- -
-
-
- Country
-
-
-
- -
-
-
- -
-
-
- Clock
-
-
-
- -
-
-
-
-
- System Clock
-
-
- -
-
- Fixed Time
-
-
-
-
- -
-
-
- Startup time
-
-
-
- -
-
-
- yyyy-MM-ddTHH:mm:ss
-
-
-
- -
-
-
- Offset time
-
-
-
- -
-
-
-
-
-
- days
-
-
- -2147483648
-
-
- 2147483647
-
-
-
- -
-
-
- HH:mm:ss
-
-
-
-
-
- -
-
-
- Initial System Ticks
-
-
-
- -
-
-
-
-
- Random
-
-
- -
-
- Fixed
-
-
-
-
- -
-
-
- Initial System Ticks Override
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- 20
-
-
-
- -
-
-
- Play Coins:
-
-
-
- -
-
-
- 300
-
-
-
- -
-
-
- Run System Setup when Home Menu is launched
-
-
-
- -
-
-
- Console ID:
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- Qt::RightToLeft
-
-
- Regenerate
-
-
-
- -
-
-
- 3GX Plugin Loader:
-
-
-
- -
-
-
- Enable 3GX plugin loader
-
-
-
- -
-
-
- Allow games to change plugin loader state
-
-
-
- -
-
-
- Download System Files from Nitendo servers
-
-
-
- -
-
-
-
-
-
-
-
-
- Minimal
-
-
- -
-
- Old 3DS
-
-
- -
-
- New 3DS
-
-
-
-
- -
-
-
-
-
- JPN
-
-
- -
-
- USA
-
-
- -
-
- EUR
-
-
- -
-
- AUS
-
-
- -
-
- CHN
-
-
- -
-
- KOR
-
-
- -
-
- TWN
-
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- Qt::RightToLeft
-
-
- Download
-
-
-
-
-
-
-
-
-
- -
-
-
- Real Console Unique Data
-
-
+
-
-
-
- SecureInfo_A/B
-
-
-
- -
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- Qt::RightToLeft
-
-
- Choose
-
-
-
-
-
+
+
+ Enable New 3DS mode
+
+
-
-
-
- LocalFriendCodeSeed_A/B
-
-
-
- -
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- Qt::RightToLeft
-
-
- Choose
-
-
-
-
-
-
- -
-
-
- CTCert
-
-
+
+
+ Use LLE applets (if installed)
+
+
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- Qt::RightToLeft
-
-
- Choose
-
-
-
-
-
+
+
+
+ 0
+ 0
+
+
+
+ 10
+
+
-
-
-
- -
-
-
- System settings are available only when game is not running.
-
-
- true
-
-
-
- -
-
-
- Qt::Vertical
-
-
-
- 20
- 40
-
-
-
-
-
+ -
+
+
+ Username
+
+
+
+ -
+
+
+ Birthday
+
+
+
+ -
+
+
-
+
+
-
+
+ January
+
+
+ -
+
+ February
+
+
+ -
+
+ March
+
+
+ -
+
+ April
+
+
+ -
+
+ May
+
+
+ -
+
+ June
+
+
+ -
+
+ July
+
+
+ -
+
+ August
+
+
+ -
+
+ September
+
+
+ -
+
+ October
+
+
+ -
+
+ November
+
+
+ -
+
+ December
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+ Language
+
+
+
+ -
+
+
+ Note: this can be overridden when region setting is auto-select
+
+
-
+
+ Japanese (日本語)
+
+
+ -
+
+ English
+
+
+ -
+
+ French (français)
+
+
+ -
+
+ German (Deutsch)
+
+
+ -
+
+ Italian (italiano)
+
+
+ -
+
+ Spanish (español)
+
+
+ -
+
+ Simplified Chinese (简体中文)
+
+
+ -
+
+ Korean (한국어)
+
+
+ -
+
+ Dutch (Nederlands)
+
+
+ -
+
+ Portuguese (português)
+
+
+ -
+
+ Russian (Русский)
+
+
+ -
+
+ Traditional Chinese (正體中文)
+
+
+
+
+ -
+
+
+ Sound output mode
+
+
+
+ -
+
+
-
+
+ Mono
+
+
+ -
+
+ Stereo
+
+
+ -
+
+ Surround
+
+
+
+
+ -
+
+
+ Country
+
+
+
+ -
+
+
+ -
+
+
+ Clock
+
+
+
+ -
+
+
-
+
+ System Clock
+
+
+ -
+
+ Fixed Time
+
+
+
+
+ -
+
+
+ Startup time
+
+
+
+ -
+
+
+ yyyy-MM-ddTHH:mm:ss
+
+
+
+ -
+
+
+ Offset time
+
+
+
+ -
+
+
-
+
+
+ days
+
+
+ -2147483648
+
+
+ 2147483647
+
+
+
+ -
+
+
+ HH:mm:ss
+
+
+
+
+
+ -
+
+
+ Initial System Ticks
+
+
+
+ -
+
+
-
+
+ Random
+
+
+ -
+
+ Fixed
+
+
+
+
+ -
+
+
+ Initial System Ticks Override
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ 20
+
+
+
+ -
+
+
+ Play Coins:
+
+
+
+ -
+
+
+ 300
+
+
+
+ -
+
+
+ Run System Setup when Home Menu is launched
+
+
+
+ -
+
+
+ Console ID:
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Qt::RightToLeft
+
+
+ Regenerate
+
+
+
+ -
+
+
+ 3GX Plugin Loader:
+
+
+
+ -
+
+
+ Enable 3GX plugin loader
+
+
+
+ -
+
+
+ Allow games to change plugin loader state
+
+
+
+ -
+
+
+ Download System Files from Nitendo servers
+
+
+
+ -
+
+
+
-
+
+
-
+
+ Minimal
+
+
+ -
+
+ Old 3DS
+
+
+ -
+
+ New 3DS
+
+
+
+
+ -
+
+
-
+
+ JPN
+
+
+ -
+
+ USA
+
+
+ -
+
+ EUR
+
+
+ -
+
+ AUS
+
+
+ -
+
+ CHN
+
+
+ -
+
+ KOR
+
+
+ -
+
+ TWN
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Qt::RightToLeft
+
+
+ Download
+
+
+
+
+
+
+
+
+
+ -
+
+
+ Real Console Unique Data
+
+
+
-
+
+
+ SecureInfo_A/B
+
+
+
+ -
+
+
+
-
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Qt::RightToLeft
+
+
+ Choose
+
+
+
+
+
+
+ -
+
+
+ LocalFriendCodeSeed_A/B
+
+
+
+ -
+
+
+
-
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Qt::RightToLeft
+
+
+ Choose
+
+
+
+
+
+
+ -
+
+
+ CTCert
+
+
+
+ -
+
+
+
-
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Qt::RightToLeft
+
+
+ Choose
+
+
+
+
+
+
+
+
+
+ -
+
+
+ System settings are available only when game is not running.
+
+
+ true
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+
+
+
+
+
diff --git a/src/citra_qt/configuration/configure_web.cpp b/src/citra_qt/configuration/configure_web.cpp
new file mode 100644
index 000000000..2c61b02dc
--- /dev/null
+++ b/src/citra_qt/configuration/configure_web.cpp
@@ -0,0 +1,36 @@
+// Copyright 2017 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include
+#include
+#include
+#include "citra_qt/configuration/configure_web.h"
+#include "citra_qt/uisettings.h"
+#include "network/network_settings.h"
+#include "ui_configure_web.h"
+
+ConfigureWeb::ConfigureWeb(QWidget* parent)
+ : QWidget(parent), ui(std::make_unique()) {
+ ui->setupUi(this);
+
+#ifndef USE_DISCORD_PRESENCE
+ ui->discord_group->setVisible(false);
+#endif
+ SetConfiguration();
+}
+
+ConfigureWeb::~ConfigureWeb() = default;
+
+void ConfigureWeb::SetConfiguration() {
+
+ ui->toggle_discordrpc->setChecked(UISettings::values.enable_discord_presence.GetValue());
+}
+
+void ConfigureWeb::ApplyConfiguration() {
+ UISettings::values.enable_discord_presence = ui->toggle_discordrpc->isChecked();
+}
+
+void ConfigureWeb::RetranslateUI() {
+ ui->retranslateUi(this);
+}
diff --git a/src/citra_qt/configuration/configure_web.h b/src/citra_qt/configuration/configure_web.h
new file mode 100644
index 000000000..53f7f2b18
--- /dev/null
+++ b/src/citra_qt/configuration/configure_web.h
@@ -0,0 +1,28 @@
+// Copyright 2017 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include
+#include
+#include
+
+namespace Ui {
+class ConfigureWeb;
+}
+
+class ConfigureWeb : public QWidget {
+ Q_OBJECT
+
+public:
+ explicit ConfigureWeb(QWidget* parent = nullptr);
+ ~ConfigureWeb() override;
+
+ void ApplyConfiguration();
+ void RetranslateUI();
+ void SetConfiguration();
+
+private:
+ std::unique_ptr ui;
+};
diff --git a/src/citra_qt/configuration/configure_web.ui b/src/citra_qt/configuration/configure_web.ui
new file mode 100644
index 000000000..a68936c59
--- /dev/null
+++ b/src/citra_qt/configuration/configure_web.ui
@@ -0,0 +1,53 @@
+
+
+ ConfigureWeb
+
+
+
+ 0
+ 0
+ 996
+ 561
+
+
+
+ Form
+
+
+ -
+
+
+ Discord Presence
+
+
+
-
+
+
+ Show Current Game in your Discord Status
+
+
+
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+
+
+
+ toggle_discordrpc
+
+
+
+
diff --git a/src/citra_qt/main.cpp b/src/citra_qt/main.cpp
index e724ea37b..0b5b16695 100644
--- a/src/citra_qt/main.cpp
+++ b/src/citra_qt/main.cpp
@@ -381,6 +381,10 @@ void GMainWindow::InitializeWidgets() {
progress_bar->hide();
statusBar()->addPermanentWidget(progress_bar);
+ artic_traffic_label = new QLabel();
+ artic_traffic_label->setToolTip(
+ tr("Current Artic Base traffic speed. Higher values indicate bigger transfer loads."));
+
emu_speed_label = new QLabel();
emu_speed_label->setToolTip(tr("Current emulation speed. Values higher or lower than 100% "
"indicate emulation is running faster or slower than a 3DS."));
@@ -392,7 +396,8 @@ void GMainWindow::InitializeWidgets() {
tr("Time taken to emulate a 3DS frame, not counting framelimiting or v-sync. For "
"full-speed emulation this should be at most 16.67 ms."));
- for (auto& label : {emu_speed_label, game_fps_label, emu_frametime_label}) {
+ for (auto& label :
+ {artic_traffic_label, emu_speed_label, game_fps_label, emu_frametime_label}) {
label->setVisible(false);
label->setFrameStyle(QFrame::NoFrame);
label->setContentsMargins(4, 0, 4, 0);
@@ -866,6 +871,7 @@ void GMainWindow::ConnectMenuEvents() {
// File
connect_menu(ui->action_Load_File, &GMainWindow::OnMenuLoadFile);
connect_menu(ui->action_Install_CIA, &GMainWindow::OnMenuInstallCIA);
+ connect_menu(ui->action_Connect_Artic, &GMainWindow::OnMenuConnectArticBase);
for (u32 region = 0; region < Core::NUM_SYSTEM_TITLE_REGIONS; region++) {
connect_menu(ui->menu_Boot_Home_Menu->actions().at(region),
[this, region] { OnMenuBootHomeMenu(region); });
@@ -935,6 +941,10 @@ void GMainWindow::ConnectMenuEvents() {
// Help
connect_menu(ui->action_Open_Citra_Folder, &GMainWindow::OnOpenCitraFolder);
+ connect_menu(ui->action_Open_Log_Folder, []() {
+ QString path = QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::LogDir));
+ QDesktopServices::openUrl(QUrl::fromLocalFile(path));
+ });
connect_menu(ui->action_FAQ, []() {
QDesktopServices::openUrl(QUrl(QStringLiteral("https://citra-emu.org/wiki/faq/")));
});
@@ -964,7 +974,7 @@ void GMainWindow::UpdateMenuState() {
action->setEnabled(emulation_running);
}
- ui->action_Capture_Screenshot->setEnabled(emulation_running && !is_paused);
+ ui->action_Capture_Screenshot->setEnabled(emulation_running);
if (emulation_running && is_paused) {
ui->action_Pause->setText(tr("&Continue"));
@@ -1203,6 +1213,14 @@ bool GMainWindow::LoadROM(const QString& filename) {
tr("GBA Virtual Console ROMs are not supported by Citra."));
break;
+ case Core::System::ResultStatus::ErrorArticDisconnected:
+ QMessageBox::critical(
+ this, tr("Artic Base Server"),
+ tr(fmt::format(
+ "An error has occurred whilst communicating with the Artic Base Server.\n{}",
+ system.GetStatusDetails())
+ .c_str()));
+ break;
default:
QMessageBox::critical(
this, tr("Error while loading ROM!"),
@@ -1223,7 +1241,13 @@ bool GMainWindow::LoadROM(const QString& filename) {
}
void GMainWindow::BootGame(const QString& filename) {
- if (filename.endsWith(QStringLiteral(".cia"))) {
+ if (emu_thread) {
+ ShutdownGame();
+ }
+
+ const bool is_artic = filename.startsWith(QString::fromStdString("articbase://"));
+
+ if (!is_artic && filename.endsWith(QStringLiteral(".cia"))) {
const auto answer = QMessageBox::question(
this, tr("CIA must be installed before usage"),
tr("Before using this CIA, you must install it. Do you want to install it now?"),
@@ -1235,8 +1259,12 @@ void GMainWindow::BootGame(const QString& filename) {
return;
}
+ show_artic_label = is_artic;
+
LOG_INFO(Frontend, "Citra starting...");
- StoreRecentFile(filename); // Put the filename on top of the list
+ if (!is_artic) {
+ StoreRecentFile(filename); // Put the filename on top of the list
+ }
if (movie_record_on_start) {
movie.PrepareForRecording();
@@ -1246,16 +1274,26 @@ void GMainWindow::BootGame(const QString& filename) {
}
const std::string path = filename.toStdString();
- const auto loader = Loader::GetLoader(path);
+ auto loader = Loader::GetLoader(path);
u64 title_id{0};
- loader->ReadProgramId(title_id);
+ Loader::ResultStatus res = loader->ReadProgramId(title_id);
+
+ if (Loader::ResultStatus::Success == res) {
+ // Load per game settings
+ const std::string name{is_artic ? "" : FileUtil::GetFilename(filename.toStdString())};
+ const std::string config_file_name =
+ title_id == 0 ? name : fmt::format("{:016X}", title_id);
+ LOG_INFO(Frontend, "Loading per game config file for title {}", config_file_name);
+ Config per_game_config(config_file_name, Config::ConfigType::PerGameConfig);
+ }
+
+ // Artic Base Server cannot accept a client multiple times, so multiple loaders are not
+ // possible. Instead register the app loader early and do not create it again on system load.
+ if (!loader->SupportsMultipleInstancesForSameFile()) {
+ system.RegisterAppLoaderEarly(loader);
+ }
- // Load per game settings
- const std::string name{FileUtil::GetFilename(filename.toStdString())};
- const std::string config_file_name = title_id == 0 ? name : fmt::format("{:016X}", title_id);
- LOG_INFO(Frontend, "Loading per game config file for title {}", config_file_name);
- Config per_game_config(config_file_name, Config::ConfigType::PerGameConfig);
system.ApplySettings();
Settings::LogSettings();
@@ -1265,8 +1303,11 @@ void GMainWindow::BootGame(const QString& filename) {
game_list->SaveInterfaceLayout();
config->Save();
- if (!LoadROM(filename))
+ if (!LoadROM(filename)) {
+ render_window->ReleaseRenderTarget();
+ secondary_window->ReleaseRenderTarget();
return;
+ }
// Set everything up
if (movie_record_on_start) {
@@ -1420,6 +1461,8 @@ void GMainWindow::ShutdownGame() {
// Disable status bar updates
status_bar_update_timer.stop();
message_label_used_for_movie = false;
+ show_artic_label = false;
+ artic_traffic_label->setVisible(false);
emu_speed_label->setVisible(false);
game_fps_label->setVisible(false);
emu_frametime_label->setVisible(false);
@@ -1759,6 +1802,17 @@ void GMainWindow::OnMenuInstallCIA() {
InstallCIA(filepaths);
}
+void GMainWindow::OnMenuConnectArticBase() {
+ bool ok = false;
+ auto res = QInputDialog::getText(this, tr("Connect to Artic Base"),
+ tr("Enter Artic Base server address:"), QLineEdit::Normal,
+ UISettings::values.last_artic_base_addr, &ok);
+ if (ok) {
+ UISettings::values.last_artic_base_addr = res;
+ BootGame(QString::fromStdString("articbase://").append(res));
+ }
+}
+
void GMainWindow::OnMenuBootHomeMenu(u32 region) {
BootGame(QString::fromStdString(Core::GetHomeMenuNcchPath(region)));
}
@@ -2365,33 +2419,47 @@ void GMainWindow::OnSaveMovie() {
}
void GMainWindow::OnCaptureScreenshot() {
- if (!emu_thread || !emu_thread->IsRunning()) [[unlikely]] {
+ if (!emu_thread) [[unlikely]] {
return;
}
- OnPauseGame();
- std::string path = UISettings::values.screenshot_path.GetValue();
- if (!FileUtil::IsDirectory(path)) {
- if (!FileUtil::CreateFullPath(path)) {
- QMessageBox::information(this, tr("Invalid Screenshot Directory"),
- tr("Cannot create specified screenshot directory. Screenshot "
- "path is set back to its default value."));
- path = FileUtil::GetUserPath(FileUtil::UserPath::UserDir);
- path.append("screenshots/");
- UISettings::values.screenshot_path = path;
- };
+ const bool was_running = emu_thread->IsRunning();
+
+ if (was_running ||
+ (QMessageBox::question(
+ this, tr("Game will unpause"),
+ tr("The game will be unpaused, and the next frame will be captured. Is this okay?"),
+ QMessageBox::Yes | QMessageBox::No, QMessageBox::No) == QMessageBox::Yes)) {
+ if (was_running) {
+ OnPauseGame();
+ }
+ std::string path = UISettings::values.screenshot_path.GetValue();
+ if (!FileUtil::IsDirectory(path)) {
+ if (!FileUtil::CreateFullPath(path)) {
+ QMessageBox::information(
+ this, tr("Invalid Screenshot Directory"),
+ tr("Cannot create specified screenshot directory. Screenshot "
+ "path is set back to its default value."));
+ path = FileUtil::GetUserPath(FileUtil::UserPath::UserDir);
+ path.append("screenshots/");
+ UISettings::values.screenshot_path = path;
+ };
+ }
+
+ static QRegularExpression expr(QStringLiteral("[\\/:?\"<>|]"));
+ const std::string filename = game_title.remove(expr).toStdString();
+ const std::string timestamp = QDateTime::currentDateTime()
+ .toString(QStringLiteral("dd.MM.yy_hh.mm.ss.z"))
+ .toStdString();
+ path.append(fmt::format("/{}_{}.png", filename, timestamp));
+
+ auto* const screenshot_window =
+ secondary_window->HasFocus() ? secondary_window : render_window;
+ screenshot_window->CaptureScreenshot(
+ UISettings::values.screenshot_resolution_factor.GetValue(),
+ QString::fromStdString(path));
+ OnStartGame();
}
-
- static QRegularExpression expr(QStringLiteral("[\\/:?\"<>|]"));
- const std::string filename = game_title.remove(expr).toStdString();
- const std::string timestamp =
- QDateTime::currentDateTime().toString(QStringLiteral("dd.MM.yy_hh.mm.ss.z")).toStdString();
- path.append(fmt::format("/{}_{}.png", filename, timestamp));
-
- auto* const screenshot_window = secondary_window->HasFocus() ? secondary_window : render_window;
- screenshot_window->CaptureScreenshot(UISettings::values.screenshot_resolution_factor.GetValue(),
- QString::fromStdString(path));
- OnStartGame();
}
void GMainWindow::OnDumpVideo() {
@@ -2575,6 +2643,53 @@ void GMainWindow::UpdateStatusBar() {
auto results = system.GetAndResetPerfStats();
+ if (show_artic_label) {
+ const bool do_mb = results.artic_transmitted >= (1000.0 * 1000.0);
+ const double value = do_mb ? (results.artic_transmitted / (1000.0 * 1000.0))
+ : (results.artic_transmitted / 1000.0);
+ static const std::array, 5>
+ perf_events = {
+ std::make_pair(Core::PerfStats::PerfArticEventBits::ARTIC_SHARED_EXT_DATA,
+ tr("(Accessing SharedExtData)")),
+ std::make_pair(Core::PerfStats::PerfArticEventBits::ARTIC_SYSTEM_SAVE_DATA,
+ tr("(Accessing SystemSaveData)")),
+ std::make_pair(Core::PerfStats::PerfArticEventBits::ARTIC_BOSS_EXT_DATA,
+ tr("(Accessing BossExtData)")),
+ std::make_pair(Core::PerfStats::PerfArticEventBits::ARTIC_EXT_DATA,
+ tr("(Accessing ExtData)")),
+ std::make_pair(Core::PerfStats::PerfArticEventBits::ARTIC_SAVE_DATA,
+ tr("(Accessing SaveData)")),
+ };
+
+ const QString unit = do_mb ? tr("MB/s") : tr("KB/s");
+ QString event{};
+ for (auto p : perf_events) {
+ if (results.artic_events.Get(p.first)) {
+ event = QString::fromStdString(" ") + p.second;
+ break;
+ }
+ }
+
+ static const std::array label_color = {QStringLiteral("#ffffff"), QStringLiteral("#eed202"),
+ QStringLiteral("#ff3333")};
+
+ int style_index;
+
+ if (value > 200.0) {
+ style_index = 2;
+ } else if (value > 125.0) {
+ style_index = 1;
+ } else {
+ style_index = 0;
+ }
+ const QString style_sheet =
+ QStringLiteral("QLabel { color: %0; }").arg(label_color[style_index]);
+
+ artic_traffic_label->setText(
+ tr("Artic Base Traffic: %1 %2%3").arg(value, 0, 'f', 0).arg(unit).arg(event));
+ artic_traffic_label->setStyleSheet(style_sheet);
+ }
+
if (Settings::values.frame_limit.GetValue() == 0) {
emu_speed_label->setText(tr("Speed: %1%").arg(results.emulation_speed * 100.0, 0, 'f', 0));
} else {
@@ -2585,6 +2700,9 @@ void GMainWindow::UpdateStatusBar() {
game_fps_label->setText(tr("Game: %1 FPS").arg(results.game_fps, 0, 'f', 0));
emu_frametime_label->setText(tr("Frame: %1 ms").arg(results.frametime * 1000.0, 0, 'f', 2));
+ if (show_artic_label) {
+ artic_traffic_label->setVisible(true);
+ }
emu_speed_label->setVisible(true);
game_fps_label->setVisible(true);
emu_frametime_label->setVisible(true);
@@ -2736,6 +2854,7 @@ void GMainWindow::OnCoreError(Core::System::ResultStatus result, std::string det
QString title, message;
QMessageBox::Icon error_severity_icon;
+ bool can_continue = true;
if (result == Core::System::ResultStatus::ErrorSystemFiles) {
const QString common_message =
tr("%1 is missing. Please
+
@@ -202,6 +203,7 @@
+
@@ -222,6 +224,11 @@
Install CIA...
+
+
+ Connect to Artic Base...
+
+
JPN
@@ -473,6 +480,14 @@
Fullscreen
+
+
+ Open Log Folder
+
+
+ Opens the Citra Log folder
+
+
Modify Citra Install
diff --git a/src/citra_qt/uisettings.h b/src/citra_qt/uisettings.h
index 6dedff0f0..5ac61e204 100644
--- a/src/citra_qt/uisettings.h
+++ b/src/citra_qt/uisettings.h
@@ -116,6 +116,7 @@ struct Values {
bool game_dir_deprecated_deepscan;
QVector game_dirs;
QStringList recent_files;
+ QString last_artic_base_addr;
QString language;
QString theme;
diff --git a/src/common/logging/formatter.h b/src/common/logging/formatter.h
index ad6adb143..1bfd534b5 100644
--- a/src/common/logging/formatter.h
+++ b/src/common/logging/formatter.h
@@ -12,9 +12,9 @@
#if FMT_VERSION >= 80100
template
struct fmt::formatter, char>>
- : formatter> {
+ : fmt::formatter> {
template
- auto format(const T& value, FormatContext& ctx) -> decltype(ctx.out()) {
+ auto format(const T& value, FormatContext& ctx) const -> decltype(ctx.out()) {
return fmt::formatter>::format(
static_cast>(value), ctx);
}
diff --git a/src/common/settings.cpp b/src/common/settings.cpp
index 657747b61..69cc6f4ef 100644
--- a/src/common/settings.cpp
+++ b/src/common/settings.cpp
@@ -83,6 +83,7 @@ void LogSettings() {
LOG_INFO(Config, "Citra Configuration:");
log_setting("Core_UseCpuJit", values.use_cpu_jit.GetValue());
log_setting("Core_CPUClockPercentage", values.cpu_clock_percentage.GetValue());
+ log_setting("Controller_UseArticController", values.use_artic_base_controller.GetValue());
log_setting("Renderer_UseGLES", values.use_gles.GetValue());
log_setting("Renderer_GraphicsAPI", GetGraphicsAPIName(values.graphics_api.GetValue()));
log_setting("Renderer_AsyncShaders", values.async_shader_compilation.GetValue());
@@ -100,6 +101,7 @@ void LogSettings() {
log_setting("Renderer_TextureFilter", GetTextureFilterName(values.texture_filter.GetValue()));
log_setting("Renderer_TextureSampling",
GetTextureSamplingName(values.texture_sampling.GetValue()));
+ log_setting("Renderer_DelayGameRenderThreasUs", values.delay_game_render_thread_us.GetValue());
log_setting("Stereoscopy_Render3d", values.render_3d.GetValue());
log_setting("Stereoscopy_Factor3d", values.factor_3d.GetValue());
log_setting("Stereoscopy_MonoRenderOption", values.mono_render_option.GetValue());
@@ -192,6 +194,7 @@ void RestoreGlobalState(bool is_powered_on) {
values.frame_limit.SetGlobal(true);
values.texture_filter.SetGlobal(true);
values.texture_sampling.SetGlobal(true);
+ values.delay_game_render_thread_us.SetGlobal(true);
values.layout_option.SetGlobal(true);
values.swap_screen.SetGlobal(true);
values.upright_screen.SetGlobal(true);
diff --git a/src/common/settings.h b/src/common/settings.h
index 64bba90ee..b61957a6a 100644
--- a/src/common/settings.h
+++ b/src/common/settings.h
@@ -425,6 +425,7 @@ struct Values {
int current_input_profile_index; ///< The current input profile index
std::vector input_profiles; ///< The list of input profiles
std::vector touch_from_button_maps;
+ Setting use_artic_base_controller{false, "use_artic_base_controller"};
SwitchableSetting enable_gamemode{true, "enable_gamemode"};
@@ -479,6 +480,8 @@ struct Values {
SwitchableSetting texture_filter{TextureFilter::None, "texture_filter"};
SwitchableSetting texture_sampling{TextureSampling::GameControlled,
"texture_sampling"};
+ SwitchableSetting delay_game_render_thread_us{0, 0, 16000,
+ "delay_game_render_thread_us"};
SwitchableSetting layout_option{LayoutOption::Default, "layout_option"};
SwitchableSetting swap_screen{false, "swap_screen"};
diff --git a/src/common/static_lru_cache.h b/src/common/static_lru_cache.h
index b91f046a0..bd692e94e 100644
--- a/src/common/static_lru_cache.h
+++ b/src/common/static_lru_cache.h
@@ -14,6 +14,7 @@
//---------------------------------------------------------------------------//
#pragma once
+#include
#include
#include
#include
diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt
index 62a9cde64..de4439ece 100644
--- a/src/core/CMakeLists.txt
+++ b/src/core/CMakeLists.txt
@@ -40,6 +40,8 @@ add_library(citra_core STATIC
dumping/backend.h
dumping/ffmpeg_backend.cpp
dumping/ffmpeg_backend.h
+ file_sys/archive_artic.cpp
+ file_sys/archive_artic.h
file_sys/archive_backend.cpp
file_sys/archive_backend.h
file_sys/archive_extsavedata.cpp
@@ -60,6 +62,8 @@ add_library(citra_core STATIC
file_sys/archive_source_sd_savedata.h
file_sys/archive_systemsavedata.cpp
file_sys/archive_systemsavedata.h
+ file_sys/artic_cache.cpp
+ file_sys/artic_cache.h
file_sys/cia_common.h
file_sys/cia_container.cpp
file_sys/cia_container.h
@@ -87,6 +91,10 @@ add_library(citra_core STATIC
file_sys/romfs_reader.h
file_sys/savedata_archive.cpp
file_sys/savedata_archive.h
+ file_sys/secure_value_backend_artic.cpp
+ file_sys/secure_value_backend_artic.h
+ file_sys/secure_value_backend.cpp
+ file_sys/secure_value_backend.h
file_sys/seed_db.cpp
file_sys/seed_db.h
file_sys/ticket.cpp
@@ -445,6 +453,8 @@ add_library(citra_core STATIC
hw/y2r.h
loader/3dsx.cpp
loader/3dsx.h
+ loader/artic.cpp
+ loader/artic.h
loader/elf.cpp
loader/elf.h
loader/loader.cpp
@@ -470,7 +480,7 @@ add_library(citra_core STATIC
tracer/citrace.h
tracer/recorder.cpp
tracer/recorder.h
-)
+ )
create_target_directory_groups(citra_core)
diff --git a/src/core/core.cpp b/src/core/core.cpp
index 3db61dfcd..9f4fcf406 100644
--- a/src/core/core.cpp
+++ b/src/core/core.cpp
@@ -256,7 +256,11 @@ System::ResultStatus System::SingleStep() {
System::ResultStatus System::Load(Frontend::EmuWindow& emu_window, const std::string& filepath,
Frontend::EmuWindow* secondary_window) {
FileUtil::SetCurrentRomPath(filepath);
- app_loader = Loader::GetLoader(filepath);
+ if (early_app_loader) {
+ app_loader = std::move(early_app_loader);
+ } else {
+ app_loader = Loader::GetLoader(filepath);
+ }
if (!app_loader) {
LOG_CRITICAL(Core, "Failed to obtain loader for {}!", filepath);
return ResultStatus::ErrorGetLoader;
@@ -286,6 +290,8 @@ System::ResultStatus System::Load(Frontend::EmuWindow& emu_window, const std::st
return ResultStatus::ErrorLoader_ErrorInvalidFormat;
case Loader::ResultStatus::ErrorGbaTitle:
return ResultStatus::ErrorLoader_ErrorGbaTitle;
+ case Loader::ResultStatus::ErrorArtic:
+ return ResultStatus::ErrorArticDisconnected;
default:
return ResultStatus::ErrorSystemMode;
}
@@ -334,6 +340,8 @@ System::ResultStatus System::Load(Frontend::EmuWindow& emu_window, const std::st
return ResultStatus::ErrorLoader_ErrorInvalidFormat;
case Loader::ResultStatus::ErrorGbaTitle:
return ResultStatus::ErrorLoader_ErrorGbaTitle;
+ case Loader::ResultStatus::ErrorArtic:
+ return ResultStatus::ErrorArticDisconnected;
default:
return ResultStatus::ErrorLoader;
}
@@ -691,6 +699,10 @@ void System::ApplySettings() {
}
}
+void System::RegisterAppLoaderEarly(std::unique_ptr& loader) {
+ early_app_loader = std::move(loader);
+}
+
template
void System::serialize(Archive& ar, const unsigned int file_version) {
diff --git a/src/core/core.h b/src/core/core.h
index 50d31ff74..a395360ac 100644
--- a/src/core/core.h
+++ b/src/core/core.h
@@ -99,6 +99,7 @@ public:
///< Console
ErrorSystemFiles, ///< Error in finding system files
ErrorSavestate, ///< Error saving or loading
+ ErrorArticDisconnected, ///< Error when artic base disconnects
ShutdownRequested, ///< Emulated program requested a system shutdown
ErrorUnknown ///< Any other error
};
@@ -169,6 +170,18 @@ public:
[[nodiscard]] PerfStats::Results GetAndResetPerfStats();
+ void ReportArticTraffic(u32 bytes) {
+ if (perf_stats) {
+ perf_stats->AddArticBaseTraffic(bytes);
+ }
+ }
+
+ void ReportPerfArticEvent(PerfStats::PerfArticEventBits event, bool set) {
+ if (perf_stats) {
+ perf_stats->ReportPerfArticEvent(event, set);
+ }
+ }
+
[[nodiscard]] PerfStats::Results GetLastPerfStats();
/**
@@ -346,6 +359,8 @@ public:
/// Applies any changes to settings to this core instance.
void ApplySettings();
+ void RegisterAppLoaderEarly(std::unique_ptr& loader);
+
private:
/**
* Initialize the emulated system.
@@ -366,6 +381,9 @@ private:
/// AppLoader used to load the current executing application
std::unique_ptr app_loader;
+ // Temporary app loader passed from frontend
+ std::unique_ptr early_app_loader;
+
/// ARM11 CPU core
std::vector> cpu_cores;
ARM_Interface* running_core = nullptr;
diff --git a/src/core/file_sys/archive_artic.cpp b/src/core/file_sys/archive_artic.cpp
new file mode 100644
index 000000000..a4eb4a599
--- /dev/null
+++ b/src/core/file_sys/archive_artic.cpp
@@ -0,0 +1,557 @@
+// Copyright 2024 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include "archive_artic.h"
+
+namespace FileSys {
+
+std::vector ArticArchive::BuildFSPath(const Path& path) {
+ std::vector ret(sizeof(u32) * 2);
+ u32* raw_data = reinterpret_cast(ret.data());
+ auto path_type = path.GetType();
+ auto binary = path.AsBinary();
+ raw_data[0] = static_cast(path_type);
+ raw_data[1] = static_cast(binary.size());
+ if (!binary.empty()) {
+ ret.insert(ret.end(), binary.begin(), binary.end());
+ }
+
+ // The insert may have invalidated the pointer
+ raw_data = reinterpret_cast(ret.data());
+ if (path_type != LowPathType::Binary && path_type != LowPathType::Invalid) {
+ if (path_type == LowPathType::Wchar) {
+ raw_data[1] += 2;
+ ret.push_back(0);
+ ret.push_back(0);
+ } else {
+ raw_data[1] += 1;
+ ret.push_back(0);
+ }
+ }
+
+ return ret;
+}
+
+Result ArticArchive::RespResult(const std::optional& resp) {
+ if (!resp.has_value() || !resp->Succeeded()) {
+ return ResultUnknown;
+ }
+ return Result(static_cast(resp->GetMethodResult()));
+}
+
+ArticArchive::~ArticArchive() {
+ if (clear_cache_on_close) {
+ cache_provider->ClearAllCache();
+ }
+ if (archive_handle != -1) {
+ auto req = client->NewRequest("FSUSER_CloseArchive");
+ req.AddParameterS64(archive_handle);
+ client->Send(req);
+ if (report_artic_event != Core::PerfStats::PerfArticEventBits::NONE) {
+ client->ReportArticEvent(static_cast(report_artic_event));
+ }
+ }
+}
+
+ResultVal> ArticArchive::Open(
+ std::shared_ptr& client, Service::FS::ArchiveIdCode archive_id,
+ const Path& path, Core::PerfStats::PerfArticEventBits report_artic_event,
+ ArticCacheProvider& cache_provider, bool clear_cache_on_close) {
+
+ auto req = client->NewRequest("FSUSER_OpenArchive");
+
+ req.AddParameterS32(static_cast(archive_id));
+ auto path_buf = BuildFSPath(path);
+ req.AddParameterBuffer(path_buf.data(), path_buf.size());
+
+ auto resp = client->Send(req);
+ if (!resp.has_value() || !resp->Succeeded()) {
+ return ResultUnknown;
+ }
+ Result res(static_cast(resp->GetMethodResult()));
+ if (res.IsError())
+ return res;
+
+ auto handle_opt = resp->GetResponseS64(0);
+ if (!handle_opt.has_value()) {
+ return ResultUnknown;
+ }
+
+ return std::make_unique(client, *handle_opt, report_artic_event, cache_provider,
+ path, clear_cache_on_close);
+}
+
+void ArticArchive::Close() {
+ if (clear_cache_on_close) {
+ cache_provider->ClearAllCache();
+ }
+
+ auto req = client->NewRequest("FSUSER_CloseArchive");
+ req.AddParameterS64(archive_handle);
+ if (RespResult(client->Send(req)).IsSuccess()) {
+ archive_handle = -1;
+ if (report_artic_event != Core::PerfStats::PerfArticEventBits::NONE) {
+ client->ReportArticEvent(static_cast(report_artic_event));
+ }
+ }
+}
+
+std::string ArticArchive::GetName() const {
+ return "ArticArchive";
+}
+
+ResultVal> ArticArchive::OpenFile(const Path& path, const Mode& mode,
+ u32 attributes) {
+ if (mode.create_flag) {
+ auto cache = cache_provider->ProvideCache(
+ client, cache_provider->PathsToVector(archive_path, path), false);
+ if (cache != nullptr) {
+ cache->Clear();
+ }
+ }
+ auto req = client->NewRequest("FSUSER_OpenFile");
+
+ req.AddParameterS64(archive_handle);
+ auto path_buf = BuildFSPath(path);
+ req.AddParameterBuffer(path_buf.data(), path_buf.size());
+ req.AddParameterU32(mode.hex);
+ req.AddParameterU32(attributes);
+
+ auto resp = client->Send(req);
+ auto res = RespResult(resp);
+ if (res.IsError())
+ return res;
+
+ auto handle_opt = resp->GetResponseS32(0);
+ if (!handle_opt.has_value())
+ return ResultUnknown;
+
+ auto size_opt = resp->GetResponseU64(1);
+ if (size_opt.has_value()) {
+ auto cache = cache_provider->ProvideCache(
+ client, cache_provider->PathsToVector(archive_path, path), true);
+ if (cache != nullptr) {
+ cache->ForceSetSize(static_cast(*size_opt));
+ }
+ }
+
+ if (open_reporter->open_files++ == 0 &&
+ report_artic_event != Core::PerfStats::PerfArticEventBits::NONE) {
+ client->ReportArticEvent(static_cast(report_artic_event) | (1ULL << 32));
+ }
+
+ return std::make_unique(client, *handle_opt, open_reporter, archive_path,
+ *cache_provider, path);
+}
+
+Result ArticArchive::DeleteFile(const Path& path) const {
+ auto cache = cache_provider->ProvideCache(
+ client, cache_provider->PathsToVector(archive_path, path), false);
+ if (cache != nullptr) {
+ cache->Clear();
+ }
+
+ auto req = client->NewRequest("FSUSER_DeleteFile");
+
+ req.AddParameterS64(archive_handle);
+ auto path_buf = BuildFSPath(path);
+ req.AddParameterBuffer(path_buf.data(), path_buf.size());
+
+ return RespResult(client->Send(req));
+}
+
+Result ArticArchive::RenameFile(const Path& src_path, const Path& dest_path) const {
+ auto cache = cache_provider->ProvideCache(
+ client, cache_provider->PathsToVector(archive_path, src_path), false);
+ if (cache != nullptr) {
+ cache->Clear();
+ }
+ cache = cache_provider->ProvideCache(
+ client, cache_provider->PathsToVector(archive_path, dest_path), false);
+ if (cache != nullptr) {
+ cache->Clear();
+ }
+
+ auto req = client->NewRequest("FSUSER_RenameFile");
+
+ req.AddParameterS64(archive_handle);
+ auto src_path_buf = BuildFSPath(src_path);
+ req.AddParameterBuffer(src_path_buf.data(), src_path_buf.size());
+ req.AddParameterS64(archive_handle);
+ auto dest_path_buf = BuildFSPath(dest_path);
+ req.AddParameterBuffer(dest_path_buf.data(), dest_path_buf.size());
+
+ return RespResult(client->Send(req));
+}
+
+Result ArticArchive::DeleteDirectory(const Path& path) const {
+ cache_provider->ClearAllCache();
+
+ auto req = client->NewRequest("FSUSER_DeleteDirectory");
+
+ req.AddParameterS64(archive_handle);
+ auto path_buf = BuildFSPath(path);
+ req.AddParameterBuffer(path_buf.data(), path_buf.size());
+
+ return RespResult(client->Send(req));
+}
+
+Result ArticArchive::DeleteDirectoryRecursively(const Path& path) const {
+ cache_provider->ClearAllCache();
+
+ auto req = client->NewRequest("FSUSER_DeleteDirectoryRec");
+
+ req.AddParameterS64(archive_handle);
+ auto path_buf = BuildFSPath(path);
+ req.AddParameterBuffer(path_buf.data(), path_buf.size());
+
+ return RespResult(client->Send(req));
+}
+
+Result ArticArchive::CreateFile(const Path& path, u64 size, u32 attributes) const {
+ auto cache = cache_provider->ProvideCache(
+ client, cache_provider->PathsToVector(archive_path, path), false);
+ if (cache != nullptr) {
+ cache->Clear();
+ }
+
+ auto req = client->NewRequest("FSUSER_CreateFile");
+
+ req.AddParameterS64(archive_handle);
+ auto path_buf = BuildFSPath(path);
+ req.AddParameterBuffer(path_buf.data(), path_buf.size());
+ req.AddParameterU32(attributes);
+ req.AddParameterU64(size);
+
+ return RespResult(client->Send(req));
+}
+
+Result ArticArchive::CreateDirectory(const Path& path, u32 attributes) const {
+ auto req = client->NewRequest("FSUSER_CreateDirectory");
+
+ req.AddParameterS64(archive_handle);
+ auto path_buf = BuildFSPath(path);
+ req.AddParameterBuffer(path_buf.data(), path_buf.size());
+ req.AddParameterU32(attributes);
+
+ return RespResult(client->Send(req));
+}
+
+Result ArticArchive::RenameDirectory(const Path& src_path, const Path& dest_path) const {
+ cache_provider->ClearAllCache();
+
+ auto req = client->NewRequest("FSUSER_RenameDirectory");
+
+ req.AddParameterS64(archive_handle);
+ auto src_path_buf = BuildFSPath(src_path);
+ req.AddParameterBuffer(src_path_buf.data(), src_path_buf.size());
+ req.AddParameterS64(archive_handle);
+ auto dest_path_buf = BuildFSPath(dest_path);
+ req.AddParameterBuffer(dest_path_buf.data(), dest_path_buf.size());
+
+ return RespResult(client->Send(req));
+}
+
+ResultVal> ArticArchive::OpenDirectory(const Path& path) {
+ auto req = client->NewRequest("FSUSER_OpenDirectory");
+
+ req.AddParameterS64(archive_handle);
+ auto path_buf = BuildFSPath(path);
+ req.AddParameterBuffer(path_buf.data(), path_buf.size());
+
+ auto resp = client->Send(req);
+ auto res = RespResult(resp);
+ if (res.IsError())
+ return res;
+
+ auto handle_opt = resp->GetResponseS32(0);
+ if (!handle_opt.has_value())
+ return ResultUnknown;
+
+ if (open_reporter->open_files++ == 0 &&
+ report_artic_event != Core::PerfStats::PerfArticEventBits::NONE) {
+ client->ReportArticEvent(static_cast(report_artic_event) | (1ULL << 32));
+ }
+
+ return std::make_unique(client, *handle_opt, archive_path,
+ open_reporter);
+}
+
+u64 ArticArchive::GetFreeBytes() const {
+ auto req = client->NewRequest("FSUSER_GetFreeBytes");
+
+ req.AddParameterS64(archive_handle);
+
+ auto resp = client->Send(req);
+ auto res = RespResult(resp);
+ if (res.IsError()) // TODO(PabloMK7): Return error code and not u64
+ return 0;
+
+ auto free_bytes_opt = resp->GetResponseS64(0);
+ return free_bytes_opt.has_value() ? static_cast(*free_bytes_opt) : 0;
+}
+
+Result ArticArchive::Control(u32 action, u8* input, size_t input_size, u8* output,
+ size_t output_size) {
+ auto req = client->NewRequest("FSUSER_ControlArchive");
+
+ req.AddParameterS64(archive_handle);
+ req.AddParameterU32(action);
+ req.AddParameterBuffer(input, input_size);
+ req.AddParameterU32(static_cast(output_size));
+
+ auto resp = client->Send(req);
+ auto res = RespResult(resp);
+ if (res.IsError())
+ return res;
+
+ auto output_buf = resp->GetResponseBuffer(0);
+ if (!output_buf.has_value())
+ return res;
+
+ if (output_buf->second != output_size)
+ return ResultUnknown;
+
+ memcpy(output, output_buf->first, output_buf->second);
+ return res;
+}
+
+Result ArticArchive::SetSaveDataSecureValue(u32 secure_value_slot, u64 secure_value, bool flush) {
+ auto req = client->NewRequest("FSUSER_SetSaveDataSecureValue");
+
+ req.AddParameterS64(archive_handle);
+ req.AddParameterU32(secure_value_slot);
+ req.AddParameterU64(secure_value);
+ req.AddParameterS8(flush != 0);
+
+ return RespResult(client->Send(req));
+}
+
+ResultVal> ArticArchive::GetSaveDataSecureValue(u32 secure_value_slot) {
+ auto req = client->NewRequest("FSUSER_GetSaveDataSecureValue");
+
+ req.AddParameterS64(archive_handle);
+ req.AddParameterU32(secure_value_slot);
+
+ auto resp = client->Send(req);
+ auto res = RespResult(resp);
+ if (res.IsError())
+ return res;
+
+ struct {
+ bool exists;
+ bool isGamecard;
+ u64 secure_value;
+ } secure_value_result;
+ static_assert(sizeof(secure_value_result) == 0x10);
+
+ auto output_buf = resp->GetResponseBuffer(0);
+ if (!output_buf.has_value())
+ return res;
+
+ if (output_buf->second != sizeof(secure_value_result))
+ return ResultUnknown;
+
+ memcpy(&secure_value_result, output_buf->first, output_buf->second);
+ return std::make_tuple(secure_value_result.exists, secure_value_result.isGamecard,
+ secure_value_result.secure_value);
+}
+
+void ArticArchive::OpenFileReporter::OnFileClosed() {
+ if (--open_files == 0 && report_artic_event != Core::PerfStats::PerfArticEventBits::NONE) {
+ client->ReportArticEvent(static_cast(report_artic_event));
+ }
+}
+
+void ArticArchive::OpenFileReporter::OnDirectoryClosed() {
+ if (--open_files == 0 && report_artic_event != Core::PerfStats::PerfArticEventBits::NONE) {
+ client->ReportArticEvent(static_cast(report_artic_event));
+ }
+}
+
+ArticFileBackend::~ArticFileBackend() {
+ if (file_handle != -1) {
+ auto req = client->NewRequest("FSFILE_Close");
+ req.AddParameterS32(file_handle);
+ client->Send(req);
+ open_reporter->OnFileClosed();
+ }
+}
+
+ResultVal ArticFileBackend::Read(u64 offset, std::size_t length, u8* buffer) const {
+ auto cache = cache_provider->ProvideCache(
+ client, cache_provider->PathsToVector(archive_path, file_path), true);
+
+ if (cache != nullptr && (offset + static_cast(length)) < GetSize()) {
+ return cache->Read(file_handle, offset, length, buffer);
+ }
+
+ size_t read_amount = 0;
+ while (read_amount != length) {
+ size_t to_read =
+ std::min(client->GetServerRequestMaxSize() - 0x100, length - read_amount);
+
+ auto req = client->NewRequest("FSFILE_Read");
+ req.AddParameterS32(file_handle);
+ req.AddParameterS64(static_cast(offset + read_amount));
+ req.AddParameterS32(static_cast(to_read));
+ auto resp = client->Send(req);
+ if (!resp.has_value() || !resp->Succeeded())
+ return Result(-1);
+
+ auto res = Result(static_cast(resp->GetMethodResult()));
+ if (res.IsError())
+ return res;
+
+ auto read_buff = resp->GetResponseBuffer(0);
+ size_t actually_read = 0;
+ if (read_buff.has_value()) {
+ actually_read = read_buff->second;
+ memcpy(buffer + read_amount, read_buff->first, actually_read);
+ }
+
+ read_amount += actually_read;
+ if (actually_read != to_read)
+ break;
+ }
+ return read_amount;
+}
+
+ResultVal ArticFileBackend::Write(u64 offset, std::size_t length, bool flush,
+ bool update_timestamp, const u8* buffer) {
+ u32 flags = (flush ? 1 : 0) | (update_timestamp ? (1 << 8) : 0);
+ auto cache = cache_provider->ProvideCache(
+ client, cache_provider->PathsToVector(archive_path, file_path), true);
+ if (cache != nullptr) {
+ return cache->Write(file_handle, offset, length, buffer, flags);
+ } else {
+ size_t written_amount = 0;
+ while (written_amount != length) {
+ size_t to_write = std::min(client->GetServerRequestMaxSize() - 0x100,
+ length - written_amount);
+
+ auto req = client->NewRequest("FSFILE_Write");
+ req.AddParameterS32(file_handle);
+ req.AddParameterS64(static_cast(offset + written_amount));
+ req.AddParameterS32(static_cast(to_write));
+ req.AddParameterS32(static_cast(flags));
+ req.AddParameterBuffer(buffer + written_amount, to_write);
+ auto resp = client->Send(req);
+ if (!resp.has_value() || !resp->Succeeded())
+ return Result(-1);
+
+ auto res = Result(static_cast(resp->GetMethodResult()));
+ if (res.IsError())
+ return res;
+
+ auto actually_written_opt = resp->GetResponseS32(0);
+ if (!actually_written_opt.has_value())
+ return Result(-1);
+
+ size_t actually_written = static_cast(actually_written_opt.value());
+
+ written_amount += actually_written;
+ if (actually_written != to_write)
+ break;
+ }
+ return written_amount;
+ }
+}
+
+u64 ArticFileBackend::GetSize() const {
+ auto cache = cache_provider->ProvideCache(
+ client, cache_provider->PathsToVector(archive_path, file_path), true);
+ if (cache != nullptr) {
+ auto res = cache->GetSize(file_handle);
+ if (res.Failed())
+ return 0;
+ return res.Unwrap();
+ } else {
+
+ auto req = client->NewRequest("FSFILE_GetSize");
+
+ req.AddParameterS32(file_handle);
+
+ auto resp = client->Send(req);
+ auto res = ArticArchive::RespResult(resp);
+ if (res.IsError())
+ return 0;
+
+ auto size_buf = resp->GetResponseS64(0);
+ if (!size_buf) {
+ return 0;
+ }
+ return *size_buf;
+ }
+}
+
+bool ArticFileBackend::SetSize(u64 size) const {
+ auto req = client->NewRequest("FSFILE_SetSize");
+
+ req.AddParameterS32(file_handle);
+ req.AddParameterU64(size);
+
+ return ArticArchive::RespResult(client->Send(req)).IsSuccess();
+}
+
+bool ArticFileBackend::Close() {
+ auto req = client->NewRequest("FSFILE_Close");
+ req.AddParameterS32(file_handle);
+ bool ret = ArticArchive::RespResult(client->Send(req)).IsSuccess();
+ if (ret) {
+ file_handle = -1;
+ open_reporter->OnFileClosed();
+ }
+ return ret;
+}
+
+void ArticFileBackend::Flush() const {
+ auto req = client->NewRequest("FSFILE_Flush");
+
+ req.AddParameterS32(file_handle);
+
+ client->Send(req);
+}
+
+ArticDirectoryBackend::~ArticDirectoryBackend() {
+ if (dir_handle != -1) {
+ auto req = client->NewRequest("FSDIR_Close");
+ req.AddParameterS32(dir_handle);
+ client->Send(req);
+ open_reporter->OnDirectoryClosed();
+ }
+}
+
+u32 ArticDirectoryBackend::Read(const u32 count, Entry* entries) {
+ auto req = client->NewRequest("FSDIR_Read");
+
+ req.AddParameterS32(dir_handle);
+ req.AddParameterU32(count);
+
+ auto resp = client->Send(req);
+ auto res = ArticArchive::RespResult(resp);
+ if (res.IsError())
+ return 0;
+
+ auto entry_buf = resp->GetResponseBuffer(0);
+ if (!entry_buf) {
+ return 0;
+ }
+ u32 ret_count = static_cast(entry_buf->second / sizeof(Entry));
+
+ memcpy(entries, entry_buf->first, ret_count * sizeof(Entry));
+ return ret_count;
+}
+
+bool ArticDirectoryBackend::Close() {
+ auto req = client->NewRequest("FSDIR_Close");
+ req.AddParameterS32(dir_handle);
+ bool ret = ArticArchive::RespResult(client->Send(req)).IsSuccess();
+ if (ret) {
+ dir_handle = -1;
+ open_reporter->OnDirectoryClosed();
+ }
+ return ret;
+}
+} // namespace FileSys
diff --git a/src/core/file_sys/archive_artic.h b/src/core/file_sys/archive_artic.h
new file mode 100644
index 000000000..beb0f63b2
--- /dev/null
+++ b/src/core/file_sys/archive_artic.h
@@ -0,0 +1,268 @@
+// Copyright 2024 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include "atomic"
+
+#include
+#include "common/common_types.h"
+#include "core/file_sys/archive_backend.h"
+#include "core/file_sys/artic_cache.h"
+#include "core/file_sys/directory_backend.h"
+#include "core/file_sys/file_backend.h"
+#include "core/hle/service/fs/archive.h"
+#include "core/perf_stats.h"
+#include "network/artic_base/artic_base_client.h"
+
+namespace FileSys {
+
+class ArticArchive : public ArchiveBackend {
+public:
+ static std::vector BuildFSPath(const Path& path);
+ static Result RespResult(const std::optional& resp);
+
+ explicit ArticArchive(std::shared_ptr& _client, s64 _archive_handle,
+ Core::PerfStats::PerfArticEventBits _report_artic_event,
+ ArticCacheProvider& _cache_provider, const Path& _archive_path,
+ bool _clear_cache_on_close)
+ : client(_client), archive_handle(_archive_handle), report_artic_event(_report_artic_event),
+ cache_provider(&_cache_provider), archive_path(_archive_path),
+ clear_cache_on_close(_clear_cache_on_close) {
+ open_reporter = std::make_shared(_client, _report_artic_event);
+ }
+ ~ArticArchive() override;
+
+ static ResultVal> Open(
+ std::shared_ptr& client, Service::FS::ArchiveIdCode archive_id,
+ const Path& path, Core::PerfStats::PerfArticEventBits report_artic_event,
+ ArticCacheProvider& cache_provider, bool clear_cache_on_close);
+
+ void Close() override;
+
+ /**
+ * Get a descriptive name for the archive (e.g. "RomFS", "SaveData", etc.)
+ */
+ std::string GetName() const override;
+
+ /**
+ * Open a file specified by its path, using the specified mode
+ * @param path Path relative to the archive
+ * @param mode Mode to open the file with
+ * @return Opened file, or error code
+ */
+ ResultVal> OpenFile(const Path& path, const Mode& mode,
+ u32 attributes) override;
+
+ /**
+ * Delete a file specified by its path
+ * @param path Path relative to the archive
+ * @return Result of the operation
+ */
+ Result DeleteFile(const Path& path) const override;
+
+ /**
+ * Rename a File specified by its path
+ * @param src_path Source path relative to the archive
+ * @param dest_path Destination path relative to the archive
+ * @return Result of the operation
+ */
+ Result RenameFile(const Path& src_path, const Path& dest_path) const override;
+
+ /**
+ * Delete a directory specified by its path
+ * @param path Path relative to the archive
+ * @return Result of the operation
+ */
+ Result DeleteDirectory(const Path& path) const override;
+
+ /**
+ * Delete a directory specified by its path and anything under it
+ * @param path Path relative to the archive
+ * @return Result of the operation
+ */
+ Result DeleteDirectoryRecursively(const Path& path) const override;
+
+ /**
+ * Create a file specified by its path
+ * @param path Path relative to the Archive
+ * @param size The size of the new file, filled with zeroes
+ * @return Result of the operation
+ */
+ Result CreateFile(const Path& path, u64 size, u32 attributes) const override;
+
+ /**
+ * Create a directory specified by its path
+ * @param path Path relative to the archive
+ * @return Result of the operation
+ */
+ Result CreateDirectory(const Path& path, u32 attributes) const override;
+
+ /**
+ * Rename a Directory specified by its path
+ * @param src_path Source path relative to the archive
+ * @param dest_path Destination path relative to the archive
+ * @return Result of the operation
+ */
+ Result RenameDirectory(const Path& src_path, const Path& dest_path) const override;
+
+ /**
+ * Open a directory specified by its path
+ * @param path Path relative to the archive
+ * @return Opened directory, or error code
+ */
+ ResultVal> OpenDirectory(const Path& path) override;
+
+ /**
+ * Get the free space
+ * @return The number of free bytes in the archive
+ */
+ u64 GetFreeBytes() const override;
+
+ Result Control(u32 action, u8* input, size_t input_size, u8* output,
+ size_t output_size) override;
+
+ Result SetSaveDataSecureValue(u32 secure_value_slot, u64 secure_value, bool flush) override;
+
+ ResultVal> GetSaveDataSecureValue(u32 secure_value_slot) override;
+
+ bool IsSlow() override {
+ return true;
+ }
+
+ const Path& GetArchivePath() {
+ return archive_path;
+ }
+
+protected:
+ ArticArchive() = default;
+
+private:
+ friend class ArticFileBackend;
+ friend class ArticDirectoryBackend;
+ class OpenFileReporter {
+ public:
+ OpenFileReporter(const std::shared_ptr& cli,
+ Core::PerfStats::PerfArticEventBits _report_artic_event)
+ : client(cli), report_artic_event(_report_artic_event) {}
+
+ void OnFileClosed();
+
+ void OnDirectoryClosed();
+
+ std::shared_ptr client;
+ Core::PerfStats::PerfArticEventBits report_artic_event =
+ Core::PerfStats::PerfArticEventBits::NONE;
+ std::atomic open_files = 0;
+ };
+
+ std::shared_ptr client;
+ s64 archive_handle;
+ std::shared_ptr open_reporter;
+ Core::PerfStats::PerfArticEventBits report_artic_event =
+ Core::PerfStats::PerfArticEventBits::NONE;
+ ArticCacheProvider* cache_provider = nullptr;
+ Path archive_path;
+ bool clear_cache_on_close;
+
+ template
+ void serialize(Archive& ar, const unsigned int) {
+ ar& boost::serialization::base_object(*this);
+ ar& archive_handle;
+ }
+ friend class boost::serialization::access;
+};
+
+class ArticFileBackend : public FileBackend {
+public:
+ explicit ArticFileBackend(std::shared_ptr& _client,
+ s32 _file_handle,
+ const std::shared_ptr& _open_reporter,
+ const Path& _archive_path, ArticCacheProvider& _cache_provider,
+ const Path& _file_path)
+ : client(_client), file_handle(_file_handle), open_reporter(_open_reporter),
+ archive_path(_archive_path), cache_provider(&_cache_provider), file_path(_file_path) {}
+ ~ArticFileBackend() override;
+
+ ResultVal Read(u64 offset, std::size_t length, u8* buffer) const override;
+
+ ResultVal Write(u64 offset, std::size_t length, bool flush, bool update_timestamp,
+ const u8* buffer) override;
+
+ u64 GetSize() const override;
+
+ bool SetSize(u64 size) const override;
+
+ bool Close() override;
+
+ void Flush() const override;
+
+ bool AllowsCachedReads() const override {
+ return true;
+ }
+
+ bool CacheReady(std::size_t file_offset, std::size_t length) override {
+ auto cache = cache_provider->ProvideCache(
+ client, cache_provider->PathsToVector(archive_path, file_path), true);
+ if (cache == nullptr) {
+ return false;
+ }
+ return cache->CacheReady(file_offset, length);
+ }
+
+protected:
+ ArticFileBackend() = default;
+
+private:
+ std::shared_ptr client;
+ s32 file_handle;
+ std::shared_ptr open_reporter;
+ Path archive_path;
+ ArticCacheProvider* cache_provider = nullptr;
+ Path file_path;
+
+ template
+ void serialize(Archive& ar, const unsigned int) {
+ ar& boost::serialization::base_object(*this);
+ ar& file_handle;
+ }
+ friend class boost::serialization::access;
+};
+
+class ArticDirectoryBackend : public DirectoryBackend {
+public:
+ explicit ArticDirectoryBackend(
+ std::shared_ptr& _client, s32 _dir_handle,
+ const Path& _archive_path,
+ const std::shared_ptr& _open_reporter)
+ : client(_client), dir_handle(_dir_handle), archive_path(_archive_path),
+ open_reporter(_open_reporter) {}
+ ~ArticDirectoryBackend() override;
+
+ u32 Read(const u32 count, Entry* entries) override;
+ bool Close() override;
+
+ bool IsSlow() override {
+ return true;
+ }
+
+protected:
+ ArticDirectoryBackend() = default;
+
+private:
+ std::shared_ptr client;
+ s32 dir_handle;
+ Path archive_path;
+ std::shared_ptr open_reporter;
+
+ template
+ void serialize(Archive& ar, const unsigned int) {
+ ar& boost::serialization::base_object(*this);
+ ar& dir_handle;
+ }
+ friend class boost::serialization::access;
+};
+} // namespace FileSys
+
+BOOST_CLASS_EXPORT_KEY(FileSys::ArticArchive)
+BOOST_CLASS_EXPORT_KEY(FileSys::ArticFileBackend)
+BOOST_CLASS_EXPORT_KEY(FileSys::ArticDirectoryBackend)
\ No newline at end of file
diff --git a/src/core/file_sys/archive_backend.cpp b/src/core/file_sys/archive_backend.cpp
index bc4df30d0..1600171fa 100644
--- a/src/core/file_sys/archive_backend.cpp
+++ b/src/core/file_sys/archive_backend.cpp
@@ -105,8 +105,7 @@ std::vector Path::AsBinary() const {
std::vector to_return(u16str.size() * 2);
for (std::size_t i = 0; i < u16str.size(); ++i) {
u16 tmp_char = u16str.at(i);
- to_return[i * 2] = (tmp_char & 0xFF00) >> 8;
- to_return[i * 2 + 1] = (tmp_char & 0x00FF);
+ *reinterpret_cast(to_return.data() + i * 2) = tmp_char;
}
return to_return;
}
diff --git a/src/core/file_sys/archive_backend.h b/src/core/file_sys/archive_backend.h
index 7eb3893ba..7997c72ab 100644
--- a/src/core/file_sys/archive_backend.h
+++ b/src/core/file_sys/archive_backend.h
@@ -103,6 +103,7 @@ struct ArchiveFormatInfo {
u8 duplicate_data; ///< Whether the archive should duplicate the data.
};
static_assert(std::is_trivial_v, "ArchiveFormatInfo is not POD");
+static_assert(sizeof(ArchiveFormatInfo) == 16, "Invalid ArchiveFormatInfo size");
class ArchiveBackend : NonCopyable {
public:
@@ -119,8 +120,8 @@ public:
* @param mode Mode to open the file with
* @return Opened file, or error code
*/
- virtual ResultVal> OpenFile(const Path& path,
- const Mode& mode) const = 0;
+ virtual ResultVal> OpenFile(const Path& path, const Mode& mode,
+ u32 attributes = 0) = 0;
/**
* Delete a file specified by its path
@@ -157,14 +158,14 @@ public:
* @param size The size of the new file, filled with zeroes
* @return Result of the operation
*/
- virtual Result CreateFile(const Path& path, u64 size) const = 0;
+ virtual Result CreateFile(const Path& path, u64 size, u32 attributes = 0) const = 0;
/**
* Create a directory specified by its path
* @param path Path relative to the archive
* @return Result of the operation
*/
- virtual Result CreateDirectory(const Path& path) const = 0;
+ virtual Result CreateDirectory(const Path& path, u32 attributes = 0) const = 0;
/**
* Rename a Directory specified by its path
@@ -179,7 +180,7 @@ public:
* @param path Path relative to the archive
* @return Opened directory, or error code
*/
- virtual ResultVal> OpenDirectory(const Path& path) const = 0;
+ virtual ResultVal> OpenDirectory(const Path& path) = 0;
/**
* Get the free space
@@ -187,6 +188,20 @@ public:
*/
virtual u64 GetFreeBytes() const = 0;
+ /**
+ * Close the archive
+ */
+ virtual void Close() {}
+
+ virtual Result Control(u32 action, u8* input, size_t input_size, u8* output,
+ size_t output_size) {
+ LOG_WARNING(Service_FS,
+ "(STUBBED) called, archive={}, action={:08X}, input_size={:08X}, "
+ "output_size={:08X}",
+ GetName(), action, input_size, output_size);
+ return ResultSuccess;
+ }
+
u64 GetOpenDelayNs() {
if (delay_generator != nullptr) {
return delay_generator->GetOpenDelayNs();
@@ -196,6 +211,31 @@ public:
return delay_generator->GetOpenDelayNs();
}
+ virtual Result SetSaveDataSecureValue(u32 secure_value_slot, u64 secure_value, bool flush) {
+
+ // TODO: Generate and Save the Secure Value
+
+ LOG_WARNING(Service_FS,
+ "(STUBBED) called, value=0x{:016x} secure_value_slot=0x{:04X} "
+ "flush={}",
+ secure_value, secure_value_slot, flush);
+
+ return ResultSuccess;
+ }
+
+ virtual ResultVal> GetSaveDataSecureValue(u32 secure_value_slot) {
+
+ // TODO: Implement Secure Value Lookup & Generation
+
+ LOG_WARNING(Service_FS, "(STUBBED) called secure_value_slot=0x{:08X}", secure_value_slot);
+
+ return std::make_tuple(false, true, 0);
+ }
+
+ virtual bool IsSlow() {
+ return false;
+ }
+
protected:
std::unique_ptr delay_generator;
@@ -232,7 +272,7 @@ public:
* @return Result of the operation, 0 on success
*/
virtual Result Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info,
- u64 program_id) = 0;
+ u64 program_id, u32 directory_buckets, u32 file_buckets) = 0;
/**
* Retrieves the format info about the archive with the specified path
@@ -242,6 +282,10 @@ public:
*/
virtual ResultVal GetFormatInfo(const Path& path, u64 program_id) const = 0;
+ virtual bool IsSlow() {
+ return false;
+ }
+
template
void serialize(Archive& ar, const unsigned int) {}
friend class boost::serialization::access;
diff --git a/src/core/file_sys/archive_extsavedata.cpp b/src/core/file_sys/archive_extsavedata.cpp
index cfa97fe7f..4e9ea6ef8 100644
--- a/src/core/file_sys/archive_extsavedata.cpp
+++ b/src/core/file_sys/archive_extsavedata.cpp
@@ -10,6 +10,7 @@
#include "common/common_types.h"
#include "common/file_util.h"
#include "common/logging/log.h"
+#include "core/file_sys/archive_artic.h"
#include "core/file_sys/archive_extsavedata.h"
#include "core/file_sys/disk_archive.h"
#include "core/file_sys/errors.h"
@@ -37,7 +38,7 @@ public:
return false;
}
- ResultVal Write(u64 offset, std::size_t length, bool flush,
+ ResultVal Write(u64 offset, std::size_t length, bool flush, bool update_timestamp,
const u8* buffer) override {
if (offset > size) {
return ResultWriteBeyondEnd;
@@ -49,7 +50,7 @@ public:
length = size - offset;
}
- return DiskFile::Write(offset, length, flush, buffer);
+ return DiskFile::Write(offset, length, flush, update_timestamp, buffer);
}
private:
@@ -100,8 +101,8 @@ public:
return "ExtSaveDataArchive: " + mount_point;
}
- ResultVal> OpenFile(const Path& path,
- const Mode& mode) const override {
+ ResultVal> OpenFile(const Path& path, const Mode& mode,
+ u32 attributes) override {
LOG_DEBUG(Service_FS, "called path={} mode={:01X}", path.DebugStr(), mode.hex);
const PathParser path_parser(path);
@@ -234,69 +235,193 @@ Path ArchiveFactory_ExtSaveData::GetCorrectedPath(const Path& path) {
return {binary_data};
}
-ResultVal> ArchiveFactory_ExtSaveData::Open(const Path& path,
- u64 program_id) {
- const auto directory = type == ExtSaveDataType::Boss ? "boss/" : "user/";
- const auto fullpath = GetExtSaveDataPath(mount_point, GetCorrectedPath(path)) + directory;
- if (!FileUtil::Exists(fullpath)) {
- // TODO(Subv): Verify the archive behavior of SharedExtSaveData compared to ExtSaveData.
- // ExtSaveData seems to return FS_NotFound (120) when the archive doesn't exist.
- if (type != ExtSaveDataType::Shared) {
- return ResultNotFoundInvalidState;
- } else {
- return ResultNotFormatted;
- }
+static Service::FS::ArchiveIdCode ExtSaveDataTypeToArchiveID(ExtSaveDataType type) {
+ switch (type) {
+ case FileSys::ExtSaveDataType::Normal:
+ return Service::FS::ArchiveIdCode::ExtSaveData;
+ case FileSys::ExtSaveDataType::Shared:
+ return Service::FS::ArchiveIdCode::SharedExtSaveData;
+ case FileSys::ExtSaveDataType::Boss:
+ return Service::FS::ArchiveIdCode::BossExtSaveData;
+ default:
+ return Service::FS::ArchiveIdCode::ExtSaveData;
}
- std::unique_ptr delay_generator = std::make_unique();
- return std::make_unique(fullpath, std::move(delay_generator));
}
-Result ArchiveFactory_ExtSaveData::Format(const Path& path,
- const FileSys::ArchiveFormatInfo& format_info,
- u64 program_id) {
- auto corrected_path = GetCorrectedPath(path);
-
- // These folders are always created with the ExtSaveData
- std::string user_path = GetExtSaveDataPath(mount_point, corrected_path) + "user/";
- std::string boss_path = GetExtSaveDataPath(mount_point, corrected_path) + "boss/";
- FileUtil::CreateFullPath(user_path);
- FileUtil::CreateFullPath(boss_path);
-
- // Write the format metadata
- std::string metadata_path = GetExtSaveDataPath(mount_point, corrected_path) + "metadata";
- FileUtil::IOFile file(metadata_path, "wb");
-
- if (!file.IsOpen()) {
- // TODO(Subv): Find the correct error code
- return ResultUnknown;
+static Core::PerfStats::PerfArticEventBits ExtSaveDataTypeToPerfArtic(ExtSaveDataType type) {
+ switch (type) {
+ case FileSys::ExtSaveDataType::Normal:
+ return Core::PerfStats::PerfArticEventBits::ARTIC_EXT_DATA;
+ case FileSys::ExtSaveDataType::Shared:
+ return Core::PerfStats::PerfArticEventBits::ARTIC_SHARED_EXT_DATA;
+ case FileSys::ExtSaveDataType::Boss:
+ return Core::PerfStats::PerfArticEventBits::ARTIC_BOSS_EXT_DATA;
+ default:
+ return Core::PerfStats::PerfArticEventBits::ARTIC_EXT_DATA;
}
+}
- file.WriteBytes(&format_info, sizeof(format_info));
- return ResultSuccess;
+ResultVal> ArchiveFactory_ExtSaveData::Open(const Path& path,
+ u64 program_id) {
+ if (IsUsingArtic()) {
+ EnsureCacheCreated();
+ return ArticArchive::Open(artic_client, ExtSaveDataTypeToArchiveID(type), path,
+ ExtSaveDataTypeToPerfArtic(type), *this,
+ type != FileSys::ExtSaveDataType::Normal);
+ } else {
+ const auto directory = type == ExtSaveDataType::Boss ? "boss/" : "user/";
+ const auto fullpath = GetExtSaveDataPath(mount_point, GetCorrectedPath(path)) + directory;
+ if (!FileUtil::Exists(fullpath)) {
+ // TODO(Subv): Verify the archive behavior of SharedExtSaveData compared to ExtSaveData.
+ // ExtSaveData seems to return FS_NotFound (120) when the archive doesn't exist.
+ if (type != ExtSaveDataType::Shared) {
+ return ResultNotFoundInvalidState;
+ } else {
+ return ResultNotFormatted;
+ }
+ }
+ std::unique_ptr delay_generator =
+ std::make_unique();
+ return std::make_unique(fullpath, std::move(delay_generator));
+ }
+}
+
+Result ArchiveFactory_ExtSaveData::FormatAsExtData(const Path& path,
+ const FileSys::ArchiveFormatInfo& format_info,
+ u8 unknown, u64 program_id, u64 total_size,
+ std::optional> icon) {
+ if (IsUsingArtic()) {
+ if (!icon.has_value()) {
+ LOG_ERROR(Service_FS, "No icon provided while using Artic Base");
+ return ResultUnknown;
+ }
+
+ ExtSaveDataArchivePath path_data;
+ std::memcpy(&path_data, path.AsBinary().data(), sizeof(path_data));
+
+ Service::FS::ExtSaveDataInfo artic_extdata_path;
+
+ artic_extdata_path.media_type = static_cast(path_data.media_type);
+ artic_extdata_path.unknown = unknown;
+ artic_extdata_path.save_id_low = path_data.save_low;
+ artic_extdata_path.save_id_high = path_data.save_high;
+
+ auto req = artic_client->NewRequest("FSUSER_CreateExtSaveData");
+
+ req.AddParameterBuffer(&artic_extdata_path, sizeof(artic_extdata_path));
+ req.AddParameterU32(format_info.number_directories);
+ req.AddParameterU32(format_info.number_files);
+ req.AddParameterU64(total_size);
+ req.AddParameterBuffer(icon->data(), icon->size());
+
+ return ArticArchive::RespResult(artic_client->Send(req));
+ } else {
+ auto corrected_path = GetCorrectedPath(path);
+
+ // These folders are always created with the ExtSaveData
+ std::string user_path = GetExtSaveDataPath(mount_point, corrected_path) + "user/";
+ std::string boss_path = GetExtSaveDataPath(mount_point, corrected_path) + "boss/";
+ FileUtil::CreateFullPath(user_path);
+ FileUtil::CreateFullPath(boss_path);
+
+ // Write the format metadata
+ std::string metadata_path = GetExtSaveDataPath(mount_point, corrected_path) + "metadata";
+ FileUtil::IOFile file(metadata_path, "wb");
+
+ if (!file.IsOpen()) {
+ // TODO(Subv): Find the correct error code
+ return ResultUnknown;
+ }
+
+ file.WriteBytes(&format_info, sizeof(format_info));
+
+ if (icon.has_value()) {
+ FileUtil::IOFile icon_file(FileSys::GetExtSaveDataPath(GetMountPoint(), path) + "icon",
+ "wb");
+ icon_file.WriteBytes(icon->data(), icon->size());
+ }
+ return ResultSuccess;
+ }
+}
+
+Result ArchiveFactory_ExtSaveData::DeleteExtData(Service::FS::MediaType media_type, u8 unknown,
+ u32 high, u32 low) {
+ if (IsUsingArtic()) {
+ Service::FS::ExtSaveDataInfo artic_extdata_path;
+
+ artic_extdata_path.media_type = static_cast(media_type);
+ artic_extdata_path.unknown = unknown;
+ artic_extdata_path.save_id_low = low;
+ artic_extdata_path.save_id_high = high;
+
+ auto req = artic_client->NewRequest("FSUSER_DeleteExtSaveData");
+
+ req.AddParameterBuffer(&artic_extdata_path, sizeof(artic_extdata_path));
+
+ return ArticArchive::RespResult(artic_client->Send(req));
+ } else {
+ // Construct the binary path to the archive first
+ FileSys::Path path =
+ FileSys::ConstructExtDataBinaryPath(static_cast(media_type), high, low);
+
+ std::string media_type_directory;
+ if (media_type == Service::FS::MediaType::NAND) {
+ media_type_directory = FileUtil::GetUserPath(FileUtil::UserPath::NANDDir);
+ } else if (media_type == Service::FS::MediaType::SDMC) {
+ media_type_directory = FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir);
+ } else {
+ LOG_ERROR(Service_FS, "Unsupported media type {}", media_type);
+ return ResultUnknown; // TODO(Subv): Find the right error code
+ }
+
+ // Delete all directories (/user, /boss) and the icon file.
+ std::string base_path = FileSys::GetExtDataContainerPath(
+ media_type_directory, media_type == Service::FS::MediaType::NAND);
+ std::string extsavedata_path = FileSys::GetExtSaveDataPath(base_path, path);
+ if (FileUtil::Exists(extsavedata_path) && !FileUtil::DeleteDirRecursively(extsavedata_path))
+ return ResultUnknown; // TODO(Subv): Find the right error code
+ return ResultSuccess;
+ }
}
ResultVal ArchiveFactory_ExtSaveData::GetFormatInfo(const Path& path,
u64 program_id) const {
- std::string metadata_path = GetExtSaveDataPath(mount_point, path) + "metadata";
- FileUtil::IOFile file(metadata_path, "rb");
+ if (IsUsingArtic()) {
+ auto req = artic_client->NewRequest("FSUSER_GetFormatInfo");
- if (!file.IsOpen()) {
- LOG_ERROR(Service_FS, "Could not open metadata information for archive");
- // TODO(Subv): Verify error code
- return ResultNotFormatted;
+ req.AddParameterS32(static_cast(ExtSaveDataTypeToArchiveID(type)));
+ auto path_artic = ArticArchive::BuildFSPath(path);
+ req.AddParameterBuffer(path_artic.data(), path_artic.size());
+
+ auto resp = artic_client->Send(req);
+ Result res = ArticArchive::RespResult(resp);
+ if (R_FAILED(res)) {
+ return res;
+ }
+
+ auto info_buf = resp->GetResponseBuffer(0);
+ if (!info_buf.has_value() || info_buf->second != sizeof(ArchiveFormatInfo)) {
+ return ResultUnknown;
+ }
+
+ ArchiveFormatInfo info;
+ memcpy(&info, info_buf->first, sizeof(info));
+ return info;
+ } else {
+ std::string metadata_path = GetExtSaveDataPath(mount_point, path) + "metadata";
+ FileUtil::IOFile file(metadata_path, "rb");
+
+ if (!file.IsOpen()) {
+ LOG_ERROR(Service_FS, "Could not open metadata information for archive");
+ // TODO(Subv): Verify error code
+ return ResultNotFormatted;
+ }
+
+ ArchiveFormatInfo info = {};
+ file.ReadBytes(&info, sizeof(info));
+ return info;
}
-
- ArchiveFormatInfo info = {};
- file.ReadBytes(&info, sizeof(info));
- return info;
}
-
-void ArchiveFactory_ExtSaveData::WriteIcon(const Path& path, std::span icon) {
- std::string game_path = FileSys::GetExtSaveDataPath(GetMountPoint(), path);
- FileUtil::IOFile icon_file(game_path + "icon", "wb");
- icon_file.WriteBytes(icon.data(), icon.size());
-}
-
} // namespace FileSys
SERIALIZE_EXPORT_IMPL(FileSys::ExtSaveDataDelayGenerator)
diff --git a/src/core/file_sys/archive_extsavedata.h b/src/core/file_sys/archive_extsavedata.h
index 5093ecdc1..29589d418 100644
--- a/src/core/file_sys/archive_extsavedata.h
+++ b/src/core/file_sys/archive_extsavedata.h
@@ -5,13 +5,17 @@
#pragma once
#include
+#include
#include
#include
#include
#include
#include "common/common_types.h"
#include "core/file_sys/archive_backend.h"
+#include "core/file_sys/artic_cache.h"
#include "core/hle/result.h"
+#include "core/hle/service/fs/archive.h"
+#include "network/artic_base/artic_base_client.h"
namespace FileSys {
@@ -22,7 +26,7 @@ enum class ExtSaveDataType {
};
/// File system interface to the ExtSaveData archive
-class ArchiveFactory_ExtSaveData final : public ArchiveFactory {
+class ArchiveFactory_ExtSaveData final : public ArchiveFactory, public ArticCacheProvider {
public:
ArchiveFactory_ExtSaveData(const std::string& mount_point, ExtSaveDataType type_);
@@ -31,21 +35,35 @@ public:
}
ResultVal> Open(const Path& path, u64 program_id) override;
- Result Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info,
- u64 program_id) override;
+
ResultVal GetFormatInfo(const Path& path, u64 program_id) const override;
+ bool IsSlow() override {
+ return IsUsingArtic();
+ }
+
const std::string& GetMountPoint() const {
return mount_point;
}
- /**
- * Writes the SMDH icon of the ExtSaveData to file
- * @param path Path of this ExtSaveData
- * @param icon_data Binary data of the icon
- * @param icon_size Size of the icon data
- */
- void WriteIcon(const Path& path, std::span icon);
+ Result Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info, u64 program_id,
+ u32 directory_buckets, u32 file_buckets) override {
+ return UnimplementedFunction(ErrorModule::FS);
+ };
+
+ Result FormatAsExtData(const Path& path, const FileSys::ArchiveFormatInfo& format_info,
+ u8 unknown, u64 program_id, u64 total_size,
+ std::optional> icon);
+
+ Result DeleteExtData(Service::FS::MediaType media_type, u8 unknown, u32 high, u32 low);
+
+ void RegisterArtic(std::shared_ptr& client) {
+ artic_client = client;
+ }
+
+ bool IsUsingArtic() const {
+ return artic_client.get() != nullptr;
+ }
private:
/// Type of ext save data archive being accessed.
@@ -61,10 +79,13 @@ private:
/// Returns a path with the correct SaveIdHigh value for Shared extdata paths.
Path GetCorrectedPath(const Path& path);
+ std::shared_ptr artic_client = nullptr;
+
ArchiveFactory_ExtSaveData() = default;
template
void serialize(Archive& ar, const unsigned int) {
ar& boost::serialization::base_object(*this);
+ ar& boost::serialization::base_object(*this);
ar& type;
ar& mount_point;
}
diff --git a/src/core/file_sys/archive_ncch.cpp b/src/core/file_sys/archive_ncch.cpp
index 4b3a478e2..54d4639c5 100644
--- a/src/core/file_sys/archive_ncch.cpp
+++ b/src/core/file_sys/archive_ncch.cpp
@@ -15,6 +15,7 @@
#include "common/string_util.h"
#include "common/swap.h"
#include "core/core.h"
+#include "core/file_sys/archive_artic.h"
#include "core/file_sys/archive_ncch.h"
#include "core/file_sys/errors.h"
#include "core/file_sys/ivfc_archive.h"
@@ -69,8 +70,9 @@ Path MakeNCCHFilePath(NCCHFileOpenType open_type, u32 content_index, NCCHFilePat
return FileSys::Path(std::move(file));
}
-ResultVal> NCCHArchive::OpenFile(const Path& path,
- const Mode& mode) const {
+ResultVal> NCCHArchive::OpenFile(const Path& path, const Mode& mode,
+ u32 attributes) {
+
if (path.GetType() != LowPathType::Binary) {
LOG_ERROR(Service_FS, "Path need to be Binary");
return ResultInvalidPath;
@@ -207,14 +209,14 @@ Result NCCHArchive::DeleteDirectoryRecursively(const Path& path) const {
return ResultUnknown;
}
-Result NCCHArchive::CreateFile(const Path& path, u64 size) const {
+Result NCCHArchive::CreateFile(const Path& path, u64 size, u32 attributes) const {
LOG_CRITICAL(Service_FS, "Attempted to create a file in an NCCH archive ({}).", GetName());
// TODO: Verify error code
return Result(ErrorDescription::NotAuthorized, ErrorModule::FS, ErrorSummary::NotSupported,
ErrorLevel::Permanent);
}
-Result NCCHArchive::CreateDirectory(const Path& path) const {
+Result NCCHArchive::CreateDirectory(const Path& path, u32 attributes) const {
LOG_CRITICAL(Service_FS, "Attempted to create a directory in an NCCH archive ({}).", GetName());
// TODO(wwylele): Use correct error code
return ResultUnknown;
@@ -226,7 +228,7 @@ Result NCCHArchive::RenameDirectory(const Path& src_path, const Path& dest_path)
return ResultUnknown;
}
-ResultVal> NCCHArchive::OpenDirectory(const Path& path) const {
+ResultVal> NCCHArchive::OpenDirectory(const Path& path) {
LOG_CRITICAL(Service_FS, "Attempted to open a directory within an NCCH archive ({}).",
GetName().c_str());
// TODO(shinyquagsire23): Use correct error code
@@ -255,7 +257,7 @@ ResultVal NCCHFile::Read(const u64 offset, const std::size_t length
}
ResultVal NCCHFile::Write(const u64 offset, const std::size_t length, const bool flush,
- const u8* buffer) {
+ const bool update_timestamp, const u8* buffer) {
LOG_ERROR(Service_FS, "Attempted to write to NCCH file");
// TODO(shinyquagsire23): Find error code
return 0ULL;
@@ -274,6 +276,13 @@ ArchiveFactory_NCCH::ArchiveFactory_NCCH() {}
ResultVal> ArchiveFactory_NCCH::Open(const Path& path,
u64 program_id) {
+
+ if (IsUsingArtic()) {
+ EnsureCacheCreated();
+ return ArticArchive::Open(artic_client, Service::FS::ArchiveIdCode::NCCH, path,
+ Core::PerfStats::PerfArticEventBits::NONE, *this, false);
+ }
+
if (path.GetType() != LowPathType::Binary) {
LOG_ERROR(Service_FS, "Path need to be Binary");
return ResultInvalidPath;
@@ -293,7 +302,7 @@ ResultVal> ArchiveFactory_NCCH::Open(const Path&
}
Result ArchiveFactory_NCCH::Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info,
- u64 program_id) {
+ u64 program_id, u32 directory_buckets, u32 file_buckets) {
LOG_ERROR(Service_FS, "Attempted to format a NCCH archive.");
// TODO: Verify error code
return Result(ErrorDescription::NotAuthorized, ErrorModule::FS, ErrorSummary::NotSupported,
diff --git a/src/core/file_sys/archive_ncch.h b/src/core/file_sys/archive_ncch.h
index 3e22ef02a..725752f19 100644
--- a/src/core/file_sys/archive_ncch.h
+++ b/src/core/file_sys/archive_ncch.h
@@ -11,8 +11,10 @@
#include
#include
#include "core/file_sys/archive_backend.h"
+#include "core/file_sys/artic_cache.h"
#include "core/file_sys/file_backend.h"
#include "core/hle/result.h"
+#include "network/artic_base/artic_base_client.h"
namespace Service::FS {
enum class MediaType : u32;
@@ -48,16 +50,16 @@ public:
return "NCCHArchive";
}
- ResultVal> OpenFile(const Path& path,
- const Mode& mode) const override;
+ ResultVal> OpenFile(const Path& path, const Mode& mode,
+ u32 attributes) override;
Result DeleteFile(const Path& path) const override;
Result RenameFile(const Path& src_path, const Path& dest_path) const override;
Result DeleteDirectory(const Path& path) const override;
Result DeleteDirectoryRecursively(const Path& path) const override;
- Result CreateFile(const Path& path, u64 size) const override;
- Result CreateDirectory(const Path& path) const override;
+ Result CreateFile(const Path& path, u64 size, u32 attributes) const override;
+ Result CreateDirectory(const Path& path, u32 attributes) const override;
Result RenameDirectory(const Path& src_path, const Path& dest_path) const override;
- ResultVal> OpenDirectory(const Path& path) const override;
+ ResultVal> OpenDirectory(const Path& path) override;
u64 GetFreeBytes() const override;
protected:
@@ -82,11 +84,11 @@ public:
explicit NCCHFile(std::vector buffer, std::unique_ptr delay_generator_);
ResultVal Read(u64 offset, std::size_t length, u8* buffer) const override;
- ResultVal Write(u64 offset, std::size_t length, bool flush,
+ ResultVal Write(u64 offset, std::size_t length, bool flush, bool update_timestamp,
const u8* buffer) override;
u64 GetSize() const override;
bool SetSize(u64 size) const override;
- bool Close() const override {
+ bool Close() override {
return false;
}
void Flush() const override {}
@@ -105,7 +107,7 @@ private:
};
/// File system interface to the NCCH archive
-class ArchiveFactory_NCCH final : public ArchiveFactory {
+class ArchiveFactory_NCCH final : public ArchiveFactory, public ArticCacheProvider {
public:
explicit ArchiveFactory_NCCH();
@@ -114,14 +116,29 @@ public:
}
ResultVal> Open(const Path& path, u64 program_id) override;
- Result Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info,
- u64 program_id) override;
+ Result Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info, u64 program_id,
+ u32 directory_buckets, u32 file_buckets) override;
ResultVal GetFormatInfo(const Path& path, u64 program_id) const override;
+ bool IsSlow() override {
+ return IsUsingArtic();
+ }
+
+ void RegisterArtic(std::shared_ptr& client) {
+ artic_client = client;
+ }
+
+ bool IsUsingArtic() const {
+ return artic_client.get() != nullptr;
+ }
+
private:
+ std::shared_ptr artic_client = nullptr;
+
template
void serialize(Archive& ar, const unsigned int) {
ar& boost::serialization::base_object(*this);
+ ar& boost::serialization::base_object(*this);
}
friend class boost::serialization::access;
};
diff --git a/src/core/file_sys/archive_other_savedata.cpp b/src/core/file_sys/archive_other_savedata.cpp
index 3944fce6f..c6aaaff0c 100644
--- a/src/core/file_sys/archive_other_savedata.cpp
+++ b/src/core/file_sys/archive_other_savedata.cpp
@@ -75,12 +75,14 @@ ResultVal> ArchiveFactory_OtherSaveDataPermitted
return ResultGamecardNotInserted;
}
- return sd_savedata_source->Open(program_id);
+ return sd_savedata_source->Open(Service::FS::ArchiveIdCode::OtherSaveDataPermitted, path,
+ program_id);
}
Result ArchiveFactory_OtherSaveDataPermitted::Format(const Path& path,
const FileSys::ArchiveFormatInfo& format_info,
- u64 program_id) {
+ u64 program_id, u32 directory_buckets,
+ u32 file_buckets) {
LOG_ERROR(Service_FS, "Attempted to format a OtherSaveDataPermitted archive.");
return ResultInvalidPath;
}
@@ -96,7 +98,8 @@ ResultVal ArchiveFactory_OtherSaveDataPermitted::GetFormatInf
return ResultGamecardNotInserted;
}
- return sd_savedata_source->GetFormatInfo(program_id);
+ return sd_savedata_source->GetFormatInfo(
+ program_id, Service::FS::ArchiveIdCode::OtherSaveDataPermitted, path);
}
ArchiveFactory_OtherSaveDataGeneral::ArchiveFactory_OtherSaveDataGeneral(
@@ -114,12 +117,14 @@ ResultVal> ArchiveFactory_OtherSaveDataGeneral::
return ResultGamecardNotInserted;
}
- return sd_savedata_source->Open(program_id);
+ return sd_savedata_source->Open(Service::FS::ArchiveIdCode::OtherSaveDataGeneral, path,
+ program_id);
}
Result ArchiveFactory_OtherSaveDataGeneral::Format(const Path& path,
const FileSys::ArchiveFormatInfo& format_info,
- u64 /*client_program_id*/) {
+ u64 /*client_program_id*/, u32 directory_buckets,
+ u32 file_buckets) {
MediaType media_type;
u64 program_id;
CASCADE_RESULT(std::tie(media_type, program_id), ParsePathGeneral(path));
@@ -129,7 +134,9 @@ Result ArchiveFactory_OtherSaveDataGeneral::Format(const Path& path,
return ResultGamecardNotInserted;
}
- return sd_savedata_source->Format(program_id, format_info);
+ return sd_savedata_source->Format(program_id, format_info,
+ Service::FS::ArchiveIdCode::OtherSaveDataPermitted, path,
+ directory_buckets, file_buckets);
}
ResultVal ArchiveFactory_OtherSaveDataGeneral::GetFormatInfo(
@@ -143,7 +150,8 @@ ResultVal ArchiveFactory_OtherSaveDataGeneral::GetFormatInfo(
return ResultGamecardNotInserted;
}
- return sd_savedata_source->GetFormatInfo(program_id);
+ return sd_savedata_source->GetFormatInfo(
+ program_id, Service::FS::ArchiveIdCode::OtherSaveDataPermitted, path);
}
} // namespace FileSys
diff --git a/src/core/file_sys/archive_other_savedata.h b/src/core/file_sys/archive_other_savedata.h
index c9e720a8d..fffcb5481 100644
--- a/src/core/file_sys/archive_other_savedata.h
+++ b/src/core/file_sys/archive_other_savedata.h
@@ -22,10 +22,14 @@ public:
}
ResultVal> Open(const Path& path, u64 program_id) override;
- Result Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info,
- u64 program_id) override;
+ Result Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info, u64 program_id,
+ u32 directory_buckets, u32 file_buckets) override;
ResultVal GetFormatInfo(const Path& path, u64 program_id) const override;
+ bool IsSlow() override {
+ return sd_savedata_source->IsUsingArtic();
+ }
+
private:
std::shared_ptr sd_savedata_source;
@@ -49,8 +53,8 @@ public:
}
ResultVal> Open(const Path& path, u64 program_id) override;
- Result Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info,
- u64 program_id) override;
+ Result Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info, u64 program_id,
+ u32 directory_buckets, u32 file_buckets) override;
ResultVal GetFormatInfo(const Path& path, u64 program_id) const override;
private:
diff --git a/src/core/file_sys/archive_savedata.cpp b/src/core/file_sys/archive_savedata.cpp
index a5e767b14..aa06f9544 100644
--- a/src/core/file_sys/archive_savedata.cpp
+++ b/src/core/file_sys/archive_savedata.cpp
@@ -18,18 +18,20 @@ ArchiveFactory_SaveData::ArchiveFactory_SaveData(
ResultVal> ArchiveFactory_SaveData::Open(const Path& path,
u64 program_id) {
- return sd_savedata_source->Open(program_id);
+ return sd_savedata_source->Open(Service::FS::ArchiveIdCode::SaveData, path, program_id);
}
Result ArchiveFactory_SaveData::Format(const Path& path,
const FileSys::ArchiveFormatInfo& format_info,
- u64 program_id) {
- return sd_savedata_source->Format(program_id, format_info);
+ u64 program_id, u32 directory_buckets, u32 file_buckets) {
+ return sd_savedata_source->Format(program_id, format_info, Service::FS::ArchiveIdCode::SaveData,
+ path, directory_buckets, file_buckets);
}
ResultVal ArchiveFactory_SaveData::GetFormatInfo(const Path& path,
u64 program_id) const {
- return sd_savedata_source->GetFormatInfo(program_id);
+ return sd_savedata_source->GetFormatInfo(program_id, Service::FS::ArchiveIdCode::SaveData,
+ path);
}
} // namespace FileSys
diff --git a/src/core/file_sys/archive_savedata.h b/src/core/file_sys/archive_savedata.h
index dba6d5cdb..4b018f214 100644
--- a/src/core/file_sys/archive_savedata.h
+++ b/src/core/file_sys/archive_savedata.h
@@ -20,11 +20,15 @@ public:
}
ResultVal> Open(const Path& path, u64 program_id) override;
- Result Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info,
- u64 program_id) override;
+ Result Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info, u64 program_id,
+ u32 directory_buckets, u32 file_buckets) override;
ResultVal GetFormatInfo(const Path& path, u64 program_id) const override;
+ bool IsSlow() override {
+ return sd_savedata_source->IsUsingArtic();
+ }
+
private:
std::shared_ptr sd_savedata_source;
diff --git a/src/core/file_sys/archive_sdmc.cpp b/src/core/file_sys/archive_sdmc.cpp
index 8ba01e7b8..91b1fa680 100644
--- a/src/core/file_sys/archive_sdmc.cpp
+++ b/src/core/file_sys/archive_sdmc.cpp
@@ -43,8 +43,8 @@ public:
SERIALIZE_DELAY_GENERATOR
};
-ResultVal> SDMCArchive::OpenFile(const Path& path,
- const Mode& mode) const {
+ResultVal> SDMCArchive::OpenFile(const Path& path, const Mode& mode,
+ u32 attributes) {
Mode modified_mode;
modified_mode.hex = mode.hex;
@@ -222,7 +222,7 @@ Result SDMCArchive::DeleteDirectoryRecursively(const Path& path) const {
path, mount_point, [](const std::string& p) { return FileUtil::DeleteDirRecursively(p); });
}
-Result SDMCArchive::CreateFile(const FileSys::Path& path, u64 size) const {
+Result SDMCArchive::CreateFile(const FileSys::Path& path, u64 size, u32 attributes) const {
const PathParser path_parser(path);
if (!path_parser.IsValid()) {
@@ -267,7 +267,7 @@ Result SDMCArchive::CreateFile(const FileSys::Path& path, u64 size) const {
ErrorLevel::Info);
}
-Result SDMCArchive::CreateDirectory(const Path& path) const {
+Result SDMCArchive::CreateDirectory(const Path& path, u32 attributes) const {
const PathParser path_parser(path);
if (!path_parser.IsValid()) {
@@ -331,7 +331,7 @@ Result SDMCArchive::RenameDirectory(const Path& src_path, const Path& dest_path)
ErrorSummary::NothingHappened, ErrorLevel::Status);
}
-ResultVal> SDMCArchive::OpenDirectory(const Path& path) const {
+ResultVal> SDMCArchive::OpenDirectory(const Path& path) {
const PathParser path_parser(path);
if (!path_parser.IsValid()) {
@@ -392,7 +392,7 @@ ResultVal> ArchiveFactory_SDMC::Open(const Path&
}
Result ArchiveFactory_SDMC::Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info,
- u64 program_id) {
+ u64 program_id, u32 directory_buckets, u32 file_buckets) {
// This is kind of an undesirable operation, so let's just ignore it. :)
return ResultSuccess;
}
diff --git a/src/core/file_sys/archive_sdmc.h b/src/core/file_sys/archive_sdmc.h
index 267b1dfdf..2da56cd2e 100644
--- a/src/core/file_sys/archive_sdmc.h
+++ b/src/core/file_sys/archive_sdmc.h
@@ -27,16 +27,16 @@ public:
return "SDMCArchive: " + mount_point;
}
- ResultVal> OpenFile(const Path& path,
- const Mode& mode) const override;
+ ResultVal> OpenFile(const Path& path, const Mode& mode,
+ u32 attributes) override;
Result DeleteFile(const Path& path) const override;
Result RenameFile(const Path& src_path, const Path& dest_path) const override;
Result DeleteDirectory(const Path& path) const override;
Result DeleteDirectoryRecursively(const Path& path) const override;
- Result CreateFile(const Path& path, u64 size) const override;
- Result CreateDirectory(const Path& path) const override;
+ Result CreateFile(const Path& path, u64 size, u32 attributes) const override;
+ Result CreateDirectory(const Path& path, u32 attributes) const override;
Result RenameDirectory(const Path& src_path, const Path& dest_path) const override;
- ResultVal> OpenDirectory(const Path& path) const override;
+ ResultVal> OpenDirectory(const Path& path) override;
u64 GetFreeBytes() const override;
protected:
@@ -68,8 +68,8 @@ public:
}
ResultVal> Open(const Path& path, u64 program_id) override;
- Result Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info,
- u64 program_id) override;
+ Result Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info, u64 program_id,
+ u32 directory_buckets, u32 file_buckets) override;
ResultVal GetFormatInfo(const Path& path, u64 program_id) const override;
private:
diff --git a/src/core/file_sys/archive_sdmcwriteonly.cpp b/src/core/file_sys/archive_sdmcwriteonly.cpp
index 31c27c2d2..fc8191964 100644
--- a/src/core/file_sys/archive_sdmcwriteonly.cpp
+++ b/src/core/file_sys/archive_sdmcwriteonly.cpp
@@ -41,7 +41,8 @@ public:
};
ResultVal> SDMCWriteOnlyArchive::OpenFile(const Path& path,
- const Mode& mode) const {
+ const Mode& mode,
+ u32 attributes) {
if (mode.read_flag) {
LOG_ERROR(Service_FS, "Read flag is not supported");
return ResultInvalidReadFlag;
@@ -49,8 +50,7 @@ ResultVal> SDMCWriteOnlyArchive::OpenFile(const Pat
return SDMCArchive::OpenFileBase(path, mode);
}
-ResultVal> SDMCWriteOnlyArchive::OpenDirectory(
- const Path& path) const {
+ResultVal> SDMCWriteOnlyArchive::OpenDirectory(const Path& path) {
LOG_ERROR(Service_FS, "Not supported");
return ResultUnsupportedOpenFlags;
}
@@ -83,7 +83,8 @@ ResultVal> ArchiveFactory_SDMCWriteOnly::Open(co
Result ArchiveFactory_SDMCWriteOnly::Format(const Path& path,
const FileSys::ArchiveFormatInfo& format_info,
- u64 program_id) {
+ u64 program_id, u32 directory_buckets,
+ u32 file_buckets) {
// TODO(wwylele): hwtest this
LOG_ERROR(Service_FS, "Attempted to format a SDMC write-only archive.");
return ResultUnknown;
diff --git a/src/core/file_sys/archive_sdmcwriteonly.h b/src/core/file_sys/archive_sdmcwriteonly.h
index c05f408d9..c60723a04 100644
--- a/src/core/file_sys/archive_sdmcwriteonly.h
+++ b/src/core/file_sys/archive_sdmcwriteonly.h
@@ -24,10 +24,10 @@ public:
return "SDMCWriteOnlyArchive: " + mount_point;
}
- ResultVal> OpenFile(const Path& path,
- const Mode& mode) const override;
+ ResultVal> OpenFile(const Path& path, const Mode& mode,
+ u32 attributes) override;
- ResultVal> OpenDirectory(const Path& path) const override;
+ ResultVal> OpenDirectory(const Path& path) override;
private:
SDMCWriteOnlyArchive() = default;
@@ -54,8 +54,8 @@ public:
}
ResultVal> Open(const Path& path, u64 program_id) override;
- Result Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info,
- u64 program_id) override;
+ Result Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info, u64 program_id,
+ u32 directory_buckets, u32 file_buckets) override;
ResultVal GetFormatInfo(const Path& path, u64 program_id) const override;
private:
diff --git a/src/core/file_sys/archive_selfncch.cpp b/src/core/file_sys/archive_selfncch.cpp
index 60454d674..5472eda01 100644
--- a/src/core/file_sys/archive_selfncch.cpp
+++ b/src/core/file_sys/archive_selfncch.cpp
@@ -51,7 +51,7 @@ public:
return data->size();
}
- ResultVal Write(u64 offset, std::size_t length, bool flush,
+ ResultVal Write(u64 offset, std::size_t length, bool flush, bool update_timestamp,
const u8* buffer) override {
LOG_ERROR(Service_FS, "The file is read-only!");
return ResultUnsupportedOpenFlags;
@@ -65,7 +65,7 @@ public:
return false;
}
- bool Close() const override {
+ bool Close() override {
return true;
}
@@ -94,7 +94,8 @@ public:
return "SelfNCCHArchive";
}
- ResultVal> OpenFile(const Path& path, const Mode&) const override {
+ ResultVal> OpenFile(const Path& path, const Mode&,
+ u32 attributes) override {
// Note: SelfNCCHArchive doesn't check the open mode.
if (path.GetType() != LowPathType::Binary) {
@@ -154,12 +155,12 @@ public:
return ResultUnsupportedOpenFlags;
}
- Result CreateFile(const Path& path, u64 size) const override {
+ Result CreateFile(const Path& path, u64 size, u32 attributes) const override {
LOG_ERROR(Service_FS, "Unsupported");
return ResultUnsupportedOpenFlags;
}
- Result CreateDirectory(const Path& path) const override {
+ Result CreateDirectory(const Path& path, u32 attributes) const override {
LOG_ERROR(Service_FS, "Unsupported");
return ResultUnsupportedOpenFlags;
}
@@ -169,7 +170,7 @@ public:
return ResultUnsupportedOpenFlags;
}
- ResultVal> OpenDirectory(const Path& path) const override {
+ ResultVal> OpenDirectory(const Path& path) override {
LOG_ERROR(Service_FS, "Unsupported");
return ResultUnsupportedOpenFlags;
}
@@ -297,7 +298,7 @@ ResultVal> ArchiveFactory_SelfNCCH::Open(const P
}
Result ArchiveFactory_SelfNCCH::Format(const Path&, const FileSys::ArchiveFormatInfo&,
- u64 program_id) {
+ u64 program_id, u32 directory_buckets, u32 file_buckets) {
LOG_ERROR(Service_FS, "Attempted to format a SelfNCCH archive.");
return ResultInvalidPath;
}
diff --git a/src/core/file_sys/archive_selfncch.h b/src/core/file_sys/archive_selfncch.h
index 0643faf63..e25526105 100644
--- a/src/core/file_sys/archive_selfncch.h
+++ b/src/core/file_sys/archive_selfncch.h
@@ -50,8 +50,8 @@ public:
return "SelfNCCH";
}
ResultVal> Open(const Path& path, u64 program_id) override;
- Result Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info,
- u64 program_id) override;
+ Result Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info, u64 program_id,
+ u32 directory_buckets, u32 file_buckets) override;
ResultVal GetFormatInfo(const Path& path, u64 program_id) const override;
private:
diff --git a/src/core/file_sys/archive_source_sd_savedata.cpp b/src/core/file_sys/archive_source_sd_savedata.cpp
index 2f4cdcb54..9ab16be0a 100644
--- a/src/core/file_sys/archive_source_sd_savedata.cpp
+++ b/src/core/file_sys/archive_source_sd_savedata.cpp
@@ -6,6 +6,7 @@
#include "common/archives.h"
#include "common/file_util.h"
#include "common/logging/log.h"
+#include "core/file_sys/archive_artic.h"
#include "core/file_sys/archive_source_sd_savedata.h"
#include "core/file_sys/errors.h"
#include "core/file_sys/savedata_archive.h"
@@ -40,49 +41,101 @@ ArchiveSource_SDSaveData::ArchiveSource_SDSaveData(const std::string& sdmc_direc
LOG_DEBUG(Service_FS, "Directory {} set as SaveData.", mount_point);
}
-ResultVal> ArchiveSource_SDSaveData::Open(u64 program_id) {
- std::string concrete_mount_point = GetSaveDataPath(mount_point, program_id);
- if (!FileUtil::Exists(concrete_mount_point)) {
- // When a SaveData archive is created for the first time, it is not yet formatted and the
- // save file/directory structure expected by the game has not yet been initialized.
- // Returning the NotFormatted error code will signal the game to provision the SaveData
- // archive with the files and folders that it expects.
- return ResultNotFormatted;
- }
+ResultVal> ArchiveSource_SDSaveData::Open(
+ Service::FS::ArchiveIdCode archive_id, const Path& path, u64 program_id) {
+ if (IsUsingArtic()) {
+ EnsureCacheCreated();
+ return ArticArchive::Open(artic_client, archive_id, path,
+ Core::PerfStats::PerfArticEventBits::ARTIC_SAVE_DATA, *this,
+ archive_id != Service::FS::ArchiveIdCode::SaveData);
+ } else {
+ std::string concrete_mount_point = GetSaveDataPath(mount_point, program_id);
+ if (!FileUtil::Exists(concrete_mount_point)) {
+ // When a SaveData archive is created for the first time, it is not yet formatted and
+ // the save file/directory structure expected by the game has not yet been initialized.
+ // Returning the NotFormatted error code will signal the game to provision the SaveData
+ // archive with the files and folders that it expects.
+ return ResultNotFormatted;
+ }
- return std::make_unique(std::move(concrete_mount_point));
+ return std::make_unique(std::move(concrete_mount_point));
+ }
}
Result ArchiveSource_SDSaveData::Format(u64 program_id,
- const FileSys::ArchiveFormatInfo& format_info) {
- std::string concrete_mount_point = GetSaveDataPath(mount_point, program_id);
- FileUtil::DeleteDirRecursively(concrete_mount_point);
- FileUtil::CreateFullPath(concrete_mount_point);
+ const FileSys::ArchiveFormatInfo& format_info,
+ Service::FS::ArchiveIdCode archive_id, const Path& path,
+ u32 directory_buckets, u32 file_buckets) {
+ if (IsUsingArtic()) {
+ ClearAllCache();
+ auto req = artic_client->NewRequest("FSUSER_FormatSaveData");
- // Write the format metadata
- std::string metadata_path = GetSaveDataMetadataPath(mount_point, program_id);
- FileUtil::IOFile file(metadata_path, "wb");
+ req.AddParameterS32(static_cast(archive_id));
+ auto artic_path = ArticArchive::BuildFSPath(path);
+ req.AddParameterBuffer(artic_path.data(), artic_path.size());
+ req.AddParameterU32(format_info.total_size / 512);
+ req.AddParameterU32(format_info.number_directories);
+ req.AddParameterU32(format_info.number_files);
+ req.AddParameterU32(directory_buckets);
+ req.AddParameterU32(file_buckets);
+ req.AddParameterU8(format_info.duplicate_data);
- if (file.IsOpen()) {
- file.WriteBytes(&format_info, sizeof(format_info));
+ auto resp = artic_client->Send(req);
+ return ArticArchive::RespResult(resp);
+ } else {
+ std::string concrete_mount_point = GetSaveDataPath(mount_point, program_id);
+ FileUtil::DeleteDirRecursively(concrete_mount_point);
+ FileUtil::CreateFullPath(concrete_mount_point);
+
+ // Write the format metadata
+ std::string metadata_path = GetSaveDataMetadataPath(mount_point, program_id);
+ FileUtil::IOFile file(metadata_path, "wb");
+
+ if (file.IsOpen()) {
+ file.WriteBytes(&format_info, sizeof(format_info));
+ return ResultSuccess;
+ }
return ResultSuccess;
}
- return ResultSuccess;
}
-ResultVal ArchiveSource_SDSaveData::GetFormatInfo(u64 program_id) const {
- std::string metadata_path = GetSaveDataMetadataPath(mount_point, program_id);
- FileUtil::IOFile file(metadata_path, "rb");
+ResultVal ArchiveSource_SDSaveData::GetFormatInfo(
+ u64 program_id, Service::FS::ArchiveIdCode archive_id, const Path& path) const {
+ if (IsUsingArtic()) {
+ auto req = artic_client->NewRequest("FSUSER_GetFormatInfo");
- if (!file.IsOpen()) {
- LOG_ERROR(Service_FS, "Could not open metadata information for archive");
- // TODO(Subv): Verify error code
- return ResultNotFormatted;
+ req.AddParameterS32(static_cast(archive_id));
+ auto path_artic = ArticArchive::BuildFSPath(path);
+ req.AddParameterBuffer(path_artic.data(), path_artic.size());
+
+ auto resp = artic_client->Send(req);
+ Result res = ArticArchive::RespResult(resp);
+ if (R_FAILED(res)) {
+ return res;
+ }
+
+ auto info_buf = resp->GetResponseBuffer(0);
+ if (!info_buf.has_value() || info_buf->second != sizeof(ArchiveFormatInfo)) {
+ return ResultUnknown;
+ }
+
+ ArchiveFormatInfo info;
+ memcpy(&info, info_buf->first, sizeof(info));
+ return info;
+ } else {
+ std::string metadata_path = GetSaveDataMetadataPath(mount_point, program_id);
+ FileUtil::IOFile file(metadata_path, "rb");
+
+ if (!file.IsOpen()) {
+ LOG_ERROR(Service_FS, "Could not open metadata information for archive");
+ // TODO(Subv): Verify error code
+ return ResultNotFormatted;
+ }
+
+ ArchiveFormatInfo info = {};
+ file.ReadBytes(&info, sizeof(info));
+ return info;
}
-
- ArchiveFormatInfo info = {};
- file.ReadBytes(&info, sizeof(info));
- return info;
}
std::string ArchiveSource_SDSaveData::GetSaveDataPathFor(const std::string& mount_point,
diff --git a/src/core/file_sys/archive_source_sd_savedata.h b/src/core/file_sys/archive_source_sd_savedata.h
index 07832c3ae..56fdb2c5b 100644
--- a/src/core/file_sys/archive_source_sd_savedata.h
+++ b/src/core/file_sys/archive_source_sd_savedata.h
@@ -9,27 +9,48 @@
#include
#include