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>
|
||||
<key>CFBundleTypeExtensions</key>
|
||||
<array>
|
||||
<string>3ds</string>
|
||||
<string>3dsx</string>
|
||||
<string>cci</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>
|
||||
<icon name="azahar"/>
|
||||
<glob pattern="*.cci"/>
|
||||
<glob pattern="*.3ds"/>
|
||||
<magic><match value="NCSD" type="string" offset="256"/></magic>
|
||||
</mime-type>
|
||||
|
||||
|
||||
@ -63,6 +63,7 @@ android {
|
||||
// The application ID refers to Lime3DS to allow for
|
||||
// the Play Store listing, which was originally set up for Lime3DS, to still be used.
|
||||
applicationId = "io.github.lime3ds.android"
|
||||
|
||||
minSdk = 28
|
||||
targetSdk = 35
|
||||
versionCode = autoVersion
|
||||
|
||||
@ -186,6 +186,8 @@ object NativeLibrary {
|
||||
|
||||
external fun unlinkConsole()
|
||||
|
||||
external fun downloadTitleFromNus(title: Long): InstallStatus
|
||||
|
||||
private var coreErrorAlertResult = false
|
||||
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 homeViewModel: HomeViewModel by activityViewModels()
|
||||
private var show3DSFileWarning: Boolean = true
|
||||
private var show3DSFileWarning: Boolean = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
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
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
@ -121,14 +121,8 @@ class HomeSettingsFragment : Fragment() {
|
||||
}
|
||||
),
|
||||
HomeSetting(
|
||||
R.string.install_game_content,
|
||||
R.string.install_game_content_description,
|
||||
R.drawable.ic_install,
|
||||
{ mainActivity.ciaFileInstaller.launch(true) }
|
||||
),
|
||||
HomeSetting(
|
||||
R.string.setup_system_files,
|
||||
R.string.setup_system_files_description,
|
||||
R.string.system_files,
|
||||
R.string.system_files_description,
|
||||
R.drawable.ic_system_update,
|
||||
{
|
||||
exitTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
|
||||
@ -136,6 +130,12 @@ class HomeSettingsFragment : Fragment() {
|
||||
?.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(
|
||||
R.string.share_log,
|
||||
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
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
package org.citra.citra_emu.fragments
|
||||
|
||||
import android.content.DialogInterface
|
||||
import android.content.res.Resources
|
||||
import android.os.Bundle
|
||||
import android.text.Html
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.view.Gravity
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.AdapterView
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.RadioButton
|
||||
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.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.navigation.findNavController
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.progressindicator.CircularProgressIndicator
|
||||
import com.google.android.material.textview.MaterialTextView
|
||||
import com.google.android.material.textfield.MaterialAutoCompleteTextView
|
||||
import com.google.android.material.transition.MaterialSharedAxis
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.citra.citra_emu.CitraApplication
|
||||
import org.citra.citra_emu.HomeNavigationDirections
|
||||
import org.citra.citra_emu.NativeLibrary
|
||||
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.features.settings.model.Settings
|
||||
import org.citra.citra_emu.model.Game
|
||||
import org.citra.citra_emu.utils.SystemSaveGame
|
||||
import org.citra.citra_emu.viewmodel.GamesViewModel
|
||||
import org.citra.citra_emu.viewmodel.HomeViewModel
|
||||
import org.citra.citra_emu.viewmodel.SystemFilesViewModel
|
||||
|
||||
class SystemFilesFragment : Fragment() {
|
||||
private var _binding: FragmentSystemFilesBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
private val homeViewModel: HomeViewModel by activityViewModels()
|
||||
private val systemFilesViewModel: SystemFilesViewModel 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 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?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@ -77,15 +92,61 @@ class SystemFilesFragment : Fragment() {
|
||||
homeViewModel.setNavigationVisibility(visible = false, animated = true)
|
||||
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
|
||||
// https://github.com/material-components/material-components-android/issues/1464
|
||||
binding.dropdownSystemType.isSaveEnabled = false
|
||||
binding.dropdownSystemRegion.isSaveEnabled = false
|
||||
binding.dropdownSystemRegionStart.isSaveEnabled = false
|
||||
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.CREATED) {
|
||||
systemFilesViewModel.shouldRefresh.collect {
|
||||
if (it) {
|
||||
reloadUi()
|
||||
systemFilesViewModel.setShouldRefresh(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
reloadUi()
|
||||
if (savedInstanceState != null) {
|
||||
setDropdownSelection(
|
||||
binding.dropdownSystemType,
|
||||
systemTypeDropdown,
|
||||
savedInstanceState.getInt(SYS_TYPE)
|
||||
)
|
||||
setDropdownSelection(
|
||||
binding.dropdownSystemRegion,
|
||||
systemRegionDropdown,
|
||||
savedInstanceState.getInt(REGION)
|
||||
)
|
||||
binding.dropdownSystemRegionStart
|
||||
.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() {
|
||||
@ -98,41 +159,6 @@ class SystemFilesFragment : Fragment() {
|
||||
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() {
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext)
|
||||
|
||||
@ -150,167 +176,31 @@ class SystemFilesFragment : Fragment() {
|
||||
gamesViewModel.setShouldSwapData(true)
|
||||
}
|
||||
|
||||
binding.setupSystemFilesDescription?.apply {
|
||||
text = HtmlCompat.fromHtml(
|
||||
context.getString(R.string.setup_system_files_preamble),
|
||||
HtmlCompat.FROM_HTML_MODE_COMPACT
|
||||
)
|
||||
movementMethod = LinkMovementMethod.getInstance()
|
||||
}
|
||||
|
||||
binding.buttonUnlinkConsoleData.isEnabled = NativeLibrary.isFullConsoleLinked()
|
||||
binding.buttonUnlinkConsoleData.setOnClickListener {
|
||||
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()
|
||||
}
|
||||
}
|
||||
if (!NativeLibrary.areKeysAvailable()) {
|
||||
binding.apply {
|
||||
systemType.isEnabled = false
|
||||
systemRegion.isEnabled = false
|
||||
buttonDownloadHomeMenu.isEnabled = false
|
||||
textKeysMissing.visibility = View.VISIBLE
|
||||
textKeysMissingHelp.visibility = View.VISIBLE
|
||||
textKeysMissingHelp.text =
|
||||
Html.fromHtml(getString(R.string.how_to_get_keys), Html.FROM_HTML_MODE_LEGACY)
|
||||
textKeysMissingHelp.movementMethod = LinkMovementMethod.getInstance()
|
||||
}
|
||||
} else {
|
||||
populateDownloadOptions()
|
||||
}
|
||||
|
||||
binding.buttonDownloadHomeMenu.setOnClickListener {
|
||||
val titleIds = NativeLibrary.getSystemTitleIds(
|
||||
systemTypeDropdown.getValue(resources),
|
||||
systemRegionDropdown.getValue(resources)
|
||||
)
|
||||
|
||||
DownloadSystemFilesDialogFragment.newInstance(titleIds).show(
|
||||
childFragmentManager,
|
||||
DownloadSystemFilesDialogFragment.TAG
|
||||
)
|
||||
}
|
||||
|
||||
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() {
|
||||
regionValues = resources.getIntArray(R.array.systemFileRegionValues)
|
||||
val regionEntries = resources.getStringArray(R.array.systemFileRegions)
|
||||
@ -350,4 +285,30 @@ class SystemFilesFragment : Fragment() {
|
||||
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 extensions: Set<String> = HashSet(
|
||||
listOf("3dsx", "elf", "axf", "cci", "cxi", "app")
|
||||
listOf("3ds", "3dsx", "elf", "axf", "cci", "cxi", "app")
|
||||
)
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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() {
|
||||
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_height="?attr/actionBarSize"
|
||||
app:navigationIcon="@drawable/ic_back"
|
||||
app:title="@string/setup_system_files" />
|
||||
app:title="@string/system_files" />
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
@ -39,48 +39,77 @@
|
||||
android:paddingBottom="16dp">
|
||||
|
||||
<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"
|
||||
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_set_up_system_files"
|
||||
android:id="@+id/button_download_home_menu"
|
||||
style="@style/Widget.Material3.Button.UnelevatedButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/setup_tool_connect" />
|
||||
|
||||
<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" />
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/download" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
style="@style/TextAppearance.Material3.TitleMedium"
|
||||
android:id="@+id/text_keys_missing"
|
||||
android:layout_width="match_parent"
|
||||
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:textAlignment="viewStart" />
|
||||
|
||||
|
||||
@ -297,6 +297,17 @@
|
||||
<item>6</item>
|
||||
</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">
|
||||
<item>@string/mono</item>
|
||||
<item>@string/stereo</item>
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
<resources>
|
||||
|
||||
<!-- 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_notification_channel_name" translatable="false">Azahar</string>
|
||||
<string name="app_notification_channel_id" translatable="false">Azahar</string>
|
||||
@ -148,12 +148,15 @@
|
||||
|
||||
<!-- System files strings -->
|
||||
<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="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_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="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="download_system_files">Download System Files</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_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_preparing">Preparing setup, please wait...</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="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_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) -->
|
||||
<string name="generic_buttons">Buttons</string>
|
||||
|
||||
@ -1250,11 +1250,17 @@ bool GMainWindow::LoadROM(const QString& filename) {
|
||||
break;
|
||||
|
||||
case Core::System::ResultStatus::ErrorLoader_ErrorEncrypted: {
|
||||
QMessageBox::critical(this, tr("App Encrypted"),
|
||||
tr("Your app is encrypted. <br/>"
|
||||
"<a "
|
||||
"href='https://azahar-emu.org/blog/game-loading-changes/'>"
|
||||
"Please check our blog for more info.</a>"));
|
||||
QMessageBox::critical(
|
||||
this, tr("ROM Encrypted"),
|
||||
tr("Your ROM is encrypted. <br/>Please follow the guides to redump your "
|
||||
"<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;
|
||||
}
|
||||
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));
|
||||
break;
|
||||
case Service::AM::InstallStatus::ErrorEncrypted:
|
||||
QMessageBox::critical(this, tr("CIA Encrypted"),
|
||||
tr("Your CIA file is encrypted.<br/>"
|
||||
"<a "
|
||||
"href='https://azahar-emu.org/blog/game-loading-changes/'>"
|
||||
"Please check our blog for more info.</a>"));
|
||||
QMessageBox::critical(this, tr("Encrypted File"),
|
||||
tr("%1 must be decrypted "
|
||||
"before being used with Azahar. A real 3DS is required.")
|
||||
.arg(filename));
|
||||
break;
|
||||
case Service::AM::InstallStatus::ErrorFileNotFound:
|
||||
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;
|
||||
}
|
||||
|
||||
static const std::array<std::string, 8> AcceptedExtensions = {"cci", "cxi", "bin", "3dsx",
|
||||
"app", "elf", "axf"};
|
||||
static const std::array<std::string, 8> AcceptedExtensions = {"cci", "3ds", "cxi", "bin",
|
||||
"3dsx", "app", "elf", "axf"};
|
||||
|
||||
static bool IsCorrectFileExtension(const QMimeData* mime) {
|
||||
const QString& filename = mime->urls().at(0).toLocalFile();
|
||||
|
||||
@ -217,16 +217,6 @@
|
||||
</widget>
|
||||
</item>
|
||||
<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">
|
||||
<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>
|
||||
@ -236,14 +226,14 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<item row="3" column="0">
|
||||
<widget class="QCheckBox" name="toggle_renderer_debug">
|
||||
<property name="text">
|
||||
<string>Enable debug renderer</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="0">
|
||||
<item row="4" column="0">
|
||||
<widget class="QCheckBox" name="toggle_dump_command_buffers">
|
||||
<property name="text">
|
||||
<string>Dump command buffers</string>
|
||||
@ -253,33 +243,43 @@
|
||||
</layout>
|
||||
</widget>
|
||||
</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>
|
||||
<widget class="QGroupBox" name="groupBox_5">
|
||||
<property name="title">
|
||||
<string>Miscellaneous</string>
|
||||
<widget class="QLabel" name="label_cpu_clock_info">
|
||||
<property name="text">
|
||||
<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>
|
||||
<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>
|
||||
|
||||
@ -238,16 +238,8 @@ ConfigureSystem::ConfigureSystem(Core::System& system_, QWidget* parent)
|
||||
connect(ui->button_regenerate_console_id, &QPushButton::clicked, this,
|
||||
&ConfigureSystem::RefreshConsoleID);
|
||||
connect(ui->button_regenerate_mac, &QPushButton::clicked, this, &ConfigureSystem::RefreshMAC);
|
||||
connect(ui->button_linked_console, &QPushButton::clicked, this,
|
||||
&ConfigureSystem::UnlinkConsole);
|
||||
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_start_download, &QPushButton::clicked, this,
|
||||
&ConfigureSystem::DownloadFromNUS);
|
||||
|
||||
connect(ui->button_secure_info, &QPushButton::clicked, this, [this] {
|
||||
ui->button_secure_info->setEnabled(false);
|
||||
@ -255,7 +247,11 @@ ConfigureSystem::ConfigureSystem(Core::System& system_, QWidget* parent)
|
||||
this, tr("Select SecureInfo_A/B"), QString(),
|
||||
tr("SecureInfo_A/B (SecureInfo_A SecureInfo_B);;All Files (*.*)"));
|
||||
ui->button_secure_info->setEnabled(true);
|
||||
#ifdef todotodo
|
||||
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] {
|
||||
ui->button_friend_code_seed->setEnabled(false);
|
||||
@ -264,6 +260,7 @@ ConfigureSystem::ConfigureSystem(Core::System& system_, QWidget* parent)
|
||||
tr("LocalFriendCodeSeed_A/B (LocalFriendCodeSeed_A "
|
||||
"LocalFriendCodeSeed_B);;All Files (*.*)"));
|
||||
ui->button_friend_code_seed->setEnabled(true);
|
||||
#ifdef todotodo
|
||||
InstallSecureData(file_path_qtstr.toStdString(),
|
||||
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 (*.*)"));
|
||||
ui->button_movable->setEnabled(true);
|
||||
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++) {
|
||||
@ -288,10 +295,36 @@ ConfigureSystem::ConfigureSystem(Core::System& system_, QWidget* parent)
|
||||
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();
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
@ -300,19 +333,6 @@ ConfigureSystem::~ConfigureSystem() = default;
|
||||
void ConfigureSystem::SetConfiguration() {
|
||||
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()));
|
||||
QDateTime date_time;
|
||||
date_time.setSecsSinceEpoch(Settings::values.init_time.GetValue());
|
||||
@ -374,7 +394,6 @@ void ConfigureSystem::ReadSystemSettings() {
|
||||
// set the country code
|
||||
country_code = cfg->GetCountryCode();
|
||||
ui->combo_country->setCurrentIndex(ui->combo_country->findData(country_code));
|
||||
CheckCountryValid(country_code);
|
||||
|
||||
// set whether system setup is needed
|
||||
system_setup = cfg->IsSystemSetupNeeded();
|
||||
@ -391,16 +410,15 @@ void ConfigureSystem::ReadSystemSettings() {
|
||||
play_coin = Service::PTM::Module::GetPlayCoins();
|
||||
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
|
||||
RefreshSecureDataStatus();
|
||||
}
|
||||
|
||||
void ConfigureSystem::ApplyConfiguration() {
|
||||
if (enabled) {
|
||||
ConfigurationShared::ApplyPerGameSetting(&Settings::values.region_value,
|
||||
ui->region_combobox,
|
||||
[](s32 index) { return index - 1; });
|
||||
|
||||
bool modified = false;
|
||||
|
||||
// apply username
|
||||
@ -591,51 +609,6 @@ void ConfigureSystem::RefreshMAC() {
|
||||
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) {
|
||||
std::string from =
|
||||
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::Copy(from, to);
|
||||
HW::UniqueData::InvalidateSecureData();
|
||||
cfg->InvalidateSecureData();
|
||||
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() {
|
||||
auto status_to_str = [](HW::UniqueData::SecureDataLoadStatus status) {
|
||||
switch (status) {
|
||||
@ -676,16 +664,38 @@ void ConfigureSystem::RefreshSecureDataStatus() {
|
||||
tr((std::string("Status: ") + status_to_str(HW::UniqueData::LoadOTP())).c_str()));
|
||||
ui->label_movable_status->setText(
|
||||
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() {
|
||||
ui->retranslateUi(this);
|
||||
@ -698,7 +708,6 @@ void ConfigureSystem::SetupPerGameUI() {
|
||||
ui->toggle_lle_applets->setEnabled(Settings::values.lle_applets.UsingGlobal());
|
||||
ui->enable_required_online_lle_modules->setEnabled(
|
||||
Settings::values.enable_required_online_lle_modules.UsingGlobal());
|
||||
ui->region_combobox->setEnabled(Settings::values.region_value.UsingGlobal());
|
||||
return;
|
||||
}
|
||||
|
||||
@ -710,7 +719,6 @@ void ConfigureSystem::SetupPerGameUI() {
|
||||
ui->label_init_ticks_type->setVisible(false);
|
||||
ui->label_init_ticks_value->setVisible(false);
|
||||
ui->label_console_id->setVisible(false);
|
||||
ui->label_mac->setVisible(false);
|
||||
ui->label_sound->setVisible(false);
|
||||
ui->label_language->setVisible(false);
|
||||
ui->label_country->setVisible(false);
|
||||
@ -732,7 +740,6 @@ void ConfigureSystem::SetupPerGameUI() {
|
||||
ui->edit_init_ticks_value->setVisible(false);
|
||||
ui->toggle_system_setup->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
|
||||
// to a chainloaded app with specific parameters. Don't allow
|
||||
// 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->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,
|
||||
is_new_3ds);
|
||||
@ -749,7 +758,45 @@ void ConfigureSystem::SetupPerGameUI() {
|
||||
ConfigurationShared::SetColoredTristate(ui->enable_required_online_lle_modules,
|
||||
Settings::values.enable_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 InstallSecureData(const std::string& from_path, const std::string& to_path);
|
||||
void InstallCTCert(const std::string& from_path);
|
||||
void RefreshSecureDataStatus();
|
||||
|
||||
void SetupPerGameUI();
|
||||
|
||||
void DownloadFromNUS();
|
||||
|
||||
private:
|
||||
std::unique_ptr<Ui::ConfigureSystem> ui;
|
||||
Core::System& system;
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>653</width>
|
||||
<width>535</width>
|
||||
<height>619</height>
|
||||
</rect>
|
||||
</property>
|
||||
@ -14,26 +14,11 @@
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<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>
|
||||
<widget class="QScrollArea" name="scrollArea">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>660</width>
|
||||
<width>0</width>
|
||||
<height>480</height>
|
||||
</size>
|
||||
</property>
|
||||
@ -64,83 +49,21 @@
|
||||
<string>System Settings</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="0">
|
||||
<item row="1" column="0">
|
||||
<widget class="QCheckBox" name="toggle_new_3ds">
|
||||
<property name="text">
|
||||
<string>Enable New 3DS mode</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<item row="2" column="0">
|
||||
<widget class="QCheckBox" name="toggle_lle_applets">
|
||||
<property name="text">
|
||||
<string>Use LLE applets (if installed)</string>
|
||||
</property>
|
||||
</widget>
|
||||
</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">
|
||||
<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">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
|
||||
@ -153,21 +76,21 @@ online features (if installed)</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<item row="3" column="0">
|
||||
<widget class="QLabel" name="label_username">
|
||||
<property name="text">
|
||||
<string>Username</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="0">
|
||||
<item row="4" column="0">
|
||||
<widget class="QLabel" name="label_birthday">
|
||||
<property name="text">
|
||||
<string>Birthday</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="1">
|
||||
<item row="4" column="1">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_birthday2">
|
||||
<item>
|
||||
<widget class="QComboBox" name="combo_birthmonth">
|
||||
@ -238,14 +161,14 @@ online features (if installed)</string>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="6" column="0">
|
||||
<item row="5" column="0">
|
||||
<widget class="QLabel" name="label_language">
|
||||
<property name="text">
|
||||
<string>Language</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="6" column="1">
|
||||
<item row="5" column="1">
|
||||
<widget class="QComboBox" name="combo_language">
|
||||
<property name="toolTip">
|
||||
<string>Note: this can be overridden when region setting is auto-select</string>
|
||||
@ -312,14 +235,14 @@ online features (if installed)</string>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="7" column="0">
|
||||
<item row="6" column="0">
|
||||
<widget class="QLabel" name="label_sound">
|
||||
<property name="text">
|
||||
<string>Sound output mode</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="7" column="1">
|
||||
<item row="6" column="1">
|
||||
<widget class="QComboBox" name="combo_sound">
|
||||
<item>
|
||||
<property name="text">
|
||||
@ -338,31 +261,24 @@ online features (if installed)</string>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="8" column="0">
|
||||
<item row="7" column="0">
|
||||
<widget class="QLabel" name="label_country">
|
||||
<property name="text">
|
||||
<string>Country</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="8" column="1">
|
||||
<item row="7" column="1">
|
||||
<widget class="QComboBox" name="combo_country"/>
|
||||
</item>
|
||||
<item row="9" column="1">
|
||||
<widget class="QLabel" name="label_country_invalid">
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="10" column="0">
|
||||
<item row="8" column="0">
|
||||
<widget class="QLabel" name="label_init_clock">
|
||||
<property name="text">
|
||||
<string>Clock</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="10" column="1">
|
||||
<item row="8" column="1">
|
||||
<widget class="QComboBox" name="combo_init_clock">
|
||||
<item>
|
||||
<property name="text">
|
||||
@ -376,28 +292,28 @@ online features (if installed)</string>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="11" column="0">
|
||||
<item row="9" column="0">
|
||||
<widget class="QLabel" name="label_init_time">
|
||||
<property name="text">
|
||||
<string>Startup time</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="11" column="1">
|
||||
<item row="9" column="1">
|
||||
<widget class="QDateTimeEdit" name="edit_init_time">
|
||||
<property name="displayFormat">
|
||||
<string>yyyy-MM-ddTHH:mm:ss</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="12" column="0">
|
||||
<item row="9" column="0">
|
||||
<widget class="QLabel" name="label_init_time_offset">
|
||||
<property name="text">
|
||||
<string>Offset time</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="12" column="1">
|
||||
<item row="9" column="1">
|
||||
<layout class="QGridLayout" name="edit_init_time_offset_grid">
|
||||
<item row="0" column="0">
|
||||
<widget class="QSpinBox" name="edit_init_time_offset_days">
|
||||
@ -421,14 +337,14 @@ online features (if installed)</string>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="13" column="0">
|
||||
<item row="10" column="0">
|
||||
<widget class="QLabel" name="label_init_ticks_type">
|
||||
<property name="text">
|
||||
<string>Initial System Ticks</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="13" column="1">
|
||||
<item row="10" column="1">
|
||||
<widget class="QComboBox" name="combo_init_ticks_type">
|
||||
<item>
|
||||
<property name="text">
|
||||
@ -442,14 +358,14 @@ online features (if installed)</string>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="14" column="0">
|
||||
<item row="11" column="0">
|
||||
<widget class="QLabel" name="label_init_ticks_value">
|
||||
<property name="text">
|
||||
<string>Initial System Ticks Override</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="14" column="1">
|
||||
<item row="11" column="1">
|
||||
<widget class="QLineEdit" name="edit_init_ticks_value">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
|
||||
@ -462,21 +378,21 @@ online features (if installed)</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="15" column="0">
|
||||
<item row="12" column="0">
|
||||
<widget class="QLabel" name="label_play_coins">
|
||||
<property name="text">
|
||||
<string>Play Coins</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="15" column="1">
|
||||
<item row="12" column="1">
|
||||
<widget class="QSpinBox" name="spinBox_play_coins">
|
||||
<property name="maximum">
|
||||
<number>300</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="16" column="0">
|
||||
<item row="13" column="0">
|
||||
<widget class="QLabel" name="label_steps_per_hour">
|
||||
<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>
|
||||
@ -486,28 +402,28 @@ online features (if installed)</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="16" column="1">
|
||||
<item row="13" column="1">
|
||||
<widget class="QSpinBox" name="spinBox_steps_per_hour">
|
||||
<property name="maximum">
|
||||
<number>9999</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="17" column="1">
|
||||
<item row="14" column="1">
|
||||
<widget class="QCheckBox" name="toggle_system_setup">
|
||||
<property name="text">
|
||||
<string>Run System Setup when Home Menu is launched</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="18" column="0">
|
||||
<item row="15" column="0">
|
||||
<widget class="QLabel" name="label_console_id">
|
||||
<property name="text">
|
||||
<string>Console ID:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="18" column="1">
|
||||
<item row="15" column="1">
|
||||
<widget class="QPushButton" name="button_regenerate_console_id">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
||||
@ -523,50 +439,114 @@ online features (if installed)</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="19" 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">
|
||||
<item row="16" column="0">
|
||||
<widget class="QLabel" name="label_plugin_loader">
|
||||
<property name="text">
|
||||
<string>3GX Plugin Loader:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="20" column="1">
|
||||
<item row="16" column="1">
|
||||
<widget class="QCheckBox" name="plugin_loader">
|
||||
<property name="text">
|
||||
<string>Enable 3GX plugin loader</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="21" column="1">
|
||||
<item row="17" column="1">
|
||||
<widget class="QCheckBox" name="allow_plugin_loader">
|
||||
<property name="text">
|
||||
<string>Allow applications to change plugin loader state</string>
|
||||
</property>
|
||||
</widget>
|
||||
</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>
|
||||
</widget>
|
||||
</item>
|
||||
@ -576,168 +556,97 @@ online features (if installed)</string>
|
||||
<string>Real Console Unique Data</string>
|
||||
</property>
|
||||
<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">
|
||||
<widget class="QLabel" name="label_movable">
|
||||
<widget class="QLabel" name="label_secure_info">
|
||||
<property name="text">
|
||||
<string>movable.sed</string>
|
||||
<string>SecureInfo_A/B</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QWidget" name="movable">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_movable">
|
||||
<widget class="QWidget" name="secure_info">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_secure_info">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_movable_status">
|
||||
<widget class="QLabel" name="label_secure_info_status">
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</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">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
@ -802,7 +711,6 @@ online features (if installed)</string>
|
||||
<tabstop>spinBox_play_coins</tabstop>
|
||||
<tabstop>spinBox_steps_per_hour</tabstop>
|
||||
<tabstop>button_regenerate_console_id</tabstop>
|
||||
<tabstop>button_regenerate_mac</tabstop>
|
||||
</tabstops>
|
||||
<resources/>
|
||||
<connections/>
|
||||
|
||||
@ -1039,7 +1039,7 @@ void GameList::LoadInterfaceLayout() {
|
||||
}
|
||||
|
||||
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")};
|
||||
|
||||
void GameList::RefreshGameDirectory() {
|
||||
|
||||
@ -184,9 +184,11 @@ public:
|
||||
if (UISettings::values.game_list_icon_size.GetValue() !=
|
||||
UISettings::GameListIconSize::NoIcon)
|
||||
setData(GetDefaultIcon(large), Qt::DecorationRole);
|
||||
/* todotodo
|
||||
if (is_encrypted) {
|
||||
setData(QObject::tr("Unsupported encrypted application"), TitleRole);
|
||||
}
|
||||
*/
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@ -94,7 +94,7 @@ struct Values {
|
||||
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_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
|
||||
Settings::Setting<bool> show_compat_column{true, "show_compat_column"};
|
||||
|
||||
@ -81,5 +81,6 @@
|
||||
// Sys files
|
||||
#define SHARED_FONT "shared_font.bin"
|
||||
#define KEYS_FILE "keys.txt"
|
||||
#define AES_KEYS "aes_keys.txt"
|
||||
#define BOOTROM9 "boot9.bin"
|
||||
#define SECRET_SECTOR "sector0x96.bin"
|
||||
|
||||
@ -386,7 +386,15 @@ public:
|
||||
[[nodiscard]] size_t ReadSpan(std::span<T> data) {
|
||||
static_assert(std::is_trivially_copyable_v<T>, "Data type must be trivially copyable.");
|
||||
|
||||
#ifdef todotodo
|
||||
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) {
|
||||
static_assert(std::is_trivially_copyable_v<T>, "Data type must be trivially copyable.");
|
||||
|
||||
#ifdef todotodo
|
||||
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 {
|
||||
|
||||
@ -70,6 +70,7 @@ HackManager hack_manager = {
|
||||
},
|
||||
}},
|
||||
|
||||
#ifdef todotodo
|
||||
{HackType::ONLINE_LLE_REQUIRED,
|
||||
HackEntry{
|
||||
.mode = HackAllowMode::FORCE,
|
||||
@ -107,6 +108,7 @@ HackManager hack_manager = {
|
||||
0x000400000D40D200,
|
||||
},
|
||||
}},
|
||||
#endif
|
||||
|
||||
{HackType::REGION_FROM_SECURE,
|
||||
HackEntry{
|
||||
|
||||
@ -137,7 +137,9 @@ Loader::ResultStatus NCCHContainer::LoadHeader() {
|
||||
return Loader::ResultStatus::Success;
|
||||
}
|
||||
|
||||
#ifdef todotodo
|
||||
for (int i = 0; i < 2; i++) {
|
||||
#endif
|
||||
if (!file->IsOpen()) {
|
||||
return Loader::ResultStatus::Error;
|
||||
}
|
||||
@ -164,6 +166,7 @@ Loader::ResultStatus NCCHContainer::LoadHeader() {
|
||||
|
||||
// Verify we are loading the correct file type...
|
||||
if (Loader::MakeMagic('N', 'C', 'C', 'H') != ncch_header.magic) {
|
||||
#ifdef todotodo
|
||||
// We may be loading a crypto file, try again
|
||||
if (i == 0) {
|
||||
file.reset();
|
||||
@ -172,8 +175,13 @@ Loader::ResultStatus NCCHContainer::LoadHeader() {
|
||||
} else {
|
||||
return Loader::ResultStatus::ErrorInvalidFormat;
|
||||
}
|
||||
#else
|
||||
return Loader::ResultStatus::ErrorInvalidFormat;
|
||||
#endif
|
||||
}
|
||||
#ifdef todotodo
|
||||
}
|
||||
#endif
|
||||
|
||||
if (file->IsCrypto()) {
|
||||
LOG_DEBUG(Service_FS, "NCCH file has console unique crypto");
|
||||
@ -192,7 +200,9 @@ Loader::ResultStatus NCCHContainer::Load() {
|
||||
if (file->IsOpen()) {
|
||||
size_t file_size;
|
||||
|
||||
#ifdef todotodo
|
||||
for (int i = 0; i < 2; i++) {
|
||||
#endif
|
||||
file_size = file->GetSize();
|
||||
|
||||
// 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...
|
||||
if (Loader::MakeMagic('N', 'C', 'C', 'H') != ncch_header.magic) {
|
||||
#ifdef todotodo
|
||||
// We may be loading a crypto file, try again
|
||||
if (i == 0) {
|
||||
file = HW::UniqueData::OpenUniqueCryptoFile(
|
||||
@ -222,14 +233,146 @@ Loader::ResultStatus NCCHContainer::Load() {
|
||||
} else {
|
||||
return Loader::ResultStatus::ErrorInvalidFormat;
|
||||
}
|
||||
#else
|
||||
return Loader::ResultStatus::ErrorInvalidFormat;
|
||||
#endif
|
||||
}
|
||||
#ifdef todotodo
|
||||
}
|
||||
#endif
|
||||
|
||||
if (file->IsCrypto()) {
|
||||
LOG_DEBUG(Service_FS, "NCCH file has console unique crypto");
|
||||
}
|
||||
|
||||
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) {
|
||||
// The NCCH is a proto version, which does not use media size units
|
||||
@ -237,10 +380,12 @@ Loader::ResultStatus NCCHContainer::Load() {
|
||||
block_size = 1;
|
||||
}
|
||||
|
||||
#ifdef todotodo
|
||||
if (!ncch_header.no_crypto) {
|
||||
// Encrypted NCCH are not supported
|
||||
return Loader::ResultStatus::ErrorEncrypted;
|
||||
}
|
||||
#endif
|
||||
|
||||
// System archives and DLC don't have an extended header but have RomFS
|
||||
// Proto apps don't have an ext header size
|
||||
@ -254,6 +399,26 @@ Loader::ResultStatus NCCHContainer::Load() {
|
||||
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 =
|
||||
fmt::format("{}mods/{:016X}/", FileUtil::GetUserPath(FileUtil::UserPath::LoadDir),
|
||||
GetModId(ncch_header.program_id));
|
||||
@ -323,6 +488,7 @@ Loader::ResultStatus NCCHContainer::Load() {
|
||||
if (file->ReadBytes(&exefs_header, sizeof(ExeFs_Header)) != sizeof(ExeFs_Header))
|
||||
return Loader::ResultStatus::Error;
|
||||
|
||||
#ifdef todotodo
|
||||
if (file->IsCrypto()) {
|
||||
exefs_file = HW::UniqueData::OpenUniqueCryptoFile(
|
||||
filepath, "rb", HW::UniqueData::UniqueCryptoFileID::NCCH);
|
||||
@ -330,6 +496,16 @@ Loader::ResultStatus NCCHContainer::Load() {
|
||||
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;
|
||||
}
|
||||
|
||||
@ -457,6 +633,17 @@ Loader::ResultStatus NCCHContainer::LoadSectionExeFS(const char* name, std::vect
|
||||
: (section.offset + exefs_offset + sizeof(ExeFs_Header) + ncch_offset);
|
||||
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;
|
||||
|
||||
if (strcmp(section.name, ".code") == 0 && is_compressed) {
|
||||
@ -466,6 +653,10 @@ Loader::ResultStatus NCCHContainer::LoadSectionExeFS(const char* name, std::vect
|
||||
temp_buffer.size())
|
||||
return Loader::ResultStatus::Error;
|
||||
|
||||
if (is_encrypted) {
|
||||
dec.ProcessData(&temp_buffer[0], &temp_buffer[0], section.size);
|
||||
}
|
||||
|
||||
// Decompress .code section...
|
||||
buffer.resize(LZSS_GetDecompressedSize(temp_buffer));
|
||||
if (!LZSS_Decompress(temp_buffer, buffer)) {
|
||||
@ -476,6 +667,9 @@ Loader::ResultStatus NCCHContainer::LoadSectionExeFS(const char* name, std::vect
|
||||
buffer.resize(section_size);
|
||||
if (exefs_file->ReadBytes(buffer.data(), section_size) != section_size)
|
||||
return Loader::ResultStatus::Error;
|
||||
if (is_encrypted) {
|
||||
dec.ProcessData(buffer.data(), buffer.data(), section.size);
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
std::unique_ptr<FileUtil::IOFile> romfs_file_inner;
|
||||
#ifdef todotodo
|
||||
if (file->IsCrypto()) {
|
||||
romfs_file_inner = HW::UniqueData::OpenUniqueCryptoFile(
|
||||
filepath, "rb", HW::UniqueData::UniqueCryptoFileID::NCCH);
|
||||
} else {
|
||||
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())
|
||||
return Loader::ResultStatus::Error;
|
||||
|
||||
#ifdef todotodo
|
||||
std::shared_ptr<RomFSReader> direct_romfs =
|
||||
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 =
|
||||
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) {
|
||||
#ifdef todotodo
|
||||
if (file->IsCrypto())
|
||||
return Loader::ResultStatus::ErrorEncrypted;
|
||||
#endif
|
||||
|
||||
std::shared_ptr<RomFSReader> direct_romfs;
|
||||
Loader::ResultStatus result = ReadRomFS(direct_romfs, false);
|
||||
|
||||
@ -348,6 +348,14 @@ private:
|
||||
bool is_loaded = 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 exefs_offset = 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
|
||||
if (segments.size() == 1 && segments[0].second > cache_line_size) {
|
||||
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);
|
||||
return length;
|
||||
}
|
||||
@ -43,6 +48,11 @@ std::size_t DirectRomFSReader::ReadFile(std::size_t offset, std::size_t length,
|
||||
if (!cache_entry.first) {
|
||||
// If not found, read from disk and cache the data
|
||||
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,
|
||||
(seg.first - page));
|
||||
} else {
|
||||
|
||||
@ -47,7 +47,13 @@ class DirectRomFSReader : public RomFSReader {
|
||||
public:
|
||||
DirectRomFSReader(std::unique_ptr<FileUtil::IOFile>&& file, std::size_t file_offset,
|
||||
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;
|
||||
|
||||
@ -62,8 +68,12 @@ public:
|
||||
bool CacheReady(std::size_t file_offset, std::size_t length) override;
|
||||
|
||||
private:
|
||||
bool is_encrypted;
|
||||
std::unique_ptr<FileUtil::IOFile> file;
|
||||
std::array<u8, 16> key;
|
||||
std::array<u8, 16> ctr;
|
||||
u64 file_offset;
|
||||
u64 crypto_offset;
|
||||
u64 data_size;
|
||||
|
||||
// Total cache size: 128KB
|
||||
@ -86,8 +96,12 @@ private:
|
||||
template <class Archive>
|
||||
void serialize(Archive& ar, const unsigned int) {
|
||||
ar& boost::serialization::base_object<RomFSReader>(*this);
|
||||
ar & is_encrypted;
|
||||
ar & file;
|
||||
ar & key;
|
||||
ar & ctr;
|
||||
ar & file_offset;
|
||||
ar & crypto_offset;
|
||||
ar & data_size;
|
||||
}
|
||||
friend class boost::serialization::access;
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "common/assert.h"
|
||||
#include "common/common_types.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) {
|
||||
std::size_t total_size = static_cast<std::size_t>(file_data.size() - offset);
|
||||
/* todotodo
|
||||
serialized_size = total_size;
|
||||
*/
|
||||
if (total_size < sizeof(u32))
|
||||
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_body, &file_data[offset + body_start], sizeof(Body));
|
||||
|
||||
/* todotodo
|
||||
std::size_t content_index_start = body_end;
|
||||
if (total_size < content_index_start + (2 * sizeof(u32)))
|
||||
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);
|
||||
std::memcpy(content_index.data(), &file_data[offset + content_index_start], content_index_size);
|
||||
|
||||
*/
|
||||
|
||||
return Loader::ResultStatus::Success;
|
||||
}
|
||||
|
||||
|
||||
@ -43,8 +43,9 @@ public:
|
||||
u8 audit;
|
||||
INSERT_PADDING_BYTES(0x42);
|
||||
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)
|
||||
|
||||
Loader::ResultStatus DoTitlekeyFixup();
|
||||
|
||||
@ -44,12 +44,22 @@ Result ErrEula::ReceiveParameterImpl(const Service::APT::MessageParameter& param
|
||||
}
|
||||
|
||||
Result ErrEula::Start(const Service::APT::MessageParameter& parameter) {
|
||||
#ifdef todotodo
|
||||
memcpy(¶m, parameter.buffer.data(), std::min(parameter.buffer.size(), sizeof(param)));
|
||||
|
||||
// Do something here, like showing error codes, or prompting for EULA agreement.
|
||||
if (param.type == DisplayType::Agree) {
|
||||
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.
|
||||
Finalize();
|
||||
@ -57,8 +67,13 @@ Result ErrEula::Start(const Service::APT::MessageParameter& parameter) {
|
||||
}
|
||||
|
||||
Result ErrEula::Finalize() {
|
||||
#ifdef todotodo
|
||||
std::vector<u8> buffer(sizeof(param));
|
||||
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);
|
||||
return ResultSuccess;
|
||||
}
|
||||
|
||||
@ -54,7 +54,8 @@ private:
|
||||
std::shared_ptr<Kernel::SharedMemory> framebuffer_memory;
|
||||
|
||||
/// Parameter received by the applet on start.
|
||||
ErrEulaParam param{};
|
||||
// ErrEulaParam param{};
|
||||
std::vector<u8> startup_param;
|
||||
};
|
||||
|
||||
} // namespace HLE::Applets
|
||||
|
||||
@ -205,12 +205,21 @@ Result TranslateCommandBuffer(Kernel::KernelSystem& kernel, Memory::MemorySystem
|
||||
buffer->GetPtr() + Memory::CITRA_PAGE_SIZE + page_offset, size);
|
||||
|
||||
// Map the guard pages and mapped pages at once.
|
||||
#ifdef todotodo
|
||||
auto target_address_result = dst_process->vm_manager.MapBackingMemoryToBase(
|
||||
Memory::IPC_MAPPING_VADDR, Memory::IPC_MAPPING_SIZE, buffer,
|
||||
static_cast<u32>(buffer->GetSize()), Kernel::MemoryState::Shared);
|
||||
|
||||
ASSERT_MSG(target_address_result.Succeeded(), "Failed to map target address");
|
||||
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.
|
||||
const VAddr low_guard_address = target_address;
|
||||
|
||||
@ -47,6 +47,9 @@ union BatteryState {
|
||||
|
||||
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 {
|
||||
Off = 0,
|
||||
Poor = 1,
|
||||
|
||||
@ -106,10 +106,26 @@ void Module::Interface::GetStatus(Kernel::HLERequestContext& ctx) {
|
||||
|
||||
void Module::Interface::GetWifiStatus(Kernel::HLERequestContext& 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);
|
||||
rb.Push(ResultSuccess);
|
||||
#ifdef todotodo
|
||||
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) {
|
||||
|
||||
@ -90,7 +90,7 @@ public:
|
||||
* AC::GetWifiStatus service function
|
||||
* Outputs:
|
||||
* 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);
|
||||
|
||||
@ -169,9 +169,14 @@ protected:
|
||||
};
|
||||
enum class WifiStatus {
|
||||
STATUS_DISCONNECTED = 0,
|
||||
#ifdef todotodo
|
||||
STATUS_CONNECTED_SLOT1 = (1 << 0),
|
||||
STATUS_CONNECTED_SLOT2 = (1 << 1),
|
||||
STATUS_CONNECTED_SLOT3 = (1 << 2),
|
||||
#else
|
||||
STATUS_CONNECTED_O3DS = 1,
|
||||
STATUS_CONNECTED_N3DS = 2,
|
||||
#endif
|
||||
};
|
||||
|
||||
struct ACConfig {
|
||||
|
||||
@ -18,6 +18,7 @@ enum {
|
||||
NotInitialized = 101,
|
||||
AlreadyInitialized = 102,
|
||||
AcStatusDisconnected = 103,
|
||||
ErrDesc103 = 103,
|
||||
ErrDesc104 = 104,
|
||||
Busy = 111,
|
||||
ErrDesc112 = 112,
|
||||
@ -317,6 +318,7 @@ enum {
|
||||
NotInitialized = 220501, // 022-0501
|
||||
AlreadyInitialized = 220502, // 022-0502
|
||||
AcStatusDisconnected = 225103, // 022-5103
|
||||
ErrCode225103 = 225103, // 022-5103
|
||||
ErrCode225104 = 225104, // 022-5104
|
||||
Busy = 220511, // 022-0511
|
||||
ErrCode225112 = 225112, // 022-5112
|
||||
|
||||
@ -89,12 +89,42 @@ struct TicketInfo {
|
||||
|
||||
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 {
|
||||
public:
|
||||
std::vector<CryptoPP::CBC_Mode<CryptoPP::AES>::Decryption> content;
|
||||
};
|
||||
|
||||
NCCHCryptoFile::NCCHCryptoFile(const std::string& out_file, bool encrypted_content) {
|
||||
#ifdef todotodo
|
||||
if (encrypted_content) {
|
||||
// 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
|
||||
@ -108,15 +138,20 @@ NCCHCryptoFile::NCCHCryptoFile(const std::string& out_file, bool encrypted_conte
|
||||
if (!file->IsOpen()) {
|
||||
is_error = true;
|
||||
}
|
||||
#else
|
||||
file = std::make_unique<FileUtil::IOFile>(out_file, "wb");
|
||||
#endif
|
||||
}
|
||||
|
||||
void NCCHCryptoFile::Write(const u8* buffer, std::size_t length) {
|
||||
if (is_error)
|
||||
return;
|
||||
|
||||
#ifdef todotodo
|
||||
if (is_not_ncch) {
|
||||
file->WriteBytes(buffer, length);
|
||||
}
|
||||
#endif
|
||||
|
||||
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 (Loader::MakeMagic('N', 'C', 'C', 'H') != ncch_header.magic) {
|
||||
#ifdef todotodo
|
||||
// Most likely DS contents, store without additional operations
|
||||
is_not_ncch = true;
|
||||
file->WriteBytes(&ncch_header, sizeof(ncch_header));
|
||||
file->WriteBytes(buffer, length);
|
||||
#else
|
||||
is_error = true;
|
||||
#endif
|
||||
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_)
|
||||
: 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>()) {
|
||||
// If data is being installing from CDN, provide a fake header to the container so that
|
||||
// it's not uninitialized.
|
||||
@ -457,6 +496,7 @@ Result CIAFile::WriteTicket() {
|
||||
ErrorLevel::Permanent};
|
||||
}
|
||||
|
||||
#ifdef todotodo
|
||||
const auto& ticket = container.GetTicket();
|
||||
const auto ticket_path = GetTicketPath(ticket.GetTitleID(), ticket.GetTicketID());
|
||||
|
||||
@ -471,6 +511,7 @@ Result CIAFile::WriteTicket() {
|
||||
// TODO: Correct result code.
|
||||
return FileSys::ResultFileNotFound;
|
||||
}
|
||||
#endif
|
||||
|
||||
install_state = CIAInstallState::TicketLoaded;
|
||||
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();
|
||||
content_written.resize(content_count);
|
||||
|
||||
#ifdef todotodo
|
||||
current_content_file.reset();
|
||||
current_content_index = -1;
|
||||
content_file_paths.clear();
|
||||
#else
|
||||
content_files.clear();
|
||||
#endif
|
||||
for (std::size_t i = 0; i < content_count; i++) {
|
||||
auto path = GetTitleContentPath(media_type, tmd.GetTitleID(), i, is_update);
|
||||
#ifdef todotodo
|
||||
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()) {
|
||||
@ -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
|
||||
// contents or only part of a larger .app's contents.
|
||||
const u64 offset_max = offset + length;
|
||||
bool success = true;
|
||||
for (std::size_t i = 0; i < content_written.size(); i++) {
|
||||
if (content_written[i] < container.GetContentSize(i)) {
|
||||
// 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
|
||||
// to get the content paths to write to.
|
||||
#ifdef todotodo
|
||||
const FileSys::TitleMetadata& tmd = container.GetTitleMetadata();
|
||||
if (i != current_content_index) {
|
||||
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;
|
||||
|
||||
#else
|
||||
FileSys::TitleMetadata tmd = container.GetTitleMetadata();
|
||||
auto& file = content_files[i];
|
||||
#endif
|
||||
std::vector<u8> temp(buffer + (range_min - offset),
|
||||
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());
|
||||
}
|
||||
|
||||
file.Write(temp.data(), temp.size());
|
||||
if (file.IsError()) {
|
||||
// This can never happen in real HW
|
||||
return Result(ErrCodes::InvalidImportState, ErrorModule::AM,
|
||||
ErrorSummary::InvalidState, ErrorLevel::Permanent);
|
||||
}
|
||||
file.WriteBytes(temp.data(), temp.size());
|
||||
|
||||
// Keep tabs on how much of this content ID has been written so new range_min
|
||||
// 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,
|
||||
@ -732,17 +787,13 @@ ResultVal<std::size_t> CIAFile::WriteContentDataIndexed(u16 content_index, u64 o
|
||||
}
|
||||
|
||||
file.Write(temp.data(), temp.size());
|
||||
if (file.IsError()) {
|
||||
// This can never happen in real HW
|
||||
return Result(ErrCodes::InvalidImportState, ErrorModule::AM, ErrorSummary::InvalidState,
|
||||
ErrorLevel::Permanent);
|
||||
}
|
||||
bool success = !file.IsError();
|
||||
|
||||
content_written[content_index] += temp.size();
|
||||
LOG_DEBUG(Service_AM, "Wrote {} to content {}, total {}", temp.size(), content_index,
|
||||
content_written[content_index]);
|
||||
|
||||
return temp.size();
|
||||
return success ? temp.size() : 0;
|
||||
}
|
||||
|
||||
u64 CIAFile::GetSize() const {
|
||||
@ -848,6 +899,12 @@ bool TicketFile::SetSize(u64 size) const {
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@ -865,6 +922,11 @@ Result TicketFile::Commit() {
|
||||
ticket_id = 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
|
||||
if (ticket.Save(ticket_path) != Loader::ResultStatus::Success) {
|
||||
LOG_ERROR(Service_AM, "Failed to install ticket provided to TicketFile.");
|
||||
@ -964,8 +1026,10 @@ InstallStatus InstallCIA(const std::string& path,
|
||||
Core::System::GetInstance(),
|
||||
Service::AM::GetTitleMediaType(container.GetTitleMetadata().GetTitleID()));
|
||||
|
||||
if (container.GetTitleMetadata().HasEncryptedContent()) {
|
||||
LOG_ERROR(Service_AM, "File {} is encrypted! Aborting...", path);
|
||||
bool title_key_available = container.GetTicket().GetTitleKey().has_value();
|
||||
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;
|
||||
}
|
||||
|
||||
@ -975,8 +1039,12 @@ InstallStatus InstallCIA(const std::string& path,
|
||||
return InstallStatus::ErrorFailedToOpenFile;
|
||||
}
|
||||
|
||||
#ifdef todotodo
|
||||
std::vector<u8> buffer;
|
||||
buffer.resize(0x10000);
|
||||
#else
|
||||
std::array<u8, 0x10000> buffer;
|
||||
#endif
|
||||
auto file_size = file.GetSize();
|
||||
std::size_t total_bytes_read = 0;
|
||||
while (total_bytes_read != file_size) {
|
||||
@ -1035,6 +1103,96 @@ InstallStatus InstallCIA(const std::string& path,
|
||||
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) {
|
||||
// Real services seem to just discard and replace the whole high word.
|
||||
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,
|
||||
title_count * sizeof(u64));
|
||||
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(
|
||||
[this, async_data](Kernel::HLERequestContext& ctx) {
|
||||
// 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
|
||||
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](Kernel::HLERequestContext& ctx) {
|
||||
async_data->res = GetTitleInfoFromList(async_data->title_id_list,
|
||||
async_data->media_type, async_data->out);
|
||||
}
|
||||
return 0;
|
||||
},
|
||||
[async_data](Kernel::HLERequestContext& ctx) {
|
||||
@ -1873,6 +2030,7 @@ void Module::Interface::GetProgramInfosImpl(Kernel::HLERequestContext& ctx, bool
|
||||
rb.PushMappedBuffer(*async_data->title_info_out);
|
||||
},
|
||||
true);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@ -2416,6 +2574,7 @@ void Module::Interface::DeleteTicket(Kernel::HLERequestContext& ctx) {
|
||||
|
||||
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
|
||||
std::scoped_lock lock(am->am_lists_mutex);
|
||||
#ifdef todotodo
|
||||
auto range = am->am_ticket_list.equal_range(title_id);
|
||||
if (range.first == range.second) {
|
||||
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);
|
||||
#endif
|
||||
|
||||
rb.Push(ResultSuccess);
|
||||
LOG_WARNING(Service_AM, "(STUBBED) called title_id=0x{:016x}", title_id);
|
||||
}
|
||||
|
||||
void Module::Interface::GetNumTickets(Kernel::HLERequestContext& ctx) {
|
||||
@ -2439,11 +2600,19 @@ void Module::Interface::GetNumTickets(Kernel::HLERequestContext& ctx) {
|
||||
LOG_DEBUG(Service_AM, "");
|
||||
|
||||
std::scoped_lock lock(am->am_lists_mutex);
|
||||
#ifdef todotodo
|
||||
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);
|
||||
rb.Push(ResultSuccess);
|
||||
rb.Push(ticket_count);
|
||||
LOG_WARNING(Service_AM, "(STUBBED) called ticket_count=0x{:08x}", ticket_count);
|
||||
}
|
||||
|
||||
void Module::Interface::GetTicketList(Kernel::HLERequestContext& ctx) {
|
||||
@ -2456,6 +2625,7 @@ void Module::Interface::GetTicketList(Kernel::HLERequestContext& ctx) {
|
||||
|
||||
u32 tickets_written = 0;
|
||||
std::scoped_lock lock(am->am_lists_mutex);
|
||||
#ifdef todotodo
|
||||
auto it = am->am_ticket_list.begin();
|
||||
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++) {
|
||||
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);
|
||||
rb.Push(ResultSuccess);
|
||||
rb.Push(tickets_written);
|
||||
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) {
|
||||
@ -2475,6 +2656,7 @@ void Module::Interface::GetDeviceID(Kernel::HLERequestContext& ctx) {
|
||||
|
||||
LOG_DEBUG(Service_AM, "");
|
||||
|
||||
#ifdef todotodo
|
||||
const auto& otp = HW::UniqueData::GetOTP();
|
||||
if (!otp.Valid()) {
|
||||
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
|
||||
@ -2490,6 +2672,13 @@ void Module::Interface::GetDeviceID(Kernel::HLERequestContext& ctx) {
|
||||
if (am->force_old_device_id) {
|
||||
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);
|
||||
rb.Push(ResultSuccess);
|
||||
@ -2505,6 +2694,7 @@ void Module::Interface::GetNumImportTitleContextsImpl(IPC::RequestParser& rp,
|
||||
IPC::RequestBuilder rb = rp.MakeBuilder(3, 0);
|
||||
rb.Push(ResultSuccess);
|
||||
|
||||
#ifdef todotodo
|
||||
u32 count = 0;
|
||||
for (auto it = am->import_title_contexts.begin(); it != am->import_title_contexts.end(); it++) {
|
||||
if ((include_installing &&
|
||||
@ -2517,6 +2707,9 @@ void Module::Interface::GetNumImportTitleContextsImpl(IPC::RequestParser& rp,
|
||||
}
|
||||
|
||||
rb.Push<u32>(count);
|
||||
#else
|
||||
rb.Push<u32>(static_cast<u32>(am->import_title_contexts.size()));
|
||||
#endif
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
#ifdef todotodo
|
||||
bool needs_cleanup = false;
|
||||
for (auto& import_ctx : am->import_title_contexts) {
|
||||
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);
|
||||
rb.Push(ResultSuccess);
|
||||
#ifdef todotodo
|
||||
rb.Push<bool>(needs_cleanup);
|
||||
#else
|
||||
rb.Push<bool>(false);
|
||||
#endif
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
#ifdef todotodo
|
||||
for (auto it = am->import_content_contexts.begin(); it != am->import_content_contexts.end();) {
|
||||
if (it->second.state == ImportTitleContextState::NEEDS_CLEANUP) {
|
||||
it = am->import_content_contexts.erase(it);
|
||||
@ -2759,6 +2959,7 @@ void Module::Interface::DoCleanup(Kernel::HLERequestContext& ctx) {
|
||||
it++;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
|
||||
rb.Push(ResultSuccess);
|
||||
@ -2775,6 +2976,7 @@ void Module::Interface::QueryAvailableTitleDatabase(Kernel::HLERequestContext& c
|
||||
LOG_WARNING(Service_AM, "(STUBBED) media_type={}", media_type);
|
||||
}
|
||||
|
||||
#ifdef todotodo
|
||||
void Module::Interface::GetPersonalizedTicketInfoList(Kernel::HLERequestContext& 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);
|
||||
}
|
||||
#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) {
|
||||
IPC::RequestParser rp(ctx);
|
||||
@ -2899,8 +3143,13 @@ void Module::Interface::BeginImportProgram(Kernel::HLERequestContext& ctx) {
|
||||
|
||||
if (am->cia_installing) {
|
||||
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
|
||||
#ifdef todotodo
|
||||
rb.Push(Result(ErrCodes::InvalidImportState, ErrorModule::AM, ErrorSummary::InvalidState,
|
||||
ErrorLevel::Permanent));
|
||||
#else
|
||||
rb.Push(Result(ErrCodes::CIACurrentlyInstalling, ErrorModule::AM,
|
||||
ErrorSummary::InvalidState, ErrorLevel::Permanent));
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
@ -2924,8 +3173,13 @@ void Module::Interface::BeginImportProgramTemporarily(Kernel::HLERequestContext&
|
||||
|
||||
if (am->cia_installing) {
|
||||
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
|
||||
#ifdef todotodo
|
||||
rb.Push(Result(ErrCodes::InvalidImportState, ErrorModule::AM, ErrorSummary::InvalidState,
|
||||
ErrorLevel::Permanent));
|
||||
#else
|
||||
rb.Push(Result(ErrCodes::CIACurrentlyInstalling, ErrorModule::AM,
|
||||
ErrorSummary::InvalidState, ErrorLevel::Permanent));
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
@ -2975,7 +3229,25 @@ void Module::Interface::EndImportProgramWithoutCommit(Kernel::HLERequestContext&
|
||||
}
|
||||
|
||||
void Module::Interface::CommitImportPrograms(Kernel::HLERequestContext& ctx) {
|
||||
#ifdef todotodo
|
||||
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.
|
||||
@ -3452,45 +3724,39 @@ void Module::Interface::BeginImportTicket(Kernel::HLERequestContext& ctx) {
|
||||
IPC::RequestBuilder rb = rp.MakeBuilder(1, 2);
|
||||
rb.Push(ResultSuccess); // No error
|
||||
rb.PushCopyObjects(file->Connect());
|
||||
|
||||
LOG_WARNING(Service_AM, "(STUBBED) called");
|
||||
}
|
||||
|
||||
#ifdef todotodo
|
||||
void Module::Interface::EndImportTicket(Kernel::HLERequestContext& ctx) {
|
||||
IPC::RequestParser rp(ctx);
|
||||
const auto ticket = rp.PopObject<Kernel::ClientSession>();
|
||||
|
||||
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
|
||||
auto ticket_file = GetFileBackendFromSession<TicketFile>(ticket);
|
||||
if (ticket_file.Succeeded()) {
|
||||
struct AsyncData {
|
||||
Service::AM::TicketFile* ticket_file;
|
||||
|
||||
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);
|
||||
rb.Push(ticket_file.Unwrap()->Commit());
|
||||
am->am_ticket_list.insert(std::make_pair(ticket_file.Unwrap()->GetTitleID(),
|
||||
ticket_file.Unwrap()->GetTicketID()));
|
||||
} else {
|
||||
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
|
||||
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) {
|
||||
IPC::RequestParser rp(ctx);
|
||||
@ -3621,6 +3887,7 @@ void Module::Interface::EndImportTitle(Kernel::HLERequestContext& ctx) {
|
||||
}
|
||||
|
||||
am->importing_title->cia_file.SetDone();
|
||||
am->ScanForTitles(am->importing_title->media_type);
|
||||
am->importing_title.reset();
|
||||
|
||||
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
|
||||
@ -3994,10 +4261,12 @@ void Module::serialize(Archive& ar, const unsigned int) {
|
||||
ar & cia_installing;
|
||||
ar & force_old_device_id;
|
||||
ar & force_new_device_id;
|
||||
ar & am_title_list;
|
||||
ar & system_updater_mutex;
|
||||
}
|
||||
SERIALIZE_IMPL(Module)
|
||||
|
||||
#ifdef todotodo
|
||||
void Module::Interface::GetDeviceCert(Kernel::HLERequestContext& ctx) {
|
||||
IPC::RequestParser rp(ctx);
|
||||
[[maybe_unused]] u32 size = rp.Pop<u32>();
|
||||
@ -4021,6 +4290,52 @@ void Module::Interface::GetDeviceCert(Kernel::HLERequestContext& ctx) {
|
||||
rb.Push(0);
|
||||
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) {
|
||||
CommitImportTitlesImpl(ctx, true, true);
|
||||
@ -4051,7 +4366,11 @@ void Module::Interface::DeleteTicketId(Kernel::HLERequestContext& ctx) {
|
||||
auto path = GetTicketPath(title_id, ticket_id);
|
||||
FileUtil::Delete(path);
|
||||
|
||||
#ifdef todotodo
|
||||
am->am_ticket_list.erase(it);
|
||||
#else
|
||||
am->ScanForTickets();
|
||||
#endif
|
||||
|
||||
rb.Push(ResultSuccess);
|
||||
}
|
||||
@ -4233,6 +4552,7 @@ void Module::Interface::ExportTicketWrapped(Kernel::HLERequestContext& ctx) {
|
||||
Module::Module(Core::System& _system) : system(_system) {
|
||||
FileUtil::CreateFullPath(GetTicketDirectory());
|
||||
ScanForAllTitles();
|
||||
LoadCTCertFile(ct_cert);
|
||||
system_updater_mutex = system.Kernel().CreateMutex(false, "AM::SystemUpdaterMutex");
|
||||
}
|
||||
|
||||
|
||||
@ -51,6 +51,7 @@ namespace Service::AM {
|
||||
namespace ErrCodes {
|
||||
enum {
|
||||
InvalidImportState = 4,
|
||||
CIACurrentlyInstalling = 4,
|
||||
InvalidTID = 31,
|
||||
EmptyCIA = 32,
|
||||
TryingToUninstallSystemApp = 44,
|
||||
@ -87,6 +88,13 @@ enum class ImportTitleContextState : u8 {
|
||||
NEEDS_CLEANUP = 6,
|
||||
};
|
||||
|
||||
enum class CTCertLoadStatus {
|
||||
Loaded,
|
||||
NotFound,
|
||||
Invalid,
|
||||
IOError,
|
||||
};
|
||||
|
||||
struct ImportTitleContext {
|
||||
u64 title_id;
|
||||
u16 version;
|
||||
@ -105,6 +113,24 @@ struct ImportContentContext {
|
||||
};
|
||||
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
|
||||
constexpr std::size_t TITLE_ID_VALID_LENGTH = 16;
|
||||
|
||||
@ -126,7 +152,7 @@ private:
|
||||
friend class CIAFile;
|
||||
std::unique_ptr<FileUtil::IOFile> file;
|
||||
bool is_error = false;
|
||||
bool is_not_ncch = false;
|
||||
// bool is_not_ncch = false;
|
||||
bool decryption_authorized = false;
|
||||
|
||||
std::size_t written = 0;
|
||||
@ -223,6 +249,7 @@ private:
|
||||
std::vector<std::string> content_file_paths;
|
||||
u16 current_content_index = -1;
|
||||
std::unique_ptr<NCCHCryptoFile> current_content_file;
|
||||
std::vector<FileUtil::IOFile> content_files;
|
||||
Service::FS::MediaType media_type;
|
||||
|
||||
class DecryptionState;
|
||||
@ -334,6 +361,13 @@ private:
|
||||
InstallStatus InstallCIA(const std::string& path,
|
||||
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
|
||||
* @param titleId the title ID
|
||||
@ -1022,6 +1056,18 @@ public:
|
||||
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:
|
||||
void ScanForTickets();
|
||||
|
||||
@ -1054,6 +1100,7 @@ private:
|
||||
std::multimap<u64, u64> am_ticket_list;
|
||||
|
||||
std::shared_ptr<Kernel::Mutex> system_updater_mutex;
|
||||
CTCert ct_cert{};
|
||||
std::shared_ptr<CurrentImportingTitle> importing_title;
|
||||
std::map<u64, ImportTitleContext> import_title_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) {
|
||||
IPC::RequestParser rp(ctx);
|
||||
|
||||
#ifdef todotodo
|
||||
const auto& secure_info_a = HW::UniqueData::GetSecureInfoA();
|
||||
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;
|
||||
#else
|
||||
u8 ret = 0;
|
||||
if (cfg->secure_info_a_loaded) {
|
||||
ret = cfg->secure_info_a.unknown;
|
||||
}
|
||||
#endif
|
||||
|
||||
IPC::RequestBuilder rb = rp.MakeBuilder(2, 0);
|
||||
rb.Push(ResultSuccess);
|
||||
rb.Push<u8>(ret);
|
||||
}
|
||||
|
||||
#ifdef todotodo
|
||||
void Module::Interface::SecureInfoGetSerialNo(Kernel::HLERequestContext& ctx) {
|
||||
IPC::RequestParser rp(ctx);
|
||||
[[maybe_unused]] u32 out_size = rp.Pop<u32>();
|
||||
@ -500,6 +508,32 @@ void Module::Interface::SecureInfoGetSerialNo(Kernel::HLERequestContext& ctx) {
|
||||
rb.Push(ResultSuccess);
|
||||
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) {
|
||||
IPC::RequestParser rp(ctx);
|
||||
@ -645,6 +679,7 @@ void Module::Interface::UpdateConfigNANDSavegame(Kernel::HLERequestContext& ctx)
|
||||
rb.Push(cfg->UpdateConfigNANDSavegame());
|
||||
}
|
||||
|
||||
#ifdef todotodo
|
||||
void Module::Interface::GetLocalFriendCodeSeedData(Kernel::HLERequestContext& ctx) {
|
||||
IPC::RequestParser rp(ctx);
|
||||
[[maybe_unused]] u32 out_size = rp.Pop<u32>();
|
||||
@ -688,6 +723,44 @@ void Module::Interface::GetLocalFriendCodeSeed(Kernel::HLERequestContext& ctx) {
|
||||
rb.Push(ResultSuccess);
|
||||
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) {
|
||||
IPC::RequestParser rp(ctx);
|
||||
@ -863,6 +936,14 @@ Result Module::UpdateConfigNANDSavegame() {
|
||||
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 res = DeleteConfigNANDSaveFile();
|
||||
// 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();
|
||||
}
|
||||
|
||||
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() {
|
||||
FileUtil::IOFile mcu_data_file(
|
||||
fmt::format("{}/mcu.dat", FileUtil::GetUserPath(FileUtil::UserPath::SysDataDir)), "rb");
|
||||
@ -976,6 +1106,8 @@ Module::Module(Core::System& system_) : system(system_) {
|
||||
SetEULAVersion(default_version);
|
||||
UpdateConfigNANDSavegame();
|
||||
}
|
||||
LoadSecureInfoAFile();
|
||||
LoadLocalFriendCodeSeedBFile();
|
||||
}
|
||||
|
||||
Module::~Module() = default;
|
||||
|
||||
@ -181,6 +181,28 @@ enum class AccessFlag : u16 {
|
||||
};
|
||||
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 {
|
||||
public:
|
||||
Module(Core::System& system_);
|
||||
@ -634,6 +656,35 @@ public:
|
||||
*/
|
||||
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:
|
||||
void UpdatePreferredRegionCode();
|
||||
SystemLanguage GetRawSystemLanguage();
|
||||
@ -644,6 +695,10 @@ private:
|
||||
std::array<u8, CONFIG_SAVEFILE_SIZE> cfg_config_file_buffer;
|
||||
std::unique_ptr<FileSys::ArchiveBackend> cfg_system_save_data_archive;
|
||||
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;
|
||||
MCUData mcu_data{};
|
||||
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
|
||||
// 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.
|
||||
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) {
|
||||
if (hex.size() < 32) {
|
||||
@ -143,6 +146,78 @@ struct KeyDesc {
|
||||
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() {
|
||||
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
|
||||
|
||||
@ -305,6 +486,8 @@ void InitKeys(bool force) {
|
||||
return;
|
||||
}
|
||||
initialized = true;
|
||||
HW::RSA::InitSlots();
|
||||
LoadBootromKeys();
|
||||
LoadPresetKeys();
|
||||
movable_key.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/stream.hpp>
|
||||
#include "common/assert.h"
|
||||
#include <common/string_util.h>
|
||||
#include "common/common_paths.h"
|
||||
#include "common/file_util.h"
|
||||
#include "common/logging/log.h"
|
||||
|
||||
@ -21,6 +21,19 @@
|
||||
|
||||
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;
|
||||
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() {
|
||||
static bool initialized = false;
|
||||
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;
|
||||
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];
|
||||
}
|
||||
|
||||
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() {
|
||||
return ticket_wrap_slot;
|
||||
}
|
||||
|
||||
@ -23,6 +23,8 @@ public:
|
||||
|
||||
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 {
|
||||
// TODO(B3N30): Maybe check if exponent and modulus are vailid
|
||||
return init;
|
||||
@ -68,4 +70,5 @@ const RsaSlot& GetTicketWrapSlot();
|
||||
const RsaSlot& GetSecureInfoSlot();
|
||||
const RsaSlot& GetLocalFriendCodeSeedSlot();
|
||||
|
||||
std::vector<u8> CreateASN1Message(std::span<const u8> data);
|
||||
} // namespace HW::RSA
|
||||
|
||||
@ -26,6 +26,23 @@ public:
|
||||
Apploader_Artic(Core::System& system_, const std::string& server_addr, u16 server_port,
|
||||
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;
|
||||
|
||||
/**
|
||||
|
||||
@ -48,7 +48,7 @@ FileType GuessFromExtension(const std::string& extension_) {
|
||||
if (extension == ".elf" || extension == ".axf")
|
||||
return FileType::ELF;
|
||||
|
||||
if (extension == ".cci")
|
||||
if (extension == ".cci" || extension == ".3ds")
|
||||
return FileType::CCI;
|
||||
|
||||
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);
|
||||
|
||||
case FileType::ARTIC: {
|
||||
#ifdef todotodo
|
||||
Apploader_Artic::ArticInitMode mode = Apploader_Artic::ArticInitMode::NONE;
|
||||
if (filename.starts_with("articinio://")) {
|
||||
mode = Apploader_Artic::ArticInitMode::O3DS;
|
||||
} else if (filename.starts_with("articinin://")) {
|
||||
mode = Apploader_Artic::ArticInitMode::N3DS;
|
||||
}
|
||||
#endif
|
||||
auto strToUInt = [](const std::string& str) -> int {
|
||||
char* pEnd = NULL;
|
||||
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);
|
||||
}
|
||||
}
|
||||
#ifdef todotodo
|
||||
return std::make_unique<Apploader_Artic>(system, server_addr, port, mode);
|
||||
#else
|
||||
return std::make_unique<Apploader_Artic>(system, server_addr, port);
|
||||
#endif
|
||||
}
|
||||
|
||||
default:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user