mirror of
https://github.com/PabloMK7/citra.git
synced 2025-09-08 03:40:05 +00:00
Merge branch 'PabloMK7:master' into wayland
This commit is contained in:
commit
9d64ddb121
171 changed files with 9190 additions and 1771 deletions
20
dist/languages/da_DK.ts
vendored
20
dist/languages/da_DK.ts
vendored
|
@ -3583,6 +3583,24 @@ Drag points to change position, or double-click table cells to edit values.</sou
|
|||
<translation>Engelsk</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>ConfigureWeb</name>
|
||||
<message>
|
||||
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="14"/>
|
||||
<source>Form</source>
|
||||
<translation>Form</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="177"/>
|
||||
<source>Discord Presence</source>
|
||||
<translation>Discord-presence</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="183"/>
|
||||
<source>Show Current Game in your Discord Status</source>
|
||||
<translation>Vis kørende spil som din Discord-status</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>DirectConnect</name>
|
||||
<message>
|
||||
|
@ -6861,4 +6879,4 @@ They may have left the room.</source>
|
|||
<translation>Afventningstræ</translation>
|
||||
</message>
|
||||
</context>
|
||||
</TS>
|
||||
</TS>
|
20
dist/languages/de.ts
vendored
20
dist/languages/de.ts
vendored
|
@ -3585,6 +3585,24 @@ Ziehe Punkte, um ihre Position zu verändern, oder doppelklicke auf Zellen in de
|
|||
<translation>Englisch</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>ConfigureWeb</name>
|
||||
<message>
|
||||
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="14"/>
|
||||
<source>Form</source>
|
||||
<translation>Form</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="177"/>
|
||||
<source>Discord Presence</source>
|
||||
<translation>Discord Presence</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="183"/>
|
||||
<source>Show Current Game in your Discord Status</source>
|
||||
<translation>Aktuelles Spiel in Ihrem Discordstatus anzeigen</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>DirectConnect</name>
|
||||
<message>
|
||||
|
@ -6868,4 +6886,4 @@ They may have left the room.</source>
|
|||
<translation>Wait Tree</translation>
|
||||
</message>
|
||||
</context>
|
||||
</TS>
|
||||
</TS>
|
20
dist/languages/el.ts
vendored
20
dist/languages/el.ts
vendored
|
@ -3584,6 +3584,24 @@ Drag points to change position, or double-click table cells to edit values.</sou
|
|||
<translation>Αγγλικά</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>ConfigureWeb</name>
|
||||
<message>
|
||||
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="14"/>
|
||||
<source>Form</source>
|
||||
<translation>Φόρμα</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="177"/>
|
||||
<source>Discord Presence</source>
|
||||
<translation>Παρουσία Discord</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="183"/>
|
||||
<source>Show Current Game in your Discord Status</source>
|
||||
<translation>Εμφάνιση τρέχοντος παιχνιδιού στην κατάσταση Discord σας</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>DirectConnect</name>
|
||||
<message>
|
||||
|
@ -6865,4 +6883,4 @@ They may have left the room.</source>
|
|||
<translation>Δένδρο αναμονής</translation>
|
||||
</message>
|
||||
</context>
|
||||
</TS>
|
||||
</TS>
|
20
dist/languages/es_ES.ts
vendored
20
dist/languages/es_ES.ts
vendored
|
@ -3585,6 +3585,24 @@ Mueve los puntos para cambiar la posición, o haz doble click en las celdas de l
|
|||
<translation>Inglés (English)</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>ConfigureWeb</name>
|
||||
<message>
|
||||
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="14"/>
|
||||
<source>Form</source>
|
||||
<translation>Formulario</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="177"/>
|
||||
<source>Discord Presence</source>
|
||||
<translation>Presencia en Discord</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="183"/>
|
||||
<source>Show Current Game in your Discord Status</source>
|
||||
<translation>Mostrar Juego Actual en el Estado de Discord</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>DirectConnect</name>
|
||||
<message>
|
||||
|
@ -6878,4 +6896,4 @@ Puede que haya dejado la sala.</translation>
|
|||
<translation>Árbol de Espera</translation>
|
||||
</message>
|
||||
</context>
|
||||
</TS>
|
||||
</TS>
|
20
dist/languages/fi.ts
vendored
20
dist/languages/fi.ts
vendored
|
@ -3583,6 +3583,24 @@ Drag points to change position, or double-click table cells to edit values.</sou
|
|||
<translation>Englanti</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>ConfigureWeb</name>
|
||||
<message>
|
||||
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="14"/>
|
||||
<source>Form</source>
|
||||
<translation>Muot</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="177"/>
|
||||
<source>Discord Presence</source>
|
||||
<translation>Discord läsnäolo</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="183"/>
|
||||
<source>Show Current Game in your Discord Status</source>
|
||||
<translation>Näytä nykyinen peli Discord tilassa</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>DirectConnect</name>
|
||||
<message>
|
||||
|
@ -6845,4 +6863,4 @@ They may have left the room.</source>
|
|||
<translation type="unfinished"/>
|
||||
</message>
|
||||
</context>
|
||||
</TS>
|
||||
</TS>
|
20
dist/languages/fr.ts
vendored
20
dist/languages/fr.ts
vendored
|
@ -3585,6 +3585,24 @@ Glissez les points pour modifier la position, ou double-cliquez les cellules pou
|
|||
<translation>Anglais</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>ConfigureWeb</name>
|
||||
<message>
|
||||
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="14"/>
|
||||
<source>Form</source>
|
||||
<translation>Forme</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="177"/>
|
||||
<source>Discord Presence</source>
|
||||
<translation>Présence sur Discord</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="183"/>
|
||||
<source>Show Current Game in your Discord Status</source>
|
||||
<translation>Afficher votre jeu en cours dans votre statut Discord</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>DirectConnect</name>
|
||||
<message>
|
||||
|
@ -6873,4 +6891,4 @@ Il a peut-être quitté la salon.</translation>
|
|||
<translation>Arbre d'instructions</translation>
|
||||
</message>
|
||||
</context>
|
||||
</TS>
|
||||
</TS>
|
68
dist/languages/hu_HU.ts
vendored
68
dist/languages/hu_HU.ts
vendored
|
@ -3582,6 +3582,72 @@ Drag points to change position, or double-click table cells to edit values.</sou
|
|||
<translation>Angol</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>ConfigureWeb</name>
|
||||
<message>
|
||||
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="14"/>
|
||||
<source>Form</source>
|
||||
<translation>Forma</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="177"/>
|
||||
<source>Discord Presence</source>
|
||||
<translation>Discord jelenlét</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="183"/>
|
||||
<source>Show Current Game in your Discord Status</source>
|
||||
<translation>Jelenlegi játék megjelenítése a Discord állapotodban</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>DirectConnect</name>
|
||||
<message>
|
||||
<location filename="../../src/citra_qt/multiplayer/direct_connect.ui" line="14"/>
|
||||
<source>Direct Connect</source>
|
||||
<translation>Közvetlen Kapcsolódás</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../src/citra_qt/multiplayer/direct_connect.ui" line="47"/>
|
||||
<source>Server Address</source>
|
||||
<translation type="unfinished"/>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../src/citra_qt/multiplayer/direct_connect.ui" line="54"/>
|
||||
<source><html><head/><body><p>Server address of the host</p></body></html></source>
|
||||
<translation type="unfinished"/>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../src/citra_qt/multiplayer/direct_connect.ui" line="64"/>
|
||||
<source>Port</source>
|
||||
<translation>Port</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../src/citra_qt/multiplayer/direct_connect.ui" line="71"/>
|
||||
<source><html><head/><body><p>Port number the host is listening on</p></body></html></source>
|
||||
<translation><html><head/><body><p>Annak a portnak a száma, amire a gazda figyel</p></body></html></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../src/citra_qt/multiplayer/direct_connect.ui" line="77"/>
|
||||
<source>24872</source>
|
||||
<translation>24872</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../src/citra_qt/multiplayer/direct_connect.ui" line="97"/>
|
||||
<source>Nickname</source>
|
||||
<translation>Becenév</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../src/citra_qt/multiplayer/direct_connect.ui" line="111"/>
|
||||
<source>Password</source>
|
||||
<translation>Jelszó</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../src/citra_qt/multiplayer/direct_connect.ui" line="153"/>
|
||||
<source>Connect</source>
|
||||
<translation>Kapcsolás</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>DirectConnectWindow</name>
|
||||
<message>
|
||||
|
@ -6807,4 +6873,4 @@ They may have left the room.</source>
|
|||
<translation>Várakozási Fa</translation>
|
||||
</message>
|
||||
</context>
|
||||
</TS>
|
||||
</TS>
|
20
dist/languages/id.ts
vendored
20
dist/languages/id.ts
vendored
|
@ -3584,6 +3584,24 @@ Drag points to change position, or double-click table cells to edit values.</sou
|
|||
<translation>Inggris</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>ConfigureWeb</name>
|
||||
<message>
|
||||
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="14"/>
|
||||
<source>Form</source>
|
||||
<translation>Formulir</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="177"/>
|
||||
<source>Discord Presence</source>
|
||||
<translation>Status Discord</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="183"/>
|
||||
<source>Show Current Game in your Discord Status</source>
|
||||
<translation>Tampilkan Game Saat Ini ke Status Discord Anda</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>DirectConnect</name>
|
||||
<message>
|
||||
|
@ -6861,4 +6879,4 @@ They may have left the room.</source>
|
|||
<translation>Tunggu Tree</translation>
|
||||
</message>
|
||||
</context>
|
||||
</TS>
|
||||
</TS>
|
20
dist/languages/it.ts
vendored
20
dist/languages/it.ts
vendored
|
@ -3585,6 +3585,24 @@ Trascina i punti per cambiarne la posizione, o fai doppio clic sulla tabella per
|
|||
<translation>Inglese</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>ConfigureWeb</name>
|
||||
<message>
|
||||
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="14"/>
|
||||
<source>Form</source>
|
||||
<translation>Modulo</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="177"/>
|
||||
<source>Discord Presence</source>
|
||||
<translation>Discord Presence</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="183"/>
|
||||
<source>Show Current Game in your Discord Status</source>
|
||||
<translation>Mostra il gioco attuale nel tuo stato di Discord</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>DirectConnect</name>
|
||||
<message>
|
||||
|
@ -6874,4 +6892,4 @@ Potrebbe aver lasciato la stanza.</translation>
|
|||
<translation>Wait Tree</translation>
|
||||
</message>
|
||||
</context>
|
||||
</TS>
|
||||
</TS>
|
20
dist/languages/ja_JP.ts
vendored
20
dist/languages/ja_JP.ts
vendored
|
@ -3587,6 +3587,24 @@ Drag points to change position, or double-click table cells to edit values.</sou
|
|||
<translation>英語</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>ConfigureWeb</name>
|
||||
<message>
|
||||
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="14"/>
|
||||
<source>Form</source>
|
||||
<translation>フォーム</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="177"/>
|
||||
<source>Discord Presence</source>
|
||||
<translation>Discord Presence</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="183"/>
|
||||
<source>Show Current Game in your Discord Status</source>
|
||||
<translation>プレイ中のゲームをDiscordに表示</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>DirectConnect</name>
|
||||
<message>
|
||||
|
@ -6866,4 +6884,4 @@ They may have left the room.</source>
|
|||
<translation>Wait Tree</translation>
|
||||
</message>
|
||||
</context>
|
||||
</TS>
|
||||
</TS>
|
20
dist/languages/ko_KR.ts
vendored
20
dist/languages/ko_KR.ts
vendored
|
@ -3585,6 +3585,24 @@ Drag points to change position, or double-click table cells to edit values.</sou
|
|||
<translation>English</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>ConfigureWeb</name>
|
||||
<message>
|
||||
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="14"/>
|
||||
<source>Form</source>
|
||||
<translation>종류</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="177"/>
|
||||
<source>Discord Presence</source>
|
||||
<translation>디스코드 있음</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="183"/>
|
||||
<source>Show Current Game in your Discord Status</source>
|
||||
<translation>사용자의 디스코드 상태에 현재 게임 표시하기</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>DirectConnect</name>
|
||||
<message>
|
||||
|
@ -6868,4 +6886,4 @@ They may have left the room.</source>
|
|||
<translation>Wait Tree</translation>
|
||||
</message>
|
||||
</context>
|
||||
</TS>
|
||||
</TS>
|
20
dist/languages/lt_LT.ts
vendored
20
dist/languages/lt_LT.ts
vendored
|
@ -3581,6 +3581,24 @@ Drag points to change position, or double-click table cells to edit values.</sou
|
|||
<translation>Anglų k.</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>ConfigureWeb</name>
|
||||
<message>
|
||||
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="14"/>
|
||||
<source>Form</source>
|
||||
<translation>Forma</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="177"/>
|
||||
<source>Discord Presence</source>
|
||||
<translation>Discord nustatymai</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="183"/>
|
||||
<source>Show Current Game in your Discord Status</source>
|
||||
<translation>Rodyti jūsų žaidžiamą žaidimą Discord'e</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>DirectConnect</name>
|
||||
<message>
|
||||
|
@ -6853,4 +6871,4 @@ They may have left the room.</source>
|
|||
<translation>Laukimo gijų medis</translation>
|
||||
</message>
|
||||
</context>
|
||||
</TS>
|
||||
</TS>
|
20
dist/languages/nb.ts
vendored
20
dist/languages/nb.ts
vendored
|
@ -3584,6 +3584,24 @@ Dra punkter for å endre posisjon, eller dobbeltklikk på tabellceller for å re
|
|||
<translation>Engelsk</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>ConfigureWeb</name>
|
||||
<message>
|
||||
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="14"/>
|
||||
<source>Form</source>
|
||||
<translation>Form</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="177"/>
|
||||
<source>Discord Presence</source>
|
||||
<translation>Discord tilstedeværelse</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="183"/>
|
||||
<source>Show Current Game in your Discord Status</source>
|
||||
<translation>Vis Gjeldende Spill i Discord Statusen din.</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>DirectConnect</name>
|
||||
<message>
|
||||
|
@ -6865,4 +6883,4 @@ They may have left the room.</source>
|
|||
<translation>Wait Tree</translation>
|
||||
</message>
|
||||
</context>
|
||||
</TS>
|
||||
</TS>
|
20
dist/languages/nl.ts
vendored
20
dist/languages/nl.ts
vendored
|
@ -3585,6 +3585,24 @@ Sleep punten om de positie te wijzigen of dubbelklik op tabelcellen om waarden t
|
|||
<translation>Engels</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>ConfigureWeb</name>
|
||||
<message>
|
||||
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="14"/>
|
||||
<source>Form</source>
|
||||
<translation>Formulier</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="177"/>
|
||||
<source>Discord Presence</source>
|
||||
<translation>Discord Presence</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="183"/>
|
||||
<source>Show Current Game in your Discord Status</source>
|
||||
<translation>Toon Huidige Spel in je Discord Status</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>DirectConnect</name>
|
||||
<message>
|
||||
|
@ -6876,4 +6894,4 @@ Misschien hebben ze de kamer verlaten.</translation>
|
|||
<translation>Wait Tree</translation>
|
||||
</message>
|
||||
</context>
|
||||
</TS>
|
||||
</TS>
|
20
dist/languages/pl_PL.ts
vendored
20
dist/languages/pl_PL.ts
vendored
|
@ -3583,6 +3583,24 @@ Drag points to change position, or double-click table cells to edit values.</sou
|
|||
<translation>Angielski (English)</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>ConfigureWeb</name>
|
||||
<message>
|
||||
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="14"/>
|
||||
<source>Form</source>
|
||||
<translation>Formularz</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="177"/>
|
||||
<source>Discord Presence</source>
|
||||
<translation>Widoczność na Discordzie</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="183"/>
|
||||
<source>Show Current Game in your Discord Status</source>
|
||||
<translation>Pokaż obecnie włączoną grę w statusie na Discrodzie</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>DirectConnect</name>
|
||||
<message>
|
||||
|
@ -6856,4 +6874,4 @@ They may have left the room.</source>
|
|||
<translation>Kolejka Oczekiwania</translation>
|
||||
</message>
|
||||
</context>
|
||||
</TS>
|
||||
</TS>
|
20
dist/languages/pt_BR.ts
vendored
20
dist/languages/pt_BR.ts
vendored
|
@ -3585,6 +3585,24 @@ Arraste os pontos para alterar a posição ou clique duas vezes nas células da
|
|||
<translation>Inglês (English)</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>ConfigureWeb</name>
|
||||
<message>
|
||||
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="14"/>
|
||||
<source>Form</source>
|
||||
<translation>Formulário</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="177"/>
|
||||
<source>Discord Presence</source>
|
||||
<translation>Presença no Discord</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="183"/>
|
||||
<source>Show Current Game in your Discord Status</source>
|
||||
<translation>Mostrar o jogo atual no seu perfil do Discord</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>DirectConnect</name>
|
||||
<message>
|
||||
|
@ -6869,4 +6887,4 @@ They may have left the room.</source>
|
|||
<translation>Árvore de espera</translation>
|
||||
</message>
|
||||
</context>
|
||||
</TS>
|
||||
</TS>
|
20
dist/languages/ro_RO.ts
vendored
20
dist/languages/ro_RO.ts
vendored
|
@ -3584,6 +3584,24 @@ Drag points to change position, or double-click table cells to edit values.</sou
|
|||
<translation>Engleză</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>ConfigureWeb</name>
|
||||
<message>
|
||||
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="14"/>
|
||||
<source>Form</source>
|
||||
<translation>Model</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="177"/>
|
||||
<source>Discord Presence</source>
|
||||
<translation>Prezență pe Discord</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="183"/>
|
||||
<source>Show Current Game in your Discord Status</source>
|
||||
<translation>Afișează Jocul Prezent pe Statusul Discord</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>DirectConnect</name>
|
||||
<message>
|
||||
|
@ -6863,4 +6881,4 @@ They may have left the room.</source>
|
|||
<translation>Copac de Așteptare</translation>
|
||||
</message>
|
||||
</context>
|
||||
</TS>
|
||||
</TS>
|
20
dist/languages/ru_RU.ts
vendored
20
dist/languages/ru_RU.ts
vendored
|
@ -3587,6 +3587,24 @@ Drag points to change position, or double-click table cells to edit values.</sou
|
|||
<translation>Английский</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>ConfigureWeb</name>
|
||||
<message>
|
||||
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="14"/>
|
||||
<source>Form</source>
|
||||
<translation>Форма</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="177"/>
|
||||
<source>Discord Presence</source>
|
||||
<translation>Интеграция с Discord</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="183"/>
|
||||
<source>Show Current Game in your Discord Status</source>
|
||||
<translation>Показывать текущую игру в статусе Discord</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>DirectConnect</name>
|
||||
<message>
|
||||
|
@ -6868,4 +6886,4 @@ They may have left the room.</source>
|
|||
<translation>Дерево цепочки ожидания</translation>
|
||||
</message>
|
||||
</context>
|
||||
</TS>
|
||||
</TS>
|
20
dist/languages/tr_TR.ts
vendored
20
dist/languages/tr_TR.ts
vendored
|
@ -3583,6 +3583,24 @@ Drag points to change position, or double-click table cells to edit values.</sou
|
|||
<translation>İngilizce</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>ConfigureWeb</name>
|
||||
<message>
|
||||
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="14"/>
|
||||
<source>Form</source>
|
||||
<translation>Form</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="177"/>
|
||||
<source>Discord Presence</source>
|
||||
<translation>Discord Görünümü</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="183"/>
|
||||
<source>Show Current Game in your Discord Status</source>
|
||||
<translation>Şu Anki Oyunu Discord Durumunda Göster</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>DirectConnect</name>
|
||||
<message>
|
||||
|
@ -6865,4 +6883,4 @@ They may have left the room.</source>
|
|||
<translation>Wait Tree</translation>
|
||||
</message>
|
||||
</context>
|
||||
</TS>
|
||||
</TS>
|
20
dist/languages/vi_VN.ts
vendored
20
dist/languages/vi_VN.ts
vendored
|
@ -3583,6 +3583,24 @@ Drag points to change position, or double-click table cells to edit values.</sou
|
|||
<translation>Tiếng Anh</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>ConfigureWeb</name>
|
||||
<message>
|
||||
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="14"/>
|
||||
<source>Form</source>
|
||||
<translation>Định dạng</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="177"/>
|
||||
<source>Discord Presence</source>
|
||||
<translation type="unfinished"/>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="183"/>
|
||||
<source>Show Current Game in your Discord Status</source>
|
||||
<translation>Hiển thị game đang chơi trên trạng thái Discord</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>DirectConnect</name>
|
||||
<message>
|
||||
|
@ -6861,4 +6879,4 @@ They may have left the room.</source>
|
|||
<translation>Wait Tree</translation>
|
||||
</message>
|
||||
</context>
|
||||
</TS>
|
||||
</TS>
|
20
dist/languages/zh_CN.ts
vendored
20
dist/languages/zh_CN.ts
vendored
|
@ -3585,6 +3585,24 @@ Drag points to change position, or double-click table cells to edit values.</sou
|
|||
<translation>英语</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>ConfigureWeb</name>
|
||||
<message>
|
||||
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="14"/>
|
||||
<source>Form</source>
|
||||
<translation>格式</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="177"/>
|
||||
<source>Discord Presence</source>
|
||||
<translation>Discord 状态</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="183"/>
|
||||
<source>Show Current Game in your Discord Status</source>
|
||||
<translation>在您的 Discord 状态中显示当前游戏</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>DirectConnect</name>
|
||||
<message>
|
||||
|
@ -6873,4 +6891,4 @@ They may have left the room.</source>
|
|||
<translation>等待树</translation>
|
||||
</message>
|
||||
</context>
|
||||
</TS>
|
||||
</TS>
|
20
dist/languages/zh_TW.ts
vendored
20
dist/languages/zh_TW.ts
vendored
|
@ -3584,6 +3584,24 @@ Drag points to change position, or double-click table cells to edit values.</sou
|
|||
<translation>English</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>ConfigureWeb</name>
|
||||
<message>
|
||||
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="14"/>
|
||||
<source>Form</source>
|
||||
<translation>Form</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="177"/>
|
||||
<source>Discord Presence</source>
|
||||
<translation>Discord 狀態</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="183"/>
|
||||
<source>Show Current Game in your Discord Status</source>
|
||||
<translation>在 Discord 狀態中顯示正在玩的遊戲</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>DirectConnect</name>
|
||||
<message>
|
||||
|
@ -6865,4 +6883,4 @@ They may have left the room.</source>
|
|||
<translation>樹狀等待</translation>
|
||||
</message>
|
||||
</context>
|
||||
</TS>
|
||||
</TS>
|
2
externals/dynarmic
vendored
2
externals/dynarmic
vendored
|
@ -1 +1 @@
|
|||
Subproject commit 30f1a3c6289075ef4af08f5ec502be2fc8627a0c
|
||||
Subproject commit a41c380246d3d9f9874f0f792d234dc0cc17c180
|
2
externals/fmt
vendored
2
externals/fmt
vendored
|
@ -1 +1 @@
|
|||
Subproject commit 2dd4fa8742fdac36468f8d8ea3e06e78215551f8
|
||||
Subproject commit fcd3e1e19c8d2df94bb6cb40d7f1c97a9872cf2b
|
|
@ -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
|
||||
|
|
|
@ -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<SaveStateInfo>?
|
||||
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -128,6 +128,8 @@ void Config::ReadValues() {
|
|||
static_cast<u16>(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<Settings::LayoutOption>(sdl2_config->GetInteger(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -82,6 +82,7 @@ static jobject ToJavaCoreError(Core::System::ResultStatus result) {
|
|||
static const std::map<Core::System::ResultStatus, const char*> 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(
|
||||
|
|
9
src/android/app/src/main/res/drawable/ic_network.xml
Normal file
9
src/android/app/src/main/res/drawable/ic_network.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="?attr/colorControlNormal"
|
||||
android:pathData="M200,840q-33,0 -56.5,-23.5T120,760q0,-33 23.5,-56.5T200,680q33,0 56.5,23.5T280,760q0,33 -23.5,56.5T200,840ZM680,840q0,-117 -44,-218.5T516,444q-76,-76 -177.5,-120T120,280v-120q142,0 265,53t216,146q93,93 146,216t53,265L680,840ZM440,840q0,-67 -25,-124.5T346,614q-44,-44 -101.5,-69T120,520v-120q92,0 171.5,34.5T431,529q60,60 94.5,139.5T560,840L440,840Z"/>
|
||||
</vector>
|
|
@ -657,4 +657,16 @@ Se esperan fallos gráficos temporales cuando ésta esté activado.</string>
|
|||
<string name="november">Noviembre</string>
|
||||
<string name="december">Diciembre</string>
|
||||
|
||||
<!-- Artic base -->
|
||||
<string name="artic_server_comm_error">Fallo de comunicación con el servidor Artic Base. La emulación se detendrá.</string>
|
||||
<string name="artic_base">Artic Base</string>
|
||||
<string name="artic_base_connect">Conectar con Artic Base</string>
|
||||
<string name="artic_base_connect_description">Conectar con una consola real que esté ejecutando un servidor Artic Base</string>
|
||||
<string name="artic_base_enter_address">Introduce la dirección del servidor Artic Base</string>
|
||||
<string name="delay_render_thread">Retrasa el hilo de dibujado del juego</string>
|
||||
<string name="delay_render_thread_description">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.</string>
|
||||
<string name="miscellaneous">Misceláneo</string>
|
||||
<string name="use_artic_base_controller">Usar Artic Controller cuando se está conectado a Artic Base Server</string>
|
||||
<string name="use_artic_base_controller_desc">Usa los controles proporcionados por Artic Base Server cuando esté conectado a él en lugar del dispositivo de entrada configurado.</string>
|
||||
|
||||
</resources>
|
||||
|
|
|
@ -683,4 +683,25 @@
|
|||
<string name="november">November</string>
|
||||
<string name="december">December</string>
|
||||
|
||||
<!-- Artic base -->
|
||||
<string name="artic_server_comm_error">Failed to communicate with the Artic Base server. Emulation will stop.</string>
|
||||
<string name="artic_base">Artic Base</string>
|
||||
<string name="artic_base_connect_description">Connect to a real console that is running an Artic Base server</string>
|
||||
<string name="artic_base_connect">Connect to Artic Base</string>
|
||||
<string name="artic_base_enter_address">Enter Artic Base server address</string>
|
||||
<string name="delay_render_thread">Delay game render thread</string>
|
||||
<string name="delay_render_thread_description">Delay the game render thread when it submits data to the GPU. Helps with performance issues in the (very few) dynamic-fps games.</string>
|
||||
|
||||
<!-- Quickload&Save-->
|
||||
<string name="emulation_quicksave_slot">Quicksave</string>
|
||||
<string name="emulation_quicksave">Quicksave</string>
|
||||
<string name="emulation_quickload">Quickload</string>
|
||||
<string name="emulation_occupied_quicksave_slot">Quicksave - %1$tF %1$tR</string>
|
||||
<string name="quicksave_saving">Saving…</string>
|
||||
<string name="quickload_loading">Loading…</string>
|
||||
<string name="quickload_not_found">No Quicksave available.</string>
|
||||
<string name="miscellaneous">Miscellaneous</string>
|
||||
<string name="use_artic_base_controller">Use Artic Controller when connected to Artic Base Server</string>
|
||||
<string name="use_artic_base_controller_desc">Use the controls provided by Artic Base Server when connected to it instead of the configured input device.</string>
|
||||
|
||||
</resources>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -97,6 +97,12 @@
|
|||
<header>configuration/configure_enhancements.h</header>
|
||||
<container>1</container>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>ConfigureWeb</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>configuration/configure_web.h</header>
|
||||
<container>1</container>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>ConfigureUi</class>
|
||||
<extends>QWidget</extends>
|
||||
|
|
|
@ -86,7 +86,7 @@
|
|||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout1">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_3">
|
||||
<property name="text">
|
||||
|
@ -100,7 +100,7 @@
|
|||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout2">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="toggle_console">
|
||||
<property name="text">
|
||||
|
@ -125,7 +125,7 @@
|
|||
<property name="title">
|
||||
<string>CPU</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_2">
|
||||
<layout class="QGridLayout" name="clock_speed_GLayout">
|
||||
<item row="1" column="0">
|
||||
<widget class="QWidget" name="clock_speed_widget" native="true">
|
||||
<layout class="QHBoxLayout" name="clock_speed_layout">
|
||||
|
|
|
@ -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<ConfigureGeneral>(this)},
|
||||
system_tab{std::make_unique<ConfigureSystem>(system, this)},
|
||||
input_tab{std::make_unique<ConfigureInput>(this)},
|
||||
input_tab{std::make_unique<ConfigureInput>(system, this)},
|
||||
hotkeys_tab{std::make_unique<ConfigureHotkeys>(this)},
|
||||
graphics_tab{
|
||||
std::make_unique<ConfigureGraphics>(gl_renderer, physical_devices, is_powered_on, this)},
|
||||
|
@ -37,7 +38,7 @@ ConfigureDialog::ConfigureDialog(QWidget* parent, HotkeyRegistry& registry_, Cor
|
|||
camera_tab{std::make_unique<ConfigureCamera>(this)},
|
||||
debug_tab{std::make_unique<ConfigureDebug>(is_powered_on, this)},
|
||||
storage_tab{std::make_unique<ConfigureStorage>(is_powered_on, this)},
|
||||
ui_tab{std::make_unique<ConfigureUi>(this)} {
|
||||
web_tab{std::make_unique<ConfigureWeb>(this)}, ui_tab{std::make_unique<ConfigureUi>(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<std::pair<QString, QList<QWidget*>>, 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();
|
||||
|
|
|
@ -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<ConfigureCamera> camera_tab;
|
||||
std::unique_ptr<ConfigureDebug> debug_tab;
|
||||
std::unique_ptr<ConfigureStorage> storage_tab;
|
||||
std::unique_ptr<ConfigureWeb> web_tab;
|
||||
std::unique_ptr<ConfigureUi> ui_tab;
|
||||
};
|
||||
|
|
|
@ -26,6 +26,10 @@ ConfigureGraphics::ConfigureGraphics(QString gl_renderer, std::span<const QStrin
|
|||
// Set the index to -1 to ensure the below lambda is called with setCurrentIndex
|
||||
ui->graphics_api_combo->setCurrentIndex(-1);
|
||||
|
||||
const auto width = static_cast<int>(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<QStandardItemModel*>(ui->graphics_api_combo->model());
|
||||
#ifndef ENABLE_SOFTWARE_RENDERER
|
||||
|
@ -82,12 +86,25 @@ ConfigureGraphics::ConfigureGraphics(QString gl_renderer, std::span<const QStrin
|
|||
connect(ui->graphics_api_combo, qOverload<int>(&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<int>(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<int>(&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(
|
||||
|
|
|
@ -307,6 +307,83 @@
|
|||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QWidget" name="delay_render_layout" native="true">
|
||||
<layout class="QHBoxLayout" name="delay_render_layout_inner">
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QComboBox" name="delay_render_combo">
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Use global</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Use per-game</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_delay_render">
|
||||
<property name="text">
|
||||
<string>Delay game render thread:</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string><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></string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSlider" name="delay_render_slider">
|
||||
<property name="minimum">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>16000</number>
|
||||
</property>
|
||||
<property name="singleStep">
|
||||
<number>100</number>
|
||||
</property>
|
||||
<property name="pageStep">
|
||||
<number>250</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="tickPosition">
|
||||
<enum>QSlider::TicksBelow</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="delay_render_display_label">
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
|
|
|
@ -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<std::string, ConfigureInput::ANALOG_SUB_BUTTONS_NUM>
|
||||
|
@ -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<Ui::ConfigureInput>()),
|
||||
ConfigureInput::ConfigureInput(Core::System& _system, QWidget* parent)
|
||||
: QWidget(parent), system(_system), ui(std::make_unique<Ui::ConfigureInput>()),
|
||||
timeout_timer(std::make_unique<QTimer>()), poll_timer(std::make_unique<QTimer>()) {
|
||||
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<QKeySequence> 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); });
|
||||
|
|
|
@ -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<QKeySequence> new_key_list);
|
||||
|
||||
private:
|
||||
Core::System& system;
|
||||
std::unique_ptr<Ui::ConfigureInput> ui;
|
||||
|
||||
std::unique_ptr<QTimer> timeout_timer;
|
||||
|
|
|
@ -841,6 +841,13 @@
|
|||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="use_artic_controller">
|
||||
<property name="text">
|
||||
<string>Use Artic Controller when connected to Artic Base Server</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
|
|
|
@ -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::AppLoader> 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)
|
||||
|
|
File diff suppressed because it is too large
Load diff
36
src/citra_qt/configuration/configure_web.cpp
Normal file
36
src/citra_qt/configuration/configure_web.cpp
Normal file
|
@ -0,0 +1,36 @@
|
|||
// Copyright 2017 Citra Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#include <QIcon>
|
||||
#include <QMessageBox>
|
||||
#include <QtConcurrent/QtConcurrentRun>
|
||||
#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::ConfigureWeb>()) {
|
||||
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);
|
||||
}
|
28
src/citra_qt/configuration/configure_web.h
Normal file
28
src/citra_qt/configuration/configure_web.h
Normal file
|
@ -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 <memory>
|
||||
#include <QFutureWatcher>
|
||||
#include <QWidget>
|
||||
|
||||
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::ConfigureWeb> ui;
|
||||
};
|
53
src/citra_qt/configuration/configure_web.ui
Normal file
53
src/citra_qt/configuration/configure_web.ui
Normal file
|
@ -0,0 +1,53 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>ConfigureWeb</class>
|
||||
<widget class="QWidget" name="ConfigureWeb">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>996</width>
|
||||
<height>561</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QGroupBox" name="discord_group">
|
||||
<property name="title">
|
||||
<string>Discord Presence</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_21">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="toggle_discordrpc">
|
||||
<property name="text">
|
||||
<string>Show Current Game in your Discord Status</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<tabstops>
|
||||
<tabstop>toggle_discordrpc</tabstop>
|
||||
</tabstops>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
|
@ -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<std::pair<Core::PerfStats::PerfArticEventBits, QString>, 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 <a "
|
||||
|
@ -2756,6 +2875,13 @@ void GMainWindow::OnCoreError(Core::System::ResultStatus result, std::string det
|
|||
title = tr("Save/load Error");
|
||||
message = QString::fromStdString(details);
|
||||
error_severity_icon = QMessageBox::Icon::Warning;
|
||||
} else if (result == Core::System::ResultStatus::ErrorArticDisconnected) {
|
||||
title = tr("Artic Base Server");
|
||||
message =
|
||||
tr(fmt::format("A communication error has occurred. The game will quit.\n{}", details)
|
||||
.c_str());
|
||||
error_severity_icon = QMessageBox::Icon::Critical;
|
||||
can_continue = false;
|
||||
} else {
|
||||
title = tr("Fatal Error");
|
||||
message =
|
||||
|
@ -2772,12 +2898,14 @@ void GMainWindow::OnCoreError(Core::System::ResultStatus result, std::string det
|
|||
message_box.setText(message);
|
||||
message_box.setIcon(error_severity_icon);
|
||||
if (error_severity_icon == QMessageBox::Icon::Critical) {
|
||||
message_box.addButton(tr("Continue"), QMessageBox::RejectRole);
|
||||
if (can_continue) {
|
||||
message_box.addButton(tr("Continue"), QMessageBox::RejectRole);
|
||||
}
|
||||
QPushButton* abort_button = message_box.addButton(tr("Quit Game"), QMessageBox::AcceptRole);
|
||||
if (result != Core::System::ResultStatus::ShutdownRequested)
|
||||
message_box.exec();
|
||||
|
||||
if (result == Core::System::ResultStatus::ShutdownRequested ||
|
||||
if (!can_continue || result == Core::System::ResultStatus::ShutdownRequested ||
|
||||
message_box.clickedButton() == abort_button) {
|
||||
if (emu_thread) {
|
||||
ShutdownGame();
|
||||
|
|
|
@ -216,6 +216,7 @@ private slots:
|
|||
void OnConfigurePerGame();
|
||||
void OnMenuLoadFile();
|
||||
void OnMenuInstallCIA();
|
||||
void OnMenuConnectArticBase();
|
||||
void OnMenuBootHomeMenu(u32 region);
|
||||
void OnUpdateProgress(std::size_t written, std::size_t total);
|
||||
void OnCIAInstallReport(Service::AM::InstallStatus status, QString filepath);
|
||||
|
@ -302,6 +303,8 @@ private:
|
|||
// Status bar elements
|
||||
QProgressBar* progress_bar = nullptr;
|
||||
QLabel* message_label = nullptr;
|
||||
bool show_artic_label = false;
|
||||
QLabel* artic_traffic_label = nullptr;
|
||||
QLabel* emu_speed_label = nullptr;
|
||||
QLabel* game_fps_label = nullptr;
|
||||
QLabel* emu_frametime_label = nullptr;
|
||||
|
|
|
@ -78,6 +78,7 @@
|
|||
</widget>
|
||||
<addaction name="action_Load_File"/>
|
||||
<addaction name="action_Install_CIA"/>
|
||||
<addaction name="action_Connect_Artic"/>
|
||||
<addaction name="menu_Boot_Home_Menu"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="menu_recent_files"/>
|
||||
|
@ -202,6 +203,7 @@
|
|||
<addaction name="separator"/>
|
||||
<addaction name="action_Report_Compatibility"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="action_Open_Log_Folder"/>
|
||||
<addaction name="action_FAQ"/>
|
||||
<addaction name="action_About"/>
|
||||
</widget>
|
||||
|
@ -222,6 +224,11 @@
|
|||
<string>Install CIA...</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_Connect_Artic">
|
||||
<property name="text">
|
||||
<string>Connect to Artic Base...</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_Boot_Home_Menu_JPN">
|
||||
<property name="text">
|
||||
<string>JPN</string>
|
||||
|
@ -473,6 +480,14 @@
|
|||
<string>Fullscreen</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_Open_Log_Folder">
|
||||
<property name="text">
|
||||
<string>Open Log Folder</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Opens the Citra Log folder</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_Open_Maintenance_Tool">
|
||||
<property name="text">
|
||||
<string>Modify Citra Install</string>
|
||||
|
|
|
@ -116,6 +116,7 @@ struct Values {
|
|||
bool game_dir_deprecated_deepscan;
|
||||
QVector<UISettings::GameDir> game_dirs;
|
||||
QStringList recent_files;
|
||||
QString last_artic_base_addr;
|
||||
QString language;
|
||||
|
||||
QString theme;
|
||||
|
|
|
@ -12,9 +12,9 @@
|
|||
#if FMT_VERSION >= 80100
|
||||
template <typename T>
|
||||
struct fmt::formatter<T, std::enable_if_t<std::is_enum_v<T>, char>>
|
||||
: formatter<std::underlying_type_t<T>> {
|
||||
: fmt::formatter<std::underlying_type_t<T>> {
|
||||
template <typename FormatContext>
|
||||
auto format(const T& value, FormatContext& ctx) -> decltype(ctx.out()) {
|
||||
auto format(const T& value, FormatContext& ctx) const -> decltype(ctx.out()) {
|
||||
return fmt::formatter<std::underlying_type_t<T>>::format(
|
||||
static_cast<std::underlying_type_t<T>>(value), ctx);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -425,6 +425,7 @@ struct Values {
|
|||
int current_input_profile_index; ///< The current input profile index
|
||||
std::vector<InputProfile> input_profiles; ///< The list of input profiles
|
||||
std::vector<TouchFromButtonMap> touch_from_button_maps;
|
||||
Setting<bool> use_artic_base_controller{false, "use_artic_base_controller"};
|
||||
|
||||
SwitchableSetting<bool> enable_gamemode{true, "enable_gamemode"};
|
||||
|
||||
|
@ -479,6 +480,8 @@ struct Values {
|
|||
SwitchableSetting<TextureFilter> texture_filter{TextureFilter::None, "texture_filter"};
|
||||
SwitchableSetting<TextureSampling> texture_sampling{TextureSampling::GameControlled,
|
||||
"texture_sampling"};
|
||||
SwitchableSetting<u16, true> delay_game_render_thread_us{0, 0, 16000,
|
||||
"delay_game_render_thread_us"};
|
||||
|
||||
SwitchableSetting<LayoutOption> layout_option{LayoutOption::Default, "layout_option"};
|
||||
SwitchableSetting<bool> swap_screen{false, "swap_screen"};
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
//---------------------------------------------------------------------------//
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <list>
|
||||
#include <tuple>
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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::AppLoader>& loader) {
|
||||
early_app_loader = std::move(loader);
|
||||
}
|
||||
|
||||
template <class Archive>
|
||||
void System::serialize(Archive& ar, const unsigned int file_version) {
|
||||
|
||||
|
|
|
@ -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::AppLoader>& loader);
|
||||
|
||||
private:
|
||||
/**
|
||||
* Initialize the emulated system.
|
||||
|
@ -366,6 +381,9 @@ private:
|
|||
/// AppLoader used to load the current executing application
|
||||
std::unique_ptr<Loader::AppLoader> app_loader;
|
||||
|
||||
// Temporary app loader passed from frontend
|
||||
std::unique_ptr<Loader::AppLoader> early_app_loader;
|
||||
|
||||
/// ARM11 CPU core
|
||||
std::vector<std::shared_ptr<ARM_Interface>> cpu_cores;
|
||||
ARM_Interface* running_core = nullptr;
|
||||
|
|
557
src/core/file_sys/archive_artic.cpp
Normal file
557
src/core/file_sys/archive_artic.cpp
Normal file
|
@ -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<u8> ArticArchive::BuildFSPath(const Path& path) {
|
||||
std::vector<u8> ret(sizeof(u32) * 2);
|
||||
u32* raw_data = reinterpret_cast<u32*>(ret.data());
|
||||
auto path_type = path.GetType();
|
||||
auto binary = path.AsBinary();
|
||||
raw_data[0] = static_cast<u32>(path_type);
|
||||
raw_data[1] = static_cast<u32>(binary.size());
|
||||
if (!binary.empty()) {
|
||||
ret.insert(ret.end(), binary.begin(), binary.end());
|
||||
}
|
||||
|
||||
// The insert may have invalidated the pointer
|
||||
raw_data = reinterpret_cast<u32*>(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<Network::ArticBase::Client::Response>& resp) {
|
||||
if (!resp.has_value() || !resp->Succeeded()) {
|
||||
return ResultUnknown;
|
||||
}
|
||||
return Result(static_cast<u32>(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<u64>(report_artic_event));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ResultVal<std::unique_ptr<ArchiveBackend>> ArticArchive::Open(
|
||||
std::shared_ptr<Network::ArticBase::Client>& 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<s32>(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<u32>(resp->GetMethodResult()));
|
||||
if (res.IsError())
|
||||
return res;
|
||||
|
||||
auto handle_opt = resp->GetResponseS64(0);
|
||||
if (!handle_opt.has_value()) {
|
||||
return ResultUnknown;
|
||||
}
|
||||
|
||||
return std::make_unique<ArticArchive>(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<u64>(report_artic_event));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::string ArticArchive::GetName() const {
|
||||
return "ArticArchive";
|
||||
}
|
||||
|
||||
ResultVal<std::unique_ptr<FileBackend>> 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_t>(*size_opt));
|
||||
}
|
||||
}
|
||||
|
||||
if (open_reporter->open_files++ == 0 &&
|
||||
report_artic_event != Core::PerfStats::PerfArticEventBits::NONE) {
|
||||
client->ReportArticEvent(static_cast<u64>(report_artic_event) | (1ULL << 32));
|
||||
}
|
||||
|
||||
return std::make_unique<ArticFileBackend>(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<std::unique_ptr<DirectoryBackend>> 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<u64>(report_artic_event) | (1ULL << 32));
|
||||
}
|
||||
|
||||
return std::make_unique<ArticDirectoryBackend>(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<u64>(*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<u32>(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<std::tuple<bool, bool, u64>> 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<u64>(report_artic_event));
|
||||
}
|
||||
}
|
||||
|
||||
void ArticArchive::OpenFileReporter::OnDirectoryClosed() {
|
||||
if (--open_files == 0 && report_artic_event != Core::PerfStats::PerfArticEventBits::NONE) {
|
||||
client->ReportArticEvent(static_cast<u64>(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<std::size_t> 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<u64>(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<size_t>(client->GetServerRequestMaxSize() - 0x100, length - read_amount);
|
||||
|
||||
auto req = client->NewRequest("FSFILE_Read");
|
||||
req.AddParameterS32(file_handle);
|
||||
req.AddParameterS64(static_cast<s64>(offset + read_amount));
|
||||
req.AddParameterS32(static_cast<s32>(to_read));
|
||||
auto resp = client->Send(req);
|
||||
if (!resp.has_value() || !resp->Succeeded())
|
||||
return Result(-1);
|
||||
|
||||
auto res = Result(static_cast<u32>(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<std::size_t> 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<size_t>(client->GetServerRequestMaxSize() - 0x100,
|
||||
length - written_amount);
|
||||
|
||||
auto req = client->NewRequest("FSFILE_Write");
|
||||
req.AddParameterS32(file_handle);
|
||||
req.AddParameterS64(static_cast<s64>(offset + written_amount));
|
||||
req.AddParameterS32(static_cast<s32>(to_write));
|
||||
req.AddParameterS32(static_cast<s32>(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<u32>(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<size_t>(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<u32>(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
|
268
src/core/file_sys/archive_artic.h
Normal file
268
src/core/file_sys/archive_artic.h
Normal file
|
@ -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 <boost/serialization/unique_ptr.hpp>
|
||||
#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<u8> BuildFSPath(const Path& path);
|
||||
static Result RespResult(const std::optional<Network::ArticBase::Client::Response>& resp);
|
||||
|
||||
explicit ArticArchive(std::shared_ptr<Network::ArticBase::Client>& _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<OpenFileReporter>(_client, _report_artic_event);
|
||||
}
|
||||
~ArticArchive() override;
|
||||
|
||||
static ResultVal<std::unique_ptr<ArchiveBackend>> Open(
|
||||
std::shared_ptr<Network::ArticBase::Client>& 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<std::unique_ptr<FileBackend>> 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<std::unique_ptr<DirectoryBackend>> 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<std::tuple<bool, bool, u64>> 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<Network::ArticBase::Client>& cli,
|
||||
Core::PerfStats::PerfArticEventBits _report_artic_event)
|
||||
: client(cli), report_artic_event(_report_artic_event) {}
|
||||
|
||||
void OnFileClosed();
|
||||
|
||||
void OnDirectoryClosed();
|
||||
|
||||
std::shared_ptr<Network::ArticBase::Client> client;
|
||||
Core::PerfStats::PerfArticEventBits report_artic_event =
|
||||
Core::PerfStats::PerfArticEventBits::NONE;
|
||||
std::atomic<u32> open_files = 0;
|
||||
};
|
||||
|
||||
std::shared_ptr<Network::ArticBase::Client> client;
|
||||
s64 archive_handle;
|
||||
std::shared_ptr<OpenFileReporter> 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 <class Archive>
|
||||
void serialize(Archive& ar, const unsigned int) {
|
||||
ar& boost::serialization::base_object<ArchiveBackend>(*this);
|
||||
ar& archive_handle;
|
||||
}
|
||||
friend class boost::serialization::access;
|
||||
};
|
||||
|
||||
class ArticFileBackend : public FileBackend {
|
||||
public:
|
||||
explicit ArticFileBackend(std::shared_ptr<Network::ArticBase::Client>& _client,
|
||||
s32 _file_handle,
|
||||
const std::shared_ptr<ArticArchive::OpenFileReporter>& _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<std::size_t> Read(u64 offset, std::size_t length, u8* buffer) const override;
|
||||
|
||||
ResultVal<std::size_t> 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<Network::ArticBase::Client> client;
|
||||
s32 file_handle;
|
||||
std::shared_ptr<ArticArchive::OpenFileReporter> open_reporter;
|
||||
Path archive_path;
|
||||
ArticCacheProvider* cache_provider = nullptr;
|
||||
Path file_path;
|
||||
|
||||
template <class Archive>
|
||||
void serialize(Archive& ar, const unsigned int) {
|
||||
ar& boost::serialization::base_object<FileBackend>(*this);
|
||||
ar& file_handle;
|
||||
}
|
||||
friend class boost::serialization::access;
|
||||
};
|
||||
|
||||
class ArticDirectoryBackend : public DirectoryBackend {
|
||||
public:
|
||||
explicit ArticDirectoryBackend(
|
||||
std::shared_ptr<Network::ArticBase::Client>& _client, s32 _dir_handle,
|
||||
const Path& _archive_path,
|
||||
const std::shared_ptr<ArticArchive::OpenFileReporter>& _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<Network::ArticBase::Client> client;
|
||||
s32 dir_handle;
|
||||
Path archive_path;
|
||||
std::shared_ptr<ArticArchive::OpenFileReporter> open_reporter;
|
||||
|
||||
template <class Archive>
|
||||
void serialize(Archive& ar, const unsigned int) {
|
||||
ar& boost::serialization::base_object<DirectoryBackend>(*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)
|
|
@ -105,8 +105,7 @@ std::vector<u8> Path::AsBinary() const {
|
|||
std::vector<u8> 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<u16*>(to_return.data() + i * 2) = tmp_char;
|
||||
}
|
||||
return to_return;
|
||||
}
|
||||
|
|
|
@ -103,6 +103,7 @@ struct ArchiveFormatInfo {
|
|||
u8 duplicate_data; ///< Whether the archive should duplicate the data.
|
||||
};
|
||||
static_assert(std::is_trivial_v<ArchiveFormatInfo>, "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<std::unique_ptr<FileBackend>> OpenFile(const Path& path,
|
||||
const Mode& mode) const = 0;
|
||||
virtual ResultVal<std::unique_ptr<FileBackend>> 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<std::unique_ptr<DirectoryBackend>> OpenDirectory(const Path& path) const = 0;
|
||||
virtual ResultVal<std::unique_ptr<DirectoryBackend>> 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<std::tuple<bool, bool, u64>> 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<bool, bool, u64>(false, true, 0);
|
||||
}
|
||||
|
||||
virtual bool IsSlow() {
|
||||
return false;
|
||||
}
|
||||
|
||||
protected:
|
||||
std::unique_ptr<DelayGenerator> 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<ArchiveFormatInfo> GetFormatInfo(const Path& path, u64 program_id) const = 0;
|
||||
|
||||
virtual bool IsSlow() {
|
||||
return false;
|
||||
}
|
||||
|
||||
template <class Archive>
|
||||
void serialize(Archive& ar, const unsigned int) {}
|
||||
friend class boost::serialization::access;
|
||||
|
|
|
@ -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<std::size_t> Write(u64 offset, std::size_t length, bool flush,
|
||||
ResultVal<std::size_t> 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<std::unique_ptr<FileBackend>> OpenFile(const Path& path,
|
||||
const Mode& mode) const override {
|
||||
ResultVal<std::unique_ptr<FileBackend>> 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<std::unique_ptr<ArchiveBackend>> 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<DelayGenerator> delay_generator = std::make_unique<ExtSaveDataDelayGenerator>();
|
||||
return std::make_unique<ExtSaveDataArchive>(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<std::unique_ptr<ArchiveBackend>> 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<DelayGenerator> delay_generator =
|
||||
std::make_unique<ExtSaveDataDelayGenerator>();
|
||||
return std::make_unique<ExtSaveDataArchive>(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<std::span<const u8>> 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<u8>(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<u8>(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<u32>(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<ArchiveFormatInfo> 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<u32>(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<const u8> 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)
|
||||
|
|
|
@ -5,13 +5,17 @@
|
|||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <boost/serialization/export.hpp>
|
||||
#include <boost/serialization/string.hpp>
|
||||
#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<std::unique_ptr<ArchiveBackend>> Open(const Path& path, u64 program_id) override;
|
||||
Result Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info,
|
||||
u64 program_id) override;
|
||||
|
||||
ResultVal<ArchiveFormatInfo> 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<const u8> 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<std::span<const u8>> icon);
|
||||
|
||||
Result DeleteExtData(Service::FS::MediaType media_type, u8 unknown, u32 high, u32 low);
|
||||
|
||||
void RegisterArtic(std::shared_ptr<Network::ArticBase::Client>& 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<Network::ArticBase::Client> artic_client = nullptr;
|
||||
|
||||
ArchiveFactory_ExtSaveData() = default;
|
||||
template <class Archive>
|
||||
void serialize(Archive& ar, const unsigned int) {
|
||||
ar& boost::serialization::base_object<ArchiveFactory>(*this);
|
||||
ar& boost::serialization::base_object<ArticCacheProvider>(*this);
|
||||
ar& type;
|
||||
ar& mount_point;
|
||||
}
|
||||
|
|
|
@ -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<std::unique_ptr<FileBackend>> NCCHArchive::OpenFile(const Path& path,
|
||||
const Mode& mode) const {
|
||||
ResultVal<std::unique_ptr<FileBackend>> 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<std::unique_ptr<DirectoryBackend>> NCCHArchive::OpenDirectory(const Path& path) const {
|
||||
ResultVal<std::unique_ptr<DirectoryBackend>> 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<std::size_t> NCCHFile::Read(const u64 offset, const std::size_t length
|
|||
}
|
||||
|
||||
ResultVal<std::size_t> 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<std::unique_ptr<ArchiveBackend>> 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<std::unique_ptr<ArchiveBackend>> 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,
|
||||
|
|
|
@ -11,8 +11,10 @@
|
|||
#include <boost/serialization/export.hpp>
|
||||
#include <boost/serialization/vector.hpp>
|
||||
#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<std::unique_ptr<FileBackend>> OpenFile(const Path& path,
|
||||
const Mode& mode) const override;
|
||||
ResultVal<std::unique_ptr<FileBackend>> 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<std::unique_ptr<DirectoryBackend>> OpenDirectory(const Path& path) const override;
|
||||
ResultVal<std::unique_ptr<DirectoryBackend>> OpenDirectory(const Path& path) override;
|
||||
u64 GetFreeBytes() const override;
|
||||
|
||||
protected:
|
||||
|
@ -82,11 +84,11 @@ public:
|
|||
explicit NCCHFile(std::vector<u8> buffer, std::unique_ptr<DelayGenerator> delay_generator_);
|
||||
|
||||
ResultVal<std::size_t> Read(u64 offset, std::size_t length, u8* buffer) const override;
|
||||
ResultVal<std::size_t> Write(u64 offset, std::size_t length, bool flush,
|
||||
ResultVal<std::size_t> 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<std::unique_ptr<ArchiveBackend>> 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<ArchiveFormatInfo> GetFormatInfo(const Path& path, u64 program_id) const override;
|
||||
|
||||
bool IsSlow() override {
|
||||
return IsUsingArtic();
|
||||
}
|
||||
|
||||
void RegisterArtic(std::shared_ptr<Network::ArticBase::Client>& client) {
|
||||
artic_client = client;
|
||||
}
|
||||
|
||||
bool IsUsingArtic() const {
|
||||
return artic_client.get() != nullptr;
|
||||
}
|
||||
|
||||
private:
|
||||
std::shared_ptr<Network::ArticBase::Client> artic_client = nullptr;
|
||||
|
||||
template <class Archive>
|
||||
void serialize(Archive& ar, const unsigned int) {
|
||||
ar& boost::serialization::base_object<ArchiveFactory>(*this);
|
||||
ar& boost::serialization::base_object<ArticCacheProvider>(*this);
|
||||
}
|
||||
friend class boost::serialization::access;
|
||||
};
|
||||
|
|
|
@ -75,12 +75,14 @@ ResultVal<std::unique_ptr<ArchiveBackend>> 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<ArchiveFormatInfo> 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<std::unique_ptr<ArchiveBackend>> 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<ArchiveFormatInfo> ArchiveFactory_OtherSaveDataGeneral::GetFormatInfo(
|
||||
|
@ -143,7 +150,8 @@ ResultVal<ArchiveFormatInfo> 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
|
||||
|
|
|
@ -22,10 +22,14 @@ public:
|
|||
}
|
||||
|
||||
ResultVal<std::unique_ptr<ArchiveBackend>> 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<ArchiveFormatInfo> GetFormatInfo(const Path& path, u64 program_id) const override;
|
||||
|
||||
bool IsSlow() override {
|
||||
return sd_savedata_source->IsUsingArtic();
|
||||
}
|
||||
|
||||
private:
|
||||
std::shared_ptr<ArchiveSource_SDSaveData> sd_savedata_source;
|
||||
|
||||
|
@ -49,8 +53,8 @@ public:
|
|||
}
|
||||
|
||||
ResultVal<std::unique_ptr<ArchiveBackend>> 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<ArchiveFormatInfo> GetFormatInfo(const Path& path, u64 program_id) const override;
|
||||
|
||||
private:
|
||||
|
|
|
@ -18,18 +18,20 @@ ArchiveFactory_SaveData::ArchiveFactory_SaveData(
|
|||
|
||||
ResultVal<std::unique_ptr<ArchiveBackend>> 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<ArchiveFormatInfo> 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
|
||||
|
|
|
@ -20,11 +20,15 @@ public:
|
|||
}
|
||||
|
||||
ResultVal<std::unique_ptr<ArchiveBackend>> 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<ArchiveFormatInfo> GetFormatInfo(const Path& path, u64 program_id) const override;
|
||||
|
||||
bool IsSlow() override {
|
||||
return sd_savedata_source->IsUsingArtic();
|
||||
}
|
||||
|
||||
private:
|
||||
std::shared_ptr<ArchiveSource_SDSaveData> sd_savedata_source;
|
||||
|
||||
|
|
|
@ -43,8 +43,8 @@ public:
|
|||
SERIALIZE_DELAY_GENERATOR
|
||||
};
|
||||
|
||||
ResultVal<std::unique_ptr<FileBackend>> SDMCArchive::OpenFile(const Path& path,
|
||||
const Mode& mode) const {
|
||||
ResultVal<std::unique_ptr<FileBackend>> 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<std::unique_ptr<DirectoryBackend>> SDMCArchive::OpenDirectory(const Path& path) const {
|
||||
ResultVal<std::unique_ptr<DirectoryBackend>> SDMCArchive::OpenDirectory(const Path& path) {
|
||||
const PathParser path_parser(path);
|
||||
|
||||
if (!path_parser.IsValid()) {
|
||||
|
@ -392,7 +392,7 @@ ResultVal<std::unique_ptr<ArchiveBackend>> 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;
|
||||
}
|
||||
|
|
|
@ -27,16 +27,16 @@ public:
|
|||
return "SDMCArchive: " + mount_point;
|
||||
}
|
||||
|
||||
ResultVal<std::unique_ptr<FileBackend>> OpenFile(const Path& path,
|
||||
const Mode& mode) const override;
|
||||
ResultVal<std::unique_ptr<FileBackend>> 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<std::unique_ptr<DirectoryBackend>> OpenDirectory(const Path& path) const override;
|
||||
ResultVal<std::unique_ptr<DirectoryBackend>> OpenDirectory(const Path& path) override;
|
||||
u64 GetFreeBytes() const override;
|
||||
|
||||
protected:
|
||||
|
@ -68,8 +68,8 @@ public:
|
|||
}
|
||||
|
||||
ResultVal<std::unique_ptr<ArchiveBackend>> 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<ArchiveFormatInfo> GetFormatInfo(const Path& path, u64 program_id) const override;
|
||||
|
||||
private:
|
||||
|
|
|
@ -41,7 +41,8 @@ public:
|
|||
};
|
||||
|
||||
ResultVal<std::unique_ptr<FileBackend>> 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<std::unique_ptr<FileBackend>> SDMCWriteOnlyArchive::OpenFile(const Pat
|
|||
return SDMCArchive::OpenFileBase(path, mode);
|
||||
}
|
||||
|
||||
ResultVal<std::unique_ptr<DirectoryBackend>> SDMCWriteOnlyArchive::OpenDirectory(
|
||||
const Path& path) const {
|
||||
ResultVal<std::unique_ptr<DirectoryBackend>> SDMCWriteOnlyArchive::OpenDirectory(const Path& path) {
|
||||
LOG_ERROR(Service_FS, "Not supported");
|
||||
return ResultUnsupportedOpenFlags;
|
||||
}
|
||||
|
@ -83,7 +83,8 @@ ResultVal<std::unique_ptr<ArchiveBackend>> 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;
|
||||
|
|
|
@ -24,10 +24,10 @@ public:
|
|||
return "SDMCWriteOnlyArchive: " + mount_point;
|
||||
}
|
||||
|
||||
ResultVal<std::unique_ptr<FileBackend>> OpenFile(const Path& path,
|
||||
const Mode& mode) const override;
|
||||
ResultVal<std::unique_ptr<FileBackend>> OpenFile(const Path& path, const Mode& mode,
|
||||
u32 attributes) override;
|
||||
|
||||
ResultVal<std::unique_ptr<DirectoryBackend>> OpenDirectory(const Path& path) const override;
|
||||
ResultVal<std::unique_ptr<DirectoryBackend>> OpenDirectory(const Path& path) override;
|
||||
|
||||
private:
|
||||
SDMCWriteOnlyArchive() = default;
|
||||
|
@ -54,8 +54,8 @@ public:
|
|||
}
|
||||
|
||||
ResultVal<std::unique_ptr<ArchiveBackend>> 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<ArchiveFormatInfo> GetFormatInfo(const Path& path, u64 program_id) const override;
|
||||
|
||||
private:
|
||||
|
|
|
@ -51,7 +51,7 @@ public:
|
|||
return data->size();
|
||||
}
|
||||
|
||||
ResultVal<std::size_t> Write(u64 offset, std::size_t length, bool flush,
|
||||
ResultVal<std::size_t> 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<std::unique_ptr<FileBackend>> OpenFile(const Path& path, const Mode&) const override {
|
||||
ResultVal<std::unique_ptr<FileBackend>> 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<std::unique_ptr<DirectoryBackend>> OpenDirectory(const Path& path) const override {
|
||||
ResultVal<std::unique_ptr<DirectoryBackend>> OpenDirectory(const Path& path) override {
|
||||
LOG_ERROR(Service_FS, "Unsupported");
|
||||
return ResultUnsupportedOpenFlags;
|
||||
}
|
||||
|
@ -297,7 +298,7 @@ ResultVal<std::unique_ptr<ArchiveBackend>> 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;
|
||||
}
|
||||
|
|
|
@ -50,8 +50,8 @@ public:
|
|||
return "SelfNCCH";
|
||||
}
|
||||
ResultVal<std::unique_ptr<ArchiveBackend>> 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<ArchiveFormatInfo> GetFormatInfo(const Path& path, u64 program_id) const override;
|
||||
|
||||
private:
|
||||
|
|
|
@ -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<std::unique_ptr<ArchiveBackend>> 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<std::unique_ptr<ArchiveBackend>> 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<SaveDataArchive>(std::move(concrete_mount_point));
|
||||
return std::make_unique<SaveDataArchive>(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<u32>(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<ArchiveFormatInfo> ArchiveSource_SDSaveData::GetFormatInfo(u64 program_id) const {
|
||||
std::string metadata_path = GetSaveDataMetadataPath(mount_point, program_id);
|
||||
FileUtil::IOFile file(metadata_path, "rb");
|
||||
ResultVal<ArchiveFormatInfo> 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<u32>(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,
|
||||
|
|
|
@ -9,27 +9,48 @@
|
|||
#include <boost/serialization/export.hpp>
|
||||
#include <boost/serialization/string.hpp>
|
||||
#include "core/file_sys/archive_backend.h"
|
||||
#include "core/file_sys/artic_cache.h"
|
||||
#include "core/hle/result.h"
|
||||
#include "network/artic_base/artic_base_client.h"
|
||||
|
||||
namespace Service::FS {
|
||||
enum class ArchiveIdCode : u32;
|
||||
} // namespace Service::FS
|
||||
|
||||
namespace FileSys {
|
||||
|
||||
/// A common source of SD save data archive
|
||||
class ArchiveSource_SDSaveData {
|
||||
class ArchiveSource_SDSaveData : public ArticCacheProvider {
|
||||
public:
|
||||
explicit ArchiveSource_SDSaveData(const std::string& mount_point);
|
||||
|
||||
ResultVal<std::unique_ptr<ArchiveBackend>> Open(u64 program_id);
|
||||
Result Format(u64 program_id, const FileSys::ArchiveFormatInfo& format_info);
|
||||
ResultVal<ArchiveFormatInfo> GetFormatInfo(u64 program_id) const;
|
||||
ResultVal<std::unique_ptr<ArchiveBackend>> Open(Service::FS::ArchiveIdCode archive_id,
|
||||
const Path& path, u64 program_id);
|
||||
Result Format(u64 program_id, const FileSys::ArchiveFormatInfo& format_info,
|
||||
Service::FS::ArchiveIdCode archive_id, const Path& path, u32 directory_buckets,
|
||||
u32 file_buckets);
|
||||
ResultVal<ArchiveFormatInfo> GetFormatInfo(u64 program_id,
|
||||
Service::FS::ArchiveIdCode archive_id,
|
||||
const Path& path) const;
|
||||
|
||||
static std::string GetSaveDataPathFor(const std::string& mount_point, u64 program_id);
|
||||
|
||||
void RegisterArtic(std::shared_ptr<Network::ArticBase::Client>& client) {
|
||||
artic_client = client;
|
||||
}
|
||||
|
||||
bool IsUsingArtic() const {
|
||||
return artic_client.get() != nullptr;
|
||||
}
|
||||
|
||||
private:
|
||||
std::string mount_point;
|
||||
std::shared_ptr<Network::ArticBase::Client> artic_client = nullptr;
|
||||
|
||||
ArchiveSource_SDSaveData() = default;
|
||||
template <class Archive>
|
||||
void serialize(Archive& ar, const unsigned int) {
|
||||
ar& boost::serialization::base_object<ArticCacheProvider>(*this);
|
||||
ar& mount_point;
|
||||
}
|
||||
friend class boost::serialization::access;
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
#include "common/archives.h"
|
||||
#include "common/common_types.h"
|
||||
#include "common/file_util.h"
|
||||
#include "core/file_sys/archive_artic.h"
|
||||
#include "core/file_sys/archive_systemsavedata.h"
|
||||
#include "core/file_sys/errors.h"
|
||||
#include "core/file_sys/savedata_archive.h"
|
||||
|
@ -52,23 +53,45 @@ Path ConstructSystemSaveDataBinaryPath(u32 high, u32 low) {
|
|||
ArchiveFactory_SystemSaveData::ArchiveFactory_SystemSaveData(const std::string& nand_path)
|
||||
: base_path(GetSystemSaveDataContainerPath(nand_path)) {}
|
||||
|
||||
static bool AllowArticSystemSaveData(const Path& path) {
|
||||
constexpr u32 APP_SYSTEM_SAVE_DATA_MASK = 0x00020000;
|
||||
if (path.GetType() == FileSys::LowPathType::Binary) {
|
||||
std::vector<u8> path_data = path.AsBinary();
|
||||
return path_data.size() == 8 &&
|
||||
(*reinterpret_cast<u32*>(path_data.data() + 4) & APP_SYSTEM_SAVE_DATA_MASK) != 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
ResultVal<std::unique_ptr<ArchiveBackend>> ArchiveFactory_SystemSaveData::Open(const Path& path,
|
||||
u64 program_id) {
|
||||
std::string fullpath = GetSystemSaveDataPath(base_path, path);
|
||||
if (!FileUtil::Exists(fullpath)) {
|
||||
// TODO(Subv): Check error code, this one is probably wrong
|
||||
return ResultNotFound;
|
||||
if (IsUsingArtic() && AllowArticSystemSaveData(path)) {
|
||||
EnsureCacheCreated();
|
||||
return ArticArchive::Open(artic_client, Service::FS::ArchiveIdCode::SystemSaveData, path,
|
||||
Core::PerfStats::PerfArticEventBits::ARTIC_SYSTEM_SAVE_DATA,
|
||||
*this, false);
|
||||
} else {
|
||||
std::string fullpath = GetSystemSaveDataPath(base_path, path);
|
||||
if (!FileUtil::Exists(fullpath)) {
|
||||
// TODO(Subv): Check error code, this one is probably wrong
|
||||
return ResultNotFound;
|
||||
}
|
||||
return std::make_unique<SaveDataArchive>(fullpath);
|
||||
}
|
||||
return std::make_unique<SaveDataArchive>(fullpath);
|
||||
}
|
||||
|
||||
Result ArchiveFactory_SystemSaveData::Format(const Path& path,
|
||||
const FileSys::ArchiveFormatInfo& format_info,
|
||||
u64 program_id) {
|
||||
std::string fullpath = GetSystemSaveDataPath(base_path, path);
|
||||
FileUtil::DeleteDirRecursively(fullpath);
|
||||
FileUtil::CreateFullPath(fullpath);
|
||||
return ResultSuccess;
|
||||
u64 program_id, u32 directory_buckets,
|
||||
u32 file_buckets) {
|
||||
const std::vector<u8> vec_data = path.AsBinary();
|
||||
u32 save_low;
|
||||
u32 save_high;
|
||||
std::memcpy(&save_low, &vec_data[4], sizeof(u32));
|
||||
std::memcpy(&save_high, &vec_data[0], sizeof(u32));
|
||||
return FormatAsSysData(save_high, save_low, format_info.total_size, 0x1000,
|
||||
format_info.number_directories, format_info.number_files,
|
||||
directory_buckets, file_buckets, format_info.duplicate_data);
|
||||
}
|
||||
|
||||
ResultVal<ArchiveFormatInfo> ArchiveFactory_SystemSaveData::GetFormatInfo(const Path& path,
|
||||
|
@ -78,4 +101,45 @@ ResultVal<ArchiveFormatInfo> ArchiveFactory_SystemSaveData::GetFormatInfo(const
|
|||
return ResultUnknown;
|
||||
}
|
||||
|
||||
Result ArchiveFactory_SystemSaveData::FormatAsSysData(u32 high, u32 low, u32 total_size,
|
||||
u32 block_size, u32 number_directories,
|
||||
u32 number_files,
|
||||
u32 number_directory_buckets,
|
||||
u32 number_file_buckets, u8 duplicate_data) {
|
||||
if (IsUsingArtic() &&
|
||||
AllowArticSystemSaveData(FileSys::ConstructSystemSaveDataBinaryPath(high, low))) {
|
||||
auto req = artic_client->NewRequest("FSUSER_CreateSysSaveData");
|
||||
|
||||
req.AddParameterU32(high);
|
||||
req.AddParameterU32(low);
|
||||
req.AddParameterU32(total_size);
|
||||
req.AddParameterU32(block_size);
|
||||
req.AddParameterU32(number_directories);
|
||||
req.AddParameterU32(number_files);
|
||||
req.AddParameterU32(number_directory_buckets);
|
||||
req.AddParameterU32(number_file_buckets);
|
||||
req.AddParameterU8(duplicate_data);
|
||||
|
||||
auto resp = artic_client->Send(req);
|
||||
if (!resp.has_value() || !resp->Succeeded()) {
|
||||
return ResultUnknown;
|
||||
}
|
||||
|
||||
Result res(static_cast<u32>(resp->GetMethodResult()));
|
||||
return res;
|
||||
|
||||
} else {
|
||||
// Construct the binary path to the archive first
|
||||
const FileSys::Path path = FileSys::ConstructSystemSaveDataBinaryPath(high, low);
|
||||
|
||||
const std::string& nand_directory = FileUtil::GetUserPath(FileUtil::UserPath::NANDDir);
|
||||
const std::string base_path = FileSys::GetSystemSaveDataContainerPath(nand_directory);
|
||||
const std::string systemsavedata_path = FileSys::GetSystemSaveDataPath(base_path, path);
|
||||
if (!FileUtil::CreateFullPath(systemsavedata_path)) {
|
||||
return ResultUnknown; // TODO(Subv): Find the right error code
|
||||
}
|
||||
return ResultSuccess;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace FileSys
|
||||
|
|
|
@ -10,27 +10,48 @@
|
|||
#include <boost/serialization/string.hpp>
|
||||
#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 {
|
||||
|
||||
/// File system interface to the SystemSaveData archive
|
||||
class ArchiveFactory_SystemSaveData final : public ArchiveFactory {
|
||||
class ArchiveFactory_SystemSaveData final : public ArchiveFactory, public ArticCacheProvider {
|
||||
public:
|
||||
explicit ArchiveFactory_SystemSaveData(const std::string& mount_point);
|
||||
|
||||
ResultVal<std::unique_ptr<ArchiveBackend>> 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<ArchiveFormatInfo> GetFormatInfo(const Path& path, u64 program_id) const override;
|
||||
|
||||
Result FormatAsSysData(u32 high, u32 low, u32 total_size, u32 block_size,
|
||||
u32 number_directories, u32 number_files, u32 number_directory_buckets,
|
||||
u32 number_file_buckets, u8 duplicate_data);
|
||||
|
||||
std::string GetName() const override {
|
||||
return "SystemSaveData";
|
||||
}
|
||||
|
||||
bool IsSlow() override {
|
||||
return IsUsingArtic();
|
||||
}
|
||||
|
||||
void RegisterArtic(std::shared_ptr<Network::ArticBase::Client>& client) {
|
||||
artic_client = client;
|
||||
}
|
||||
|
||||
bool IsUsingArtic() const {
|
||||
return artic_client.get() != nullptr;
|
||||
}
|
||||
|
||||
private:
|
||||
std::string base_path;
|
||||
|
||||
std::shared_ptr<Network::ArticBase::Client> artic_client = nullptr;
|
||||
|
||||
ArchiveFactory_SystemSaveData() = default;
|
||||
template <class Archive>
|
||||
void serialize(Archive& ar, const unsigned int) {
|
||||
|
|
236
src/core/file_sys/artic_cache.cpp
Normal file
236
src/core/file_sys/artic_cache.cpp
Normal file
|
@ -0,0 +1,236 @@
|
|||
// Copyright 2024 Citra Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#include "artic_cache.h"
|
||||
|
||||
namespace FileSys {
|
||||
ResultVal<std::size_t> ArticCache::Read(s32 file_handle, std::size_t offset, std::size_t length,
|
||||
u8* buffer) {
|
||||
if (length == 0)
|
||||
return size_t();
|
||||
|
||||
const auto segments = BreakupRead(offset, length);
|
||||
std::size_t read_progress = 0;
|
||||
|
||||
// Skip cache if the read is too big
|
||||
if (segments.size() == 1 && segments[0].second > cache_line_size) {
|
||||
if (segments[0].second < big_cache_skip) {
|
||||
std::unique_lock big_read_guard(big_cache_mutex);
|
||||
auto big_cache_entry = big_cache.request(std::make_pair(offset, length));
|
||||
if (!big_cache_entry.first) {
|
||||
LOG_TRACE(Service_FS, "ArticCache BMISS: offset={}, length={}", offset, length);
|
||||
big_cache_entry.second.clear();
|
||||
big_cache_entry.second.resize(length);
|
||||
auto res =
|
||||
ReadFromArtic(file_handle, reinterpret_cast<u8*>(big_cache_entry.second.data()),
|
||||
length, offset);
|
||||
if (res.Failed())
|
||||
return res;
|
||||
length = res.Unwrap();
|
||||
} else {
|
||||
LOG_TRACE(Service_FS, "ArticCache BHIT: offset={}, length={}", offset, length);
|
||||
}
|
||||
memcpy(buffer, big_cache_entry.second.data(), length);
|
||||
} else {
|
||||
if (segments[0].second < very_big_cache_skip) {
|
||||
std::unique_lock very_big_read_guard(very_big_cache_mutex);
|
||||
auto very_big_cache_entry = very_big_cache.request(std::make_pair(offset, length));
|
||||
if (!very_big_cache_entry.first) {
|
||||
LOG_TRACE(Service_FS, "ArticCache VBMISS: offset={}, length={}", offset,
|
||||
length);
|
||||
very_big_cache_entry.second.clear();
|
||||
very_big_cache_entry.second.resize(length);
|
||||
auto res = ReadFromArtic(
|
||||
file_handle, reinterpret_cast<u8*>(very_big_cache_entry.second.data()),
|
||||
length, offset);
|
||||
if (res.Failed())
|
||||
return res;
|
||||
length = res.Unwrap();
|
||||
} else {
|
||||
LOG_TRACE(Service_FS, "ArticCache VBHIT: offset={}, length={}", offset, length);
|
||||
}
|
||||
memcpy(buffer, very_big_cache_entry.second.data(), length);
|
||||
} else {
|
||||
LOG_TRACE(Service_FS, "ArticCache SKIP: offset={}, length={}", offset, length);
|
||||
|
||||
auto res = ReadFromArtic(file_handle, buffer, length, offset);
|
||||
if (res.Failed())
|
||||
return res;
|
||||
length = res.Unwrap();
|
||||
}
|
||||
}
|
||||
return length;
|
||||
}
|
||||
|
||||
// TODO(PabloMK7): Make cache thread safe, read the comment in CacheReady function.
|
||||
std::unique_lock read_guard(cache_mutex);
|
||||
for (const auto& seg : segments) {
|
||||
std::size_t read_size = cache_line_size;
|
||||
std::size_t page = OffsetToPage(seg.first);
|
||||
// Check if segment is in cache
|
||||
auto cache_entry = cache.request(page);
|
||||
if (!cache_entry.first) {
|
||||
// If not found, read from artic and cache the data
|
||||
auto res = ReadFromArtic(file_handle, cache_entry.second.data(), read_size, page);
|
||||
if (res.Failed())
|
||||
return res;
|
||||
read_size = res.Unwrap();
|
||||
LOG_TRACE(Service_FS, "ArticCache MISS: page={}, length={}, into={}", page, seg.second,
|
||||
(seg.first - page));
|
||||
} else {
|
||||
LOG_TRACE(Service_FS, "ArticCache HIT: page={}, length={}, into={}", page, seg.second,
|
||||
(seg.first - page));
|
||||
}
|
||||
std::size_t copy_amount =
|
||||
(read_size > (seg.first - page))
|
||||
? std::min((seg.first - page) + seg.second, read_size) - (seg.first - page)
|
||||
: 0;
|
||||
std::memcpy(buffer + read_progress, cache_entry.second.data() + (seg.first - page),
|
||||
copy_amount);
|
||||
read_progress += copy_amount;
|
||||
}
|
||||
return read_progress;
|
||||
}
|
||||
|
||||
bool ArticCache::CacheReady(std::size_t file_offset, std::size_t length) {
|
||||
auto segments = BreakupRead(file_offset, length);
|
||||
if (segments.size() == 1 && segments[0].second > cache_line_size) {
|
||||
return false;
|
||||
} else {
|
||||
std::shared_lock read_guard(cache_mutex);
|
||||
for (auto it = segments.begin(); it != segments.end(); it++) {
|
||||
if (!cache.contains(OffsetToPage(it->first)))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
void ArticCache::Clear() {
|
||||
std::unique_lock l1(cache_mutex), l2(big_cache_mutex), l3(very_big_cache_mutex);
|
||||
cache.clear();
|
||||
big_cache.clear();
|
||||
very_big_cache.clear();
|
||||
data_size = std::nullopt;
|
||||
}
|
||||
|
||||
ResultVal<size_t> ArticCache::Write(s32 file_handle, std::size_t offset, std::size_t length,
|
||||
const u8* buffer, u32 flags) {
|
||||
// Can probably do better, but write operations are usually done at the end, so it doesn't
|
||||
// matter much
|
||||
Clear();
|
||||
|
||||
size_t written_amount = 0;
|
||||
while (written_amount != length) {
|
||||
size_t to_write =
|
||||
std::min<size_t>(client->GetServerRequestMaxSize() - 0x100, length - written_amount);
|
||||
|
||||
auto req = client->NewRequest("FSFILE_Write");
|
||||
req.AddParameterS32(file_handle);
|
||||
req.AddParameterS64(static_cast<s64>(offset + written_amount));
|
||||
req.AddParameterS32(static_cast<s32>(to_write));
|
||||
req.AddParameterS32(static_cast<s32>(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<u32>(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<size_t>(actually_written_opt.value());
|
||||
|
||||
written_amount += actually_written;
|
||||
if (actually_written != to_write)
|
||||
break;
|
||||
}
|
||||
return written_amount;
|
||||
}
|
||||
|
||||
ResultVal<size_t> ArticCache::GetSize(s32 file_handle) {
|
||||
std::unique_lock l1(cache_mutex);
|
||||
|
||||
if (data_size.has_value())
|
||||
return data_size.value();
|
||||
|
||||
auto req = client->NewRequest("FSFILE_GetSize");
|
||||
|
||||
req.AddParameterS32(file_handle);
|
||||
|
||||
auto resp = client->Send(req);
|
||||
if (!resp.has_value() || !resp->Succeeded())
|
||||
return Result(-1);
|
||||
|
||||
auto res = Result(static_cast<u32>(resp->GetMethodResult()));
|
||||
if (res.IsError())
|
||||
return res;
|
||||
|
||||
auto size_buf = resp->GetResponseS64(0);
|
||||
if (!size_buf) {
|
||||
return Result(-1);
|
||||
}
|
||||
|
||||
data_size = static_cast<size_t>(*size_buf);
|
||||
return data_size.value();
|
||||
}
|
||||
|
||||
ResultVal<size_t> ArticCache::ReadFromArtic(s32 file_handle, u8* buffer, size_t len,
|
||||
size_t offset) {
|
||||
size_t read_amount = 0;
|
||||
while (read_amount != len) {
|
||||
size_t to_read =
|
||||
std::min<size_t>(client->GetServerRequestMaxSize() - 0x100, len - read_amount);
|
||||
|
||||
auto req = client->NewRequest("FSFILE_Read");
|
||||
req.AddParameterS32(file_handle);
|
||||
req.AddParameterS64(static_cast<s64>(offset + read_amount));
|
||||
req.AddParameterS32(static_cast<s32>(to_read));
|
||||
auto resp = client->Send(req);
|
||||
if (!resp.has_value() || !resp->Succeeded())
|
||||
return Result(-1);
|
||||
|
||||
auto res = Result(static_cast<u32>(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;
|
||||
}
|
||||
|
||||
std::vector<std::pair<std::size_t, std::size_t>> ArticCache::BreakupRead(std::size_t offset,
|
||||
std::size_t length) {
|
||||
std::vector<std::pair<std::size_t, std::size_t>> ret;
|
||||
|
||||
// Reads bigger than the cache line size will probably never hit again
|
||||
if (length > max_breakup_size) {
|
||||
ret.push_back(std::make_pair(offset, length));
|
||||
return ret;
|
||||
}
|
||||
|
||||
std::size_t curr_offset = offset;
|
||||
while (length) {
|
||||
std::size_t next_page = OffsetToPage(curr_offset + cache_line_size);
|
||||
std::size_t curr_page_len = std::min(length, next_page - curr_offset);
|
||||
ret.push_back(std::make_pair(curr_offset, curr_page_len));
|
||||
curr_offset = next_page;
|
||||
length -= curr_page_len;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
} // namespace FileSys
|
154
src/core/file_sys/artic_cache.h
Normal file
154
src/core/file_sys/artic_cache.h
Normal file
|
@ -0,0 +1,154 @@
|
|||
// Copyright 2024 Citra Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <shared_mutex>
|
||||
#include "vector"
|
||||
|
||||
#include <boost/serialization/array.hpp>
|
||||
#include <boost/serialization/base_object.hpp>
|
||||
#include <boost/serialization/export.hpp>
|
||||
#include "common/alignment.h"
|
||||
#include "common/common_types.h"
|
||||
#include "common/static_lru_cache.h"
|
||||
#include "core/file_sys/archive_backend.h"
|
||||
#include "core/hle/result.h"
|
||||
#include "network/artic_base/artic_base_client.h"
|
||||
|
||||
namespace FileSys {
|
||||
class ArticCache {
|
||||
public:
|
||||
ArticCache() = default;
|
||||
|
||||
ArticCache(const std::shared_ptr<Network::ArticBase::Client>& cli) : client(cli) {}
|
||||
|
||||
ResultVal<std::size_t> Read(s32 file_handle, std::size_t offset, std::size_t length,
|
||||
u8* buffer);
|
||||
|
||||
bool CacheReady(std::size_t file_offset, std::size_t length);
|
||||
|
||||
void Clear();
|
||||
|
||||
ResultVal<std::size_t> Write(s32 file_handle, std::size_t offset, std::size_t length,
|
||||
const u8* buffer, u32 flags);
|
||||
|
||||
ResultVal<size_t> GetSize(s32 file_handle);
|
||||
|
||||
void ForceSetSize(const std::optional<size_t>& size) {
|
||||
data_size = size;
|
||||
}
|
||||
|
||||
private:
|
||||
std::shared_ptr<Network::ArticBase::Client> client;
|
||||
std::optional<size_t> data_size;
|
||||
|
||||
// Total cache size: 32MB small, 512MB big (worst case), 160MB very big (worst case).
|
||||
// The worst case values are unrealistic, they will never happen in any real game.
|
||||
static constexpr std::size_t cache_line_size = 4 * 1024;
|
||||
static constexpr std::size_t cache_line_count = 256;
|
||||
static constexpr std::size_t max_breakup_size = 8 * 1024;
|
||||
|
||||
static constexpr std::size_t big_cache_skip = 1 * 1024 * 1024;
|
||||
static constexpr std::size_t big_cache_lines = 1024;
|
||||
|
||||
static constexpr std::size_t very_big_cache_skip = 10 * 1024 * 1024;
|
||||
static constexpr std::size_t very_big_cache_lines = 24;
|
||||
|
||||
Common::StaticLRUCache<std::size_t, std::array<u8, cache_line_size>, cache_line_count> cache;
|
||||
std::shared_mutex cache_mutex;
|
||||
|
||||
struct NoInitChar {
|
||||
u8 value;
|
||||
NoInitChar() noexcept {
|
||||
// do nothing
|
||||
static_assert(sizeof *this == sizeof value, "invalid size");
|
||||
}
|
||||
};
|
||||
Common::StaticLRUCache<std::pair<std::size_t, std::size_t>, std::vector<NoInitChar>,
|
||||
big_cache_lines>
|
||||
big_cache;
|
||||
std::shared_mutex big_cache_mutex;
|
||||
Common::StaticLRUCache<std::pair<std::size_t, std::size_t>, std::vector<NoInitChar>,
|
||||
very_big_cache_lines>
|
||||
very_big_cache;
|
||||
std::shared_mutex very_big_cache_mutex;
|
||||
|
||||
ResultVal<std::size_t> ReadFromArtic(s32 file_handle, u8* buffer, size_t len, size_t offset);
|
||||
|
||||
std::size_t OffsetToPage(std::size_t offset) {
|
||||
return Common::AlignDown<std::size_t>(offset, cache_line_size);
|
||||
}
|
||||
|
||||
std::vector<std::pair<std::size_t, std::size_t>> BreakupRead(std::size_t offset,
|
||||
std::size_t length);
|
||||
|
||||
protected:
|
||||
template <class Archive>
|
||||
void serialize(Archive& ar, const unsigned int) {}
|
||||
friend class boost::serialization::access;
|
||||
};
|
||||
|
||||
class ArticCacheProvider {
|
||||
public:
|
||||
virtual ~ArticCacheProvider() {}
|
||||
|
||||
std::vector<u8> PathsToVector(const Path& archive_path, const Path& file_path) {
|
||||
auto archive_path_binary = archive_path.AsBinary();
|
||||
auto file_path_binary = file_path.AsBinary();
|
||||
|
||||
std::vector<u8> ret;
|
||||
ret.push_back(static_cast<u8>(file_path.GetType()));
|
||||
ret.insert(ret.end(), archive_path_binary.begin(), archive_path_binary.end());
|
||||
ret.push_back(static_cast<u8>(archive_path.GetType()));
|
||||
ret.insert(ret.end(), file_path_binary.begin(), file_path_binary.end());
|
||||
return ret;
|
||||
}
|
||||
|
||||
virtual std::shared_ptr<ArticCache> ProvideCache(
|
||||
const std::shared_ptr<Network::ArticBase::Client>& cli, const std::vector<u8>& path,
|
||||
bool create) {
|
||||
if (file_caches == nullptr)
|
||||
return nullptr;
|
||||
|
||||
auto it = file_caches->find(path);
|
||||
if (it == file_caches->end()) {
|
||||
if (!create) {
|
||||
return nullptr;
|
||||
}
|
||||
auto res = std::make_shared<ArticCache>(cli);
|
||||
file_caches->insert({path, res});
|
||||
return res;
|
||||
}
|
||||
return it->second;
|
||||
}
|
||||
|
||||
virtual void ClearAllCache() {
|
||||
if (file_caches != nullptr) {
|
||||
file_caches->clear();
|
||||
}
|
||||
}
|
||||
|
||||
virtual void EnsureCacheCreated() {
|
||||
if (file_caches == nullptr) {
|
||||
file_caches =
|
||||
std::make_unique<std::map<std::vector<u8>, std::shared_ptr<ArticCache>>>();
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
template <class Archive>
|
||||
void serialize(Archive& ar, const unsigned int) {}
|
||||
friend class boost::serialization::access;
|
||||
|
||||
private:
|
||||
std::unique_ptr<std::map<std::vector<u8>, std::shared_ptr<ArticCache>>> file_caches = nullptr;
|
||||
std::shared_ptr<Network::ArticBase::Client> client;
|
||||
};
|
||||
|
||||
} // namespace FileSys
|
||||
|
||||
BOOST_CLASS_EXPORT_KEY(FileSys::ArticCache)
|
||||
BOOST_CLASS_EXPORT_KEY(FileSys::ArticCacheProvider)
|
|
@ -49,7 +49,11 @@ public:
|
|||
* Close the directory
|
||||
* @return true if the directory closed correctly
|
||||
*/
|
||||
virtual bool Close() const = 0;
|
||||
virtual bool Close() = 0;
|
||||
|
||||
virtual bool IsSlow() {
|
||||
return false;
|
||||
}
|
||||
|
||||
private:
|
||||
template <class Archive>
|
||||
|
|
|
@ -26,7 +26,7 @@ ResultVal<std::size_t> DiskFile::Read(const u64 offset, const std::size_t length
|
|||
}
|
||||
|
||||
ResultVal<std::size_t> DiskFile::Write(const u64 offset, const std::size_t length, const bool flush,
|
||||
const u8* buffer) {
|
||||
const bool update_timestamp, const u8* buffer) {
|
||||
if (!mode.write_flag)
|
||||
return ResultInvalidOpenFlags;
|
||||
|
||||
|
@ -47,7 +47,7 @@ bool DiskFile::SetSize(const u64 size) const {
|
|||
return true;
|
||||
}
|
||||
|
||||
bool DiskFile::Close() const {
|
||||
bool DiskFile::Close() {
|
||||
return file->Close();
|
||||
}
|
||||
|
||||
|
|
|
@ -30,11 +30,11 @@ public:
|
|||
}
|
||||
|
||||
ResultVal<std::size_t> Read(u64 offset, std::size_t length, u8* buffer) const override;
|
||||
ResultVal<std::size_t> Write(u64 offset, std::size_t length, bool flush,
|
||||
ResultVal<std::size_t> 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;
|
||||
|
||||
void Flush() const override {
|
||||
file->Flush();
|
||||
|
@ -66,7 +66,7 @@ public:
|
|||
|
||||
u32 Read(u32 count, Entry* entries) override;
|
||||
|
||||
bool Close() const override {
|
||||
bool Close() override {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue