forked from moonpower/azahar-UWP
Restore features
This commit is contained in:
parent
5ade69f5f4
commit
3e64dc0c8c
10
README.md
10
README.md
@ -1,3 +1,13 @@
|
|||||||
|
<b>AzaharPlus</b> is a fork of the Azahar 3DS emulator that restores some features.
|
||||||
|
|
||||||
|
Each version is the same as the corresponding version of Azahar exept for these features:
|
||||||
|
- Support of 3DS files. If a file works with earlier Citra forks, it works with AzaharPlus.
|
||||||
|
- Ability to download system files from official servers. No need for an actual 3DS.
|
||||||
|
|
||||||
|
Below is the readme from Azahar, unchanged.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||

|

|
||||||
|
|||||||
1
dist/apple/Info.plist.in
vendored
1
dist/apple/Info.plist.in
vendored
@ -31,6 +31,7 @@
|
|||||||
<dict>
|
<dict>
|
||||||
<key>CFBundleTypeExtensions</key>
|
<key>CFBundleTypeExtensions</key>
|
||||||
<array>
|
<array>
|
||||||
|
<string>3ds</string>
|
||||||
<string>3dsx</string>
|
<string>3dsx</string>
|
||||||
<string>cci</string>
|
<string>cci</string>
|
||||||
<string>cxi</string>
|
<string>cxi</string>
|
||||||
|
|||||||
1
dist/org.azahar_emu.Azahar.xml
vendored
1
dist/org.azahar_emu.Azahar.xml
vendored
@ -16,6 +16,7 @@
|
|||||||
<expanded-acronym>CTR Cart Image</expanded-acronym>
|
<expanded-acronym>CTR Cart Image</expanded-acronym>
|
||||||
<icon name="azahar"/>
|
<icon name="azahar"/>
|
||||||
<glob pattern="*.cci"/>
|
<glob pattern="*.cci"/>
|
||||||
|
<glob pattern="*.3ds"/>
|
||||||
<magic><match value="NCSD" type="string" offset="256"/></magic>
|
<magic><match value="NCSD" type="string" offset="256"/></magic>
|
||||||
</mime-type>
|
</mime-type>
|
||||||
|
|
||||||
|
|||||||
@ -63,6 +63,7 @@ android {
|
|||||||
// The application ID refers to Lime3DS to allow for
|
// The application ID refers to Lime3DS to allow for
|
||||||
// the Play Store listing, which was originally set up for Lime3DS, to still be used.
|
// the Play Store listing, which was originally set up for Lime3DS, to still be used.
|
||||||
applicationId = "io.github.lime3ds.android"
|
applicationId = "io.github.lime3ds.android"
|
||||||
|
|
||||||
minSdk = 28
|
minSdk = 28
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = autoVersion
|
versionCode = autoVersion
|
||||||
|
|||||||
@ -186,6 +186,8 @@ object NativeLibrary {
|
|||||||
|
|
||||||
external fun unlinkConsole()
|
external fun unlinkConsole()
|
||||||
|
|
||||||
|
external fun downloadTitleFromNus(title: Long): InstallStatus
|
||||||
|
|
||||||
private var coreErrorAlertResult = false
|
private var coreErrorAlertResult = false
|
||||||
private val coreErrorAlertLock = Object()
|
private val coreErrorAlertLock = Object()
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,152 @@
|
|||||||
|
// Copyright 2023 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
package org.citra.citra_emu.fragments
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
import kotlinx.coroutines.flow.collect
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.citra.citra_emu.NativeLibrary.InstallStatus
|
||||||
|
import org.citra.citra_emu.R
|
||||||
|
import org.citra.citra_emu.databinding.DialogProgressBarBinding
|
||||||
|
import org.citra.citra_emu.viewmodel.GamesViewModel
|
||||||
|
import org.citra.citra_emu.viewmodel.SystemFilesViewModel
|
||||||
|
|
||||||
|
class DownloadSystemFilesDialogFragment : DialogFragment() {
|
||||||
|
private var _binding: DialogProgressBarBinding? = null
|
||||||
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
|
private val downloadViewModel: SystemFilesViewModel by activityViewModels()
|
||||||
|
private val gamesViewModel: GamesViewModel by activityViewModels()
|
||||||
|
|
||||||
|
private lateinit var titles: LongArray
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
_binding = DialogProgressBarBinding.inflate(layoutInflater)
|
||||||
|
|
||||||
|
titles = requireArguments().getLongArray(TITLES)!!
|
||||||
|
|
||||||
|
binding.progressText.visibility = View.GONE
|
||||||
|
|
||||||
|
binding.progressBar.min = 0
|
||||||
|
binding.progressBar.max = titles.size
|
||||||
|
if (downloadViewModel.isDownloading.value != true) {
|
||||||
|
binding.progressBar.progress = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
isCancelable = false
|
||||||
|
return MaterialAlertDialogBuilder(requireContext())
|
||||||
|
.setView(binding.root)
|
||||||
|
.setTitle(R.string.downloading_files)
|
||||||
|
.setMessage(R.string.downloading_files_description)
|
||||||
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
.create()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
viewLifecycleOwner.lifecycleScope.apply {
|
||||||
|
launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.CREATED) {
|
||||||
|
downloadViewModel.progress.collectLatest { binding.progressBar.progress = it }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.CREATED) {
|
||||||
|
downloadViewModel.result.collect {
|
||||||
|
when (it) {
|
||||||
|
InstallStatus.Success -> {
|
||||||
|
downloadViewModel.clear()
|
||||||
|
dismiss()
|
||||||
|
MessageDialogFragment.newInstance(R.string.download_success, 0)
|
||||||
|
.show(requireActivity().supportFragmentManager, MessageDialogFragment.TAG)
|
||||||
|
gamesViewModel.setShouldSwapData(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
InstallStatus.ErrorFailedToOpenFile,
|
||||||
|
InstallStatus.ErrorEncrypted,
|
||||||
|
InstallStatus.ErrorFileNotFound,
|
||||||
|
InstallStatus.ErrorInvalid,
|
||||||
|
InstallStatus.ErrorAborted -> {
|
||||||
|
downloadViewModel.clear()
|
||||||
|
dismiss()
|
||||||
|
MessageDialogFragment.newInstance(
|
||||||
|
R.string.download_failed,
|
||||||
|
R.string.download_failed_description
|
||||||
|
).show(requireActivity().supportFragmentManager, MessageDialogFragment.TAG)
|
||||||
|
gamesViewModel.setShouldSwapData(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
InstallStatus.Cancelled -> {
|
||||||
|
downloadViewModel.clear()
|
||||||
|
dismiss()
|
||||||
|
MessageDialogFragment.newInstance(
|
||||||
|
R.string.download_cancelled,
|
||||||
|
R.string.download_cancelled_description
|
||||||
|
).show(requireActivity().supportFragmentManager, MessageDialogFragment.TAG)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do nothing on null
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Consider using WorkManager here. While the home menu can only really amount to
|
||||||
|
// about 150MBs, this could be a problem on inconsistent networks
|
||||||
|
downloadViewModel.download(titles)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
val alertDialog = dialog as AlertDialog
|
||||||
|
val negativeButton = alertDialog.getButton(Dialog.BUTTON_NEGATIVE)
|
||||||
|
negativeButton.setOnClickListener {
|
||||||
|
downloadViewModel.cancel()
|
||||||
|
dialog?.setTitle(R.string.cancelling)
|
||||||
|
binding.progressBar.isIndeterminate = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
super.onDestroyView()
|
||||||
|
_binding = null
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val TAG = "DownloadSystemFilesDialogFragment"
|
||||||
|
|
||||||
|
const val TITLES = "Titles"
|
||||||
|
|
||||||
|
fun newInstance(titles: LongArray): DownloadSystemFilesDialogFragment {
|
||||||
|
val dialog = DownloadSystemFilesDialogFragment()
|
||||||
|
val args = Bundle()
|
||||||
|
args.putLongArray(TITLES, titles)
|
||||||
|
dialog.arguments = args
|
||||||
|
return dialog
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -45,7 +45,7 @@ class GamesFragment : Fragment() {
|
|||||||
|
|
||||||
private val gamesViewModel: GamesViewModel by activityViewModels()
|
private val gamesViewModel: GamesViewModel by activityViewModels()
|
||||||
private val homeViewModel: HomeViewModel by activityViewModels()
|
private val homeViewModel: HomeViewModel by activityViewModels()
|
||||||
private var show3DSFileWarning: Boolean = true
|
private var show3DSFileWarning: Boolean = false
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
// Copyright Citra Emulator Project / Azahar Emulator Project
|
// Copyright 2023 Citra Emulator Project
|
||||||
// Licensed under GPLv2 or any later version
|
// Licensed under GPLv2 or any later version
|
||||||
// Refer to the license.txt file included.
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
@ -121,14 +121,8 @@ class HomeSettingsFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
),
|
),
|
||||||
HomeSetting(
|
HomeSetting(
|
||||||
R.string.install_game_content,
|
R.string.system_files,
|
||||||
R.string.install_game_content_description,
|
R.string.system_files_description,
|
||||||
R.drawable.ic_install,
|
|
||||||
{ mainActivity.ciaFileInstaller.launch(true) }
|
|
||||||
),
|
|
||||||
HomeSetting(
|
|
||||||
R.string.setup_system_files,
|
|
||||||
R.string.setup_system_files_description,
|
|
||||||
R.drawable.ic_system_update,
|
R.drawable.ic_system_update,
|
||||||
{
|
{
|
||||||
exitTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
|
exitTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
|
||||||
@ -136,6 +130,12 @@ class HomeSettingsFragment : Fragment() {
|
|||||||
?.navigate(R.id.action_homeSettingsFragment_to_systemFilesFragment)
|
?.navigate(R.id.action_homeSettingsFragment_to_systemFilesFragment)
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
HomeSetting(
|
||||||
|
R.string.install_game_content,
|
||||||
|
R.string.install_game_content_description,
|
||||||
|
R.drawable.ic_install,
|
||||||
|
{ mainActivity.ciaFileInstaller.launch(true) }
|
||||||
|
),
|
||||||
HomeSetting(
|
HomeSetting(
|
||||||
R.string.share_log,
|
R.string.share_log,
|
||||||
R.string.share_log_description,
|
R.string.share_log_description,
|
||||||
|
|||||||
@ -1,61 +1,76 @@
|
|||||||
// Copyright Citra Emulator Project / Azahar Emulator Project
|
// Copyright 2023 Citra Emulator Project
|
||||||
// Licensed under GPLv2 or any later version
|
// Licensed under GPLv2 or any later version
|
||||||
// Refer to the license.txt file included.
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
package org.citra.citra_emu.fragments
|
package org.citra.citra_emu.fragments
|
||||||
|
|
||||||
import android.content.DialogInterface
|
import android.content.res.Resources
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.text.Html
|
||||||
import android.text.method.LinkMovementMethod
|
import android.text.method.LinkMovementMethod
|
||||||
import android.view.Gravity
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.widget.AdapterView
|
||||||
import android.widget.ArrayAdapter
|
import android.widget.ArrayAdapter
|
||||||
import android.widget.FrameLayout
|
import androidx.core.view.ViewCompat
|
||||||
import android.widget.LinearLayout
|
import androidx.core.view.WindowInsetsCompat
|
||||||
import android.widget.RadioButton
|
import androidx.core.view.updatePadding
|
||||||
import android.widget.RadioGroup
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.core.text.HtmlCompat
|
|
||||||
import androidx.core.widget.doOnTextChanged
|
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import androidx.navigation.findNavController
|
import androidx.navigation.findNavController
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.textfield.MaterialAutoCompleteTextView
|
||||||
import com.google.android.material.progressindicator.CircularProgressIndicator
|
|
||||||
import com.google.android.material.textview.MaterialTextView
|
|
||||||
import com.google.android.material.transition.MaterialSharedAxis
|
import com.google.android.material.transition.MaterialSharedAxis
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import org.citra.citra_emu.CitraApplication
|
import org.citra.citra_emu.CitraApplication
|
||||||
import org.citra.citra_emu.HomeNavigationDirections
|
import org.citra.citra_emu.HomeNavigationDirections
|
||||||
import org.citra.citra_emu.NativeLibrary
|
import org.citra.citra_emu.NativeLibrary
|
||||||
import org.citra.citra_emu.R
|
import org.citra.citra_emu.R
|
||||||
import org.citra.citra_emu.databinding.DialogSoftwareKeyboardBinding
|
import org.citra.citra_emu.activities.EmulationActivity
|
||||||
import org.citra.citra_emu.databinding.FragmentSystemFilesBinding
|
import org.citra.citra_emu.databinding.FragmentSystemFilesBinding
|
||||||
import org.citra.citra_emu.features.settings.model.Settings
|
import org.citra.citra_emu.features.settings.model.Settings
|
||||||
import org.citra.citra_emu.model.Game
|
import org.citra.citra_emu.model.Game
|
||||||
import org.citra.citra_emu.utils.SystemSaveGame
|
import org.citra.citra_emu.utils.SystemSaveGame
|
||||||
import org.citra.citra_emu.viewmodel.GamesViewModel
|
import org.citra.citra_emu.viewmodel.GamesViewModel
|
||||||
import org.citra.citra_emu.viewmodel.HomeViewModel
|
import org.citra.citra_emu.viewmodel.HomeViewModel
|
||||||
|
import org.citra.citra_emu.viewmodel.SystemFilesViewModel
|
||||||
|
|
||||||
class SystemFilesFragment : Fragment() {
|
class SystemFilesFragment : Fragment() {
|
||||||
private var _binding: FragmentSystemFilesBinding? = null
|
private var _binding: FragmentSystemFilesBinding? = null
|
||||||
private val binding get() = _binding!!
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
private val homeViewModel: HomeViewModel by activityViewModels()
|
private val homeViewModel: HomeViewModel by activityViewModels()
|
||||||
|
private val systemFilesViewModel: SystemFilesViewModel by activityViewModels()
|
||||||
private val gamesViewModel: GamesViewModel by activityViewModels()
|
private val gamesViewModel: GamesViewModel by activityViewModels()
|
||||||
|
|
||||||
|
private lateinit var regionValues: IntArray
|
||||||
|
|
||||||
|
private val systemTypeDropdown = DropdownItem(R.array.systemFileTypeValues)
|
||||||
|
private val systemRegionDropdown = DropdownItem(R.array.systemFileRegionValues)
|
||||||
|
|
||||||
|
private val SYS_TYPE = "SysType"
|
||||||
|
private val REGION = "Region"
|
||||||
private val REGION_START = "RegionStart"
|
private val REGION_START = "RegionStart"
|
||||||
|
|
||||||
private val homeMenuMap: MutableMap<String, String> = mutableMapOf()
|
private val homeMenuMap: MutableMap<String, String> = mutableMapOf()
|
||||||
private var setupStateCached: BooleanArray? = null
|
|
||||||
private lateinit var regionValues: IntArray
|
private val WARNING_SHOWN = "SystemFilesWarningShown"
|
||||||
|
|
||||||
|
private class DropdownItem(val valuesId: Int) : AdapterView.OnItemClickListener {
|
||||||
|
var position = 0
|
||||||
|
|
||||||
|
fun getValue(resources: Resources): Int {
|
||||||
|
return resources.getIntArray(valuesId)[position]
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemClick(p0: AdapterView<*>?, view: View?, position: Int, id: Long) {
|
||||||
|
this.position = position
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@ -77,15 +92,61 @@ class SystemFilesFragment : Fragment() {
|
|||||||
homeViewModel.setNavigationVisibility(visible = false, animated = true)
|
homeViewModel.setNavigationVisibility(visible = false, animated = true)
|
||||||
homeViewModel.setStatusBarShadeVisibility(visible = false)
|
homeViewModel.setStatusBarShadeVisibility(visible = false)
|
||||||
|
|
||||||
|
val preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext)
|
||||||
|
if (!preferences.getBoolean(WARNING_SHOWN, false)) {
|
||||||
|
MessageDialogFragment.newInstance(
|
||||||
|
R.string.home_menu_warning,
|
||||||
|
R.string.home_menu_warning_description
|
||||||
|
).show(childFragmentManager, MessageDialogFragment.TAG)
|
||||||
|
preferences.edit()
|
||||||
|
.putBoolean(WARNING_SHOWN, true)
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.toolbarSystemFiles.setNavigationOnClickListener {
|
||||||
|
binding.root.findNavController().popBackStack()
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Remove workaround for text filtering issue in material components when fixed
|
// TODO: Remove workaround for text filtering issue in material components when fixed
|
||||||
// https://github.com/material-components/material-components-android/issues/1464
|
// https://github.com/material-components/material-components-android/issues/1464
|
||||||
|
binding.dropdownSystemType.isSaveEnabled = false
|
||||||
|
binding.dropdownSystemRegion.isSaveEnabled = false
|
||||||
binding.dropdownSystemRegionStart.isSaveEnabled = false
|
binding.dropdownSystemRegionStart.isSaveEnabled = false
|
||||||
|
|
||||||
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.CREATED) {
|
||||||
|
systemFilesViewModel.shouldRefresh.collect {
|
||||||
|
if (it) {
|
||||||
|
reloadUi()
|
||||||
|
systemFilesViewModel.setShouldRefresh(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
reloadUi()
|
reloadUi()
|
||||||
if (savedInstanceState != null) {
|
if (savedInstanceState != null) {
|
||||||
|
setDropdownSelection(
|
||||||
|
binding.dropdownSystemType,
|
||||||
|
systemTypeDropdown,
|
||||||
|
savedInstanceState.getInt(SYS_TYPE)
|
||||||
|
)
|
||||||
|
setDropdownSelection(
|
||||||
|
binding.dropdownSystemRegion,
|
||||||
|
systemRegionDropdown,
|
||||||
|
savedInstanceState.getInt(REGION)
|
||||||
|
)
|
||||||
binding.dropdownSystemRegionStart
|
binding.dropdownSystemRegionStart
|
||||||
.setText(savedInstanceState.getString(REGION_START), false)
|
.setText(savedInstanceState.getString(REGION_START), false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setInsets()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
outState.putInt(SYS_TYPE, systemTypeDropdown.position)
|
||||||
|
outState.putInt(REGION, systemRegionDropdown.position)
|
||||||
|
outState.putString(REGION_START, binding.dropdownSystemRegionStart.text.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
@ -98,41 +159,6 @@ class SystemFilesFragment : Fragment() {
|
|||||||
SystemSaveGame.save()
|
SystemSaveGame.save()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showProgressDialog(
|
|
||||||
main_title: CharSequence,
|
|
||||||
main_text: CharSequence
|
|
||||||
): AlertDialog? {
|
|
||||||
val context = requireContext()
|
|
||||||
val progressIndicator = CircularProgressIndicator(context).apply {
|
|
||||||
isIndeterminate = true
|
|
||||||
layoutParams = FrameLayout.LayoutParams(
|
|
||||||
FrameLayout.LayoutParams.WRAP_CONTENT,
|
|
||||||
FrameLayout.LayoutParams.WRAP_CONTENT,
|
|
||||||
Gravity.CENTER // Center the progress indicator
|
|
||||||
).apply {
|
|
||||||
setMargins(50, 50, 50, 50) // Add margins (left, top, right, bottom)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val pleaseWaitText = MaterialTextView(context).apply {
|
|
||||||
text = main_text
|
|
||||||
}
|
|
||||||
|
|
||||||
val container = LinearLayout(context).apply {
|
|
||||||
orientation = LinearLayout.VERTICAL
|
|
||||||
gravity = Gravity.CENTER
|
|
||||||
setPadding(40, 40, 40, 40) // Optional: Add padding to the entire layout
|
|
||||||
addView(pleaseWaitText)
|
|
||||||
addView(progressIndicator)
|
|
||||||
}
|
|
||||||
|
|
||||||
return MaterialAlertDialogBuilder(context)
|
|
||||||
.setTitle(main_title)
|
|
||||||
.setView(container)
|
|
||||||
.setCancelable(false)
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun reloadUi() {
|
private fun reloadUi() {
|
||||||
val preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext)
|
val preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext)
|
||||||
|
|
||||||
@ -150,167 +176,31 @@ class SystemFilesFragment : Fragment() {
|
|||||||
gamesViewModel.setShouldSwapData(true)
|
gamesViewModel.setShouldSwapData(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.setupSystemFilesDescription?.apply {
|
if (!NativeLibrary.areKeysAvailable()) {
|
||||||
text = HtmlCompat.fromHtml(
|
binding.apply {
|
||||||
context.getString(R.string.setup_system_files_preamble),
|
systemType.isEnabled = false
|
||||||
HtmlCompat.FROM_HTML_MODE_COMPACT
|
systemRegion.isEnabled = false
|
||||||
)
|
buttonDownloadHomeMenu.isEnabled = false
|
||||||
movementMethod = LinkMovementMethod.getInstance()
|
textKeysMissing.visibility = View.VISIBLE
|
||||||
}
|
textKeysMissingHelp.visibility = View.VISIBLE
|
||||||
|
textKeysMissingHelp.text =
|
||||||
binding.buttonUnlinkConsoleData.isEnabled = NativeLibrary.isFullConsoleLinked()
|
Html.fromHtml(getString(R.string.how_to_get_keys), Html.FROM_HTML_MODE_LEGACY)
|
||||||
binding.buttonUnlinkConsoleData.setOnClickListener {
|
textKeysMissingHelp.movementMethod = LinkMovementMethod.getInstance()
|
||||||
MaterialAlertDialogBuilder(requireContext())
|
|
||||||
.setTitle(R.string.delete_system_files)
|
|
||||||
.setMessage(HtmlCompat.fromHtml(
|
|
||||||
requireContext().getString(R.string.delete_system_files_description),
|
|
||||||
HtmlCompat.FROM_HTML_MODE_COMPACT
|
|
||||||
))
|
|
||||||
.setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
|
|
||||||
NativeLibrary.unlinkConsole()
|
|
||||||
binding.buttonUnlinkConsoleData.isEnabled = NativeLibrary.isFullConsoleLinked()
|
|
||||||
}
|
|
||||||
.setNegativeButton(android.R.string.cancel, null)
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.buttonSetUpSystemFiles.setOnClickListener {
|
|
||||||
val inflater = LayoutInflater.from(context)
|
|
||||||
val inputBinding = DialogSoftwareKeyboardBinding.inflate(inflater)
|
|
||||||
var textInputValue: String = preferences.getString("last_artic_base_addr", "")!!
|
|
||||||
|
|
||||||
val progressDialog = showProgressDialog(
|
|
||||||
getText(R.string.setup_system_files),
|
|
||||||
getString(R.string.setup_system_files_detect)
|
|
||||||
)
|
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
val setupState = setupStateCached ?: NativeLibrary.areSystemTitlesInstalled().also {
|
|
||||||
setupStateCached = it
|
|
||||||
}
|
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
progressDialog?.dismiss()
|
|
||||||
|
|
||||||
inputBinding.editTextInput.setText(textInputValue)
|
|
||||||
inputBinding.editTextInput.doOnTextChanged { text, _, _, _ ->
|
|
||||||
textInputValue = text.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
val buttonGroup = context?.let { it1 -> RadioGroup(it1) }!!
|
|
||||||
|
|
||||||
val buttonO3ds = context?.let { it1 ->
|
|
||||||
RadioButton(it1).apply {
|
|
||||||
text = context.getString(R.string.setup_system_files_o3ds)
|
|
||||||
isChecked = false
|
|
||||||
}
|
|
||||||
}!!
|
|
||||||
|
|
||||||
val buttonN3ds = context?.let { it1 ->
|
|
||||||
RadioButton(it1).apply {
|
|
||||||
text = context.getString(R.string.setup_system_files_n3ds)
|
|
||||||
isChecked = false
|
|
||||||
}
|
|
||||||
}!!
|
|
||||||
|
|
||||||
val textO3ds: String
|
|
||||||
val textN3ds: String
|
|
||||||
|
|
||||||
val colorO3ds: Int
|
|
||||||
val colorN3ds: Int
|
|
||||||
|
|
||||||
if (!setupStateCached!![0]) {
|
|
||||||
textO3ds = getString(R.string.setup_system_files_possible)
|
|
||||||
colorO3ds = R.color.citra_primary_blue
|
|
||||||
|
|
||||||
textN3ds = getString(R.string.setup_system_files_o3ds_needed)
|
|
||||||
colorN3ds = R.color.citra_primary_yellow
|
|
||||||
|
|
||||||
buttonN3ds.isEnabled = false
|
|
||||||
} else {
|
|
||||||
textO3ds = getString(R.string.setup_system_files_completed)
|
|
||||||
colorO3ds = R.color.citra_primary_green
|
|
||||||
|
|
||||||
if (!setupStateCached!![1]) {
|
|
||||||
textN3ds = getString(R.string.setup_system_files_possible)
|
|
||||||
colorN3ds = R.color.citra_primary_blue
|
|
||||||
} else {
|
|
||||||
textN3ds = getString(R.string.setup_system_files_completed)
|
|
||||||
colorN3ds = R.color.citra_primary_green
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val tooltipO3ds = context?.let { it1 ->
|
|
||||||
MaterialTextView(it1).apply {
|
|
||||||
text = textO3ds
|
|
||||||
textSize = 12f
|
|
||||||
setTextColor(ContextCompat.getColor(requireContext(), colorO3ds))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val tooltipN3ds = context?.let { it1 ->
|
|
||||||
MaterialTextView(it1).apply {
|
|
||||||
text = textN3ds
|
|
||||||
textSize = 12f
|
|
||||||
setTextColor(ContextCompat.getColor(requireContext(), colorN3ds))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
buttonGroup.apply {
|
|
||||||
addView(buttonO3ds)
|
|
||||||
addView(tooltipO3ds)
|
|
||||||
addView(buttonN3ds)
|
|
||||||
addView(tooltipN3ds)
|
|
||||||
}
|
|
||||||
|
|
||||||
inputBinding.root.apply {
|
|
||||||
addView(buttonGroup)
|
|
||||||
}
|
|
||||||
|
|
||||||
val dialog = context?.let {
|
|
||||||
MaterialAlertDialogBuilder(it)
|
|
||||||
.setView(inputBinding.root)
|
|
||||||
.setTitle(getString(R.string.setup_system_files_enter_address))
|
|
||||||
.setPositiveButton(android.R.string.ok) { diag, _ ->
|
|
||||||
if (textInputValue.isNotEmpty() && !(!buttonO3ds.isChecked && !buttonN3ds.isChecked)) {
|
|
||||||
preferences.edit()
|
|
||||||
.putString("last_artic_base_addr", textInputValue)
|
|
||||||
.apply()
|
|
||||||
val menu = Game(
|
|
||||||
title = getString(R.string.artic_base),
|
|
||||||
path = if (buttonO3ds.isChecked) {
|
|
||||||
"articinio://$textInputValue"
|
|
||||||
} else {
|
|
||||||
"articinin://$textInputValue"
|
|
||||||
},
|
|
||||||
filename = ""
|
|
||||||
)
|
|
||||||
val progressDialog2 = showProgressDialog(
|
|
||||||
getText(R.string.setup_system_files),
|
|
||||||
getString(
|
|
||||||
R.string.setup_system_files_preparing
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
NativeLibrary.uninstallSystemFiles(buttonO3ds.isChecked)
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
setupStateCached = null
|
|
||||||
progressDialog2?.dismiss()
|
|
||||||
val action =
|
|
||||||
HomeNavigationDirections.actionGlobalEmulationActivity(
|
|
||||||
menu
|
|
||||||
)
|
|
||||||
binding.root.findNavController().navigate(action)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
populateDownloadOptions()
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.buttonDownloadHomeMenu.setOnClickListener {
|
||||||
|
val titleIds = NativeLibrary.getSystemTitleIds(
|
||||||
|
systemTypeDropdown.getValue(resources),
|
||||||
|
systemRegionDropdown.getValue(resources)
|
||||||
|
)
|
||||||
|
|
||||||
|
DownloadSystemFilesDialogFragment.newInstance(titleIds).show(
|
||||||
|
childFragmentManager,
|
||||||
|
DownloadSystemFilesDialogFragment.TAG
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
populateHomeMenuOptions()
|
populateHomeMenuOptions()
|
||||||
@ -326,6 +216,51 @@ class SystemFilesFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun populateDropdown(
|
||||||
|
dropdown: MaterialAutoCompleteTextView,
|
||||||
|
valuesId: Int,
|
||||||
|
dropdownItem: DropdownItem
|
||||||
|
) {
|
||||||
|
val valuesAdapter = ArrayAdapter.createFromResource(
|
||||||
|
requireContext(),
|
||||||
|
valuesId,
|
||||||
|
R.layout.support_simple_spinner_dropdown_item
|
||||||
|
)
|
||||||
|
dropdown.setAdapter(valuesAdapter)
|
||||||
|
dropdown.onItemClickListener = dropdownItem
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setDropdownSelection(
|
||||||
|
dropdown: MaterialAutoCompleteTextView,
|
||||||
|
dropdownItem: DropdownItem,
|
||||||
|
selection: Int
|
||||||
|
) {
|
||||||
|
if (dropdown.adapter != null) {
|
||||||
|
dropdown.setText(dropdown.adapter.getItem(selection).toString(), false)
|
||||||
|
}
|
||||||
|
dropdownItem.position = selection
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun populateDownloadOptions() {
|
||||||
|
populateDropdown(binding.dropdownSystemType, R.array.systemFileTypes, systemTypeDropdown)
|
||||||
|
populateDropdown(
|
||||||
|
binding.dropdownSystemRegion,
|
||||||
|
R.array.systemFileRegions,
|
||||||
|
systemRegionDropdown
|
||||||
|
)
|
||||||
|
|
||||||
|
setDropdownSelection(
|
||||||
|
binding.dropdownSystemType,
|
||||||
|
systemTypeDropdown,
|
||||||
|
systemTypeDropdown.position
|
||||||
|
)
|
||||||
|
setDropdownSelection(
|
||||||
|
binding.dropdownSystemRegion,
|
||||||
|
systemRegionDropdown,
|
||||||
|
systemRegionDropdown.position
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private fun populateHomeMenuOptions() {
|
private fun populateHomeMenuOptions() {
|
||||||
regionValues = resources.getIntArray(R.array.systemFileRegionValues)
|
regionValues = resources.getIntArray(R.array.systemFileRegionValues)
|
||||||
val regionEntries = resources.getStringArray(R.array.systemFileRegions)
|
val regionEntries = resources.getStringArray(R.array.systemFileRegions)
|
||||||
@ -350,4 +285,30 @@ class SystemFilesFragment : Fragment() {
|
|||||||
binding.dropdownSystemRegionStart.setText(availableMenus.keys.first(), false)
|
binding.dropdownSystemRegionStart.setText(availableMenus.keys.first(), false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun setInsets() =
|
||||||
|
ViewCompat.setOnApplyWindowInsetsListener(
|
||||||
|
binding.root
|
||||||
|
) { _: View, windowInsets: WindowInsetsCompat ->
|
||||||
|
val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||||
|
val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
|
||||||
|
|
||||||
|
val leftInsets = barInsets.left + cutoutInsets.left
|
||||||
|
val rightInsets = barInsets.right + cutoutInsets.right
|
||||||
|
|
||||||
|
val mlpAppBar = binding.toolbarSystemFiles.layoutParams as ViewGroup.MarginLayoutParams
|
||||||
|
mlpAppBar.leftMargin = leftInsets
|
||||||
|
mlpAppBar.rightMargin = rightInsets
|
||||||
|
binding.toolbarSystemFiles.layoutParams = mlpAppBar
|
||||||
|
|
||||||
|
val mlpScrollSystemFiles =
|
||||||
|
binding.scrollSystemFiles.layoutParams as ViewGroup.MarginLayoutParams
|
||||||
|
mlpScrollSystemFiles.leftMargin = leftInsets
|
||||||
|
mlpScrollSystemFiles.rightMargin = rightInsets
|
||||||
|
binding.scrollSystemFiles.layoutParams = mlpScrollSystemFiles
|
||||||
|
|
||||||
|
binding.scrollSystemFiles.updatePadding(bottom = barInsets.bottom)
|
||||||
|
|
||||||
|
windowInsets
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -63,7 +63,7 @@ class Game(
|
|||||||
val allExtensions: Set<String> get() = extensions + badExtensions
|
val allExtensions: Set<String> get() = extensions + badExtensions
|
||||||
|
|
||||||
val extensions: Set<String> = HashSet(
|
val extensions: Set<String> = HashSet(
|
||||||
listOf("3dsx", "elf", "axf", "cci", "cxi", "app")
|
listOf("3ds", "3dsx", "elf", "axf", "cci", "cxi", "app")
|
||||||
)
|
)
|
||||||
|
|
||||||
val badExtensions: Set<String> = HashSet(
|
val badExtensions: Set<String> = HashSet(
|
||||||
|
|||||||
@ -0,0 +1,139 @@
|
|||||||
|
// Copyright 2023 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
package org.citra.citra_emu.viewmodel
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.cancelChildren
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.yield
|
||||||
|
import org.citra.citra_emu.NativeLibrary
|
||||||
|
import org.citra.citra_emu.NativeLibrary.InstallStatus
|
||||||
|
import org.citra.citra_emu.utils.Log
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
class SystemFilesViewModel : ViewModel() {
|
||||||
|
private var job: Job
|
||||||
|
private val coroutineContext: CoroutineContext
|
||||||
|
get() = Dispatchers.IO + job
|
||||||
|
|
||||||
|
val isDownloading get() = _isDownloading.asStateFlow()
|
||||||
|
private val _isDownloading = MutableStateFlow(false)
|
||||||
|
|
||||||
|
val progress get() = _progress.asStateFlow()
|
||||||
|
private val _progress = MutableStateFlow(0)
|
||||||
|
|
||||||
|
val result get() = _result.asStateFlow()
|
||||||
|
private val _result = MutableStateFlow<InstallStatus?>(null)
|
||||||
|
|
||||||
|
val shouldRefresh get() = _shouldRefresh.asStateFlow()
|
||||||
|
private val _shouldRefresh = MutableStateFlow(false)
|
||||||
|
|
||||||
|
private var cancelled = false
|
||||||
|
|
||||||
|
private val RETRY_AMOUNT = 3
|
||||||
|
|
||||||
|
init {
|
||||||
|
job = Job()
|
||||||
|
clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setShouldRefresh(refresh: Boolean) {
|
||||||
|
_shouldRefresh.value = refresh
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setProgress(progress: Int) {
|
||||||
|
_progress.value = progress
|
||||||
|
}
|
||||||
|
|
||||||
|
fun download(titles: LongArray) {
|
||||||
|
if (isDownloading.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
clear()
|
||||||
|
_isDownloading.value = true
|
||||||
|
Log.debug("System menu download started.")
|
||||||
|
|
||||||
|
val minExecutors = min(Runtime.getRuntime().availableProcessors(), titles.size)
|
||||||
|
val segment = (titles.size / minExecutors)
|
||||||
|
val atomicProgress = AtomicInteger(0)
|
||||||
|
for (i in 0 until minExecutors) {
|
||||||
|
val titlesSegment = if (i < minExecutors - 1) {
|
||||||
|
titles.copyOfRange(i * segment, (i + 1) * segment)
|
||||||
|
} else {
|
||||||
|
titles.copyOfRange(i * segment, titles.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
CoroutineScope(coroutineContext).launch {
|
||||||
|
titlesSegment.forEach { title: Long ->
|
||||||
|
// Notify UI of cancellation before ending coroutine
|
||||||
|
if (cancelled) {
|
||||||
|
_result.value = InstallStatus.ErrorAborted
|
||||||
|
cancelled = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Takes a moment to see if the coroutine was cancelled
|
||||||
|
yield()
|
||||||
|
|
||||||
|
// Retry downloading a title repeatedly
|
||||||
|
for (j in 0 until RETRY_AMOUNT) {
|
||||||
|
val result = tryDownloadTitle(title)
|
||||||
|
if (result == InstallStatus.Success) {
|
||||||
|
break
|
||||||
|
} else if (j == RETRY_AMOUNT - 1) {
|
||||||
|
_result.value = result
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
Log.warning("Download for title{$title} failed, retrying in 3s...")
|
||||||
|
delay(3000L)
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.debug("Successfully installed title - $title")
|
||||||
|
setProgress(atomicProgress.incrementAndGet())
|
||||||
|
|
||||||
|
Log.debug("System File Progress - ${atomicProgress.get()} / ${titles.size}")
|
||||||
|
if (atomicProgress.get() == titles.size) {
|
||||||
|
_result.value = InstallStatus.Success
|
||||||
|
setShouldRefresh(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun tryDownloadTitle(title: Long): InstallStatus {
|
||||||
|
val result = NativeLibrary.downloadTitleFromNus(title)
|
||||||
|
if (result != InstallStatus.Success) {
|
||||||
|
Log.error("Failed to install title $title with error - $result")
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clear() {
|
||||||
|
Log.debug("Clearing")
|
||||||
|
job.cancelChildren()
|
||||||
|
job = Job()
|
||||||
|
_progress.value = 0
|
||||||
|
_result.value = null
|
||||||
|
_isDownloading.value = false
|
||||||
|
cancelled = false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cancel() {
|
||||||
|
Log.debug("Canceling system file download.")
|
||||||
|
cancelled = true
|
||||||
|
job.cancelChildren()
|
||||||
|
job = Job()
|
||||||
|
_progress.value = 0
|
||||||
|
_result.value = InstallStatus.Cancelled
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -464,6 +464,17 @@ void Java_org_citra_citra_1emu_NativeLibrary_uninstallSystemFiles(JNIEnv* env,
|
|||||||
: Core::SystemTitleSet::New3ds);
|
: Core::SystemTitleSet::New3ds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
jobject Java_org_citra_citra_1emu_NativeLibrary_downloadTitleFromNus([[maybe_unused]] JNIEnv* env,
|
||||||
|
[[maybe_unused]] jobject obj,
|
||||||
|
jlong title) {
|
||||||
|
[[maybe_unused]] const auto title_id = static_cast<u64>(title);
|
||||||
|
Service::AM::InstallStatus status = Service::AM::InstallFromNus(title_id);
|
||||||
|
if (status != Service::AM::InstallStatus::Success) {
|
||||||
|
return IDCache::GetJavaCiaInstallStatus(status);
|
||||||
|
}
|
||||||
|
return IDCache::GetJavaCiaInstallStatus(Service::AM::InstallStatus::Success);
|
||||||
|
}
|
||||||
|
|
||||||
[[maybe_unused]] static bool CheckKgslPresent() {
|
[[maybe_unused]] static bool CheckKgslPresent() {
|
||||||
constexpr auto KgslPath{"/dev/kgsl-3d0"};
|
constexpr auto KgslPath{"/dev/kgsl-3d0"};
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,219 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/coordinator_about"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="?attr/colorSurface">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
|
android:id="@+id/appbar_about"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:fitsSystemWindows="true">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.MaterialToolbar
|
||||||
|
android:id="@+id/toolbar_system_files"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="?attr/actionBarSize"
|
||||||
|
app:navigationIcon="@drawable/ic_back"
|
||||||
|
app:title="@string/system_files" />
|
||||||
|
|
||||||
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
|
<androidx.core.widget.NestedScrollView
|
||||||
|
android:id="@+id/scroll_system_files"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:fadeScrollbars="false"
|
||||||
|
android:scrollbars="vertical"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="24dp"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:paddingBottom="16dp"
|
||||||
|
android:layout_weight="1">
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
style="@style/TextAppearance.Material3.TitleSmall"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:text="@string/download_system_files"
|
||||||
|
android:textAlignment="viewStart" />
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:id="@+id/system_type"
|
||||||
|
style="@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:hint="@string/system_type"
|
||||||
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.MaterialAutoCompleteTextView
|
||||||
|
android:id="@+id/dropdown_system_type"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="none" />
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:id="@+id/system_region"
|
||||||
|
style="@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:hint="@string/emulated_region"
|
||||||
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.MaterialAutoCompleteTextView
|
||||||
|
android:id="@+id/dropdown_system_region"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="none" />
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/button_download_home_menu"
|
||||||
|
style="@style/Widget.Material3.Button.UnelevatedButton"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:text="@string/download" />
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/text_keys_missing"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:text="@string/keys_missing"
|
||||||
|
android:textAlignment="viewStart"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/text_keys_missing_help"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAlignment="viewStart"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:text="How to get keys?" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="24dp"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:paddingBottom="16dp"
|
||||||
|
android:layout_weight="1">
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
style="@style/TextAppearance.Material3.TitleSmall"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:text="@string/boot_home_menu"
|
||||||
|
android:textAlignment="viewStart" />
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:id="@+id/system_region_start"
|
||||||
|
style="@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:hint="@string/emulated_region"
|
||||||
|
android:enabled="false"
|
||||||
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.MaterialAutoCompleteTextView
|
||||||
|
android:id="@+id/dropdown_system_region_start"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="none" />
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/button_start_home_menu"
|
||||||
|
style="@style/Widget.Material3.Button.UnelevatedButton"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:enabled="false"
|
||||||
|
android:text="@string/start" />
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp">
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/text_run_system_setup"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/run_system_setup"
|
||||||
|
android:textAlignment="viewStart"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/switch_run_system_setup"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<com.google.android.material.materialswitch.MaterialSwitch
|
||||||
|
android:id="@+id/switch_run_system_setup"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp">
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/text_show_apps"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/show_home_apps"
|
||||||
|
android:textAlignment="viewStart"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/switch_show_apps"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<com.google.android.material.materialswitch.MaterialSwitch
|
||||||
|
android:id="@+id/switch_show_apps"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</androidx.core.widget.NestedScrollView>
|
||||||
|
|
||||||
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
@ -18,7 +18,7 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="?attr/actionBarSize"
|
android:layout_height="?attr/actionBarSize"
|
||||||
app:navigationIcon="@drawable/ic_back"
|
app:navigationIcon="@drawable/ic_back"
|
||||||
app:title="@string/setup_system_files" />
|
app:title="@string/system_files" />
|
||||||
|
|
||||||
</com.google.android.material.appbar.AppBarLayout>
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
@ -39,48 +39,77 @@
|
|||||||
android:paddingBottom="16dp">
|
android:paddingBottom="16dp">
|
||||||
|
|
||||||
<com.google.android.material.textview.MaterialTextView
|
<com.google.android.material.textview.MaterialTextView
|
||||||
style="@style/TextAppearance.Material3.TitleMedium"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="16dp"
|
|
||||||
android:text="@string/setup_system_files"
|
|
||||||
android:textAlignment="viewStart" />
|
|
||||||
|
|
||||||
<com.google.android.material.textview.MaterialTextView
|
|
||||||
android:id="@+id/setupSystemFilesDescription"
|
|
||||||
style="@style/TextAppearance.Material3.TitleSmall"
|
style="@style/TextAppearance.Material3.TitleSmall"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="16dp"
|
android:layout_marginTop="16dp"
|
||||||
|
android:text="@string/download_system_files"
|
||||||
android:textAlignment="viewStart" />
|
android:textAlignment="viewStart" />
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:id="@+id/system_type"
|
||||||
|
style="@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:hint="@string/system_type"
|
||||||
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.MaterialAutoCompleteTextView
|
||||||
|
android:id="@+id/dropdown_system_type"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="none" />
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:id="@+id/system_region"
|
||||||
|
style="@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:hint="@string/emulated_region"
|
||||||
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.MaterialAutoCompleteTextView
|
||||||
|
android:id="@+id/dropdown_system_region"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="none" />
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/button_set_up_system_files"
|
android:id="@+id/button_download_home_menu"
|
||||||
style="@style/Widget.Material3.Button.UnelevatedButton"
|
style="@style/Widget.Material3.Button.UnelevatedButton"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@string/setup_tool_connect" />
|
android:layout_marginTop="16dp"
|
||||||
|
android:text="@string/download" />
|
||||||
<Button
|
|
||||||
android:id="@+id/button_unlink_console_data"
|
|
||||||
style="@style/Widget.Material3.Button.UnelevatedButton"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/delete_system_files" />
|
|
||||||
|
|
||||||
<View
|
|
||||||
android:id="@+id/divider2"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="1dp"
|
|
||||||
android:layout_marginTop="24dp"
|
|
||||||
android:background="?android:attr/listDivider"
|
|
||||||
android:padding="40px" />
|
|
||||||
|
|
||||||
<com.google.android.material.textview.MaterialTextView
|
<com.google.android.material.textview.MaterialTextView
|
||||||
style="@style/TextAppearance.Material3.TitleMedium"
|
android:id="@+id/text_keys_missing"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="24dp"
|
android:layout_marginTop="16dp"
|
||||||
|
android:text="@string/keys_missing"
|
||||||
|
android:textAlignment="viewStart"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/text_keys_missing_help"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAlignment="viewStart"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:text="How to get keys?" />
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
style="@style/TextAppearance.Material3.TitleSmall"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="48dp"
|
||||||
android:text="@string/boot_home_menu"
|
android:text="@string/boot_home_menu"
|
||||||
android:textAlignment="viewStart" />
|
android:textAlignment="viewStart" />
|
||||||
|
|
||||||
|
|||||||
@ -297,6 +297,17 @@
|
|||||||
<item>6</item>
|
<item>6</item>
|
||||||
</integer-array>
|
</integer-array>
|
||||||
|
|
||||||
|
<string-array name="systemFileTypes">
|
||||||
|
<item>@string/system_type_minimal</item>
|
||||||
|
<item>@string/system_type_old_3ds</item>
|
||||||
|
<item>@string/system_type_new_3ds</item>
|
||||||
|
</string-array>
|
||||||
|
<integer-array name="systemFileTypeValues">
|
||||||
|
<item>1</item>
|
||||||
|
<item>2</item>
|
||||||
|
<item>4</item>
|
||||||
|
</integer-array>
|
||||||
|
|
||||||
<string-array name="soundOutputModes">
|
<string-array name="soundOutputModes">
|
||||||
<item>@string/mono</item>
|
<item>@string/mono</item>
|
||||||
<item>@string/stereo</item>
|
<item>@string/stereo</item>
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<!-- General application strings -->
|
<!-- General application strings -->
|
||||||
<string name="app_name" translatable="false">Azahar</string>
|
<string name="app_name" translatable="false">AzaharPlus</string>
|
||||||
<string name="app_disclaimer">This software will run applications for the Nintendo 3DS handheld game console. No game titles are included.\n\nBefore you can begin with emulating, please select a folder to store Azahar\'s user data in.\n\nWhat\'s this:\n<a href='https://web.archive.org/web/20240304193549/https://github.com/citra-emu/citra/wiki/Citra-Android-user-data-and-storage'>Wiki - Citra Android user data and storage</a></string>
|
<string name="app_disclaimer">This software will run applications for the Nintendo 3DS handheld game console. No game titles are included.\n\nBefore you can begin with emulating, please select a folder to store Azahar\'s user data in.\n\nWhat\'s this:\n<a href='https://web.archive.org/web/20240304193549/https://github.com/citra-emu/citra/wiki/Citra-Android-user-data-and-storage'>Wiki - Citra Android user data and storage</a></string>
|
||||||
<string name="app_notification_channel_name" translatable="false">Azahar</string>
|
<string name="app_notification_channel_name" translatable="false">Azahar</string>
|
||||||
<string name="app_notification_channel_id" translatable="false">Azahar</string>
|
<string name="app_notification_channel_id" translatable="false">Azahar</string>
|
||||||
@ -148,12 +148,15 @@
|
|||||||
|
|
||||||
<!-- System files strings -->
|
<!-- System files strings -->
|
||||||
<string name="setup_system_files">System Files</string>
|
<string name="setup_system_files">System Files</string>
|
||||||
|
<string name="system_files">System Files</string>
|
||||||
<string name="setup_system_files_description">Perform system file operations such as installing system files or booting the Home Menu</string>
|
<string name="setup_system_files_description">Perform system file operations such as installing system files or booting the Home Menu</string>
|
||||||
|
<string name="system_files_description">Download system files to get Mii files, boot the HOME menu, and more</string>
|
||||||
<string name="setup_tool_connect">Connect to Artic Setup Tool</string>
|
<string name="setup_tool_connect">Connect to Artic Setup Tool</string>
|
||||||
<string name="setup_system_files_preamble"><![CDATA[Azahar needs console unique data and firmware files from a real console to be able to use some of its features. Such files and data can be set up with the <a href=https://github.com/azahar-emu/ArticSetupTool>Azahar Artic Setup Tool</a>.<br>Notes:<ul><li><b>This operation will install console unique data to Azahar, do not share your user or nand folders after performing the setup process!</b></li><li>While doing the setup process, Azahar will link to the console running the setup tool. You can unlink the console later from the System Files tab in the emulator options menu.</li><li>Do not go online with both Azahar and your 3DS console at the same time after setting up system files, as this could cause issues.</li><li>Old 3DS setup is needed for the New 3DS setup to work (setting up both is recommended).</li><li>Both setup modes will work regardless of the model of the console running the setup tool.</li></ul>]]></string>
|
<string name="setup_system_files_preamble"><![CDATA[Azahar needs console unique data and firmware files from a real console to be able to use some of its features. Such files and data can be set up with the <a href=https://github.com/azahar-emu/ArticSetupTool>Azahar Artic Setup Tool</a>.<br>Notes:<ul><li><b>This operation will install console unique data to Azahar, do not share your user or nand folders after performing the setup process!</b></li><li>While doing the setup process, Azahar will link to the console running the setup tool. You can unlink the console later from the System Files tab in the emulator options menu.</li><li>Do not go online with both Azahar and your 3DS console at the same time after setting up system files, as this could cause issues.</li><li>Old 3DS setup is needed for the New 3DS setup to work (setting up both is recommended).</li><li>Both setup modes will work regardless of the model of the console running the setup tool.</li></ul>]]></string>
|
||||||
<string name="setup_system_files_detect">Fetching current system files status, please wait...</string>
|
<string name="setup_system_files_detect">Fetching current system files status, please wait...</string>
|
||||||
<string name="delete_system_files">Unlink Console Unique Data</string>
|
<string name="delete_system_files">Unlink Console Unique Data</string>
|
||||||
<string name="delete_system_files_description"><![CDATA[This action will unlink your real console from Azahar, with the following consequences:<br><ul><li>Your OTP, SecureInfo and LocalFriendCodeSeed will be removed from Azahar.</li><li>Your friend list will reset and you will be logged out of your NNID/PNID account.</li><li>System files and eshop titles obtained through Azahar will become inaccessible until the same console is linked again using the setup tool (save data will not be lost).</li></ul><br>Continue?]]></string>
|
<string name="delete_system_files_description"><![CDATA[This action will unlink your real console from Azahar, with the following consequences:<br><ul><li>Your OTP, SecureInfo and LocalFriendCodeSeed will be removed from Azahar.</li><li>Your friend list will reset and you will be logged out of your NNID/PNID account.</li><li>System files and eshop titles obtained through Azahar will become inaccessible until the same console is linked again using the setup tool (save data will not be lost).</li></ul><br>Continue?]]></string>
|
||||||
|
<string name="download_system_files">Download System Files</string>
|
||||||
<string name="setup_system_files_o3ds">Old 3DS Setup</string>
|
<string name="setup_system_files_o3ds">Old 3DS Setup</string>
|
||||||
<string name="setup_system_files_n3ds">New 3DS Setup</string>
|
<string name="setup_system_files_n3ds">New 3DS Setup</string>
|
||||||
<string name="setup_system_files_possible">Setup is possible.</string>
|
<string name="setup_system_files_possible">Setup is possible.</string>
|
||||||
@ -162,9 +165,25 @@
|
|||||||
<string name="setup_system_files_enter_address">Enter Artic Setup Tool address</string>
|
<string name="setup_system_files_enter_address">Enter Artic Setup Tool address</string>
|
||||||
<string name="setup_system_files_preparing">Preparing setup, please wait...</string>
|
<string name="setup_system_files_preparing">Preparing setup, please wait...</string>
|
||||||
<string name="boot_home_menu">Boot the HOME Menu</string>
|
<string name="boot_home_menu">Boot the HOME Menu</string>
|
||||||
|
<string name="system_type">System Type</string>
|
||||||
|
<string name="download">Download</string>
|
||||||
|
<string name="keys_missing">Azahar is missing keys to download system files.</string>
|
||||||
|
<string name="how_to_get_keys"><![CDATA[<a href="https://web.archive.org/web/20240304203412/https://citra-emu.org/wiki/aes-keys/">How to get keys?</a>]]></string>
|
||||||
<string name="show_home_apps">Show HOME menu apps in Applications list</string>
|
<string name="show_home_apps">Show HOME menu apps in Applications list</string>
|
||||||
<string name="run_system_setup">Run System Setup when the HOME Menu is launched</string>
|
<string name="run_system_setup">Run System Setup when the HOME Menu is launched</string>
|
||||||
|
<string name="system_type_minimal">Minimal</string>
|
||||||
|
<string name="system_type_old_3ds">Old 3DS</string>
|
||||||
|
<string name="system_type_new_3ds">New 3DS</string>
|
||||||
|
<string name="downloading_files">Downloading Files…</string>
|
||||||
|
<string name="downloading_files_description">Please do not close the app.</string>
|
||||||
|
<string name="download_failed">Download Failed</string>
|
||||||
|
<string name="download_failed_description">Please make sure you are connected to the internet and try again.</string>
|
||||||
|
<string name="download_success">Download Complete!</string>
|
||||||
|
<string name="download_cancelled">Download Cancelled</string>
|
||||||
|
<string name="download_cancelled_description">Please restart the download to prevent issues with having incomplete system files.</string>
|
||||||
<string name="home_menu">HOME Menu</string>
|
<string name="home_menu">HOME Menu</string>
|
||||||
|
<string name="home_menu_warning">System Files Warning</string>
|
||||||
|
<string name="home_menu_warning_description">Due to how slow Android\'s storage access framework is for accessing Azahar\'s files, downloading multiple versions of system files can dramatically slow down loading for applications, save states, and the Applications list. Only download the files that you require to avoid any issues with loading speeds.</string>
|
||||||
|
|
||||||
<!-- Generic buttons (Shared with lots of stuff) -->
|
<!-- Generic buttons (Shared with lots of stuff) -->
|
||||||
<string name="generic_buttons">Buttons</string>
|
<string name="generic_buttons">Buttons</string>
|
||||||
|
|||||||
@ -1250,11 +1250,17 @@ bool GMainWindow::LoadROM(const QString& filename) {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case Core::System::ResultStatus::ErrorLoader_ErrorEncrypted: {
|
case Core::System::ResultStatus::ErrorLoader_ErrorEncrypted: {
|
||||||
QMessageBox::critical(this, tr("App Encrypted"),
|
QMessageBox::critical(
|
||||||
tr("Your app is encrypted. <br/>"
|
this, tr("ROM Encrypted"),
|
||||||
"<a "
|
tr("Your ROM is encrypted. <br/>Please follow the guides to redump your "
|
||||||
"href='https://azahar-emu.org/blog/game-loading-changes/'>"
|
"<a "
|
||||||
"Please check our blog for more info.</a>"));
|
"href='https://web.archive.org/web/20240304210021/https://citra-emu.org/wiki/"
|
||||||
|
"dumping-game-cartridges/'>game "
|
||||||
|
"cartridges</a> or "
|
||||||
|
"<a "
|
||||||
|
"href='https://web.archive.org/web/20240304210011/https://citra-emu.org/wiki/"
|
||||||
|
"dumping-installed-titles/'>installed "
|
||||||
|
"titles</a>."));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case Core::System::ResultStatus::ErrorLoader_ErrorInvalidFormat:
|
case Core::System::ResultStatus::ErrorLoader_ErrorInvalidFormat:
|
||||||
@ -2271,11 +2277,10 @@ void GMainWindow::OnCIAInstallReport(Service::AM::InstallStatus status, QString
|
|||||||
QMessageBox::critical(this, tr("Invalid File"), tr("%1 is not a valid CIA").arg(filename));
|
QMessageBox::critical(this, tr("Invalid File"), tr("%1 is not a valid CIA").arg(filename));
|
||||||
break;
|
break;
|
||||||
case Service::AM::InstallStatus::ErrorEncrypted:
|
case Service::AM::InstallStatus::ErrorEncrypted:
|
||||||
QMessageBox::critical(this, tr("CIA Encrypted"),
|
QMessageBox::critical(this, tr("Encrypted File"),
|
||||||
tr("Your CIA file is encrypted.<br/>"
|
tr("%1 must be decrypted "
|
||||||
"<a "
|
"before being used with Azahar. A real 3DS is required.")
|
||||||
"href='https://azahar-emu.org/blog/game-loading-changes/'>"
|
.arg(filename));
|
||||||
"Please check our blog for more info.</a>"));
|
|
||||||
break;
|
break;
|
||||||
case Service::AM::InstallStatus::ErrorFileNotFound:
|
case Service::AM::InstallStatus::ErrorFileNotFound:
|
||||||
QMessageBox::critical(this, tr("Unable to find File"),
|
QMessageBox::critical(this, tr("Unable to find File"),
|
||||||
@ -3421,8 +3426,8 @@ static bool IsSingleFileDropEvent(const QMimeData* mime) {
|
|||||||
return mime->hasUrls() && mime->urls().length() == 1;
|
return mime->hasUrls() && mime->urls().length() == 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
static const std::array<std::string, 8> AcceptedExtensions = {"cci", "cxi", "bin", "3dsx",
|
static const std::array<std::string, 8> AcceptedExtensions = {"cci", "3ds", "cxi", "bin",
|
||||||
"app", "elf", "axf"};
|
"3dsx", "app", "elf", "axf"};
|
||||||
|
|
||||||
static bool IsCorrectFileExtension(const QMimeData* mime) {
|
static bool IsCorrectFileExtension(const QMimeData* mime) {
|
||||||
const QString& filename = mime->urls().at(0).toLocalFile();
|
const QString& filename = mime->urls().at(0).toLocalFile();
|
||||||
|
|||||||
@ -217,16 +217,6 @@
|
|||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="2" column="0">
|
<item row="2" column="0">
|
||||||
<widget class="QLabel" name="label_cpu_clock_info">
|
|
||||||
<property name="text">
|
|
||||||
<string><html><head/><body>Underclocking can increase performance but may cause the application to freeze.<br/>Overclocking may reduce lag in applications but also might cause freezes</p></body></html></string>
|
|
||||||
</property>
|
|
||||||
<property name="textFormat">
|
|
||||||
<enum>Qt::RichText</enum>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="3" column="0">
|
|
||||||
<widget class="QCheckBox" name="toggle_cpu_jit">
|
<widget class="QCheckBox" name="toggle_cpu_jit">
|
||||||
<property name="toolTip">
|
<property name="toolTip">
|
||||||
<string><html><head/><body><p>Enables the use of the ARM JIT compiler for emulating the 3DS CPUs. Don't disable unless for debugging purposes</p></body></html></string>
|
<string><html><head/><body><p>Enables the use of the ARM JIT compiler for emulating the 3DS CPUs. Don't disable unless for debugging purposes</p></body></html></string>
|
||||||
@ -236,14 +226,14 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="4" column="0">
|
<item row="3" column="0">
|
||||||
<widget class="QCheckBox" name="toggle_renderer_debug">
|
<widget class="QCheckBox" name="toggle_renderer_debug">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Enable debug renderer</string>
|
<string>Enable debug renderer</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="5" column="0">
|
<item row="4" column="0">
|
||||||
<widget class="QCheckBox" name="toggle_dump_command_buffers">
|
<widget class="QCheckBox" name="toggle_dump_command_buffers">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Dump command buffers</string>
|
<string>Dump command buffers</string>
|
||||||
@ -253,33 +243,43 @@
|
|||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QGroupBox" name="groupBox_5">
|
||||||
|
<property name="title">
|
||||||
|
<string>Miscellaneous</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QGridLayout" name="gridLayout_3">
|
||||||
|
<item row="1" column="0">
|
||||||
|
<widget class="QCheckBox" name="delay_start_for_lle_modules">
|
||||||
|
<property name="toolTip">
|
||||||
|
<string><html><head/><body><p>Introduces a delay to the first ever launched app thread if LLE modules are enabled, to allow them to initialize.</p></body></html></string>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Delay app start for LLE module initialization</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="0">
|
||||||
|
<widget class="QCheckBox" name="deterministic_async_operations">
|
||||||
|
<property name="text">
|
||||||
|
<string>Force deterministic async operations</string>
|
||||||
|
</property>
|
||||||
|
<property name="toolTip">
|
||||||
|
<string><html><head/><body><p>Forces all async operations to run on the main thread, making them deterministic. Do not enable if you don't know what you are doing.</p></body></html></string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QGroupBox" name="groupBox_5">
|
<widget class="QLabel" name="label_cpu_clock_info">
|
||||||
<property name="title">
|
<property name="text">
|
||||||
<string>Miscellaneous</string>
|
<string><html><head/><body><p>CPU Clock Speed Information<br/>Underclocking can increase performance but may cause the application to freeze.<br/>Overclocking may reduce lag in applications but also might cause freezes</p></body></html></string>
|
||||||
|
</property>
|
||||||
|
<property name="textFormat">
|
||||||
|
<enum>Qt::RichText</enum>
|
||||||
</property>
|
</property>
|
||||||
<layout class="QGridLayout" name="gridLayout_3">
|
|
||||||
<item row="1" column="0">
|
|
||||||
<widget class="QCheckBox" name="delay_start_for_lle_modules">
|
|
||||||
<property name="toolTip">
|
|
||||||
<string><html><head/><body><p>Introduces a delay to the first ever launched app thread if LLE modules are enabled, to allow them to initialize.</p></body></html></string>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>Delay app start for LLE module initialization</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="2" column="0">
|
|
||||||
<widget class="QCheckBox" name="deterministic_async_operations">
|
|
||||||
<property name="text">
|
|
||||||
<string>Force deterministic async operations</string>
|
|
||||||
</property>
|
|
||||||
<property name="toolTip">
|
|
||||||
<string><html><head/><body><p>Forces all async operations to run on the main thread, making them deterministic. Do not enable if you don't know what you are doing.</p></body></html></string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
|
|||||||
@ -238,16 +238,8 @@ ConfigureSystem::ConfigureSystem(Core::System& system_, QWidget* parent)
|
|||||||
connect(ui->button_regenerate_console_id, &QPushButton::clicked, this,
|
connect(ui->button_regenerate_console_id, &QPushButton::clicked, this,
|
||||||
&ConfigureSystem::RefreshConsoleID);
|
&ConfigureSystem::RefreshConsoleID);
|
||||||
connect(ui->button_regenerate_mac, &QPushButton::clicked, this, &ConfigureSystem::RefreshMAC);
|
connect(ui->button_regenerate_mac, &QPushButton::clicked, this, &ConfigureSystem::RefreshMAC);
|
||||||
connect(ui->button_linked_console, &QPushButton::clicked, this,
|
connect(ui->button_start_download, &QPushButton::clicked, this,
|
||||||
&ConfigureSystem::UnlinkConsole);
|
&ConfigureSystem::DownloadFromNUS);
|
||||||
connect(ui->combo_country, qOverload<int>(&QComboBox::currentIndexChanged), this,
|
|
||||||
[this](int index) {
|
|
||||||
CheckCountryValid(static_cast<u8>(ui->combo_country->itemData(index).toInt()));
|
|
||||||
});
|
|
||||||
connect(ui->region_combobox, qOverload<int>(&QComboBox::currentIndexChanged), this,
|
|
||||||
[this]([[maybe_unused]] int index) {
|
|
||||||
CheckCountryValid(static_cast<u8>(ui->combo_country->currentData().toInt()));
|
|
||||||
});
|
|
||||||
|
|
||||||
connect(ui->button_secure_info, &QPushButton::clicked, this, [this] {
|
connect(ui->button_secure_info, &QPushButton::clicked, this, [this] {
|
||||||
ui->button_secure_info->setEnabled(false);
|
ui->button_secure_info->setEnabled(false);
|
||||||
@ -255,7 +247,11 @@ ConfigureSystem::ConfigureSystem(Core::System& system_, QWidget* parent)
|
|||||||
this, tr("Select SecureInfo_A/B"), QString(),
|
this, tr("Select SecureInfo_A/B"), QString(),
|
||||||
tr("SecureInfo_A/B (SecureInfo_A SecureInfo_B);;All Files (*.*)"));
|
tr("SecureInfo_A/B (SecureInfo_A SecureInfo_B);;All Files (*.*)"));
|
||||||
ui->button_secure_info->setEnabled(true);
|
ui->button_secure_info->setEnabled(true);
|
||||||
|
#ifdef todotodo
|
||||||
InstallSecureData(file_path_qtstr.toStdString(), HW::UniqueData::GetSecureInfoAPath());
|
InstallSecureData(file_path_qtstr.toStdString(), HW::UniqueData::GetSecureInfoAPath());
|
||||||
|
#else
|
||||||
|
InstallSecureData(file_path_qtstr.toStdString(), cfg->GetSecureInfoAPath());
|
||||||
|
#endif
|
||||||
});
|
});
|
||||||
connect(ui->button_friend_code_seed, &QPushButton::clicked, this, [this] {
|
connect(ui->button_friend_code_seed, &QPushButton::clicked, this, [this] {
|
||||||
ui->button_friend_code_seed->setEnabled(false);
|
ui->button_friend_code_seed->setEnabled(false);
|
||||||
@ -264,6 +260,7 @@ ConfigureSystem::ConfigureSystem(Core::System& system_, QWidget* parent)
|
|||||||
tr("LocalFriendCodeSeed_A/B (LocalFriendCodeSeed_A "
|
tr("LocalFriendCodeSeed_A/B (LocalFriendCodeSeed_A "
|
||||||
"LocalFriendCodeSeed_B);;All Files (*.*)"));
|
"LocalFriendCodeSeed_B);;All Files (*.*)"));
|
||||||
ui->button_friend_code_seed->setEnabled(true);
|
ui->button_friend_code_seed->setEnabled(true);
|
||||||
|
#ifdef todotodo
|
||||||
InstallSecureData(file_path_qtstr.toStdString(),
|
InstallSecureData(file_path_qtstr.toStdString(),
|
||||||
HW::UniqueData::GetLocalFriendCodeSeedBPath());
|
HW::UniqueData::GetLocalFriendCodeSeedBPath());
|
||||||
});
|
});
|
||||||
@ -281,6 +278,16 @@ ConfigureSystem::ConfigureSystem(Core::System& system_, QWidget* parent)
|
|||||||
this, tr("Select movable.sed"), QString(), tr("Sed file (*.sed);;All Files (*.*)"));
|
this, tr("Select movable.sed"), QString(), tr("Sed file (*.sed);;All Files (*.*)"));
|
||||||
ui->button_movable->setEnabled(true);
|
ui->button_movable->setEnabled(true);
|
||||||
InstallSecureData(file_path_qtstr.toStdString(), HW::UniqueData::GetMovablePath());
|
InstallSecureData(file_path_qtstr.toStdString(), HW::UniqueData::GetMovablePath());
|
||||||
|
#else
|
||||||
|
InstallSecureData(file_path_qtstr.toStdString(), cfg->GetLocalFriendCodeSeedBPath());
|
||||||
|
});
|
||||||
|
connect(ui->button_ct_cert, &QPushButton::clicked, this, [this] {
|
||||||
|
ui->button_ct_cert->setEnabled(false);
|
||||||
|
const QString file_path_qtstr = QFileDialog::getOpenFileName(
|
||||||
|
this, tr("Select CTCert"), QString(), tr("CTCert.bin (*.bin);;All Files (*.*)"));
|
||||||
|
ui->button_ct_cert->setEnabled(true);
|
||||||
|
InstallCTCert(file_path_qtstr.toStdString());
|
||||||
|
#endif
|
||||||
});
|
});
|
||||||
|
|
||||||
for (u8 i = 0; i < country_names.size(); i++) {
|
for (u8 i = 0; i < country_names.size(); i++) {
|
||||||
@ -288,10 +295,36 @@ ConfigureSystem::ConfigureSystem(Core::System& system_, QWidget* parent)
|
|||||||
ui->combo_country->addItem(tr(country_names.at(i)), i);
|
ui->combo_country->addItem(tr(country_names.at(i)), i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ui->label_country_invalid->setVisible(false);
|
|
||||||
ui->label_country_invalid->setStyleSheet(QStringLiteral("QLabel { color: #ff3333; }"));
|
|
||||||
|
|
||||||
SetupPerGameUI();
|
SetupPerGameUI();
|
||||||
|
|
||||||
|
ui->combo_download_set->setCurrentIndex(0); // set to Minimal
|
||||||
|
ui->combo_download_region->setCurrentIndex(0); // set to the base region
|
||||||
|
|
||||||
|
HW::AES::InitKeys(true);
|
||||||
|
bool keys_available = HW::AES::IsKeyXAvailable(HW::AES::KeySlotID::NCCHSecure1) &&
|
||||||
|
HW::AES::IsKeyXAvailable(HW::AES::KeySlotID::NCCHSecure2);
|
||||||
|
for (u8 i = 0; i < HW::AES::MaxCommonKeySlot && keys_available; i++) {
|
||||||
|
HW::AES::SelectCommonKeyIndex(i);
|
||||||
|
if (!HW::AES::IsNormalKeyAvailable(HW::AES::KeySlotID::TicketCommonKey)) {
|
||||||
|
keys_available = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (keys_available) {
|
||||||
|
ui->button_start_download->setEnabled(true);
|
||||||
|
ui->combo_download_set->setEnabled(true);
|
||||||
|
ui->combo_download_region->setEnabled(true);
|
||||||
|
ui->label_nus_download->setText(tr("Download System Files from Nintendo servers"));
|
||||||
|
} else {
|
||||||
|
ui->button_start_download->setEnabled(false);
|
||||||
|
ui->combo_download_set->setEnabled(false);
|
||||||
|
ui->combo_download_region->setEnabled(false);
|
||||||
|
ui->label_nus_download->setTextInteractionFlags(Qt::TextBrowserInteraction);
|
||||||
|
ui->label_nus_download->setOpenExternalLinks(true);
|
||||||
|
ui->label_nus_download->setText(tr("Azahar is missing keys to download system files."));
|
||||||
|
}
|
||||||
|
|
||||||
ConfigureTime();
|
ConfigureTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -300,19 +333,6 @@ ConfigureSystem::~ConfigureSystem() = default;
|
|||||||
void ConfigureSystem::SetConfiguration() {
|
void ConfigureSystem::SetConfiguration() {
|
||||||
enabled = !system.IsPoweredOn();
|
enabled = !system.IsPoweredOn();
|
||||||
|
|
||||||
if (!Settings::IsConfiguringGlobal()) {
|
|
||||||
ConfigurationShared::SetHighlight(ui->region_label,
|
|
||||||
!Settings::values.region_value.UsingGlobal());
|
|
||||||
const bool is_region_global = Settings::values.region_value.UsingGlobal();
|
|
||||||
ui->region_combobox->setCurrentIndex(
|
|
||||||
is_region_global ? ConfigurationShared::USE_GLOBAL_INDEX
|
|
||||||
: static_cast<int>(Settings::values.region_value.GetValue()) +
|
|
||||||
ConfigurationShared::USE_GLOBAL_OFFSET + 1);
|
|
||||||
} else {
|
|
||||||
// The first item is "auto-select" with actual value -1, so plus one here will do the trick
|
|
||||||
ui->region_combobox->setCurrentIndex(Settings::values.region_value.GetValue() + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
ui->combo_init_clock->setCurrentIndex(static_cast<u8>(Settings::values.init_clock.GetValue()));
|
ui->combo_init_clock->setCurrentIndex(static_cast<u8>(Settings::values.init_clock.GetValue()));
|
||||||
QDateTime date_time;
|
QDateTime date_time;
|
||||||
date_time.setSecsSinceEpoch(Settings::values.init_time.GetValue());
|
date_time.setSecsSinceEpoch(Settings::values.init_time.GetValue());
|
||||||
@ -374,7 +394,6 @@ void ConfigureSystem::ReadSystemSettings() {
|
|||||||
// set the country code
|
// set the country code
|
||||||
country_code = cfg->GetCountryCode();
|
country_code = cfg->GetCountryCode();
|
||||||
ui->combo_country->setCurrentIndex(ui->combo_country->findData(country_code));
|
ui->combo_country->setCurrentIndex(ui->combo_country->findData(country_code));
|
||||||
CheckCountryValid(country_code);
|
|
||||||
|
|
||||||
// set whether system setup is needed
|
// set whether system setup is needed
|
||||||
system_setup = cfg->IsSystemSetupNeeded();
|
system_setup = cfg->IsSystemSetupNeeded();
|
||||||
@ -391,16 +410,15 @@ void ConfigureSystem::ReadSystemSettings() {
|
|||||||
play_coin = Service::PTM::Module::GetPlayCoins();
|
play_coin = Service::PTM::Module::GetPlayCoins();
|
||||||
ui->spinBox_play_coins->setValue(play_coin);
|
ui->spinBox_play_coins->setValue(play_coin);
|
||||||
|
|
||||||
|
// set firmware download region
|
||||||
|
ui->combo_download_region->setCurrentIndex(static_cast<int>(cfg->GetRegionValue()));
|
||||||
|
|
||||||
// Refresh secure data status
|
// Refresh secure data status
|
||||||
RefreshSecureDataStatus();
|
RefreshSecureDataStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ConfigureSystem::ApplyConfiguration() {
|
void ConfigureSystem::ApplyConfiguration() {
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
ConfigurationShared::ApplyPerGameSetting(&Settings::values.region_value,
|
|
||||||
ui->region_combobox,
|
|
||||||
[](s32 index) { return index - 1; });
|
|
||||||
|
|
||||||
bool modified = false;
|
bool modified = false;
|
||||||
|
|
||||||
// apply username
|
// apply username
|
||||||
@ -591,51 +609,6 @@ void ConfigureSystem::RefreshMAC() {
|
|||||||
ui->label_mac->setText(tr("MAC: %1").arg(QString::fromStdString(mac_address)));
|
ui->label_mac->setText(tr("MAC: %1").arg(QString::fromStdString(mac_address)));
|
||||||
}
|
}
|
||||||
|
|
||||||
void ConfigureSystem::UnlinkConsole() {
|
|
||||||
QMessageBox::StandardButton reply;
|
|
||||||
QString warning_text =
|
|
||||||
tr("This action will unlink your real console from Azahar, with the following "
|
|
||||||
"consequences:<br><ul><li>Your OTP, SecureInfo and LocalFriendCodeSeed will be removed "
|
|
||||||
"from Azahar.</li><li>Your friend list will reset and you will be logged out of your "
|
|
||||||
"NNID/PNID account.</li><li>System files and eshop titles obtained through Azahar will "
|
|
||||||
"become inaccessible until the same console is linked again (save data will not be "
|
|
||||||
"lost).</li></ul><br>Continue?");
|
|
||||||
reply =
|
|
||||||
QMessageBox::warning(this, tr("Warning"), warning_text, QMessageBox::No | QMessageBox::Yes);
|
|
||||||
if (reply == QMessageBox::No) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
HW::UniqueData::UnlinkConsole();
|
|
||||||
RefreshSecureDataStatus();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ConfigureSystem::CheckCountryValid(u8 country) {
|
|
||||||
// TODO(PabloMK7): Make this per-game compatible
|
|
||||||
if (!Settings::IsConfiguringGlobal())
|
|
||||||
return;
|
|
||||||
|
|
||||||
s32 region = ui->region_combobox->currentIndex() - 1;
|
|
||||||
QString label_text;
|
|
||||||
|
|
||||||
if (region != Settings::REGION_VALUE_AUTO_SELECT &&
|
|
||||||
!cfg->IsValidRegionCountry(static_cast<u32>(region), country)) {
|
|
||||||
label_text = tr("Invalid country for configured region");
|
|
||||||
}
|
|
||||||
if (HW::UniqueData::GetSecureInfoA().IsValid()) {
|
|
||||||
region = static_cast<u32>(cfg->GetRegionValue(true));
|
|
||||||
if (!cfg->IsValidRegionCountry(static_cast<u32>(region), country)) {
|
|
||||||
if (!label_text.isEmpty()) {
|
|
||||||
label_text += QString::fromStdString("\n");
|
|
||||||
}
|
|
||||||
label_text += tr("Invalid country for console unique data");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ui->label_country_invalid->setText(label_text);
|
|
||||||
ui->label_country_invalid->setVisible(!label_text.isEmpty());
|
|
||||||
}
|
|
||||||
|
|
||||||
void ConfigureSystem::InstallSecureData(const std::string& from_path, const std::string& to_path) {
|
void ConfigureSystem::InstallSecureData(const std::string& from_path, const std::string& to_path) {
|
||||||
std::string from =
|
std::string from =
|
||||||
FileUtil::SanitizePath(from_path, FileUtil::DirectorySeparator::PlatformDefault);
|
FileUtil::SanitizePath(from_path, FileUtil::DirectorySeparator::PlatformDefault);
|
||||||
@ -646,9 +619,24 @@ void ConfigureSystem::InstallSecureData(const std::string& from_path, const std:
|
|||||||
FileUtil::CreateFullPath(to);
|
FileUtil::CreateFullPath(to);
|
||||||
FileUtil::Copy(from, to);
|
FileUtil::Copy(from, to);
|
||||||
HW::UniqueData::InvalidateSecureData();
|
HW::UniqueData::InvalidateSecureData();
|
||||||
|
cfg->InvalidateSecureData();
|
||||||
RefreshSecureDataStatus();
|
RefreshSecureDataStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ConfigureSystem::InstallCTCert(const std::string& from_path) {
|
||||||
|
std::string from =
|
||||||
|
FileUtil::SanitizePath(from_path, FileUtil::DirectorySeparator::PlatformDefault);
|
||||||
|
std::string to = FileUtil::SanitizePath(Service::AM::Module::GetCTCertPath(),
|
||||||
|
FileUtil::DirectorySeparator::PlatformDefault);
|
||||||
|
if (from.empty() || from == to) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
FileUtil::Copy(from, to);
|
||||||
|
RefreshSecureDataStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
// todotodo
|
||||||
|
#ifdef todotodo
|
||||||
void ConfigureSystem::RefreshSecureDataStatus() {
|
void ConfigureSystem::RefreshSecureDataStatus() {
|
||||||
auto status_to_str = [](HW::UniqueData::SecureDataLoadStatus status) {
|
auto status_to_str = [](HW::UniqueData::SecureDataLoadStatus status) {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
@ -676,16 +664,38 @@ void ConfigureSystem::RefreshSecureDataStatus() {
|
|||||||
tr((std::string("Status: ") + status_to_str(HW::UniqueData::LoadOTP())).c_str()));
|
tr((std::string("Status: ") + status_to_str(HW::UniqueData::LoadOTP())).c_str()));
|
||||||
ui->label_movable_status->setText(
|
ui->label_movable_status->setText(
|
||||||
tr((std::string("Status: ") + status_to_str(HW::UniqueData::LoadMovable())).c_str()));
|
tr((std::string("Status: ") + status_to_str(HW::UniqueData::LoadMovable())).c_str()));
|
||||||
|
|
||||||
if (HW::UniqueData::IsFullConsoleLinked()) {
|
|
||||||
ui->linked_console->setVisible(true);
|
|
||||||
ui->button_otp->setEnabled(false);
|
|
||||||
ui->button_secure_info->setEnabled(false);
|
|
||||||
ui->button_friend_code_seed->setEnabled(false);
|
|
||||||
} else {
|
|
||||||
ui->linked_console->setVisible(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
//--
|
||||||
|
void ConfigureSystem::RefreshSecureDataStatus() {
|
||||||
|
auto status_to_str = [](Service::CFG::SecureDataLoadStatus status) {
|
||||||
|
switch (status) {
|
||||||
|
case Service::CFG::SecureDataLoadStatus::Loaded:
|
||||||
|
return "Loaded";
|
||||||
|
case Service::CFG::SecureDataLoadStatus::NotFound:
|
||||||
|
return "Not Found";
|
||||||
|
case Service::CFG::SecureDataLoadStatus::Invalid:
|
||||||
|
return "Invalid";
|
||||||
|
case Service::CFG::SecureDataLoadStatus::IOError:
|
||||||
|
return "IO Error";
|
||||||
|
default:
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Service::AM::CTCert ct_cert;
|
||||||
|
|
||||||
|
ui->label_secure_info_status->setText(
|
||||||
|
tr((std::string("Status: ") + status_to_str(cfg->LoadSecureInfoAFile())).c_str()));
|
||||||
|
ui->label_friend_code_seed_status->setText(
|
||||||
|
tr((std::string("Status: ") + status_to_str(cfg->LoadLocalFriendCodeSeedBFile())).c_str()));
|
||||||
|
ui->label_ct_cert_status->setText(
|
||||||
|
tr((std::string("Status: ") + status_to_str(static_cast<Service::CFG::SecureDataLoadStatus>(
|
||||||
|
Service::AM::Module::LoadCTCertFile(ct_cert))))
|
||||||
|
.c_str()));
|
||||||
|
}
|
||||||
|
//--
|
||||||
|
|
||||||
void ConfigureSystem::RetranslateUI() {
|
void ConfigureSystem::RetranslateUI() {
|
||||||
ui->retranslateUi(this);
|
ui->retranslateUi(this);
|
||||||
@ -698,7 +708,6 @@ void ConfigureSystem::SetupPerGameUI() {
|
|||||||
ui->toggle_lle_applets->setEnabled(Settings::values.lle_applets.UsingGlobal());
|
ui->toggle_lle_applets->setEnabled(Settings::values.lle_applets.UsingGlobal());
|
||||||
ui->enable_required_online_lle_modules->setEnabled(
|
ui->enable_required_online_lle_modules->setEnabled(
|
||||||
Settings::values.enable_required_online_lle_modules.UsingGlobal());
|
Settings::values.enable_required_online_lle_modules.UsingGlobal());
|
||||||
ui->region_combobox->setEnabled(Settings::values.region_value.UsingGlobal());
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -710,7 +719,6 @@ void ConfigureSystem::SetupPerGameUI() {
|
|||||||
ui->label_init_ticks_type->setVisible(false);
|
ui->label_init_ticks_type->setVisible(false);
|
||||||
ui->label_init_ticks_value->setVisible(false);
|
ui->label_init_ticks_value->setVisible(false);
|
||||||
ui->label_console_id->setVisible(false);
|
ui->label_console_id->setVisible(false);
|
||||||
ui->label_mac->setVisible(false);
|
|
||||||
ui->label_sound->setVisible(false);
|
ui->label_sound->setVisible(false);
|
||||||
ui->label_language->setVisible(false);
|
ui->label_language->setVisible(false);
|
||||||
ui->label_country->setVisible(false);
|
ui->label_country->setVisible(false);
|
||||||
@ -732,7 +740,6 @@ void ConfigureSystem::SetupPerGameUI() {
|
|||||||
ui->edit_init_ticks_value->setVisible(false);
|
ui->edit_init_ticks_value->setVisible(false);
|
||||||
ui->toggle_system_setup->setVisible(false);
|
ui->toggle_system_setup->setVisible(false);
|
||||||
ui->button_regenerate_console_id->setVisible(false);
|
ui->button_regenerate_console_id->setVisible(false);
|
||||||
ui->button_regenerate_mac->setVisible(false);
|
|
||||||
// Apps can change the state of the plugin loader, so plugins load
|
// Apps can change the state of the plugin loader, so plugins load
|
||||||
// to a chainloaded app with specific parameters. Don't allow
|
// to a chainloaded app with specific parameters. Don't allow
|
||||||
// the plugin loader state to be configured per-game as it may
|
// the plugin loader state to be configured per-game as it may
|
||||||
@ -740,7 +747,9 @@ void ConfigureSystem::SetupPerGameUI() {
|
|||||||
ui->label_plugin_loader->setVisible(false);
|
ui->label_plugin_loader->setVisible(false);
|
||||||
ui->plugin_loader->setVisible(false);
|
ui->plugin_loader->setVisible(false);
|
||||||
ui->allow_plugin_loader->setVisible(false);
|
ui->allow_plugin_loader->setVisible(false);
|
||||||
ui->group_real_console_unique_data->setVisible(false);
|
// Disable the system firmware downloader.
|
||||||
|
ui->label_nus_download->setVisible(false);
|
||||||
|
ui->body_nus_download->setVisible(false);
|
||||||
|
|
||||||
ConfigurationShared::SetColoredTristate(ui->toggle_new_3ds, Settings::values.is_new_3ds,
|
ConfigurationShared::SetColoredTristate(ui->toggle_new_3ds, Settings::values.is_new_3ds,
|
||||||
is_new_3ds);
|
is_new_3ds);
|
||||||
@ -749,7 +758,45 @@ void ConfigureSystem::SetupPerGameUI() {
|
|||||||
ConfigurationShared::SetColoredTristate(ui->enable_required_online_lle_modules,
|
ConfigurationShared::SetColoredTristate(ui->enable_required_online_lle_modules,
|
||||||
Settings::values.enable_required_online_lle_modules,
|
Settings::values.enable_required_online_lle_modules,
|
||||||
required_online_lle_modules);
|
required_online_lle_modules);
|
||||||
ConfigurationShared::SetColoredComboBox(
|
}
|
||||||
ui->region_combobox, ui->region_label,
|
|
||||||
static_cast<u32>(Settings::values.region_value.GetValue(true) + 1));
|
void ConfigureSystem::DownloadFromNUS() {
|
||||||
|
ui->button_start_download->setEnabled(false);
|
||||||
|
|
||||||
|
const auto mode =
|
||||||
|
static_cast<Core::SystemTitleSet>(1 << ui->combo_download_set->currentIndex());
|
||||||
|
const auto region = static_cast<u32>(ui->combo_download_region->currentIndex());
|
||||||
|
const std::vector<u64> titles = Core::GetSystemTitleIds(mode, region);
|
||||||
|
|
||||||
|
QProgressDialog progress(tr("Downloading files..."), tr("Cancel"), 0,
|
||||||
|
static_cast<int>(titles.size()), this);
|
||||||
|
progress.setWindowModality(Qt::WindowModal);
|
||||||
|
|
||||||
|
QFutureWatcher<void> future_watcher;
|
||||||
|
QObject::connect(&future_watcher, &QFutureWatcher<void>::finished, &progress,
|
||||||
|
&QProgressDialog::reset);
|
||||||
|
QObject::connect(&progress, &QProgressDialog::canceled, &future_watcher,
|
||||||
|
&QFutureWatcher<void>::cancel);
|
||||||
|
QObject::connect(&future_watcher, &QFutureWatcher<void>::progressValueChanged, &progress,
|
||||||
|
&QProgressDialog::setValue);
|
||||||
|
|
||||||
|
auto failed = false;
|
||||||
|
const auto download_title = [&future_watcher, &failed](const u64& title_id) {
|
||||||
|
if (Service::AM::InstallFromNus(title_id) != Service::AM::InstallStatus::Success) {
|
||||||
|
failed = true;
|
||||||
|
future_watcher.cancel();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
future_watcher.setFuture(QtConcurrent::map(titles, download_title));
|
||||||
|
progress.exec();
|
||||||
|
future_watcher.waitForFinished();
|
||||||
|
|
||||||
|
if (failed) {
|
||||||
|
QMessageBox::critical(this, tr("Azahar"), tr("Downloading system files failed."));
|
||||||
|
} else if (!future_watcher.isCanceled()) {
|
||||||
|
QMessageBox::information(this, tr("Azahar"), tr("Successfully downloaded system files."));
|
||||||
|
}
|
||||||
|
|
||||||
|
ui->button_start_download->setEnabled(true);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -56,10 +56,13 @@ private:
|
|||||||
void CheckCountryValid(u8 country);
|
void CheckCountryValid(u8 country);
|
||||||
|
|
||||||
void InstallSecureData(const std::string& from_path, const std::string& to_path);
|
void InstallSecureData(const std::string& from_path, const std::string& to_path);
|
||||||
|
void InstallCTCert(const std::string& from_path);
|
||||||
void RefreshSecureDataStatus();
|
void RefreshSecureDataStatus();
|
||||||
|
|
||||||
void SetupPerGameUI();
|
void SetupPerGameUI();
|
||||||
|
|
||||||
|
void DownloadFromNUS();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::unique_ptr<Ui::ConfigureSystem> ui;
|
std::unique_ptr<Ui::ConfigureSystem> ui;
|
||||||
Core::System& system;
|
Core::System& system;
|
||||||
|
|||||||
@ -6,7 +6,7 @@
|
|||||||
<rect>
|
<rect>
|
||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>653</width>
|
<width>535</width>
|
||||||
<height>619</height>
|
<height>619</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
@ -14,26 +14,11 @@
|
|||||||
<string>Form</string>
|
<string>Form</string>
|
||||||
</property>
|
</property>
|
||||||
<layout class="QVBoxLayout" name="verticalLayout_scrollbar">
|
<layout class="QVBoxLayout" name="verticalLayout_scrollbar">
|
||||||
<property name="spacing">
|
|
||||||
<number>0</number>
|
|
||||||
</property>
|
|
||||||
<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>
|
<item>
|
||||||
<widget class="QScrollArea" name="scrollArea">
|
<widget class="QScrollArea" name="scrollArea">
|
||||||
<property name="minimumSize">
|
<property name="minimumSize">
|
||||||
<size>
|
<size>
|
||||||
<width>660</width>
|
<width>0</width>
|
||||||
<height>480</height>
|
<height>480</height>
|
||||||
</size>
|
</size>
|
||||||
</property>
|
</property>
|
||||||
@ -64,83 +49,21 @@
|
|||||||
<string>System Settings</string>
|
<string>System Settings</string>
|
||||||
</property>
|
</property>
|
||||||
<layout class="QGridLayout" name="gridLayout">
|
<layout class="QGridLayout" name="gridLayout">
|
||||||
<item row="0" column="0">
|
<item row="1" column="0">
|
||||||
<widget class="QCheckBox" name="toggle_new_3ds">
|
<widget class="QCheckBox" name="toggle_new_3ds">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Enable New 3DS mode</string>
|
<string>Enable New 3DS mode</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="1" column="0">
|
<item row="2" column="0">
|
||||||
<widget class="QCheckBox" name="toggle_lle_applets">
|
<widget class="QCheckBox" name="toggle_lle_applets">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Use LLE applets (if installed)</string>
|
<string>Use LLE applets (if installed)</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="2" column="0">
|
|
||||||
<widget class="QCheckBox" name="enable_required_online_lle_modules">
|
|
||||||
<property name="text">
|
|
||||||
<string>Enable required LLE modules for
|
|
||||||
online features (if installed)</string>
|
|
||||||
</property>
|
|
||||||
<property name="toolTip">
|
|
||||||
<string>Enables the LLE modules needed for online multiplayer, eShop access, etc.</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="3" column="0">
|
|
||||||
<widget class="QLabel" name="region_label">
|
|
||||||
<property name="text">
|
|
||||||
<string>Region:</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="3" column="1">
|
<item row="3" column="1">
|
||||||
<widget class="QComboBox" name="region_combobox">
|
|
||||||
<item>
|
|
||||||
<property name="text">
|
|
||||||
<string>Auto-select</string>
|
|
||||||
</property>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<property name="text">
|
|
||||||
<string notr="true">JPN</string>
|
|
||||||
</property>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<property name="text">
|
|
||||||
<string notr="true">USA</string>
|
|
||||||
</property>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<property name="text">
|
|
||||||
<string notr="true">EUR</string>
|
|
||||||
</property>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<property name="text">
|
|
||||||
<string notr="true">AUS</string>
|
|
||||||
</property>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<property name="text">
|
|
||||||
<string notr="true">CHN</string>
|
|
||||||
</property>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<property name="text">
|
|
||||||
<string notr="true">KOR</string>
|
|
||||||
</property>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<property name="text">
|
|
||||||
<string notr="true">TWN</string>
|
|
||||||
</property>
|
|
||||||
</item>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="4" column="1">
|
|
||||||
<widget class="QLineEdit" name="edit_username">
|
<widget class="QLineEdit" name="edit_username">
|
||||||
<property name="sizePolicy">
|
<property name="sizePolicy">
|
||||||
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
|
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
|
||||||
@ -153,21 +76,21 @@ online features (if installed)</string>
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="4" column="0">
|
<item row="3" column="0">
|
||||||
<widget class="QLabel" name="label_username">
|
<widget class="QLabel" name="label_username">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Username</string>
|
<string>Username</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="5" column="0">
|
<item row="4" column="0">
|
||||||
<widget class="QLabel" name="label_birthday">
|
<widget class="QLabel" name="label_birthday">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Birthday</string>
|
<string>Birthday</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="5" column="1">
|
<item row="4" column="1">
|
||||||
<layout class="QHBoxLayout" name="horizontalLayout_birthday2">
|
<layout class="QHBoxLayout" name="horizontalLayout_birthday2">
|
||||||
<item>
|
<item>
|
||||||
<widget class="QComboBox" name="combo_birthmonth">
|
<widget class="QComboBox" name="combo_birthmonth">
|
||||||
@ -238,14 +161,14 @@ online features (if installed)</string>
|
|||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
<item row="6" column="0">
|
<item row="5" column="0">
|
||||||
<widget class="QLabel" name="label_language">
|
<widget class="QLabel" name="label_language">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Language</string>
|
<string>Language</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="6" column="1">
|
<item row="5" column="1">
|
||||||
<widget class="QComboBox" name="combo_language">
|
<widget class="QComboBox" name="combo_language">
|
||||||
<property name="toolTip">
|
<property name="toolTip">
|
||||||
<string>Note: this can be overridden when region setting is auto-select</string>
|
<string>Note: this can be overridden when region setting is auto-select</string>
|
||||||
@ -312,14 +235,14 @@ online features (if installed)</string>
|
|||||||
</item>
|
</item>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="7" column="0">
|
<item row="6" column="0">
|
||||||
<widget class="QLabel" name="label_sound">
|
<widget class="QLabel" name="label_sound">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Sound output mode</string>
|
<string>Sound output mode</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="7" column="1">
|
<item row="6" column="1">
|
||||||
<widget class="QComboBox" name="combo_sound">
|
<widget class="QComboBox" name="combo_sound">
|
||||||
<item>
|
<item>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
@ -338,31 +261,24 @@ online features (if installed)</string>
|
|||||||
</item>
|
</item>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="8" column="0">
|
<item row="7" column="0">
|
||||||
<widget class="QLabel" name="label_country">
|
<widget class="QLabel" name="label_country">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Country</string>
|
<string>Country</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="8" column="1">
|
<item row="7" column="1">
|
||||||
<widget class="QComboBox" name="combo_country"/>
|
<widget class="QComboBox" name="combo_country"/>
|
||||||
</item>
|
</item>
|
||||||
<item row="9" column="1">
|
<item row="8" column="0">
|
||||||
<widget class="QLabel" name="label_country_invalid">
|
|
||||||
<property name="text">
|
|
||||||
<string/>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="10" column="0">
|
|
||||||
<widget class="QLabel" name="label_init_clock">
|
<widget class="QLabel" name="label_init_clock">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Clock</string>
|
<string>Clock</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="10" column="1">
|
<item row="8" column="1">
|
||||||
<widget class="QComboBox" name="combo_init_clock">
|
<widget class="QComboBox" name="combo_init_clock">
|
||||||
<item>
|
<item>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
@ -376,28 +292,28 @@ online features (if installed)</string>
|
|||||||
</item>
|
</item>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="11" column="0">
|
<item row="9" column="0">
|
||||||
<widget class="QLabel" name="label_init_time">
|
<widget class="QLabel" name="label_init_time">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Startup time</string>
|
<string>Startup time</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="11" column="1">
|
<item row="9" column="1">
|
||||||
<widget class="QDateTimeEdit" name="edit_init_time">
|
<widget class="QDateTimeEdit" name="edit_init_time">
|
||||||
<property name="displayFormat">
|
<property name="displayFormat">
|
||||||
<string>yyyy-MM-ddTHH:mm:ss</string>
|
<string>yyyy-MM-ddTHH:mm:ss</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="12" column="0">
|
<item row="9" column="0">
|
||||||
<widget class="QLabel" name="label_init_time_offset">
|
<widget class="QLabel" name="label_init_time_offset">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Offset time</string>
|
<string>Offset time</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="12" column="1">
|
<item row="9" column="1">
|
||||||
<layout class="QGridLayout" name="edit_init_time_offset_grid">
|
<layout class="QGridLayout" name="edit_init_time_offset_grid">
|
||||||
<item row="0" column="0">
|
<item row="0" column="0">
|
||||||
<widget class="QSpinBox" name="edit_init_time_offset_days">
|
<widget class="QSpinBox" name="edit_init_time_offset_days">
|
||||||
@ -421,14 +337,14 @@ online features (if installed)</string>
|
|||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
<item row="13" column="0">
|
<item row="10" column="0">
|
||||||
<widget class="QLabel" name="label_init_ticks_type">
|
<widget class="QLabel" name="label_init_ticks_type">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Initial System Ticks</string>
|
<string>Initial System Ticks</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="13" column="1">
|
<item row="10" column="1">
|
||||||
<widget class="QComboBox" name="combo_init_ticks_type">
|
<widget class="QComboBox" name="combo_init_ticks_type">
|
||||||
<item>
|
<item>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
@ -442,14 +358,14 @@ online features (if installed)</string>
|
|||||||
</item>
|
</item>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="14" column="0">
|
<item row="11" column="0">
|
||||||
<widget class="QLabel" name="label_init_ticks_value">
|
<widget class="QLabel" name="label_init_ticks_value">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Initial System Ticks Override</string>
|
<string>Initial System Ticks Override</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="14" column="1">
|
<item row="11" column="1">
|
||||||
<widget class="QLineEdit" name="edit_init_ticks_value">
|
<widget class="QLineEdit" name="edit_init_ticks_value">
|
||||||
<property name="sizePolicy">
|
<property name="sizePolicy">
|
||||||
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
|
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
|
||||||
@ -462,21 +378,21 @@ online features (if installed)</string>
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="15" column="0">
|
<item row="12" column="0">
|
||||||
<widget class="QLabel" name="label_play_coins">
|
<widget class="QLabel" name="label_play_coins">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Play Coins</string>
|
<string>Play Coins</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="15" column="1">
|
<item row="12" column="1">
|
||||||
<widget class="QSpinBox" name="spinBox_play_coins">
|
<widget class="QSpinBox" name="spinBox_play_coins">
|
||||||
<property name="maximum">
|
<property name="maximum">
|
||||||
<number>300</number>
|
<number>300</number>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="16" column="0">
|
<item row="13" column="0">
|
||||||
<widget class="QLabel" name="label_steps_per_hour">
|
<widget class="QLabel" name="label_steps_per_hour">
|
||||||
<property name="toolTip">
|
<property name="toolTip">
|
||||||
<string><html><head/><body><p>Number of steps per hour reported by the pedometer. Range from 0 to 65,535.</p></body></html></string>
|
<string><html><head/><body><p>Number of steps per hour reported by the pedometer. Range from 0 to 65,535.</p></body></html></string>
|
||||||
@ -486,28 +402,28 @@ online features (if installed)</string>
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="16" column="1">
|
<item row="13" column="1">
|
||||||
<widget class="QSpinBox" name="spinBox_steps_per_hour">
|
<widget class="QSpinBox" name="spinBox_steps_per_hour">
|
||||||
<property name="maximum">
|
<property name="maximum">
|
||||||
<number>9999</number>
|
<number>9999</number>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="17" column="1">
|
<item row="14" column="1">
|
||||||
<widget class="QCheckBox" name="toggle_system_setup">
|
<widget class="QCheckBox" name="toggle_system_setup">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Run System Setup when Home Menu is launched</string>
|
<string>Run System Setup when Home Menu is launched</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="18" column="0">
|
<item row="15" column="0">
|
||||||
<widget class="QLabel" name="label_console_id">
|
<widget class="QLabel" name="label_console_id">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Console ID:</string>
|
<string>Console ID:</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="18" column="1">
|
<item row="15" column="1">
|
||||||
<widget class="QPushButton" name="button_regenerate_console_id">
|
<widget class="QPushButton" name="button_regenerate_console_id">
|
||||||
<property name="sizePolicy">
|
<property name="sizePolicy">
|
||||||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
||||||
@ -523,50 +439,114 @@ online features (if installed)</string>
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="19" column="0">
|
<item row="16" column="0">
|
||||||
<widget class="QLabel" name="label_mac">
|
|
||||||
<property name="text">
|
|
||||||
<string>MAC:</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="19" column="1">
|
|
||||||
<widget class="QPushButton" name="button_regenerate_mac">
|
|
||||||
<property name="sizePolicy">
|
|
||||||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
|
||||||
<horstretch>0</horstretch>
|
|
||||||
<verstretch>0</verstretch>
|
|
||||||
</sizepolicy>
|
|
||||||
</property>
|
|
||||||
<property name="layoutDirection">
|
|
||||||
<enum>Qt::RightToLeft</enum>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>Regenerate</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="20" column="0">
|
|
||||||
<widget class="QLabel" name="label_plugin_loader">
|
<widget class="QLabel" name="label_plugin_loader">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>3GX Plugin Loader:</string>
|
<string>3GX Plugin Loader:</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="20" column="1">
|
<item row="16" column="1">
|
||||||
<widget class="QCheckBox" name="plugin_loader">
|
<widget class="QCheckBox" name="plugin_loader">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Enable 3GX plugin loader</string>
|
<string>Enable 3GX plugin loader</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="21" column="1">
|
<item row="17" column="1">
|
||||||
<widget class="QCheckBox" name="allow_plugin_loader">
|
<widget class="QCheckBox" name="allow_plugin_loader">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Allow applications to change plugin loader state</string>
|
<string>Allow applications to change plugin loader state</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item row="18" column="0">
|
||||||
|
<widget class="QLabel" name="label_nus_download">
|
||||||
|
<property name="text">
|
||||||
|
<string>Download System Files from Nintendo servers</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="18" column="1">
|
||||||
|
<widget class="QWidget" name="body_nus_download">
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_nus_download">
|
||||||
|
<item>
|
||||||
|
<widget class="QComboBox" name="combo_download_set">
|
||||||
|
<item>
|
||||||
|
<property name="text">
|
||||||
|
<string>Minimal</string>
|
||||||
|
</property>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<property name="text">
|
||||||
|
<string>Old 3DS</string>
|
||||||
|
</property>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<property name="text">
|
||||||
|
<string>New 3DS</string>
|
||||||
|
</property>
|
||||||
|
</item>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QComboBox" name="combo_download_region">
|
||||||
|
<item>
|
||||||
|
<property name="text">
|
||||||
|
<string>JPN</string>
|
||||||
|
</property>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<property name="text">
|
||||||
|
<string>USA</string>
|
||||||
|
</property>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<property name="text">
|
||||||
|
<string>EUR</string>
|
||||||
|
</property>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<property name="text">
|
||||||
|
<string>AUS</string>
|
||||||
|
</property>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<property name="text">
|
||||||
|
<string>CHN</string>
|
||||||
|
</property>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<property name="text">
|
||||||
|
<string>KOR</string>
|
||||||
|
</property>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<property name="text">
|
||||||
|
<string>TWN</string>
|
||||||
|
</property>
|
||||||
|
</item>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="button_start_download">
|
||||||
|
<property name="sizePolicy">
|
||||||
|
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
||||||
|
<horstretch>0</horstretch>
|
||||||
|
<verstretch>0</verstretch>
|
||||||
|
</sizepolicy>
|
||||||
|
</property>
|
||||||
|
<property name="layoutDirection">
|
||||||
|
<enum>Qt::RightToLeft</enum>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Download</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
@ -576,168 +556,97 @@ online features (if installed)</string>
|
|||||||
<string>Real Console Unique Data</string>
|
<string>Real Console Unique Data</string>
|
||||||
</property>
|
</property>
|
||||||
<layout class="QGridLayout" name="gridLayout1">
|
<layout class="QGridLayout" name="gridLayout1">
|
||||||
<item row="0" column="0" colspan="2">
|
|
||||||
<widget class="QGroupBox" name="group_real_console_unique_data_core">
|
|
||||||
<layout class="QGridLayout" name="gridLayout2">
|
|
||||||
<item row="0" column="0" colspan="2">
|
|
||||||
<widget class="QWidget" name="linked_console">
|
|
||||||
<layout class="QHBoxLayout" name="horizontalLayout_linked_console">
|
|
||||||
<item>
|
|
||||||
<widget class="QLabel" name="label_linked_console">
|
|
||||||
<property name="text">
|
|
||||||
<string>Your real console is linked to Azahar.</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<widget class="QPushButton" name="button_linked_console">
|
|
||||||
<property name="sizePolicy">
|
|
||||||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
|
||||||
<horstretch>0</horstretch>
|
|
||||||
<verstretch>0</verstretch>
|
|
||||||
</sizepolicy>
|
|
||||||
</property>
|
|
||||||
<property name="layoutDirection">
|
|
||||||
<enum>Qt::RightToLeft</enum>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>Unlink</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="1" column="0">
|
|
||||||
<widget class="QLabel" name="label_otp">
|
|
||||||
<property name="text">
|
|
||||||
<string>OTP</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="1" column="1">
|
|
||||||
<widget class="QWidget" name="otp">
|
|
||||||
<layout class="QHBoxLayout" name="horizontalLayout_otp">
|
|
||||||
<item>
|
|
||||||
<widget class="QLabel" name="label_otp_status">
|
|
||||||
<property name="text">
|
|
||||||
<string/>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<widget class="QPushButton" name="button_otp">
|
|
||||||
<property name="sizePolicy">
|
|
||||||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
|
||||||
<horstretch>0</horstretch>
|
|
||||||
<verstretch>0</verstretch>
|
|
||||||
</sizepolicy>
|
|
||||||
</property>
|
|
||||||
<property name="layoutDirection">
|
|
||||||
<enum>Qt::RightToLeft</enum>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>Choose</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="2" column="0">
|
|
||||||
<widget class="QLabel" name="label_secure_info">
|
|
||||||
<property name="text">
|
|
||||||
<string>SecureInfo_A/B</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="2" column="1">
|
|
||||||
<widget class="QWidget" name="secure_info">
|
|
||||||
<layout class="QHBoxLayout" name="horizontalLayout_secure_info">
|
|
||||||
<item>
|
|
||||||
<widget class="QLabel" name="label_secure_info_status">
|
|
||||||
<property name="text">
|
|
||||||
<string/>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<widget class="QPushButton" name="button_secure_info">
|
|
||||||
<property name="sizePolicy">
|
|
||||||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
|
||||||
<horstretch>0</horstretch>
|
|
||||||
<verstretch>0</verstretch>
|
|
||||||
</sizepolicy>
|
|
||||||
</property>
|
|
||||||
<property name="layoutDirection">
|
|
||||||
<enum>Qt::RightToLeft</enum>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>Choose</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="3" column="0">
|
|
||||||
<widget class="QLabel" name="label_friend_code_seed">
|
|
||||||
<property name="text">
|
|
||||||
<string>LocalFriendCodeSeed_A/B</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="3" column="1">
|
|
||||||
<widget class="QWidget" name="friend_code_seed">
|
|
||||||
<layout class="QHBoxLayout" name="horizontalLayout_friend_code_seed">
|
|
||||||
<item>
|
|
||||||
<widget class="QLabel" name="label_friend_code_seed_status">
|
|
||||||
<property name="text">
|
|
||||||
<string/>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<widget class="QPushButton" name="button_friend_code_seed">
|
|
||||||
<property name="sizePolicy">
|
|
||||||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
|
||||||
<horstretch>0</horstretch>
|
|
||||||
<verstretch>0</verstretch>
|
|
||||||
</sizepolicy>
|
|
||||||
</property>
|
|
||||||
<property name="layoutDirection">
|
|
||||||
<enum>Qt::RightToLeft</enum>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>Choose</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="1" column="0">
|
<item row="1" column="0">
|
||||||
<widget class="QLabel" name="label_movable">
|
<widget class="QLabel" name="label_secure_info">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>movable.sed</string>
|
<string>SecureInfo_A/B</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="1" column="1">
|
<item row="1" column="1">
|
||||||
<widget class="QWidget" name="movable">
|
<widget class="QWidget" name="secure_info">
|
||||||
<layout class="QHBoxLayout" name="horizontalLayout_movable">
|
<layout class="QHBoxLayout" name="horizontalLayout_secure_info">
|
||||||
<item>
|
<item>
|
||||||
<widget class="QLabel" name="label_movable_status">
|
<widget class="QLabel" name="label_secure_info_status">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string/>
|
<string/>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QPushButton" name="button_movable">
|
<widget class="QPushButton" name="button_secure_info">
|
||||||
|
<property name="sizePolicy">
|
||||||
|
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
||||||
|
<horstretch>0</horstretch>
|
||||||
|
<verstretch>0</verstretch>
|
||||||
|
</sizepolicy>
|
||||||
|
</property>
|
||||||
|
<property name="layoutDirection">
|
||||||
|
<enum>Qt::RightToLeft</enum>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Choose</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="0">
|
||||||
|
<widget class="QLabel" name="label_friend_code_seed">
|
||||||
|
<property name="text">
|
||||||
|
<string>LocalFriendCodeSeed_A/B</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="1">
|
||||||
|
<widget class="QWidget" name="friend_code_seed">
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_friend_code_seed">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_friend_code_seed_status">
|
||||||
|
<property name="text">
|
||||||
|
<string/>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="button_friend_code_seed">
|
||||||
|
<property name="sizePolicy">
|
||||||
|
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
||||||
|
<horstretch>0</horstretch>
|
||||||
|
<verstretch>0</verstretch>
|
||||||
|
</sizepolicy>
|
||||||
|
</property>
|
||||||
|
<property name="layoutDirection">
|
||||||
|
<enum>Qt::RightToLeft</enum>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Choose</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="3" column="0">
|
||||||
|
<widget class="QLabel" name="label_ct_cert">
|
||||||
|
<property name="text">
|
||||||
|
<string>CTCert</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="3" column="1">
|
||||||
|
<widget class="QWidget" name="ct_cert">
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_ct_cert">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_ct_cert_status">
|
||||||
|
<property name="text">
|
||||||
|
<string/>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="button_ct_cert">
|
||||||
<property name="sizePolicy">
|
<property name="sizePolicy">
|
||||||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
||||||
<horstretch>0</horstretch>
|
<horstretch>0</horstretch>
|
||||||
@ -802,7 +711,6 @@ online features (if installed)</string>
|
|||||||
<tabstop>spinBox_play_coins</tabstop>
|
<tabstop>spinBox_play_coins</tabstop>
|
||||||
<tabstop>spinBox_steps_per_hour</tabstop>
|
<tabstop>spinBox_steps_per_hour</tabstop>
|
||||||
<tabstop>button_regenerate_console_id</tabstop>
|
<tabstop>button_regenerate_console_id</tabstop>
|
||||||
<tabstop>button_regenerate_mac</tabstop>
|
|
||||||
</tabstops>
|
</tabstops>
|
||||||
<resources/>
|
<resources/>
|
||||||
<connections/>
|
<connections/>
|
||||||
|
|||||||
@ -1039,7 +1039,7 @@ void GameList::LoadInterfaceLayout() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const QStringList GameList::supported_file_extensions = {
|
const QStringList GameList::supported_file_extensions = {
|
||||||
QStringLiteral("3dsx"), QStringLiteral("elf"), QStringLiteral("axf"),
|
QStringLiteral("3ds"), QStringLiteral("3dsx"), QStringLiteral("elf"), QStringLiteral("axf"),
|
||||||
QStringLiteral("cci"), QStringLiteral("cxi"), QStringLiteral("app")};
|
QStringLiteral("cci"), QStringLiteral("cxi"), QStringLiteral("app")};
|
||||||
|
|
||||||
void GameList::RefreshGameDirectory() {
|
void GameList::RefreshGameDirectory() {
|
||||||
|
|||||||
@ -184,9 +184,11 @@ public:
|
|||||||
if (UISettings::values.game_list_icon_size.GetValue() !=
|
if (UISettings::values.game_list_icon_size.GetValue() !=
|
||||||
UISettings::GameListIconSize::NoIcon)
|
UISettings::GameListIconSize::NoIcon)
|
||||||
setData(GetDefaultIcon(large), Qt::DecorationRole);
|
setData(GetDefaultIcon(large), Qt::DecorationRole);
|
||||||
|
/* todotodo
|
||||||
if (is_encrypted) {
|
if (is_encrypted) {
|
||||||
setData(QObject::tr("Unsupported encrypted application"), TitleRole);
|
setData(QObject::tr("Unsupported encrypted application"), TitleRole);
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -94,7 +94,7 @@ struct Values {
|
|||||||
Settings::Setting<GameListText> game_list_row_2{GameListText::FileName, "row2"};
|
Settings::Setting<GameListText> game_list_row_2{GameListText::FileName, "row2"};
|
||||||
Settings::Setting<bool> game_list_hide_no_icon{false, "hideNoIcon"};
|
Settings::Setting<bool> game_list_hide_no_icon{false, "hideNoIcon"};
|
||||||
Settings::Setting<bool> game_list_single_line_mode{false, "singleLineMode"};
|
Settings::Setting<bool> game_list_single_line_mode{false, "singleLineMode"};
|
||||||
Settings::Setting<bool> show_3ds_files_warning{true, "show_3ds_files_warning"};
|
Settings::Setting<bool> show_3ds_files_warning{false, "show_3ds_files_warning"};
|
||||||
|
|
||||||
// Compatibility List
|
// Compatibility List
|
||||||
Settings::Setting<bool> show_compat_column{true, "show_compat_column"};
|
Settings::Setting<bool> show_compat_column{true, "show_compat_column"};
|
||||||
|
|||||||
@ -81,5 +81,6 @@
|
|||||||
// Sys files
|
// Sys files
|
||||||
#define SHARED_FONT "shared_font.bin"
|
#define SHARED_FONT "shared_font.bin"
|
||||||
#define KEYS_FILE "keys.txt"
|
#define KEYS_FILE "keys.txt"
|
||||||
|
#define AES_KEYS "aes_keys.txt"
|
||||||
#define BOOTROM9 "boot9.bin"
|
#define BOOTROM9 "boot9.bin"
|
||||||
#define SECRET_SECTOR "sector0x96.bin"
|
#define SECRET_SECTOR "sector0x96.bin"
|
||||||
|
|||||||
@ -386,7 +386,15 @@ public:
|
|||||||
[[nodiscard]] size_t ReadSpan(std::span<T> data) {
|
[[nodiscard]] size_t ReadSpan(std::span<T> data) {
|
||||||
static_assert(std::is_trivially_copyable_v<T>, "Data type must be trivially copyable.");
|
static_assert(std::is_trivially_copyable_v<T>, "Data type must be trivially copyable.");
|
||||||
|
|
||||||
|
#ifdef todotodo
|
||||||
return ReadImpl(data.data(), data.size(), sizeof(T));
|
return ReadImpl(data.data(), data.size(), sizeof(T));
|
||||||
|
#else
|
||||||
|
if (!IsOpen()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return std::fread(data.data(), sizeof(T), data.size(), m_file);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -408,7 +416,15 @@ public:
|
|||||||
[[nodiscard]] size_t WriteSpan(std::span<const T> data) {
|
[[nodiscard]] size_t WriteSpan(std::span<const T> data) {
|
||||||
static_assert(std::is_trivially_copyable_v<T>, "Data type must be trivially copyable.");
|
static_assert(std::is_trivially_copyable_v<T>, "Data type must be trivially copyable.");
|
||||||
|
|
||||||
|
#ifdef todotodo
|
||||||
return WriteImpl(data.data(), data.size(), sizeof(T));
|
return WriteImpl(data.data(), data.size(), sizeof(T));
|
||||||
|
#else
|
||||||
|
if (!IsOpen()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return std::fwrite(data.data(), sizeof(T), data.size(), m_file);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
[[nodiscard]] bool IsOpen() const {
|
[[nodiscard]] bool IsOpen() const {
|
||||||
|
|||||||
@ -70,6 +70,7 @@ HackManager hack_manager = {
|
|||||||
},
|
},
|
||||||
}},
|
}},
|
||||||
|
|
||||||
|
#ifdef todotodo
|
||||||
{HackType::ONLINE_LLE_REQUIRED,
|
{HackType::ONLINE_LLE_REQUIRED,
|
||||||
HackEntry{
|
HackEntry{
|
||||||
.mode = HackAllowMode::FORCE,
|
.mode = HackAllowMode::FORCE,
|
||||||
@ -107,6 +108,7 @@ HackManager hack_manager = {
|
|||||||
0x000400000D40D200,
|
0x000400000D40D200,
|
||||||
},
|
},
|
||||||
}},
|
}},
|
||||||
|
#endif
|
||||||
|
|
||||||
{HackType::REGION_FROM_SECURE,
|
{HackType::REGION_FROM_SECURE,
|
||||||
HackEntry{
|
HackEntry{
|
||||||
|
|||||||
@ -137,7 +137,9 @@ Loader::ResultStatus NCCHContainer::LoadHeader() {
|
|||||||
return Loader::ResultStatus::Success;
|
return Loader::ResultStatus::Success;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef todotodo
|
||||||
for (int i = 0; i < 2; i++) {
|
for (int i = 0; i < 2; i++) {
|
||||||
|
#endif
|
||||||
if (!file->IsOpen()) {
|
if (!file->IsOpen()) {
|
||||||
return Loader::ResultStatus::Error;
|
return Loader::ResultStatus::Error;
|
||||||
}
|
}
|
||||||
@ -164,6 +166,7 @@ Loader::ResultStatus NCCHContainer::LoadHeader() {
|
|||||||
|
|
||||||
// Verify we are loading the correct file type...
|
// Verify we are loading the correct file type...
|
||||||
if (Loader::MakeMagic('N', 'C', 'C', 'H') != ncch_header.magic) {
|
if (Loader::MakeMagic('N', 'C', 'C', 'H') != ncch_header.magic) {
|
||||||
|
#ifdef todotodo
|
||||||
// We may be loading a crypto file, try again
|
// We may be loading a crypto file, try again
|
||||||
if (i == 0) {
|
if (i == 0) {
|
||||||
file.reset();
|
file.reset();
|
||||||
@ -172,8 +175,13 @@ Loader::ResultStatus NCCHContainer::LoadHeader() {
|
|||||||
} else {
|
} else {
|
||||||
return Loader::ResultStatus::ErrorInvalidFormat;
|
return Loader::ResultStatus::ErrorInvalidFormat;
|
||||||
}
|
}
|
||||||
|
#else
|
||||||
|
return Loader::ResultStatus::ErrorInvalidFormat;
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
#ifdef todotodo
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
if (file->IsCrypto()) {
|
if (file->IsCrypto()) {
|
||||||
LOG_DEBUG(Service_FS, "NCCH file has console unique crypto");
|
LOG_DEBUG(Service_FS, "NCCH file has console unique crypto");
|
||||||
@ -192,7 +200,9 @@ Loader::ResultStatus NCCHContainer::Load() {
|
|||||||
if (file->IsOpen()) {
|
if (file->IsOpen()) {
|
||||||
size_t file_size;
|
size_t file_size;
|
||||||
|
|
||||||
|
#ifdef todotodo
|
||||||
for (int i = 0; i < 2; i++) {
|
for (int i = 0; i < 2; i++) {
|
||||||
|
#endif
|
||||||
file_size = file->GetSize();
|
file_size = file->GetSize();
|
||||||
|
|
||||||
// Reset read pointer in case this file has been read before.
|
// Reset read pointer in case this file has been read before.
|
||||||
@ -215,6 +225,7 @@ Loader::ResultStatus NCCHContainer::Load() {
|
|||||||
|
|
||||||
// Verify we are loading the correct file type...
|
// Verify we are loading the correct file type...
|
||||||
if (Loader::MakeMagic('N', 'C', 'C', 'H') != ncch_header.magic) {
|
if (Loader::MakeMagic('N', 'C', 'C', 'H') != ncch_header.magic) {
|
||||||
|
#ifdef todotodo
|
||||||
// We may be loading a crypto file, try again
|
// We may be loading a crypto file, try again
|
||||||
if (i == 0) {
|
if (i == 0) {
|
||||||
file = HW::UniqueData::OpenUniqueCryptoFile(
|
file = HW::UniqueData::OpenUniqueCryptoFile(
|
||||||
@ -222,14 +233,146 @@ Loader::ResultStatus NCCHContainer::Load() {
|
|||||||
} else {
|
} else {
|
||||||
return Loader::ResultStatus::ErrorInvalidFormat;
|
return Loader::ResultStatus::ErrorInvalidFormat;
|
||||||
}
|
}
|
||||||
|
#else
|
||||||
|
return Loader::ResultStatus::ErrorInvalidFormat;
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
#ifdef todotodo
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
if (file->IsCrypto()) {
|
if (file->IsCrypto()) {
|
||||||
LOG_DEBUG(Service_FS, "NCCH file has console unique crypto");
|
LOG_DEBUG(Service_FS, "NCCH file has console unique crypto");
|
||||||
}
|
}
|
||||||
|
|
||||||
has_header = true;
|
has_header = true;
|
||||||
|
bool failed_to_decrypt = false;
|
||||||
|
if (!ncch_header.no_crypto) {
|
||||||
|
is_encrypted = true;
|
||||||
|
|
||||||
|
// Find primary and secondary keys
|
||||||
|
if (ncch_header.fixed_key) {
|
||||||
|
LOG_DEBUG(Service_FS, "Fixed-key crypto");
|
||||||
|
primary_key.fill(0);
|
||||||
|
secondary_key.fill(0);
|
||||||
|
} else {
|
||||||
|
using namespace HW::AES;
|
||||||
|
InitKeys();
|
||||||
|
std::array<u8, 16> key_y_primary, key_y_secondary;
|
||||||
|
|
||||||
|
std::copy(ncch_header.signature, ncch_header.signature + key_y_primary.size(),
|
||||||
|
key_y_primary.begin());
|
||||||
|
|
||||||
|
if (!ncch_header.seed_crypto) {
|
||||||
|
key_y_secondary = key_y_primary;
|
||||||
|
} else {
|
||||||
|
auto opt{FileSys::GetSeed(ncch_header.program_id)};
|
||||||
|
if (!opt.has_value()) {
|
||||||
|
LOG_ERROR(Service_FS, "Seed for program {:016X} not found",
|
||||||
|
ncch_header.program_id);
|
||||||
|
failed_to_decrypt = true;
|
||||||
|
} else {
|
||||||
|
auto seed{*opt};
|
||||||
|
std::array<u8, 32> input;
|
||||||
|
std::memcpy(input.data(), key_y_primary.data(), key_y_primary.size());
|
||||||
|
std::memcpy(input.data() + key_y_primary.size(), seed.data(), seed.size());
|
||||||
|
CryptoPP::SHA256 sha;
|
||||||
|
std::array<u8, CryptoPP::SHA256::DIGESTSIZE> hash;
|
||||||
|
sha.CalculateDigest(hash.data(), input.data(), input.size());
|
||||||
|
std::memcpy(key_y_secondary.data(), hash.data(), key_y_secondary.size());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SetKeyY(KeySlotID::NCCHSecure1, key_y_primary);
|
||||||
|
if (!IsNormalKeyAvailable(KeySlotID::NCCHSecure1)) {
|
||||||
|
LOG_ERROR(Service_FS, "Secure1 KeyX missing");
|
||||||
|
failed_to_decrypt = true;
|
||||||
|
}
|
||||||
|
primary_key = GetNormalKey(KeySlotID::NCCHSecure1);
|
||||||
|
|
||||||
|
switch (ncch_header.secondary_key_slot) {
|
||||||
|
case 0:
|
||||||
|
LOG_DEBUG(Service_FS, "Secure1 crypto");
|
||||||
|
SetKeyY(KeySlotID::NCCHSecure1, key_y_secondary);
|
||||||
|
if (!IsNormalKeyAvailable(KeySlotID::NCCHSecure1)) {
|
||||||
|
LOG_ERROR(Service_FS, "Secure1 KeyX missing");
|
||||||
|
failed_to_decrypt = true;
|
||||||
|
}
|
||||||
|
secondary_key = GetNormalKey(KeySlotID::NCCHSecure1);
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
LOG_DEBUG(Service_FS, "Secure2 crypto");
|
||||||
|
SetKeyY(KeySlotID::NCCHSecure2, key_y_secondary);
|
||||||
|
if (!IsNormalKeyAvailable(KeySlotID::NCCHSecure2)) {
|
||||||
|
LOG_ERROR(Service_FS, "Secure2 KeyX missing");
|
||||||
|
failed_to_decrypt = true;
|
||||||
|
}
|
||||||
|
secondary_key = GetNormalKey(KeySlotID::NCCHSecure2);
|
||||||
|
break;
|
||||||
|
case 10:
|
||||||
|
LOG_DEBUG(Service_FS, "Secure3 crypto");
|
||||||
|
SetKeyY(KeySlotID::NCCHSecure3, key_y_secondary);
|
||||||
|
if (!IsNormalKeyAvailable(KeySlotID::NCCHSecure3)) {
|
||||||
|
LOG_ERROR(Service_FS, "Secure3 KeyX missing");
|
||||||
|
failed_to_decrypt = true;
|
||||||
|
}
|
||||||
|
secondary_key = GetNormalKey(KeySlotID::NCCHSecure3);
|
||||||
|
break;
|
||||||
|
case 11:
|
||||||
|
LOG_DEBUG(Service_FS, "Secure4 crypto");
|
||||||
|
SetKeyY(KeySlotID::NCCHSecure4, key_y_secondary);
|
||||||
|
if (!IsNormalKeyAvailable(KeySlotID::NCCHSecure4)) {
|
||||||
|
LOG_ERROR(Service_FS, "Secure4 KeyX missing");
|
||||||
|
failed_to_decrypt = true;
|
||||||
|
}
|
||||||
|
secondary_key = GetNormalKey(KeySlotID::NCCHSecure4);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find CTR for each section
|
||||||
|
// Written with reference to
|
||||||
|
// https://github.com/d0k3/GodMode9/blob/99af6a73be48fa7872649aaa7456136da0df7938/arm9/source/game/ncch.c#L34-L52
|
||||||
|
if (ncch_header.version == 0 || ncch_header.version == 2) {
|
||||||
|
LOG_DEBUG(Loader, "NCCH version 0/2");
|
||||||
|
// In this version, CTR for each section is a magic number prefixed by partition ID
|
||||||
|
// (reverse order)
|
||||||
|
std::reverse_copy(ncch_header.partition_id, ncch_header.partition_id + 8,
|
||||||
|
exheader_ctr.begin());
|
||||||
|
exefs_ctr = romfs_ctr = exheader_ctr;
|
||||||
|
exheader_ctr[8] = 1;
|
||||||
|
exefs_ctr[8] = 2;
|
||||||
|
romfs_ctr[8] = 3;
|
||||||
|
} else if (ncch_header.version == 1) {
|
||||||
|
LOG_DEBUG(Loader, "NCCH version 1");
|
||||||
|
// In this version, CTR for each section is the section offset prefixed by partition
|
||||||
|
// ID, as if the entire NCCH image is encrypted using a single CTR stream.
|
||||||
|
std::copy(ncch_header.partition_id, ncch_header.partition_id + 8,
|
||||||
|
exheader_ctr.begin());
|
||||||
|
exefs_ctr = romfs_ctr = exheader_ctr;
|
||||||
|
auto u32ToBEArray = [](u32 value) -> std::array<u8, 4> {
|
||||||
|
return std::array<u8, 4>{
|
||||||
|
static_cast<u8>(value >> 24),
|
||||||
|
static_cast<u8>((value >> 16) & 0xFF),
|
||||||
|
static_cast<u8>((value >> 8) & 0xFF),
|
||||||
|
static_cast<u8>(value & 0xFF),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
auto offset_exheader = u32ToBEArray(0x200); // exheader offset
|
||||||
|
auto offset_exefs = u32ToBEArray(ncch_header.exefs_offset * kBlockSize);
|
||||||
|
auto offset_romfs = u32ToBEArray(ncch_header.romfs_offset * kBlockSize);
|
||||||
|
std::copy(offset_exheader.begin(), offset_exheader.end(),
|
||||||
|
exheader_ctr.begin() + 12);
|
||||||
|
std::copy(offset_exefs.begin(), offset_exefs.end(), exefs_ctr.begin() + 12);
|
||||||
|
std::copy(offset_romfs.begin(), offset_romfs.end(), romfs_ctr.begin() + 12);
|
||||||
|
} else {
|
||||||
|
LOG_ERROR(Service_FS, "Unknown NCCH version {}", ncch_header.version);
|
||||||
|
failed_to_decrypt = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LOG_DEBUG(Service_FS, "No crypto");
|
||||||
|
is_encrypted = false;
|
||||||
|
}
|
||||||
|
|
||||||
if (ncch_header.content_size == file_size) {
|
if (ncch_header.content_size == file_size) {
|
||||||
// The NCCH is a proto version, which does not use media size units
|
// The NCCH is a proto version, which does not use media size units
|
||||||
@ -237,10 +380,12 @@ Loader::ResultStatus NCCHContainer::Load() {
|
|||||||
block_size = 1;
|
block_size = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef todotodo
|
||||||
if (!ncch_header.no_crypto) {
|
if (!ncch_header.no_crypto) {
|
||||||
// Encrypted NCCH are not supported
|
// Encrypted NCCH are not supported
|
||||||
return Loader::ResultStatus::ErrorEncrypted;
|
return Loader::ResultStatus::ErrorEncrypted;
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
// System archives and DLC don't have an extended header but have RomFS
|
// System archives and DLC don't have an extended header but have RomFS
|
||||||
// Proto apps don't have an ext header size
|
// Proto apps don't have an ext header size
|
||||||
@ -254,6 +399,26 @@ Loader::ResultStatus NCCHContainer::Load() {
|
|||||||
return Loader::ResultStatus::Error;
|
return Loader::ResultStatus::Error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (is_encrypted) {
|
||||||
|
// This ID check is masked to low 32-bit as a toleration to ill-formed ROM created
|
||||||
|
// by merging games and its updates.
|
||||||
|
if ((exheader_header.system_info.jump_id & 0xFFFFFFFF) ==
|
||||||
|
(ncch_header.program_id & 0xFFFFFFFF)) {
|
||||||
|
LOG_WARNING(Service_FS, "NCCH is marked as encrypted but with decrypted "
|
||||||
|
"exheader. Force no crypto scheme.");
|
||||||
|
is_encrypted = false;
|
||||||
|
} else {
|
||||||
|
if (failed_to_decrypt) {
|
||||||
|
LOG_ERROR(Service_FS, "Failed to decrypt");
|
||||||
|
return Loader::ResultStatus::ErrorEncrypted;
|
||||||
|
}
|
||||||
|
CryptoPP::byte* data = reinterpret_cast<CryptoPP::byte*>(&exheader_header);
|
||||||
|
CryptoPP::CTR_Mode<CryptoPP::AES>::Decryption(
|
||||||
|
primary_key.data(), primary_key.size(), exheader_ctr.data())
|
||||||
|
.ProcessData(data, data, sizeof(exheader_header));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const auto mods_path =
|
const auto mods_path =
|
||||||
fmt::format("{}mods/{:016X}/", FileUtil::GetUserPath(FileUtil::UserPath::LoadDir),
|
fmt::format("{}mods/{:016X}/", FileUtil::GetUserPath(FileUtil::UserPath::LoadDir),
|
||||||
GetModId(ncch_header.program_id));
|
GetModId(ncch_header.program_id));
|
||||||
@ -323,6 +488,7 @@ Loader::ResultStatus NCCHContainer::Load() {
|
|||||||
if (file->ReadBytes(&exefs_header, sizeof(ExeFs_Header)) != sizeof(ExeFs_Header))
|
if (file->ReadBytes(&exefs_header, sizeof(ExeFs_Header)) != sizeof(ExeFs_Header))
|
||||||
return Loader::ResultStatus::Error;
|
return Loader::ResultStatus::Error;
|
||||||
|
|
||||||
|
#ifdef todotodo
|
||||||
if (file->IsCrypto()) {
|
if (file->IsCrypto()) {
|
||||||
exefs_file = HW::UniqueData::OpenUniqueCryptoFile(
|
exefs_file = HW::UniqueData::OpenUniqueCryptoFile(
|
||||||
filepath, "rb", HW::UniqueData::UniqueCryptoFileID::NCCH);
|
filepath, "rb", HW::UniqueData::UniqueCryptoFileID::NCCH);
|
||||||
@ -330,6 +496,16 @@ Loader::ResultStatus NCCHContainer::Load() {
|
|||||||
exefs_file = std::make_unique<FileUtil::IOFile>(filepath, "rb");
|
exefs_file = std::make_unique<FileUtil::IOFile>(filepath, "rb");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#else
|
||||||
|
if (is_encrypted) {
|
||||||
|
CryptoPP::byte* data = reinterpret_cast<CryptoPP::byte*>(&exefs_header);
|
||||||
|
CryptoPP::CTR_Mode<CryptoPP::AES>::Decryption(primary_key.data(),
|
||||||
|
primary_key.size(), exefs_ctr.data())
|
||||||
|
.ProcessData(data, data, sizeof(exefs_header));
|
||||||
|
}
|
||||||
|
|
||||||
|
exefs_file = std::make_unique<FileUtil::IOFile>(filepath, "rb");
|
||||||
|
#endif
|
||||||
has_exefs = true;
|
has_exefs = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -457,6 +633,17 @@ Loader::ResultStatus NCCHContainer::LoadSectionExeFS(const char* name, std::vect
|
|||||||
: (section.offset + exefs_offset + sizeof(ExeFs_Header) + ncch_offset);
|
: (section.offset + exefs_offset + sizeof(ExeFs_Header) + ncch_offset);
|
||||||
exefs_file->Seek(section_offset, SEEK_SET);
|
exefs_file->Seek(section_offset, SEEK_SET);
|
||||||
|
|
||||||
|
std::array<u8, 16> key;
|
||||||
|
if (strcmp(section.name, "icon") == 0 || strcmp(section.name, "banner") == 0) {
|
||||||
|
key = primary_key;
|
||||||
|
} else {
|
||||||
|
key = secondary_key;
|
||||||
|
}
|
||||||
|
|
||||||
|
CryptoPP::CTR_Mode<CryptoPP::AES>::Decryption dec(key.data(), key.size(),
|
||||||
|
exefs_ctr.data());
|
||||||
|
dec.Seek(section.offset + sizeof(ExeFs_Header));
|
||||||
|
|
||||||
size_t section_size = is_proto ? Common::AlignUp(section.size, 0x10) : section.size;
|
size_t section_size = is_proto ? Common::AlignUp(section.size, 0x10) : section.size;
|
||||||
|
|
||||||
if (strcmp(section.name, ".code") == 0 && is_compressed) {
|
if (strcmp(section.name, ".code") == 0 && is_compressed) {
|
||||||
@ -466,6 +653,10 @@ Loader::ResultStatus NCCHContainer::LoadSectionExeFS(const char* name, std::vect
|
|||||||
temp_buffer.size())
|
temp_buffer.size())
|
||||||
return Loader::ResultStatus::Error;
|
return Loader::ResultStatus::Error;
|
||||||
|
|
||||||
|
if (is_encrypted) {
|
||||||
|
dec.ProcessData(&temp_buffer[0], &temp_buffer[0], section.size);
|
||||||
|
}
|
||||||
|
|
||||||
// Decompress .code section...
|
// Decompress .code section...
|
||||||
buffer.resize(LZSS_GetDecompressedSize(temp_buffer));
|
buffer.resize(LZSS_GetDecompressedSize(temp_buffer));
|
||||||
if (!LZSS_Decompress(temp_buffer, buffer)) {
|
if (!LZSS_Decompress(temp_buffer, buffer)) {
|
||||||
@ -476,6 +667,9 @@ Loader::ResultStatus NCCHContainer::LoadSectionExeFS(const char* name, std::vect
|
|||||||
buffer.resize(section_size);
|
buffer.resize(section_size);
|
||||||
if (exefs_file->ReadBytes(buffer.data(), section_size) != section_size)
|
if (exefs_file->ReadBytes(buffer.data(), section_size) != section_size)
|
||||||
return Loader::ResultStatus::Error;
|
return Loader::ResultStatus::Error;
|
||||||
|
if (is_encrypted) {
|
||||||
|
dec.ProcessData(buffer.data(), buffer.data(), section.size);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Loader::ResultStatus::Success;
|
return Loader::ResultStatus::Success;
|
||||||
@ -607,18 +801,34 @@ Loader::ResultStatus NCCHContainer::ReadRomFS(std::shared_ptr<RomFSReader>& romf
|
|||||||
|
|
||||||
// We reopen the file, to allow its position to be independent from file's
|
// We reopen the file, to allow its position to be independent from file's
|
||||||
std::unique_ptr<FileUtil::IOFile> romfs_file_inner;
|
std::unique_ptr<FileUtil::IOFile> romfs_file_inner;
|
||||||
|
#ifdef todotodo
|
||||||
if (file->IsCrypto()) {
|
if (file->IsCrypto()) {
|
||||||
romfs_file_inner = HW::UniqueData::OpenUniqueCryptoFile(
|
romfs_file_inner = HW::UniqueData::OpenUniqueCryptoFile(
|
||||||
filepath, "rb", HW::UniqueData::UniqueCryptoFileID::NCCH);
|
filepath, "rb", HW::UniqueData::UniqueCryptoFileID::NCCH);
|
||||||
} else {
|
} else {
|
||||||
romfs_file_inner = std::make_unique<FileUtil::IOFile>(filepath, "rb");
|
romfs_file_inner = std::make_unique<FileUtil::IOFile>(filepath, "rb");
|
||||||
}
|
}
|
||||||
|
#else
|
||||||
|
romfs_file_inner = std::make_unique<FileUtil::IOFile>(filepath, "rb");
|
||||||
|
#endif
|
||||||
|
|
||||||
if (!romfs_file_inner->IsOpen())
|
if (!romfs_file_inner->IsOpen())
|
||||||
return Loader::ResultStatus::Error;
|
return Loader::ResultStatus::Error;
|
||||||
|
|
||||||
|
#ifdef todotodo
|
||||||
std::shared_ptr<RomFSReader> direct_romfs =
|
std::shared_ptr<RomFSReader> direct_romfs =
|
||||||
std::make_shared<DirectRomFSReader>(std::move(romfs_file_inner), romfs_offset, romfs_size);
|
std::make_shared<DirectRomFSReader>(std::move(romfs_file_inner), romfs_offset, romfs_size);
|
||||||
|
#else
|
||||||
|
std::shared_ptr<RomFSReader> direct_romfs;
|
||||||
|
if (is_encrypted) {
|
||||||
|
direct_romfs =
|
||||||
|
std::make_shared<DirectRomFSReader>(std::move(romfs_file_inner), romfs_offset,
|
||||||
|
romfs_size, secondary_key, romfs_ctr, 0x1000);
|
||||||
|
} else {
|
||||||
|
direct_romfs = std::make_shared<DirectRomFSReader>(std::move(romfs_file_inner),
|
||||||
|
romfs_offset, romfs_size);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
const auto path =
|
const auto path =
|
||||||
fmt::format("{}mods/{:016X}/", FileUtil::GetUserPath(FileUtil::UserPath::LoadDir),
|
fmt::format("{}mods/{:016X}/", FileUtil::GetUserPath(FileUtil::UserPath::LoadDir),
|
||||||
@ -636,8 +846,10 @@ Loader::ResultStatus NCCHContainer::ReadRomFS(std::shared_ptr<RomFSReader>& romf
|
|||||||
}
|
}
|
||||||
|
|
||||||
Loader::ResultStatus NCCHContainer::DumpRomFS(const std::string& target_path) {
|
Loader::ResultStatus NCCHContainer::DumpRomFS(const std::string& target_path) {
|
||||||
|
#ifdef todotodo
|
||||||
if (file->IsCrypto())
|
if (file->IsCrypto())
|
||||||
return Loader::ResultStatus::ErrorEncrypted;
|
return Loader::ResultStatus::ErrorEncrypted;
|
||||||
|
#endif
|
||||||
|
|
||||||
std::shared_ptr<RomFSReader> direct_romfs;
|
std::shared_ptr<RomFSReader> direct_romfs;
|
||||||
Loader::ResultStatus result = ReadRomFS(direct_romfs, false);
|
Loader::ResultStatus result = ReadRomFS(direct_romfs, false);
|
||||||
|
|||||||
@ -348,6 +348,14 @@ private:
|
|||||||
bool is_loaded = false;
|
bool is_loaded = false;
|
||||||
bool is_compressed = false;
|
bool is_compressed = false;
|
||||||
|
|
||||||
|
bool is_encrypted = false;
|
||||||
|
// for decrypting exheader, exefs header and icon/banner section
|
||||||
|
std::array<u8, 16> primary_key{};
|
||||||
|
std::array<u8, 16> secondary_key{}; // for decrypting romfs and .code section
|
||||||
|
std::array<u8, 16> exheader_ctr{};
|
||||||
|
std::array<u8, 16> exefs_ctr{};
|
||||||
|
std::array<u8, 16> romfs_ctr{};
|
||||||
|
|
||||||
u32 ncch_offset = 0; // Offset to NCCH header, can be 0 for NCCHs or non-zero for CIAs/NCSDs
|
u32 ncch_offset = 0; // Offset to NCCH header, can be 0 for NCCHs or non-zero for CIAs/NCSDs
|
||||||
u32 exefs_offset = 0;
|
u32 exefs_offset = 0;
|
||||||
u32 partition = 0;
|
u32 partition = 0;
|
||||||
|
|||||||
@ -29,6 +29,11 @@ std::size_t DirectRomFSReader::ReadFile(std::size_t offset, std::size_t length,
|
|||||||
// Skip cache if the read is too big
|
// Skip cache if the read is too big
|
||||||
if (segments.size() == 1 && segments[0].second > cache_line_size) {
|
if (segments.size() == 1 && segments[0].second > cache_line_size) {
|
||||||
length = file->ReadAtBytes(buffer, length, file_offset + offset);
|
length = file->ReadAtBytes(buffer, length, file_offset + offset);
|
||||||
|
if (is_encrypted) {
|
||||||
|
CryptoPP::CTR_Mode<CryptoPP::AES>::Decryption d(key.data(), key.size(), ctr.data());
|
||||||
|
d.Seek(crypto_offset + offset);
|
||||||
|
d.ProcessData(buffer, buffer, length);
|
||||||
|
}
|
||||||
LOG_TRACE(Service_FS, "RomFS Cache SKIP: offset={}, length={}", offset, length);
|
LOG_TRACE(Service_FS, "RomFS Cache SKIP: offset={}, length={}", offset, length);
|
||||||
return length;
|
return length;
|
||||||
}
|
}
|
||||||
@ -43,6 +48,11 @@ std::size_t DirectRomFSReader::ReadFile(std::size_t offset, std::size_t length,
|
|||||||
if (!cache_entry.first) {
|
if (!cache_entry.first) {
|
||||||
// If not found, read from disk and cache the data
|
// If not found, read from disk and cache the data
|
||||||
read_size = file->ReadAtBytes(cache_entry.second.data(), read_size, file_offset + page);
|
read_size = file->ReadAtBytes(cache_entry.second.data(), read_size, file_offset + page);
|
||||||
|
if (is_encrypted && read_size) {
|
||||||
|
CryptoPP::CTR_Mode<CryptoPP::AES>::Decryption d(key.data(), key.size(), ctr.data());
|
||||||
|
d.Seek(crypto_offset + page);
|
||||||
|
d.ProcessData(cache_entry.second.data(), cache_entry.second.data(), read_size);
|
||||||
|
}
|
||||||
LOG_TRACE(Service_FS, "RomFS Cache MISS: page={}, length={}, into={}", page, seg.second,
|
LOG_TRACE(Service_FS, "RomFS Cache MISS: page={}, length={}, into={}", page, seg.second,
|
||||||
(seg.first - page));
|
(seg.first - page));
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -47,7 +47,13 @@ class DirectRomFSReader : public RomFSReader {
|
|||||||
public:
|
public:
|
||||||
DirectRomFSReader(std::unique_ptr<FileUtil::IOFile>&& file, std::size_t file_offset,
|
DirectRomFSReader(std::unique_ptr<FileUtil::IOFile>&& file, std::size_t file_offset,
|
||||||
std::size_t data_size)
|
std::size_t data_size)
|
||||||
: file(std::move(file)), file_offset(file_offset), data_size(data_size) {}
|
: is_encrypted(false), file(std::move(file)), file_offset(file_offset), data_size(data_size) {}
|
||||||
|
|
||||||
|
DirectRomFSReader(std::unique_ptr<FileUtil::IOFile>&& file, std::size_t file_offset, std::size_t data_size,
|
||||||
|
const std::array<u8, 16>& key, const std::array<u8, 16>& ctr,
|
||||||
|
std::size_t crypto_offset)
|
||||||
|
: is_encrypted(true), file(std::move(file)), key(key), ctr(ctr), file_offset(file_offset),
|
||||||
|
crypto_offset(crypto_offset), data_size(data_size) {}
|
||||||
|
|
||||||
~DirectRomFSReader() override = default;
|
~DirectRomFSReader() override = default;
|
||||||
|
|
||||||
@ -62,8 +68,12 @@ public:
|
|||||||
bool CacheReady(std::size_t file_offset, std::size_t length) override;
|
bool CacheReady(std::size_t file_offset, std::size_t length) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
bool is_encrypted;
|
||||||
std::unique_ptr<FileUtil::IOFile> file;
|
std::unique_ptr<FileUtil::IOFile> file;
|
||||||
|
std::array<u8, 16> key;
|
||||||
|
std::array<u8, 16> ctr;
|
||||||
u64 file_offset;
|
u64 file_offset;
|
||||||
|
u64 crypto_offset;
|
||||||
u64 data_size;
|
u64 data_size;
|
||||||
|
|
||||||
// Total cache size: 128KB
|
// Total cache size: 128KB
|
||||||
@ -86,8 +96,12 @@ private:
|
|||||||
template <class Archive>
|
template <class Archive>
|
||||||
void serialize(Archive& ar, const unsigned int) {
|
void serialize(Archive& ar, const unsigned int) {
|
||||||
ar& boost::serialization::base_object<RomFSReader>(*this);
|
ar& boost::serialization::base_object<RomFSReader>(*this);
|
||||||
|
ar & is_encrypted;
|
||||||
ar & file;
|
ar & file;
|
||||||
|
ar & key;
|
||||||
|
ar & ctr;
|
||||||
ar & file_offset;
|
ar & file_offset;
|
||||||
|
ar & crypto_offset;
|
||||||
ar & data_size;
|
ar & data_size;
|
||||||
}
|
}
|
||||||
friend class boost::serialization::access;
|
friend class boost::serialization::access;
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "common/assert.h"
|
||||||
#include "common/common_types.h"
|
#include "common/common_types.h"
|
||||||
#include "common/logging/log.h"
|
#include "common/logging/log.h"
|
||||||
|
|
||||||
|
|||||||
@ -64,7 +64,9 @@ Loader::ResultStatus Ticket::DoTitlekeyFixup() {
|
|||||||
|
|
||||||
Loader::ResultStatus Ticket::Load(std::span<const u8> file_data, std::size_t offset) {
|
Loader::ResultStatus Ticket::Load(std::span<const u8> file_data, std::size_t offset) {
|
||||||
std::size_t total_size = static_cast<std::size_t>(file_data.size() - offset);
|
std::size_t total_size = static_cast<std::size_t>(file_data.size() - offset);
|
||||||
|
/* todotodo
|
||||||
serialized_size = total_size;
|
serialized_size = total_size;
|
||||||
|
*/
|
||||||
if (total_size < sizeof(u32))
|
if (total_size < sizeof(u32))
|
||||||
return Loader::ResultStatus::Error;
|
return Loader::ResultStatus::Error;
|
||||||
|
|
||||||
@ -88,6 +90,7 @@ Loader::ResultStatus Ticket::Load(std::span<const u8> file_data, std::size_t off
|
|||||||
std::memcpy(ticket_signature.data(), &file_data[offset + sizeof(u32)], signature_size);
|
std::memcpy(ticket_signature.data(), &file_data[offset + sizeof(u32)], signature_size);
|
||||||
std::memcpy(&ticket_body, &file_data[offset + body_start], sizeof(Body));
|
std::memcpy(&ticket_body, &file_data[offset + body_start], sizeof(Body));
|
||||||
|
|
||||||
|
/* todotodo
|
||||||
std::size_t content_index_start = body_end;
|
std::size_t content_index_start = body_end;
|
||||||
if (total_size < content_index_start + (2 * sizeof(u32)))
|
if (total_size < content_index_start + (2 * sizeof(u32)))
|
||||||
return Loader::ResultStatus::Error;
|
return Loader::ResultStatus::Error;
|
||||||
@ -103,6 +106,8 @@ Loader::ResultStatus Ticket::Load(std::span<const u8> file_data, std::size_t off
|
|||||||
content_index.resize(content_index_size);
|
content_index.resize(content_index_size);
|
||||||
std::memcpy(content_index.data(), &file_data[offset + content_index_start], content_index_size);
|
std::memcpy(content_index.data(), &file_data[offset + content_index_start], content_index_size);
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
return Loader::ResultStatus::Success;
|
return Loader::ResultStatus::Success;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -43,8 +43,9 @@ public:
|
|||||||
u8 audit;
|
u8 audit;
|
||||||
INSERT_PADDING_BYTES(0x42);
|
INSERT_PADDING_BYTES(0x42);
|
||||||
std::array<u8, 0x40> limits;
|
std::array<u8, 0x40> limits;
|
||||||
|
std::array<u8, 0xAC> content_index;
|
||||||
};
|
};
|
||||||
static_assert(sizeof(Body) == 0x164, "Ticket body structure size is wrong");
|
static_assert(sizeof(Body) == 0x210, "Ticket body structure size is wrong");
|
||||||
#pragma pack(pop)
|
#pragma pack(pop)
|
||||||
|
|
||||||
Loader::ResultStatus DoTitlekeyFixup();
|
Loader::ResultStatus DoTitlekeyFixup();
|
||||||
|
|||||||
@ -44,12 +44,22 @@ Result ErrEula::ReceiveParameterImpl(const Service::APT::MessageParameter& param
|
|||||||
}
|
}
|
||||||
|
|
||||||
Result ErrEula::Start(const Service::APT::MessageParameter& parameter) {
|
Result ErrEula::Start(const Service::APT::MessageParameter& parameter) {
|
||||||
|
#ifdef todotodo
|
||||||
memcpy(¶m, parameter.buffer.data(), std::min(parameter.buffer.size(), sizeof(param)));
|
memcpy(¶m, parameter.buffer.data(), std::min(parameter.buffer.size(), sizeof(param)));
|
||||||
|
|
||||||
// Do something here, like showing error codes, or prompting for EULA agreement.
|
// Do something here, like showing error codes, or prompting for EULA agreement.
|
||||||
if (param.type == DisplayType::Agree) {
|
if (param.type == DisplayType::Agree) {
|
||||||
param.result = 1;
|
param.result = 1;
|
||||||
}
|
}
|
||||||
|
#else
|
||||||
|
startup_param = parameter.buffer;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
//--
|
||||||
|
// TODO(Subv): Set the expected fields in the response buffer before resending it to the
|
||||||
|
// application.
|
||||||
|
// TODO(Subv): Reverse the parameter format for the ErrEula applet
|
||||||
|
//--
|
||||||
|
|
||||||
// Let the application know that we're closing.
|
// Let the application know that we're closing.
|
||||||
Finalize();
|
Finalize();
|
||||||
@ -57,8 +67,13 @@ Result ErrEula::Start(const Service::APT::MessageParameter& parameter) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Result ErrEula::Finalize() {
|
Result ErrEula::Finalize() {
|
||||||
|
#ifdef todotodo
|
||||||
std::vector<u8> buffer(sizeof(param));
|
std::vector<u8> buffer(sizeof(param));
|
||||||
memcpy(buffer.data(), ¶m, buffer.size());
|
memcpy(buffer.data(), ¶m, buffer.size());
|
||||||
|
#else
|
||||||
|
std::vector<u8> buffer(startup_param.size());
|
||||||
|
std::fill(buffer.begin(), buffer.end(), 0);
|
||||||
|
#endif
|
||||||
CloseApplet(nullptr, buffer);
|
CloseApplet(nullptr, buffer);
|
||||||
return ResultSuccess;
|
return ResultSuccess;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -54,7 +54,8 @@ private:
|
|||||||
std::shared_ptr<Kernel::SharedMemory> framebuffer_memory;
|
std::shared_ptr<Kernel::SharedMemory> framebuffer_memory;
|
||||||
|
|
||||||
/// Parameter received by the applet on start.
|
/// Parameter received by the applet on start.
|
||||||
ErrEulaParam param{};
|
// ErrEulaParam param{};
|
||||||
|
std::vector<u8> startup_param;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace HLE::Applets
|
} // namespace HLE::Applets
|
||||||
|
|||||||
@ -205,12 +205,21 @@ Result TranslateCommandBuffer(Kernel::KernelSystem& kernel, Memory::MemorySystem
|
|||||||
buffer->GetPtr() + Memory::CITRA_PAGE_SIZE + page_offset, size);
|
buffer->GetPtr() + Memory::CITRA_PAGE_SIZE + page_offset, size);
|
||||||
|
|
||||||
// Map the guard pages and mapped pages at once.
|
// Map the guard pages and mapped pages at once.
|
||||||
|
#ifdef todotodo
|
||||||
auto target_address_result = dst_process->vm_manager.MapBackingMemoryToBase(
|
auto target_address_result = dst_process->vm_manager.MapBackingMemoryToBase(
|
||||||
Memory::IPC_MAPPING_VADDR, Memory::IPC_MAPPING_SIZE, buffer,
|
Memory::IPC_MAPPING_VADDR, Memory::IPC_MAPPING_SIZE, buffer,
|
||||||
static_cast<u32>(buffer->GetSize()), Kernel::MemoryState::Shared);
|
static_cast<u32>(buffer->GetSize()), Kernel::MemoryState::Shared);
|
||||||
|
|
||||||
ASSERT_MSG(target_address_result.Succeeded(), "Failed to map target address");
|
ASSERT_MSG(target_address_result.Succeeded(), "Failed to map target address");
|
||||||
target_address = target_address_result.Unwrap();
|
target_address = target_address_result.Unwrap();
|
||||||
|
#else
|
||||||
|
target_address =
|
||||||
|
dst_process->vm_manager
|
||||||
|
.MapBackingMemoryToBase(Memory::IPC_MAPPING_VADDR, Memory::IPC_MAPPING_SIZE,
|
||||||
|
buffer, static_cast<u32>(buffer->GetSize()),
|
||||||
|
Kernel::MemoryState::Shared)
|
||||||
|
.Unwrap();
|
||||||
|
#endif
|
||||||
|
|
||||||
// Change the permissions and state of the guard pages.
|
// Change the permissions and state of the guard pages.
|
||||||
const VAddr low_guard_address = target_address;
|
const VAddr low_guard_address = target_address;
|
||||||
|
|||||||
@ -47,6 +47,9 @@ union BatteryState {
|
|||||||
|
|
||||||
using MacAddress = std::array<u8, 6>;
|
using MacAddress = std::array<u8, 6>;
|
||||||
|
|
||||||
|
// Default MAC address in the Nintendo 3DS range
|
||||||
|
constexpr MacAddress DefaultMac = {0x40, 0xF4, 0x07, 0x00, 0x00, 0x00};
|
||||||
|
|
||||||
enum class WifiLinkLevel : u8 {
|
enum class WifiLinkLevel : u8 {
|
||||||
Off = 0,
|
Off = 0,
|
||||||
Poor = 1,
|
Poor = 1,
|
||||||
|
|||||||
@ -106,10 +106,26 @@ void Module::Interface::GetStatus(Kernel::HLERequestContext& ctx) {
|
|||||||
|
|
||||||
void Module::Interface::GetWifiStatus(Kernel::HLERequestContext& ctx) {
|
void Module::Interface::GetWifiStatus(Kernel::HLERequestContext& ctx) {
|
||||||
IPC::RequestParser rp(ctx);
|
IPC::RequestParser rp(ctx);
|
||||||
|
/* todotodo
|
||||||
|
*/
|
||||||
|
//--
|
||||||
|
bool can_reach_internet = false;
|
||||||
|
|
||||||
|
std::shared_ptr<SOC::SOC_U> socu_module = SOC::GetService(ac->system);
|
||||||
|
if (socu_module) {
|
||||||
|
can_reach_internet = socu_module->GetDefaultInterfaceInfo().has_value();
|
||||||
|
}
|
||||||
|
//--
|
||||||
IPC::RequestBuilder rb = rp.MakeBuilder(2, 0);
|
IPC::RequestBuilder rb = rp.MakeBuilder(2, 0);
|
||||||
rb.Push(ResultSuccess);
|
rb.Push(ResultSuccess);
|
||||||
|
#ifdef todotodo
|
||||||
rb.Push<u32>(static_cast<u32>(WifiStatus::STATUS_CONNECTED_SLOT1));
|
rb.Push<u32>(static_cast<u32>(WifiStatus::STATUS_CONNECTED_SLOT1));
|
||||||
|
#else
|
||||||
|
rb.Push<u32>(static_cast<u32>(can_reach_internet ? (Settings::values.is_new_3ds
|
||||||
|
? WifiStatus::STATUS_CONNECTED_N3DS
|
||||||
|
: WifiStatus::STATUS_CONNECTED_O3DS)
|
||||||
|
: WifiStatus::STATUS_DISCONNECTED));
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void Module::Interface::GetInfraPriority(Kernel::HLERequestContext& ctx) {
|
void Module::Interface::GetInfraPriority(Kernel::HLERequestContext& ctx) {
|
||||||
|
|||||||
@ -90,7 +90,7 @@ public:
|
|||||||
* AC::GetWifiStatus service function
|
* AC::GetWifiStatus service function
|
||||||
* Outputs:
|
* Outputs:
|
||||||
* 1 : Result of function, 0 on success, otherwise error code
|
* 1 : Result of function, 0 on success, otherwise error code
|
||||||
* 2 : WifiStatus
|
* 2 : Output connection type, 0 = none, 1 = Old3DS Internet, 2 = New3DS Internet.
|
||||||
*/
|
*/
|
||||||
void GetWifiStatus(Kernel::HLERequestContext& ctx);
|
void GetWifiStatus(Kernel::HLERequestContext& ctx);
|
||||||
|
|
||||||
@ -169,9 +169,14 @@ protected:
|
|||||||
};
|
};
|
||||||
enum class WifiStatus {
|
enum class WifiStatus {
|
||||||
STATUS_DISCONNECTED = 0,
|
STATUS_DISCONNECTED = 0,
|
||||||
|
#ifdef todotodo
|
||||||
STATUS_CONNECTED_SLOT1 = (1 << 0),
|
STATUS_CONNECTED_SLOT1 = (1 << 0),
|
||||||
STATUS_CONNECTED_SLOT2 = (1 << 1),
|
STATUS_CONNECTED_SLOT2 = (1 << 1),
|
||||||
STATUS_CONNECTED_SLOT3 = (1 << 2),
|
STATUS_CONNECTED_SLOT3 = (1 << 2),
|
||||||
|
#else
|
||||||
|
STATUS_CONNECTED_O3DS = 1,
|
||||||
|
STATUS_CONNECTED_N3DS = 2,
|
||||||
|
#endif
|
||||||
};
|
};
|
||||||
|
|
||||||
struct ACConfig {
|
struct ACConfig {
|
||||||
|
|||||||
@ -18,6 +18,7 @@ enum {
|
|||||||
NotInitialized = 101,
|
NotInitialized = 101,
|
||||||
AlreadyInitialized = 102,
|
AlreadyInitialized = 102,
|
||||||
AcStatusDisconnected = 103,
|
AcStatusDisconnected = 103,
|
||||||
|
ErrDesc103 = 103,
|
||||||
ErrDesc104 = 104,
|
ErrDesc104 = 104,
|
||||||
Busy = 111,
|
Busy = 111,
|
||||||
ErrDesc112 = 112,
|
ErrDesc112 = 112,
|
||||||
@ -317,6 +318,7 @@ enum {
|
|||||||
NotInitialized = 220501, // 022-0501
|
NotInitialized = 220501, // 022-0501
|
||||||
AlreadyInitialized = 220502, // 022-0502
|
AlreadyInitialized = 220502, // 022-0502
|
||||||
AcStatusDisconnected = 225103, // 022-5103
|
AcStatusDisconnected = 225103, // 022-5103
|
||||||
|
ErrCode225103 = 225103, // 022-5103
|
||||||
ErrCode225104 = 225104, // 022-5104
|
ErrCode225104 = 225104, // 022-5104
|
||||||
Busy = 220511, // 022-0511
|
Busy = 220511, // 022-0511
|
||||||
ErrCode225112 = 225112, // 022-5112
|
ErrCode225112 = 225112, // 022-5112
|
||||||
|
|||||||
@ -89,12 +89,42 @@ struct TicketInfo {
|
|||||||
|
|
||||||
static_assert(sizeof(TicketInfo) == 0x18, "Ticket info structure size is wrong");
|
static_assert(sizeof(TicketInfo) == 0x18, "Ticket info structure size is wrong");
|
||||||
|
|
||||||
|
bool CTCert::IsValid() const {
|
||||||
|
constexpr std::string_view expected_issuer_prod = "Nintendo CA - G3_NintendoCTR2prod";
|
||||||
|
constexpr std::string_view expected_issuer_dev = "Nintendo CA - G3_NintendoCTR2dev";
|
||||||
|
constexpr u32 expected_signature_type = 0x010005;
|
||||||
|
|
||||||
|
return signature_type == expected_signature_type &&
|
||||||
|
(std::string(issuer.data()) == expected_issuer_prod ||
|
||||||
|
std::string(issuer.data()) == expected_issuer_dev);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
u32 CTCert::GetDeviceID() const {
|
||||||
|
constexpr std::string_view key_id_prefix = "CT";
|
||||||
|
|
||||||
|
const std::string key_id_str(key_id.data());
|
||||||
|
if (key_id_str.starts_with(key_id_prefix)) {
|
||||||
|
const std::string device_id =
|
||||||
|
key_id_str.substr(key_id_prefix.size(), key_id_str.find('-') - key_id_prefix.size());
|
||||||
|
char* end_ptr;
|
||||||
|
const u32 device_id_value = std::strtoul(device_id.c_str(), &end_ptr, 16);
|
||||||
|
if (*end_ptr == '\0') {
|
||||||
|
return device_id_value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Error
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
class CIAFile::DecryptionState {
|
class CIAFile::DecryptionState {
|
||||||
public:
|
public:
|
||||||
std::vector<CryptoPP::CBC_Mode<CryptoPP::AES>::Decryption> content;
|
std::vector<CryptoPP::CBC_Mode<CryptoPP::AES>::Decryption> content;
|
||||||
};
|
};
|
||||||
|
|
||||||
NCCHCryptoFile::NCCHCryptoFile(const std::string& out_file, bool encrypted_content) {
|
NCCHCryptoFile::NCCHCryptoFile(const std::string& out_file, bool encrypted_content) {
|
||||||
|
#ifdef todotodo
|
||||||
if (encrypted_content) {
|
if (encrypted_content) {
|
||||||
// A console unique crypto file is used to store the decrypted NCCH file. This is done
|
// A console unique crypto file is used to store the decrypted NCCH file. This is done
|
||||||
// to prevent Azahar being used as a tool to download easy shareable decrypted contents
|
// to prevent Azahar being used as a tool to download easy shareable decrypted contents
|
||||||
@ -108,15 +138,20 @@ NCCHCryptoFile::NCCHCryptoFile(const std::string& out_file, bool encrypted_conte
|
|||||||
if (!file->IsOpen()) {
|
if (!file->IsOpen()) {
|
||||||
is_error = true;
|
is_error = true;
|
||||||
}
|
}
|
||||||
|
#else
|
||||||
|
file = std::make_unique<FileUtil::IOFile>(out_file, "wb");
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void NCCHCryptoFile::Write(const u8* buffer, std::size_t length) {
|
void NCCHCryptoFile::Write(const u8* buffer, std::size_t length) {
|
||||||
if (is_error)
|
if (is_error)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
#ifdef todotodo
|
||||||
if (is_not_ncch) {
|
if (is_not_ncch) {
|
||||||
file->WriteBytes(buffer, length);
|
file->WriteBytes(buffer, length);
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
const int kBlockSize = 0x200; ///< Size of ExeFS blocks (in bytes)
|
const int kBlockSize = 0x200; ///< Size of ExeFS blocks (in bytes)
|
||||||
|
|
||||||
@ -130,10 +165,14 @@ void NCCHCryptoFile::Write(const u8* buffer, std::size_t length) {
|
|||||||
|
|
||||||
if (!header_parsed && header_size == sizeof(NCCH_Header)) {
|
if (!header_parsed && header_size == sizeof(NCCH_Header)) {
|
||||||
if (Loader::MakeMagic('N', 'C', 'C', 'H') != ncch_header.magic) {
|
if (Loader::MakeMagic('N', 'C', 'C', 'H') != ncch_header.magic) {
|
||||||
|
#ifdef todotodo
|
||||||
// Most likely DS contents, store without additional operations
|
// Most likely DS contents, store without additional operations
|
||||||
is_not_ncch = true;
|
is_not_ncch = true;
|
||||||
file->WriteBytes(&ncch_header, sizeof(ncch_header));
|
file->WriteBytes(&ncch_header, sizeof(ncch_header));
|
||||||
file->WriteBytes(buffer, length);
|
file->WriteBytes(buffer, length);
|
||||||
|
#else
|
||||||
|
is_error = true;
|
||||||
|
#endif
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -420,7 +459,7 @@ void AuthorizeCIAFileDecryption(CIAFile* cia_file, Kernel::HLERequestContext& ct
|
|||||||
}
|
}
|
||||||
|
|
||||||
CIAFile::CIAFile(Core::System& system_, Service::FS::MediaType media_type, bool from_cdn_)
|
CIAFile::CIAFile(Core::System& system_, Service::FS::MediaType media_type, bool from_cdn_)
|
||||||
: system(system_), from_cdn(from_cdn_), decryption_authorized(false), media_type(media_type),
|
: system(system_), from_cdn(from_cdn_), decryption_authorized(true), media_type(media_type),
|
||||||
decryption_state(std::make_unique<DecryptionState>()) {
|
decryption_state(std::make_unique<DecryptionState>()) {
|
||||||
// If data is being installing from CDN, provide a fake header to the container so that
|
// If data is being installing from CDN, provide a fake header to the container so that
|
||||||
// it's not uninitialized.
|
// it's not uninitialized.
|
||||||
@ -457,6 +496,7 @@ Result CIAFile::WriteTicket() {
|
|||||||
ErrorLevel::Permanent};
|
ErrorLevel::Permanent};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef todotodo
|
||||||
const auto& ticket = container.GetTicket();
|
const auto& ticket = container.GetTicket();
|
||||||
const auto ticket_path = GetTicketPath(ticket.GetTitleID(), ticket.GetTicketID());
|
const auto ticket_path = GetTicketPath(ticket.GetTitleID(), ticket.GetTicketID());
|
||||||
|
|
||||||
@ -471,6 +511,7 @@ Result CIAFile::WriteTicket() {
|
|||||||
// TODO: Correct result code.
|
// TODO: Correct result code.
|
||||||
return FileSys::ResultFileNotFound;
|
return FileSys::ResultFileNotFound;
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
install_state = CIAInstallState::TicketLoaded;
|
install_state = CIAInstallState::TicketLoaded;
|
||||||
return ResultSuccess;
|
return ResultSuccess;
|
||||||
@ -519,12 +560,25 @@ Result CIAFile::WriteTitleMetadata(std::span<const u8> tmd_data, std::size_t off
|
|||||||
auto content_count = container.GetTitleMetadata().GetContentCount();
|
auto content_count = container.GetTitleMetadata().GetContentCount();
|
||||||
content_written.resize(content_count);
|
content_written.resize(content_count);
|
||||||
|
|
||||||
|
#ifdef todotodo
|
||||||
current_content_file.reset();
|
current_content_file.reset();
|
||||||
current_content_index = -1;
|
current_content_index = -1;
|
||||||
content_file_paths.clear();
|
content_file_paths.clear();
|
||||||
|
#else
|
||||||
|
content_files.clear();
|
||||||
|
#endif
|
||||||
for (std::size_t i = 0; i < content_count; i++) {
|
for (std::size_t i = 0; i < content_count; i++) {
|
||||||
auto path = GetTitleContentPath(media_type, tmd.GetTitleID(), i, is_update);
|
auto path = GetTitleContentPath(media_type, tmd.GetTitleID(), i, is_update);
|
||||||
|
#ifdef todotodo
|
||||||
content_file_paths.emplace_back(path);
|
content_file_paths.emplace_back(path);
|
||||||
|
#else
|
||||||
|
auto& file = content_files.emplace_back(path, "wb");
|
||||||
|
if (!file.IsOpen()) {
|
||||||
|
LOG_ERROR(Service_AM, "Could not open output file '{}' for content {}.", path, i);
|
||||||
|
// TODO: Correct error code.
|
||||||
|
return FileSys::ResultFileNotFound;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
if (container.GetTitleMetadata().HasEncryptedContent()) {
|
if (container.GetTitleMetadata().HasEncryptedContent()) {
|
||||||
@ -561,6 +615,7 @@ ResultVal<std::size_t> CIAFile::WriteContentData(u64 offset, std::size_t length,
|
|||||||
// has been written since we might get a written buffer which contains multiple .app
|
// has been written since we might get a written buffer which contains multiple .app
|
||||||
// contents or only part of a larger .app's contents.
|
// contents or only part of a larger .app's contents.
|
||||||
const u64 offset_max = offset + length;
|
const u64 offset_max = offset + length;
|
||||||
|
bool success = true;
|
||||||
for (std::size_t i = 0; i < content_written.size(); i++) {
|
for (std::size_t i = 0; i < content_written.size(); i++) {
|
||||||
if (content_written[i] < container.GetContentSize(i)) {
|
if (content_written[i] < container.GetContentSize(i)) {
|
||||||
// The size, minimum unwritten offset, and maximum unwritten offset of this content
|
// The size, minimum unwritten offset, and maximum unwritten offset of this content
|
||||||
@ -579,6 +634,7 @@ ResultVal<std::size_t> CIAFile::WriteContentData(u64 offset, std::size_t length,
|
|||||||
|
|
||||||
// Since the incoming TMD has already been written, we can use GetTitleContentPath
|
// Since the incoming TMD has already been written, we can use GetTitleContentPath
|
||||||
// to get the content paths to write to.
|
// to get the content paths to write to.
|
||||||
|
#ifdef todotodo
|
||||||
const FileSys::TitleMetadata& tmd = container.GetTitleMetadata();
|
const FileSys::TitleMetadata& tmd = container.GetTitleMetadata();
|
||||||
if (i != current_content_index) {
|
if (i != current_content_index) {
|
||||||
current_content_index = static_cast<u16>(i);
|
current_content_index = static_cast<u16>(i);
|
||||||
@ -588,6 +644,10 @@ ResultVal<std::size_t> CIAFile::WriteContentData(u64 offset, std::size_t length,
|
|||||||
}
|
}
|
||||||
auto& file = *current_content_file;
|
auto& file = *current_content_file;
|
||||||
|
|
||||||
|
#else
|
||||||
|
FileSys::TitleMetadata tmd = container.GetTitleMetadata();
|
||||||
|
auto& file = content_files[i];
|
||||||
|
#endif
|
||||||
std::vector<u8> temp(buffer + (range_min - offset),
|
std::vector<u8> temp(buffer + (range_min - offset),
|
||||||
buffer + (range_min - offset) + available_to_write);
|
buffer + (range_min - offset) + available_to_write);
|
||||||
|
|
||||||
@ -595,12 +655,7 @@ ResultVal<std::size_t> CIAFile::WriteContentData(u64 offset, std::size_t length,
|
|||||||
decryption_state->content[i].ProcessData(temp.data(), temp.data(), temp.size());
|
decryption_state->content[i].ProcessData(temp.data(), temp.data(), temp.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
file.Write(temp.data(), temp.size());
|
file.WriteBytes(temp.data(), temp.size());
|
||||||
if (file.IsError()) {
|
|
||||||
// This can never happen in real HW
|
|
||||||
return Result(ErrCodes::InvalidImportState, ErrorModule::AM,
|
|
||||||
ErrorSummary::InvalidState, ErrorLevel::Permanent);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keep tabs on how much of this content ID has been written so new range_min
|
// Keep tabs on how much of this content ID has been written so new range_min
|
||||||
// values can be calculated.
|
// values can be calculated.
|
||||||
@ -610,7 +665,7 @@ ResultVal<std::size_t> CIAFile::WriteContentData(u64 offset, std::size_t length,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return length;
|
return success ? length : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
ResultVal<std::size_t> CIAFile::Write(u64 offset, std::size_t length, bool flush,
|
ResultVal<std::size_t> CIAFile::Write(u64 offset, std::size_t length, bool flush,
|
||||||
@ -732,17 +787,13 @@ ResultVal<std::size_t> CIAFile::WriteContentDataIndexed(u16 content_index, u64 o
|
|||||||
}
|
}
|
||||||
|
|
||||||
file.Write(temp.data(), temp.size());
|
file.Write(temp.data(), temp.size());
|
||||||
if (file.IsError()) {
|
bool success = !file.IsError();
|
||||||
// This can never happen in real HW
|
|
||||||
return Result(ErrCodes::InvalidImportState, ErrorModule::AM, ErrorSummary::InvalidState,
|
|
||||||
ErrorLevel::Permanent);
|
|
||||||
}
|
|
||||||
|
|
||||||
content_written[content_index] += temp.size();
|
content_written[content_index] += temp.size();
|
||||||
LOG_DEBUG(Service_AM, "Wrote {} to content {}, total {}", temp.size(), content_index,
|
LOG_DEBUG(Service_AM, "Wrote {} to content {}, total {}", temp.size(), content_index,
|
||||||
content_written[content_index]);
|
content_written[content_index]);
|
||||||
|
|
||||||
return temp.size();
|
return success ? temp.size() : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
u64 CIAFile::GetSize() const {
|
u64 CIAFile::GetSize() const {
|
||||||
@ -848,6 +899,12 @@ bool TicketFile::SetSize(u64 size) const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool TicketFile::Close() {
|
bool TicketFile::Close() {
|
||||||
|
FileSys::Ticket ticket;
|
||||||
|
if (ticket.Load(data, 0) == Loader::ResultStatus::Success) {
|
||||||
|
LOG_WARNING(Service_AM, "Discarding ticket for {:#016X}.", ticket.GetTitleID());
|
||||||
|
} else {
|
||||||
|
LOG_ERROR(Service_AM, "Invalid ticket provided to TicketFile.");
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -865,6 +922,11 @@ Result TicketFile::Commit() {
|
|||||||
ticket_id = ticket.GetTicketID();
|
ticket_id = ticket.GetTicketID();
|
||||||
const auto ticket_path = GetTicketPath(ticket.GetTitleID(), ticket.GetTicketID());
|
const auto ticket_path = GetTicketPath(ticket.GetTitleID(), ticket.GetTicketID());
|
||||||
|
|
||||||
|
// Create ticket folder if it does not exist
|
||||||
|
std::string ticket_folder;
|
||||||
|
Common::SplitPath(ticket_path, &ticket_folder, nullptr, nullptr);
|
||||||
|
FileUtil::CreateFullPath(ticket_folder);
|
||||||
|
|
||||||
// Save ticket
|
// Save ticket
|
||||||
if (ticket.Save(ticket_path) != Loader::ResultStatus::Success) {
|
if (ticket.Save(ticket_path) != Loader::ResultStatus::Success) {
|
||||||
LOG_ERROR(Service_AM, "Failed to install ticket provided to TicketFile.");
|
LOG_ERROR(Service_AM, "Failed to install ticket provided to TicketFile.");
|
||||||
@ -964,8 +1026,10 @@ InstallStatus InstallCIA(const std::string& path,
|
|||||||
Core::System::GetInstance(),
|
Core::System::GetInstance(),
|
||||||
Service::AM::GetTitleMediaType(container.GetTitleMetadata().GetTitleID()));
|
Service::AM::GetTitleMediaType(container.GetTitleMetadata().GetTitleID()));
|
||||||
|
|
||||||
if (container.GetTitleMetadata().HasEncryptedContent()) {
|
bool title_key_available = container.GetTicket().GetTitleKey().has_value();
|
||||||
LOG_ERROR(Service_AM, "File {} is encrypted! Aborting...", path);
|
if (!title_key_available && container.GetTitleMetadata().HasEncryptedContent()) {
|
||||||
|
LOG_ERROR(Service_AM, "File {} is encrypted and no title key is available! Aborting...",
|
||||||
|
path);
|
||||||
return InstallStatus::ErrorEncrypted;
|
return InstallStatus::ErrorEncrypted;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -975,8 +1039,12 @@ InstallStatus InstallCIA(const std::string& path,
|
|||||||
return InstallStatus::ErrorFailedToOpenFile;
|
return InstallStatus::ErrorFailedToOpenFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef todotodo
|
||||||
std::vector<u8> buffer;
|
std::vector<u8> buffer;
|
||||||
buffer.resize(0x10000);
|
buffer.resize(0x10000);
|
||||||
|
#else
|
||||||
|
std::array<u8, 0x10000> buffer;
|
||||||
|
#endif
|
||||||
auto file_size = file.GetSize();
|
auto file_size = file.GetSize();
|
||||||
std::size_t total_bytes_read = 0;
|
std::size_t total_bytes_read = 0;
|
||||||
while (total_bytes_read != file_size) {
|
while (total_bytes_read != file_size) {
|
||||||
@ -1035,6 +1103,96 @@ InstallStatus InstallCIA(const std::string& path,
|
|||||||
return InstallStatus::ErrorInvalid;
|
return InstallStatus::ErrorInvalid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
InstallStatus InstallFromNus(u64 title_id, int version) {
|
||||||
|
LOG_DEBUG(Service_AM, "Downloading {:X}", title_id);
|
||||||
|
|
||||||
|
CIAFile install_file{Core::System::GetInstance(), GetTitleMediaType(title_id)};
|
||||||
|
|
||||||
|
std::string path = fmt::format("/ccs/download/{:016X}/tmd", title_id);
|
||||||
|
if (version != -1) {
|
||||||
|
path += fmt::format(".{}", version);
|
||||||
|
}
|
||||||
|
auto tmd_response = Core::NUS::Download(path);
|
||||||
|
if (!tmd_response) {
|
||||||
|
LOG_ERROR(Service_AM, "Failed to download tmd for {:016X}", title_id);
|
||||||
|
return InstallStatus::ErrorFileNotFound;
|
||||||
|
}
|
||||||
|
FileSys::TitleMetadata tmd;
|
||||||
|
tmd.Load(*tmd_response);
|
||||||
|
|
||||||
|
path = fmt::format("/ccs/download/{:016X}/cetk", title_id);
|
||||||
|
auto cetk_response = Core::NUS::Download(path);
|
||||||
|
if (!cetk_response) {
|
||||||
|
LOG_ERROR(Service_AM, "Failed to download cetk for {:016X}", title_id);
|
||||||
|
return InstallStatus::ErrorFileNotFound;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<u8> content;
|
||||||
|
const auto content_count = tmd.GetContentCount();
|
||||||
|
for (std::size_t i = 0; i < content_count; ++i) {
|
||||||
|
const std::string filename = fmt::format("{:08x}", tmd.GetContentIDByIndex(i));
|
||||||
|
path = fmt::format("/ccs/download/{:016X}/{}", title_id, filename);
|
||||||
|
const auto temp_response = Core::NUS::Download(path);
|
||||||
|
if (!temp_response) {
|
||||||
|
LOG_ERROR(Service_AM, "Failed to download content for {:016X}", title_id);
|
||||||
|
return InstallStatus::ErrorFileNotFound;
|
||||||
|
}
|
||||||
|
content.insert(content.end(), temp_response->begin(), temp_response->end());
|
||||||
|
}
|
||||||
|
|
||||||
|
FileSys::CIAContainer::Header fake_header{
|
||||||
|
.header_size = sizeof(FileSys::CIAContainer::Header),
|
||||||
|
.type = 0,
|
||||||
|
.version = 0,
|
||||||
|
.cert_size = 0,
|
||||||
|
.tik_size = static_cast<u32_le>(cetk_response->size()),
|
||||||
|
.tmd_size = static_cast<u32_le>(tmd_response->size()),
|
||||||
|
.meta_size = 0,
|
||||||
|
};
|
||||||
|
for (u16 i = 0; i < content_count; ++i) {
|
||||||
|
fake_header.SetContentPresent(i);
|
||||||
|
}
|
||||||
|
std::vector<u8> header_data(sizeof(fake_header));
|
||||||
|
std::memcpy(header_data.data(), &fake_header, sizeof(fake_header));
|
||||||
|
|
||||||
|
std::size_t current_offset = 0;
|
||||||
|
const auto write_to_cia_file_aligned = [&install_file, ¤t_offset](std::vector<u8>& data) {
|
||||||
|
const u64 offset =
|
||||||
|
Common::AlignUp(current_offset + data.size(), FileSys::CIA_SECTION_ALIGNMENT);
|
||||||
|
data.resize(offset - current_offset, 0);
|
||||||
|
const auto result =
|
||||||
|
install_file.Write(current_offset, data.size(), true, false, data.data());
|
||||||
|
if (result.Failed()) {
|
||||||
|
LOG_ERROR(Service_AM, "CIA file installation aborted with error code {:08x}",
|
||||||
|
result.Code().raw);
|
||||||
|
return InstallStatus::ErrorAborted;
|
||||||
|
}
|
||||||
|
current_offset += data.size();
|
||||||
|
return InstallStatus::Success;
|
||||||
|
};
|
||||||
|
|
||||||
|
auto result = write_to_cia_file_aligned(header_data);
|
||||||
|
if (result != InstallStatus::Success) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = write_to_cia_file_aligned(*cetk_response);
|
||||||
|
if (result != InstallStatus::Success) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = write_to_cia_file_aligned(*tmd_response);
|
||||||
|
if (result != InstallStatus::Success) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = write_to_cia_file_aligned(content);
|
||||||
|
if (result != InstallStatus::Success) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return InstallStatus::Success;
|
||||||
|
}
|
||||||
|
|
||||||
u64 GetTitleUpdateId(u64 title_id) {
|
u64 GetTitleUpdateId(u64 title_id) {
|
||||||
// Real services seem to just discard and replace the whole high word.
|
// Real services seem to just discard and replace the whole high word.
|
||||||
return (title_id & 0xFFFFFFFF) | (static_cast<u64>(TID_HIGH_UPDATE) << 32);
|
return (title_id & 0xFFFFFFFF) | (static_cast<u64>(TID_HIGH_UPDATE) << 32);
|
||||||
@ -1831,35 +1989,34 @@ void Module::Interface::GetProgramInfosImpl(Kernel::HLERequestContext& ctx, bool
|
|||||||
async_data->title_id_list_buffer->Read(async_data->title_id_list.data(), 0,
|
async_data->title_id_list_buffer->Read(async_data->title_id_list.data(), 0,
|
||||||
title_count * sizeof(u64));
|
title_count * sizeof(u64));
|
||||||
async_data->title_info_out = &rp.PopMappedBuffer();
|
async_data->title_info_out = &rp.PopMappedBuffer();
|
||||||
|
#ifdef todotodo
|
||||||
|
// nim checks if the current importing title already exists during installation.
|
||||||
|
// Normally, since the program wouldn't be commited, getting the title info returns not
|
||||||
|
// found. However, since GetTitleInfoFromList does not care if the program was commited and
|
||||||
|
// only checks for the tmd, it will detect the title and return information while it
|
||||||
|
// shouldn't. To prevent this, we check if the importing context is present and not
|
||||||
|
// committed. If that's the case, return not found
|
||||||
|
Result result = ResultSuccess;
|
||||||
|
for (auto tid : title_id_list) {
|
||||||
|
for (auto& import_ctx : am->import_title_contexts) {
|
||||||
|
if (import_ctx.first == tid &&
|
||||||
|
(import_ctx.second.state == ImportTitleContextState::WAITING_FOR_IMPORT ||
|
||||||
|
import_ctx.second.state == ImportTitleContextState::WAITING_FOR_COMMIT ||
|
||||||
|
import_ctx.second.state == ImportTitleContextState::RESUMABLE)) {
|
||||||
|
LOG_DEBUG(Service_AM, "title pending commit title_id={:016X}", tid);
|
||||||
|
result = Result(ErrorDescription::NotFound, ErrorModule::AM,
|
||||||
|
ErrorSummary::InvalidState, ErrorLevel::Permanent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.IsSuccess())
|
||||||
|
result = GetTitleInfoFromList(title_id_list, media_type, title_info_out);
|
||||||
|
#else
|
||||||
ctx.RunAsync(
|
ctx.RunAsync(
|
||||||
[this, async_data](Kernel::HLERequestContext& ctx) {
|
[async_data](Kernel::HLERequestContext& ctx) {
|
||||||
// nim checks if the current importing title already exists during installation.
|
async_data->res = GetTitleInfoFromList(async_data->title_id_list,
|
||||||
// Normally, since the program wouldn't be commited, getting the title info returns
|
|
||||||
// not found. However, since GetTitleInfoFromList does not care if the program was
|
|
||||||
// commited and only checks for the tmd, it will detect the title and return
|
|
||||||
// information while it shouldn't. To prevent this, we check if the importing
|
|
||||||
// context is present and not committed. If that's the case, return not found
|
|
||||||
for (auto tid : async_data->title_id_list) {
|
|
||||||
for (auto& import_ctx : am->import_title_contexts) {
|
|
||||||
if (import_ctx.first == tid &&
|
|
||||||
(import_ctx.second.state ==
|
|
||||||
ImportTitleContextState::WAITING_FOR_IMPORT ||
|
|
||||||
import_ctx.second.state ==
|
|
||||||
ImportTitleContextState::WAITING_FOR_COMMIT ||
|
|
||||||
import_ctx.second.state == ImportTitleContextState::RESUMABLE)) {
|
|
||||||
LOG_DEBUG(Service_AM, "title pending commit title_id={:016X}", tid);
|
|
||||||
async_data->res =
|
|
||||||
Result(ErrorDescription::NotFound, ErrorModule::AM,
|
|
||||||
ErrorSummary::InvalidState, ErrorLevel::Permanent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (async_data->res.IsSuccess()) {
|
|
||||||
async_data->res = GetTitleInfoFromList(async_data->title_id_list,
|
|
||||||
async_data->media_type, async_data->out);
|
async_data->media_type, async_data->out);
|
||||||
}
|
|
||||||
return 0;
|
return 0;
|
||||||
},
|
},
|
||||||
[async_data](Kernel::HLERequestContext& ctx) {
|
[async_data](Kernel::HLERequestContext& ctx) {
|
||||||
@ -1873,6 +2030,7 @@ void Module::Interface::GetProgramInfosImpl(Kernel::HLERequestContext& ctx, bool
|
|||||||
rb.PushMappedBuffer(*async_data->title_info_out);
|
rb.PushMappedBuffer(*async_data->title_info_out);
|
||||||
},
|
},
|
||||||
true);
|
true);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2416,6 +2574,7 @@ void Module::Interface::DeleteTicket(Kernel::HLERequestContext& ctx) {
|
|||||||
|
|
||||||
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
|
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
|
||||||
std::scoped_lock lock(am->am_lists_mutex);
|
std::scoped_lock lock(am->am_lists_mutex);
|
||||||
|
#ifdef todotodo
|
||||||
auto range = am->am_ticket_list.equal_range(title_id);
|
auto range = am->am_ticket_list.equal_range(title_id);
|
||||||
if (range.first == range.second) {
|
if (range.first == range.second) {
|
||||||
rb.Push(Result(ErrorDescription::AlreadyDone, ErrorModule::AM, ErrorSummary::Success,
|
rb.Push(Result(ErrorDescription::AlreadyDone, ErrorModule::AM, ErrorSummary::Success,
|
||||||
@ -2429,8 +2588,10 @@ void Module::Interface::DeleteTicket(Kernel::HLERequestContext& ctx) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
am->am_ticket_list.erase(range.first, range.second);
|
am->am_ticket_list.erase(range.first, range.second);
|
||||||
|
#endif
|
||||||
|
|
||||||
rb.Push(ResultSuccess);
|
rb.Push(ResultSuccess);
|
||||||
|
LOG_WARNING(Service_AM, "(STUBBED) called title_id=0x{:016x}", title_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Module::Interface::GetNumTickets(Kernel::HLERequestContext& ctx) {
|
void Module::Interface::GetNumTickets(Kernel::HLERequestContext& ctx) {
|
||||||
@ -2439,11 +2600,19 @@ void Module::Interface::GetNumTickets(Kernel::HLERequestContext& ctx) {
|
|||||||
LOG_DEBUG(Service_AM, "");
|
LOG_DEBUG(Service_AM, "");
|
||||||
|
|
||||||
std::scoped_lock lock(am->am_lists_mutex);
|
std::scoped_lock lock(am->am_lists_mutex);
|
||||||
|
#ifdef todotodo
|
||||||
u32 ticket_count = static_cast<u32>(am->am_ticket_list.size());
|
u32 ticket_count = static_cast<u32>(am->am_ticket_list.size());
|
||||||
|
#else
|
||||||
|
u32 ticket_count = 0;
|
||||||
|
for (const auto& title_list : am->am_title_list) {
|
||||||
|
ticket_count += static_cast<u32>(title_list.size());
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
IPC::RequestBuilder rb = rp.MakeBuilder(2, 0);
|
IPC::RequestBuilder rb = rp.MakeBuilder(2, 0);
|
||||||
rb.Push(ResultSuccess);
|
rb.Push(ResultSuccess);
|
||||||
rb.Push(ticket_count);
|
rb.Push(ticket_count);
|
||||||
|
LOG_WARNING(Service_AM, "(STUBBED) called ticket_count=0x{:08x}", ticket_count);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Module::Interface::GetTicketList(Kernel::HLERequestContext& ctx) {
|
void Module::Interface::GetTicketList(Kernel::HLERequestContext& ctx) {
|
||||||
@ -2456,6 +2625,7 @@ void Module::Interface::GetTicketList(Kernel::HLERequestContext& ctx) {
|
|||||||
|
|
||||||
u32 tickets_written = 0;
|
u32 tickets_written = 0;
|
||||||
std::scoped_lock lock(am->am_lists_mutex);
|
std::scoped_lock lock(am->am_lists_mutex);
|
||||||
|
#ifdef todotodo
|
||||||
auto it = am->am_ticket_list.begin();
|
auto it = am->am_ticket_list.begin();
|
||||||
std::advance(it, std::min(static_cast<size_t>(ticket_index), am->am_ticket_list.size()));
|
std::advance(it, std::min(static_cast<size_t>(ticket_index), am->am_ticket_list.size()));
|
||||||
|
|
||||||
@ -2463,11 +2633,22 @@ void Module::Interface::GetTicketList(Kernel::HLERequestContext& ctx) {
|
|||||||
it++, tickets_written++) {
|
it++, tickets_written++) {
|
||||||
ticket_tids_out.Write(&it->first, tickets_written * sizeof(u64), sizeof(u64));
|
ticket_tids_out.Write(&it->first, tickets_written * sizeof(u64), sizeof(u64));
|
||||||
}
|
}
|
||||||
|
#else
|
||||||
|
for (const auto& title_list : am->am_title_list) {
|
||||||
|
const auto tickets_to_write =
|
||||||
|
std::min(static_cast<u32>(title_list.size()), ticket_list_count - tickets_written);
|
||||||
|
ticket_tids_out.Write(title_list.data(), tickets_written * sizeof(u64),
|
||||||
|
tickets_to_write * sizeof(u64));
|
||||||
|
tickets_written += tickets_to_write;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
IPC::RequestBuilder rb = rp.MakeBuilder(2, 2);
|
IPC::RequestBuilder rb = rp.MakeBuilder(2, 2);
|
||||||
rb.Push(ResultSuccess);
|
rb.Push(ResultSuccess);
|
||||||
rb.Push(tickets_written);
|
rb.Push(tickets_written);
|
||||||
rb.PushMappedBuffer(ticket_tids_out);
|
rb.PushMappedBuffer(ticket_tids_out);
|
||||||
|
LOG_WARNING(Service_AM, "(STUBBED) ticket_list_count=0x{:08x}, ticket_index=0x{:08x}",
|
||||||
|
ticket_list_count, ticket_index);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Module::Interface::GetDeviceID(Kernel::HLERequestContext& ctx) {
|
void Module::Interface::GetDeviceID(Kernel::HLERequestContext& ctx) {
|
||||||
@ -2475,6 +2656,7 @@ void Module::Interface::GetDeviceID(Kernel::HLERequestContext& ctx) {
|
|||||||
|
|
||||||
LOG_DEBUG(Service_AM, "");
|
LOG_DEBUG(Service_AM, "");
|
||||||
|
|
||||||
|
#ifdef todotodo
|
||||||
const auto& otp = HW::UniqueData::GetOTP();
|
const auto& otp = HW::UniqueData::GetOTP();
|
||||||
if (!otp.Valid()) {
|
if (!otp.Valid()) {
|
||||||
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
|
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
|
||||||
@ -2490,6 +2672,13 @@ void Module::Interface::GetDeviceID(Kernel::HLERequestContext& ctx) {
|
|||||||
if (am->force_old_device_id) {
|
if (am->force_old_device_id) {
|
||||||
deviceID &= ~0x80000000;
|
deviceID &= ~0x80000000;
|
||||||
}
|
}
|
||||||
|
#else
|
||||||
|
const u32 deviceID = am->ct_cert.IsValid() ? am->ct_cert.GetDeviceID() : 0;
|
||||||
|
|
||||||
|
if (deviceID == 0) {
|
||||||
|
LOG_ERROR(Service_AM, "Invalid or missing CTCert");
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
IPC::RequestBuilder rb = rp.MakeBuilder(3, 0);
|
IPC::RequestBuilder rb = rp.MakeBuilder(3, 0);
|
||||||
rb.Push(ResultSuccess);
|
rb.Push(ResultSuccess);
|
||||||
@ -2505,6 +2694,7 @@ void Module::Interface::GetNumImportTitleContextsImpl(IPC::RequestParser& rp,
|
|||||||
IPC::RequestBuilder rb = rp.MakeBuilder(3, 0);
|
IPC::RequestBuilder rb = rp.MakeBuilder(3, 0);
|
||||||
rb.Push(ResultSuccess);
|
rb.Push(ResultSuccess);
|
||||||
|
|
||||||
|
#ifdef todotodo
|
||||||
u32 count = 0;
|
u32 count = 0;
|
||||||
for (auto it = am->import_title_contexts.begin(); it != am->import_title_contexts.end(); it++) {
|
for (auto it = am->import_title_contexts.begin(); it != am->import_title_contexts.end(); it++) {
|
||||||
if ((include_installing &&
|
if ((include_installing &&
|
||||||
@ -2517,6 +2707,9 @@ void Module::Interface::GetNumImportTitleContextsImpl(IPC::RequestParser& rp,
|
|||||||
}
|
}
|
||||||
|
|
||||||
rb.Push<u32>(count);
|
rb.Push<u32>(count);
|
||||||
|
#else
|
||||||
|
rb.Push<u32>(static_cast<u32>(am->import_title_contexts.size()));
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void Module::Interface::GetImportTitleContextListImpl(IPC::RequestParser& rp,
|
void Module::Interface::GetImportTitleContextListImpl(IPC::RequestParser& rp,
|
||||||
@ -2717,6 +2910,7 @@ void Module::Interface::NeedsCleanup(Kernel::HLERequestContext& ctx) {
|
|||||||
|
|
||||||
LOG_DEBUG(Service_AM, "(STUBBED) media_type=0x{:02x}", media_type);
|
LOG_DEBUG(Service_AM, "(STUBBED) media_type=0x{:02x}", media_type);
|
||||||
|
|
||||||
|
#ifdef todotodo
|
||||||
bool needs_cleanup = false;
|
bool needs_cleanup = false;
|
||||||
for (auto& import_ctx : am->import_title_contexts) {
|
for (auto& import_ctx : am->import_title_contexts) {
|
||||||
if (import_ctx.second.state == ImportTitleContextState::NEEDS_CLEANUP) {
|
if (import_ctx.second.state == ImportTitleContextState::NEEDS_CLEANUP) {
|
||||||
@ -2732,10 +2926,15 @@ void Module::Interface::NeedsCleanup(Kernel::HLERequestContext& ctx) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
IPC::RequestBuilder rb = rp.MakeBuilder(2, 0);
|
IPC::RequestBuilder rb = rp.MakeBuilder(2, 0);
|
||||||
rb.Push(ResultSuccess);
|
rb.Push(ResultSuccess);
|
||||||
|
#ifdef todotodo
|
||||||
rb.Push<bool>(needs_cleanup);
|
rb.Push<bool>(needs_cleanup);
|
||||||
|
#else
|
||||||
|
rb.Push<bool>(false);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void Module::Interface::DoCleanup(Kernel::HLERequestContext& ctx) {
|
void Module::Interface::DoCleanup(Kernel::HLERequestContext& ctx) {
|
||||||
@ -2744,6 +2943,7 @@ void Module::Interface::DoCleanup(Kernel::HLERequestContext& ctx) {
|
|||||||
|
|
||||||
LOG_DEBUG(Service_AM, "(STUBBED) called, media_type={:#02x}", media_type);
|
LOG_DEBUG(Service_AM, "(STUBBED) called, media_type={:#02x}", media_type);
|
||||||
|
|
||||||
|
#ifdef todotodo
|
||||||
for (auto it = am->import_content_contexts.begin(); it != am->import_content_contexts.end();) {
|
for (auto it = am->import_content_contexts.begin(); it != am->import_content_contexts.end();) {
|
||||||
if (it->second.state == ImportTitleContextState::NEEDS_CLEANUP) {
|
if (it->second.state == ImportTitleContextState::NEEDS_CLEANUP) {
|
||||||
it = am->import_content_contexts.erase(it);
|
it = am->import_content_contexts.erase(it);
|
||||||
@ -2759,6 +2959,7 @@ void Module::Interface::DoCleanup(Kernel::HLERequestContext& ctx) {
|
|||||||
it++;
|
it++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
|
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
|
||||||
rb.Push(ResultSuccess);
|
rb.Push(ResultSuccess);
|
||||||
@ -2775,6 +2976,7 @@ void Module::Interface::QueryAvailableTitleDatabase(Kernel::HLERequestContext& c
|
|||||||
LOG_WARNING(Service_AM, "(STUBBED) media_type={}", media_type);
|
LOG_WARNING(Service_AM, "(STUBBED) media_type={}", media_type);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef todotodo
|
||||||
void Module::Interface::GetPersonalizedTicketInfoList(Kernel::HLERequestContext& ctx) {
|
void Module::Interface::GetPersonalizedTicketInfoList(Kernel::HLERequestContext& ctx) {
|
||||||
IPC::RequestParser rp(ctx);
|
IPC::RequestParser rp(ctx);
|
||||||
|
|
||||||
@ -2859,6 +3061,48 @@ void Module::Interface::GetImportTitleContextListFiltered(Kernel::HLERequestCont
|
|||||||
|
|
||||||
LOG_WARNING(Service_AM, "(STUBBED) called, media_type={}, filter={}", media_type, filter);
|
LOG_WARNING(Service_AM, "(STUBBED) called, media_type={}, filter={}", media_type, filter);
|
||||||
}
|
}
|
||||||
|
#else
|
||||||
|
void Module::Interface::GetPersonalizedTicketInfoList(Kernel::HLERequestContext& ctx) {
|
||||||
|
IPC::RequestParser rp(ctx);
|
||||||
|
[[maybe_unused]] u32 ticket_count = rp.Pop<u32>();
|
||||||
|
[[maybe_unused]] auto& buffer = rp.PopMappedBuffer();
|
||||||
|
|
||||||
|
std::scoped_lock lock(am->am_lists_mutex);
|
||||||
|
|
||||||
|
IPC::RequestBuilder rb = rp.MakeBuilder(2, 0);
|
||||||
|
rb.Push(ResultSuccess); // No error
|
||||||
|
rb.Push(0);
|
||||||
|
|
||||||
|
LOG_WARNING(Service_AM, "(STUBBED) called, ticket_count={}", ticket_count);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Module::Interface::GetNumImportTitleContextsFiltered(Kernel::HLERequestContext& ctx) {
|
||||||
|
IPC::RequestParser rp(ctx);
|
||||||
|
u8 media_type = rp.Pop<u8>();
|
||||||
|
u8 filter = rp.Pop<u8>();
|
||||||
|
|
||||||
|
IPC::RequestBuilder rb = rp.MakeBuilder(2, 0);
|
||||||
|
rb.Push(ResultSuccess); // No error
|
||||||
|
rb.Push(0);
|
||||||
|
|
||||||
|
LOG_WARNING(Service_AM, "(STUBBED) called, media_type={}, filter={}", media_type, filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Module::Interface::GetImportTitleContextListFiltered(Kernel::HLERequestContext& ctx) {
|
||||||
|
IPC::RequestParser rp(ctx);
|
||||||
|
[[maybe_unused]] const u32 count = rp.Pop<u32>();
|
||||||
|
const u8 media_type = rp.Pop<u8>();
|
||||||
|
const u8 filter = rp.Pop<u8>();
|
||||||
|
auto& buffer = rp.PopMappedBuffer();
|
||||||
|
|
||||||
|
IPC::RequestBuilder rb = rp.MakeBuilder(2, 2);
|
||||||
|
rb.Push(ResultSuccess); // No error
|
||||||
|
rb.Push(0);
|
||||||
|
rb.PushMappedBuffer(buffer);
|
||||||
|
|
||||||
|
LOG_WARNING(Service_AM, "(STUBBED) called, media_type={}, filter={}", media_type, filter);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
void Module::Interface::CheckContentRights(Kernel::HLERequestContext& ctx) {
|
void Module::Interface::CheckContentRights(Kernel::HLERequestContext& ctx) {
|
||||||
IPC::RequestParser rp(ctx);
|
IPC::RequestParser rp(ctx);
|
||||||
@ -2899,8 +3143,13 @@ void Module::Interface::BeginImportProgram(Kernel::HLERequestContext& ctx) {
|
|||||||
|
|
||||||
if (am->cia_installing) {
|
if (am->cia_installing) {
|
||||||
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
|
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
|
||||||
|
#ifdef todotodo
|
||||||
rb.Push(Result(ErrCodes::InvalidImportState, ErrorModule::AM, ErrorSummary::InvalidState,
|
rb.Push(Result(ErrCodes::InvalidImportState, ErrorModule::AM, ErrorSummary::InvalidState,
|
||||||
ErrorLevel::Permanent));
|
ErrorLevel::Permanent));
|
||||||
|
#else
|
||||||
|
rb.Push(Result(ErrCodes::CIACurrentlyInstalling, ErrorModule::AM,
|
||||||
|
ErrorSummary::InvalidState, ErrorLevel::Permanent));
|
||||||
|
#endif
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2924,8 +3173,13 @@ void Module::Interface::BeginImportProgramTemporarily(Kernel::HLERequestContext&
|
|||||||
|
|
||||||
if (am->cia_installing) {
|
if (am->cia_installing) {
|
||||||
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
|
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
|
||||||
|
#ifdef todotodo
|
||||||
rb.Push(Result(ErrCodes::InvalidImportState, ErrorModule::AM, ErrorSummary::InvalidState,
|
rb.Push(Result(ErrCodes::InvalidImportState, ErrorModule::AM, ErrorSummary::InvalidState,
|
||||||
ErrorLevel::Permanent));
|
ErrorLevel::Permanent));
|
||||||
|
#else
|
||||||
|
rb.Push(Result(ErrCodes::CIACurrentlyInstalling, ErrorModule::AM,
|
||||||
|
ErrorSummary::InvalidState, ErrorLevel::Permanent));
|
||||||
|
#endif
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2975,7 +3229,25 @@ void Module::Interface::EndImportProgramWithoutCommit(Kernel::HLERequestContext&
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Module::Interface::CommitImportPrograms(Kernel::HLERequestContext& ctx) {
|
void Module::Interface::CommitImportPrograms(Kernel::HLERequestContext& ctx) {
|
||||||
|
#ifdef todotodo
|
||||||
CommitImportTitlesImpl(ctx, false, false);
|
CommitImportTitlesImpl(ctx, false, false);
|
||||||
|
#else
|
||||||
|
IPC::RequestParser rp(ctx);
|
||||||
|
[[maybe_unused]] const auto media_type = static_cast<FS::MediaType>(rp.Pop<u8>());
|
||||||
|
[[maybe_unused]] const u32 title_count = rp.Pop<u32>();
|
||||||
|
[[maybe_unused]] const u8 database = rp.Pop<u8>();
|
||||||
|
const auto buffer = rp.PopMappedBuffer();
|
||||||
|
|
||||||
|
// Note: This function is basically a no-op for us since we don't use title.db or ticket.db
|
||||||
|
// files to keep track of installed titles.
|
||||||
|
am->ScanForAllTitles();
|
||||||
|
|
||||||
|
IPC::RequestBuilder rb = rp.MakeBuilder(1, 2);
|
||||||
|
rb.Push(ResultSuccess);
|
||||||
|
rb.PushMappedBuffer(buffer);
|
||||||
|
|
||||||
|
LOG_WARNING(Service_AM, "(STUBBED)");
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Wraps all File operations to allow adding an offset to them.
|
/// Wraps all File operations to allow adding an offset to them.
|
||||||
@ -3452,45 +3724,39 @@ void Module::Interface::BeginImportTicket(Kernel::HLERequestContext& ctx) {
|
|||||||
IPC::RequestBuilder rb = rp.MakeBuilder(1, 2);
|
IPC::RequestBuilder rb = rp.MakeBuilder(1, 2);
|
||||||
rb.Push(ResultSuccess); // No error
|
rb.Push(ResultSuccess); // No error
|
||||||
rb.PushCopyObjects(file->Connect());
|
rb.PushCopyObjects(file->Connect());
|
||||||
|
|
||||||
|
LOG_WARNING(Service_AM, "(STUBBED) called");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef todotodo
|
||||||
void Module::Interface::EndImportTicket(Kernel::HLERequestContext& ctx) {
|
void Module::Interface::EndImportTicket(Kernel::HLERequestContext& ctx) {
|
||||||
IPC::RequestParser rp(ctx);
|
IPC::RequestParser rp(ctx);
|
||||||
const auto ticket = rp.PopObject<Kernel::ClientSession>();
|
const auto ticket = rp.PopObject<Kernel::ClientSession>();
|
||||||
|
|
||||||
|
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
|
||||||
auto ticket_file = GetFileBackendFromSession<TicketFile>(ticket);
|
auto ticket_file = GetFileBackendFromSession<TicketFile>(ticket);
|
||||||
if (ticket_file.Succeeded()) {
|
if (ticket_file.Succeeded()) {
|
||||||
struct AsyncData {
|
rb.Push(ticket_file.Unwrap()->Commit());
|
||||||
Service::AM::TicketFile* ticket_file;
|
am->am_ticket_list.insert(std::make_pair(ticket_file.Unwrap()->GetTitleID(),
|
||||||
|
ticket_file.Unwrap()->GetTicketID()));
|
||||||
Result res{0};
|
|
||||||
};
|
|
||||||
std::shared_ptr<AsyncData> async_data = std::make_shared<AsyncData>();
|
|
||||||
async_data->ticket_file = ticket_file.Unwrap();
|
|
||||||
|
|
||||||
ctx.RunAsync(
|
|
||||||
[this, async_data](Kernel::HLERequestContext& ctx) {
|
|
||||||
async_data->res = async_data->ticket_file->Commit();
|
|
||||||
|
|
||||||
std::scoped_lock lock(am->am_lists_mutex);
|
|
||||||
am->am_ticket_list.insert(std::make_pair(async_data->ticket_file->GetTitleID(),
|
|
||||||
async_data->ticket_file->GetTicketID()));
|
|
||||||
|
|
||||||
LOG_DEBUG(Service_AM, "EndImportTicket: title_id={:016X} ticket_id={:016X}",
|
|
||||||
async_data->ticket_file->GetTitleID(),
|
|
||||||
async_data->ticket_file->GetTicketID());
|
|
||||||
return 0;
|
|
||||||
},
|
|
||||||
[async_data](Kernel::HLERequestContext& ctx) {
|
|
||||||
IPC::RequestBuilder rb(ctx, 1, 0);
|
|
||||||
rb.Push(async_data->res);
|
|
||||||
},
|
|
||||||
true);
|
|
||||||
} else {
|
} else {
|
||||||
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
|
|
||||||
rb.Push(ticket_file.Code());
|
rb.Push(ticket_file.Code());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LOG_DEBUG(Service_AM, "title_id={:016X} ticket_id={:016X}", ticket_file.Unwrap()->GetTitleID(),
|
||||||
|
ticket_file.Unwrap()->GetTicketID());
|
||||||
}
|
}
|
||||||
|
#else
|
||||||
|
void Module::Interface::EndImportTicket(Kernel::HLERequestContext& ctx) {
|
||||||
|
IPC::RequestParser rp(ctx);
|
||||||
|
[[maybe_unused]] const auto ticket = rp.PopObject<Kernel::ClientSession>();
|
||||||
|
|
||||||
|
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
|
||||||
|
rb.Push(ResultSuccess);
|
||||||
|
|
||||||
|
LOG_WARNING(Service_AM, "(STUBBED) called");
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
void Module::Interface::BeginImportTitle(Kernel::HLERequestContext& ctx) {
|
void Module::Interface::BeginImportTitle(Kernel::HLERequestContext& ctx) {
|
||||||
IPC::RequestParser rp(ctx);
|
IPC::RequestParser rp(ctx);
|
||||||
@ -3621,6 +3887,7 @@ void Module::Interface::EndImportTitle(Kernel::HLERequestContext& ctx) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
am->importing_title->cia_file.SetDone();
|
am->importing_title->cia_file.SetDone();
|
||||||
|
am->ScanForTitles(am->importing_title->media_type);
|
||||||
am->importing_title.reset();
|
am->importing_title.reset();
|
||||||
|
|
||||||
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
|
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
|
||||||
@ -3994,10 +4261,12 @@ void Module::serialize(Archive& ar, const unsigned int) {
|
|||||||
ar & cia_installing;
|
ar & cia_installing;
|
||||||
ar & force_old_device_id;
|
ar & force_old_device_id;
|
||||||
ar & force_new_device_id;
|
ar & force_new_device_id;
|
||||||
|
ar & am_title_list;
|
||||||
ar & system_updater_mutex;
|
ar & system_updater_mutex;
|
||||||
}
|
}
|
||||||
SERIALIZE_IMPL(Module)
|
SERIALIZE_IMPL(Module)
|
||||||
|
|
||||||
|
#ifdef todotodo
|
||||||
void Module::Interface::GetDeviceCert(Kernel::HLERequestContext& ctx) {
|
void Module::Interface::GetDeviceCert(Kernel::HLERequestContext& ctx) {
|
||||||
IPC::RequestParser rp(ctx);
|
IPC::RequestParser rp(ctx);
|
||||||
[[maybe_unused]] u32 size = rp.Pop<u32>();
|
[[maybe_unused]] u32 size = rp.Pop<u32>();
|
||||||
@ -4021,6 +4290,52 @@ void Module::Interface::GetDeviceCert(Kernel::HLERequestContext& ctx) {
|
|||||||
rb.Push(0);
|
rb.Push(0);
|
||||||
rb.PushMappedBuffer(buffer);
|
rb.PushMappedBuffer(buffer);
|
||||||
}
|
}
|
||||||
|
#else
|
||||||
|
void Module::Interface::GetDeviceCert(Kernel::HLERequestContext& ctx) {
|
||||||
|
IPC::RequestParser rp(ctx);
|
||||||
|
[[maybe_unused]] u32 size = rp.Pop<u32>();
|
||||||
|
auto buffer = rp.PopMappedBuffer();
|
||||||
|
|
||||||
|
if (!am->ct_cert.IsValid()) {
|
||||||
|
LOG_ERROR(Service_AM, "Invalid or missing CTCert");
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.Write(&am->ct_cert, 0, std::min(sizeof(CTCert), buffer.GetSize()));
|
||||||
|
IPC::RequestBuilder rb = rp.MakeBuilder(2, 2);
|
||||||
|
rb.Push(ResultSuccess);
|
||||||
|
rb.Push(0);
|
||||||
|
rb.PushMappedBuffer(buffer);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
std::string Module::GetCTCertPath() {
|
||||||
|
return FileUtil::GetUserPath(FileUtil::UserPath::SysDataDir) + "CTCert.bin";
|
||||||
|
}
|
||||||
|
|
||||||
|
CTCertLoadStatus Module::LoadCTCertFile(CTCert& output) {
|
||||||
|
if (output.IsValid()) {
|
||||||
|
return CTCertLoadStatus::Loaded;
|
||||||
|
}
|
||||||
|
std::string file_path = GetCTCertPath();
|
||||||
|
if (!FileUtil::Exists(file_path)) {
|
||||||
|
return CTCertLoadStatus::NotFound;
|
||||||
|
}
|
||||||
|
FileUtil::IOFile file(file_path, "rb");
|
||||||
|
if (!file.IsOpen()) {
|
||||||
|
return CTCertLoadStatus::IOError;
|
||||||
|
}
|
||||||
|
if (file.GetSize() != sizeof(CTCert)) {
|
||||||
|
return CTCertLoadStatus::Invalid;
|
||||||
|
}
|
||||||
|
if (file.ReadBytes(&output, sizeof(CTCert)) != sizeof(CTCert)) {
|
||||||
|
return CTCertLoadStatus::IOError;
|
||||||
|
}
|
||||||
|
if (!output.IsValid()) {
|
||||||
|
output = CTCert();
|
||||||
|
return CTCertLoadStatus::Invalid;
|
||||||
|
}
|
||||||
|
return CTCertLoadStatus::Loaded;
|
||||||
|
}
|
||||||
|
|
||||||
void Module::Interface::CommitImportTitlesAndUpdateFirmwareAuto(Kernel::HLERequestContext& ctx) {
|
void Module::Interface::CommitImportTitlesAndUpdateFirmwareAuto(Kernel::HLERequestContext& ctx) {
|
||||||
CommitImportTitlesImpl(ctx, true, true);
|
CommitImportTitlesImpl(ctx, true, true);
|
||||||
@ -4051,7 +4366,11 @@ void Module::Interface::DeleteTicketId(Kernel::HLERequestContext& ctx) {
|
|||||||
auto path = GetTicketPath(title_id, ticket_id);
|
auto path = GetTicketPath(title_id, ticket_id);
|
||||||
FileUtil::Delete(path);
|
FileUtil::Delete(path);
|
||||||
|
|
||||||
|
#ifdef todotodo
|
||||||
am->am_ticket_list.erase(it);
|
am->am_ticket_list.erase(it);
|
||||||
|
#else
|
||||||
|
am->ScanForTickets();
|
||||||
|
#endif
|
||||||
|
|
||||||
rb.Push(ResultSuccess);
|
rb.Push(ResultSuccess);
|
||||||
}
|
}
|
||||||
@ -4233,6 +4552,7 @@ void Module::Interface::ExportTicketWrapped(Kernel::HLERequestContext& ctx) {
|
|||||||
Module::Module(Core::System& _system) : system(_system) {
|
Module::Module(Core::System& _system) : system(_system) {
|
||||||
FileUtil::CreateFullPath(GetTicketDirectory());
|
FileUtil::CreateFullPath(GetTicketDirectory());
|
||||||
ScanForAllTitles();
|
ScanForAllTitles();
|
||||||
|
LoadCTCertFile(ct_cert);
|
||||||
system_updater_mutex = system.Kernel().CreateMutex(false, "AM::SystemUpdaterMutex");
|
system_updater_mutex = system.Kernel().CreateMutex(false, "AM::SystemUpdaterMutex");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -51,6 +51,7 @@ namespace Service::AM {
|
|||||||
namespace ErrCodes {
|
namespace ErrCodes {
|
||||||
enum {
|
enum {
|
||||||
InvalidImportState = 4,
|
InvalidImportState = 4,
|
||||||
|
CIACurrentlyInstalling = 4,
|
||||||
InvalidTID = 31,
|
InvalidTID = 31,
|
||||||
EmptyCIA = 32,
|
EmptyCIA = 32,
|
||||||
TryingToUninstallSystemApp = 44,
|
TryingToUninstallSystemApp = 44,
|
||||||
@ -87,6 +88,13 @@ enum class ImportTitleContextState : u8 {
|
|||||||
NEEDS_CLEANUP = 6,
|
NEEDS_CLEANUP = 6,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
enum class CTCertLoadStatus {
|
||||||
|
Loaded,
|
||||||
|
NotFound,
|
||||||
|
Invalid,
|
||||||
|
IOError,
|
||||||
|
};
|
||||||
|
|
||||||
struct ImportTitleContext {
|
struct ImportTitleContext {
|
||||||
u64 title_id;
|
u64 title_id;
|
||||||
u16 version;
|
u16 version;
|
||||||
@ -105,6 +113,24 @@ struct ImportContentContext {
|
|||||||
};
|
};
|
||||||
static_assert(sizeof(ImportContentContext) == 0x18, "Invalid ImportContentContext size");
|
static_assert(sizeof(ImportContentContext) == 0x18, "Invalid ImportContentContext size");
|
||||||
|
|
||||||
|
struct CTCert {
|
||||||
|
u32_be signature_type{};
|
||||||
|
std::array<u8, 0x1E> signature_r{};
|
||||||
|
std::array<u8, 0x1E> signature_s{};
|
||||||
|
INSERT_PADDING_BYTES(0x40) {};
|
||||||
|
std::array<char, 0x40> issuer{};
|
||||||
|
u32_be key_type{};
|
||||||
|
std::array<char, 0x40> key_id{};
|
||||||
|
u32_be expiration_time{};
|
||||||
|
std::array<u8, 0x1E> public_key_x{};
|
||||||
|
std::array<u8, 0x1E> public_key_y{};
|
||||||
|
INSERT_PADDING_BYTES(0x3C) {};
|
||||||
|
|
||||||
|
bool IsValid() const;
|
||||||
|
u32 GetDeviceID() const;
|
||||||
|
};
|
||||||
|
static_assert(sizeof(CTCert) == 0x180, "Invalid CTCert size.");
|
||||||
|
|
||||||
// Title ID valid length
|
// Title ID valid length
|
||||||
constexpr std::size_t TITLE_ID_VALID_LENGTH = 16;
|
constexpr std::size_t TITLE_ID_VALID_LENGTH = 16;
|
||||||
|
|
||||||
@ -126,7 +152,7 @@ private:
|
|||||||
friend class CIAFile;
|
friend class CIAFile;
|
||||||
std::unique_ptr<FileUtil::IOFile> file;
|
std::unique_ptr<FileUtil::IOFile> file;
|
||||||
bool is_error = false;
|
bool is_error = false;
|
||||||
bool is_not_ncch = false;
|
// bool is_not_ncch = false;
|
||||||
bool decryption_authorized = false;
|
bool decryption_authorized = false;
|
||||||
|
|
||||||
std::size_t written = 0;
|
std::size_t written = 0;
|
||||||
@ -223,6 +249,7 @@ private:
|
|||||||
std::vector<std::string> content_file_paths;
|
std::vector<std::string> content_file_paths;
|
||||||
u16 current_content_index = -1;
|
u16 current_content_index = -1;
|
||||||
std::unique_ptr<NCCHCryptoFile> current_content_file;
|
std::unique_ptr<NCCHCryptoFile> current_content_file;
|
||||||
|
std::vector<FileUtil::IOFile> content_files;
|
||||||
Service::FS::MediaType media_type;
|
Service::FS::MediaType media_type;
|
||||||
|
|
||||||
class DecryptionState;
|
class DecryptionState;
|
||||||
@ -334,6 +361,13 @@ private:
|
|||||||
InstallStatus InstallCIA(const std::string& path,
|
InstallStatus InstallCIA(const std::string& path,
|
||||||
std::function<ProgressCallback>&& update_callback = nullptr);
|
std::function<ProgressCallback>&& update_callback = nullptr);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Downloads and installs title form the Nintendo Update Service.
|
||||||
|
* @param title_id the title_id to download
|
||||||
|
* @returns whether the install was successful or error code
|
||||||
|
*/
|
||||||
|
InstallStatus InstallFromNus(u64 title_id, int version = -1);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the update title ID for a title
|
* Get the update title ID for a title
|
||||||
* @param titleId the title ID
|
* @param titleId the title ID
|
||||||
@ -1022,6 +1056,18 @@ public:
|
|||||||
force_new_device_id = true;
|
force_new_device_id = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the CTCert.bin path in the host filesystem
|
||||||
|
* @returns std::string CTCert.bin path in the host filesystem
|
||||||
|
*/
|
||||||
|
static std::string GetCTCertPath();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the CTCert.bin file from the filesystem.
|
||||||
|
* @returns CTCertLoadStatus indicating the file load status.
|
||||||
|
*/
|
||||||
|
static CTCertLoadStatus LoadCTCertFile(CTCert& output);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void ScanForTickets();
|
void ScanForTickets();
|
||||||
|
|
||||||
@ -1054,6 +1100,7 @@ private:
|
|||||||
std::multimap<u64, u64> am_ticket_list;
|
std::multimap<u64, u64> am_ticket_list;
|
||||||
|
|
||||||
std::shared_ptr<Kernel::Mutex> system_updater_mutex;
|
std::shared_ptr<Kernel::Mutex> system_updater_mutex;
|
||||||
|
CTCert ct_cert{};
|
||||||
std::shared_ptr<CurrentImportingTitle> importing_title;
|
std::shared_ptr<CurrentImportingTitle> importing_title;
|
||||||
std::map<u64, ImportTitleContext> import_title_contexts;
|
std::map<u64, ImportTitleContext> import_title_contexts;
|
||||||
std::multimap<u64, ImportContentContext> import_content_contexts;
|
std::multimap<u64, ImportContentContext> import_content_contexts;
|
||||||
|
|||||||
@ -453,6 +453,7 @@ void Module::Interface::GetRegion(Kernel::HLERequestContext& ctx) {
|
|||||||
void Module::Interface::SecureInfoGetByte101(Kernel::HLERequestContext& ctx) {
|
void Module::Interface::SecureInfoGetByte101(Kernel::HLERequestContext& ctx) {
|
||||||
IPC::RequestParser rp(ctx);
|
IPC::RequestParser rp(ctx);
|
||||||
|
|
||||||
|
#ifdef todotodo
|
||||||
const auto& secure_info_a = HW::UniqueData::GetSecureInfoA();
|
const auto& secure_info_a = HW::UniqueData::GetSecureInfoA();
|
||||||
const auto& local_friend_code_seed_b = HW::UniqueData::GetLocalFriendCodeSeedB();
|
const auto& local_friend_code_seed_b = HW::UniqueData::GetLocalFriendCodeSeedB();
|
||||||
|
|
||||||
@ -465,12 +466,19 @@ void Module::Interface::SecureInfoGetByte101(Kernel::HLERequestContext& ctx) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
u8 ret = secure_info_a.body.unknown;
|
u8 ret = secure_info_a.body.unknown;
|
||||||
|
#else
|
||||||
|
u8 ret = 0;
|
||||||
|
if (cfg->secure_info_a_loaded) {
|
||||||
|
ret = cfg->secure_info_a.unknown;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
IPC::RequestBuilder rb = rp.MakeBuilder(2, 0);
|
IPC::RequestBuilder rb = rp.MakeBuilder(2, 0);
|
||||||
rb.Push(ResultSuccess);
|
rb.Push(ResultSuccess);
|
||||||
rb.Push<u8>(ret);
|
rb.Push<u8>(ret);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef todotodo
|
||||||
void Module::Interface::SecureInfoGetSerialNo(Kernel::HLERequestContext& ctx) {
|
void Module::Interface::SecureInfoGetSerialNo(Kernel::HLERequestContext& ctx) {
|
||||||
IPC::RequestParser rp(ctx);
|
IPC::RequestParser rp(ctx);
|
||||||
[[maybe_unused]] u32 out_size = rp.Pop<u32>();
|
[[maybe_unused]] u32 out_size = rp.Pop<u32>();
|
||||||
@ -500,6 +508,32 @@ void Module::Interface::SecureInfoGetSerialNo(Kernel::HLERequestContext& ctx) {
|
|||||||
rb.Push(ResultSuccess);
|
rb.Push(ResultSuccess);
|
||||||
rb.PushMappedBuffer(out_buffer);
|
rb.PushMappedBuffer(out_buffer);
|
||||||
}
|
}
|
||||||
|
#else
|
||||||
|
void Module::Interface::SecureInfoGetSerialNo(Kernel::HLERequestContext& ctx) {
|
||||||
|
IPC::RequestParser rp(ctx);
|
||||||
|
[[maybe_unused]] u32 out_size = rp.Pop<u32>();
|
||||||
|
auto out_buffer = rp.PopMappedBuffer();
|
||||||
|
|
||||||
|
if (out_buffer.GetSize() < sizeof(SecureInfoA::serial_number)) {
|
||||||
|
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
|
||||||
|
rb.Push(Result(ErrorDescription::InvalidSize, ErrorModule::Config,
|
||||||
|
ErrorSummary::WrongArgument, ErrorLevel::Permanent));
|
||||||
|
}
|
||||||
|
// Never happens on real hardware, but may happen if user didn't supply a dump.
|
||||||
|
// Always make sure to have available both secure data kinds or error otherwise.
|
||||||
|
if (!cfg->secure_info_a_loaded || !cfg->local_friend_code_seed_b_loaded) {
|
||||||
|
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
|
||||||
|
rb.Push(Result(ErrorDescription::NotFound, ErrorModule::Config, ErrorSummary::InvalidState,
|
||||||
|
ErrorLevel::Permanent));
|
||||||
|
}
|
||||||
|
|
||||||
|
out_buffer.Write(&cfg->secure_info_a.serial_number, 0, sizeof(SecureInfoA::serial_number));
|
||||||
|
|
||||||
|
IPC::RequestBuilder rb = rp.MakeBuilder(1, 2);
|
||||||
|
rb.Push(ResultSuccess);
|
||||||
|
rb.PushMappedBuffer(out_buffer);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
void Module::Interface::SetUUIDClockSequence(Kernel::HLERequestContext& ctx) {
|
void Module::Interface::SetUUIDClockSequence(Kernel::HLERequestContext& ctx) {
|
||||||
IPC::RequestParser rp(ctx);
|
IPC::RequestParser rp(ctx);
|
||||||
@ -645,6 +679,7 @@ void Module::Interface::UpdateConfigNANDSavegame(Kernel::HLERequestContext& ctx)
|
|||||||
rb.Push(cfg->UpdateConfigNANDSavegame());
|
rb.Push(cfg->UpdateConfigNANDSavegame());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef todotodo
|
||||||
void Module::Interface::GetLocalFriendCodeSeedData(Kernel::HLERequestContext& ctx) {
|
void Module::Interface::GetLocalFriendCodeSeedData(Kernel::HLERequestContext& ctx) {
|
||||||
IPC::RequestParser rp(ctx);
|
IPC::RequestParser rp(ctx);
|
||||||
[[maybe_unused]] u32 out_size = rp.Pop<u32>();
|
[[maybe_unused]] u32 out_size = rp.Pop<u32>();
|
||||||
@ -688,6 +723,44 @@ void Module::Interface::GetLocalFriendCodeSeed(Kernel::HLERequestContext& ctx) {
|
|||||||
rb.Push(ResultSuccess);
|
rb.Push(ResultSuccess);
|
||||||
rb.Push<u64>(local_friend_code_seed_b.body.friend_code_seed);
|
rb.Push<u64>(local_friend_code_seed_b.body.friend_code_seed);
|
||||||
}
|
}
|
||||||
|
#else
|
||||||
|
void Module::Interface::GetLocalFriendCodeSeedData(Kernel::HLERequestContext& ctx) {
|
||||||
|
IPC::RequestParser rp(ctx);
|
||||||
|
[[maybe_unused]] u32 out_size = rp.Pop<u32>();
|
||||||
|
auto out_buffer = rp.PopMappedBuffer();
|
||||||
|
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
|
||||||
|
|
||||||
|
if (out_buffer.GetSize() < sizeof(LocalFriendCodeSeedB)) {
|
||||||
|
rb.Push(Result(ErrorDescription::InvalidSize, ErrorModule::Config,
|
||||||
|
ErrorSummary::WrongArgument, ErrorLevel::Permanent));
|
||||||
|
}
|
||||||
|
// Never happens on real hardware, but may happen if user didn't supply a dump.
|
||||||
|
// Always make sure to have available both secure data kinds or error otherwise.
|
||||||
|
if (!cfg->secure_info_a_loaded || !cfg->local_friend_code_seed_b_loaded) {
|
||||||
|
rb.Push(Result(ErrorDescription::NotFound, ErrorModule::Config, ErrorSummary::InvalidState,
|
||||||
|
ErrorLevel::Permanent));
|
||||||
|
}
|
||||||
|
|
||||||
|
out_buffer.Write(&cfg->local_friend_code_seed_b, 0, sizeof(LocalFriendCodeSeedB));
|
||||||
|
rb.Push(ResultSuccess);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Module::Interface::GetLocalFriendCodeSeed(Kernel::HLERequestContext& ctx) {
|
||||||
|
IPC::RequestParser rp(ctx);
|
||||||
|
|
||||||
|
// Never happens on real hardware, but may happen if user didn't supply a dump.
|
||||||
|
// Always make sure to have available both secure data kinds or error otherwise.
|
||||||
|
if (!cfg->secure_info_a_loaded || !cfg->local_friend_code_seed_b_loaded) {
|
||||||
|
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
|
||||||
|
rb.Push(Result(ErrorDescription::NotFound, ErrorModule::Config, ErrorSummary::InvalidState,
|
||||||
|
ErrorLevel::Permanent));
|
||||||
|
}
|
||||||
|
|
||||||
|
IPC::RequestBuilder rb = rp.MakeBuilder(3, 0);
|
||||||
|
rb.Push(ResultSuccess);
|
||||||
|
rb.Push<u64>(cfg->local_friend_code_seed_b.friend_code_seed);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
void Module::Interface::FormatConfig(Kernel::HLERequestContext& ctx) {
|
void Module::Interface::FormatConfig(Kernel::HLERequestContext& ctx) {
|
||||||
IPC::RequestParser rp(ctx);
|
IPC::RequestParser rp(ctx);
|
||||||
@ -863,6 +936,14 @@ Result Module::UpdateConfigNANDSavegame() {
|
|||||||
return ResultSuccess;
|
return ResultSuccess;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string Module::GetLocalFriendCodeSeedBPath() {
|
||||||
|
return FileUtil::GetUserPath(FileUtil::UserPath::NANDDir) + "rw/sys/LocalFriendCodeSeed_B";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string Module::GetSecureInfoAPath() {
|
||||||
|
return FileUtil::GetUserPath(FileUtil::UserPath::NANDDir) + "rw/sys/SecureInfo_A";
|
||||||
|
}
|
||||||
|
|
||||||
Result Module::FormatConfig() {
|
Result Module::FormatConfig() {
|
||||||
Result res = DeleteConfigNANDSaveFile();
|
Result res = DeleteConfigNANDSaveFile();
|
||||||
// The delete command fails if the file doesn't exist, so we have to check that too
|
// The delete command fails if the file doesn't exist, so we have to check that too
|
||||||
@ -938,6 +1019,55 @@ Result Module::LoadConfigNANDSaveFile() {
|
|||||||
return FormatConfig();
|
return FormatConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Module::InvalidateSecureData() {
|
||||||
|
secure_info_a_loaded = local_friend_code_seed_b_loaded = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
SecureDataLoadStatus Module::LoadSecureInfoAFile() {
|
||||||
|
if (secure_info_a_loaded) {
|
||||||
|
return SecureDataLoadStatus::Loaded;
|
||||||
|
}
|
||||||
|
std::string file_path = GetSecureInfoAPath();
|
||||||
|
if (!FileUtil::Exists(file_path)) {
|
||||||
|
return SecureDataLoadStatus::NotFound;
|
||||||
|
}
|
||||||
|
FileUtil::IOFile file(file_path, "rb");
|
||||||
|
if (!file.IsOpen()) {
|
||||||
|
return SecureDataLoadStatus::IOError;
|
||||||
|
}
|
||||||
|
if (file.GetSize() != sizeof(SecureInfoA)) {
|
||||||
|
return SecureDataLoadStatus::Invalid;
|
||||||
|
}
|
||||||
|
if (file.ReadBytes(&secure_info_a, sizeof(SecureInfoA)) != sizeof(SecureInfoA)) {
|
||||||
|
return SecureDataLoadStatus::IOError;
|
||||||
|
}
|
||||||
|
secure_info_a_loaded = true;
|
||||||
|
return SecureDataLoadStatus::Loaded;
|
||||||
|
}
|
||||||
|
|
||||||
|
SecureDataLoadStatus Module::LoadLocalFriendCodeSeedBFile() {
|
||||||
|
if (local_friend_code_seed_b_loaded) {
|
||||||
|
return SecureDataLoadStatus::Loaded;
|
||||||
|
}
|
||||||
|
std::string file_path = GetLocalFriendCodeSeedBPath();
|
||||||
|
if (!FileUtil::Exists(file_path)) {
|
||||||
|
return SecureDataLoadStatus::NotFound;
|
||||||
|
}
|
||||||
|
FileUtil::IOFile file(file_path, "rb");
|
||||||
|
if (!file.IsOpen()) {
|
||||||
|
return SecureDataLoadStatus::IOError;
|
||||||
|
}
|
||||||
|
if (file.GetSize() != sizeof(LocalFriendCodeSeedB)) {
|
||||||
|
return SecureDataLoadStatus::Invalid;
|
||||||
|
}
|
||||||
|
if (file.ReadBytes(&local_friend_code_seed_b, sizeof(LocalFriendCodeSeedB)) !=
|
||||||
|
sizeof(LocalFriendCodeSeedB)) {
|
||||||
|
return SecureDataLoadStatus::IOError;
|
||||||
|
}
|
||||||
|
local_friend_code_seed_b_loaded = true;
|
||||||
|
return SecureDataLoadStatus::Loaded;
|
||||||
|
}
|
||||||
|
|
||||||
void Module::LoadMCUConfig() {
|
void Module::LoadMCUConfig() {
|
||||||
FileUtil::IOFile mcu_data_file(
|
FileUtil::IOFile mcu_data_file(
|
||||||
fmt::format("{}/mcu.dat", FileUtil::GetUserPath(FileUtil::UserPath::SysDataDir)), "rb");
|
fmt::format("{}/mcu.dat", FileUtil::GetUserPath(FileUtil::UserPath::SysDataDir)), "rb");
|
||||||
@ -976,6 +1106,8 @@ Module::Module(Core::System& system_) : system(system_) {
|
|||||||
SetEULAVersion(default_version);
|
SetEULAVersion(default_version);
|
||||||
UpdateConfigNANDSavegame();
|
UpdateConfigNANDSavegame();
|
||||||
}
|
}
|
||||||
|
LoadSecureInfoAFile();
|
||||||
|
LoadLocalFriendCodeSeedBFile();
|
||||||
}
|
}
|
||||||
|
|
||||||
Module::~Module() = default;
|
Module::~Module() = default;
|
||||||
|
|||||||
@ -181,6 +181,28 @@ enum class AccessFlag : u16 {
|
|||||||
};
|
};
|
||||||
DECLARE_ENUM_FLAG_OPERATORS(AccessFlag);
|
DECLARE_ENUM_FLAG_OPERATORS(AccessFlag);
|
||||||
|
|
||||||
|
struct SecureInfoA {
|
||||||
|
std::array<u8, 0x100> signature;
|
||||||
|
u8 region;
|
||||||
|
u8 unknown;
|
||||||
|
std::array<u8, 0xF> serial_number;
|
||||||
|
};
|
||||||
|
static_assert(sizeof(SecureInfoA) == 0x111);
|
||||||
|
|
||||||
|
struct LocalFriendCodeSeedB {
|
||||||
|
std::array<u8, 0x100> signature;
|
||||||
|
u64 unknown;
|
||||||
|
u64 friend_code_seed;
|
||||||
|
};
|
||||||
|
static_assert(sizeof(LocalFriendCodeSeedB) == 0x110);
|
||||||
|
|
||||||
|
enum class SecureDataLoadStatus {
|
||||||
|
Loaded,
|
||||||
|
NotFound,
|
||||||
|
Invalid,
|
||||||
|
IOError,
|
||||||
|
};
|
||||||
|
|
||||||
class Module final {
|
class Module final {
|
||||||
public:
|
public:
|
||||||
Module(Core::System& system_);
|
Module(Core::System& system_);
|
||||||
@ -634,6 +656,35 @@ public:
|
|||||||
*/
|
*/
|
||||||
void SaveMacAddress();
|
void SaveMacAddress();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidates the loaded secure data so that it is loaded again.
|
||||||
|
*/
|
||||||
|
void InvalidateSecureData();
|
||||||
|
/**
|
||||||
|
* Loads the LocalFriendCodeSeed_B file from NAND.
|
||||||
|
* @returns LocalFriendCodeSeedBLoadStatus indicating the file load status.
|
||||||
|
*/
|
||||||
|
SecureDataLoadStatus LoadSecureInfoAFile();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the LocalFriendCodeSeed_B file from NAND.
|
||||||
|
* @returns LocalFriendCodeSeedBLoadStatus indicating the file load status.
|
||||||
|
*/
|
||||||
|
SecureDataLoadStatus LoadLocalFriendCodeSeedBFile();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the SecureInfo_A path in the host filesystem
|
||||||
|
* @returns std::string SecureInfo_A path in the host filesystem
|
||||||
|
*/
|
||||||
|
std::string GetSecureInfoAPath();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the LocalFriendCodeSeed_B path in the host filesystem
|
||||||
|
* @returns std::string LocalFriendCodeSeed_B path in the host filesystem
|
||||||
|
*/
|
||||||
|
std::string GetLocalFriendCodeSeedBPath();
|
||||||
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void UpdatePreferredRegionCode();
|
void UpdatePreferredRegionCode();
|
||||||
SystemLanguage GetRawSystemLanguage();
|
SystemLanguage GetRawSystemLanguage();
|
||||||
@ -644,6 +695,10 @@ private:
|
|||||||
std::array<u8, CONFIG_SAVEFILE_SIZE> cfg_config_file_buffer;
|
std::array<u8, CONFIG_SAVEFILE_SIZE> cfg_config_file_buffer;
|
||||||
std::unique_ptr<FileSys::ArchiveBackend> cfg_system_save_data_archive;
|
std::unique_ptr<FileSys::ArchiveBackend> cfg_system_save_data_archive;
|
||||||
u32 preferred_region_code = 0;
|
u32 preferred_region_code = 0;
|
||||||
|
bool secure_info_a_loaded = false;
|
||||||
|
SecureInfoA secure_info_a;
|
||||||
|
bool local_friend_code_seed_b_loaded = false;
|
||||||
|
LocalFriendCodeSeedB local_friend_code_seed_b;
|
||||||
bool preferred_region_chosen = false;
|
bool preferred_region_chosen = false;
|
||||||
MCUData mcu_data{};
|
MCUData mcu_data{};
|
||||||
std::string mac_address{};
|
std::string mac_address{};
|
||||||
|
|||||||
@ -32,7 +32,10 @@ namespace {
|
|||||||
// On a real 3DS the generation for the normal key is hardware based, and thus the constant can't
|
// On a real 3DS the generation for the normal key is hardware based, and thus the constant can't
|
||||||
// get dumped. Generated normal keys are also not accessible on a 3DS. The used formula for
|
// get dumped. Generated normal keys are also not accessible on a 3DS. The used formula for
|
||||||
// calculating the constant is a software implementation of what the hardware generator does.
|
// calculating the constant is a software implementation of what the hardware generator does.
|
||||||
AESKey generator_constant;
|
//AESKey generator_constant;
|
||||||
|
|
||||||
|
constexpr AESKey generator_constant = {{0x1F, 0xF9, 0xE9, 0xAA, 0xC5, 0xFE, 0x04, 0x08, 0x02, 0x45,
|
||||||
|
0x91, 0xDC, 0x5D, 0x52, 0x76, 0x8A}};
|
||||||
|
|
||||||
AESKey HexToKey(const std::string& hex) {
|
AESKey HexToKey(const std::string& hex) {
|
||||||
if (hex.size() < 32) {
|
if (hex.size() < 32) {
|
||||||
@ -143,6 +146,78 @@ struct KeyDesc {
|
|||||||
bool same_as_before;
|
bool same_as_before;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
void LoadBootromKeys() {
|
||||||
|
constexpr std::array<KeyDesc, 80> keys = {
|
||||||
|
{{'X', 0x2C, false}, {'X', 0x2D, true}, {'X', 0x2E, true}, {'X', 0x2F, true},
|
||||||
|
{'X', 0x30, false}, {'X', 0x31, true}, {'X', 0x32, true}, {'X', 0x33, true},
|
||||||
|
{'X', 0x34, false}, {'X', 0x35, true}, {'X', 0x36, true}, {'X', 0x37, true},
|
||||||
|
{'X', 0x38, false}, {'X', 0x39, true}, {'X', 0x3A, true}, {'X', 0x3B, true},
|
||||||
|
{'X', 0x3C, false}, {'X', 0x3D, false}, {'X', 0x3E, false}, {'X', 0x3F, false},
|
||||||
|
{'Y', 0x4, false}, {'Y', 0x5, false}, {'Y', 0x6, false}, {'Y', 0x7, false},
|
||||||
|
{'Y', 0x8, false}, {'Y', 0x9, false}, {'Y', 0xA, false}, {'Y', 0xB, false},
|
||||||
|
{'N', 0xC, false}, {'N', 0xD, true}, {'N', 0xE, true}, {'N', 0xF, true},
|
||||||
|
{'N', 0x10, false}, {'N', 0x11, true}, {'N', 0x12, true}, {'N', 0x13, true},
|
||||||
|
{'N', 0x14, false}, {'N', 0x15, false}, {'N', 0x16, false}, {'N', 0x17, false},
|
||||||
|
{'N', 0x18, false}, {'N', 0x19, true}, {'N', 0x1A, true}, {'N', 0x1B, true},
|
||||||
|
{'N', 0x1C, false}, {'N', 0x1D, true}, {'N', 0x1E, true}, {'N', 0x1F, true},
|
||||||
|
{'N', 0x20, false}, {'N', 0x21, true}, {'N', 0x22, true}, {'N', 0x23, true},
|
||||||
|
{'N', 0x24, false}, {'N', 0x25, true}, {'N', 0x26, true}, {'N', 0x27, true},
|
||||||
|
{'N', 0x28, true}, {'N', 0x29, false}, {'N', 0x2A, false}, {'N', 0x2B, false},
|
||||||
|
{'N', 0x2C, false}, {'N', 0x2D, true}, {'N', 0x2E, true}, {'N', 0x2F, true},
|
||||||
|
{'N', 0x30, false}, {'N', 0x31, true}, {'N', 0x32, true}, {'N', 0x33, true},
|
||||||
|
{'N', 0x34, false}, {'N', 0x35, true}, {'N', 0x36, true}, {'N', 0x37, true},
|
||||||
|
{'N', 0x38, false}, {'N', 0x39, true}, {'N', 0x3A, true}, {'N', 0x3B, true},
|
||||||
|
{'N', 0x3C, true}, {'N', 0x3D, false}, {'N', 0x3E, false}, {'N', 0x3F, false}}};
|
||||||
|
|
||||||
|
// Bootrom sets all these keys when executed, but later some of the normal keys get overwritten
|
||||||
|
// by other applications e.g. process9. These normal keys thus aren't used by any application
|
||||||
|
// and have no value for emulation
|
||||||
|
|
||||||
|
const std::string filepath = FileUtil::GetUserPath(FileUtil::UserPath::SysDataDir) + BOOTROM9;
|
||||||
|
auto file = FileUtil::IOFile(filepath, "rb");
|
||||||
|
if (!file) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::size_t length = file.GetSize();
|
||||||
|
if (length != 65536) {
|
||||||
|
LOG_ERROR(HW_AES, "Bootrom9 size is wrong: {}", length);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr std::size_t KEY_SECTION_START = 55760;
|
||||||
|
file.Seek(KEY_SECTION_START, SEEK_SET); // Jump to the key section
|
||||||
|
|
||||||
|
AESKey new_key;
|
||||||
|
for (const auto& key : keys) {
|
||||||
|
if (!key.same_as_before) {
|
||||||
|
file.ReadArray(new_key.data(), new_key.size());
|
||||||
|
if (!file) {
|
||||||
|
LOG_ERROR(HW_AES, "Reading from Bootrom9 failed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_DEBUG(HW_AES, "Loaded Slot{:#02x} Key{} from Bootrom9.", key.slot_id, key.key_type);
|
||||||
|
|
||||||
|
switch (key.key_type) {
|
||||||
|
case 'X':
|
||||||
|
key_slots.at(key.slot_id).SetKeyX(new_key);
|
||||||
|
break;
|
||||||
|
case 'Y':
|
||||||
|
key_slots.at(key.slot_id).SetKeyY(new_key);
|
||||||
|
break;
|
||||||
|
case 'N':
|
||||||
|
key_slots.at(key.slot_id).SetNormalKey(new_key);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
LOG_ERROR(HW_AES, "Invalid key type {}", key.key_type);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef todotodo
|
||||||
void LoadPresetKeys() {
|
void LoadPresetKeys() {
|
||||||
auto s = GetKeysStream();
|
auto s = GetKeysStream();
|
||||||
|
|
||||||
@ -276,6 +351,112 @@ void LoadPresetKeys() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#else
|
||||||
|
void LoadPresetKeys() {
|
||||||
|
const std::string filepath = FileUtil::GetUserPath(FileUtil::UserPath::SysDataDir) + AES_KEYS;
|
||||||
|
FileUtil::CreateFullPath(filepath); // Create path if not already created
|
||||||
|
|
||||||
|
boost::iostreams::stream<boost::iostreams::file_descriptor_source> file;
|
||||||
|
FileUtil::OpenFStream<std::ios_base::in>(file, filepath);
|
||||||
|
if (!file.is_open()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (!file.eof()) {
|
||||||
|
std::string line;
|
||||||
|
std::getline(file, line);
|
||||||
|
|
||||||
|
// Ignore empty or commented lines.
|
||||||
|
if (line.empty() || line.starts_with("#")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto parts = Common::SplitString(line, '=');
|
||||||
|
if (parts.size() != 2) {
|
||||||
|
LOG_ERROR(HW_AES, "Failed to parse {}", line);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string& name = parts[0];
|
||||||
|
|
||||||
|
const auto nfc_secret = ParseNfcSecretName(name);
|
||||||
|
if (nfc_secret) {
|
||||||
|
auto value = HexToVector(parts[1]);
|
||||||
|
if (nfc_secret->first >= nfc_secrets.size()) {
|
||||||
|
LOG_ERROR(HW_AES, "Invalid NFC secret index {}", nfc_secret->first);
|
||||||
|
} else if (nfc_secret->second == "Phrase") {
|
||||||
|
nfc_secrets[nfc_secret->first].phrase = value;
|
||||||
|
} else if (nfc_secret->second == "Seed") {
|
||||||
|
nfc_secrets[nfc_secret->first].seed = value;
|
||||||
|
} else if (nfc_secret->second == "HmacKey") {
|
||||||
|
nfc_secrets[nfc_secret->first].hmac_key = value;
|
||||||
|
} else {
|
||||||
|
LOG_ERROR(HW_AES, "Invalid NFC secret '{}'", name);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
AESKey key;
|
||||||
|
try {
|
||||||
|
key = HexToKey(parts[1]);
|
||||||
|
} catch (const std::logic_error& e) {
|
||||||
|
LOG_ERROR(HW_AES, "Invalid key {}: {}", parts[1], e.what());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto common_key = ParseCommonKeyName(name);
|
||||||
|
if (common_key) {
|
||||||
|
if (common_key >= common_key_y_slots.size()) {
|
||||||
|
LOG_ERROR(HW_AES, "Invalid common key index {}", common_key.value());
|
||||||
|
} else {
|
||||||
|
common_key_y_slots[common_key.value()] = key;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name == "dlpKeyY") {
|
||||||
|
dlp_nfc_key_y_slots[DlpNfcKeyY::Dlp] = key;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name == "nfcKeyY") {
|
||||||
|
dlp_nfc_key_y_slots[DlpNfcKeyY::Nfc] = key;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name == "nfcIv") {
|
||||||
|
nfc_iv = key;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto key_slot = ParseKeySlotName(name);
|
||||||
|
if (!key_slot) {
|
||||||
|
LOG_ERROR(HW_AES, "Invalid key name '{}'", name);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key_slot->first >= MaxKeySlotID) {
|
||||||
|
LOG_ERROR(HW_AES, "Out of range key slot ID {:#X}", key_slot->first);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (key_slot->second) {
|
||||||
|
case 'X':
|
||||||
|
key_slots.at(key_slot->first).SetKeyX(key);
|
||||||
|
break;
|
||||||
|
case 'Y':
|
||||||
|
key_slots.at(key_slot->first).SetKeyY(key);
|
||||||
|
break;
|
||||||
|
case 'N':
|
||||||
|
key_slots.at(key_slot->first).SetNormalKey(key);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
LOG_ERROR(HW_AES, "Invalid key type '{}'", key_slot->second);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
@ -305,6 +486,8 @@ void InitKeys(bool force) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
initialized = true;
|
initialized = true;
|
||||||
|
HW::RSA::InitSlots();
|
||||||
|
LoadBootromKeys();
|
||||||
LoadPresetKeys();
|
LoadPresetKeys();
|
||||||
movable_key.SetKeyX(key_slots[0x35].x);
|
movable_key.SetKeyX(key_slots[0x35].x);
|
||||||
movable_cmac.SetKeyX(key_slots[0x35].x);
|
movable_cmac.SetKeyX(key_slots[0x35].x);
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
#include <boost/iostreams/device/file_descriptor.hpp>
|
#include <boost/iostreams/device/file_descriptor.hpp>
|
||||||
#include <boost/iostreams/stream.hpp>
|
#include <boost/iostreams/stream.hpp>
|
||||||
#include "common/assert.h"
|
#include "common/assert.h"
|
||||||
|
#include <common/string_util.h>
|
||||||
#include "common/common_paths.h"
|
#include "common/common_paths.h"
|
||||||
#include "common/file_util.h"
|
#include "common/file_util.h"
|
||||||
#include "common/logging/log.h"
|
#include "common/logging/log.h"
|
||||||
|
|||||||
@ -21,6 +21,19 @@
|
|||||||
|
|
||||||
namespace HW::RSA {
|
namespace HW::RSA {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
std::vector<u8> HexToBytes(const std::string& hex) {
|
||||||
|
std::vector<u8> bytes;
|
||||||
|
|
||||||
|
for (unsigned int i = 0; i < hex.length(); i += 2) {
|
||||||
|
std::string byteString = hex.substr(i, 2);
|
||||||
|
u8 byte = static_cast<u8>(std::strtol(byteString.c_str(), nullptr, 16));
|
||||||
|
bytes.push_back(byte);
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
|
};
|
||||||
|
} // namespace
|
||||||
|
|
||||||
constexpr std::size_t SlotSize = 4;
|
constexpr std::size_t SlotSize = 4;
|
||||||
std::array<RsaSlot, SlotSize> rsa_slots;
|
std::array<RsaSlot, SlotSize> rsa_slots;
|
||||||
|
|
||||||
@ -92,6 +105,23 @@ std::optional<std::pair<std::size_t, char>> ParseKeySlotName(const std::string&
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::vector<u8> RsaSlot::GetSignature(std::span<const u8> message) const {
|
||||||
|
CryptoPP::Integer sig =
|
||||||
|
CryptoPP::ModularExponentiation(CryptoPP::Integer(message.data(), message.size()),
|
||||||
|
CryptoPP::Integer(exponent.data(), exponent.size()),
|
||||||
|
CryptoPP::Integer(modulus.data(), modulus.size()));
|
||||||
|
std::stringstream ss;
|
||||||
|
ss << std::hex << sig;
|
||||||
|
CryptoPP::HexDecoder decoder;
|
||||||
|
decoder.Put(reinterpret_cast<unsigned char*>(ss.str().data()), ss.str().size());
|
||||||
|
decoder.MessageEnd();
|
||||||
|
std::vector<u8> result(decoder.MaxRetrievable());
|
||||||
|
decoder.Get(result.data(), result.size());
|
||||||
|
return HexToBytes(ss.str());
|
||||||
|
}
|
||||||
|
|
||||||
|
// todotodo
|
||||||
|
#ifdef todotodo
|
||||||
void InitSlots() {
|
void InitSlots() {
|
||||||
static bool initialized = false;
|
static bool initialized = false;
|
||||||
if (initialized)
|
if (initialized)
|
||||||
@ -193,6 +223,42 @@ void InitSlots() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
//--
|
||||||
|
void InitSlots() {
|
||||||
|
static bool initialized = false;
|
||||||
|
if (initialized)
|
||||||
|
return;
|
||||||
|
initialized = true;
|
||||||
|
|
||||||
|
const std::string filepath = FileUtil::GetUserPath(FileUtil::UserPath::SysDataDir) + BOOTROM9;
|
||||||
|
FileUtil::IOFile file(filepath, "rb");
|
||||||
|
if (!file) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::size_t length = file.GetSize();
|
||||||
|
if (length != 65536) {
|
||||||
|
LOG_ERROR(HW_AES, "Bootrom9 size is wrong: {}", length);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr std::size_t RSA_MODULUS_POS = 0xB3E0;
|
||||||
|
file.Seek(RSA_MODULUS_POS, SEEK_SET);
|
||||||
|
std::vector<u8> modulus(256);
|
||||||
|
file.ReadArray(modulus.data(), modulus.size());
|
||||||
|
|
||||||
|
constexpr std::size_t RSA_EXPONENT_POS = 0xB4E0;
|
||||||
|
file.Seek(RSA_EXPONENT_POS, SEEK_SET);
|
||||||
|
std::vector<u8> exponent(256);
|
||||||
|
file.ReadArray(exponent.data(), exponent.size());
|
||||||
|
|
||||||
|
rsa_slots[0] = RsaSlot(std::move(exponent), std::move(modulus));
|
||||||
|
// TODO(B3N30): Initalize the other slots. But since they aren't used at all, we can skip them
|
||||||
|
// for now
|
||||||
|
}
|
||||||
|
//--
|
||||||
|
|
||||||
static RsaSlot empty_slot;
|
static RsaSlot empty_slot;
|
||||||
const RsaSlot& GetSlot(std::size_t slot_id) {
|
const RsaSlot& GetSlot(std::size_t slot_id) {
|
||||||
@ -201,6 +267,31 @@ const RsaSlot& GetSlot(std::size_t slot_id) {
|
|||||||
return rsa_slots[slot_id];
|
return rsa_slots[slot_id];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::vector<u8> CreateASN1Message(std::span<const u8> data) {
|
||||||
|
static constexpr std::array<u8, 224> asn1_header = {
|
||||||
|
{0x00, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x30, 0x31, 0x30, 0x0D, 0x06,
|
||||||
|
0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01, 0x05, 0x00, 0x04, 0x20}};
|
||||||
|
|
||||||
|
std::vector<u8> message(asn1_header.begin(), asn1_header.end());
|
||||||
|
CryptoPP::SHA256 sha;
|
||||||
|
message.resize(message.size() + CryptoPP::SHA256::DIGESTSIZE);
|
||||||
|
sha.CalculateDigest(message.data() + asn1_header.size(), data.data(), data.size());
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
const RsaSlot& GetTicketWrapSlot() {
|
const RsaSlot& GetTicketWrapSlot() {
|
||||||
return ticket_wrap_slot;
|
return ticket_wrap_slot;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,6 +23,8 @@ public:
|
|||||||
|
|
||||||
bool Verify(std::span<const u8> message, std::span<const u8> signature) const;
|
bool Verify(std::span<const u8> message, std::span<const u8> signature) const;
|
||||||
|
|
||||||
|
std::vector<u8> GetSignature(std::span<const u8> message) const;
|
||||||
|
|
||||||
explicit operator bool() const {
|
explicit operator bool() const {
|
||||||
// TODO(B3N30): Maybe check if exponent and modulus are vailid
|
// TODO(B3N30): Maybe check if exponent and modulus are vailid
|
||||||
return init;
|
return init;
|
||||||
@ -68,4 +70,5 @@ const RsaSlot& GetTicketWrapSlot();
|
|||||||
const RsaSlot& GetSecureInfoSlot();
|
const RsaSlot& GetSecureInfoSlot();
|
||||||
const RsaSlot& GetLocalFriendCodeSeedSlot();
|
const RsaSlot& GetLocalFriendCodeSeedSlot();
|
||||||
|
|
||||||
|
std::vector<u8> CreateASN1Message(std::span<const u8> data);
|
||||||
} // namespace HW::RSA
|
} // namespace HW::RSA
|
||||||
|
|||||||
@ -26,6 +26,23 @@ public:
|
|||||||
Apploader_Artic(Core::System& system_, const std::string& server_addr, u16 server_port,
|
Apploader_Artic(Core::System& system_, const std::string& server_addr, u16 server_port,
|
||||||
ArticInitMode init_mode);
|
ArticInitMode init_mode);
|
||||||
|
|
||||||
|
Apploader_Artic(Core::System& system_, const std::string& server_addr, u16 server_port)
|
||||||
|
: AppLoader(system_, FileUtil::IOFile()) {
|
||||||
|
client = std::make_shared<Network::ArticBase::Client>(server_addr, server_port);
|
||||||
|
client->SetCommunicationErrorCallback([&system_](const std::string& msg) {
|
||||||
|
system_.SetStatus(Core::System::ResultStatus::ErrorArticDisconnected,
|
||||||
|
msg.empty() ? nullptr : msg.c_str());
|
||||||
|
});
|
||||||
|
client->SetArticReportTrafficCallback(
|
||||||
|
[&system_](u32 bytes) { system_.ReportArticTraffic(bytes); });
|
||||||
|
client->SetReportArticEventCallback([&system_](u64 event) {
|
||||||
|
Core::PerfStats::PerfArticEventBits ev =
|
||||||
|
static_cast<Core::PerfStats::PerfArticEventBits>(event & 0xFFFFFFFF);
|
||||||
|
bool set = (event > 32) != 0;
|
||||||
|
system_.ReportPerfArticEvent(ev, set);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
~Apploader_Artic() override;
|
~Apploader_Artic() override;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -48,7 +48,7 @@ FileType GuessFromExtension(const std::string& extension_) {
|
|||||||
if (extension == ".elf" || extension == ".axf")
|
if (extension == ".elf" || extension == ".axf")
|
||||||
return FileType::ELF;
|
return FileType::ELF;
|
||||||
|
|
||||||
if (extension == ".cci")
|
if (extension == ".cci" || extension == ".3ds")
|
||||||
return FileType::CCI;
|
return FileType::CCI;
|
||||||
|
|
||||||
if (extension == ".cxi" || extension == ".app")
|
if (extension == ".cxi" || extension == ".app")
|
||||||
@ -112,12 +112,14 @@ static std::unique_ptr<AppLoader> GetFileLoader(Core::System& system, FileUtil::
|
|||||||
return std::make_unique<AppLoader_NCCH>(system, std::move(file), filepath);
|
return std::make_unique<AppLoader_NCCH>(system, std::move(file), filepath);
|
||||||
|
|
||||||
case FileType::ARTIC: {
|
case FileType::ARTIC: {
|
||||||
|
#ifdef todotodo
|
||||||
Apploader_Artic::ArticInitMode mode = Apploader_Artic::ArticInitMode::NONE;
|
Apploader_Artic::ArticInitMode mode = Apploader_Artic::ArticInitMode::NONE;
|
||||||
if (filename.starts_with("articinio://")) {
|
if (filename.starts_with("articinio://")) {
|
||||||
mode = Apploader_Artic::ArticInitMode::O3DS;
|
mode = Apploader_Artic::ArticInitMode::O3DS;
|
||||||
} else if (filename.starts_with("articinin://")) {
|
} else if (filename.starts_with("articinin://")) {
|
||||||
mode = Apploader_Artic::ArticInitMode::N3DS;
|
mode = Apploader_Artic::ArticInitMode::N3DS;
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
auto strToUInt = [](const std::string& str) -> int {
|
auto strToUInt = [](const std::string& str) -> int {
|
||||||
char* pEnd = NULL;
|
char* pEnd = NULL;
|
||||||
unsigned long ul = ::strtoul(str.c_str(), &pEnd, 10);
|
unsigned long ul = ::strtoul(str.c_str(), &pEnd, 10);
|
||||||
@ -136,7 +138,11 @@ static std::unique_ptr<AppLoader> GetFileLoader(Core::System& system, FileUtil::
|
|||||||
server_addr = server_addr.substr(0, pos);
|
server_addr = server_addr.substr(0, pos);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#ifdef todotodo
|
||||||
return std::make_unique<Apploader_Artic>(system, server_addr, port, mode);
|
return std::make_unique<Apploader_Artic>(system, server_addr, port, mode);
|
||||||
|
#else
|
||||||
|
return std::make_unique<Apploader_Artic>(system, server_addr, port);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user