diff options
| author | 2024-01-07 07:33:31 -0500 | |
|---|---|---|
| committer | 2024-01-07 07:33:31 -0500 | |
| commit | 87430acff1c981e80c40f55fd76c312a37b87a3c (patch) | |
| tree | cb8b1058953a873c680e5b90fa1fbd63ef2b1faa | |
| parent | Merge pull request #12596 from german77/hid_qlaunch (diff) | |
| parent | android: Re-add global save manager (diff) | |
| download | yuzu-87430acff1c981e80c40f55fd76c312a37b87a3c.tar.gz yuzu-87430acff1c981e80c40f55fd76c312a37b87a3c.tar.xz yuzu-87430acff1c981e80c40f55fd76c312a37b87a3c.zip | |
Merge pull request #12576 from t895/total-save-manager
android: Re-add global save manager
6 files changed, 264 insertions, 0 deletions
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt index 010c44951..b7556e353 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt | |||
| @@ -548,6 +548,15 @@ object NativeLibrary { | |||
| 548 | external fun getSavePath(programId: String): String | 548 | external fun getSavePath(programId: String): String |
| 549 | 549 | ||
| 550 | /** | 550 | /** |
| 551 | * Gets the root save directory for the default profile as either | ||
| 552 | * /user/save/account/<user id raw string> or /user/save/000...000/<user id> | ||
| 553 | * | ||
| 554 | * @param future If true, returns the /user/save/account/... directory | ||
| 555 | * @return Save data path that may not exist yet | ||
| 556 | */ | ||
| 557 | external fun getDefaultProfileSaveDataRoot(future: Boolean): String | ||
| 558 | |||
| 559 | /** | ||
| 551 | * Adds a file to the manual filesystem provider in our EmulationSession instance | 560 | * Adds a file to the manual filesystem provider in our EmulationSession instance |
| 552 | * @param path Path to the file we're adding. Can be a string representation of a [Uri] or | 561 | * @param path Path to the file we're adding. Can be a string representation of a [Uri] or |
| 553 | * a normal path | 562 | * a normal path |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt index 569727b90..5b4bf2c9f 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt | |||
| @@ -7,20 +7,39 @@ import android.os.Bundle | |||
| 7 | import android.view.LayoutInflater | 7 | import android.view.LayoutInflater |
| 8 | import android.view.View | 8 | import android.view.View |
| 9 | import android.view.ViewGroup | 9 | import android.view.ViewGroup |
| 10 | import android.widget.Toast | ||
| 11 | import androidx.activity.result.contract.ActivityResultContracts | ||
| 10 | import androidx.core.view.ViewCompat | 12 | import androidx.core.view.ViewCompat |
| 11 | import androidx.core.view.WindowInsetsCompat | 13 | import androidx.core.view.WindowInsetsCompat |
| 12 | import androidx.core.view.updatePadding | 14 | import androidx.core.view.updatePadding |
| 13 | import androidx.fragment.app.Fragment | 15 | import androidx.fragment.app.Fragment |
| 14 | import androidx.fragment.app.activityViewModels | 16 | import androidx.fragment.app.activityViewModels |
| 17 | import androidx.lifecycle.Lifecycle | ||
| 18 | import androidx.lifecycle.lifecycleScope | ||
| 19 | import androidx.lifecycle.repeatOnLifecycle | ||
| 15 | import androidx.navigation.findNavController | 20 | import androidx.navigation.findNavController |
| 16 | import androidx.recyclerview.widget.GridLayoutManager | 21 | import androidx.recyclerview.widget.GridLayoutManager |
| 17 | import com.google.android.material.transition.MaterialSharedAxis | 22 | import com.google.android.material.transition.MaterialSharedAxis |
| 23 | import kotlinx.coroutines.Dispatchers | ||
| 24 | import kotlinx.coroutines.launch | ||
| 25 | import kotlinx.coroutines.withContext | ||
| 26 | import org.yuzu.yuzu_emu.NativeLibrary | ||
| 18 | import org.yuzu.yuzu_emu.R | 27 | import org.yuzu.yuzu_emu.R |
| 28 | import org.yuzu.yuzu_emu.YuzuApplication | ||
| 19 | import org.yuzu.yuzu_emu.adapters.InstallableAdapter | 29 | import org.yuzu.yuzu_emu.adapters.InstallableAdapter |
| 20 | import org.yuzu.yuzu_emu.databinding.FragmentInstallablesBinding | 30 | import org.yuzu.yuzu_emu.databinding.FragmentInstallablesBinding |
| 21 | import org.yuzu.yuzu_emu.model.HomeViewModel | 31 | import org.yuzu.yuzu_emu.model.HomeViewModel |
| 22 | import org.yuzu.yuzu_emu.model.Installable | 32 | import org.yuzu.yuzu_emu.model.Installable |
| 33 | import org.yuzu.yuzu_emu.model.TaskState | ||
| 23 | import org.yuzu.yuzu_emu.ui.main.MainActivity | 34 | import org.yuzu.yuzu_emu.ui.main.MainActivity |
| 35 | import org.yuzu.yuzu_emu.utils.DirectoryInitialization | ||
| 36 | import org.yuzu.yuzu_emu.utils.FileUtil | ||
| 37 | import java.io.BufferedInputStream | ||
| 38 | import java.io.BufferedOutputStream | ||
| 39 | import java.io.File | ||
| 40 | import java.math.BigInteger | ||
| 41 | import java.time.LocalDateTime | ||
| 42 | import java.time.format.DateTimeFormatter | ||
| 24 | 43 | ||
| 25 | class InstallableFragment : Fragment() { | 44 | class InstallableFragment : Fragment() { |
| 26 | private var _binding: FragmentInstallablesBinding? = null | 45 | private var _binding: FragmentInstallablesBinding? = null |
| @@ -56,6 +75,17 @@ class InstallableFragment : Fragment() { | |||
| 56 | binding.root.findNavController().popBackStack() | 75 | binding.root.findNavController().popBackStack() |
| 57 | } | 76 | } |
| 58 | 77 | ||
| 78 | viewLifecycleOwner.lifecycleScope.launch { | ||
| 79 | repeatOnLifecycle(Lifecycle.State.CREATED) { | ||
| 80 | homeViewModel.openImportSaves.collect { | ||
| 81 | if (it) { | ||
| 82 | importSaves.launch(arrayOf("application/zip")) | ||
| 83 | homeViewModel.setOpenImportSaves(false) | ||
| 84 | } | ||
| 85 | } | ||
| 86 | } | ||
| 87 | } | ||
| 88 | |||
| 59 | val installables = listOf( | 89 | val installables = listOf( |
| 60 | Installable( | 90 | Installable( |
| 61 | R.string.user_data, | 91 | R.string.user_data, |
| @@ -64,6 +94,43 @@ class InstallableFragment : Fragment() { | |||
| 64 | export = { mainActivity.exportUserData.launch("export.zip") } | 94 | export = { mainActivity.exportUserData.launch("export.zip") } |
| 65 | ), | 95 | ), |
| 66 | Installable( | 96 | Installable( |
| 97 | R.string.manage_save_data, | ||
| 98 | R.string.manage_save_data_description, | ||
| 99 | install = { | ||
| 100 | MessageDialogFragment.newInstance( | ||
| 101 | requireActivity(), | ||
| 102 | titleId = R.string.import_save_warning, | ||
| 103 | descriptionId = R.string.import_save_warning_description, | ||
| 104 | positiveAction = { homeViewModel.setOpenImportSaves(true) } | ||
| 105 | ).show(parentFragmentManager, MessageDialogFragment.TAG) | ||
| 106 | }, | ||
| 107 | export = { | ||
| 108 | val oldSaveDataFolder = File( | ||
| 109 | "${DirectoryInitialization.userDirectory}/nand" + | ||
| 110 | NativeLibrary.getDefaultProfileSaveDataRoot(false) | ||
| 111 | ) | ||
| 112 | val futureSaveDataFolder = File( | ||
| 113 | "${DirectoryInitialization.userDirectory}/nand" + | ||
| 114 | NativeLibrary.getDefaultProfileSaveDataRoot(true) | ||
| 115 | ) | ||
| 116 | if (!oldSaveDataFolder.exists() && !futureSaveDataFolder.exists()) { | ||
| 117 | Toast.makeText( | ||
| 118 | YuzuApplication.appContext, | ||
| 119 | R.string.no_save_data_found, | ||
| 120 | Toast.LENGTH_SHORT | ||
| 121 | ).show() | ||
| 122 | return@Installable | ||
| 123 | } else { | ||
| 124 | exportSaves.launch( | ||
| 125 | "${getString(R.string.save_data)} " + | ||
| 126 | LocalDateTime.now().format( | ||
| 127 | DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm") | ||
| 128 | ) | ||
| 129 | ) | ||
| 130 | } | ||
| 131 | } | ||
| 132 | ), | ||
| 133 | Installable( | ||
| 67 | R.string.install_game_content, | 134 | R.string.install_game_content, |
| 68 | R.string.install_game_content_description, | 135 | R.string.install_game_content_description, |
| 69 | install = { mainActivity.installGameUpdate.launch(arrayOf("*/*")) } | 136 | install = { mainActivity.installGameUpdate.launch(arrayOf("*/*")) } |
| @@ -121,4 +188,156 @@ class InstallableFragment : Fragment() { | |||
| 121 | 188 | ||
| 122 | windowInsets | 189 | windowInsets |
| 123 | } | 190 | } |
| 191 | |||
| 192 | private val importSaves = | ||
| 193 | registerForActivityResult(ActivityResultContracts.OpenDocument()) { result -> | ||
| 194 | if (result == null) { | ||
| 195 | return@registerForActivityResult | ||
| 196 | } | ||
| 197 | |||
| 198 | val inputZip = requireContext().contentResolver.openInputStream(result) | ||
| 199 | val cacheSaveDir = File("${requireContext().cacheDir.path}/saves/") | ||
| 200 | cacheSaveDir.mkdir() | ||
| 201 | |||
| 202 | if (inputZip == null) { | ||
| 203 | Toast.makeText( | ||
| 204 | YuzuApplication.appContext, | ||
| 205 | getString(R.string.fatal_error), | ||
| 206 | Toast.LENGTH_LONG | ||
| 207 | ).show() | ||
| 208 | return@registerForActivityResult | ||
| 209 | } | ||
| 210 | |||
| 211 | IndeterminateProgressDialogFragment.newInstance( | ||
| 212 | requireActivity(), | ||
| 213 | R.string.save_files_importing, | ||
| 214 | false | ||
| 215 | ) { | ||
| 216 | try { | ||
| 217 | FileUtil.unzipToInternalStorage(BufferedInputStream(inputZip), cacheSaveDir) | ||
| 218 | val files = cacheSaveDir.listFiles() | ||
| 219 | var successfulImports = 0 | ||
| 220 | var failedImports = 0 | ||
| 221 | if (files != null) { | ||
| 222 | for (file in files) { | ||
| 223 | if (file.isDirectory) { | ||
| 224 | val baseSaveDir = | ||
| 225 | NativeLibrary.getSavePath(BigInteger(file.name, 16).toString()) | ||
| 226 | if (baseSaveDir.isEmpty()) { | ||
| 227 | failedImports++ | ||
| 228 | continue | ||
| 229 | } | ||
| 230 | |||
| 231 | val internalSaveFolder = File( | ||
| 232 | "${DirectoryInitialization.userDirectory}/nand$baseSaveDir" | ||
| 233 | ) | ||
| 234 | internalSaveFolder.deleteRecursively() | ||
| 235 | internalSaveFolder.mkdir() | ||
| 236 | file.copyRecursively(target = internalSaveFolder, overwrite = true) | ||
| 237 | successfulImports++ | ||
| 238 | } | ||
| 239 | } | ||
| 240 | } | ||
| 241 | |||
| 242 | withContext(Dispatchers.Main) { | ||
| 243 | if (successfulImports == 0) { | ||
| 244 | MessageDialogFragment.newInstance( | ||
| 245 | requireActivity(), | ||
| 246 | titleId = R.string.save_file_invalid_zip_structure, | ||
| 247 | descriptionId = R.string.save_file_invalid_zip_structure_description | ||
| 248 | ).show(parentFragmentManager, MessageDialogFragment.TAG) | ||
| 249 | return@withContext | ||
| 250 | } | ||
| 251 | val successString = if (failedImports > 0) { | ||
| 252 | """ | ||
| 253 | ${ | ||
| 254 | requireContext().resources.getQuantityString( | ||
| 255 | R.plurals.saves_import_success, | ||
| 256 | successfulImports, | ||
| 257 | successfulImports | ||
| 258 | ) | ||
| 259 | } | ||
| 260 | ${ | ||
| 261 | requireContext().resources.getQuantityString( | ||
| 262 | R.plurals.saves_import_failed, | ||
| 263 | failedImports, | ||
| 264 | failedImports | ||
| 265 | ) | ||
| 266 | } | ||
| 267 | """ | ||
| 268 | } else { | ||
| 269 | requireContext().resources.getQuantityString( | ||
| 270 | R.plurals.saves_import_success, | ||
| 271 | successfulImports, | ||
| 272 | successfulImports | ||
| 273 | ) | ||
| 274 | } | ||
| 275 | MessageDialogFragment.newInstance( | ||
| 276 | requireActivity(), | ||
| 277 | titleId = R.string.import_complete, | ||
| 278 | descriptionString = successString | ||
| 279 | ).show(parentFragmentManager, MessageDialogFragment.TAG) | ||
| 280 | } | ||
| 281 | |||
| 282 | cacheSaveDir.deleteRecursively() | ||
| 283 | } catch (e: Exception) { | ||
| 284 | Toast.makeText( | ||
| 285 | YuzuApplication.appContext, | ||
| 286 | getString(R.string.fatal_error), | ||
| 287 | Toast.LENGTH_LONG | ||
| 288 | ).show() | ||
| 289 | } | ||
| 290 | }.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG) | ||
| 291 | } | ||
| 292 | |||
| 293 | private val exportSaves = registerForActivityResult( | ||
| 294 | ActivityResultContracts.CreateDocument("application/zip") | ||
| 295 | ) { result -> | ||
| 296 | if (result == null) { | ||
| 297 | return@registerForActivityResult | ||
| 298 | } | ||
| 299 | |||
| 300 | IndeterminateProgressDialogFragment.newInstance( | ||
| 301 | requireActivity(), | ||
| 302 | R.string.save_files_exporting, | ||
| 303 | false | ||
| 304 | ) { | ||
| 305 | val cacheSaveDir = File("${requireContext().cacheDir.path}/saves/") | ||
| 306 | cacheSaveDir.mkdir() | ||
| 307 | |||
| 308 | val oldSaveDataFolder = File( | ||
| 309 | "${DirectoryInitialization.userDirectory}/nand" + | ||
| 310 | NativeLibrary.getDefaultProfileSaveDataRoot(false) | ||
| 311 | ) | ||
| 312 | if (oldSaveDataFolder.exists()) { | ||
| 313 | oldSaveDataFolder.copyRecursively(cacheSaveDir) | ||
| 314 | } | ||
| 315 | |||
| 316 | val futureSaveDataFolder = File( | ||
| 317 | "${DirectoryInitialization.userDirectory}/nand" + | ||
| 318 | NativeLibrary.getDefaultProfileSaveDataRoot(true) | ||
| 319 | ) | ||
| 320 | if (futureSaveDataFolder.exists()) { | ||
| 321 | futureSaveDataFolder.copyRecursively(cacheSaveDir) | ||
| 322 | } | ||
| 323 | |||
| 324 | val saveFilesTotal = cacheSaveDir.listFiles()?.size ?: 0 | ||
| 325 | if (saveFilesTotal == 0) { | ||
| 326 | cacheSaveDir.deleteRecursively() | ||
| 327 | return@newInstance getString(R.string.no_save_data_found) | ||
| 328 | } | ||
| 329 | |||
| 330 | val zipResult = FileUtil.zipFromInternalStorage( | ||
| 331 | cacheSaveDir, | ||
| 332 | cacheSaveDir.path, | ||
| 333 | BufferedOutputStream(requireContext().contentResolver.openOutputStream(result)) | ||
| 334 | ) | ||
| 335 | cacheSaveDir.deleteRecursively() | ||
| 336 | |||
| 337 | return@newInstance when (zipResult) { | ||
| 338 | TaskState.Completed -> getString(R.string.export_success) | ||
| 339 | TaskState.Cancelled, TaskState.Failed -> getString(R.string.export_failed) | ||
| 340 | } | ||
| 341 | }.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG) | ||
| 342 | } | ||
| 124 | } | 343 | } |
diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp index 056920a4a..136c8dee6 100644 --- a/src/android/app/src/main/jni/native.cpp +++ b/src/android/app/src/main/jni/native.cpp | |||
| @@ -862,6 +862,9 @@ jobjectArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getAddonsForFile(JNIEnv* env, | |||
| 862 | jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getSavePath(JNIEnv* env, jobject jobj, | 862 | jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getSavePath(JNIEnv* env, jobject jobj, |
| 863 | jstring jprogramId) { | 863 | jstring jprogramId) { |
| 864 | auto program_id = EmulationSession::GetProgramId(env, jprogramId); | 864 | auto program_id = EmulationSession::GetProgramId(env, jprogramId); |
| 865 | if (program_id == 0) { | ||
| 866 | return ToJString(env, ""); | ||
| 867 | } | ||
| 865 | 868 | ||
| 866 | auto& system = EmulationSession::GetInstance().System(); | 869 | auto& system = EmulationSession::GetInstance().System(); |
| 867 | 870 | ||
| @@ -880,6 +883,19 @@ jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getSavePath(JNIEnv* env, jobject j | |||
| 880 | return ToJString(env, user_save_data_path); | 883 | return ToJString(env, user_save_data_path); |
| 881 | } | 884 | } |
| 882 | 885 | ||
| 886 | jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getDefaultProfileSaveDataRoot(JNIEnv* env, | ||
| 887 | jobject jobj, | ||
| 888 | jboolean jfuture) { | ||
| 889 | Service::Account::ProfileManager manager; | ||
| 890 | // TODO: Pass in a selected user once we get the relevant UI working | ||
| 891 | const auto user_id = manager.GetUser(static_cast<std::size_t>(0)); | ||
| 892 | ASSERT(user_id); | ||
| 893 | |||
| 894 | const auto user_save_data_root = | ||
| 895 | FileSys::SaveDataFactory::GetUserGameSaveDataRoot(user_id->AsU128(), jfuture); | ||
| 896 | return ToJString(env, user_save_data_root); | ||
| 897 | } | ||
| 898 | |||
| 883 | void Java_org_yuzu_yuzu_1emu_NativeLibrary_addFileToFilesystemProvider(JNIEnv* env, jobject jobj, | 899 | void Java_org_yuzu_yuzu_1emu_NativeLibrary_addFileToFilesystemProvider(JNIEnv* env, jobject jobj, |
| 884 | jstring jpath) { | 900 | jstring jpath) { |
| 885 | EmulationSession::GetInstance().ConfigureFilesystemProvider(GetJString(env, jpath)); | 901 | EmulationSession::GetInstance().ConfigureFilesystemProvider(GetJString(env, jpath)); |
diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index 83aa1b781..3bb92ad67 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml | |||
| @@ -133,6 +133,15 @@ | |||
| 133 | <string name="add_game_folder">Add game folder</string> | 133 | <string name="add_game_folder">Add game folder</string> |
| 134 | <string name="folder_already_added">This folder was already added!</string> | 134 | <string name="folder_already_added">This folder was already added!</string> |
| 135 | <string name="game_folder_properties">Game folder properties</string> | 135 | <string name="game_folder_properties">Game folder properties</string> |
| 136 | <plurals name="saves_import_failed"> | ||
| 137 | <item quantity="one">Failed to import %d save</item> | ||
| 138 | <item quantity="other">Failed to import %d saves</item> | ||
| 139 | </plurals> | ||
| 140 | <plurals name="saves_import_success"> | ||
| 141 | <item quantity="one">Successfully imported %d save</item> | ||
| 142 | <item quantity="other">Successfully imported %d saves</item> | ||
| 143 | </plurals> | ||
| 144 | <string name="no_save_data_found">No save data found</string> | ||
| 136 | 145 | ||
| 137 | <!-- Applet launcher strings --> | 146 | <!-- Applet launcher strings --> |
| 138 | <string name="applets">Applet launcher</string> | 147 | <string name="applets">Applet launcher</string> |
| @@ -276,6 +285,7 @@ | |||
| 276 | <string name="global">Global</string> | 285 | <string name="global">Global</string> |
| 277 | <string name="custom">Custom</string> | 286 | <string name="custom">Custom</string> |
| 278 | <string name="notice">Notice</string> | 287 | <string name="notice">Notice</string> |
| 288 | <string name="import_complete">Import complete</string> | ||
| 279 | 289 | ||
| 280 | <!-- GPU driver installation --> | 290 | <!-- GPU driver installation --> |
| 281 | <string name="select_gpu_driver">Select GPU driver</string> | 291 | <string name="select_gpu_driver">Select GPU driver</string> |
diff --git a/src/core/file_sys/savedata_factory.cpp b/src/core/file_sys/savedata_factory.cpp index 8d5d593e8..12b3bd797 100644 --- a/src/core/file_sys/savedata_factory.cpp +++ b/src/core/file_sys/savedata_factory.cpp | |||
| @@ -189,6 +189,15 @@ std::string SaveDataFactory::GetFullPath(Core::System& system, VirtualDir dir, | |||
| 189 | } | 189 | } |
| 190 | } | 190 | } |
| 191 | 191 | ||
| 192 | std::string SaveDataFactory::GetUserGameSaveDataRoot(u128 user_id, bool future) { | ||
| 193 | if (future) { | ||
| 194 | Common::UUID uuid; | ||
| 195 | std::memcpy(uuid.uuid.data(), user_id.data(), sizeof(Common::UUID)); | ||
| 196 | return fmt::format("/user/save/account/{}", uuid.RawString()); | ||
| 197 | } | ||
| 198 | return fmt::format("/user/save/{:016X}/{:016X}{:016X}", 0, user_id[1], user_id[0]); | ||
| 199 | } | ||
| 200 | |||
| 192 | SaveDataSize SaveDataFactory::ReadSaveDataSize(SaveDataType type, u64 title_id, | 201 | SaveDataSize SaveDataFactory::ReadSaveDataSize(SaveDataType type, u64 title_id, |
| 193 | u128 user_id) const { | 202 | u128 user_id) const { |
| 194 | const auto path = | 203 | const auto path = |
diff --git a/src/core/file_sys/savedata_factory.h b/src/core/file_sys/savedata_factory.h index e3a0f8cef..fd4887e99 100644 --- a/src/core/file_sys/savedata_factory.h +++ b/src/core/file_sys/savedata_factory.h | |||
| @@ -101,6 +101,7 @@ public: | |||
| 101 | static std::string GetSaveDataSpaceIdPath(SaveDataSpaceId space); | 101 | static std::string GetSaveDataSpaceIdPath(SaveDataSpaceId space); |
| 102 | static std::string GetFullPath(Core::System& system, VirtualDir dir, SaveDataSpaceId space, | 102 | static std::string GetFullPath(Core::System& system, VirtualDir dir, SaveDataSpaceId space, |
| 103 | SaveDataType type, u64 title_id, u128 user_id, u64 save_id); | 103 | SaveDataType type, u64 title_id, u128 user_id, u64 save_id); |
| 104 | static std::string GetUserGameSaveDataRoot(u128 user_id, bool future); | ||
| 104 | 105 | ||
| 105 | SaveDataSize ReadSaveDataSize(SaveDataType type, u64 title_id, u128 user_id) const; | 106 | SaveDataSize ReadSaveDataSize(SaveDataType type, u64 title_id, u128 user_id) const; |
| 106 | void WriteSaveDataSize(SaveDataType type, u64 title_id, u128 user_id, | 107 | void WriteSaveDataSize(SaveDataType type, u64 title_id, u128 user_id, |