diff options
Diffstat (limited to 'src')
196 files changed, 8961 insertions, 3470 deletions
diff --git a/src/android/app/src/main/AndroidManifest.xml b/src/android/app/src/main/AndroidManifest.xml index 7890b30ca..b037fc055 100644 --- a/src/android/app/src/main/AndroidManifest.xml +++ b/src/android/app/src/main/AndroidManifest.xml @@ -14,6 +14,7 @@ SPDX-License-Identifier: GPL-3.0-or-later <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.NFC" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> + <uses-permission android:name="android.permission.VIBRATE" /> <application android:name="org.yuzu.yuzu_emu.YuzuApplication" diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt index 6ebb46af7..02a20dacf 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt @@ -3,24 +3,21 @@ package org.yuzu.yuzu_emu -import android.app.Dialog import android.content.DialogInterface import android.net.Uri -import android.os.Bundle import android.text.Html import android.text.method.LinkMovementMethod import android.view.Surface import android.view.View import android.widget.TextView import androidx.annotation.Keep -import androidx.fragment.app.DialogFragment import com.google.android.material.dialog.MaterialAlertDialogBuilder import java.lang.ref.WeakReference import org.yuzu.yuzu_emu.activities.EmulationActivity +import org.yuzu.yuzu_emu.fragments.CoreErrorDialogFragment import org.yuzu.yuzu_emu.utils.DocumentsTree import org.yuzu.yuzu_emu.utils.FileUtil import org.yuzu.yuzu_emu.utils.Log -import org.yuzu.yuzu_emu.utils.SerializableHelper.serializable import org.yuzu.yuzu_emu.model.InstallResult import org.yuzu.yuzu_emu.model.Patch import org.yuzu.yuzu_emu.model.GameVerificationResult @@ -30,34 +27,6 @@ import org.yuzu.yuzu_emu.model.GameVerificationResult * with the native side of the Yuzu code. */ object NativeLibrary { - /** - * Default controller id for each device - */ - const val Player1Device = 0 - const val Player2Device = 1 - const val Player3Device = 2 - const val Player4Device = 3 - const val Player5Device = 4 - const val Player6Device = 5 - const val Player7Device = 6 - const val Player8Device = 7 - const val ConsoleDevice = 8 - - /** - * Controller type for each device - */ - const val ProController = 3 - const val Handheld = 4 - const val JoyconDual = 5 - const val JoyconLeft = 6 - const val JoyconRight = 7 - const val GameCube = 8 - const val Pokeball = 9 - const val NES = 10 - const val SNES = 11 - const val N64 = 12 - const val SegaGenesis = 13 - @JvmField var sEmulationActivity = WeakReference<EmulationActivity?>(null) @@ -127,112 +96,6 @@ object NativeLibrary { FileUtil.getFilename(Uri.parse(path)) } - /** - * Returns true if pro controller isn't available and handheld is - */ - external fun isHandheldOnly(): Boolean - - /** - * Changes controller type for a specific device. - * - * @param Device The input descriptor of the gamepad. - * @param Type The NpadStyleIndex of the gamepad. - */ - external fun setDeviceType(Device: Int, Type: Int): Boolean - - /** - * Handles event when a gamepad is connected. - * - * @param Device The input descriptor of the gamepad. - */ - external fun onGamePadConnectEvent(Device: Int): Boolean - - /** - * Handles event when a gamepad is disconnected. - * - * @param Device The input descriptor of the gamepad. - */ - external fun onGamePadDisconnectEvent(Device: Int): Boolean - - /** - * Handles button press events for a gamepad. - * - * @param Device The input descriptor of the gamepad. - * @param Button Key code identifying which button was pressed. - * @param Action Mask identifying which action is happening (button pressed down, or button released). - * @return If we handled the button press. - */ - external fun onGamePadButtonEvent(Device: Int, Button: Int, Action: Int): Boolean - - /** - * Handles joystick movement events. - * - * @param Device The device ID of the gamepad. - * @param Axis The axis ID - * @param x_axis The value of the x-axis represented by the given ID. - * @param y_axis The value of the y-axis represented by the given ID. - */ - external fun onGamePadJoystickEvent( - Device: Int, - Axis: Int, - x_axis: Float, - y_axis: Float - ): Boolean - - /** - * Handles motion events. - * - * @param delta_timestamp The finger id corresponding to this event - * @param gyro_x,gyro_y,gyro_z The value of the accelerometer sensor. - * @param accel_x,accel_y,accel_z The value of the y-axis - */ - external fun onGamePadMotionEvent( - Device: Int, - delta_timestamp: Long, - gyro_x: Float, - gyro_y: Float, - gyro_z: Float, - accel_x: Float, - accel_y: Float, - accel_z: Float - ): Boolean - - /** - * Signals and load a nfc tag - * - * @param data Byte array containing all the data from a nfc tag - */ - external fun onReadNfcTag(data: ByteArray?): Boolean - - /** - * Removes current loaded nfc tag - */ - external fun onRemoveNfcTag(): Boolean - - /** - * Handles touch press events. - * - * @param finger_id The finger id corresponding to this event - * @param x_axis The value of the x-axis. - * @param y_axis The value of the y-axis. - */ - external fun onTouchPressed(finger_id: Int, x_axis: Float, y_axis: Float) - - /** - * Handles touch movement. - * - * @param x_axis The value of the instantaneous x-axis. - * @param y_axis The value of the instantaneous y-axis. - */ - external fun onTouchMoved(finger_id: Int, x_axis: Float, y_axis: Float) - - /** - * Handles touch release events. - * - * @param finger_id The finger id corresponding to this event - */ - external fun onTouchReleased(finger_id: Int) - external fun setAppDirectory(directory: String) /** @@ -318,46 +181,13 @@ object NativeLibrary { ErrorUnknown } - private var coreErrorAlertResult = false - private val coreErrorAlertLock = Object() - - class CoreErrorDialogFragment : DialogFragment() { - override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - val title = requireArguments().serializable<String>("title") - val message = requireArguments().serializable<String>("message") - - return MaterialAlertDialogBuilder(requireActivity()) - .setTitle(title) - .setMessage(message) - .setPositiveButton(R.string.continue_button, null) - .setNegativeButton(R.string.abort_button) { _: DialogInterface?, _: Int -> - coreErrorAlertResult = false - synchronized(coreErrorAlertLock) { coreErrorAlertLock.notify() } - } - .create() - } - - override fun onDismiss(dialog: DialogInterface) { - coreErrorAlertResult = true - synchronized(coreErrorAlertLock) { coreErrorAlertLock.notify() } - } - - companion object { - fun newInstance(title: String?, message: String?): CoreErrorDialogFragment { - val frag = CoreErrorDialogFragment() - val args = Bundle() - args.putString("title", title) - args.putString("message", message) - frag.arguments = args - return frag - } - } - } + var coreErrorAlertResult = false + val coreErrorAlertLock = Object() private fun onCoreErrorImpl(title: String, message: String) { val emulationActivity = sEmulationActivity.get() if (emulationActivity == null) { - error("[NativeLibrary] EmulationActivity not present") + Log.error("[NativeLibrary] EmulationActivity not present") return } @@ -373,7 +203,7 @@ object NativeLibrary { fun onCoreError(error: CoreError?, details: String): Boolean { val emulationActivity = sEmulationActivity.get() if (emulationActivity == null) { - error("[NativeLibrary] EmulationActivity not present") + Log.error("[NativeLibrary] EmulationActivity not present") return false } @@ -404,7 +234,7 @@ object NativeLibrary { } // Show the AlertDialog on the main thread. - emulationActivity.runOnUiThread(Runnable { onCoreErrorImpl(title, message) }) + emulationActivity.runOnUiThread { onCoreErrorImpl(title, message) } // Wait for the lock to notify that it is complete. synchronized(coreErrorAlertLock) { coreErrorAlertLock.wait() } @@ -629,46 +459,4 @@ object NativeLibrary { * Checks if all necessary keys are present for decryption */ external fun areKeysPresent(): Boolean - - /** - * Button type for use in onTouchEvent - */ - object ButtonType { - const val BUTTON_A = 0 - const val BUTTON_B = 1 - const val BUTTON_X = 2 - const val BUTTON_Y = 3 - const val STICK_L = 4 - const val STICK_R = 5 - const val TRIGGER_L = 6 - const val TRIGGER_R = 7 - const val TRIGGER_ZL = 8 - const val TRIGGER_ZR = 9 - const val BUTTON_PLUS = 10 - const val BUTTON_MINUS = 11 - const val DPAD_LEFT = 12 - const val DPAD_UP = 13 - const val DPAD_RIGHT = 14 - const val DPAD_DOWN = 15 - const val BUTTON_SL = 16 - const val BUTTON_SR = 17 - const val BUTTON_HOME = 18 - const val BUTTON_CAPTURE = 19 - } - - /** - * Stick type for use in onTouchEvent - */ - object StickType { - const val STICK_L = 0 - const val STICK_R = 1 - } - - /** - * Button states - */ - object ButtonState { - const val RELEASED = 0 - const val PRESSED = 1 - } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt index 76778c10a..72943f33e 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt @@ -7,6 +7,7 @@ import android.app.Application import android.app.NotificationChannel import android.app.NotificationManager import android.content.Context +import org.yuzu.yuzu_emu.features.input.NativeInput import java.io.File import org.yuzu.yuzu_emu.utils.DirectoryInitialization import org.yuzu.yuzu_emu.utils.DocumentsTree @@ -37,6 +38,7 @@ class YuzuApplication : Application() { documentsTree = DocumentsTree() DirectoryInitialization.start() GpuDriverHelper.initializeDriverParameters() + NativeInput.reloadInputDevices() NativeLibrary.logDeviceInfo() Log.logDeviceInfo() diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt index 7a8d03610..c962558a7 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt @@ -39,6 +39,7 @@ import org.yuzu.yuzu_emu.NativeLibrary import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.YuzuApplication import org.yuzu.yuzu_emu.databinding.ActivityEmulationBinding +import org.yuzu.yuzu_emu.features.input.NativeInput import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting import org.yuzu.yuzu_emu.features.settings.model.IntSetting import org.yuzu.yuzu_emu.features.settings.model.Settings @@ -47,7 +48,9 @@ import org.yuzu.yuzu_emu.model.Game import org.yuzu.yuzu_emu.utils.InputHandler import org.yuzu.yuzu_emu.utils.Log import org.yuzu.yuzu_emu.utils.MemoryUtil +import org.yuzu.yuzu_emu.utils.NativeConfig import org.yuzu.yuzu_emu.utils.NfcReader +import org.yuzu.yuzu_emu.utils.ParamPackage import org.yuzu.yuzu_emu.utils.ThemeHelper import java.text.NumberFormat import kotlin.math.roundToInt @@ -63,8 +66,6 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener { private var motionTimestamp: Long = 0 private var flipMotionOrientation: Boolean = false - private var controllerIds = InputHandler.getGameControllerIds() - private val actionPause = "ACTION_EMULATOR_PAUSE" private val actionPlay = "ACTION_EMULATOR_PLAY" private val actionMute = "ACTION_EMULATOR_MUTE" @@ -78,6 +79,33 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener { super.onCreate(savedInstanceState) + InputHandler.updateControllerData() + val players = NativeConfig.getInputSettings(true) + var hasConfiguredControllers = false + players.forEach { + if (it.hasMapping()) { + hasConfiguredControllers = true + } + } + if (!hasConfiguredControllers && InputHandler.androidControllers.isNotEmpty()) { + var params: ParamPackage? = null + for (controller in InputHandler.registeredControllers) { + if (controller.get("port", -1) == 0) { + params = controller + break + } + } + + if (params != null) { + NativeInput.updateMappingsWithDefault( + 0, + params, + params.get("display", getString(R.string.unknown)) + ) + NativeConfig.saveGlobalConfig() + } + } + binding = ActivityEmulationBinding.inflate(layoutInflater) setContentView(binding.root) @@ -95,8 +123,6 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener { nfcReader = NfcReader(this) nfcReader.initialize() - InputHandler.initialize() - val preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) if (!preferences.getBoolean(Settings.PREF_MEMORY_WARNING_SHOWN, false)) { if (MemoryUtil.isLessThan(MemoryUtil.REQUIRED_MEMORY, MemoryUtil.totalMemory)) { @@ -147,7 +173,7 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener { super.onResume() nfcReader.startScanning() startMotionSensorListener() - InputHandler.updateControllerIds() + InputHandler.updateControllerData() buildPictureInPictureParams() } @@ -172,6 +198,7 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener { super.onNewIntent(intent) setIntent(intent) nfcReader.onNewIntent(intent) + InputHandler.updateControllerData() } override fun dispatchKeyEvent(event: KeyEvent): Boolean { @@ -244,8 +271,8 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener { } val deltaTimestamp = (event.timestamp - motionTimestamp) / 1000 motionTimestamp = event.timestamp - NativeLibrary.onGamePadMotionEvent( - NativeLibrary.Player1Device, + NativeInput.onDeviceMotionEvent( + NativeInput.Player1Device, deltaTimestamp, gyro[0], gyro[1], @@ -254,8 +281,8 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener { accel[1], accel[2] ) - NativeLibrary.onGamePadMotionEvent( - NativeLibrary.ConsoleDevice, + NativeInput.onDeviceMotionEvent( + NativeInput.ConsoleDevice, deltaTimestamp, gyro[0], gyro[1], diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/DriverAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/DriverAdapter.kt index f218c76ef..50663ad91 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/DriverAdapter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/DriverAdapter.kt @@ -3,15 +3,15 @@ package org.yuzu.yuzu_emu.adapters -import android.text.TextUtils import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.databinding.CardDriverOptionBinding import org.yuzu.yuzu_emu.features.settings.model.StringSetting import org.yuzu.yuzu_emu.model.Driver import org.yuzu.yuzu_emu.model.DriverViewModel +import org.yuzu.yuzu_emu.utils.ViewUtils.marquee +import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder class DriverAdapter(private val driverViewModel: DriverViewModel) : @@ -44,25 +44,15 @@ class DriverAdapter(private val driverViewModel: DriverViewModel) : } // Delay marquee by 3s - title.postDelayed( - { - title.isSelected = true - title.ellipsize = TextUtils.TruncateAt.MARQUEE - version.isSelected = true - version.ellipsize = TextUtils.TruncateAt.MARQUEE - description.isSelected = true - description.ellipsize = TextUtils.TruncateAt.MARQUEE - }, - 3000 - ) + title.marquee() + version.marquee() + description.marquee() title.text = model.title version.text = model.version description.text = model.description - if (model.title != binding.root.context.getString(R.string.system_gpu_driver)) { - buttonDelete.visibility = View.VISIBLE - } else { - buttonDelete.visibility = View.GONE - } + buttonDelete.setVisible( + model.title != binding.root.context.getString(R.string.system_gpu_driver) + ) } } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/FolderAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/FolderAdapter.kt index 3d8f0bda8..5cbd15d2a 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/FolderAdapter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/FolderAdapter.kt @@ -4,7 +4,6 @@ package org.yuzu.yuzu_emu.adapters import android.net.Uri -import android.text.TextUtils import android.view.LayoutInflater import android.view.ViewGroup import androidx.fragment.app.FragmentActivity @@ -12,6 +11,7 @@ import org.yuzu.yuzu_emu.databinding.CardFolderBinding import org.yuzu.yuzu_emu.fragments.GameFolderPropertiesDialogFragment import org.yuzu.yuzu_emu.model.GameDir import org.yuzu.yuzu_emu.model.GamesViewModel +import org.yuzu.yuzu_emu.utils.ViewUtils.marquee import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder class FolderAdapter(val activity: FragmentActivity, val gamesViewModel: GamesViewModel) : @@ -29,13 +29,7 @@ class FolderAdapter(val activity: FragmentActivity, val gamesViewModel: GamesVie override fun bind(model: GameDir) { binding.apply { path.text = Uri.parse(model.uriString).path - path.postDelayed( - { - path.isSelected = true - path.ellipsize = TextUtils.TruncateAt.MARQUEE - }, - 3000 - ) + path.marquee() buttonEdit.setOnClickListener { GameFolderPropertiesDialogFragment.newInstance(model) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt index 85c8249e6..b1f247ac3 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt @@ -4,7 +4,6 @@ package org.yuzu.yuzu_emu.adapters import android.net.Uri -import android.text.TextUtils import android.view.LayoutInflater import android.view.ViewGroup import android.widget.ImageView @@ -27,6 +26,7 @@ import org.yuzu.yuzu_emu.databinding.CardGameBinding import org.yuzu.yuzu_emu.model.Game import org.yuzu.yuzu_emu.model.GamesViewModel import org.yuzu.yuzu_emu.utils.GameIconUtils +import org.yuzu.yuzu_emu.utils.ViewUtils.marquee import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder class GameAdapter(private val activity: AppCompatActivity) : @@ -44,14 +44,7 @@ class GameAdapter(private val activity: AppCompatActivity) : binding.textGameTitle.text = model.title.replace("[\\t\\n\\r]+".toRegex(), " ") - binding.textGameTitle.postDelayed( - { - binding.textGameTitle.ellipsize = TextUtils.TruncateAt.MARQUEE - binding.textGameTitle.isSelected = true - }, - 3000 - ) - + binding.textGameTitle.marquee() binding.cardGame.setOnClickListener { onClick(model) } binding.cardGame.setOnLongClickListener { onLongClick(model) } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GamePropertiesAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GamePropertiesAdapter.kt index 0046d5314..7366e2c77 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GamePropertiesAdapter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GamePropertiesAdapter.kt @@ -3,21 +3,18 @@ package org.yuzu.yuzu_emu.adapters -import android.text.TextUtils import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup import androidx.core.content.res.ResourcesCompat -import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle -import kotlinx.coroutines.launch import org.yuzu.yuzu_emu.databinding.CardInstallableIconBinding import org.yuzu.yuzu_emu.databinding.CardSimpleOutlinedBinding import org.yuzu.yuzu_emu.model.GameProperty import org.yuzu.yuzu_emu.model.InstallableProperty import org.yuzu.yuzu_emu.model.SubmenuProperty +import org.yuzu.yuzu_emu.utils.ViewUtils.marquee +import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible +import org.yuzu.yuzu_emu.utils.collect import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder class GamePropertiesAdapter( @@ -76,23 +73,15 @@ class GamePropertiesAdapter( ) ) - binding.details.postDelayed({ - binding.details.isSelected = true - binding.details.ellipsize = TextUtils.TruncateAt.MARQUEE - }, 3000) - + binding.details.marquee() if (submenuProperty.details != null) { - binding.details.visibility = View.VISIBLE + binding.details.setVisible(true) binding.details.text = submenuProperty.details.invoke() } else if (submenuProperty.detailsFlow != null) { - binding.details.visibility = View.VISIBLE - viewLifecycle.lifecycleScope.launch { - viewLifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { - submenuProperty.detailsFlow.collect { binding.details.text = it } - } - } + binding.details.setVisible(true) + submenuProperty.detailsFlow.collect(viewLifecycle) { binding.details.text = it } } else { - binding.details.visibility = View.GONE + binding.details.setVisible(false) } } } @@ -112,14 +101,10 @@ class GamePropertiesAdapter( ) ) - if (installableProperty.install != null) { - binding.buttonInstall.visibility = View.VISIBLE - binding.buttonInstall.setOnClickListener { installableProperty.install.invoke() } - } - if (installableProperty.export != null) { - binding.buttonExport.visibility = View.VISIBLE - binding.buttonExport.setOnClickListener { installableProperty.export.invoke() } - } + binding.buttonInstall.setVisible(installableProperty.install != null) + binding.buttonInstall.setOnClickListener { installableProperty.install?.invoke() } + binding.buttonExport.setVisible(installableProperty.export != null) + binding.buttonExport.setOnClickListener { installableProperty.export?.invoke() } } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/HomeSettingAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/HomeSettingAdapter.kt index b512845d5..0bd196673 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/HomeSettingAdapter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/HomeSettingAdapter.kt @@ -3,22 +3,19 @@ package org.yuzu.yuzu_emu.adapters -import android.text.TextUtils import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat import androidx.core.content.res.ResourcesCompat -import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle -import kotlinx.coroutines.launch import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.databinding.CardHomeOptionBinding import org.yuzu.yuzu_emu.fragments.MessageDialogFragment import org.yuzu.yuzu_emu.model.HomeSetting +import org.yuzu.yuzu_emu.utils.ViewUtils.marquee +import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible +import org.yuzu.yuzu_emu.utils.collect import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder class HomeSettingAdapter( @@ -59,18 +56,8 @@ class HomeSettingAdapter( binding.optionIcon.alpha = 0.5f } - viewLifecycle.lifecycleScope.launch { - viewLifecycle.repeatOnLifecycle(Lifecycle.State.CREATED) { - model.details.collect { updateOptionDetails(it) } - } - } - binding.optionDetail.postDelayed( - { - binding.optionDetail.ellipsize = TextUtils.TruncateAt.MARQUEE - binding.optionDetail.isSelected = true - }, - 3000 - ) + model.details.collect(viewLifecycle) { updateOptionDetails(it) } + binding.optionDetail.marquee() binding.root.setOnClickListener { onClick(model) } } @@ -90,7 +77,7 @@ class HomeSettingAdapter( private fun updateOptionDetails(detailString: String) { if (detailString.isNotEmpty()) { binding.optionDetail.text = detailString - binding.optionDetail.visibility = View.VISIBLE + binding.optionDetail.setVisible(true) } } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/InstallableAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/InstallableAdapter.kt index 4218c4e52..1ba75fa2f 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/InstallableAdapter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/InstallableAdapter.kt @@ -4,10 +4,10 @@ package org.yuzu.yuzu_emu.adapters import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup import org.yuzu.yuzu_emu.databinding.CardInstallableBinding import org.yuzu.yuzu_emu.model.Installable +import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder class InstallableAdapter(installables: List<Installable>) : @@ -26,14 +26,10 @@ class InstallableAdapter(installables: List<Installable>) : binding.title.setText(model.titleId) binding.description.setText(model.descriptionId) - if (model.install != null) { - binding.buttonInstall.visibility = View.VISIBLE - binding.buttonInstall.setOnClickListener { model.install.invoke() } - } - if (model.export != null) { - binding.buttonExport.visibility = View.VISIBLE - binding.buttonExport.setOnClickListener { model.export.invoke() } - } + binding.buttonInstall.setVisible(model.install != null) + binding.buttonInstall.setOnClickListener { model.install?.invoke() } + binding.buttonExport.setVisible(model.export != null) + binding.buttonExport.setOnClickListener { model.export?.invoke() } } } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/LicenseAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/LicenseAdapter.kt index 38bb1f96f..1379968f9 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/LicenseAdapter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/LicenseAdapter.kt @@ -4,12 +4,12 @@ package org.yuzu.yuzu_emu.adapters import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup import androidx.appcompat.app.AppCompatActivity import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding import org.yuzu.yuzu_emu.fragments.LicenseBottomSheetDialogFragment import org.yuzu.yuzu_emu.model.License +import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder class LicenseAdapter(private val activity: AppCompatActivity, licenses: List<License>) : @@ -25,7 +25,7 @@ class LicenseAdapter(private val activity: AppCompatActivity, licenses: List<Lic binding.apply { textSettingName.text = root.context.getString(model.titleId) textSettingDescription.text = root.context.getString(model.descriptionId) - textSettingValue.visibility = View.GONE + textSettingValue.setVisible(false) root.setOnClickListener { onClick(model) } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/SetupAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/SetupAdapter.kt index 02118e1a8..a5f610b31 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/SetupAdapter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/SetupAdapter.kt @@ -5,7 +5,6 @@ package org.yuzu.yuzu_emu.adapters import android.text.Html import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup import androidx.appcompat.app.AppCompatActivity import androidx.core.content.res.ResourcesCompat @@ -17,6 +16,7 @@ import org.yuzu.yuzu_emu.model.SetupCallback import org.yuzu.yuzu_emu.model.SetupPage import org.yuzu.yuzu_emu.model.StepState import org.yuzu.yuzu_emu.utils.ViewUtils +import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder class SetupAdapter(val activity: AppCompatActivity, pages: List<SetupPage>) : @@ -30,8 +30,8 @@ class SetupAdapter(val activity: AppCompatActivity, pages: List<SetupPage>) : AbstractViewHolder<SetupPage>(binding), SetupCallback { override fun bind(model: SetupPage) { if (model.stepCompleted.invoke() == StepState.COMPLETE) { - binding.buttonAction.visibility = View.INVISIBLE - binding.textConfirmation.visibility = View.VISIBLE + binding.buttonAction.setVisible(visible = false, gone = false) + binding.textConfirmation.setVisible(true) } binding.icon.setImageDrawable( diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/NativeInput.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/NativeInput.kt new file mode 100644 index 000000000..15d776311 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/NativeInput.kt @@ -0,0 +1,416 @@ +// SPDX-FileCopyrightText: 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.features.input + +import org.yuzu.yuzu_emu.features.input.model.NativeButton +import org.yuzu.yuzu_emu.features.input.model.NativeAnalog +import org.yuzu.yuzu_emu.features.input.model.InputType +import org.yuzu.yuzu_emu.features.input.model.ButtonName +import org.yuzu.yuzu_emu.features.input.model.NpadStyleIndex +import org.yuzu.yuzu_emu.utils.NativeConfig +import org.yuzu.yuzu_emu.utils.ParamPackage +import android.view.InputDevice + +object NativeInput { + /** + * Default controller id for each device + */ + const val Player1Device = 0 + const val Player2Device = 1 + const val Player3Device = 2 + const val Player4Device = 3 + const val Player5Device = 4 + const val Player6Device = 5 + const val Player7Device = 6 + const val Player8Device = 7 + const val ConsoleDevice = 8 + + /** + * Button states + */ + object ButtonState { + const val RELEASED = 0 + const val PRESSED = 1 + } + + /** + * Returns true if pro controller isn't available and handheld is. + * Intended to check where the input overlay should direct its inputs. + */ + external fun isHandheldOnly(): Boolean + + /** + * Handles button press events for a gamepad. + * @param guid 32 character hexadecimal string consisting of the controller's PID+VID. + * @param port Port determined by controller connection order. + * @param buttonId The Android Keycode corresponding to this event. + * @param action Mask identifying which action is happening (button pressed down, or button released). + */ + external fun onGamePadButtonEvent( + guid: String, + port: Int, + buttonId: Int, + action: Int + ) + + /** + * Handles axis movement events. + * @param guid 32 character hexadecimal string consisting of the controller's PID+VID. + * @param port Port determined by controller connection order. + * @param axis The axis ID. + * @param value Value along the given axis. + */ + external fun onGamePadAxisEvent(guid: String, port: Int, axis: Int, value: Float) + + /** + * Handles motion events. + * @param guid 32 character hexadecimal string consisting of the controller's PID+VID. + * @param port Port determined by controller connection order. + * @param deltaTimestamp The finger id corresponding to this event. + * @param xGyro The value of the x-axis for the gyroscope. + * @param yGyro The value of the y-axis for the gyroscope. + * @param zGyro The value of the z-axis for the gyroscope. + * @param xAccel The value of the x-axis for the accelerometer. + * @param yAccel The value of the y-axis for the accelerometer. + * @param zAccel The value of the z-axis for the accelerometer. + */ + external fun onGamePadMotionEvent( + guid: String, + port: Int, + deltaTimestamp: Long, + xGyro: Float, + yGyro: Float, + zGyro: Float, + xAccel: Float, + yAccel: Float, + zAccel: Float + ) + + /** + * Signals and load a nfc tag + * @param data Byte array containing all the data from a nfc tag. + */ + external fun onReadNfcTag(data: ByteArray?) + + /** + * Removes current loaded nfc tag. + */ + external fun onRemoveNfcTag() + + /** + * Handles touch press events. + * @param fingerId The finger id corresponding to this event. + * @param xAxis The value of the x-axis on the touchscreen. + * @param yAxis The value of the y-axis on the touchscreen. + */ + external fun onTouchPressed(fingerId: Int, xAxis: Float, yAxis: Float) + + /** + * Handles touch movement. + * @param fingerId The finger id corresponding to this event. + * @param xAxis The value of the x-axis on the touchscreen. + * @param yAxis The value of the y-axis on the touchscreen. + */ + external fun onTouchMoved(fingerId: Int, xAxis: Float, yAxis: Float) + + /** + * Handles touch release events. + * @param fingerId The finger id corresponding to this event + */ + external fun onTouchReleased(fingerId: Int) + + /** + * Sends a button input to the global virtual controllers. + * @param port Port determined by controller connection order. + * @param button The [NativeButton] corresponding to this event. + * @param action Mask identifying which action is happening (button pressed down, or button released). + */ + fun onOverlayButtonEvent(port: Int, button: NativeButton, action: Int) = + onOverlayButtonEventImpl(port, button.int, action) + + private external fun onOverlayButtonEventImpl(port: Int, buttonId: Int, action: Int) + + /** + * Sends a joystick input to the global virtual controllers. + * @param port Port determined by controller connection order. + * @param stick The [NativeAnalog] corresponding to this event. + * @param xAxis Value along the X axis. + * @param yAxis Value along the Y axis. + */ + fun onOverlayJoystickEvent(port: Int, stick: NativeAnalog, xAxis: Float, yAxis: Float) = + onOverlayJoystickEventImpl(port, stick.int, xAxis, yAxis) + + private external fun onOverlayJoystickEventImpl( + port: Int, + stickId: Int, + xAxis: Float, + yAxis: Float + ) + + /** + * Handles motion events for the global virtual controllers. + * @param port Port determined by controller connection order + * @param deltaTimestamp The finger id corresponding to this event. + * @param xGyro The value of the x-axis for the gyroscope. + * @param yGyro The value of the y-axis for the gyroscope. + * @param zGyro The value of the z-axis for the gyroscope. + * @param xAccel The value of the x-axis for the accelerometer. + * @param yAccel The value of the y-axis for the accelerometer. + * @param zAccel The value of the z-axis for the accelerometer. + */ + external fun onDeviceMotionEvent( + port: Int, + deltaTimestamp: Long, + xGyro: Float, + yGyro: Float, + zGyro: Float, + xAccel: Float, + yAccel: Float, + zAccel: Float + ) + + /** + * Reloads all input devices from the currently loaded Settings::values.players into HID Core + */ + external fun reloadInputDevices() + + /** + * Registers a controller to be used with mapping + * @param device An [InputDevice] or the input overlay wrapped with [YuzuInputDevice] + */ + external fun registerController(device: YuzuInputDevice) + + /** + * Gets the names of input devices that have been registered with the input subsystem via [registerController] + */ + external fun getInputDevices(): Array<String> + + /** + * Reads all input profiles from disk. Must be called before creating a profile picker. + */ + external fun loadInputProfiles() + + /** + * Gets the names of each available input profile. + */ + external fun getInputProfileNames(): Array<String> + + /** + * Checks if the user-provided name for an input profile is valid. + * @param name User-provided name for an input profile. + * @return Whether [name] is valid or not. + */ + external fun isProfileNameValid(name: String): Boolean + + /** + * Creates a new input profile. + * @param name The new profile's name. + * @param playerIndex Index of the player that's currently being edited. Used to write the profile + * name to this player's config. + * @return Whether creating the profile was successful or not. + */ + external fun createProfile(name: String, playerIndex: Int): Boolean + + /** + * Deletes an input profile. + * @param name Name of the profile to delete. + * @param playerIndex Index of the player that's currently being edited. Used to remove the profile + * name from this player's config if they have it loaded. + * @return Whether deleting this profile was successful or not. + */ + external fun deleteProfile(name: String, playerIndex: Int): Boolean + + /** + * Loads an input profile. + * @param name Name of the input profile to load. + * @param playerIndex Index of the player that will have this profile loaded. + * @return Whether loading this profile was successful or not. + */ + external fun loadProfile(name: String, playerIndex: Int): Boolean + + /** + * Saves an input profile. + * @param name Name of the profile to save. + * @param playerIndex Index of the player that's currently being edited. Used to write the profile + * name to this player's config. + * @return Whether saving the profile was successful or not. + */ + external fun saveProfile(name: String, playerIndex: Int): Boolean + + /** + * Intended to be used immediately before a call to [NativeConfig.saveControlPlayerValues] + * Must be used while per-game config is loaded. + */ + external fun loadPerGameConfiguration( + playerIndex: Int, + selectedIndex: Int, + selectedProfileName: String + ) + + /** + * Tells the input subsystem to start listening for inputs to map. + * @param type Type of input to map as shown by the int property in each [InputType]. + */ + external fun beginMapping(type: Int) + + /** + * Gets an input's [ParamPackage] as a serialized string. Used for input verification before mapping. + * Must be run after [beginMapping] and before [stopMapping]. + */ + external fun getNextInput(): String + + /** + * Tells the input subsystem to stop listening for inputs to map. + */ + external fun stopMapping() + + /** + * Updates a controller's mappings with auto-mapping params. + * @param playerIndex Index of the player to auto-map. + * @param deviceParams [ParamPackage] representing the device to auto-map as received + * from [getInputDevices]. + * @param displayName Name of the device to auto-map as received from the "display" param in [deviceParams]. + * Intended to be a way to provide a default name for a controller if the "display" param is empty. + */ + fun updateMappingsWithDefault( + playerIndex: Int, + deviceParams: ParamPackage, + displayName: String + ) = updateMappingsWithDefaultImpl(playerIndex, deviceParams.serialize(), displayName) + + private external fun updateMappingsWithDefaultImpl( + playerIndex: Int, + deviceParams: String, + displayName: String + ) + + /** + * Gets the params for a specific button. + * @param playerIndex Index of the player to get params from. + * @param button The [NativeButton] to get params for. + * @return A [ParamPackage] representing a player's specific button. + */ + fun getButtonParam(playerIndex: Int, button: NativeButton): ParamPackage = + ParamPackage(getButtonParamImpl(playerIndex, button.int)) + + private external fun getButtonParamImpl(playerIndex: Int, buttonId: Int): String + + /** + * Sets the params for a specific button. + * @param playerIndex Index of the player to set params for. + * @param button The [NativeButton] to set params for. + * @param param A [ParamPackage] to set. + */ + fun setButtonParam(playerIndex: Int, button: NativeButton, param: ParamPackage) = + setButtonParamImpl(playerIndex, button.int, param.serialize()) + + private external fun setButtonParamImpl(playerIndex: Int, buttonId: Int, param: String) + + /** + * Gets the params for a specific stick. + * @param playerIndex Index of the player to get params from. + * @param stick The [NativeAnalog] to get params for. + * @return A [ParamPackage] representing a player's specific stick. + */ + fun getStickParam(playerIndex: Int, stick: NativeAnalog): ParamPackage = + ParamPackage(getStickParamImpl(playerIndex, stick.int)) + + private external fun getStickParamImpl(playerIndex: Int, stickId: Int): String + + /** + * Sets the params for a specific stick. + * @param playerIndex Index of the player to set params for. + * @param stick The [NativeAnalog] to set params for. + * @param param A [ParamPackage] to set. + */ + fun setStickParam(playerIndex: Int, stick: NativeAnalog, param: ParamPackage) = + setStickParamImpl(playerIndex, stick.int, param.serialize()) + + private external fun setStickParamImpl(playerIndex: Int, stickId: Int, param: String) + + /** + * Gets the int representation of a [ButtonName]. Tells you what to show as the mapped input for + * a button/analog/other. + * @param param A [ParamPackage] that represents a specific button's params. + * @return The [ButtonName] for [param]. + */ + fun getButtonName(param: ParamPackage): ButtonName = + ButtonName.from(getButtonNameImpl(param.serialize())) + + private external fun getButtonNameImpl(param: String): Int + + /** + * Gets each supported [NpadStyleIndex] for a given player. + * @param playerIndex Index of the player to get supported indexes for. + * @return List of each supported [NpadStyleIndex]. + */ + fun getSupportedStyleTags(playerIndex: Int): List<NpadStyleIndex> = + getSupportedStyleTagsImpl(playerIndex).map { NpadStyleIndex.from(it) } + + private external fun getSupportedStyleTagsImpl(playerIndex: Int): IntArray + + /** + * Gets the [NpadStyleIndex] for a given player. + * @param playerIndex Index of the player to get an [NpadStyleIndex] from. + * @return The [NpadStyleIndex] for a given player. + */ + fun getStyleIndex(playerIndex: Int): NpadStyleIndex = + NpadStyleIndex.from(getStyleIndexImpl(playerIndex)) + + private external fun getStyleIndexImpl(playerIndex: Int): Int + + /** + * Sets the [NpadStyleIndex] for a given player. + * @param playerIndex Index of the player to change. + * @param style The new style to set. + */ + fun setStyleIndex(playerIndex: Int, style: NpadStyleIndex) = + setStyleIndexImpl(playerIndex, style.int) + + private external fun setStyleIndexImpl(playerIndex: Int, styleIndex: Int) + + /** + * Checks if a device is a controller. + * @param params [ParamPackage] for an input device retrieved from [getInputDevices] + * @return Whether the device is a controller or not. + */ + fun isController(params: ParamPackage): Boolean = isControllerImpl(params.serialize()) + + private external fun isControllerImpl(params: String): Boolean + + /** + * Checks if a controller is connected + * @param playerIndex Index of the player to check. + * @return Whether the player is connected or not. + */ + external fun getIsConnected(playerIndex: Int): Boolean + + /** + * Connects/disconnects a controller and ensures that connection order stays in-tact. + * @param playerIndex Index of the player to connect/disconnect. + * @param connected Whether to connect or disconnect this controller. + */ + fun connectControllers(playerIndex: Int, connected: Boolean = true) { + val connectedControllers = mutableListOf<Boolean>().apply { + if (connected) { + for (i in 0 until 8) { + add(i <= playerIndex) + } + } else { + for (i in 0 until 8) { + add(i < playerIndex) + } + } + } + connectControllersImpl(connectedControllers.toBooleanArray()) + } + + private external fun connectControllersImpl(connected: BooleanArray) + + /** + * Resets all of the button and analog mappings for a player. + * @param playerIndex Index of the player that will have its mappings reset. + */ + external fun resetControllerMappings(playerIndex: Int) +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/YuzuInputDevice.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/YuzuInputDevice.kt new file mode 100644 index 000000000..15cc38c7f --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/YuzuInputDevice.kt @@ -0,0 +1,93 @@ +// SPDX-FileCopyrightText: 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.features.input + +import android.view.InputDevice +import androidx.annotation.Keep +import org.yuzu.yuzu_emu.YuzuApplication +import org.yuzu.yuzu_emu.R +import org.yuzu.yuzu_emu.utils.InputHandler.getGUID + +@Keep +interface YuzuInputDevice { + fun getName(): String + + fun getGUID(): String + + fun getPort(): Int + + fun getSupportsVibration(): Boolean + + fun vibrate(intensity: Float) + + fun getAxes(): Array<Int> = arrayOf() + fun hasKeys(keys: IntArray): BooleanArray = BooleanArray(0) +} + +class YuzuPhysicalDevice( + private val device: InputDevice, + private val port: Int, + useSystemVibrator: Boolean +) : YuzuInputDevice { + private val vibrator = if (useSystemVibrator) { + YuzuVibrator.getSystemVibrator() + } else { + YuzuVibrator.getControllerVibrator(device) + } + + override fun getName(): String { + return device.name + } + + override fun getGUID(): String { + return device.getGUID() + } + + override fun getPort(): Int { + return port + } + + override fun getSupportsVibration(): Boolean { + return vibrator.supportsVibration() + } + + override fun vibrate(intensity: Float) { + vibrator.vibrate(intensity) + } + + override fun getAxes(): Array<Int> = device.motionRanges.map { it.axis }.toTypedArray() + override fun hasKeys(keys: IntArray): BooleanArray = device.hasKeys(*keys) +} + +class YuzuInputOverlayDevice( + private val vibration: Boolean, + private val port: Int +) : YuzuInputDevice { + private val vibrator = YuzuVibrator.getSystemVibrator() + + override fun getName(): String { + return YuzuApplication.appContext.getString(R.string.input_overlay) + } + + override fun getGUID(): String { + return "00000000000000000000000000000000" + } + + override fun getPort(): Int { + return port + } + + override fun getSupportsVibration(): Boolean { + if (vibration) { + return vibrator.supportsVibration() + } + return false + } + + override fun vibrate(intensity: Float) { + if (vibration) { + vibrator.vibrate(intensity) + } + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/YuzuVibrator.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/YuzuVibrator.kt new file mode 100644 index 000000000..aac49ecae --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/YuzuVibrator.kt @@ -0,0 +1,76 @@ +// SPDX-FileCopyrightText: 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.features.input + +import android.content.Context +import android.os.Build +import android.os.CombinedVibration +import android.os.VibrationEffect +import android.os.Vibrator +import android.os.VibratorManager +import android.view.InputDevice +import androidx.annotation.Keep +import androidx.annotation.RequiresApi +import org.yuzu.yuzu_emu.YuzuApplication + +@Keep +@Suppress("DEPRECATION") +interface YuzuVibrator { + fun supportsVibration(): Boolean + + fun vibrate(intensity: Float) + + companion object { + fun getControllerVibrator(device: InputDevice): YuzuVibrator = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + YuzuVibratorManager(device.vibratorManager) + } else { + YuzuVibratorManagerCompat(device.vibrator) + } + + fun getSystemVibrator(): YuzuVibrator = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + val vibratorManager = YuzuApplication.appContext + .getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager + YuzuVibratorManager(vibratorManager) + } else { + val vibrator = YuzuApplication.appContext + .getSystemService(Context.VIBRATOR_SERVICE) as Vibrator + YuzuVibratorManagerCompat(vibrator) + } + + fun getVibrationEffect(intensity: Float): VibrationEffect? { + if (intensity > 0f) { + return VibrationEffect.createOneShot( + 50, + (255.0 * intensity).toInt().coerceIn(1, 255) + ) + } + return null + } + } +} + +@RequiresApi(Build.VERSION_CODES.S) +class YuzuVibratorManager(private val vibratorManager: VibratorManager) : YuzuVibrator { + override fun supportsVibration(): Boolean { + return vibratorManager.vibratorIds.isNotEmpty() + } + + override fun vibrate(intensity: Float) { + val vibration = YuzuVibrator.getVibrationEffect(intensity) ?: return + vibratorManager.vibrate(CombinedVibration.createParallel(vibration)) + } +} + +class YuzuVibratorManagerCompat(private val vibrator: Vibrator) : YuzuVibrator { + override fun supportsVibration(): Boolean { + return vibrator.hasVibrator() + } + + override fun vibrate(intensity: Float) { + val vibration = YuzuVibrator.getVibrationEffect(intensity) ?: return + vibrator.vibrate(vibration) + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/AnalogDirection.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/AnalogDirection.kt new file mode 100644 index 000000000..0a5fab2ae --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/AnalogDirection.kt @@ -0,0 +1,11 @@ +// SPDX-FileCopyrightText: 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.features.input.model + +enum class AnalogDirection(val int: Int, val param: String) { + Up(0, "up"), + Down(1, "down"), + Left(2, "left"), + Right(3, "right") +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/ButtonName.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/ButtonName.kt new file mode 100644 index 000000000..b8846ecad --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/ButtonName.kt @@ -0,0 +1,19 @@ +// SPDX-FileCopyrightText: 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.features.input.model + +// Loosely matches the enum in common/input.h +enum class ButtonName(val int: Int) { + Invalid(1), + + // This will display the engine name instead of the button name + Engine(2), + + // This will display the button by value instead of the button name + Value(3); + + companion object { + fun from(int: Int): ButtonName = entries.firstOrNull { it.int == int } ?: Invalid + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/InputType.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/InputType.kt new file mode 100644 index 000000000..f725231cb --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/InputType.kt @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.features.input.model + +// Must match the corresponding enum in input_common/main.h +enum class InputType(val int: Int) { + None(0), + Button(1), + Stick(2), + Motion(3), + Touch(4) +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/NativeAnalog.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/NativeAnalog.kt new file mode 100644 index 000000000..c3b7a785d --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/NativeAnalog.kt @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.features.input.model + +// Must match enum in src/common/settings_input.h +enum class NativeAnalog(val int: Int) { + LStick(0), + RStick(1); + + companion object { + fun from(int: Int): NativeAnalog = entries.firstOrNull { it.int == int } ?: LStick + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/NativeButton.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/NativeButton.kt new file mode 100644 index 000000000..c5ccd7115 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/NativeButton.kt @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.features.input.model + +// Must match enum in src/common/settings_input.h +enum class NativeButton(val int: Int) { + A(0), + B(1), + X(2), + Y(3), + LStick(4), + RStick(5), + L(6), + R(7), + ZL(8), + ZR(9), + Plus(10), + Minus(11), + + DLeft(12), + DUp(13), + DRight(14), + DDown(15), + + SLLeft(16), + SRLeft(17), + + Home(18), + Capture(19), + + SLRight(20), + SRRight(21); + + companion object { + fun from(int: Int): NativeButton = entries.firstOrNull { it.int == int } ?: A + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/NativeTrigger.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/NativeTrigger.kt new file mode 100644 index 000000000..625f352b4 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/NativeTrigger.kt @@ -0,0 +1,10 @@ +// SPDX-FileCopyrightText: 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.features.input.model + +// Must match enum in src/common/settings_input.h +enum class NativeTrigger(val int: Int) { + LTrigger(0), + RTrigger(1) +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/NpadStyleIndex.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/NpadStyleIndex.kt new file mode 100644 index 000000000..e2a3d7aff --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/NpadStyleIndex.kt @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.features.input.model + +import androidx.annotation.StringRes +import org.yuzu.yuzu_emu.R + +// Must match enum in src/core/hid/hid_types.h +enum class NpadStyleIndex(val int: Int, @StringRes val nameId: Int = 0) { + None(0), + Fullkey(3, R.string.pro_controller), + Handheld(4, R.string.handheld), + HandheldNES(4), + JoyconDual(5, R.string.dual_joycons), + JoyconLeft(6, R.string.left_joycon), + JoyconRight(7, R.string.right_joycon), + GameCube(8, R.string.gamecube_controller), + Pokeball(9), + NES(10), + SNES(12), + N64(13), + SegaGenesis(14), + SystemExt(32), + System(33); + + companion object { + fun from(int: Int): NpadStyleIndex = entries.firstOrNull { it.int == int } ?: None + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/PlayerInput.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/PlayerInput.kt new file mode 100644 index 000000000..d35de80c4 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/PlayerInput.kt @@ -0,0 +1,83 @@ +// SPDX-FileCopyrightText: 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.features.input.model + +import androidx.annotation.Keep + +@Keep +data class PlayerInput( + var connected: Boolean, + var buttons: Array<String>, + var analogs: Array<String>, + var motions: Array<String>, + + var vibrationEnabled: Boolean, + var vibrationStrength: Int, + + var bodyColorLeft: Long, + var bodyColorRight: Long, + var buttonColorLeft: Long, + var buttonColorRight: Long, + var profileName: String, + + var useSystemVibrator: Boolean +) { + // It's recommended to use the generated equals() and hashCode() methods + // when using arrays in a data class + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as PlayerInput + + if (connected != other.connected) return false + if (!buttons.contentEquals(other.buttons)) return false + if (!analogs.contentEquals(other.analogs)) return false + if (!motions.contentEquals(other.motions)) return false + if (vibrationEnabled != other.vibrationEnabled) return false + if (vibrationStrength != other.vibrationStrength) return false + if (bodyColorLeft != other.bodyColorLeft) return false + if (bodyColorRight != other.bodyColorRight) return false + if (buttonColorLeft != other.buttonColorLeft) return false + if (buttonColorRight != other.buttonColorRight) return false + if (profileName != other.profileName) return false + return useSystemVibrator == other.useSystemVibrator + } + + override fun hashCode(): Int { + var result = connected.hashCode() + result = 31 * result + buttons.contentHashCode() + result = 31 * result + analogs.contentHashCode() + result = 31 * result + motions.contentHashCode() + result = 31 * result + vibrationEnabled.hashCode() + result = 31 * result + vibrationStrength + result = 31 * result + bodyColorLeft.hashCode() + result = 31 * result + bodyColorRight.hashCode() + result = 31 * result + buttonColorLeft.hashCode() + result = 31 * result + buttonColorRight.hashCode() + result = 31 * result + profileName.hashCode() + result = 31 * result + useSystemVibrator.hashCode() + return result + } + + fun hasMapping(): Boolean { + var hasMapping = false + buttons.forEach { + if (it != "[empty]") { + hasMapping = true + } + } + analogs.forEach { + if (it != "[empty]") { + hasMapping = true + } + } + motions.forEach { + if (it != "[empty]") { + hasMapping = true + } + } + return hasMapping + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt index 862c6c483..4f6b93bd2 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt @@ -4,17 +4,30 @@ package org.yuzu.yuzu_emu.features.settings.model import org.yuzu.yuzu_emu.R +import org.yuzu.yuzu_emu.YuzuApplication object Settings { - enum class MenuTag(val titleId: Int) { + enum class MenuTag(val titleId: Int = 0) { SECTION_ROOT(R.string.advanced_settings), SECTION_SYSTEM(R.string.preferences_system), SECTION_RENDERER(R.string.preferences_graphics), SECTION_AUDIO(R.string.preferences_audio), + SECTION_INPUT(R.string.preferences_controls), + SECTION_INPUT_PLAYER_ONE, + SECTION_INPUT_PLAYER_TWO, + SECTION_INPUT_PLAYER_THREE, + SECTION_INPUT_PLAYER_FOUR, + SECTION_INPUT_PLAYER_FIVE, + SECTION_INPUT_PLAYER_SIX, + SECTION_INPUT_PLAYER_SEVEN, + SECTION_INPUT_PLAYER_EIGHT, SECTION_THEME(R.string.preferences_theme), SECTION_DEBUG(R.string.preferences_debug); } + fun getPlayerString(player: Int): String = + YuzuApplication.appContext.getString(R.string.preferences_player, player) + const val PREF_FIRST_APP_LAUNCH = "FirstApplicationLaunch" const val PREF_MEMORY_WARNING_SHOWN = "MemoryWarningShown" diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/AnalogInputSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/AnalogInputSetting.kt new file mode 100644 index 000000000..a2996725e --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/AnalogInputSetting.kt @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.features.settings.model.view + +import androidx.annotation.StringRes +import org.yuzu.yuzu_emu.features.input.NativeInput +import org.yuzu.yuzu_emu.features.input.model.AnalogDirection +import org.yuzu.yuzu_emu.features.input.model.InputType +import org.yuzu.yuzu_emu.features.input.model.NativeAnalog +import org.yuzu.yuzu_emu.utils.ParamPackage + +class AnalogInputSetting( + override val playerIndex: Int, + val nativeAnalog: NativeAnalog, + val analogDirection: AnalogDirection, + @StringRes titleId: Int = 0, + titleString: String = "" +) : InputSetting(titleId, titleString) { + override val type = TYPE_INPUT + override val inputType = InputType.Stick + + override fun getSelectedValue(): String { + val params = NativeInput.getStickParam(playerIndex, nativeAnalog) + val analog = analogToText(params, analogDirection.param) + return getDisplayString(params, analog) + } + + override fun setSelectedValue(param: ParamPackage) = + NativeInput.setStickParam(playerIndex, nativeAnalog, param) +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/ButtonInputSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/ButtonInputSetting.kt new file mode 100644 index 000000000..786d09a7a --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/ButtonInputSetting.kt @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.features.settings.model.view + +import androidx.annotation.StringRes +import org.yuzu.yuzu_emu.utils.ParamPackage +import org.yuzu.yuzu_emu.features.input.NativeInput +import org.yuzu.yuzu_emu.features.input.model.InputType +import org.yuzu.yuzu_emu.features.input.model.NativeButton + +class ButtonInputSetting( + override val playerIndex: Int, + val nativeButton: NativeButton, + @StringRes titleId: Int = 0, + titleString: String = "" +) : InputSetting(titleId, titleString) { + override val type = TYPE_INPUT + override val inputType = InputType.Button + + override fun getSelectedValue(): String { + val params = NativeInput.getButtonParam(playerIndex, nativeButton) + val button = buttonToText(params) + return getDisplayString(params, button) + } + + override fun setSelectedValue(param: ParamPackage) = + NativeInput.setButtonParam(playerIndex, nativeButton, param) +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/DateTimeSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/DateTimeSetting.kt index 1d81f5f2b..58febff1d 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/DateTimeSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/DateTimeSetting.kt @@ -3,13 +3,16 @@ package org.yuzu.yuzu_emu.features.settings.model.view +import androidx.annotation.StringRes import org.yuzu.yuzu_emu.features.settings.model.AbstractLongSetting class DateTimeSetting( private val longSetting: AbstractLongSetting, - titleId: Int, - descriptionId: Int -) : SettingsItem(longSetting, titleId, descriptionId) { + @StringRes titleId: Int = 0, + titleString: String = "", + @StringRes descriptionId: Int = 0, + descriptionString: String = "" +) : SettingsItem(longSetting, titleId, titleString, descriptionId, descriptionString) { override val type = TYPE_DATETIME_SETTING fun getValue(needsGlobal: Boolean = false): Long = longSetting.getLong(needsGlobal) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/HeaderSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/HeaderSetting.kt index d31ce1c31..8a6a51d5c 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/HeaderSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/HeaderSetting.kt @@ -3,8 +3,11 @@ package org.yuzu.yuzu_emu.features.settings.model.view +import androidx.annotation.StringRes + class HeaderSetting( - titleId: Int -) : SettingsItem(emptySetting, titleId, 0) { + @StringRes titleId: Int = 0, + titleString: String = "" +) : SettingsItem(emptySetting, titleId, titleString, 0, "") { override val type = TYPE_HEADER } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/InputProfileSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/InputProfileSetting.kt new file mode 100644 index 000000000..c46de08c5 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/InputProfileSetting.kt @@ -0,0 +1,32 @@ +// SPDX-FileCopyrightText: 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.features.settings.model.view + +import org.yuzu.yuzu_emu.R +import org.yuzu.yuzu_emu.features.input.NativeInput +import org.yuzu.yuzu_emu.utils.NativeConfig + +class InputProfileSetting(private val playerIndex: Int) : + SettingsItem(emptySetting, R.string.profile, "", 0, "") { + override val type = TYPE_INPUT_PROFILE + + fun getCurrentProfile(): String = + NativeConfig.getInputSettings(true)[playerIndex].profileName + + fun getProfileNames(): Array<String> = NativeInput.getInputProfileNames() + + fun isProfileNameValid(name: String): Boolean = NativeInput.isProfileNameValid(name) + + fun createProfile(name: String): Boolean = NativeInput.createProfile(name, playerIndex) + + fun deleteProfile(name: String): Boolean = NativeInput.deleteProfile(name, playerIndex) + + fun loadProfile(name: String): Boolean { + val result = NativeInput.loadProfile(name, playerIndex) + NativeInput.reloadInputDevices() + return result + } + + fun saveProfile(name: String): Boolean = NativeInput.saveProfile(name, playerIndex) +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/InputSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/InputSetting.kt new file mode 100644 index 000000000..2d118bff3 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/InputSetting.kt @@ -0,0 +1,134 @@ +// SPDX-FileCopyrightText: 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.features.settings.model.view + +import androidx.annotation.StringRes +import org.yuzu.yuzu_emu.R +import org.yuzu.yuzu_emu.YuzuApplication +import org.yuzu.yuzu_emu.features.input.NativeInput +import org.yuzu.yuzu_emu.features.input.model.ButtonName +import org.yuzu.yuzu_emu.features.input.model.InputType +import org.yuzu.yuzu_emu.utils.ParamPackage + +sealed class InputSetting( + @StringRes titleId: Int, + titleString: String +) : SettingsItem(emptySetting, titleId, titleString, 0, "") { + override val type = TYPE_INPUT + abstract val inputType: InputType + abstract val playerIndex: Int + + protected val context get() = YuzuApplication.appContext + + abstract fun getSelectedValue(): String + + abstract fun setSelectedValue(param: ParamPackage) + + protected fun getDisplayString(params: ParamPackage, control: String): String { + val deviceName = params.get("display", "") + deviceName.ifEmpty { + return context.getString(R.string.not_set) + } + return "$deviceName: $control" + } + + private fun getDirectionName(direction: String): String = + when (direction) { + "up" -> context.getString(R.string.up) + "down" -> context.getString(R.string.down) + "left" -> context.getString(R.string.left) + "right" -> context.getString(R.string.right) + else -> direction + } + + protected fun buttonToText(param: ParamPackage): String { + if (!param.has("engine")) { + return context.getString(R.string.not_set) + } + + val toggle = if (param.get("toggle", false)) "~" else "" + val inverted = if (param.get("inverted", false)) "!" else "" + val invert = if (param.get("invert", "+") == "-") "-" else "" + val turbo = if (param.get("turbo", false)) "$" else "" + val commonButtonName = NativeInput.getButtonName(param) + + if (commonButtonName == ButtonName.Invalid) { + return context.getString(R.string.invalid) + } + + if (commonButtonName == ButtonName.Engine) { + return param.get("engine", "") + } + + if (commonButtonName == ButtonName.Value) { + if (param.has("hat")) { + val hat = getDirectionName(param.get("direction", "")) + return context.getString(R.string.qualified_hat, turbo, toggle, inverted, hat) + } + if (param.has("axis")) { + val axis = param.get("axis", "") + return context.getString( + R.string.qualified_button_stick_axis, + toggle, + inverted, + invert, + axis + ) + } + if (param.has("button")) { + val button = param.get("button", "") + return context.getString(R.string.qualified_button, turbo, toggle, inverted, button) + } + } + + return context.getString(R.string.unknown) + } + + protected fun analogToText(param: ParamPackage, direction: String): String { + if (!param.has("engine")) { + return context.getString(R.string.not_set) + } + + if (param.get("engine", "") == "analog_from_button") { + return buttonToText(ParamPackage(param.get(direction, ""))) + } + + if (!param.has("axis_x") || !param.has("axis_y")) { + return context.getString(R.string.unknown) + } + + val xAxis = param.get("axis_x", "") + val yAxis = param.get("axis_y", "") + val xInvert = param.get("invert_x", "+") == "-" + val yInvert = param.get("invert_y", "+") == "-" + + if (direction == "modifier") { + return context.getString(R.string.unused) + } + + when (direction) { + "up" -> { + val yInvertString = if (yInvert) "+" else "-" + return context.getString(R.string.qualified_axis, yAxis, yInvertString) + } + + "down" -> { + val yInvertString = if (yInvert) "-" else "+" + return context.getString(R.string.qualified_axis, yAxis, yInvertString) + } + + "left" -> { + val xInvertString = if (xInvert) "+" else "-" + return context.getString(R.string.qualified_axis, xAxis, xInvertString) + } + + "right" -> { + val xInvertString = if (xInvert) "-" else "+" + return context.getString(R.string.qualified_axis, xAxis, xInvertString) + } + } + + return context.getString(R.string.unknown) + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/IntSingleChoiceSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/IntSingleChoiceSetting.kt new file mode 100644 index 000000000..e024c793a --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/IntSingleChoiceSetting.kt @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.features.settings.model.view + +import androidx.annotation.StringRes +import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting + +class IntSingleChoiceSetting( + private val intSetting: AbstractIntSetting, + @StringRes titleId: Int = 0, + titleString: String = "", + @StringRes descriptionId: Int = 0, + descriptionString: String = "", + val choices: Array<String>, + val values: Array<Int> +) : SettingsItem(intSetting, titleId, titleString, descriptionId, descriptionString) { + override val type = TYPE_INT_SINGLE_CHOICE + + fun getValueAt(index: Int): Int = + if (values.indices.contains(index)) values[index] else -1 + + fun getChoiceAt(index: Int): String = + if (choices.indices.contains(index)) choices[index] else "" + + fun getSelectedValue(needsGlobal: Boolean = false) = intSetting.getInt(needsGlobal) + fun setSelectedValue(value: Int) = intSetting.setInt(value) + + val selectedValueIndex: Int + get() { + for (i in values.indices) { + if (values[i] == getSelectedValue()) { + return i + } + } + return -1 + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/ModifierInputSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/ModifierInputSetting.kt new file mode 100644 index 000000000..a1db3cc87 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/ModifierInputSetting.kt @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.features.settings.model.view + +import androidx.annotation.StringRes +import org.yuzu.yuzu_emu.features.input.NativeInput +import org.yuzu.yuzu_emu.features.input.model.InputType +import org.yuzu.yuzu_emu.features.input.model.NativeAnalog +import org.yuzu.yuzu_emu.utils.ParamPackage + +class ModifierInputSetting( + override val playerIndex: Int, + val nativeAnalog: NativeAnalog, + @StringRes titleId: Int = 0, + titleString: String = "" +) : InputSetting(titleId, titleString) { + override val inputType = InputType.Button + + override fun getSelectedValue(): String { + val analogParam = NativeInput.getStickParam(playerIndex, nativeAnalog) + val modifierParam = ParamPackage(analogParam.get("modifier", "")) + return buttonToText(modifierParam) + } + + override fun setSelectedValue(param: ParamPackage) { + val newParam = NativeInput.getStickParam(playerIndex, nativeAnalog) + newParam.set("modifier", param.serialize()) + NativeInput.setStickParam(playerIndex, nativeAnalog, newParam) + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/RunnableSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/RunnableSetting.kt index 425160024..06f607424 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/RunnableSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/RunnableSetting.kt @@ -4,13 +4,16 @@ package org.yuzu.yuzu_emu.features.settings.model.view import androidx.annotation.DrawableRes +import androidx.annotation.StringRes class RunnableSetting( - titleId: Int, - descriptionId: Int, - val isRuntimeRunnable: Boolean, + @StringRes titleId: Int = 0, + titleString: String = "", + @StringRes descriptionId: Int = 0, + descriptionString: String = "", + val isRunnable: Boolean, @DrawableRes val iconId: Int = 0, val runnable: () -> Unit -) : SettingsItem(emptySetting, titleId, descriptionId) { +) : SettingsItem(emptySetting, titleId, titleString, descriptionId, descriptionString) { override val type = TYPE_RUNNABLE } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt index 21ca97bc1..8f724835e 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt @@ -3,8 +3,12 @@ package org.yuzu.yuzu_emu.features.settings.model.view +import androidx.annotation.StringRes import org.yuzu.yuzu_emu.NativeLibrary import org.yuzu.yuzu_emu.R +import org.yuzu.yuzu_emu.YuzuApplication +import org.yuzu.yuzu_emu.features.input.NativeInput +import org.yuzu.yuzu_emu.features.input.model.NpadStyleIndex import org.yuzu.yuzu_emu.features.settings.model.AbstractBooleanSetting import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting @@ -23,13 +27,34 @@ import org.yuzu.yuzu_emu.utils.NativeConfig */ abstract class SettingsItem( val setting: AbstractSetting, - val nameId: Int, - val descriptionId: Int + @StringRes val titleId: Int, + val titleString: String, + @StringRes val descriptionId: Int, + val descriptionString: String ) { abstract val type: Int + val title: String by lazy { + if (titleId != 0) { + return@lazy YuzuApplication.appContext.getString(titleId) + } + return@lazy titleString + } + + val description: String by lazy { + if (descriptionId != 0) { + return@lazy YuzuApplication.appContext.getString(descriptionId) + } + return@lazy descriptionString + } + val isEditable: Boolean get() { + // Can't change docked mode toggle when using handheld mode + if (setting.key == BooleanSetting.USE_DOCKED_MODE.key) { + return NativeInput.getStyleIndex(0) != NpadStyleIndex.Handheld + } + // Can't edit settings that aren't saveable in per-game config even if they are switchable if (NativeConfig.isPerGameConfigLoaded() && !setting.isSaveable) { return false @@ -59,6 +84,9 @@ abstract class SettingsItem( const val TYPE_STRING_SINGLE_CHOICE = 5 const val TYPE_DATETIME_SETTING = 6 const val TYPE_RUNNABLE = 7 + const val TYPE_INPUT = 8 + const val TYPE_INT_SINGLE_CHOICE = 9 + const val TYPE_INPUT_PROFILE = 10 const val FASTMEM_COMBINED = "fastmem_combined" @@ -80,237 +108,242 @@ abstract class SettingsItem( put( SwitchSetting( BooleanSetting.RENDERER_USE_SPEED_LIMIT, - R.string.frame_limit_enable, - R.string.frame_limit_enable_description + titleId = R.string.frame_limit_enable, + descriptionId = R.string.frame_limit_enable_description ) ) put( SliderSetting( ShortSetting.RENDERER_SPEED_LIMIT, - R.string.frame_limit_slider, - R.string.frame_limit_slider_description, - 1, - 400, - "%" + titleId = R.string.frame_limit_slider, + descriptionId = R.string.frame_limit_slider_description, + min = 1, + max = 400, + units = "%" ) ) put( SingleChoiceSetting( IntSetting.CPU_BACKEND, - R.string.cpu_backend, - 0, - R.array.cpuBackendArm64Names, - R.array.cpuBackendArm64Values + titleId = R.string.cpu_backend, + choicesId = R.array.cpuBackendArm64Names, + valuesId = R.array.cpuBackendArm64Values ) ) put( SingleChoiceSetting( IntSetting.CPU_ACCURACY, - R.string.cpu_accuracy, - 0, - R.array.cpuAccuracyNames, - R.array.cpuAccuracyValues + titleId = R.string.cpu_accuracy, + choicesId = R.array.cpuAccuracyNames, + valuesId = R.array.cpuAccuracyValues ) ) put( SwitchSetting( BooleanSetting.PICTURE_IN_PICTURE, - R.string.picture_in_picture, - R.string.picture_in_picture_description + titleId = R.string.picture_in_picture, + descriptionId = R.string.picture_in_picture_description ) ) + + val dockedModeSetting = object : AbstractBooleanSetting { + override val key = BooleanSetting.USE_DOCKED_MODE.key + + override fun getBoolean(needsGlobal: Boolean): Boolean { + if (NativeInput.getStyleIndex(0) == NpadStyleIndex.Handheld) { + return false + } + return BooleanSetting.USE_DOCKED_MODE.getBoolean(needsGlobal) + } + + override fun setBoolean(value: Boolean) = + BooleanSetting.USE_DOCKED_MODE.setBoolean(value) + + override val defaultValue = BooleanSetting.USE_DOCKED_MODE.defaultValue + + override fun getValueAsString(needsGlobal: Boolean): String = + BooleanSetting.USE_DOCKED_MODE.getValueAsString(needsGlobal) + + override fun reset() = BooleanSetting.USE_DOCKED_MODE.reset() + } put( SwitchSetting( - BooleanSetting.USE_DOCKED_MODE, - R.string.use_docked_mode, - R.string.use_docked_mode_description + dockedModeSetting, + titleId = R.string.use_docked_mode, + descriptionId = R.string.use_docked_mode_description ) ) + put( SingleChoiceSetting( IntSetting.REGION_INDEX, - R.string.emulated_region, - 0, - R.array.regionNames, - R.array.regionValues + titleId = R.string.emulated_region, + choicesId = R.array.regionNames, + valuesId = R.array.regionValues ) ) put( SingleChoiceSetting( IntSetting.LANGUAGE_INDEX, - R.string.emulated_language, - 0, - R.array.languageNames, - R.array.languageValues + titleId = R.string.emulated_language, + choicesId = R.array.languageNames, + valuesId = R.array.languageValues ) ) put( SwitchSetting( BooleanSetting.USE_CUSTOM_RTC, - R.string.use_custom_rtc, - R.string.use_custom_rtc_description + titleId = R.string.use_custom_rtc, + descriptionId = R.string.use_custom_rtc_description ) ) - put(DateTimeSetting(LongSetting.CUSTOM_RTC, R.string.set_custom_rtc, 0)) + put(DateTimeSetting(LongSetting.CUSTOM_RTC, titleId = R.string.set_custom_rtc)) put( SingleChoiceSetting( IntSetting.RENDERER_ACCURACY, - R.string.renderer_accuracy, - 0, - R.array.rendererAccuracyNames, - R.array.rendererAccuracyValues + titleId = R.string.renderer_accuracy, + choicesId = R.array.rendererAccuracyNames, + valuesId = R.array.rendererAccuracyValues ) ) put( SingleChoiceSetting( IntSetting.RENDERER_RESOLUTION, - R.string.renderer_resolution, - 0, - R.array.rendererResolutionNames, - R.array.rendererResolutionValues + titleId = R.string.renderer_resolution, + choicesId = R.array.rendererResolutionNames, + valuesId = R.array.rendererResolutionValues ) ) put( SingleChoiceSetting( IntSetting.RENDERER_VSYNC, - R.string.renderer_vsync, - 0, - R.array.rendererVSyncNames, - R.array.rendererVSyncValues + titleId = R.string.renderer_vsync, + choicesId = R.array.rendererVSyncNames, + valuesId = R.array.rendererVSyncValues ) ) put( SingleChoiceSetting( IntSetting.RENDERER_SCALING_FILTER, - R.string.renderer_scaling_filter, - 0, - R.array.rendererScalingFilterNames, - R.array.rendererScalingFilterValues + titleId = R.string.renderer_scaling_filter, + choicesId = R.array.rendererScalingFilterNames, + valuesId = R.array.rendererScalingFilterValues ) ) put( SliderSetting( IntSetting.FSR_SHARPENING_SLIDER, - R.string.fsr_sharpness, - R.string.fsr_sharpness_description, - 0, - 100, - "%" + titleId = R.string.fsr_sharpness, + descriptionId = R.string.fsr_sharpness_description, + units = "%" ) ) put( SingleChoiceSetting( IntSetting.RENDERER_ANTI_ALIASING, - R.string.renderer_anti_aliasing, - 0, - R.array.rendererAntiAliasingNames, - R.array.rendererAntiAliasingValues + titleId = R.string.renderer_anti_aliasing, + choicesId = R.array.rendererAntiAliasingNames, + valuesId = R.array.rendererAntiAliasingValues ) ) put( SingleChoiceSetting( IntSetting.RENDERER_SCREEN_LAYOUT, - R.string.renderer_screen_layout, - 0, - R.array.rendererScreenLayoutNames, - R.array.rendererScreenLayoutValues + titleId = R.string.renderer_screen_layout, + choicesId = R.array.rendererScreenLayoutNames, + valuesId = R.array.rendererScreenLayoutValues ) ) put( SingleChoiceSetting( IntSetting.RENDERER_ASPECT_RATIO, - R.string.renderer_aspect_ratio, - 0, - R.array.rendererAspectRatioNames, - R.array.rendererAspectRatioValues + titleId = R.string.renderer_aspect_ratio, + choicesId = R.array.rendererAspectRatioNames, + valuesId = R.array.rendererAspectRatioValues ) ) put( SingleChoiceSetting( IntSetting.VERTICAL_ALIGNMENT, - R.string.vertical_alignment, - 0, - R.array.verticalAlignmentEntries, - R.array.verticalAlignmentValues + titleId = R.string.vertical_alignment, + descriptionId = 0, + choicesId = R.array.verticalAlignmentEntries, + valuesId = R.array.verticalAlignmentValues ) ) put( SwitchSetting( BooleanSetting.RENDERER_USE_DISK_SHADER_CACHE, - R.string.use_disk_shader_cache, - R.string.use_disk_shader_cache_description + titleId = R.string.use_disk_shader_cache, + descriptionId = R.string.use_disk_shader_cache_description ) ) put( SwitchSetting( BooleanSetting.RENDERER_FORCE_MAX_CLOCK, - R.string.renderer_force_max_clock, - R.string.renderer_force_max_clock_description + titleId = R.string.renderer_force_max_clock, + descriptionId = R.string.renderer_force_max_clock_description ) ) put( SwitchSetting( BooleanSetting.RENDERER_ASYNCHRONOUS_SHADERS, - R.string.renderer_asynchronous_shaders, - R.string.renderer_asynchronous_shaders_description + titleId = R.string.renderer_asynchronous_shaders, + descriptionId = R.string.renderer_asynchronous_shaders_description ) ) put( SwitchSetting( BooleanSetting.RENDERER_REACTIVE_FLUSHING, - R.string.renderer_reactive_flushing, - R.string.renderer_reactive_flushing_description + titleId = R.string.renderer_reactive_flushing, + descriptionId = R.string.renderer_reactive_flushing_description ) ) put( SingleChoiceSetting( IntSetting.MAX_ANISOTROPY, - R.string.anisotropic_filtering, - R.string.anisotropic_filtering_description, - R.array.anisoEntries, - R.array.anisoValues + titleId = R.string.anisotropic_filtering, + descriptionId = R.string.anisotropic_filtering_description, + choicesId = R.array.anisoEntries, + valuesId = R.array.anisoValues ) ) put( SingleChoiceSetting( IntSetting.AUDIO_OUTPUT_ENGINE, - R.string.audio_output_engine, - 0, - R.array.outputEngineEntries, - R.array.outputEngineValues + titleId = R.string.audio_output_engine, + choicesId = R.array.outputEngineEntries, + valuesId = R.array.outputEngineValues ) ) put( SliderSetting( ByteSetting.AUDIO_VOLUME, - R.string.audio_volume, - R.string.audio_volume_description, - 0, - 100, - "%" + titleId = R.string.audio_volume, + descriptionId = R.string.audio_volume_description, + units = "%" ) ) put( SingleChoiceSetting( IntSetting.RENDERER_BACKEND, - R.string.renderer_api, - 0, - R.array.rendererApiNames, - R.array.rendererApiValues + titleId = R.string.renderer_api, + choicesId = R.array.rendererApiNames, + valuesId = R.array.rendererApiValues ) ) put( SwitchSetting( BooleanSetting.RENDERER_DEBUG, - R.string.renderer_debug, - R.string.renderer_debug_description + titleId = R.string.renderer_debug, + descriptionId = R.string.renderer_debug_description ) ) put( SwitchSetting( BooleanSetting.CPU_DEBUG_MODE, - R.string.cpu_debug_mode, - R.string.cpu_debug_mode_description + titleId = R.string.cpu_debug_mode, + descriptionId = R.string.cpu_debug_mode_description ) ) @@ -346,7 +379,7 @@ abstract class SettingsItem( override fun reset() = setBoolean(defaultValue) } - put(SwitchSetting(fastmem, R.string.fastmem, 0)) + put(SwitchSetting(fastmem, R.string.fastmem)) } } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SingleChoiceSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SingleChoiceSetting.kt index 97a5a9e59..ea5e099ed 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SingleChoiceSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SingleChoiceSetting.kt @@ -3,16 +3,20 @@ package org.yuzu.yuzu_emu.features.settings.model.view +import androidx.annotation.ArrayRes +import androidx.annotation.StringRes import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting class SingleChoiceSetting( setting: AbstractSetting, - titleId: Int, - descriptionId: Int, - val choicesId: Int, - val valuesId: Int -) : SettingsItem(setting, titleId, descriptionId) { + @StringRes titleId: Int = 0, + titleString: String = "", + @StringRes descriptionId: Int = 0, + descriptionString: String = "", + @ArrayRes val choicesId: Int, + @ArrayRes val valuesId: Int +) : SettingsItem(setting, titleId, titleString, descriptionId, descriptionString) { override val type = TYPE_SINGLE_CHOICE fun getSelectedValue(needsGlobal: Boolean = false) = diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SliderSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SliderSetting.kt index b9b709bf7..6a5cdf48b 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SliderSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SliderSetting.kt @@ -3,6 +3,7 @@ package org.yuzu.yuzu_emu.features.settings.model.view +import androidx.annotation.StringRes import org.yuzu.yuzu_emu.features.settings.model.AbstractByteSetting import org.yuzu.yuzu_emu.features.settings.model.AbstractFloatSetting import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting @@ -12,12 +13,14 @@ import kotlin.math.roundToInt class SliderSetting( setting: AbstractSetting, - titleId: Int, - descriptionId: Int, - val min: Int, - val max: Int, - val units: String -) : SettingsItem(setting, titleId, descriptionId) { + @StringRes titleId: Int = 0, + titleString: String = "", + @StringRes descriptionId: Int = 0, + descriptionString: String = "", + val min: Int = 0, + val max: Int = 100, + val units: String = "" +) : SettingsItem(setting, titleId, titleString, descriptionId, descriptionString) { override val type = TYPE_SLIDER fun getSelectedValue(needsGlobal: Boolean = false) = diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/StringSingleChoiceSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/StringSingleChoiceSetting.kt index ba7920f50..5260ff4dc 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/StringSingleChoiceSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/StringSingleChoiceSetting.kt @@ -3,15 +3,18 @@ package org.yuzu.yuzu_emu.features.settings.model.view +import androidx.annotation.StringRes import org.yuzu.yuzu_emu.features.settings.model.AbstractStringSetting class StringSingleChoiceSetting( private val stringSetting: AbstractStringSetting, - titleId: Int, - descriptionId: Int, + @StringRes titleId: Int = 0, + titleString: String = "", + @StringRes descriptionId: Int = 0, + descriptionString: String = "", val choices: Array<String>, val values: Array<String> -) : SettingsItem(stringSetting, titleId, descriptionId) { +) : SettingsItem(stringSetting, titleId, titleString, descriptionId, descriptionString) { override val type = TYPE_STRING_SINGLE_CHOICE fun getValueAt(index: Int): String = @@ -20,7 +23,7 @@ class StringSingleChoiceSetting( fun getSelectedValue(needsGlobal: Boolean = false) = stringSetting.getString(needsGlobal) fun setSelectedValue(value: String) = stringSetting.setString(value) - val selectValueIndex: Int + val selectedValueIndex: Int get() { for (i in values.indices) { if (values[i] == getSelectedValue()) { diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SubmenuSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SubmenuSetting.kt index 94953b18a..c722393dd 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SubmenuSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SubmenuSetting.kt @@ -8,10 +8,12 @@ import androidx.annotation.StringRes import org.yuzu.yuzu_emu.features.settings.model.Settings class SubmenuSetting( - @StringRes titleId: Int, - @StringRes descriptionId: Int, - @DrawableRes val iconId: Int, + @StringRes titleId: Int = 0, + titleString: String = "", + @StringRes descriptionId: Int = 0, + descriptionString: String = "", + @DrawableRes val iconId: Int = 0, val menuKey: Settings.MenuTag -) : SettingsItem(emptySetting, titleId, descriptionId) { +) : SettingsItem(emptySetting, titleId, titleString, descriptionId, descriptionString) { override val type = TYPE_SUBMENU } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SwitchSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SwitchSetting.kt index 44d47dd69..4984bf52e 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SwitchSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SwitchSetting.kt @@ -3,15 +3,18 @@ package org.yuzu.yuzu_emu.features.settings.model.view +import androidx.annotation.StringRes import org.yuzu.yuzu_emu.features.settings.model.AbstractBooleanSetting import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting class SwitchSetting( setting: AbstractSetting, - titleId: Int, - descriptionId: Int -) : SettingsItem(setting, titleId, descriptionId) { + @StringRes titleId: Int = 0, + titleString: String = "", + @StringRes descriptionId: Int = 0, + descriptionString: String = "" +) : SettingsItem(setting, titleId, titleString, descriptionId, descriptionString) { override val type = TYPE_SWITCH fun getIsChecked(needsGlobal: Boolean = false): Boolean { diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/InputDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/InputDialogFragment.kt new file mode 100644 index 000000000..16a1d0504 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/InputDialogFragment.kt @@ -0,0 +1,300 @@ +// SPDX-FileCopyrightText: 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.features.settings.ui + +import android.app.Dialog +import android.graphics.drawable.Animatable2 +import android.graphics.drawable.AnimatedVectorDrawable +import android.graphics.drawable.Drawable +import android.os.Bundle +import android.view.InputDevice +import android.view.KeyEvent +import android.view.LayoutInflater +import android.view.MotionEvent +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.activityViewModels +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import org.yuzu.yuzu_emu.R +import org.yuzu.yuzu_emu.databinding.DialogMappingBinding +import org.yuzu.yuzu_emu.features.input.NativeInput +import org.yuzu.yuzu_emu.features.input.model.NativeAnalog +import org.yuzu.yuzu_emu.features.input.model.NativeButton +import org.yuzu.yuzu_emu.features.settings.model.view.AnalogInputSetting +import org.yuzu.yuzu_emu.features.settings.model.view.ButtonInputSetting +import org.yuzu.yuzu_emu.features.settings.model.view.InputSetting +import org.yuzu.yuzu_emu.features.settings.model.view.ModifierInputSetting +import org.yuzu.yuzu_emu.utils.InputHandler +import org.yuzu.yuzu_emu.utils.ParamPackage + +class InputDialogFragment : DialogFragment() { + private var inputAccepted = false + + private var position: Int = 0 + + private lateinit var inputSetting: InputSetting + + private lateinit var binding: DialogMappingBinding + + private val settingsViewModel: SettingsViewModel by activityViewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + if (settingsViewModel.clickedItem == null) dismiss() + + position = requireArguments().getInt(POSITION) + + InputHandler.updateControllerData() + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + inputSetting = settingsViewModel.clickedItem as InputSetting + binding = DialogMappingBinding.inflate(layoutInflater) + + val builder = MaterialAlertDialogBuilder(requireContext()) + .setPositiveButton(android.R.string.cancel) { _, _ -> + NativeInput.stopMapping() + dismiss() + } + .setView(binding.root) + + val playButtonMapAnimation = { twoDirections: Boolean -> + val stickAnimation: AnimatedVectorDrawable + val buttonAnimation: AnimatedVectorDrawable + binding.imageStickAnimation.apply { + val anim = if (twoDirections) { + R.drawable.stick_two_direction_anim + } else { + R.drawable.stick_one_direction_anim + } + setBackgroundResource(anim) + stickAnimation = background as AnimatedVectorDrawable + } + binding.imageButtonAnimation.apply { + setBackgroundResource(R.drawable.button_anim) + buttonAnimation = background as AnimatedVectorDrawable + } + stickAnimation.registerAnimationCallback(object : Animatable2.AnimationCallback() { + override fun onAnimationEnd(drawable: Drawable?) { + buttonAnimation.start() + } + }) + buttonAnimation.registerAnimationCallback(object : Animatable2.AnimationCallback() { + override fun onAnimationEnd(drawable: Drawable?) { + stickAnimation.start() + } + }) + stickAnimation.start() + } + + when (val setting = inputSetting) { + is AnalogInputSetting -> { + when (setting.nativeAnalog) { + NativeAnalog.LStick -> builder.setTitle( + getString(R.string.map_control, getString(R.string.left_stick)) + ) + + NativeAnalog.RStick -> builder.setTitle( + getString(R.string.map_control, getString(R.string.right_stick)) + ) + } + + builder.setMessage(R.string.stick_map_description) + + playButtonMapAnimation.invoke(true) + } + + is ModifierInputSetting -> { + builder.setTitle(getString(R.string.map_control, setting.title)) + .setMessage(R.string.button_map_description) + playButtonMapAnimation.invoke(false) + } + + is ButtonInputSetting -> { + if (setting.nativeButton == NativeButton.DUp || + setting.nativeButton == NativeButton.DDown || + setting.nativeButton == NativeButton.DLeft || + setting.nativeButton == NativeButton.DRight + ) { + builder.setTitle(getString(R.string.map_dpad_direction, setting.title)) + } else { + builder.setTitle(getString(R.string.map_control, setting.title)) + } + builder.setMessage(R.string.button_map_description) + playButtonMapAnimation.invoke(false) + } + } + + return builder.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) + view.requestFocus() + view.setOnFocusChangeListener { v, hasFocus -> if (!hasFocus) v.requestFocus() } + dialog?.setOnKeyListener { _, _, keyEvent -> onKeyEvent(keyEvent) } + binding.root.setOnGenericMotionListener { _, motionEvent -> onMotionEvent(motionEvent) } + NativeInput.beginMapping(inputSetting.inputType.int) + } + + private fun onKeyEvent(event: KeyEvent): Boolean { + if (event.source and InputDevice.SOURCE_JOYSTICK != InputDevice.SOURCE_JOYSTICK && + event.source and InputDevice.SOURCE_GAMEPAD != InputDevice.SOURCE_GAMEPAD + ) { + return false + } + + val action = when (event.action) { + KeyEvent.ACTION_DOWN -> NativeInput.ButtonState.PRESSED + KeyEvent.ACTION_UP -> NativeInput.ButtonState.RELEASED + else -> return false + } + val controllerData = + InputHandler.androidControllers[event.device.controllerNumber] ?: return false + NativeInput.onGamePadButtonEvent( + controllerData.getGUID(), + controllerData.getPort(), + event.keyCode, + action + ) + onInputReceived(event.device) + return true + } + + private fun onMotionEvent(event: MotionEvent): Boolean { + if (event.source and InputDevice.SOURCE_JOYSTICK != InputDevice.SOURCE_JOYSTICK && + event.source and InputDevice.SOURCE_GAMEPAD != InputDevice.SOURCE_GAMEPAD + ) { + return false + } + + // Temp workaround for DPads that give both axis and button input. The input system can't + // take in a specific axis direction for a binding so you lose half of the directions for a DPad. + + val controllerData = + InputHandler.androidControllers[event.device.controllerNumber] ?: return false + event.device.motionRanges.forEach { + NativeInput.onGamePadAxisEvent( + controllerData.getGUID(), + controllerData.getPort(), + it.axis, + event.getAxisValue(it.axis) + ) + onInputReceived(event.device) + } + return true + } + + private fun onInputReceived(device: InputDevice) { + val params = ParamPackage(NativeInput.getNextInput()) + if (params.has("engine") && isInputAcceptable(params) && !inputAccepted) { + inputAccepted = true + setResult(params, device) + } + } + + private fun setResult(params: ParamPackage, device: InputDevice) { + NativeInput.stopMapping() + params.set("display", "${device.name} ${params.get("port", 0)}") + when (val item = settingsViewModel.clickedItem as InputSetting) { + is ModifierInputSetting, + is ButtonInputSetting -> { + // Invert DPad up and left bindings by default + val tempSetting = inputSetting as? ButtonInputSetting + if (tempSetting != null) { + if (tempSetting.nativeButton == NativeButton.DUp || + tempSetting.nativeButton == NativeButton.DLeft && + params.has("axis") + ) { + params.set("invert", "-") + } + } + + item.setSelectedValue(params) + settingsViewModel.setAdapterItemChanged(position) + } + + is AnalogInputSetting -> { + var analogParam = NativeInput.getStickParam(item.playerIndex, item.nativeAnalog) + analogParam = adjustAnalogParam(params, analogParam, item.analogDirection.param) + + // Invert Y-Axis by default + analogParam.set("invert_y", "-") + + item.setSelectedValue(analogParam) + settingsViewModel.setReloadListAndNotifyDataset(true) + } + } + dismiss() + } + + private fun adjustAnalogParam( + inputParam: ParamPackage, + analogParam: ParamPackage, + buttonName: String + ): ParamPackage { + // The poller returned a complete axis, so set all the buttons + if (inputParam.has("axis_x") && inputParam.has("axis_y")) { + return inputParam + } + + // Check if the current configuration has either no engine or an axis binding. + // Clears out the old binding and adds one with analog_from_button. + if (!analogParam.has("engine") || analogParam.has("axis_x") || analogParam.has("axis_y")) { + analogParam.clear() + analogParam.set("engine", "analog_from_button") + } + analogParam.set(buttonName, inputParam.serialize()) + return analogParam + } + + private fun isInputAcceptable(params: ParamPackage): Boolean { + if (InputHandler.registeredControllers.size == 1) { + return true + } + + if (params.has("motion")) { + return true + } + + val currentDevice = settingsViewModel.getCurrentDeviceParams(params) + if (currentDevice.get("engine", "any") == "any") { + return true + } + + val guidMatch = params.get("guid", "") == currentDevice.get("guid", "") || + params.get("guid", "") == currentDevice.get("guid2", "") + return params.get("engine", "") == currentDevice.get("engine", "") && + guidMatch && + params.get("port", 0) == currentDevice.get("port", 0) + } + + companion object { + const val TAG = "InputDialogFragment" + + const val POSITION = "Position" + + fun newInstance( + inputMappingViewModel: SettingsViewModel, + setting: InputSetting, + position: Int + ): InputDialogFragment { + inputMappingViewModel.clickedItem = setting + val args = Bundle() + args.putInt(POSITION, position) + val fragment = InputDialogFragment() + fragment.arguments = args + return fragment + } + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/InputProfileAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/InputProfileAdapter.kt new file mode 100644 index 000000000..5656e9d8d --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/InputProfileAdapter.kt @@ -0,0 +1,68 @@ +// SPDX-FileCopyrightText: 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.features.settings.ui + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import org.yuzu.yuzu_emu.YuzuApplication +import org.yuzu.yuzu_emu.adapters.AbstractListAdapter +import org.yuzu.yuzu_emu.databinding.ListItemInputProfileBinding +import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder +import org.yuzu.yuzu_emu.R + +class InputProfileAdapter(options: List<ProfileItem>) : + AbstractListAdapter<ProfileItem, AbstractViewHolder<ProfileItem>>(options) { + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): AbstractViewHolder<ProfileItem> { + ListItemInputProfileBinding.inflate(LayoutInflater.from(parent.context), parent, false) + .also { return InputProfileViewHolder(it) } + } + + inner class InputProfileViewHolder(val binding: ListItemInputProfileBinding) : + AbstractViewHolder<ProfileItem>(binding) { + override fun bind(model: ProfileItem) { + when (model) { + is ExistingProfileItem -> { + binding.title.text = model.name + binding.buttonNew.visibility = View.GONE + binding.buttonDelete.visibility = View.VISIBLE + binding.buttonDelete.setOnClickListener { model.deleteProfile.invoke() } + binding.buttonSave.visibility = View.VISIBLE + binding.buttonSave.setOnClickListener { model.saveProfile.invoke() } + binding.buttonLoad.visibility = View.VISIBLE + binding.buttonLoad.setOnClickListener { model.loadProfile.invoke() } + } + + is NewProfileItem -> { + binding.title.text = model.name + binding.buttonNew.visibility = View.VISIBLE + binding.buttonNew.setOnClickListener { model.createNewProfile.invoke() } + binding.buttonSave.visibility = View.GONE + binding.buttonDelete.visibility = View.GONE + binding.buttonLoad.visibility = View.GONE + } + } + } + } +} + +sealed interface ProfileItem { + val name: String +} + +data class NewProfileItem( + val createNewProfile: () -> Unit +) : ProfileItem { + override val name: String = YuzuApplication.appContext.getString(R.string.create_new_profile) +} + +data class ExistingProfileItem( + override val name: String, + val deleteProfile: () -> Unit, + val saveProfile: () -> Unit, + val loadProfile: () -> Unit +) : ProfileItem diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/InputProfileDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/InputProfileDialogFragment.kt new file mode 100644 index 000000000..1bae593ae --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/InputProfileDialogFragment.kt @@ -0,0 +1,148 @@ +// SPDX-FileCopyrightText: 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.features.settings.ui + +import android.app.Dialog +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.activityViewModels +import androidx.recyclerview.widget.LinearLayoutManager +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import org.yuzu.yuzu_emu.R +import org.yuzu.yuzu_emu.databinding.DialogInputProfilesBinding +import org.yuzu.yuzu_emu.features.settings.model.view.InputProfileSetting +import org.yuzu.yuzu_emu.fragments.MessageDialogFragment +import org.yuzu.yuzu_emu.utils.collect + +class InputProfileDialogFragment : DialogFragment() { + private var position = 0 + + private val settingsViewModel: SettingsViewModel by activityViewModels() + + private lateinit var binding: DialogInputProfilesBinding + + private lateinit var setting: InputProfileSetting + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + position = requireArguments().getInt(POSITION) + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + binding = DialogInputProfilesBinding.inflate(layoutInflater) + + setting = settingsViewModel.clickedItem as InputProfileSetting + val options = mutableListOf<ProfileItem>().apply { + add( + NewProfileItem( + createNewProfile = { + NewInputProfileDialogFragment.newInstance( + settingsViewModel, + setting, + position + ).show(parentFragmentManager, NewInputProfileDialogFragment.TAG) + dismiss() + } + ) + ) + + val onActionDismiss = { + settingsViewModel.setReloadListAndNotifyDataset(true) + dismiss() + } + setting.getProfileNames().forEach { + add( + ExistingProfileItem( + it, + deleteProfile = { + settingsViewModel.setShouldShowDeleteProfileDialog(it) + }, + saveProfile = { + if (!setting.saveProfile(it)) { + Toast.makeText( + requireContext(), + R.string.failed_to_save_profile, + Toast.LENGTH_SHORT + ).show() + } + onActionDismiss.invoke() + }, + loadProfile = { + if (!setting.loadProfile(it)) { + Toast.makeText( + requireContext(), + R.string.failed_to_load_profile, + Toast.LENGTH_SHORT + ).show() + } + onActionDismiss.invoke() + } + ) + ) + } + } + binding.listProfiles.apply { + layoutManager = LinearLayoutManager(requireContext()) + adapter = InputProfileAdapter(options) + } + + return MaterialAlertDialogBuilder(requireContext()) + .setView(binding.root) + .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) + + settingsViewModel.shouldShowDeleteProfileDialog.collect(viewLifecycleOwner) { + if (it.isNotEmpty()) { + MessageDialogFragment.newInstance( + activity = requireActivity(), + titleId = R.string.delete_input_profile, + descriptionId = R.string.delete_input_profile_description, + positiveAction = { + setting.deleteProfile(it) + settingsViewModel.setReloadListAndNotifyDataset(true) + }, + negativeAction = {}, + negativeButtonTitleId = android.R.string.cancel + ).show(parentFragmentManager, MessageDialogFragment.TAG) + settingsViewModel.setShouldShowDeleteProfileDialog("") + dismiss() + } + } + } + + companion object { + const val TAG = "InputProfileDialogFragment" + + const val POSITION = "Position" + + fun newInstance( + settingsViewModel: SettingsViewModel, + profileSetting: InputProfileSetting, + position: Int + ): InputProfileDialogFragment { + settingsViewModel.clickedItem = profileSetting + + val args = Bundle() + args.putInt(POSITION, position) + val fragment = InputProfileDialogFragment() + fragment.arguments = args + return fragment + } + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/NewInputProfileDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/NewInputProfileDialogFragment.kt new file mode 100644 index 000000000..6e52bea80 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/NewInputProfileDialogFragment.kt @@ -0,0 +1,79 @@ +// SPDX-FileCopyrightText: 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.features.settings.ui + +import android.app.Dialog +import android.os.Bundle +import android.widget.Toast +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.activityViewModels +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import org.yuzu.yuzu_emu.databinding.DialogEditTextBinding +import org.yuzu.yuzu_emu.features.settings.model.view.InputProfileSetting +import org.yuzu.yuzu_emu.R + +class NewInputProfileDialogFragment : DialogFragment() { + private var position = 0 + + private val settingsViewModel: SettingsViewModel by activityViewModels() + + private lateinit var binding: DialogEditTextBinding + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + position = requireArguments().getInt(POSITION) + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + binding = DialogEditTextBinding.inflate(layoutInflater) + + val setting = settingsViewModel.clickedItem as InputProfileSetting + return MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.enter_profile_name) + .setPositiveButton(android.R.string.ok) { _, _ -> + val profileName = binding.editText.text.toString() + if (!setting.isProfileNameValid(profileName)) { + Toast.makeText( + requireContext(), + R.string.invalid_profile_name, + Toast.LENGTH_SHORT + ).show() + return@setPositiveButton + } + + if (!setting.createProfile(profileName)) { + Toast.makeText( + requireContext(), + R.string.profile_name_already_exists, + Toast.LENGTH_SHORT + ).show() + } else { + settingsViewModel.setAdapterItemChanged(position) + } + } + .setNegativeButton(android.R.string.cancel, null) + .setView(binding.root) + .show() + } + + companion object { + const val TAG = "NewInputProfileDialogFragment" + + const val POSITION = "Position" + + fun newInstance( + settingsViewModel: SettingsViewModel, + profileSetting: InputProfileSetting, + position: Int + ): NewInputProfileDialogFragment { + settingsViewModel.clickedItem = profileSetting + + val args = Bundle() + args.putInt(POSITION, position) + val fragment = NewInputProfileDialogFragment() + fragment.arguments = args + return fragment + } + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt index 6f072241a..455b3b5ff 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt @@ -13,21 +13,16 @@ import androidx.appcompat.app.AppCompatActivity import androidx.core.view.ViewCompat import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle import androidx.navigation.fragment.NavHostFragment import androidx.navigation.navArgs import com.google.android.material.color.MaterialColors -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.launch import org.yuzu.yuzu_emu.NativeLibrary import java.io.IOException import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.databinding.ActivitySettingsBinding +import org.yuzu.yuzu_emu.features.input.NativeInput import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile import org.yuzu.yuzu_emu.fragments.ResetSettingsDialogFragment -import org.yuzu.yuzu_emu.model.SettingsViewModel import org.yuzu.yuzu_emu.utils.* class SettingsActivity : AppCompatActivity() { @@ -70,39 +65,23 @@ class SettingsActivity : AppCompatActivity() { ) } - lifecycleScope.apply { - launch { - repeatOnLifecycle(Lifecycle.State.CREATED) { - settingsViewModel.shouldRecreate.collectLatest { - if (it) { - settingsViewModel.setShouldRecreate(false) - recreate() - } - } - } - } - launch { - repeatOnLifecycle(Lifecycle.State.CREATED) { - settingsViewModel.shouldNavigateBack.collectLatest { - if (it) { - settingsViewModel.setShouldNavigateBack(false) - navigateBack() - } - } - } - } - launch { - repeatOnLifecycle(Lifecycle.State.CREATED) { - settingsViewModel.shouldShowResetSettingsDialog.collectLatest { - if (it) { - settingsViewModel.setShouldShowResetSettingsDialog(false) - ResetSettingsDialogFragment().show( - supportFragmentManager, - ResetSettingsDialogFragment.TAG - ) - } - } - } + settingsViewModel.shouldRecreate.collect( + this, + resetState = { settingsViewModel.setShouldRecreate(false) } + ) { if (it) recreate() } + settingsViewModel.shouldNavigateBack.collect( + this, + resetState = { settingsViewModel.setShouldNavigateBack(false) } + ) { if (it) navigateBack() } + settingsViewModel.shouldShowResetSettingsDialog.collect( + this, + resetState = { settingsViewModel.setShouldShowResetSettingsDialog(false) } + ) { + if (it) { + ResetSettingsDialogFragment().show( + supportFragmentManager, + ResetSettingsDialogFragment.TAG + ) } } @@ -137,6 +116,7 @@ class SettingsActivity : AppCompatActivity() { super.onStop() Log.info("[SettingsActivity] Settings activity stopping. Saving settings to INI...") if (isFinishing) { + NativeInput.reloadInputDevices() NativeLibrary.applySettings() if (args.game == null) { NativeConfig.saveGlobalConfig() diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt index be9b3031b..45c8faa10 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt @@ -8,12 +8,11 @@ import android.icu.util.Calendar import android.icu.util.TimeZone import android.text.format.DateFormat import android.view.LayoutInflater +import android.view.View import android.view.ViewGroup +import android.widget.PopupMenu import androidx.fragment.app.Fragment -import androidx.lifecycle.Lifecycle import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle import androidx.navigation.findNavController import androidx.recyclerview.widget.AsyncDifferConfig import androidx.recyclerview.widget.DiffUtil @@ -21,16 +20,18 @@ import androidx.recyclerview.widget.ListAdapter import com.google.android.material.datepicker.MaterialDatePicker import com.google.android.material.timepicker.MaterialTimePicker import com.google.android.material.timepicker.TimeFormat -import kotlinx.coroutines.launch import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.SettingsNavigationDirections import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding +import org.yuzu.yuzu_emu.databinding.ListItemSettingInputBinding import org.yuzu.yuzu_emu.databinding.ListItemSettingSwitchBinding import org.yuzu.yuzu_emu.databinding.ListItemSettingsHeaderBinding +import org.yuzu.yuzu_emu.features.input.NativeInput +import org.yuzu.yuzu_emu.features.input.model.AnalogDirection +import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting import org.yuzu.yuzu_emu.features.settings.model.view.* import org.yuzu.yuzu_emu.features.settings.ui.viewholder.* -import org.yuzu.yuzu_emu.fragments.SettingsDialogFragment -import org.yuzu.yuzu_emu.model.SettingsViewModel +import org.yuzu.yuzu_emu.utils.ParamPackage class SettingsAdapter( private val fragment: Fragment, @@ -41,19 +42,6 @@ class SettingsAdapter( private val settingsViewModel: SettingsViewModel get() = ViewModelProvider(fragment.requireActivity())[SettingsViewModel::class.java] - init { - fragment.viewLifecycleOwner.lifecycleScope.launch { - fragment.repeatOnLifecycle(Lifecycle.State.STARTED) { - settingsViewModel.adapterItemChanged.collect { - if (it != -1) { - notifyItemChanged(it) - settingsViewModel.setAdapterItemChanged(-1) - } - } - } - } - } - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SettingViewHolder { val inflater = LayoutInflater.from(parent.context) return when (viewType) { @@ -85,8 +73,19 @@ class SettingsAdapter( RunnableViewHolder(ListItemSettingBinding.inflate(inflater), this) } + SettingsItem.TYPE_INPUT -> { + InputViewHolder(ListItemSettingInputBinding.inflate(inflater), this) + } + + SettingsItem.TYPE_INT_SINGLE_CHOICE -> { + SingleChoiceViewHolder(ListItemSettingBinding.inflate(inflater), this) + } + + SettingsItem.TYPE_INPUT_PROFILE -> { + InputProfileViewHolder(ListItemSettingBinding.inflate(inflater), this) + } + else -> { - // TODO: Create an error view since we can't return null now HeaderViewHolder(ListItemSettingsHeaderBinding.inflate(inflater), this) } } @@ -126,6 +125,15 @@ class SettingsAdapter( ).show(fragment.childFragmentManager, SettingsDialogFragment.TAG) } + fun onIntSingleChoiceClick(item: IntSingleChoiceSetting, position: Int) { + SettingsDialogFragment.newInstance( + settingsViewModel, + item, + SettingsItem.TYPE_INT_SINGLE_CHOICE, + position + ).show(fragment.childFragmentManager, SettingsDialogFragment.TAG) + } + fun onDateTimeClick(item: DateTimeSetting, position: Int) { val storedTime = item.getValue() * 1000 @@ -185,6 +193,205 @@ class SettingsAdapter( fragment.view?.findNavController()?.navigate(action) } + fun onInputProfileClick(item: InputProfileSetting, position: Int) { + InputProfileDialogFragment.newInstance( + settingsViewModel, + item, + position + ).show(fragment.childFragmentManager, InputProfileDialogFragment.TAG) + } + + fun onInputClick(item: InputSetting, position: Int) { + InputDialogFragment.newInstance( + settingsViewModel, + item, + position + ).show(fragment.childFragmentManager, InputDialogFragment.TAG) + } + + fun onInputOptionsClick(anchor: View, item: InputSetting, position: Int) { + val popup = PopupMenu(context, anchor) + popup.menuInflater.inflate(R.menu.menu_input_options, popup.menu) + + popup.menu.apply { + val invertAxis = findItem(R.id.invert_axis) + val invertButton = findItem(R.id.invert_button) + val toggleButton = findItem(R.id.toggle_button) + val turboButton = findItem(R.id.turbo_button) + val setThreshold = findItem(R.id.set_threshold) + val toggleAxis = findItem(R.id.toggle_axis) + when (item) { + is AnalogInputSetting -> { + val params = NativeInput.getStickParam(item.playerIndex, item.nativeAnalog) + + invertAxis.isVisible = true + invertAxis.isCheckable = true + invertAxis.isChecked = when (item.analogDirection) { + AnalogDirection.Left, AnalogDirection.Right -> { + params.get("invert_x", "+") == "-" + } + + AnalogDirection.Up, AnalogDirection.Down -> { + params.get("invert_y", "+") == "-" + } + } + invertAxis.setOnMenuItemClickListener { + if (item.analogDirection == AnalogDirection.Left || + item.analogDirection == AnalogDirection.Right + ) { + val invertValue = params.get("invert_x", "+") == "-" + val invertString = if (invertValue) "+" else "-" + params.set("invert_x", invertString) + } else if ( + item.analogDirection == AnalogDirection.Up || + item.analogDirection == AnalogDirection.Down + ) { + val invertValue = params.get("invert_y", "+") == "-" + val invertString = if (invertValue) "+" else "-" + params.set("invert_y", invertString) + } + true + } + + popup.setOnDismissListener { + NativeInput.setStickParam(item.playerIndex, item.nativeAnalog, params) + settingsViewModel.setDatasetChanged(true) + } + } + + is ButtonInputSetting -> { + val params = NativeInput.getButtonParam(item.playerIndex, item.nativeButton) + if (params.has("code") || params.has("button") || params.has("hat")) { + val buttonInvert = params.get("inverted", false) + invertButton.isVisible = true + invertButton.isCheckable = true + invertButton.isChecked = buttonInvert + invertButton.setOnMenuItemClickListener { + params.set("inverted", !buttonInvert) + true + } + + val toggle = params.get("toggle", false) + toggleButton.isVisible = true + toggleButton.isCheckable = true + toggleButton.isChecked = toggle + toggleButton.setOnMenuItemClickListener { + params.set("toggle", !toggle) + true + } + + val turbo = params.get("turbo", false) + turboButton.isVisible = true + turboButton.isCheckable = true + turboButton.isChecked = turbo + turboButton.setOnMenuItemClickListener { + params.set("turbo", !turbo) + true + } + } else if (params.has("axis")) { + val axisInvert = params.get("invert", "+") == "-" + invertAxis.isVisible = true + invertAxis.isCheckable = true + invertAxis.isChecked = axisInvert + invertAxis.setOnMenuItemClickListener { + params.set("invert", if (!axisInvert) "-" else "+") + true + } + + val buttonInvert = params.get("inverted", false) + invertButton.isVisible = true + invertButton.isCheckable = true + invertButton.isChecked = buttonInvert + invertButton.setOnMenuItemClickListener { + params.set("inverted", !buttonInvert) + true + } + + setThreshold.isVisible = true + val thresholdSetting = object : AbstractIntSetting { + override val key = "" + + override fun getInt(needsGlobal: Boolean): Int = + (params.get("threshold", 0.5f) * 100).toInt() + + override fun setInt(value: Int) { + params.set("threshold", value.toFloat() / 100) + NativeInput.setButtonParam( + item.playerIndex, + item.nativeButton, + params + ) + } + + override val defaultValue = 50 + + override fun getValueAsString(needsGlobal: Boolean): String = + getInt(needsGlobal).toString() + + override fun reset() = setInt(defaultValue) + } + setThreshold.setOnMenuItemClickListener { + onSliderClick( + SliderSetting(thresholdSetting, R.string.set_threshold), + position + ) + true + } + + val axisToggle = params.get("toggle", false) + toggleAxis.isVisible = true + toggleAxis.isCheckable = true + toggleAxis.isChecked = axisToggle + toggleAxis.setOnMenuItemClickListener { + params.set("toggle", !axisToggle) + true + } + } + + popup.setOnDismissListener { + NativeInput.setButtonParam(item.playerIndex, item.nativeButton, params) + settingsViewModel.setAdapterItemChanged(position) + } + } + + is ModifierInputSetting -> { + val stickParams = NativeInput.getStickParam(item.playerIndex, item.nativeAnalog) + val modifierParams = ParamPackage(stickParams.get("modifier", "")) + + val invert = modifierParams.get("inverted", false) + invertButton.isVisible = true + invertButton.isCheckable = true + invertButton.isChecked = invert + invertButton.setOnMenuItemClickListener { + modifierParams.set("inverted", !invert) + stickParams.set("modifier", modifierParams.serialize()) + true + } + + val toggle = modifierParams.get("toggle", false) + toggleButton.isVisible = true + toggleButton.isCheckable = true + toggleButton.isChecked = toggle + toggleButton.setOnMenuItemClickListener { + modifierParams.set("toggle", !toggle) + stickParams.set("modifier", modifierParams.serialize()) + true + } + + popup.setOnDismissListener { + NativeInput.setStickParam( + item.playerIndex, + item.nativeAnalog, + stickParams + ) + settingsViewModel.setAdapterItemChanged(position) + } + } + } + } + popup.show() + } + fun onLongClick(item: SettingsItem, position: Int): Boolean { SettingsDialogFragment.newInstance( settingsViewModel, diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsDialogFragment.kt index 60e029f34..a81ff6b1a 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsDialogFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsDialogFragment.kt @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2023 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later -package org.yuzu.yuzu_emu.fragments +package org.yuzu.yuzu_emu.features.settings.ui import android.app.Dialog import android.content.DialogInterface @@ -11,19 +11,21 @@ import android.view.View import android.view.ViewGroup 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 com.google.android.material.slider.Slider -import kotlinx.coroutines.launch import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.databinding.DialogSliderBinding +import org.yuzu.yuzu_emu.features.input.NativeInput +import org.yuzu.yuzu_emu.features.input.model.AnalogDirection +import org.yuzu.yuzu_emu.features.settings.model.view.AnalogInputSetting +import org.yuzu.yuzu_emu.features.settings.model.view.ButtonInputSetting +import org.yuzu.yuzu_emu.features.settings.model.view.IntSingleChoiceSetting import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem import org.yuzu.yuzu_emu.features.settings.model.view.SingleChoiceSetting import org.yuzu.yuzu_emu.features.settings.model.view.SliderSetting import org.yuzu.yuzu_emu.features.settings.model.view.StringSingleChoiceSetting -import org.yuzu.yuzu_emu.model.SettingsViewModel +import org.yuzu.yuzu_emu.utils.ParamPackage +import org.yuzu.yuzu_emu.utils.collect class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener { private var type = 0 @@ -50,8 +52,49 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener MaterialAlertDialogBuilder(requireContext()) .setMessage(R.string.reset_setting_confirmation) .setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int -> - settingsViewModel.clickedItem!!.setting.reset() - settingsViewModel.setAdapterItemChanged(position) + when (val item = settingsViewModel.clickedItem) { + is AnalogInputSetting -> { + val stickParam = NativeInput.getStickParam( + item.playerIndex, + item.nativeAnalog + ) + if (stickParam.get("engine", "") == "analog_from_button") { + when (item.analogDirection) { + AnalogDirection.Up -> stickParam.erase("up") + AnalogDirection.Down -> stickParam.erase("down") + AnalogDirection.Left -> stickParam.erase("left") + AnalogDirection.Right -> stickParam.erase("right") + } + NativeInput.setStickParam( + item.playerIndex, + item.nativeAnalog, + stickParam + ) + settingsViewModel.setAdapterItemChanged(position) + } else { + NativeInput.setStickParam( + item.playerIndex, + item.nativeAnalog, + ParamPackage() + ) + settingsViewModel.setDatasetChanged(true) + } + } + + is ButtonInputSetting -> { + NativeInput.setButtonParam( + item.playerIndex, + item.nativeButton, + ParamPackage() + ) + settingsViewModel.setAdapterItemChanged(position) + } + + else -> { + settingsViewModel.clickedItem!!.setting.reset() + settingsViewModel.setAdapterItemChanged(position) + } + } } .setNegativeButton(android.R.string.cancel, null) .create() @@ -61,7 +104,7 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener val item = settingsViewModel.clickedItem as SingleChoiceSetting val value = getSelectionForSingleChoiceValue(item) MaterialAlertDialogBuilder(requireContext()) - .setTitle(item.nameId) + .setTitle(item.title) .setSingleChoiceItems(item.choicesId, value, this) .create() } @@ -81,7 +124,7 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener } MaterialAlertDialogBuilder(requireContext()) - .setTitle(item.nameId) + .setTitle(item.title) .setView(sliderBinding.root) .setPositiveButton(android.R.string.ok, this) .setNegativeButton(android.R.string.cancel, defaultCancelListener) @@ -91,8 +134,16 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener SettingsItem.TYPE_STRING_SINGLE_CHOICE -> { val item = settingsViewModel.clickedItem as StringSingleChoiceSetting MaterialAlertDialogBuilder(requireContext()) - .setTitle(item.nameId) - .setSingleChoiceItems(item.choices, item.selectValueIndex, this) + .setTitle(item.title) + .setSingleChoiceItems(item.choices, item.selectedValueIndex, this) + .create() + } + + SettingsItem.TYPE_INT_SINGLE_CHOICE -> { + val item = settingsViewModel.clickedItem as IntSingleChoiceSetting + MaterialAlertDialogBuilder(requireContext()) + .setTitle(item.title) + .setSingleChoiceItems(item.choices, item.selectedValueIndex, this) .create() } @@ -115,17 +166,11 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener super.onViewCreated(view, savedInstanceState) when (type) { SettingsItem.TYPE_SLIDER -> { - viewLifecycleOwner.lifecycleScope.launch { - repeatOnLifecycle(Lifecycle.State.CREATED) { - settingsViewModel.sliderTextValue.collect { - sliderBinding.textValue.text = it - } - } - repeatOnLifecycle(Lifecycle.State.CREATED) { - settingsViewModel.sliderProgress.collect { - sliderBinding.slider.value = it.toFloat() - } - } + settingsViewModel.sliderTextValue.collect(viewLifecycleOwner) { + sliderBinding.textValue.text = it + } + settingsViewModel.sliderProgress.collect(viewLifecycleOwner) { + sliderBinding.slider.value = it.toFloat() } } } @@ -145,6 +190,12 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener scSetting.setSelectedValue(value) } + is IntSingleChoiceSetting -> { + val scSetting = settingsViewModel.clickedItem as IntSingleChoiceSetting + val value = scSetting.getValueAt(which) + scSetting.setSelectedValue(value) + } + is SliderSetting -> { val sliderSetting = settingsViewModel.clickedItem as SliderSetting sliderSetting.setSelectedValue(settingsViewModel.sliderProgress.value) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt index 6f6e7be10..ec16f16c4 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt @@ -13,20 +13,17 @@ 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.navigation.fragment.navArgs import androidx.recyclerview.widget.LinearLayoutManager import com.google.android.material.transition.MaterialSharedAxis -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.launch import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.databinding.FragmentSettingsBinding +import org.yuzu.yuzu_emu.features.input.NativeInput import org.yuzu.yuzu_emu.features.settings.model.Settings -import org.yuzu.yuzu_emu.model.SettingsViewModel +import org.yuzu.yuzu_emu.fragments.MessageDialogFragment import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins +import org.yuzu.yuzu_emu.utils.collect class SettingsFragment : Fragment() { private lateinit var presenter: SettingsFragmentPresenter @@ -45,6 +42,12 @@ class SettingsFragment : Fragment() { returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false) reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false) exitTransition = MaterialSharedAxis(MaterialSharedAxis.X, true) + + val playerIndex = getPlayerIndex() + if (playerIndex != -1) { + NativeInput.loadInputProfiles() + NativeInput.reloadInputDevices() + } } override fun onCreateView( @@ -56,9 +59,9 @@ class SettingsFragment : Fragment() { return binding.root } - // This is using the correct scope, lint is just acting up - @SuppressLint("UnsafeRepeatOnLifecycleDetector") + @SuppressLint("NotifyDataSetChanged") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) settingsAdapter = SettingsAdapter(this, requireContext()) presenter = SettingsFragmentPresenter( settingsViewModel, @@ -71,7 +74,17 @@ class SettingsFragment : Fragment() { ) { args.game!!.title } else { - getString(args.menuTag.titleId) + when (args.menuTag) { + Settings.MenuTag.SECTION_INPUT_PLAYER_ONE -> Settings.getPlayerString(1) + Settings.MenuTag.SECTION_INPUT_PLAYER_TWO -> Settings.getPlayerString(2) + Settings.MenuTag.SECTION_INPUT_PLAYER_THREE -> Settings.getPlayerString(3) + Settings.MenuTag.SECTION_INPUT_PLAYER_FOUR -> Settings.getPlayerString(4) + Settings.MenuTag.SECTION_INPUT_PLAYER_FIVE -> Settings.getPlayerString(5) + Settings.MenuTag.SECTION_INPUT_PLAYER_SIX -> Settings.getPlayerString(6) + Settings.MenuTag.SECTION_INPUT_PLAYER_SEVEN -> Settings.getPlayerString(7) + Settings.MenuTag.SECTION_INPUT_PLAYER_EIGHT -> Settings.getPlayerString(8) + else -> getString(args.menuTag.titleId) + } } binding.listSettings.apply { adapter = settingsAdapter @@ -82,16 +95,37 @@ class SettingsFragment : Fragment() { settingsViewModel.setShouldNavigateBack(true) } - viewLifecycleOwner.lifecycleScope.apply { - launch { - repeatOnLifecycle(Lifecycle.State.CREATED) { - settingsViewModel.shouldReloadSettingsList.collectLatest { - if (it) { - settingsViewModel.setShouldReloadSettingsList(false) - presenter.loadSettingsList() - } - } - } + settingsViewModel.shouldReloadSettingsList.collect( + viewLifecycleOwner, + resetState = { settingsViewModel.setShouldReloadSettingsList(false) } + ) { if (it) presenter.loadSettingsList() } + settingsViewModel.adapterItemChanged.collect( + viewLifecycleOwner, + resetState = { settingsViewModel.setAdapterItemChanged(-1) } + ) { if (it != -1) settingsAdapter?.notifyItemChanged(it) } + settingsViewModel.datasetChanged.collect( + viewLifecycleOwner, + resetState = { settingsViewModel.setDatasetChanged(false) } + ) { if (it) settingsAdapter?.notifyDataSetChanged() } + settingsViewModel.reloadListAndNotifyDataset.collect( + viewLifecycleOwner, + resetState = { settingsViewModel.setReloadListAndNotifyDataset(false) } + ) { if (it) presenter.loadSettingsList(true) } + settingsViewModel.shouldShowResetInputDialog.collect( + viewLifecycleOwner, + resetState = { settingsViewModel.setShouldShowResetInputDialog(false) } + ) { + if (it) { + MessageDialogFragment.newInstance( + activity = requireActivity(), + titleId = R.string.reset_mapping, + descriptionId = R.string.reset_mapping_description, + positiveAction = { + NativeInput.resetControllerMappings(getPlayerIndex()) + settingsViewModel.setReloadListAndNotifyDataset(true) + }, + negativeAction = {} + ).show(parentFragmentManager, MessageDialogFragment.TAG) } } @@ -115,6 +149,19 @@ class SettingsFragment : Fragment() { setInsets() } + private fun getPlayerIndex(): Int = + when (args.menuTag) { + Settings.MenuTag.SECTION_INPUT_PLAYER_ONE -> 0 + Settings.MenuTag.SECTION_INPUT_PLAYER_TWO -> 1 + Settings.MenuTag.SECTION_INPUT_PLAYER_THREE -> 2 + Settings.MenuTag.SECTION_INPUT_PLAYER_FOUR -> 3 + Settings.MenuTag.SECTION_INPUT_PLAYER_FIVE -> 4 + Settings.MenuTag.SECTION_INPUT_PLAYER_SIX -> 5 + Settings.MenuTag.SECTION_INPUT_PLAYER_SEVEN -> 6 + Settings.MenuTag.SECTION_INPUT_PLAYER_EIGHT -> 7 + else -> -1 + } + private fun setInsets() { ViewCompat.setOnApplyWindowInsetsListener( binding.root diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt index db1a58147..e491c29a2 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt @@ -3,11 +3,17 @@ package org.yuzu.yuzu_emu.features.settings.ui +import android.annotation.SuppressLint import android.os.Build import android.widget.Toast import org.yuzu.yuzu_emu.NativeLibrary import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.YuzuApplication +import org.yuzu.yuzu_emu.features.input.NativeInput +import org.yuzu.yuzu_emu.features.input.model.AnalogDirection +import org.yuzu.yuzu_emu.features.input.model.NativeAnalog +import org.yuzu.yuzu_emu.features.input.model.NativeButton +import org.yuzu.yuzu_emu.features.input.model.NpadStyleIndex import org.yuzu.yuzu_emu.features.settings.model.AbstractBooleanSetting import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting @@ -15,18 +21,21 @@ import org.yuzu.yuzu_emu.features.settings.model.ByteSetting import org.yuzu.yuzu_emu.features.settings.model.IntSetting import org.yuzu.yuzu_emu.features.settings.model.LongSetting import org.yuzu.yuzu_emu.features.settings.model.Settings +import org.yuzu.yuzu_emu.features.settings.model.Settings.MenuTag import org.yuzu.yuzu_emu.features.settings.model.ShortSetting import org.yuzu.yuzu_emu.features.settings.model.view.* -import org.yuzu.yuzu_emu.model.SettingsViewModel +import org.yuzu.yuzu_emu.utils.InputHandler import org.yuzu.yuzu_emu.utils.NativeConfig class SettingsFragmentPresenter( private val settingsViewModel: SettingsViewModel, private val adapter: SettingsAdapter, - private var menuTag: Settings.MenuTag + private var menuTag: MenuTag ) { private var settingsList = ArrayList<SettingsItem>() + private val context get() = YuzuApplication.appContext + // Extension for altering settings list based on each setting's properties fun ArrayList<SettingsItem>.add(key: String) { val item = SettingsItem.settingsItems[key]!! @@ -53,73 +62,90 @@ class SettingsFragmentPresenter( add(item) } + // Allows you to show/hide abstract settings based on the paired setting key + fun ArrayList<SettingsItem>.addAbstract(item: SettingsItem) { + val pairedSettingKey = item.setting.pairedSettingKey + if (pairedSettingKey.isNotEmpty()) { + val pairedSettingsItem = + this.firstOrNull { it.setting.key == pairedSettingKey } ?: return + val pairedSetting = pairedSettingsItem.setting as AbstractBooleanSetting + if (!pairedSetting.getBoolean(!NativeConfig.isPerGameConfigLoaded())) return + } + add(item) + } + fun onViewCreated() { loadSettingsList() } - fun loadSettingsList() { + @SuppressLint("NotifyDataSetChanged") + fun loadSettingsList(notifyDataSetChanged: Boolean = false) { val sl = ArrayList<SettingsItem>() when (menuTag) { - Settings.MenuTag.SECTION_ROOT -> addConfigSettings(sl) - Settings.MenuTag.SECTION_SYSTEM -> addSystemSettings(sl) - Settings.MenuTag.SECTION_RENDERER -> addGraphicsSettings(sl) - Settings.MenuTag.SECTION_AUDIO -> addAudioSettings(sl) - Settings.MenuTag.SECTION_THEME -> addThemeSettings(sl) - Settings.MenuTag.SECTION_DEBUG -> addDebugSettings(sl) - else -> { - val context = YuzuApplication.appContext - Toast.makeText( - context, - context.getString(R.string.unimplemented_menu), - Toast.LENGTH_SHORT - ).show() - return - } + MenuTag.SECTION_ROOT -> addConfigSettings(sl) + MenuTag.SECTION_SYSTEM -> addSystemSettings(sl) + MenuTag.SECTION_RENDERER -> addGraphicsSettings(sl) + MenuTag.SECTION_AUDIO -> addAudioSettings(sl) + MenuTag.SECTION_INPUT -> addInputSettings(sl) + MenuTag.SECTION_INPUT_PLAYER_ONE -> addInputPlayer(sl, 0) + MenuTag.SECTION_INPUT_PLAYER_TWO -> addInputPlayer(sl, 1) + MenuTag.SECTION_INPUT_PLAYER_THREE -> addInputPlayer(sl, 2) + MenuTag.SECTION_INPUT_PLAYER_FOUR -> addInputPlayer(sl, 3) + MenuTag.SECTION_INPUT_PLAYER_FIVE -> addInputPlayer(sl, 4) + MenuTag.SECTION_INPUT_PLAYER_SIX -> addInputPlayer(sl, 5) + MenuTag.SECTION_INPUT_PLAYER_SEVEN -> addInputPlayer(sl, 6) + MenuTag.SECTION_INPUT_PLAYER_EIGHT -> addInputPlayer(sl, 7) + MenuTag.SECTION_THEME -> addThemeSettings(sl) + MenuTag.SECTION_DEBUG -> addDebugSettings(sl) } settingsList = sl - adapter.submitList(settingsList) + adapter.submitList(settingsList) { + if (notifyDataSetChanged) { + adapter.notifyDataSetChanged() + } + } } private fun addConfigSettings(sl: ArrayList<SettingsItem>) { sl.apply { add( SubmenuSetting( - R.string.preferences_system, - R.string.preferences_system_description, - R.drawable.ic_system_settings, - Settings.MenuTag.SECTION_SYSTEM + titleId = R.string.preferences_system, + descriptionId = R.string.preferences_system_description, + iconId = R.drawable.ic_system_settings, + menuKey = MenuTag.SECTION_SYSTEM ) ) add( SubmenuSetting( - R.string.preferences_graphics, - R.string.preferences_graphics_description, - R.drawable.ic_graphics, - Settings.MenuTag.SECTION_RENDERER + titleId = R.string.preferences_graphics, + descriptionId = R.string.preferences_graphics_description, + iconId = R.drawable.ic_graphics, + menuKey = MenuTag.SECTION_RENDERER ) ) add( SubmenuSetting( - R.string.preferences_audio, - R.string.preferences_audio_description, - R.drawable.ic_audio, - Settings.MenuTag.SECTION_AUDIO + titleId = R.string.preferences_audio, + descriptionId = R.string.preferences_audio_description, + iconId = R.drawable.ic_audio, + menuKey = MenuTag.SECTION_AUDIO ) ) add( SubmenuSetting( - R.string.preferences_debug, - R.string.preferences_debug_description, - R.drawable.ic_code, - Settings.MenuTag.SECTION_DEBUG + titleId = R.string.preferences_debug, + descriptionId = R.string.preferences_debug_description, + iconId = R.drawable.ic_code, + menuKey = MenuTag.SECTION_DEBUG ) ) add( RunnableSetting( - R.string.reset_to_default, - R.string.reset_to_default_description, - false, - R.drawable.ic_restore + titleId = R.string.reset_to_default, + descriptionId = R.string.reset_to_default_description, + isRunnable = !NativeLibrary.isRunning(), + iconId = R.drawable.ic_restore ) { settingsViewModel.setShouldShowResetSettingsDialog(true) } ) } @@ -164,6 +190,671 @@ class SettingsFragmentPresenter( } } + private fun addInputSettings(sl: ArrayList<SettingsItem>) { + settingsViewModel.currentDevice = 0 + + if (NativeConfig.isPerGameConfigLoaded()) { + NativeInput.loadInputProfiles() + val profiles = NativeInput.getInputProfileNames().toMutableList() + profiles.add(0, "") + val prettyProfiles = profiles.toTypedArray() + prettyProfiles[0] = + context.getString(R.string.use_global_input_configuration) + sl.apply { + for (i in 0 until 8) { + add( + IntSingleChoiceSetting( + getPerGameProfileSetting(profiles, i), + titleString = getPlayerProfileString(i + 1), + choices = prettyProfiles, + values = IntArray(profiles.size) { it }.toTypedArray() + ) + ) + } + } + return + } + + val getConnectedIcon: (Int) -> Int = { playerIndex: Int -> + if (NativeInput.getIsConnected(playerIndex)) { + R.drawable.ic_controller + } else { + R.drawable.ic_controller_disconnected + } + } + + val inputSettings = NativeConfig.getInputSettings(true) + sl.apply { + add( + SubmenuSetting( + titleString = Settings.getPlayerString(1), + descriptionString = inputSettings[0].profileName, + menuKey = MenuTag.SECTION_INPUT_PLAYER_ONE, + iconId = getConnectedIcon(0) + ) + ) + add( + SubmenuSetting( + titleString = Settings.getPlayerString(2), + descriptionString = inputSettings[1].profileName, + menuKey = MenuTag.SECTION_INPUT_PLAYER_TWO, + iconId = getConnectedIcon(1) + ) + ) + add( + SubmenuSetting( + titleString = Settings.getPlayerString(3), + descriptionString = inputSettings[2].profileName, + menuKey = MenuTag.SECTION_INPUT_PLAYER_THREE, + iconId = getConnectedIcon(2) + ) + ) + add( + SubmenuSetting( + titleString = Settings.getPlayerString(4), + descriptionString = inputSettings[3].profileName, + menuKey = MenuTag.SECTION_INPUT_PLAYER_FOUR, + iconId = getConnectedIcon(3) + ) + ) + add( + SubmenuSetting( + titleString = Settings.getPlayerString(5), + descriptionString = inputSettings[4].profileName, + menuKey = MenuTag.SECTION_INPUT_PLAYER_FIVE, + iconId = getConnectedIcon(4) + ) + ) + add( + SubmenuSetting( + titleString = Settings.getPlayerString(6), + descriptionString = inputSettings[5].profileName, + menuKey = MenuTag.SECTION_INPUT_PLAYER_SIX, + iconId = getConnectedIcon(5) + ) + ) + add( + SubmenuSetting( + titleString = Settings.getPlayerString(7), + descriptionString = inputSettings[6].profileName, + menuKey = MenuTag.SECTION_INPUT_PLAYER_SEVEN, + iconId = getConnectedIcon(6) + ) + ) + add( + SubmenuSetting( + titleString = Settings.getPlayerString(8), + descriptionString = inputSettings[7].profileName, + menuKey = MenuTag.SECTION_INPUT_PLAYER_EIGHT, + iconId = getConnectedIcon(7) + ) + ) + } + } + + private fun getPlayerProfileString(player: Int): String = + context.getString(R.string.player_num_profile, player) + + private fun getPerGameProfileSetting( + profiles: List<String>, + playerIndex: Int + ): AbstractIntSetting { + return object : AbstractIntSetting { + private val players + get() = NativeConfig.getInputSettings(false) + + override val key = "" + + override fun getInt(needsGlobal: Boolean): Int { + val currentProfile = players[playerIndex].profileName + profiles.forEachIndexed { i, profile -> + if (profile == currentProfile) { + return i + } + } + return 0 + } + + override fun setInt(value: Int) { + NativeInput.loadPerGameConfiguration(playerIndex, value, profiles[value]) + NativeInput.connectControllers(playerIndex) + NativeConfig.saveControlPlayerValues() + } + + override val defaultValue = 0 + + override fun getValueAsString(needsGlobal: Boolean): String = getInt().toString() + + override fun reset() = setInt(defaultValue) + + override var global = true + + override val isRuntimeModifiable = true + + override val isSaveable = true + } + } + + private fun addInputPlayer(sl: ArrayList<SettingsItem>, playerIndex: Int) { + sl.apply { + val connectedSetting = object : AbstractBooleanSetting { + override val key = "connected" + + override fun getBoolean(needsGlobal: Boolean): Boolean = + NativeInput.getIsConnected(playerIndex) + + override fun setBoolean(value: Boolean) = + NativeInput.connectControllers(playerIndex, value) + + override val defaultValue = playerIndex == 0 + + override fun getValueAsString(needsGlobal: Boolean): String = + getBoolean(needsGlobal).toString() + + override fun reset() = setBoolean(defaultValue) + } + add(SwitchSetting(connectedSetting, R.string.connected)) + + val styleTags = NativeInput.getSupportedStyleTags(playerIndex) + val npadType = object : AbstractIntSetting { + override val key = "npad_type" + override fun getInt(needsGlobal: Boolean): Int { + val styleIndex = NativeInput.getStyleIndex(playerIndex) + return styleTags.indexOfFirst { it == styleIndex } + } + + override fun setInt(value: Int) { + NativeInput.setStyleIndex(playerIndex, styleTags[value]) + settingsViewModel.setReloadListAndNotifyDataset(true) + } + + override val defaultValue = NpadStyleIndex.Fullkey.int + override fun getValueAsString(needsGlobal: Boolean): String = getInt().toString() + override fun reset() = setInt(defaultValue) + override val pairedSettingKey: String = "connected" + } + addAbstract( + IntSingleChoiceSetting( + npadType, + titleId = R.string.controller_type, + choices = styleTags.map { context.getString(it.nameId) } + .toTypedArray(), + values = IntArray(styleTags.size) { it }.toTypedArray() + ) + ) + + InputHandler.updateControllerData() + + val autoMappingSetting = object : AbstractIntSetting { + override val key = "auto_mapping_device" + + override fun getInt(needsGlobal: Boolean): Int = -1 + + override fun setInt(value: Int) { + val registeredController = InputHandler.registeredControllers[value + 1] + val displayName = registeredController.get( + "display", + context.getString(R.string.unknown) + ) + NativeInput.updateMappingsWithDefault( + playerIndex, + registeredController, + displayName + ) + Toast.makeText( + context, + context.getString(R.string.attempted_auto_map, displayName), + Toast.LENGTH_SHORT + ).show() + settingsViewModel.setReloadListAndNotifyDataset(true) + } + + override val defaultValue = -1 + + override fun getValueAsString(needsGlobal: Boolean) = getInt().toString() + + override fun reset() = setInt(defaultValue) + + override val isRuntimeModifiable: Boolean = true + } + + val unknownString = context.getString(R.string.unknown) + val prettyAutoMappingControllerList = InputHandler.registeredControllers.mapNotNull { + val port = it.get("port", -1) + return@mapNotNull if (port == 100 || port == -1) { + null + } else { + it.get("display", unknownString) + } + }.toTypedArray() + add( + IntSingleChoiceSetting( + autoMappingSetting, + titleId = R.string.auto_map, + descriptionId = R.string.auto_map_description, + choices = prettyAutoMappingControllerList, + values = IntArray(prettyAutoMappingControllerList.size) { it }.toTypedArray() + ) + ) + + val mappingFilterSetting = object : AbstractIntSetting { + override val key = "mapping_filter" + + override fun getInt(needsGlobal: Boolean): Int = settingsViewModel.currentDevice + + override fun setInt(value: Int) { + settingsViewModel.currentDevice = value + } + + override val defaultValue = 0 + + override fun getValueAsString(needsGlobal: Boolean) = getInt().toString() + + override fun reset() = setInt(defaultValue) + + override val isRuntimeModifiable: Boolean = true + } + + val prettyControllerList = InputHandler.registeredControllers.mapNotNull { + return@mapNotNull if (it.get("port", 0) == 100) { + null + } else { + it.get("display", unknownString) + } + }.toTypedArray() + add( + IntSingleChoiceSetting( + mappingFilterSetting, + titleId = R.string.input_mapping_filter, + descriptionId = R.string.input_mapping_filter_description, + choices = prettyControllerList, + values = IntArray(prettyControllerList.size) { it }.toTypedArray() + ) + ) + + add(InputProfileSetting(playerIndex)) + add( + RunnableSetting(titleId = R.string.reset_to_default, isRunnable = true) { + settingsViewModel.setShouldShowResetInputDialog(true) + } + ) + + val styleIndex = NativeInput.getStyleIndex(playerIndex) + + // Buttons + when (styleIndex) { + NpadStyleIndex.Fullkey, + NpadStyleIndex.Handheld, + NpadStyleIndex.JoyconDual -> { + add(HeaderSetting(R.string.buttons)) + add(ButtonInputSetting(playerIndex, NativeButton.A, R.string.button_a)) + add(ButtonInputSetting(playerIndex, NativeButton.B, R.string.button_b)) + add(ButtonInputSetting(playerIndex, NativeButton.X, R.string.button_x)) + add(ButtonInputSetting(playerIndex, NativeButton.Y, R.string.button_y)) + add(ButtonInputSetting(playerIndex, NativeButton.Plus, R.string.button_plus)) + add(ButtonInputSetting(playerIndex, NativeButton.Minus, R.string.button_minus)) + add(ButtonInputSetting(playerIndex, NativeButton.Home, R.string.button_home)) + add( + ButtonInputSetting( + playerIndex, + NativeButton.Capture, + R.string.button_capture + ) + ) + } + + NpadStyleIndex.JoyconLeft -> { + add(HeaderSetting(R.string.buttons)) + add(ButtonInputSetting(playerIndex, NativeButton.Minus, R.string.button_minus)) + add( + ButtonInputSetting( + playerIndex, + NativeButton.Capture, + R.string.button_capture + ) + ) + } + + NpadStyleIndex.JoyconRight -> { + add(HeaderSetting(R.string.buttons)) + add(ButtonInputSetting(playerIndex, NativeButton.A, R.string.button_a)) + add(ButtonInputSetting(playerIndex, NativeButton.B, R.string.button_b)) + add(ButtonInputSetting(playerIndex, NativeButton.X, R.string.button_x)) + add(ButtonInputSetting(playerIndex, NativeButton.Y, R.string.button_y)) + add(ButtonInputSetting(playerIndex, NativeButton.Plus, R.string.button_plus)) + add(ButtonInputSetting(playerIndex, NativeButton.Home, R.string.button_home)) + } + + NpadStyleIndex.GameCube -> { + add(HeaderSetting(R.string.buttons)) + add(ButtonInputSetting(playerIndex, NativeButton.A, R.string.button_a)) + add(ButtonInputSetting(playerIndex, NativeButton.B, R.string.button_b)) + add(ButtonInputSetting(playerIndex, NativeButton.X, R.string.button_x)) + add(ButtonInputSetting(playerIndex, NativeButton.Y, R.string.button_y)) + add(ButtonInputSetting(playerIndex, NativeButton.Plus, R.string.start_pause)) + } + + else -> { + // No-op + } + } + + when (styleIndex) { + NpadStyleIndex.Fullkey, + NpadStyleIndex.Handheld, + NpadStyleIndex.JoyconDual, + NpadStyleIndex.JoyconLeft -> { + add(HeaderSetting(R.string.dpad)) + add(ButtonInputSetting(playerIndex, NativeButton.DUp, R.string.up)) + add(ButtonInputSetting(playerIndex, NativeButton.DDown, R.string.down)) + add(ButtonInputSetting(playerIndex, NativeButton.DLeft, R.string.left)) + add(ButtonInputSetting(playerIndex, NativeButton.DRight, R.string.right)) + } + + else -> { + // No-op + } + } + + // Left stick + when (styleIndex) { + NpadStyleIndex.Fullkey, + NpadStyleIndex.Handheld, + NpadStyleIndex.JoyconDual, + NpadStyleIndex.JoyconLeft -> { + add(HeaderSetting(R.string.left_stick)) + addAll(getStickDirections(playerIndex, NativeAnalog.LStick)) + add(ButtonInputSetting(playerIndex, NativeButton.LStick, R.string.pressed)) + addAll(getExtraStickSettings(playerIndex, NativeAnalog.LStick)) + } + + NpadStyleIndex.GameCube -> { + add(HeaderSetting(R.string.control_stick)) + addAll(getStickDirections(playerIndex, NativeAnalog.LStick)) + addAll(getExtraStickSettings(playerIndex, NativeAnalog.LStick)) + } + + else -> { + // No-op + } + } + + // Right stick + when (styleIndex) { + NpadStyleIndex.Fullkey, + NpadStyleIndex.Handheld, + NpadStyleIndex.JoyconDual, + NpadStyleIndex.JoyconRight -> { + add(HeaderSetting(R.string.right_stick)) + addAll(getStickDirections(playerIndex, NativeAnalog.RStick)) + add(ButtonInputSetting(playerIndex, NativeButton.RStick, R.string.pressed)) + addAll(getExtraStickSettings(playerIndex, NativeAnalog.RStick)) + } + + NpadStyleIndex.GameCube -> { + add(HeaderSetting(R.string.c_stick)) + addAll(getStickDirections(playerIndex, NativeAnalog.RStick)) + addAll(getExtraStickSettings(playerIndex, NativeAnalog.RStick)) + } + + else -> { + // No-op + } + } + + // L/R, ZL/ZR, and SL/SR + when (styleIndex) { + NpadStyleIndex.Fullkey, + NpadStyleIndex.Handheld -> { + add(HeaderSetting(R.string.triggers)) + add(ButtonInputSetting(playerIndex, NativeButton.L, R.string.button_l)) + add(ButtonInputSetting(playerIndex, NativeButton.R, R.string.button_r)) + add(ButtonInputSetting(playerIndex, NativeButton.ZL, R.string.button_zl)) + add(ButtonInputSetting(playerIndex, NativeButton.ZR, R.string.button_zr)) + } + + NpadStyleIndex.JoyconDual -> { + add(HeaderSetting(R.string.triggers)) + add(ButtonInputSetting(playerIndex, NativeButton.L, R.string.button_l)) + add(ButtonInputSetting(playerIndex, NativeButton.R, R.string.button_r)) + add(ButtonInputSetting(playerIndex, NativeButton.ZL, R.string.button_zl)) + add(ButtonInputSetting(playerIndex, NativeButton.ZR, R.string.button_zr)) + add( + ButtonInputSetting( + playerIndex, + NativeButton.SLLeft, + R.string.button_sl_left + ) + ) + add( + ButtonInputSetting( + playerIndex, + NativeButton.SRLeft, + R.string.button_sr_left + ) + ) + add( + ButtonInputSetting( + playerIndex, + NativeButton.SLRight, + R.string.button_sl_right + ) + ) + add( + ButtonInputSetting( + playerIndex, + NativeButton.SRRight, + R.string.button_sr_right + ) + ) + } + + NpadStyleIndex.JoyconLeft -> { + add(HeaderSetting(R.string.triggers)) + add(ButtonInputSetting(playerIndex, NativeButton.L, R.string.button_l)) + add(ButtonInputSetting(playerIndex, NativeButton.ZL, R.string.button_zl)) + add( + ButtonInputSetting( + playerIndex, + NativeButton.SLLeft, + R.string.button_sl_left + ) + ) + add( + ButtonInputSetting( + playerIndex, + NativeButton.SRLeft, + R.string.button_sr_left + ) + ) + } + + NpadStyleIndex.JoyconRight -> { + add(HeaderSetting(R.string.triggers)) + add(ButtonInputSetting(playerIndex, NativeButton.R, R.string.button_r)) + add(ButtonInputSetting(playerIndex, NativeButton.ZR, R.string.button_zr)) + add( + ButtonInputSetting( + playerIndex, + NativeButton.SLRight, + R.string.button_sl_right + ) + ) + add( + ButtonInputSetting( + playerIndex, + NativeButton.SRRight, + R.string.button_sr_right + ) + ) + } + + NpadStyleIndex.GameCube -> { + add(HeaderSetting(R.string.triggers)) + add(ButtonInputSetting(playerIndex, NativeButton.R, R.string.button_z)) + add(ButtonInputSetting(playerIndex, NativeButton.ZL, R.string.button_l)) + add(ButtonInputSetting(playerIndex, NativeButton.ZR, R.string.button_r)) + } + + else -> { + // No-op + } + } + + add(HeaderSetting(R.string.vibration)) + val vibrationEnabledSetting = object : AbstractBooleanSetting { + override val key = "vibration" + + override fun getBoolean(needsGlobal: Boolean): Boolean = + NativeConfig.getInputSettings(true)[playerIndex].vibrationEnabled + + override fun setBoolean(value: Boolean) { + val settings = NativeConfig.getInputSettings(true) + settings[playerIndex].vibrationEnabled = value + NativeConfig.setInputSettings(settings, true) + } + + override val defaultValue = true + + override fun getValueAsString(needsGlobal: Boolean): String = + getBoolean(needsGlobal).toString() + + override fun reset() = setBoolean(defaultValue) + } + add(SwitchSetting(vibrationEnabledSetting, R.string.vibration)) + + val useSystemVibratorSetting = object : AbstractBooleanSetting { + override val key = "" + + override fun getBoolean(needsGlobal: Boolean): Boolean = + NativeConfig.getInputSettings(true)[playerIndex].useSystemVibrator + + override fun setBoolean(value: Boolean) { + val settings = NativeConfig.getInputSettings(true) + settings[playerIndex].useSystemVibrator = value + NativeConfig.setInputSettings(settings, true) + } + + override val defaultValue = playerIndex == 0 + + override fun getValueAsString(needsGlobal: Boolean): String = + getBoolean(needsGlobal).toString() + + override fun reset() = setBoolean(defaultValue) + + override val pairedSettingKey: String = "vibration" + } + addAbstract(SwitchSetting(useSystemVibratorSetting, R.string.use_system_vibrator)) + + val vibrationStrengthSetting = object : AbstractIntSetting { + override val key = "" + + override fun getInt(needsGlobal: Boolean): Int = + NativeConfig.getInputSettings(true)[playerIndex].vibrationStrength + + override fun setInt(value: Int) { + val settings = NativeConfig.getInputSettings(true) + settings[playerIndex].vibrationStrength = value + NativeConfig.setInputSettings(settings, true) + } + + override val defaultValue = 100 + + override fun getValueAsString(needsGlobal: Boolean): String = + getInt(needsGlobal).toString() + + override fun reset() = setInt(defaultValue) + + override val pairedSettingKey: String = "vibration" + } + addAbstract( + SliderSetting(vibrationStrengthSetting, R.string.vibration_strength, units = "%") + ) + } + } + + // Convenience function for creating AbstractIntSettings for modifier range/stick range/stick deadzones + private fun getStickIntSettingFromParam( + playerIndex: Int, + paramName: String, + stick: NativeAnalog, + defaultValue: Int + ): AbstractIntSetting = + object : AbstractIntSetting { + val params get() = NativeInput.getStickParam(playerIndex, stick) + + override val key = "" + + override fun getInt(needsGlobal: Boolean): Int = + (params.get(paramName, 0.15f) * 100).toInt() + + override fun setInt(value: Int) { + val tempParams = params + tempParams.set(paramName, value.toFloat() / 100) + NativeInput.setStickParam(playerIndex, stick, tempParams) + } + + override val defaultValue = defaultValue + + override fun getValueAsString(needsGlobal: Boolean): String = + getInt(needsGlobal).toString() + + override fun reset() = setInt(defaultValue) + } + + private fun getExtraStickSettings( + playerIndex: Int, + nativeAnalog: NativeAnalog + ): List<SettingsItem> { + val stickIsController = + NativeInput.isController(NativeInput.getStickParam(playerIndex, nativeAnalog)) + val modifierRangeSetting = + getStickIntSettingFromParam(playerIndex, "modifier_scale", nativeAnalog, 50) + val stickRangeSetting = + getStickIntSettingFromParam(playerIndex, "range", nativeAnalog, 95) + val stickDeadzoneSetting = + getStickIntSettingFromParam(playerIndex, "deadzone", nativeAnalog, 15) + + val out = mutableListOf<SettingsItem>().apply { + if (stickIsController) { + add(SliderSetting(stickRangeSetting, titleId = R.string.range, min = 25, max = 150)) + add(SliderSetting(stickDeadzoneSetting, R.string.deadzone)) + } else { + add(ModifierInputSetting(playerIndex, NativeAnalog.LStick, R.string.modifier)) + add(SliderSetting(modifierRangeSetting, R.string.modifier_range)) + } + } + return out + } + + private fun getStickDirections(player: Int, stick: NativeAnalog): List<AnalogInputSetting> = + listOf( + AnalogInputSetting( + player, + stick, + AnalogDirection.Up, + R.string.up + ), + AnalogInputSetting( + player, + stick, + AnalogDirection.Down, + R.string.down + ), + AnalogInputSetting( + player, + stick, + AnalogDirection.Left, + R.string.left + ), + AnalogInputSetting( + player, + stick, + AnalogDirection.Right, + R.string.right + ) + ) + private fun addThemeSettings(sl: ArrayList<SettingsItem>) { sl.apply { val theme: AbstractIntSetting = object : AbstractIntSetting { @@ -186,20 +877,18 @@ class SettingsFragmentPresenter( add( SingleChoiceSetting( theme, - R.string.change_app_theme, - 0, - R.array.themeEntriesA12, - R.array.themeValuesA12 + titleId = R.string.change_app_theme, + choicesId = R.array.themeEntriesA12, + valuesId = R.array.themeValuesA12 ) ) } else { add( SingleChoiceSetting( theme, - R.string.change_app_theme, - 0, - R.array.themeEntries, - R.array.themeValues + titleId = R.string.change_app_theme, + choicesId = R.array.themeEntries, + valuesId = R.array.themeValues ) ) } @@ -228,10 +917,9 @@ class SettingsFragmentPresenter( add( SingleChoiceSetting( themeMode, - R.string.change_theme_mode, - 0, - R.array.themeModeEntries, - R.array.themeModeValues + titleId = R.string.change_theme_mode, + choicesId = R.array.themeModeEntries, + valuesId = R.array.themeModeValues ) ) @@ -262,8 +950,8 @@ class SettingsFragmentPresenter( add( SwitchSetting( blackBackgrounds, - R.string.use_black_backgrounds, - R.string.use_black_backgrounds_description + titleId = R.string.use_black_backgrounds, + descriptionId = R.string.use_black_backgrounds_description ) ) } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsSearchFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsSearchFragment.kt index a135b80b4..ed60cf34f 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsSearchFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsSearchFragment.kt @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2023 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later -package org.yuzu.yuzu_emu.fragments +package org.yuzu.yuzu_emu.features.settings.ui import android.content.Context import android.os.Bundle @@ -15,21 +15,17 @@ import androidx.core.view.updatePadding import androidx.core.widget.doOnTextChanged import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle import androidx.recyclerview.widget.LinearLayoutManager import com.google.android.material.divider.MaterialDividerItemDecoration import com.google.android.material.transition.MaterialSharedAxis import info.debatty.java.stringsimilarity.Cosine -import kotlinx.coroutines.launch import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.databinding.FragmentSettingsSearchBinding import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem -import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter -import org.yuzu.yuzu_emu.model.SettingsViewModel import org.yuzu.yuzu_emu.utils.NativeConfig +import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins +import org.yuzu.yuzu_emu.utils.collect class SettingsSearchFragment : Fragment() { private var _binding: FragmentSettingsSearchBinding? = null @@ -85,14 +81,10 @@ class SettingsSearchFragment : Fragment() { search() binding.settingsList.smoothScrollToPosition(0) } - viewLifecycleOwner.lifecycleScope.launch { - repeatOnLifecycle(Lifecycle.State.CREATED) { - settingsViewModel.shouldReloadSettingsList.collect { - if (it) { - settingsViewModel.setShouldReloadSettingsList(false) - search() - } - } + settingsViewModel.shouldReloadSettingsList.collect(viewLifecycleOwner) { + if (it) { + settingsViewModel.setShouldReloadSettingsList(false) + search() } } @@ -108,10 +100,9 @@ class SettingsSearchFragment : Fragment() { private fun search() { val searchTerm = binding.searchText.text.toString().lowercase() - binding.clearButton.visibility = - if (searchTerm.isEmpty()) View.INVISIBLE else View.VISIBLE + binding.clearButton.setVisible(visible = searchTerm.isNotEmpty(), gone = false) if (searchTerm.isEmpty()) { - binding.noResultsView.visibility = View.VISIBLE + binding.noResultsView.setVisible(visible = false, gone = false) settingsAdapter?.submitList(emptyList()) return } @@ -119,7 +110,7 @@ class SettingsSearchFragment : Fragment() { val baseList = SettingsItem.settingsItems val similarityAlgorithm = if (searchTerm.length > 2) Cosine() else Cosine(1) val sortedList: List<SettingsItem> = baseList.mapNotNull { item -> - val title = getString(item.value.nameId).lowercase() + val title = item.value.title.lowercase() val similarity = similarityAlgorithm.similarity(searchTerm, title) if (similarity > 0.08) { Pair(similarity, item) @@ -138,8 +129,7 @@ class SettingsSearchFragment : Fragment() { optionalSetting } settingsAdapter?.submitList(sortedList) - binding.noResultsView.visibility = - if (sortedList.isEmpty()) View.VISIBLE else View.INVISIBLE + binding.noResultsView.setVisible(visible = sortedList.isEmpty(), gone = false) } private fun focusSearch() { diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SettingsViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsViewModel.kt index 5cb6a5d57..fbdca04e9 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SettingsViewModel.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsViewModel.kt @@ -1,20 +1,26 @@ // SPDX-FileCopyrightText: 2023 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later -package org.yuzu.yuzu_emu.model +package org.yuzu.yuzu_emu.features.settings.ui import androidx.lifecycle.ViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.YuzuApplication import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem +import org.yuzu.yuzu_emu.model.Game +import org.yuzu.yuzu_emu.utils.InputHandler +import org.yuzu.yuzu_emu.utils.ParamPackage class SettingsViewModel : ViewModel() { var game: Game? = null var clickedItem: SettingsItem? = null + var currentDevice = 0 + val shouldRecreate: StateFlow<Boolean> get() = _shouldRecreate private val _shouldRecreate = MutableStateFlow(false) @@ -36,6 +42,18 @@ class SettingsViewModel : ViewModel() { val adapterItemChanged: StateFlow<Int> get() = _adapterItemChanged private val _adapterItemChanged = MutableStateFlow(-1) + private val _datasetChanged = MutableStateFlow(false) + val datasetChanged = _datasetChanged.asStateFlow() + + private val _reloadListAndNotifyDataset = MutableStateFlow(false) + val reloadListAndNotifyDataset = _reloadListAndNotifyDataset.asStateFlow() + + private val _shouldShowDeleteProfileDialog = MutableStateFlow("") + val shouldShowDeleteProfileDialog = _shouldShowDeleteProfileDialog.asStateFlow() + + private val _shouldShowResetInputDialog = MutableStateFlow(false) + val shouldShowResetInputDialog = _shouldShowResetInputDialog.asStateFlow() + fun setShouldRecreate(value: Boolean) { _shouldRecreate.value = value } @@ -68,4 +86,27 @@ class SettingsViewModel : ViewModel() { fun setAdapterItemChanged(value: Int) { _adapterItemChanged.value = value } + + fun setDatasetChanged(value: Boolean) { + _datasetChanged.value = value + } + + fun setReloadListAndNotifyDataset(value: Boolean) { + _reloadListAndNotifyDataset.value = value + } + + fun setShouldShowDeleteProfileDialog(profile: String) { + _shouldShowDeleteProfileDialog.value = profile + } + + fun setShouldShowResetInputDialog(value: Boolean) { + _shouldShowResetInputDialog.value = value + } + + fun getCurrentDeviceParams(defaultParams: ParamPackage): ParamPackage = + try { + InputHandler.registeredControllers[currentDevice] + } catch (e: IndexOutOfBoundsException) { + defaultParams + } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/DateTimeViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/DateTimeViewHolder.kt index 5ad0899dd..367db7fd2 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/DateTimeViewHolder.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/DateTimeViewHolder.kt @@ -14,6 +14,7 @@ import org.yuzu.yuzu_emu.features.settings.model.view.DateTimeSetting import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter import org.yuzu.yuzu_emu.utils.NativeConfig +import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible class DateTimeViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) : SettingViewHolder(binding.root, adapter) { @@ -21,28 +22,19 @@ class DateTimeViewHolder(val binding: ListItemSettingBinding, adapter: SettingsA override fun bind(item: SettingsItem) { setting = item as DateTimeSetting - binding.textSettingName.setText(item.nameId) - if (item.descriptionId != 0) { - binding.textSettingDescription.setText(item.descriptionId) - binding.textSettingDescription.visibility = View.VISIBLE - } else { - binding.textSettingDescription.visibility = View.GONE - } - - binding.textSettingValue.visibility = View.VISIBLE + binding.textSettingName.text = item.title + binding.textSettingDescription.setVisible(item.description.isNotEmpty()) + binding.textSettingDescription.text = item.description + binding.textSettingValue.setVisible(true) val epochTime = setting.getValue() val instant = Instant.ofEpochMilli(epochTime * 1000) val zonedTime = ZonedDateTime.ofInstant(instant, ZoneId.of("UTC")) val dateFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM) binding.textSettingValue.text = dateFormatter.format(zonedTime) - binding.buttonClear.visibility = if (setting.setting.global || - !NativeConfig.isPerGameConfigLoaded() - ) { - View.GONE - } else { - View.VISIBLE - } + binding.buttonClear.setVisible( + !setting.setting.global || NativeConfig.isPerGameConfigLoaded() + ) binding.buttonClear.setOnClickListener { adapter.onClearClick(setting, bindingAdapterPosition) } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/HeaderViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/HeaderViewHolder.kt index f5bcf705c..0815c36e2 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/HeaderViewHolder.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/HeaderViewHolder.kt @@ -16,7 +16,7 @@ class HeaderViewHolder(val binding: ListItemSettingsHeaderBinding, adapter: Sett } override fun bind(item: SettingsItem) { - binding.textHeaderName.setText(item.nameId) + binding.textHeaderName.text = item.title } override fun onClick(clicked: View) { diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/InputProfileViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/InputProfileViewHolder.kt new file mode 100644 index 000000000..86af3a226 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/InputProfileViewHolder.kt @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.features.settings.ui.viewholder + +import android.view.View +import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding +import org.yuzu.yuzu_emu.features.settings.model.view.InputProfileSetting +import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem +import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter +import org.yuzu.yuzu_emu.R +import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible + +class InputProfileViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) : + SettingViewHolder(binding.root, adapter) { + private lateinit var setting: InputProfileSetting + + override fun bind(item: SettingsItem) { + setting = item as InputProfileSetting + binding.textSettingName.text = setting.title + binding.textSettingValue.text = + setting.getCurrentProfile().ifEmpty { binding.root.context.getString(R.string.not_set) } + + binding.textSettingDescription.setVisible(false) + binding.buttonClear.setVisible(false) + binding.icon.setVisible(false) + binding.buttonClear.setVisible(false) + } + + override fun onClick(clicked: View) = + adapter.onInputProfileClick(setting, bindingAdapterPosition) + + override fun onLongClick(clicked: View): Boolean = false +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/InputViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/InputViewHolder.kt new file mode 100644 index 000000000..9d9047804 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/InputViewHolder.kt @@ -0,0 +1,60 @@ +// SPDX-FileCopyrightText: 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.features.settings.ui.viewholder + +import android.view.View +import org.yuzu.yuzu_emu.databinding.ListItemSettingInputBinding +import org.yuzu.yuzu_emu.features.input.NativeInput +import org.yuzu.yuzu_emu.features.settings.model.view.AnalogInputSetting +import org.yuzu.yuzu_emu.features.settings.model.view.ButtonInputSetting +import org.yuzu.yuzu_emu.features.settings.model.view.InputSetting +import org.yuzu.yuzu_emu.features.settings.model.view.ModifierInputSetting +import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem +import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter +import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible + +class InputViewHolder(val binding: ListItemSettingInputBinding, adapter: SettingsAdapter) : + SettingViewHolder(binding.root, adapter) { + private lateinit var setting: InputSetting + + override fun bind(item: SettingsItem) { + setting = item as InputSetting + binding.textSettingName.text = setting.title + binding.textSettingValue.text = setting.getSelectedValue() + + when (item) { + is AnalogInputSetting -> { + val param = NativeInput.getStickParam(item.playerIndex, item.nativeAnalog) + binding.buttonOptions.setVisible( + param.get("engine", "") == "analog_from_button" || + param.has("axis_x") || param.has("axis_y") + ) + } + + is ButtonInputSetting -> { + val param = NativeInput.getButtonParam(item.playerIndex, item.nativeButton) + binding.buttonOptions.setVisible( + param.has("code") || param.has("button") || param.has("hat") || + param.has("axis") + ) + } + + is ModifierInputSetting -> { + val params = NativeInput.getStickParam(item.playerIndex, item.nativeAnalog) + binding.buttonOptions.setVisible(params.has("modifier")) + } + } + + binding.buttonOptions.setOnClickListener(null) + binding.buttonOptions.setOnClickListener { + adapter.onInputOptionsClick(binding.buttonOptions, setting, bindingAdapterPosition) + } + } + + override fun onClick(clicked: View) = + adapter.onInputClick(setting, bindingAdapterPosition) + + override fun onLongClick(clicked: View): Boolean = + adapter.onLongClick(setting, bindingAdapterPosition) +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/RunnableViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/RunnableViewHolder.kt index 507184238..fc2ffb515 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/RunnableViewHolder.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/RunnableViewHolder.kt @@ -5,11 +5,11 @@ package org.yuzu.yuzu_emu.features.settings.ui.viewholder import android.view.View import androidx.core.content.res.ResourcesCompat -import org.yuzu.yuzu_emu.NativeLibrary import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding import org.yuzu.yuzu_emu.features.settings.model.view.RunnableSetting import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter +import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible class RunnableViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) : SettingViewHolder(binding.root, adapter) { @@ -17,34 +17,28 @@ class RunnableViewHolder(val binding: ListItemSettingBinding, adapter: SettingsA override fun bind(item: SettingsItem) { setting = item as RunnableSetting - if (item.iconId != 0) { - binding.icon.visibility = View.VISIBLE + binding.icon.setVisible(setting.iconId != 0) + if (setting.iconId != 0) { binding.icon.setImageDrawable( ResourcesCompat.getDrawable( binding.icon.resources, - item.iconId, + setting.iconId, binding.icon.context.theme ) ) - } else { - binding.icon.visibility = View.GONE } - binding.textSettingName.setText(item.nameId) - if (item.descriptionId != 0) { - binding.textSettingDescription.setText(item.descriptionId) - binding.textSettingDescription.visibility = View.VISIBLE - } else { - binding.textSettingDescription.visibility = View.GONE - } - binding.textSettingValue.visibility = View.GONE - binding.buttonClear.visibility = View.GONE + binding.textSettingName.text = setting.title + binding.textSettingDescription.setVisible(setting.description.isNotEmpty()) + binding.textSettingDescription.text = item.description + binding.textSettingValue.setVisible(false) + binding.buttonClear.setVisible(false) setStyle(setting.isEditable, binding) } override fun onClick(clicked: View) { - if (!setting.isRuntimeRunnable && !NativeLibrary.isRunning()) { + if (setting.isRunnable) { setting.runnable.invoke() } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.kt index 02dab3785..e2fe0b072 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.kt @@ -5,11 +5,13 @@ package org.yuzu.yuzu_emu.features.settings.ui.viewholder import android.view.View import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding +import org.yuzu.yuzu_emu.features.settings.model.view.IntSingleChoiceSetting import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem import org.yuzu.yuzu_emu.features.settings.model.view.SingleChoiceSetting import org.yuzu.yuzu_emu.features.settings.model.view.StringSingleChoiceSetting import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter import org.yuzu.yuzu_emu.utils.NativeConfig +import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible class SingleChoiceViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) : SettingViewHolder(binding.root, adapter) { @@ -17,40 +19,38 @@ class SingleChoiceViewHolder(val binding: ListItemSettingBinding, adapter: Setti override fun bind(item: SettingsItem) { setting = item - binding.textSettingName.setText(item.nameId) - if (item.descriptionId != 0) { - binding.textSettingDescription.setText(item.descriptionId) - binding.textSettingDescription.visibility = View.VISIBLE - } else { - binding.textSettingDescription.visibility = View.GONE - } + binding.textSettingName.text = setting.title + binding.textSettingDescription.setVisible(item.description.isNotEmpty()) + binding.textSettingDescription.text = item.description - binding.textSettingValue.visibility = View.VISIBLE - if (item is SingleChoiceSetting) { - val resMgr = binding.textSettingValue.context.resources - val values = resMgr.getIntArray(item.valuesId) - for (i in values.indices) { - if (values[i] == item.getSelectedValue()) { - binding.textSettingValue.text = resMgr.getStringArray(item.choicesId)[i] - break + binding.textSettingValue.setVisible(true) + when (item) { + is SingleChoiceSetting -> { + val resMgr = binding.textSettingValue.context.resources + val values = resMgr.getIntArray(item.valuesId) + for (i in values.indices) { + if (values[i] == item.getSelectedValue()) { + binding.textSettingValue.text = resMgr.getStringArray(item.choicesId)[i] + break + } } } - } else if (item is StringSingleChoiceSetting) { - for (i in item.values.indices) { - if (item.values[i] == item.getSelectedValue()) { - binding.textSettingValue.text = item.choices[i] - break - } + + is StringSingleChoiceSetting -> { + binding.textSettingValue.text = item.getSelectedValue() } - } - binding.buttonClear.visibility = if (setting.setting.global || - !NativeConfig.isPerGameConfigLoaded() - ) { - View.GONE - } else { - View.VISIBLE + is IntSingleChoiceSetting -> { + binding.textSettingValue.text = item.getChoiceAt(item.getSelectedValue()) + } + } + if (binding.textSettingValue.text.isEmpty()) { + binding.textSettingValue.setVisible(false) } + + binding.buttonClear.setVisible( + !setting.setting.global || NativeConfig.isPerGameConfigLoaded() + ) binding.buttonClear.setOnClickListener { adapter.onClearClick(setting, bindingAdapterPosition) } @@ -63,16 +63,25 @@ class SingleChoiceViewHolder(val binding: ListItemSettingBinding, adapter: Setti return } - if (setting is SingleChoiceSetting) { - adapter.onSingleChoiceClick( - (setting as SingleChoiceSetting), - bindingAdapterPosition - ) - } else if (setting is StringSingleChoiceSetting) { - adapter.onStringSingleChoiceClick( - (setting as StringSingleChoiceSetting), + when (setting) { + is SingleChoiceSetting -> adapter.onSingleChoiceClick( + setting as SingleChoiceSetting, bindingAdapterPosition ) + + is StringSingleChoiceSetting -> { + adapter.onStringSingleChoiceClick( + setting as StringSingleChoiceSetting, + bindingAdapterPosition + ) + } + + is IntSingleChoiceSetting -> { + adapter.onIntSingleChoiceClick( + setting as IntSingleChoiceSetting, + bindingAdapterPosition + ) + } } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SliderViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SliderViewHolder.kt index 596c18012..a37b59b44 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SliderViewHolder.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SliderViewHolder.kt @@ -10,6 +10,7 @@ import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem import org.yuzu.yuzu_emu.features.settings.model.view.SliderSetting import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter import org.yuzu.yuzu_emu.utils.NativeConfig +import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible class SliderViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) : SettingViewHolder(binding.root, adapter) { @@ -17,27 +18,19 @@ class SliderViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAda override fun bind(item: SettingsItem) { setting = item as SliderSetting - binding.textSettingName.setText(item.nameId) - if (item.descriptionId != 0) { - binding.textSettingDescription.setText(item.descriptionId) - binding.textSettingDescription.visibility = View.VISIBLE - } else { - binding.textSettingDescription.visibility = View.GONE - } - binding.textSettingValue.visibility = View.VISIBLE + binding.textSettingName.text = setting.title + binding.textSettingDescription.setVisible(item.description.isNotEmpty()) + binding.textSettingDescription.text = setting.description + binding.textSettingValue.setVisible(true) binding.textSettingValue.text = String.format( binding.textSettingValue.context.getString(R.string.value_with_units), setting.getSelectedValue(), setting.units ) - binding.buttonClear.visibility = if (setting.setting.global || - !NativeConfig.isPerGameConfigLoaded() - ) { - View.GONE - } else { - View.VISIBLE - } + binding.buttonClear.setVisible( + !setting.setting.global || NativeConfig.isPerGameConfigLoaded() + ) binding.buttonClear.setOnClickListener { adapter.onClearClick(setting, bindingAdapterPosition) } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SubmenuViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SubmenuViewHolder.kt index 20d35a17d..f7a9c08c3 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SubmenuViewHolder.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SubmenuViewHolder.kt @@ -9,39 +9,34 @@ import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem import org.yuzu.yuzu_emu.features.settings.model.view.SubmenuSetting import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter +import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible class SubmenuViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) : SettingViewHolder(binding.root, adapter) { - private lateinit var item: SubmenuSetting + private lateinit var setting: SubmenuSetting override fun bind(item: SettingsItem) { - this.item = item as SubmenuSetting - if (item.iconId != 0) { - binding.icon.visibility = View.VISIBLE + setting = item as SubmenuSetting + binding.icon.setVisible(setting.iconId != 0) + if (setting.iconId != 0) { binding.icon.setImageDrawable( ResourcesCompat.getDrawable( binding.icon.resources, - item.iconId, + setting.iconId, binding.icon.context.theme ) ) - } else { - binding.icon.visibility = View.GONE } - binding.textSettingName.setText(item.nameId) - if (item.descriptionId != 0) { - binding.textSettingDescription.setText(item.descriptionId) - binding.textSettingDescription.visibility = View.VISIBLE - } else { - binding.textSettingDescription.visibility = View.GONE - } - binding.textSettingValue.visibility = View.GONE - binding.buttonClear.visibility = View.GONE + binding.textSettingName.text = setting.title + binding.textSettingDescription.setVisible(setting.description.isNotEmpty()) + binding.textSettingDescription.text = setting.description + binding.textSettingValue.setVisible(false) + binding.buttonClear.setVisible(false) } override fun onClick(clicked: View) { - adapter.onSubmenuClick(item) + adapter.onSubmenuClick(setting) } override fun onLongClick(clicked: View): Boolean { diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt index d26bf9374..53f7b301f 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt @@ -10,6 +10,7 @@ import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem import org.yuzu.yuzu_emu.features.settings.model.view.SwitchSetting import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter import org.yuzu.yuzu_emu.utils.NativeConfig +import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible class SwitchSettingViewHolder(val binding: ListItemSettingSwitchBinding, adapter: SettingsAdapter) : SettingViewHolder(binding.root, adapter) { @@ -18,28 +19,19 @@ class SwitchSettingViewHolder(val binding: ListItemSettingSwitchBinding, adapter override fun bind(item: SettingsItem) { setting = item as SwitchSetting - binding.textSettingName.setText(item.nameId) - if (item.descriptionId != 0) { - binding.textSettingDescription.setText(item.descriptionId) - binding.textSettingDescription.visibility = View.VISIBLE - } else { - binding.textSettingDescription.text = "" - binding.textSettingDescription.visibility = View.GONE - } + binding.textSettingName.text = setting.title + binding.textSettingDescription.setVisible(setting.description.isNotEmpty()) + binding.textSettingDescription.text = setting.description binding.switchWidget.setOnCheckedChangeListener(null) binding.switchWidget.isChecked = setting.getIsChecked(setting.needsRuntimeGlobal) binding.switchWidget.setOnCheckedChangeListener { _: CompoundButton, _: Boolean -> - adapter.onBooleanClick(item, binding.switchWidget.isChecked, bindingAdapterPosition) + adapter.onBooleanClick(setting, binding.switchWidget.isChecked, bindingAdapterPosition) } - binding.buttonClear.visibility = if (setting.setting.global || - !NativeConfig.isPerGameConfigLoaded() - ) { - View.GONE - } else { - View.VISIBLE - } + binding.buttonClear.setVisible( + !setting.setting.global || NativeConfig.isPerGameConfigLoaded() + ) binding.buttonClear.setOnClickListener { adapter.onClearClick(setting, bindingAdapterPosition) } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddonsFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddonsFragment.kt index 872553ac4..110aa2960 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddonsFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddonsFragment.kt @@ -3,7 +3,6 @@ package org.yuzu.yuzu_emu.fragments -import android.annotation.SuppressLint import android.content.Intent import android.os.Bundle import android.view.LayoutInflater @@ -16,9 +15,6 @@ import androidx.core.view.updatePadding import androidx.documentfile.provider.DocumentFile 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.navigation.fragment.navArgs import androidx.recyclerview.widget.LinearLayoutManager @@ -32,6 +28,7 @@ import org.yuzu.yuzu_emu.model.HomeViewModel import org.yuzu.yuzu_emu.utils.AddonUtil import org.yuzu.yuzu_emu.utils.FileUtil.copyFilesTo import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins +import org.yuzu.yuzu_emu.utils.collect import java.io.File class AddonsFragment : Fragment() { @@ -60,8 +57,6 @@ class AddonsFragment : Fragment() { return binding.root } - // This is using the correct scope, lint is just acting up - @SuppressLint("UnsafeRepeatOnLifecycleDetector") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) homeViewModel.setNavigationVisibility(visible = false, animated = false) @@ -78,57 +73,41 @@ class AddonsFragment : Fragment() { adapter = AddonAdapter(addonViewModel) } - viewLifecycleOwner.lifecycleScope.apply { - launch { - repeatOnLifecycle(Lifecycle.State.STARTED) { - addonViewModel.addonList.collect { - (binding.listAddons.adapter as AddonAdapter).submitList(it) - } - } - } - launch { - repeatOnLifecycle(Lifecycle.State.STARTED) { - addonViewModel.showModInstallPicker.collect { - if (it) { - installAddon.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data) - addonViewModel.showModInstallPicker(false) - } - } - } - } - launch { - repeatOnLifecycle(Lifecycle.State.STARTED) { - addonViewModel.showModNoticeDialog.collect { - if (it) { - MessageDialogFragment.newInstance( - requireActivity(), - titleId = R.string.addon_notice, - descriptionId = R.string.addon_notice_description, - dismissible = false, - positiveAction = { addonViewModel.showModInstallPicker(true) }, - negativeAction = {}, - negativeButtonTitleId = R.string.close - ).show(parentFragmentManager, MessageDialogFragment.TAG) - addonViewModel.showModNoticeDialog(false) - } - } - } + addonViewModel.addonList.collect(viewLifecycleOwner) { + (binding.listAddons.adapter as AddonAdapter).submitList(it) + } + addonViewModel.showModInstallPicker.collect( + viewLifecycleOwner, + resetState = { addonViewModel.showModInstallPicker(false) } + ) { if (it) installAddon.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data) } + addonViewModel.showModNoticeDialog.collect( + viewLifecycleOwner, + resetState = { addonViewModel.showModNoticeDialog(false) } + ) { + if (it) { + MessageDialogFragment.newInstance( + requireActivity(), + titleId = R.string.addon_notice, + descriptionId = R.string.addon_notice_description, + dismissible = false, + positiveAction = { addonViewModel.showModInstallPicker(true) }, + negativeAction = {}, + negativeButtonTitleId = R.string.close + ).show(parentFragmentManager, MessageDialogFragment.TAG) } - launch { - repeatOnLifecycle(Lifecycle.State.STARTED) { - addonViewModel.addonToDelete.collect { - if (it != null) { - MessageDialogFragment.newInstance( - requireActivity(), - titleId = R.string.confirm_uninstall, - descriptionId = R.string.confirm_uninstall_description, - positiveAction = { addonViewModel.onDeleteAddon(it) }, - negativeAction = {} - ).show(parentFragmentManager, MessageDialogFragment.TAG) - addonViewModel.setAddonToDelete(null) - } - } - } + } + addonViewModel.addonToDelete.collect( + viewLifecycleOwner, + resetState = { addonViewModel.setAddonToDelete(null) } + ) { + if (it != null) { + MessageDialogFragment.newInstance( + requireActivity(), + titleId = R.string.confirm_uninstall, + descriptionId = R.string.confirm_uninstall_description, + positiveAction = { addonViewModel.onDeleteAddon(it) }, + negativeAction = {} + ).show(parentFragmentManager, MessageDialogFragment.TAG) } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/CoreErrorDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/CoreErrorDialogFragment.kt new file mode 100644 index 000000000..299f8da71 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/CoreErrorDialogFragment.kt @@ -0,0 +1,47 @@ +// SPDX-FileCopyrightText: 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.fragments + +import android.app.Dialog +import android.content.DialogInterface +import android.os.Bundle +import androidx.fragment.app.DialogFragment +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import org.yuzu.yuzu_emu.NativeLibrary +import org.yuzu.yuzu_emu.R + +class CoreErrorDialogFragment : DialogFragment() { + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = + MaterialAlertDialogBuilder(requireActivity()) + .setTitle(requireArguments().getString(TITLE)) + .setMessage(requireArguments().getString(MESSAGE)) + .setPositiveButton(R.string.continue_button, null) + .setNegativeButton(R.string.abort_button) { _: DialogInterface?, _: Int -> + NativeLibrary.coreErrorAlertResult = false + synchronized(NativeLibrary.coreErrorAlertLock) { + NativeLibrary.coreErrorAlertLock.notify() + } + } + .create() + + override fun onDismiss(dialog: DialogInterface) { + super.onDismiss(dialog) + NativeLibrary.coreErrorAlertResult = true + synchronized(NativeLibrary.coreErrorAlertLock) { NativeLibrary.coreErrorAlertLock.notify() } + } + + companion object { + const val TITLE = "Title" + const val MESSAGE = "Message" + + fun newInstance(title: String, message: String): CoreErrorDialogFragment { + val frag = CoreErrorDialogFragment() + val args = Bundle() + args.putString(TITLE, title) + args.putString(MESSAGE, message) + frag.arguments = args + return frag + } + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt index 41cff46c1..8b23a1021 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt @@ -3,7 +3,6 @@ package org.yuzu.yuzu_emu.fragments -import android.annotation.SuppressLint import android.os.Bundle import android.view.LayoutInflater import android.view.View @@ -14,9 +13,6 @@ 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.navigation.fragment.navArgs import androidx.recyclerview.widget.GridLayoutManager @@ -35,6 +31,7 @@ import org.yuzu.yuzu_emu.utils.FileUtil import org.yuzu.yuzu_emu.utils.GpuDriverHelper import org.yuzu.yuzu_emu.utils.NativeConfig import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins +import org.yuzu.yuzu_emu.utils.collect import java.io.File import java.io.IOException @@ -63,8 +60,6 @@ class DriverManagerFragment : Fragment() { return binding.root } - // This is using the correct scope, lint is just acting up - @SuppressLint("UnsafeRepeatOnLifecycleDetector") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) homeViewModel.setNavigationVisibility(visible = false, animated = true) @@ -89,15 +84,8 @@ class DriverManagerFragment : Fragment() { } } - viewLifecycleOwner.lifecycleScope.apply { - launch { - repeatOnLifecycle(Lifecycle.State.STARTED) { - driverViewModel.showClearButton.collect { - binding.toolbarDrivers.menu - .findItem(R.id.menu_driver_use_global).isVisible = it - } - } - } + driverViewModel.showClearButton.collect(viewLifecycleOwner) { + binding.toolbarDrivers.menu.findItem(R.id.menu_driver_use_global).isVisible = it } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriversLoadingDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriversLoadingDialogFragment.kt index 6a47b29f0..bad56e434 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriversLoadingDialogFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriversLoadingDialogFragment.kt @@ -10,14 +10,11 @@ import android.view.View import android.view.ViewGroup 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.launch import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding import org.yuzu.yuzu_emu.model.DriverViewModel +import org.yuzu.yuzu_emu.utils.collect class DriversLoadingDialogFragment : DialogFragment() { private val driverViewModel: DriverViewModel by activityViewModels() @@ -44,13 +41,7 @@ class DriversLoadingDialogFragment : DialogFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - viewLifecycleOwner.lifecycleScope.apply { - launch { - repeatOnLifecycle(Lifecycle.State.RESUMED) { - driverViewModel.isInteractionAllowed.collect { if (it) dismiss() } - } - } - } + driverViewModel.isInteractionAllowed.collect(viewLifecycleOwner) { if (it) dismiss() } } companion object { diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt index 6b25cc525..c3b2b11f8 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt @@ -32,9 +32,6 @@ import androidx.drawerlayout.widget.DrawerLayout import androidx.drawerlayout.widget.DrawerLayout.DrawerListener 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.navigation.fragment.navArgs import androidx.window.layout.FoldingFeature @@ -42,9 +39,6 @@ import androidx.window.layout.WindowInfoTracker import androidx.window.layout.WindowLayoutInfo import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.slider.Slider -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.launch import org.yuzu.yuzu_emu.HomeNavigationDirections import org.yuzu.yuzu_emu.NativeLibrary import org.yuzu.yuzu_emu.R @@ -63,6 +57,7 @@ import org.yuzu.yuzu_emu.model.EmulationViewModel import org.yuzu.yuzu_emu.overlay.model.OverlayControl import org.yuzu.yuzu_emu.overlay.model.OverlayLayout import org.yuzu.yuzu_emu.utils.* +import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible import java.lang.NullPointerException class EmulationFragment : Fragment(), SurfaceHolder.Callback { @@ -90,14 +85,6 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { if (context is EmulationActivity) { emulationActivity = context NativeLibrary.setEmulationActivity(context) - - lifecycleScope.launch(Dispatchers.Main) { - lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { - WindowInfoTracker.getOrCreate(context) - .windowLayoutInfo(context) - .collect { updateFoldableLayout(context, it) } - } - } } else { throw IllegalStateException("EmulationFragment must have EmulationActivity parent") } @@ -168,8 +155,6 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { return binding.root } - // This is using the correct scope, lint is just acting up - @SuppressLint("UnsafeRepeatOnLifecycleDetector") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) if (requireActivity().isFinishing) { @@ -277,6 +262,15 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { true } + R.id.menu_controls -> { + val action = HomeNavigationDirections.actionGlobalSettingsActivity( + null, + Settings.MenuTag.SECTION_INPUT + ) + binding.root.findNavController().navigate(action) + true + } + R.id.menu_overlay_controls -> { showOverlayOptions() true @@ -341,129 +335,86 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { binding.loadingTitle.isSelected = true binding.loadingText.isSelected = true - viewLifecycleOwner.lifecycleScope.apply { - launch { - repeatOnLifecycle(Lifecycle.State.STARTED) { - WindowInfoTracker.getOrCreate(requireContext()) - .windowLayoutInfo(requireActivity()) - .collect { - updateFoldableLayout(requireActivity() as EmulationActivity, it) - } - } + WindowInfoTracker.getOrCreate(requireContext()) + .windowLayoutInfo(requireActivity()).collect(viewLifecycleOwner) { + updateFoldableLayout(requireActivity() as EmulationActivity, it) } - launch { - repeatOnLifecycle(Lifecycle.State.CREATED) { - emulationViewModel.shaderProgress.collectLatest { - if (it > 0 && it != emulationViewModel.totalShaders.value) { - binding.loadingProgressIndicator.isIndeterminate = false - - if (it < binding.loadingProgressIndicator.max) { - binding.loadingProgressIndicator.progress = it - } - } + emulationViewModel.shaderProgress.collect(viewLifecycleOwner) { + if (it > 0 && it != emulationViewModel.totalShaders.value) { + binding.loadingProgressIndicator.isIndeterminate = false - if (it == emulationViewModel.totalShaders.value) { - binding.loadingText.setText(R.string.loading) - binding.loadingProgressIndicator.isIndeterminate = true - } - } + if (it < binding.loadingProgressIndicator.max) { + binding.loadingProgressIndicator.progress = it } } - launch { - repeatOnLifecycle(Lifecycle.State.CREATED) { - emulationViewModel.totalShaders.collectLatest { - binding.loadingProgressIndicator.max = it - } - } - } - launch { - repeatOnLifecycle(Lifecycle.State.CREATED) { - emulationViewModel.shaderMessage.collectLatest { - if (it.isNotEmpty()) { - binding.loadingText.text = it - } - } - } + + if (it == emulationViewModel.totalShaders.value) { + binding.loadingText.setText(R.string.loading) + binding.loadingProgressIndicator.isIndeterminate = true } - launch { - repeatOnLifecycle(Lifecycle.State.RESUMED) { - driverViewModel.isInteractionAllowed.collect { - if (it) { - startEmulation() - } - } - } + } + emulationViewModel.totalShaders.collect(viewLifecycleOwner) { + binding.loadingProgressIndicator.max = it + } + emulationViewModel.shaderMessage.collect(viewLifecycleOwner) { + if (it.isNotEmpty()) { + binding.loadingText.text = it } - launch { - repeatOnLifecycle(Lifecycle.State.CREATED) { - emulationViewModel.emulationStarted.collectLatest { - if (it) { - binding.drawerLayout.setDrawerLockMode(IntSetting.LOCK_DRAWER.getInt()) - ViewUtils.showView(binding.surfaceInputOverlay) - ViewUtils.hideView(binding.loadingIndicator) - - emulationState.updateSurface() - - // Setup overlays - updateShowFpsOverlay() - updateThermalOverlay() - } - } - } + } + + emulationViewModel.emulationStarted.collect(viewLifecycleOwner) { + if (it) { + binding.drawerLayout.setDrawerLockMode(IntSetting.LOCK_DRAWER.getInt()) + ViewUtils.showView(binding.surfaceInputOverlay) + ViewUtils.hideView(binding.loadingIndicator) + + emulationState.updateSurface() + + // Setup overlays + updateShowFpsOverlay() + updateThermalOverlay() } - launch { - repeatOnLifecycle(Lifecycle.State.CREATED) { - emulationViewModel.isEmulationStopping.collectLatest { - if (it) { - binding.loadingText.setText(R.string.shutting_down) - ViewUtils.showView(binding.loadingIndicator) - ViewUtils.hideView(binding.inputContainer) - ViewUtils.hideView(binding.showFpsText) - } - } - } + } + emulationViewModel.isEmulationStopping.collect(viewLifecycleOwner) { + if (it) { + binding.loadingText.setText(R.string.shutting_down) + ViewUtils.showView(binding.loadingIndicator) + ViewUtils.hideView(binding.inputContainer) + ViewUtils.hideView(binding.showFpsText) } - launch { - repeatOnLifecycle(Lifecycle.State.CREATED) { - emulationViewModel.drawerOpen.collect { - if (it) { - binding.drawerLayout.open() - binding.inGameMenu.requestFocus() - } else { - binding.drawerLayout.close() - } - } - } + } + emulationViewModel.drawerOpen.collect(viewLifecycleOwner) { + if (it) { + binding.drawerLayout.open() + binding.inGameMenu.requestFocus() + } else { + binding.drawerLayout.close() } - launch { - repeatOnLifecycle(Lifecycle.State.CREATED) { - emulationViewModel.programChanged.collect { - if (it != 0) { - emulationViewModel.setEmulationStarted(false) - binding.drawerLayout.close() - binding.drawerLayout - .setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED) - ViewUtils.hideView(binding.surfaceInputOverlay) - ViewUtils.showView(binding.loadingIndicator) - } - } - } + } + emulationViewModel.programChanged.collect(viewLifecycleOwner) { + if (it != 0) { + emulationViewModel.setEmulationStarted(false) + binding.drawerLayout.close() + binding.drawerLayout + .setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED) + ViewUtils.hideView(binding.surfaceInputOverlay) + ViewUtils.showView(binding.loadingIndicator) } - launch { - repeatOnLifecycle(Lifecycle.State.CREATED) { - emulationViewModel.emulationStopped.collect { - if (it && emulationViewModel.programChanged.value != -1) { - if (perfStatsUpdater != null) { - perfStatsUpdateHandler.removeCallbacks(perfStatsUpdater!!) - } - emulationState.changeProgram(emulationViewModel.programChanged.value) - emulationViewModel.setProgramChanged(-1) - emulationViewModel.setEmulationStopped(false) - } - } + } + emulationViewModel.emulationStopped.collect(viewLifecycleOwner) { + if (it && emulationViewModel.programChanged.value != -1) { + if (perfStatsUpdater != null) { + perfStatsUpdateHandler.removeCallbacks(perfStatsUpdater!!) } + emulationState.changeProgram(emulationViewModel.programChanged.value) + emulationViewModel.setProgramChanged(-1) + emulationViewModel.setEmulationStopped(false) } } + + driverViewModel.isInteractionAllowed.collect(viewLifecycleOwner) { + if (it) startEmulation() + } } private fun startEmulation(programIndex: Int = 0) { @@ -491,14 +442,12 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { binding.drawerLayout.close() } if (showInputOverlay) { - binding.surfaceInputOverlay.visibility = View.INVISIBLE + binding.surfaceInputOverlay.setVisible(visible = false, gone = false) } } else { - if (showInputOverlay && emulationViewModel.emulationStarted.value) { - binding.surfaceInputOverlay.visibility = View.VISIBLE - } else { - binding.surfaceInputOverlay.visibility = View.INVISIBLE - } + binding.surfaceInputOverlay.setVisible( + showInputOverlay && emulationViewModel.emulationStarted.value + ) if (!isInFoldableLayout) { if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) { binding.surfaceInputOverlay.layout = OverlayLayout.Portrait @@ -535,7 +484,9 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { } private fun updateShowFpsOverlay() { - if (BooleanSetting.SHOW_PERFORMANCE_OVERLAY.getBoolean()) { + val showOverlay = BooleanSetting.SHOW_PERFORMANCE_OVERLAY.getBoolean() + binding.showFpsText.setVisible(showOverlay) + if (showOverlay) { val SYSTEM_FPS = 0 val FPS = 1 val FRAMETIME = 2 @@ -555,17 +506,17 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { } } perfStatsUpdateHandler.post(perfStatsUpdater!!) - binding.showFpsText.visibility = View.VISIBLE } else { if (perfStatsUpdater != null) { perfStatsUpdateHandler.removeCallbacks(perfStatsUpdater!!) } - binding.showFpsText.visibility = View.GONE } } private fun updateThermalOverlay() { - if (BooleanSetting.SHOW_THERMAL_OVERLAY.getBoolean()) { + val showOverlay = BooleanSetting.SHOW_THERMAL_OVERLAY.getBoolean() + binding.showThermalsText.setVisible(showOverlay) + if (showOverlay) { thermalStatsUpdater = { if (emulationViewModel.emulationStarted.value && !emulationViewModel.isEmulationStopping.value @@ -587,12 +538,10 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { } } thermalStatsUpdateHandler.post(thermalStatsUpdater!!) - binding.showThermalsText.visibility = View.VISIBLE } else { if (thermalStatsUpdater != null) { thermalStatsUpdateHandler.removeCallbacks(thermalStatsUpdater!!) } - binding.showThermalsText.visibility = View.GONE } } @@ -861,12 +810,12 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { } } } - binding.doneControlConfig.visibility = View.VISIBLE + binding.doneControlConfig.setVisible(false) binding.surfaceInputOverlay.setIsInEditMode(true) } private fun stopConfiguringControls() { - binding.doneControlConfig.visibility = View.GONE + binding.doneControlConfig.setVisible(false) binding.surfaceInputOverlay.setIsInEditMode(false) // Unlock the orientation if it was locked for editing if (IntSetting.RENDERER_SCREEN_LAYOUT.getInt() == EmulationOrientation.Unspecified.int) { diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameFoldersFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameFoldersFragment.kt index 5c558b1a5..3a6f7a38c 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameFoldersFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameFoldersFragment.kt @@ -13,9 +13,6 @@ 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.recyclerview.widget.GridLayoutManager import com.google.android.material.transition.MaterialSharedAxis @@ -27,6 +24,7 @@ import org.yuzu.yuzu_emu.model.GamesViewModel import org.yuzu.yuzu_emu.model.HomeViewModel import org.yuzu.yuzu_emu.ui.main.MainActivity import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins +import org.yuzu.yuzu_emu.utils.collect class GameFoldersFragment : Fragment() { private var _binding: FragmentFoldersBinding? = null @@ -70,12 +68,8 @@ class GameFoldersFragment : Fragment() { adapter = FolderAdapter(requireActivity(), gamesViewModel) } - viewLifecycleOwner.lifecycleScope.launch { - repeatOnLifecycle(Lifecycle.State.CREATED) { - gamesViewModel.folders.collect { - (binding.listFolders.adapter as FolderAdapter).submitList(it) - } - } + gamesViewModel.folders.collect(viewLifecycleOwner) { + (binding.listFolders.adapter as FolderAdapter).submitList(it) } val mainActivity = requireActivity() as MainActivity diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameInfoFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameInfoFragment.kt index dbd56e84f..97a8954bb 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameInfoFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameInfoFragment.kt @@ -27,6 +27,7 @@ import org.yuzu.yuzu_emu.databinding.FragmentGameInfoBinding import org.yuzu.yuzu_emu.model.GameVerificationResult import org.yuzu.yuzu_emu.model.HomeViewModel import org.yuzu.yuzu_emu.utils.GameMetadata +import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins class GameInfoFragment : Fragment() { @@ -85,7 +86,7 @@ class GameInfoFragment : Fragment() { copyToClipboard(getString(R.string.developer), args.game.developer) } } else { - developer.visibility = View.GONE + developer.setVisible(false) } version.setHint(R.string.version) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt index 3ea5e16ca..c06842c59 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt @@ -3,11 +3,9 @@ package org.yuzu.yuzu_emu.fragments -import android.annotation.SuppressLint import android.content.pm.ShortcutInfo import android.content.pm.ShortcutManager import android.os.Bundle -import android.text.TextUtils import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -18,9 +16,7 @@ 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.navigation.fragment.navArgs import androidx.recyclerview.widget.GridLayoutManager @@ -46,7 +42,9 @@ import org.yuzu.yuzu_emu.utils.FileUtil import org.yuzu.yuzu_emu.utils.GameIconUtils import org.yuzu.yuzu_emu.utils.GpuDriverHelper import org.yuzu.yuzu_emu.utils.MemoryUtil +import org.yuzu.yuzu_emu.utils.ViewUtils.marquee import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins +import org.yuzu.yuzu_emu.utils.collect import java.io.BufferedOutputStream import java.io.File @@ -76,8 +74,6 @@ class GamePropertiesFragment : Fragment() { return binding.root } - // This is using the correct scope, lint is just acting up - @SuppressLint("UnsafeRepeatOnLifecycleDetector") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) homeViewModel.setNavigationVisibility(visible = false, animated = true) @@ -107,13 +103,7 @@ class GamePropertiesFragment : Fragment() { GameIconUtils.loadGameIcon(args.game, binding.imageGameScreen) binding.title.text = args.game.title - binding.title.postDelayed( - { - binding.title.ellipsize = TextUtils.TruncateAt.MARQUEE - binding.title.isSelected = true - }, - 3000 - ) + binding.title.marquee() binding.buttonStart.setOnClickListener { LaunchGameDialogFragment.newInstance(args.game) @@ -122,28 +112,14 @@ class GamePropertiesFragment : Fragment() { reloadList() - viewLifecycleOwner.lifecycleScope.apply { - launch { - repeatOnLifecycle(Lifecycle.State.STARTED) { - homeViewModel.openImportSaves.collect { - if (it) { - importSaves.launch(arrayOf("application/zip")) - homeViewModel.setOpenImportSaves(false) - } - } - } - } - launch { - repeatOnLifecycle(Lifecycle.State.STARTED) { - homeViewModel.reloadPropertiesList.collect { - if (it) { - reloadList() - homeViewModel.reloadPropertiesList(false) - } - } - } - } - } + homeViewModel.openImportSaves.collect( + viewLifecycleOwner, + resetState = { homeViewModel.setOpenImportSaves(false) } + ) { if (it) importSaves.launch(arrayOf("application/zip")) } + homeViewModel.reloadPropertiesList.collect( + viewLifecycleOwner, + resetState = { homeViewModel.reloadPropertiesList(false) } + ) { if (it) reloadList() } setInsets() } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt index 87e130d3e..14a2504b6 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt @@ -91,6 +91,20 @@ class HomeSettingsFragment : Fragment() { ) add( HomeSetting( + R.string.preferences_controls, + R.string.preferences_controls_description, + R.drawable.ic_controller, + { + val action = HomeNavigationDirections.actionGlobalSettingsActivity( + null, + Settings.MenuTag.SECTION_INPUT + ) + binding.root.findNavController().navigate(action) + } + ) + ) + add( + HomeSetting( R.string.gpu_driver_manager, R.string.install_gpu_driver_description, R.drawable.ic_build, diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt index 63112dc6f..d218da1c8 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt @@ -14,9 +14,6 @@ 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.recyclerview.widget.GridLayoutManager import com.google.android.material.transition.MaterialSharedAxis @@ -35,6 +32,7 @@ import org.yuzu.yuzu_emu.ui.main.MainActivity import org.yuzu.yuzu_emu.utils.DirectoryInitialization import org.yuzu.yuzu_emu.utils.FileUtil import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins +import org.yuzu.yuzu_emu.utils.collect import java.io.BufferedOutputStream import java.io.File import java.math.BigInteger @@ -75,14 +73,10 @@ class InstallableFragment : Fragment() { binding.root.findNavController().popBackStack() } - viewLifecycleOwner.lifecycleScope.launch { - repeatOnLifecycle(Lifecycle.State.CREATED) { - homeViewModel.openImportSaves.collect { - if (it) { - importSaves.launch(arrayOf("application/zip")) - homeViewModel.setOpenImportSaves(false) - } - } + homeViewModel.openImportSaves.collect(viewLifecycleOwner) { + if (it) { + importSaves.launch(arrayOf("application/zip")) + homeViewModel.setOpenImportSaves(false) } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ProgressDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ProgressDialogFragment.kt index d201cb80c..ee3bb0386 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ProgressDialogFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ProgressDialogFragment.kt @@ -13,15 +13,13 @@ import androidx.appcompat.app.AlertDialog import androidx.fragment.app.DialogFragment import androidx.fragment.app.FragmentActivity import androidx.fragment.app.activityViewModels -import androidx.lifecycle.Lifecycle import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle import com.google.android.material.dialog.MaterialAlertDialogBuilder -import kotlinx.coroutines.launch import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding import org.yuzu.yuzu_emu.model.TaskViewModel +import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible +import org.yuzu.yuzu_emu.utils.collect class ProgressDialogFragment : DialogFragment() { private val taskViewModel: TaskViewModel by activityViewModels() @@ -64,72 +62,50 @@ class ProgressDialogFragment : DialogFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) binding.message.isSelected = true - viewLifecycleOwner.lifecycleScope.apply { - launch { - repeatOnLifecycle(Lifecycle.State.CREATED) { - taskViewModel.isComplete.collect { - if (it) { - dismiss() - when (val result = taskViewModel.result.value) { - is String -> Toast.makeText( - requireContext(), - result, - Toast.LENGTH_LONG - ).show() - - is MessageDialogFragment -> result.show( - requireActivity().supportFragmentManager, - MessageDialogFragment.TAG - ) - - else -> { - // Do nothing - } - } - taskViewModel.clear() - } + taskViewModel.isComplete.collect(viewLifecycleOwner) { + if (it) { + dismiss() + when (val result = taskViewModel.result.value) { + is String -> Toast.makeText( + requireContext(), + result, + Toast.LENGTH_LONG + ).show() + + is MessageDialogFragment -> result.show( + requireActivity().supportFragmentManager, + MessageDialogFragment.TAG + ) + + else -> { + // Do nothing } } + taskViewModel.clear() } - launch { - repeatOnLifecycle(Lifecycle.State.CREATED) { - taskViewModel.cancelled.collect { - if (it) { - dialog?.setTitle(R.string.cancelling) - } - } - } - } - launch { - repeatOnLifecycle(Lifecycle.State.CREATED) { - taskViewModel.progress.collect { - if (it != 0.0) { - binding.progressBar.apply { - isIndeterminate = false - progress = ( - (it / taskViewModel.maxProgress.value) * - PROGRESS_BAR_RESOLUTION - ).toInt() - min = 0 - max = PROGRESS_BAR_RESOLUTION - } - } - } - } + } + taskViewModel.cancelled.collect(viewLifecycleOwner) { + if (it) { + dialog?.setTitle(R.string.cancelling) } - launch { - repeatOnLifecycle(Lifecycle.State.CREATED) { - taskViewModel.message.collect { - if (it.isEmpty()) { - binding.message.visibility = View.GONE - } else { - binding.message.visibility = View.VISIBLE - binding.message.text = it - } - } + } + taskViewModel.progress.collect(viewLifecycleOwner) { + if (it != 0.0) { + binding.progressBar.apply { + isIndeterminate = false + progress = ( + (it / taskViewModel.maxProgress.value) * + PROGRESS_BAR_RESOLUTION + ).toInt() + min = 0 + max = PROGRESS_BAR_RESOLUTION } } } + taskViewModel.message.collect(viewLifecycleOwner) { + binding.message.setVisible(it.isNotEmpty()) + binding.message.text = it + } } // By default, the ProgressDialog will immediately dismiss itself upon a button being pressed. diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SearchFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SearchFragment.kt index 20b10b1a0..662ae9760 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SearchFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SearchFragment.kt @@ -3,7 +3,6 @@ package org.yuzu.yuzu_emu.fragments -import android.annotation.SuppressLint import android.content.Context import android.content.SharedPreferences import android.os.Bundle @@ -18,14 +17,9 @@ import androidx.core.view.updatePadding import androidx.core.widget.doOnTextChanged import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle import androidx.preference.PreferenceManager import info.debatty.java.stringsimilarity.Jaccard import info.debatty.java.stringsimilarity.JaroWinkler -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.launch import java.util.Locale import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.YuzuApplication @@ -35,6 +29,8 @@ import org.yuzu.yuzu_emu.layout.AutofitGridLayoutManager import org.yuzu.yuzu_emu.model.Game import org.yuzu.yuzu_emu.model.GamesViewModel import org.yuzu.yuzu_emu.model.HomeViewModel +import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible +import org.yuzu.yuzu_emu.utils.collect class SearchFragment : Fragment() { private var _binding: FragmentSearchBinding? = null @@ -58,8 +54,6 @@ class SearchFragment : Fragment() { return binding.root } - // This is using the correct scope, lint is just acting up - @SuppressLint("UnsafeRepeatOnLifecycleDetector") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) homeViewModel.setNavigationVisibility(visible = true, animated = true) @@ -81,42 +75,18 @@ class SearchFragment : Fragment() { binding.chipGroup.setOnCheckedStateChangeListener { _, _ -> filterAndSearch() } binding.searchText.doOnTextChanged { text: CharSequence?, _: Int, _: Int, _: Int -> - if (text.toString().isNotEmpty()) { - binding.clearButton.visibility = View.VISIBLE - } else { - binding.clearButton.visibility = View.INVISIBLE - } + binding.clearButton.setVisible(text.toString().isNotEmpty()) filterAndSearch() } - viewLifecycleOwner.lifecycleScope.apply { - launch { - repeatOnLifecycle(Lifecycle.State.CREATED) { - gamesViewModel.searchFocused.collect { - if (it) { - focusSearch() - gamesViewModel.setSearchFocused(false) - } - } - } - } - launch { - repeatOnLifecycle(Lifecycle.State.CREATED) { - gamesViewModel.games.collectLatest { filterAndSearch() } - } - } - launch { - repeatOnLifecycle(Lifecycle.State.CREATED) { - gamesViewModel.searchedGames.collect { - (binding.gridGamesSearch.adapter as GameAdapter).submitList(it) - if (it.isEmpty()) { - binding.noResultsView.visibility = View.VISIBLE - } else { - binding.noResultsView.visibility = View.GONE - } - } - } - } + gamesViewModel.searchFocused.collect( + viewLifecycleOwner, + resetState = { gamesViewModel.setSearchFocused(false) } + ) { if (it) focusSearch() } + gamesViewModel.games.collect(viewLifecycleOwner) { filterAndSearch() } + gamesViewModel.searchedGames.collect(viewLifecycleOwner) { + (binding.gridGamesSearch.adapter as GameAdapter).submitList(it) + binding.noResultsView.setVisible(it.isNotEmpty()) } binding.clearButton.setOnClickListener { binding.searchText.setText("") } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt index ebf41a639..4f7548e98 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt @@ -4,7 +4,6 @@ package org.yuzu.yuzu_emu.fragments import android.Manifest -import android.annotation.SuppressLint import android.content.Intent import android.os.Build import android.os.Bundle @@ -23,9 +22,6 @@ import androidx.core.view.isVisible 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 androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback @@ -46,6 +42,8 @@ import org.yuzu.yuzu_emu.ui.main.MainActivity import org.yuzu.yuzu_emu.utils.DirectoryInitialization import org.yuzu.yuzu_emu.utils.NativeConfig import org.yuzu.yuzu_emu.utils.ViewUtils +import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible +import org.yuzu.yuzu_emu.utils.collect class SetupFragment : Fragment() { private var _binding: FragmentSetupBinding? = null @@ -77,8 +75,6 @@ class SetupFragment : Fragment() { return binding.root } - // This is using the correct scope, lint is just acting up - @SuppressLint("UnsafeRepeatOnLifecycleDetector") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { mainActivity = requireActivity() as MainActivity @@ -210,28 +206,14 @@ class SetupFragment : Fragment() { ) } - viewLifecycleOwner.lifecycleScope.apply { - launch { - repeatOnLifecycle(Lifecycle.State.CREATED) { - homeViewModel.shouldPageForward.collect { - if (it) { - pageForward() - homeViewModel.setShouldPageForward(false) - } - } - } - } - launch { - repeatOnLifecycle(Lifecycle.State.CREATED) { - homeViewModel.gamesDirSelected.collect { - if (it) { - gamesDirCallback.onStepCompleted() - homeViewModel.setGamesDirSelected(false) - } - } - } - } - } + homeViewModel.shouldPageForward.collect( + viewLifecycleOwner, + resetState = { homeViewModel.setShouldPageForward(false) } + ) { if (it) pageForward() } + homeViewModel.gamesDirSelected.collect( + viewLifecycleOwner, + resetState = { homeViewModel.setGamesDirSelected(false) } + ) { if (it) gamesDirCallback.onStepCompleted() } binding.viewPager2.apply { adapter = SetupAdapter(requireActivity() as AppCompatActivity, pages) @@ -292,12 +274,8 @@ class SetupFragment : Fragment() { val backIsVisible = savedInstanceState.getBoolean(KEY_BACK_VISIBILITY) hasBeenWarned = savedInstanceState.getBooleanArray(KEY_HAS_BEEN_WARNED)!! - if (nextIsVisible) { - binding.buttonNext.visibility = View.VISIBLE - } - if (backIsVisible) { - binding.buttonBack.visibility = View.VISIBLE - } + binding.buttonNext.setVisible(nextIsVisible) + binding.buttonBack.setVisible(backIsVisible) } else { hasBeenWarned = BooleanArray(pages.size) } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt index c87486c90..66907085a 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt @@ -24,10 +24,10 @@ import androidx.core.content.ContextCompat import androidx.window.layout.WindowMetricsCalculator import kotlin.math.max import kotlin.math.min -import org.yuzu.yuzu_emu.NativeLibrary -import org.yuzu.yuzu_emu.NativeLibrary.ButtonType -import org.yuzu.yuzu_emu.NativeLibrary.StickType +import org.yuzu.yuzu_emu.features.input.NativeInput import org.yuzu.yuzu_emu.R +import org.yuzu.yuzu_emu.features.input.model.NativeAnalog +import org.yuzu.yuzu_emu.features.input.model.NativeButton import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting import org.yuzu.yuzu_emu.features.settings.model.IntSetting import org.yuzu.yuzu_emu.overlay.model.OverlayControl @@ -100,19 +100,19 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : var shouldUpdateView = false val playerIndex = - if (NativeLibrary.isHandheldOnly()) { - NativeLibrary.ConsoleDevice + if (NativeInput.isHandheldOnly()) { + NativeInput.ConsoleDevice } else { - NativeLibrary.Player1Device + NativeInput.Player1Device } for (button in overlayButtons) { if (!button.updateStatus(event)) { continue } - NativeLibrary.onGamePadButtonEvent( + NativeInput.onOverlayButtonEvent( playerIndex, - button.buttonId, + button.button, button.status ) playHaptics(event) @@ -123,24 +123,24 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : if (!dpad.updateStatus(event, BooleanSetting.DPAD_SLIDE.getBoolean())) { continue } - NativeLibrary.onGamePadButtonEvent( + NativeInput.onOverlayButtonEvent( playerIndex, - dpad.upId, + dpad.up, dpad.upStatus ) - NativeLibrary.onGamePadButtonEvent( + NativeInput.onOverlayButtonEvent( playerIndex, - dpad.downId, + dpad.down, dpad.downStatus ) - NativeLibrary.onGamePadButtonEvent( + NativeInput.onOverlayButtonEvent( playerIndex, - dpad.leftId, + dpad.left, dpad.leftStatus ) - NativeLibrary.onGamePadButtonEvent( + NativeInput.onOverlayButtonEvent( playerIndex, - dpad.rightId, + dpad.right, dpad.rightStatus ) playHaptics(event) @@ -151,16 +151,15 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : if (!joystick.updateStatus(event)) { continue } - val axisID = joystick.joystickId - NativeLibrary.onGamePadJoystickEvent( + NativeInput.onOverlayJoystickEvent( playerIndex, - axisID, + joystick.joystick, joystick.xAxis, joystick.realYAxis ) - NativeLibrary.onGamePadButtonEvent( + NativeInput.onOverlayButtonEvent( playerIndex, - joystick.buttonId, + joystick.button, joystick.buttonStatus ) playHaptics(event) @@ -187,7 +186,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : motionEvent == MotionEvent.ACTION_UP || motionEvent == MotionEvent.ACTION_POINTER_UP if (isActionDown && !isTouchInputConsumed(pointerId)) { - NativeLibrary.onTouchPressed(pointerId, xPosition.toFloat(), yPosition.toFloat()) + NativeInput.onTouchPressed(pointerId, xPosition.toFloat(), yPosition.toFloat()) } if (isActionMove) { @@ -196,12 +195,12 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : if (isTouchInputConsumed(fingerId)) { continue } - NativeLibrary.onTouchMoved(fingerId, event.getX(i), event.getY(i)) + NativeInput.onTouchMoved(fingerId, event.getX(i), event.getY(i)) } } if (isActionUp && !isTouchInputConsumed(pointerId)) { - NativeLibrary.onTouchReleased(pointerId) + NativeInput.onTouchReleased(pointerId) } return true @@ -359,7 +358,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : windowSize, R.drawable.facebutton_a, R.drawable.facebutton_a_depressed, - ButtonType.BUTTON_A, + NativeButton.A, data, position ) @@ -373,7 +372,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : windowSize, R.drawable.facebutton_b, R.drawable.facebutton_b_depressed, - ButtonType.BUTTON_B, + NativeButton.B, data, position ) @@ -387,7 +386,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : windowSize, R.drawable.facebutton_x, R.drawable.facebutton_x_depressed, - ButtonType.BUTTON_X, + NativeButton.X, data, position ) @@ -401,7 +400,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : windowSize, R.drawable.facebutton_y, R.drawable.facebutton_y_depressed, - ButtonType.BUTTON_Y, + NativeButton.Y, data, position ) @@ -415,7 +414,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : windowSize, R.drawable.facebutton_plus, R.drawable.facebutton_plus_depressed, - ButtonType.BUTTON_PLUS, + NativeButton.Plus, data, position ) @@ -429,7 +428,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : windowSize, R.drawable.facebutton_minus, R.drawable.facebutton_minus_depressed, - ButtonType.BUTTON_MINUS, + NativeButton.Minus, data, position ) @@ -443,7 +442,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : windowSize, R.drawable.facebutton_home, R.drawable.facebutton_home_depressed, - ButtonType.BUTTON_HOME, + NativeButton.Home, data, position ) @@ -457,7 +456,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : windowSize, R.drawable.facebutton_screenshot, R.drawable.facebutton_screenshot_depressed, - ButtonType.BUTTON_CAPTURE, + NativeButton.Capture, data, position ) @@ -471,7 +470,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : windowSize, R.drawable.l_shoulder, R.drawable.l_shoulder_depressed, - ButtonType.TRIGGER_L, + NativeButton.L, data, position ) @@ -485,7 +484,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : windowSize, R.drawable.r_shoulder, R.drawable.r_shoulder_depressed, - ButtonType.TRIGGER_R, + NativeButton.R, data, position ) @@ -499,7 +498,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : windowSize, R.drawable.zl_trigger, R.drawable.zl_trigger_depressed, - ButtonType.TRIGGER_ZL, + NativeButton.ZL, data, position ) @@ -513,7 +512,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : windowSize, R.drawable.zr_trigger, R.drawable.zr_trigger_depressed, - ButtonType.TRIGGER_ZR, + NativeButton.ZR, data, position ) @@ -527,7 +526,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : windowSize, R.drawable.button_l3, R.drawable.button_l3_depressed, - ButtonType.STICK_L, + NativeButton.LStick, data, position ) @@ -541,7 +540,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : windowSize, R.drawable.button_r3, R.drawable.button_r3_depressed, - ButtonType.STICK_R, + NativeButton.RStick, data, position ) @@ -556,8 +555,8 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : R.drawable.joystick_range, R.drawable.joystick, R.drawable.joystick_depressed, - StickType.STICK_L, - ButtonType.STICK_L, + NativeAnalog.LStick, + NativeButton.LStick, data, position ) @@ -572,8 +571,8 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : R.drawable.joystick_range, R.drawable.joystick, R.drawable.joystick_depressed, - StickType.STICK_R, - ButtonType.STICK_R, + NativeAnalog.RStick, + NativeButton.RStick, data, position ) @@ -835,7 +834,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : windowSize: Pair<Point, Point>, defaultResId: Int, pressedResId: Int, - buttonId: Int, + button: NativeButton, overlayControlData: OverlayControlData, position: Pair<Double, Double> ): InputOverlayDrawableButton { @@ -869,7 +868,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : res, defaultStateBitmap, pressedStateBitmap, - buttonId, + button, overlayControlData ) @@ -940,11 +939,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : res, defaultStateBitmap, pressedOneDirectionStateBitmap, - pressedTwoDirectionsStateBitmap, - ButtonType.DPAD_UP, - ButtonType.DPAD_DOWN, - ButtonType.DPAD_LEFT, - ButtonType.DPAD_RIGHT + pressedTwoDirectionsStateBitmap ) // Get the minimum and maximum coordinates of the screen where the button can be placed. @@ -993,8 +988,8 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : resOuter: Int, defaultResInner: Int, pressedResInner: Int, - joystick: Int, - buttonId: Int, + joystick: NativeAnalog, + button: NativeButton, overlayControlData: OverlayControlData, position: Pair<Double, Double> ): InputOverlayDrawableJoystick { @@ -1042,7 +1037,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : outerRect, innerRect, joystick, - buttonId, + button, overlayControlData.id ) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableButton.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableButton.kt index b14a4f96e..fee3d04ee 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableButton.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableButton.kt @@ -9,7 +9,8 @@ import android.graphics.Canvas import android.graphics.Rect import android.graphics.drawable.BitmapDrawable import android.view.MotionEvent -import org.yuzu.yuzu_emu.NativeLibrary.ButtonState +import org.yuzu.yuzu_emu.features.input.NativeInput.ButtonState +import org.yuzu.yuzu_emu.features.input.model.NativeButton import org.yuzu.yuzu_emu.overlay.model.OverlayControlData /** @@ -19,13 +20,13 @@ import org.yuzu.yuzu_emu.overlay.model.OverlayControlData * @param res [Resources] instance. * @param defaultStateBitmap [Bitmap] to use with the default state Drawable. * @param pressedStateBitmap [Bitmap] to use with the pressed state Drawable. - * @param buttonId Identifier for this type of button. + * @param button [NativeButton] for this type of button. */ class InputOverlayDrawableButton( res: Resources, defaultStateBitmap: Bitmap, pressedStateBitmap: Bitmap, - val buttonId: Int, + val button: NativeButton, val overlayControlData: OverlayControlData ) { // The ID value what motion event is tracking diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableDpad.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableDpad.kt index 8aef6f5a5..0cb6ff244 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableDpad.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableDpad.kt @@ -9,7 +9,8 @@ import android.graphics.Canvas import android.graphics.Rect import android.graphics.drawable.BitmapDrawable import android.view.MotionEvent -import org.yuzu.yuzu_emu.NativeLibrary.ButtonState +import org.yuzu.yuzu_emu.features.input.NativeInput.ButtonState +import org.yuzu.yuzu_emu.features.input.model.NativeButton /** * Custom [BitmapDrawable] that is capable @@ -19,20 +20,12 @@ import org.yuzu.yuzu_emu.NativeLibrary.ButtonState * @param defaultStateBitmap [Bitmap] of the default state. * @param pressedOneDirectionStateBitmap [Bitmap] of the pressed state in one direction. * @param pressedTwoDirectionsStateBitmap [Bitmap] of the pressed state in two direction. - * @param buttonUp Identifier for the up button. - * @param buttonDown Identifier for the down button. - * @param buttonLeft Identifier for the left button. - * @param buttonRight Identifier for the right button. */ class InputOverlayDrawableDpad( res: Resources, defaultStateBitmap: Bitmap, pressedOneDirectionStateBitmap: Bitmap, - pressedTwoDirectionsStateBitmap: Bitmap, - buttonUp: Int, - buttonDown: Int, - buttonLeft: Int, - buttonRight: Int + pressedTwoDirectionsStateBitmap: Bitmap ) { /** * Gets one of the InputOverlayDrawableDpad's button IDs. @@ -40,10 +33,10 @@ class InputOverlayDrawableDpad( * @return the requested InputOverlayDrawableDpad's button ID. */ // The ID identifying what type of button this Drawable represents. - val upId: Int - val downId: Int - val leftId: Int - val rightId: Int + val up = NativeButton.DUp + val down = NativeButton.DDown + val left = NativeButton.DLeft + val right = NativeButton.DRight var trackId: Int val width: Int @@ -69,10 +62,6 @@ class InputOverlayDrawableDpad( this.pressedTwoDirectionsStateBitmap = BitmapDrawable(res, pressedTwoDirectionsStateBitmap) width = this.defaultStateBitmap.intrinsicWidth height = this.defaultStateBitmap.intrinsicHeight - upId = buttonUp - downId = buttonDown - leftId = buttonLeft - rightId = buttonRight trackId = -1 } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableJoystick.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableJoystick.kt index 113bf7c24..4b07107fc 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableJoystick.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableJoystick.kt @@ -13,7 +13,9 @@ import kotlin.math.atan2 import kotlin.math.cos import kotlin.math.sin import kotlin.math.sqrt -import org.yuzu.yuzu_emu.NativeLibrary +import org.yuzu.yuzu_emu.features.input.NativeInput.ButtonState +import org.yuzu.yuzu_emu.features.input.model.NativeAnalog +import org.yuzu.yuzu_emu.features.input.model.NativeButton import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting /** @@ -26,8 +28,8 @@ import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting * @param bitmapInnerPressed [Bitmap] which represents the pressed inner movable part of the joystick. * @param rectOuter [Rect] which represents the outer joystick bounds. * @param rectInner [Rect] which represents the inner joystick bounds. - * @param joystickId The ID value what type of joystick this Drawable represents. - * @param buttonId The ID value what type of button this Drawable represents. + * @param joystick The [NativeAnalog] this Drawable represents. + * @param button The [NativeButton] this Drawable represents. */ class InputOverlayDrawableJoystick( res: Resources, @@ -36,8 +38,8 @@ class InputOverlayDrawableJoystick( bitmapInnerPressed: Bitmap, rectOuter: Rect, rectInner: Rect, - val joystickId: Int, - val buttonId: Int, + val joystick: NativeAnalog, + val button: NativeButton, val prefId: String ) { // The ID value what motion event is tracking @@ -69,8 +71,7 @@ class InputOverlayDrawableJoystick( // TODO: Add button support val buttonStatus: Int - get() = - NativeLibrary.ButtonState.RELEASED + get() = ButtonState.RELEASED var bounds: Rect get() = outerBitmap.bounds set(bounds) { diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/GamesFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/GamesFragment.kt index 23ca49b53..fadb20e39 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/GamesFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/GamesFragment.kt @@ -3,7 +3,6 @@ package org.yuzu.yuzu_emu.ui -import android.annotation.SuppressLint import android.os.Bundle import android.view.LayoutInflater import android.view.View @@ -14,19 +13,16 @@ 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 com.google.android.material.color.MaterialColors -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.launch import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.adapters.GameAdapter import org.yuzu.yuzu_emu.databinding.FragmentGamesBinding import org.yuzu.yuzu_emu.layout.AutofitGridLayoutManager import org.yuzu.yuzu_emu.model.GamesViewModel import org.yuzu.yuzu_emu.model.HomeViewModel +import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins +import org.yuzu.yuzu_emu.utils.collect class GamesFragment : Fragment() { private var _binding: FragmentGamesBinding? = null @@ -44,8 +40,6 @@ class GamesFragment : Fragment() { return binding.root } - // This is using the correct scope, lint is just acting up - @SuppressLint("UnsafeRepeatOnLifecycleDetector") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) homeViewModel.setNavigationVisibility(visible = true, animated = true) @@ -88,49 +82,28 @@ class GamesFragment : Fragment() { } } - viewLifecycleOwner.lifecycleScope.apply { - launch { - repeatOnLifecycle(Lifecycle.State.RESUMED) { - gamesViewModel.isReloading.collect { - binding.swipeRefresh.isRefreshing = it - if (gamesViewModel.games.value.isEmpty() && !it) { - binding.noticeText.visibility = View.VISIBLE - } else { - binding.noticeText.visibility = View.INVISIBLE - } - } - } - } - launch { - repeatOnLifecycle(Lifecycle.State.RESUMED) { - gamesViewModel.games.collectLatest { - (binding.gridGames.adapter as GameAdapter).submitList(it) - } - } - } - launch { - repeatOnLifecycle(Lifecycle.State.RESUMED) { - gamesViewModel.shouldSwapData.collect { - if (it) { - (binding.gridGames.adapter as GameAdapter).submitList( - gamesViewModel.games.value - ) - gamesViewModel.setShouldSwapData(false) - } - } - } - } - launch { - repeatOnLifecycle(Lifecycle.State.RESUMED) { - gamesViewModel.shouldScrollToTop.collect { - if (it) { - scrollToTop() - gamesViewModel.setShouldScrollToTop(false) - } - } - } + gamesViewModel.isReloading.collect(viewLifecycleOwner) { + binding.swipeRefresh.isRefreshing = it + binding.noticeText.setVisible( + visible = gamesViewModel.games.value.isEmpty() && !it, + gone = false + ) + } + gamesViewModel.games.collect(viewLifecycleOwner) { + (binding.gridGames.adapter as GameAdapter).submitList(it) + } + gamesViewModel.shouldSwapData.collect( + viewLifecycleOwner, + resetState = { gamesViewModel.setShouldSwapData(false) } + ) { + if (it) { + (binding.gridGames.adapter as GameAdapter).submitList(gamesViewModel.games.value) } } + gamesViewModel.shouldScrollToTop.collect( + viewLifecycleOwner, + resetState = { gamesViewModel.setShouldScrollToTop(false) } + ) { if (it) scrollToTop() } setInsets() } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt index 4df4ac4c6..757463a0b 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt @@ -19,9 +19,6 @@ import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.view.ViewCompat import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle import androidx.navigation.NavController import androidx.navigation.fragment.NavHostFragment import androidx.navigation.ui.setupWithNavController @@ -30,7 +27,6 @@ import com.google.android.material.color.MaterialColors import com.google.android.material.navigation.NavigationBarView import java.io.File import java.io.FilenameFilter -import kotlinx.coroutines.launch import org.yuzu.yuzu_emu.HomeNavigationDirections import org.yuzu.yuzu_emu.NativeLibrary import org.yuzu.yuzu_emu.R @@ -47,6 +43,7 @@ import org.yuzu.yuzu_emu.model.InstallResult import org.yuzu.yuzu_emu.model.TaskState import org.yuzu.yuzu_emu.model.TaskViewModel import org.yuzu.yuzu_emu.utils.* +import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible import java.io.BufferedInputStream import java.io.BufferedOutputStream import java.util.zip.ZipEntry @@ -139,42 +136,23 @@ class MainActivity : AppCompatActivity(), ThemeProvider { // Prevents navigation from being drawn for a short time on recreation if set to hidden if (!homeViewModel.navigationVisible.value.first) { - binding.navigationView.visibility = View.INVISIBLE - binding.statusBarShade.visibility = View.INVISIBLE + binding.navigationView.setVisible(visible = false, gone = false) + binding.statusBarShade.setVisible(visible = false, gone = false) } - lifecycleScope.apply { - launch { - repeatOnLifecycle(Lifecycle.State.CREATED) { - homeViewModel.navigationVisible.collect { showNavigation(it.first, it.second) } - } - } - launch { - repeatOnLifecycle(Lifecycle.State.CREATED) { - homeViewModel.statusBarShadeVisible.collect { showStatusBarShade(it) } - } - } - launch { - repeatOnLifecycle(Lifecycle.State.CREATED) { - homeViewModel.contentToInstall.collect { - if (it != null) { - installContent(it) - homeViewModel.setContentToInstall(null) - } - } - } - } - launch { - repeatOnLifecycle(Lifecycle.State.CREATED) { - homeViewModel.checkKeys.collect { - if (it) { - checkKeys() - homeViewModel.setCheckKeys(false) - } - } - } + homeViewModel.navigationVisible.collect(this) { showNavigation(it.first, it.second) } + homeViewModel.statusBarShadeVisible.collect(this) { showStatusBarShade(it) } + homeViewModel.contentToInstall.collect( + this, + resetState = { homeViewModel.setContentToInstall(null) } + ) { + if (it != null) { + installContent(it) } } + homeViewModel.checkKeys.collect(this, resetState = { homeViewModel.setCheckKeys(false) }) { + if (it) checkKeys() + } setInsets() } @@ -214,18 +192,14 @@ class MainActivity : AppCompatActivity(), ThemeProvider { private fun showNavigation(visible: Boolean, animated: Boolean) { if (!animated) { - if (visible) { - binding.navigationView.visibility = View.VISIBLE - } else { - binding.navigationView.visibility = View.INVISIBLE - } + binding.navigationView.setVisible(visible) return } val smallLayout = resources.getBoolean(R.bool.small_layout) binding.navigationView.animate().apply { if (visible) { - binding.navigationView.visibility = View.VISIBLE + binding.navigationView.setVisible(true) duration = 300 interpolator = PathInterpolator(0.05f, 0.7f, 0.1f, 1f) @@ -264,7 +238,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider { } }.withEndAction { if (!visible) { - binding.navigationView.visibility = View.INVISIBLE + binding.navigationView.setVisible(visible = false, gone = false) } }.start() } @@ -272,7 +246,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider { private fun showStatusBarShade(visible: Boolean) { binding.statusBarShade.animate().apply { if (visible) { - binding.statusBarShade.visibility = View.VISIBLE + binding.statusBarShade.setVisible(true) binding.statusBarShade.translationY = binding.statusBarShade.height.toFloat() * -2 duration = 300 translationY(0f) @@ -284,7 +258,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider { } }.withEndAction { if (!visible) { - binding.statusBarShade.visibility = View.INVISIBLE + binding.statusBarShade.setVisible(visible = false, gone = false) } }.start() } @@ -524,7 +498,8 @@ class MainActivity : AppCompatActivity(), ThemeProvider { this@MainActivity, titleId = R.string.content_install_notice, descriptionId = R.string.content_install_notice_description, - positiveAction = { homeViewModel.setContentToInstall(documents) } + positiveAction = { homeViewModel.setContentToInstall(documents) }, + negativeAction = {} ) } }.show(supportFragmentManager, ProgressDialogFragment.TAG) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/InputHandler.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/InputHandler.kt index e63382e1d..2c7356e6a 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/InputHandler.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/InputHandler.kt @@ -6,439 +6,89 @@ package org.yuzu.yuzu_emu.utils import android.view.InputDevice import android.view.KeyEvent import android.view.MotionEvent -import kotlin.math.sqrt -import org.yuzu.yuzu_emu.NativeLibrary +import org.yuzu.yuzu_emu.features.input.NativeInput +import org.yuzu.yuzu_emu.features.input.YuzuInputOverlayDevice +import org.yuzu.yuzu_emu.features.input.YuzuPhysicalDevice object InputHandler { - private var controllerIds = getGameControllerIds() - - fun initialize() { - // Connect first controller - NativeLibrary.onGamePadConnectEvent(getPlayerNumber(NativeLibrary.Player1Device)) - } - - fun updateControllerIds() { - controllerIds = getGameControllerIds() - } + var androidControllers = mapOf<Int, YuzuPhysicalDevice>() + var registeredControllers = mutableListOf<ParamPackage>() fun dispatchKeyEvent(event: KeyEvent): Boolean { - val button: Int = when (event.device.vendorId) { - 0x045E -> getInputXboxButtonKey(event.keyCode) - 0x054C -> getInputDS5ButtonKey(event.keyCode) - 0x057E -> getInputJoyconButtonKey(event.keyCode) - 0x1532 -> getInputRazerButtonKey(event.keyCode) - 0x3537 -> getInputRedmagicButtonKey(event.keyCode) - 0x358A -> getInputBackboneLabsButtonKey(event.keyCode) - else -> getInputGenericButtonKey(event.keyCode) - } - val action = when (event.action) { - KeyEvent.ACTION_DOWN -> NativeLibrary.ButtonState.PRESSED - KeyEvent.ACTION_UP -> NativeLibrary.ButtonState.RELEASED + KeyEvent.ACTION_DOWN -> NativeInput.ButtonState.PRESSED + KeyEvent.ACTION_UP -> NativeInput.ButtonState.RELEASED else -> return false } - // Ignore invalid buttons - if (button < 0) { - return false + var controllerData = androidControllers[event.device.controllerNumber] + if (controllerData == null) { + updateControllerData() + controllerData = androidControllers[event.device.controllerNumber] ?: return false } - return NativeLibrary.onGamePadButtonEvent( - getPlayerNumber(event.device.controllerNumber, event.deviceId), - button, + NativeInput.onGamePadButtonEvent( + controllerData.getGUID(), + controllerData.getPort(), + event.keyCode, action ) + return true } fun dispatchGenericMotionEvent(event: MotionEvent): Boolean { - val device = event.device - // Check every axis input available on the controller - for (range in device.motionRanges) { - val axis = range.axis - when (device.vendorId) { - 0x045E -> setGenericAxisInput(event, axis) - 0x054C -> setGenericAxisInput(event, axis) - 0x057E -> setJoyconAxisInput(event, axis) - 0x1532 -> setRazerAxisInput(event, axis) - else -> setGenericAxisInput(event, axis) - } + val controllerData = + androidControllers[event.device.controllerNumber] ?: return false + event.device.motionRanges.forEach { + NativeInput.onGamePadAxisEvent( + controllerData.getGUID(), + controllerData.getPort(), + it.axis, + event.getAxisValue(it.axis) + ) } - return true } - private fun getPlayerNumber(index: Int, deviceId: Int = -1): Int { - var deviceIndex = index - if (deviceId != -1) { - deviceIndex = controllerIds[deviceId] ?: 0 - } - - // TODO: Joycons are handled as different controllers. Find a way to merge them. - return when (deviceIndex) { - 2 -> NativeLibrary.Player2Device - 3 -> NativeLibrary.Player3Device - 4 -> NativeLibrary.Player4Device - 5 -> NativeLibrary.Player5Device - 6 -> NativeLibrary.Player6Device - 7 -> NativeLibrary.Player7Device - 8 -> NativeLibrary.Player8Device - else -> if (NativeLibrary.isHandheldOnly()) { - NativeLibrary.ConsoleDevice - } else { - NativeLibrary.Player1Device - } - } - } - - private fun setStickState(playerNumber: Int, index: Int, xAxis: Float, yAxis: Float) { - // Calculate vector size - val r2 = xAxis * xAxis + yAxis * yAxis - var r = sqrt(r2.toDouble()).toFloat() - - // Adjust range of joystick - val deadzone = 0.15f - var x = xAxis - var y = yAxis - - if (r > deadzone) { - val deadzoneFactor = 1.0f / r * (r - deadzone) / (1.0f - deadzone) - x *= deadzoneFactor - y *= deadzoneFactor - r *= deadzoneFactor - } else { - x = 0.0f - y = 0.0f - } - - // Normalize joystick - if (r > 1.0f) { - x /= r - y /= r - } - - NativeLibrary.onGamePadJoystickEvent( - playerNumber, - index, - x, - -y - ) - } - - private fun getAxisToButton(axis: Float): Int { - return if (axis > 0.5f) { - NativeLibrary.ButtonState.PRESSED - } else { - NativeLibrary.ButtonState.RELEASED - } - } - - private fun setAxisDpadState(playerNumber: Int, xAxis: Float, yAxis: Float) { - NativeLibrary.onGamePadButtonEvent( - playerNumber, - NativeLibrary.ButtonType.DPAD_UP, - getAxisToButton(-yAxis) - ) - NativeLibrary.onGamePadButtonEvent( - playerNumber, - NativeLibrary.ButtonType.DPAD_DOWN, - getAxisToButton(yAxis) - ) - NativeLibrary.onGamePadButtonEvent( - playerNumber, - NativeLibrary.ButtonType.DPAD_LEFT, - getAxisToButton(-xAxis) - ) - NativeLibrary.onGamePadButtonEvent( - playerNumber, - NativeLibrary.ButtonType.DPAD_RIGHT, - getAxisToButton(xAxis) - ) - } - - private fun getInputDS5ButtonKey(key: Int): Int { - // The missing ds5 buttons are axis - return when (key) { - KeyEvent.KEYCODE_BUTTON_A -> NativeLibrary.ButtonType.BUTTON_B - KeyEvent.KEYCODE_BUTTON_B -> NativeLibrary.ButtonType.BUTTON_A - KeyEvent.KEYCODE_BUTTON_X -> NativeLibrary.ButtonType.BUTTON_Y - KeyEvent.KEYCODE_BUTTON_Y -> NativeLibrary.ButtonType.BUTTON_X - KeyEvent.KEYCODE_BUTTON_L1 -> NativeLibrary.ButtonType.TRIGGER_L - KeyEvent.KEYCODE_BUTTON_R1 -> NativeLibrary.ButtonType.TRIGGER_R - KeyEvent.KEYCODE_BUTTON_THUMBL -> NativeLibrary.ButtonType.STICK_L - KeyEvent.KEYCODE_BUTTON_THUMBR -> NativeLibrary.ButtonType.STICK_R - KeyEvent.KEYCODE_BUTTON_START -> NativeLibrary.ButtonType.BUTTON_PLUS - KeyEvent.KEYCODE_BUTTON_SELECT -> NativeLibrary.ButtonType.BUTTON_MINUS - else -> -1 - } - } - - private fun getInputJoyconButtonKey(key: Int): Int { - // Joycon support is half dead. A lot of buttons can't be mapped - return when (key) { - KeyEvent.KEYCODE_BUTTON_A -> NativeLibrary.ButtonType.BUTTON_B - KeyEvent.KEYCODE_BUTTON_B -> NativeLibrary.ButtonType.BUTTON_A - KeyEvent.KEYCODE_BUTTON_X -> NativeLibrary.ButtonType.BUTTON_X - KeyEvent.KEYCODE_BUTTON_Y -> NativeLibrary.ButtonType.BUTTON_Y - KeyEvent.KEYCODE_DPAD_UP -> NativeLibrary.ButtonType.DPAD_UP - KeyEvent.KEYCODE_DPAD_DOWN -> NativeLibrary.ButtonType.DPAD_DOWN - KeyEvent.KEYCODE_DPAD_LEFT -> NativeLibrary.ButtonType.DPAD_LEFT - KeyEvent.KEYCODE_DPAD_RIGHT -> NativeLibrary.ButtonType.DPAD_RIGHT - KeyEvent.KEYCODE_BUTTON_L1 -> NativeLibrary.ButtonType.TRIGGER_L - KeyEvent.KEYCODE_BUTTON_R1 -> NativeLibrary.ButtonType.TRIGGER_R - KeyEvent.KEYCODE_BUTTON_L2 -> NativeLibrary.ButtonType.TRIGGER_ZL - KeyEvent.KEYCODE_BUTTON_R2 -> NativeLibrary.ButtonType.TRIGGER_ZR - KeyEvent.KEYCODE_BUTTON_THUMBL -> NativeLibrary.ButtonType.STICK_L - KeyEvent.KEYCODE_BUTTON_THUMBR -> NativeLibrary.ButtonType.STICK_R - KeyEvent.KEYCODE_BUTTON_START -> NativeLibrary.ButtonType.BUTTON_PLUS - KeyEvent.KEYCODE_BUTTON_SELECT -> NativeLibrary.ButtonType.BUTTON_MINUS - else -> -1 - } - } - - private fun getInputXboxButtonKey(key: Int): Int { - // The missing xbox buttons are axis - return when (key) { - KeyEvent.KEYCODE_BUTTON_A -> NativeLibrary.ButtonType.BUTTON_A - KeyEvent.KEYCODE_BUTTON_B -> NativeLibrary.ButtonType.BUTTON_B - KeyEvent.KEYCODE_BUTTON_X -> NativeLibrary.ButtonType.BUTTON_X - KeyEvent.KEYCODE_BUTTON_Y -> NativeLibrary.ButtonType.BUTTON_Y - KeyEvent.KEYCODE_BUTTON_L1 -> NativeLibrary.ButtonType.TRIGGER_L - KeyEvent.KEYCODE_BUTTON_R1 -> NativeLibrary.ButtonType.TRIGGER_R - KeyEvent.KEYCODE_BUTTON_THUMBL -> NativeLibrary.ButtonType.STICK_L - KeyEvent.KEYCODE_BUTTON_THUMBR -> NativeLibrary.ButtonType.STICK_R - KeyEvent.KEYCODE_BUTTON_START -> NativeLibrary.ButtonType.BUTTON_PLUS - KeyEvent.KEYCODE_BUTTON_SELECT -> NativeLibrary.ButtonType.BUTTON_MINUS - else -> -1 - } - } - - private fun getInputRazerButtonKey(key: Int): Int { - // The missing xbox buttons are axis - return when (key) { - KeyEvent.KEYCODE_BUTTON_A -> NativeLibrary.ButtonType.BUTTON_B - KeyEvent.KEYCODE_BUTTON_B -> NativeLibrary.ButtonType.BUTTON_A - KeyEvent.KEYCODE_BUTTON_X -> NativeLibrary.ButtonType.BUTTON_Y - KeyEvent.KEYCODE_BUTTON_Y -> NativeLibrary.ButtonType.BUTTON_X - KeyEvent.KEYCODE_BUTTON_L1 -> NativeLibrary.ButtonType.TRIGGER_L - KeyEvent.KEYCODE_BUTTON_R1 -> NativeLibrary.ButtonType.TRIGGER_R - KeyEvent.KEYCODE_BUTTON_THUMBL -> NativeLibrary.ButtonType.STICK_L - KeyEvent.KEYCODE_BUTTON_THUMBR -> NativeLibrary.ButtonType.STICK_R - KeyEvent.KEYCODE_BUTTON_START -> NativeLibrary.ButtonType.BUTTON_PLUS - KeyEvent.KEYCODE_BUTTON_SELECT -> NativeLibrary.ButtonType.BUTTON_MINUS - else -> -1 - } - } - - private fun getInputRedmagicButtonKey(key: Int): Int { - return when (key) { - KeyEvent.KEYCODE_BUTTON_A -> NativeLibrary.ButtonType.BUTTON_B - KeyEvent.KEYCODE_BUTTON_B -> NativeLibrary.ButtonType.BUTTON_A - KeyEvent.KEYCODE_BUTTON_X -> NativeLibrary.ButtonType.BUTTON_Y - KeyEvent.KEYCODE_BUTTON_Y -> NativeLibrary.ButtonType.BUTTON_X - KeyEvent.KEYCODE_BUTTON_L1 -> NativeLibrary.ButtonType.TRIGGER_L - KeyEvent.KEYCODE_BUTTON_R1 -> NativeLibrary.ButtonType.TRIGGER_R - KeyEvent.KEYCODE_BUTTON_L2 -> NativeLibrary.ButtonType.TRIGGER_ZL - KeyEvent.KEYCODE_BUTTON_R2 -> NativeLibrary.ButtonType.TRIGGER_ZR - KeyEvent.KEYCODE_BUTTON_THUMBL -> NativeLibrary.ButtonType.STICK_L - KeyEvent.KEYCODE_BUTTON_THUMBR -> NativeLibrary.ButtonType.STICK_R - KeyEvent.KEYCODE_BUTTON_START -> NativeLibrary.ButtonType.BUTTON_PLUS - KeyEvent.KEYCODE_BUTTON_SELECT -> NativeLibrary.ButtonType.BUTTON_MINUS - else -> -1 - } - } - - private fun getInputBackboneLabsButtonKey(key: Int): Int { - return when (key) { - KeyEvent.KEYCODE_BUTTON_A -> NativeLibrary.ButtonType.BUTTON_B - KeyEvent.KEYCODE_BUTTON_B -> NativeLibrary.ButtonType.BUTTON_A - KeyEvent.KEYCODE_BUTTON_X -> NativeLibrary.ButtonType.BUTTON_Y - KeyEvent.KEYCODE_BUTTON_Y -> NativeLibrary.ButtonType.BUTTON_X - KeyEvent.KEYCODE_BUTTON_L1 -> NativeLibrary.ButtonType.TRIGGER_L - KeyEvent.KEYCODE_BUTTON_R1 -> NativeLibrary.ButtonType.TRIGGER_R - KeyEvent.KEYCODE_BUTTON_L2 -> NativeLibrary.ButtonType.TRIGGER_ZL - KeyEvent.KEYCODE_BUTTON_R2 -> NativeLibrary.ButtonType.TRIGGER_ZR - KeyEvent.KEYCODE_BUTTON_THUMBL -> NativeLibrary.ButtonType.STICK_L - KeyEvent.KEYCODE_BUTTON_THUMBR -> NativeLibrary.ButtonType.STICK_R - KeyEvent.KEYCODE_BUTTON_START -> NativeLibrary.ButtonType.BUTTON_PLUS - KeyEvent.KEYCODE_BUTTON_SELECT -> NativeLibrary.ButtonType.BUTTON_MINUS - else -> -1 - } - } - - private fun getInputGenericButtonKey(key: Int): Int { - return when (key) { - KeyEvent.KEYCODE_BUTTON_A -> NativeLibrary.ButtonType.BUTTON_A - KeyEvent.KEYCODE_BUTTON_B -> NativeLibrary.ButtonType.BUTTON_B - KeyEvent.KEYCODE_BUTTON_X -> NativeLibrary.ButtonType.BUTTON_X - KeyEvent.KEYCODE_BUTTON_Y -> NativeLibrary.ButtonType.BUTTON_Y - KeyEvent.KEYCODE_DPAD_UP -> NativeLibrary.ButtonType.DPAD_UP - KeyEvent.KEYCODE_DPAD_DOWN -> NativeLibrary.ButtonType.DPAD_DOWN - KeyEvent.KEYCODE_DPAD_LEFT -> NativeLibrary.ButtonType.DPAD_LEFT - KeyEvent.KEYCODE_DPAD_RIGHT -> NativeLibrary.ButtonType.DPAD_RIGHT - KeyEvent.KEYCODE_BUTTON_L1 -> NativeLibrary.ButtonType.TRIGGER_L - KeyEvent.KEYCODE_BUTTON_R1 -> NativeLibrary.ButtonType.TRIGGER_R - KeyEvent.KEYCODE_BUTTON_L2 -> NativeLibrary.ButtonType.TRIGGER_ZL - KeyEvent.KEYCODE_BUTTON_R2 -> NativeLibrary.ButtonType.TRIGGER_ZR - KeyEvent.KEYCODE_BUTTON_THUMBL -> NativeLibrary.ButtonType.STICK_L - KeyEvent.KEYCODE_BUTTON_THUMBR -> NativeLibrary.ButtonType.STICK_R - KeyEvent.KEYCODE_BUTTON_START -> NativeLibrary.ButtonType.BUTTON_PLUS - KeyEvent.KEYCODE_BUTTON_SELECT -> NativeLibrary.ButtonType.BUTTON_MINUS - else -> -1 - } - } - - private fun setGenericAxisInput(event: MotionEvent, axis: Int) { - val playerNumber = getPlayerNumber(event.device.controllerNumber, event.deviceId) - - when (axis) { - MotionEvent.AXIS_X, MotionEvent.AXIS_Y -> - setStickState( - playerNumber, - NativeLibrary.StickType.STICK_L, - event.getAxisValue(MotionEvent.AXIS_X), - event.getAxisValue(MotionEvent.AXIS_Y) - ) - MotionEvent.AXIS_RX, MotionEvent.AXIS_RY -> - setStickState( - playerNumber, - NativeLibrary.StickType.STICK_R, - event.getAxisValue(MotionEvent.AXIS_RX), - event.getAxisValue(MotionEvent.AXIS_RY) - ) - MotionEvent.AXIS_Z, MotionEvent.AXIS_RZ -> - setStickState( - playerNumber, - NativeLibrary.StickType.STICK_R, - event.getAxisValue(MotionEvent.AXIS_Z), - event.getAxisValue(MotionEvent.AXIS_RZ) - ) - MotionEvent.AXIS_LTRIGGER -> - NativeLibrary.onGamePadButtonEvent( - playerNumber, - NativeLibrary.ButtonType.TRIGGER_ZL, - getAxisToButton(event.getAxisValue(MotionEvent.AXIS_LTRIGGER)) - ) - MotionEvent.AXIS_BRAKE -> - NativeLibrary.onGamePadButtonEvent( - playerNumber, - NativeLibrary.ButtonType.TRIGGER_ZL, - getAxisToButton(event.getAxisValue(MotionEvent.AXIS_BRAKE)) - ) - MotionEvent.AXIS_RTRIGGER -> - NativeLibrary.onGamePadButtonEvent( - playerNumber, - NativeLibrary.ButtonType.TRIGGER_ZR, - getAxisToButton(event.getAxisValue(MotionEvent.AXIS_RTRIGGER)) - ) - MotionEvent.AXIS_GAS -> - NativeLibrary.onGamePadButtonEvent( - playerNumber, - NativeLibrary.ButtonType.TRIGGER_ZR, - getAxisToButton(event.getAxisValue(MotionEvent.AXIS_GAS)) - ) - MotionEvent.AXIS_HAT_X, MotionEvent.AXIS_HAT_Y -> - setAxisDpadState( - playerNumber, - event.getAxisValue(MotionEvent.AXIS_HAT_X), - event.getAxisValue(MotionEvent.AXIS_HAT_Y) - ) - } - } - - private fun setJoyconAxisInput(event: MotionEvent, axis: Int) { - // Joycon support is half dead. Right joystick doesn't work - val playerNumber = getPlayerNumber(event.device.controllerNumber, event.deviceId) - - when (axis) { - MotionEvent.AXIS_X, MotionEvent.AXIS_Y -> - setStickState( - playerNumber, - NativeLibrary.StickType.STICK_L, - event.getAxisValue(MotionEvent.AXIS_X), - event.getAxisValue(MotionEvent.AXIS_Y) - ) - MotionEvent.AXIS_Z, MotionEvent.AXIS_RZ -> - setStickState( - playerNumber, - NativeLibrary.StickType.STICK_R, - event.getAxisValue(MotionEvent.AXIS_Z), - event.getAxisValue(MotionEvent.AXIS_RZ) - ) - MotionEvent.AXIS_RX, MotionEvent.AXIS_RY -> - setStickState( - playerNumber, - NativeLibrary.StickType.STICK_R, - event.getAxisValue(MotionEvent.AXIS_RX), - event.getAxisValue(MotionEvent.AXIS_RY) - ) - } - } - - private fun setRazerAxisInput(event: MotionEvent, axis: Int) { - val playerNumber = getPlayerNumber(event.device.controllerNumber, event.deviceId) - - when (axis) { - MotionEvent.AXIS_X, MotionEvent.AXIS_Y -> - setStickState( - playerNumber, - NativeLibrary.StickType.STICK_L, - event.getAxisValue(MotionEvent.AXIS_X), - event.getAxisValue(MotionEvent.AXIS_Y) - ) - MotionEvent.AXIS_Z, MotionEvent.AXIS_RZ -> - setStickState( - playerNumber, - NativeLibrary.StickType.STICK_R, - event.getAxisValue(MotionEvent.AXIS_Z), - event.getAxisValue(MotionEvent.AXIS_RZ) - ) - MotionEvent.AXIS_BRAKE -> - NativeLibrary.onGamePadButtonEvent( - playerNumber, - NativeLibrary.ButtonType.TRIGGER_ZL, - getAxisToButton(event.getAxisValue(MotionEvent.AXIS_BRAKE)) - ) - MotionEvent.AXIS_GAS -> - NativeLibrary.onGamePadButtonEvent( - playerNumber, - NativeLibrary.ButtonType.TRIGGER_ZR, - getAxisToButton(event.getAxisValue(MotionEvent.AXIS_GAS)) - ) - MotionEvent.AXIS_HAT_X, MotionEvent.AXIS_HAT_Y -> - setAxisDpadState( - playerNumber, - event.getAxisValue(MotionEvent.AXIS_HAT_X), - event.getAxisValue(MotionEvent.AXIS_HAT_Y) - ) - } - } - - fun getGameControllerIds(): Map<Int, Int> { - val gameControllerDeviceIds = mutableMapOf<Int, Int>() + fun getDevices(): Map<Int, YuzuPhysicalDevice> { + val gameControllerDeviceIds = mutableMapOf<Int, YuzuPhysicalDevice>() val deviceIds = InputDevice.getDeviceIds() - var controllerSlot = 1 + var port = 0 + val inputSettings = NativeConfig.getInputSettings(true) deviceIds.forEach { deviceId -> InputDevice.getDevice(deviceId)?.apply { - // Don't over-assign controllers - if (controllerSlot >= 8) { - return gameControllerDeviceIds - } - // Verify that the device has gamepad buttons, control sticks, or both. if (sources and InputDevice.SOURCE_GAMEPAD == InputDevice.SOURCE_GAMEPAD || sources and InputDevice.SOURCE_JOYSTICK == InputDevice.SOURCE_JOYSTICK ) { - // This device is a game controller. Store its device ID. - if (deviceId and id and vendorId and productId != 0) { - // Additionally filter out devices that have no ID - gameControllerDeviceIds - .takeIf { !it.contains(deviceId) } - ?.put(deviceId, controllerSlot) - controllerSlot++ + if (!gameControllerDeviceIds.contains(controllerNumber)) { + gameControllerDeviceIds[controllerNumber] = YuzuPhysicalDevice( + this, + port, + inputSettings[port].useSystemVibrator + ) } + port++ } } } return gameControllerDeviceIds } + + fun updateControllerData() { + androidControllers = getDevices() + androidControllers.forEach { + NativeInput.registerController(it.value) + } + + // Register the input overlay on a dedicated port for all player 1 vibrations + NativeInput.registerController(YuzuInputOverlayDevice(androidControllers.isEmpty(), 100)) + registeredControllers.clear() + NativeInput.getInputDevices().forEach { + registeredControllers.add(ParamPackage(it)) + } + registeredControllers.sortBy { it.get("port", 0) } + } + + fun InputDevice.getGUID(): String = String.format("%016x%016x", productId, vendorId) } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/LifecycleUtils.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/LifecycleUtils.kt new file mode 100644 index 000000000..d5c19c681 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/LifecycleUtils.kt @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.utils + +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.launch + +/** + * Collects this [Flow] with a given [LifecycleOwner]. + * @param scope [LifecycleOwner] that this [Flow] will be collected with. + * @param repeatState When to repeat collection on this [Flow]. + * @param resetState Optional lambda to reset state of an underlying [MutableStateFlow] after + * [stateCollector] has been run. + * @param stateCollector Lambda that receives new state. + */ +inline fun <reified T> Flow<T>.collect( + scope: LifecycleOwner, + repeatState: Lifecycle.State = Lifecycle.State.CREATED, + crossinline resetState: () -> Unit = {}, + crossinline stateCollector: (state: T) -> Unit +) { + scope.apply { + lifecycleScope.launch { + repeatOnLifecycle(repeatState) { + this@collect.collect { + stateCollector(it) + resetState() + } + } + } + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativeConfig.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativeConfig.kt index a4c14b3a7..7228f25d2 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativeConfig.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativeConfig.kt @@ -6,6 +6,8 @@ package org.yuzu.yuzu_emu.utils import org.yuzu.yuzu_emu.model.GameDir import org.yuzu.yuzu_emu.overlay.model.OverlayControlData +import org.yuzu.yuzu_emu.features.input.model.PlayerInput + object NativeConfig { /** * Loads global config. @@ -168,4 +170,17 @@ object NativeConfig { */ @Synchronized external fun setOverlayControlData(overlayControlData: Array<OverlayControlData>) + + @Synchronized + external fun getInputSettings(global: Boolean): Array<PlayerInput> + + @Synchronized + external fun setInputSettings(value: Array<PlayerInput>, global: Boolean) + + /** + * Saves control values for a specific player + * Must be used when per game config is loaded + */ + @Synchronized + external fun saveControlPlayerValues() } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NfcReader.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NfcReader.kt index 68ed66565..331b7ddca 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NfcReader.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NfcReader.kt @@ -14,7 +14,7 @@ import android.os.Build import android.os.Handler import android.os.Looper import java.io.IOException -import org.yuzu.yuzu_emu.NativeLibrary +import org.yuzu.yuzu_emu.features.input.NativeInput class NfcReader(private val activity: Activity) { private var nfcAdapter: NfcAdapter? = null @@ -76,12 +76,12 @@ class NfcReader(private val activity: Activity) { amiibo.connect() val tagData = ntag215ReadAll(amiibo) ?: return - NativeLibrary.onReadNfcTag(tagData) + NativeInput.onReadNfcTag(tagData) nfcAdapter?.ignore( tag, 1000, - { NativeLibrary.onRemoveNfcTag() }, + { NativeInput.onRemoveNfcTag() }, Handler(Looper.getMainLooper()) ) } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ParamPackage.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ParamPackage.kt new file mode 100644 index 000000000..83fc7da3c --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ParamPackage.kt @@ -0,0 +1,141 @@ +// SPDX-FileCopyrightText: 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.utils + +// Kotlin version of src/common/param_package.h +class ParamPackage(serialized: String = "") { + private val KEY_VALUE_SEPARATOR = ":" + private val PARAM_SEPARATOR = "," + + private val ESCAPE_CHARACTER = "$" + private val KEY_VALUE_SEPARATOR_ESCAPE = "$0" + private val PARAM_SEPARATOR_ESCAPE = "$1" + private val ESCAPE_CHARACTER_ESCAPE = "$2" + + private val EMPTY_PLACEHOLDER = "[empty]" + + val data = mutableMapOf<String, String>() + + init { + val pairs = serialized.split(PARAM_SEPARATOR) + for (pair in pairs) { + val keyValue = pair.split(KEY_VALUE_SEPARATOR).toMutableList() + if (keyValue.size != 2) { + Log.error("[ParamPackage] Invalid key pair $keyValue") + continue + } + + keyValue.forEachIndexed { i: Int, _: String -> + keyValue[i] = keyValue[i].replace(KEY_VALUE_SEPARATOR_ESCAPE, KEY_VALUE_SEPARATOR) + keyValue[i] = keyValue[i].replace(PARAM_SEPARATOR_ESCAPE, PARAM_SEPARATOR) + keyValue[i] = keyValue[i].replace(ESCAPE_CHARACTER_ESCAPE, ESCAPE_CHARACTER) + } + + set(keyValue[0], keyValue[1]) + } + } + + constructor(params: List<Pair<String, String>>) : this() { + params.forEach { + data[it.first] = it.second + } + } + + fun serialize(): String { + if (data.isEmpty()) { + return EMPTY_PLACEHOLDER + } + + val result = StringBuilder() + data.forEach { + val keyValue = mutableListOf(it.key, it.value) + keyValue.forEachIndexed { i, _ -> + keyValue[i] = keyValue[i].replace(ESCAPE_CHARACTER, ESCAPE_CHARACTER_ESCAPE) + keyValue[i] = keyValue[i].replace(PARAM_SEPARATOR, PARAM_SEPARATOR_ESCAPE) + keyValue[i] = keyValue[i].replace(KEY_VALUE_SEPARATOR, KEY_VALUE_SEPARATOR_ESCAPE) + } + result.append("${keyValue[0]}$KEY_VALUE_SEPARATOR${keyValue[1]}$PARAM_SEPARATOR") + } + return result.removeSuffix(PARAM_SEPARATOR).toString() + } + + fun get(key: String, defaultValue: String): String = + if (has(key)) { + data[key]!! + } else { + Log.debug("[ParamPackage] key $key not found") + defaultValue + } + + fun get(key: String, defaultValue: Int): Int = + if (has(key)) { + try { + data[key]!!.toInt() + } catch (e: NumberFormatException) { + Log.debug("[ParamPackage] failed to convert ${data[key]!!} to int") + defaultValue + } + } else { + Log.debug("[ParamPackage] key $key not found") + defaultValue + } + + private fun Int.toBoolean(): Boolean = + if (this == 1) { + true + } else if (this == 0) { + false + } else { + throw Exception("Tried to convert a value to a boolean that was not 0 or 1!") + } + + fun get(key: String, defaultValue: Boolean): Boolean = + if (has(key)) { + try { + get(key, if (defaultValue) 1 else 0).toBoolean() + } catch (e: Exception) { + Log.debug("[ParamPackage] failed to convert ${data[key]!!} to boolean") + defaultValue + } + } else { + Log.debug("[ParamPackage] key $key not found") + defaultValue + } + + fun get(key: String, defaultValue: Float): Float = + if (has(key)) { + try { + data[key]!!.toFloat() + } catch (e: NumberFormatException) { + Log.debug("[ParamPackage] failed to convert ${data[key]!!} to float") + defaultValue + } + } else { + Log.debug("[ParamPackage] key $key not found") + defaultValue + } + + fun set(key: String, value: String) { + data[key] = value + } + + fun set(key: String, value: Int) { + data[key] = value.toString() + } + + fun Boolean.toInt(): Int = if (this) 1 else 0 + fun set(key: String, value: Boolean) { + data[key] = value.toInt().toString() + } + + fun set(key: String, value: Float) { + data[key] = value.toString() + } + + fun has(key: String): Boolean = data.containsKey(key) + + fun erase(key: String) = data.remove(key) + + fun clear() = data.clear() +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ViewUtils.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ViewUtils.kt index ffbfa9337..244091aec 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ViewUtils.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ViewUtils.kt @@ -3,8 +3,10 @@ package org.yuzu.yuzu_emu.utils +import android.text.TextUtils import android.view.View import android.view.ViewGroup +import android.widget.TextView object ViewUtils { fun showView(view: View, length: Long = 300) { @@ -57,4 +59,35 @@ object ViewUtils { } this.layoutParams = layoutParams } + + /** + * Shows or hides a view. + * @param visible Whether a view will be made View.VISIBLE or View.INVISIBLE/GONE. + * @param gone Optional parameter for hiding a view. Uses View.GONE if true and View.INVISIBLE otherwise. + */ + fun View.setVisible(visible: Boolean, gone: Boolean = true) { + visibility = if (visible) { + View.VISIBLE + } else { + if (gone) { + View.GONE + } else { + View.INVISIBLE + } + } + } + + /** + * Starts a marquee on some text. + * @param delay Optional parameter for changing the start delay. 3 seconds of delay by default. + */ + fun TextView.marquee(delay: Long = 3000) { + ellipsize = null + marqueeRepeatLimit = -1 + isSingleLine = true + postDelayed({ + ellipsize = TextUtils.TruncateAt.MARQUEE + isSelected = true + }, delay) + } } diff --git a/src/android/app/src/main/jni/CMakeLists.txt b/src/android/app/src/main/jni/CMakeLists.txt index 20b319c12..ec8ae5c57 100644 --- a/src/android/app/src/main/jni/CMakeLists.txt +++ b/src/android/app/src/main/jni/CMakeLists.txt @@ -12,6 +12,7 @@ add_library(yuzu-android SHARED native_log.cpp android_config.cpp android_config.h + native_input.cpp ) set_property(TARGET yuzu-android PROPERTY IMPORTED_LOCATION ${FFmpeg_LIBRARY_DIR}) diff --git a/src/android/app/src/main/jni/android_config.cpp b/src/android/app/src/main/jni/android_config.cpp index e147560c3..a79a64afb 100644 --- a/src/android/app/src/main/jni/android_config.cpp +++ b/src/android/app/src/main/jni/android_config.cpp @@ -1,6 +1,8 @@ // SPDX-FileCopyrightText: 2023 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include <common/logging/log.h> +#include <input_common/main.h> #include "android_config.h" #include "android_settings.h" #include "common/settings_setting.h" @@ -32,6 +34,7 @@ void AndroidConfig::ReadAndroidValues() { ReadOverlayValues(); } ReadDriverValues(); + ReadAndroidControlValues(); } void AndroidConfig::ReadAndroidUIValues() { @@ -107,6 +110,76 @@ void AndroidConfig::ReadOverlayValues() { EndGroup(); } +void AndroidConfig::ReadAndroidPlayerValues(std::size_t player_index) { + std::string player_prefix; + if (type != ConfigType::InputProfile) { + player_prefix.append("player_").append(ToString(player_index)).append("_"); + } + + auto& player = Settings::values.players.GetValue()[player_index]; + if (IsCustomConfig()) { + const auto profile_name = + ReadStringSetting(std::string(player_prefix).append("profile_name")); + if (profile_name.empty()) { + // Use the global input config + player = Settings::values.players.GetValue(true)[player_index]; + player.profile_name = ""; + return; + } + } + + // Android doesn't have default options for controllers. We have the input overlay for that. + for (int i = 0; i < Settings::NativeButton::NumButtons; ++i) { + const std::string default_param; + auto& player_buttons = player.buttons[i]; + + player_buttons = ReadStringSetting( + std::string(player_prefix).append(Settings::NativeButton::mapping[i]), default_param); + if (player_buttons.empty()) { + player_buttons = default_param; + } + } + for (int i = 0; i < Settings::NativeAnalog::NumAnalogs; ++i) { + const std::string default_param; + auto& player_analogs = player.analogs[i]; + + player_analogs = ReadStringSetting( + std::string(player_prefix).append(Settings::NativeAnalog::mapping[i]), default_param); + if (player_analogs.empty()) { + player_analogs = default_param; + } + } + for (int i = 0; i < Settings::NativeMotion::NumMotions; ++i) { + const std::string default_param; + auto& player_motions = player.motions[i]; + + player_motions = ReadStringSetting( + std::string(player_prefix).append(Settings::NativeMotion::mapping[i]), default_param); + if (player_motions.empty()) { + player_motions = default_param; + } + } + player.use_system_vibrator = ReadBooleanSetting( + std::string(player_prefix).append("use_system_vibrator"), player_index == 0); +} + +void AndroidConfig::ReadAndroidControlValues() { + BeginGroup(Settings::TranslateCategory(Settings::Category::Controls)); + + Settings::values.players.SetGlobal(!IsCustomConfig()); + for (std::size_t p = 0; p < Settings::values.players.GetValue().size(); ++p) { + ReadAndroidPlayerValues(p); + } + if (IsCustomConfig()) { + EndGroup(); + return; + } + // ReadDebugControlValues(); + // ReadHidbusValues(); + + EndGroup(); +} + void AndroidConfig::SaveAndroidValues() { if (global) { SaveAndroidUIValues(); @@ -114,6 +187,7 @@ void AndroidConfig::SaveAndroidValues() { SaveOverlayValues(); } SaveDriverValues(); + SaveAndroidControlValues(); WriteToIni(); } @@ -187,6 +261,52 @@ void AndroidConfig::SaveOverlayValues() { EndGroup(); } +void AndroidConfig::SaveAndroidPlayerValues(std::size_t player_index) { + std::string player_prefix; + if (type != ConfigType::InputProfile) { + player_prefix = std::string("player_").append(ToString(player_index)).append("_"); + } + + const auto& player = Settings::values.players.GetValue()[player_index]; + if (IsCustomConfig() && player.profile_name.empty()) { + // No custom profile selected + return; + } + + const std::string default_param; + for (int i = 0; i < Settings::NativeButton::NumButtons; ++i) { + WriteStringSetting(std::string(player_prefix).append(Settings::NativeButton::mapping[i]), + player.buttons[i], std::make_optional(default_param)); + } + for (int i = 0; i < Settings::NativeAnalog::NumAnalogs; ++i) { + WriteStringSetting(std::string(player_prefix).append(Settings::NativeAnalog::mapping[i]), + player.analogs[i], std::make_optional(default_param)); + } + for (int i = 0; i < Settings::NativeMotion::NumMotions; ++i) { + WriteStringSetting(std::string(player_prefix).append(Settings::NativeMotion::mapping[i]), + player.motions[i], std::make_optional(default_param)); + } + WriteBooleanSetting(std::string(player_prefix).append("use_system_vibrator"), + player.use_system_vibrator, std::make_optional(player_index == 0)); +} + +void AndroidConfig::SaveAndroidControlValues() { + BeginGroup(Settings::TranslateCategory(Settings::Category::Controls)); + + Settings::values.players.SetGlobal(!IsCustomConfig()); + for (std::size_t p = 0; p < Settings::values.players.GetValue().size(); ++p) { + SaveAndroidPlayerValues(p); + } + if (IsCustomConfig()) { + EndGroup(); + return; + } + // SaveDebugControlValues(); + // SaveHidbusValues(); + + EndGroup(); +} + std::vector<Settings::BasicSetting*>& AndroidConfig::FindRelevantList(Settings::Category category) { auto& map = Settings::values.linkage.by_category; if (map.contains(category)) { @@ -194,3 +314,24 @@ std::vector<Settings::BasicSetting*>& AndroidConfig::FindRelevantList(Settings:: } return AndroidSettings::values.linkage.by_category[category]; } + +void AndroidConfig::ReadAndroidControlPlayerValues(std::size_t player_index) { + BeginGroup(Settings::TranslateCategory(Settings::Category::Controls)); + + ReadPlayerValues(player_index); + ReadAndroidPlayerValues(player_index); + + EndGroup(); +} + +void AndroidConfig::SaveAndroidControlPlayerValues(std::size_t player_index) { + BeginGroup(Settings::TranslateCategory(Settings::Category::Controls)); + + LOG_DEBUG(Config, "Saving players control configuration values"); + SavePlayerValues(player_index); + SaveAndroidPlayerValues(player_index); + + EndGroup(); + + WriteToIni(); +} diff --git a/src/android/app/src/main/jni/android_config.h b/src/android/app/src/main/jni/android_config.h index 693e1e3f0..28ef5d0a8 100644 --- a/src/android/app/src/main/jni/android_config.h +++ b/src/android/app/src/main/jni/android_config.h @@ -13,7 +13,12 @@ public: void ReloadAllValues() override; void SaveAllValues() override; + void ReadAndroidControlPlayerValues(std::size_t player_index); + void SaveAndroidControlPlayerValues(std::size_t player_index); + protected: + void ReadAndroidPlayerValues(std::size_t player_index); + void ReadAndroidControlValues(); void ReadAndroidValues(); void ReadAndroidUIValues(); void ReadDriverValues(); @@ -27,6 +32,8 @@ protected: void ReadUILayoutValues() override {} void ReadMultiplayerValues() override {} + void SaveAndroidPlayerValues(std::size_t player_index); + void SaveAndroidControlValues(); void SaveAndroidValues(); void SaveAndroidUIValues(); void SaveDriverValues(); diff --git a/src/android/app/src/main/jni/emu_window/emu_window.cpp b/src/android/app/src/main/jni/emu_window/emu_window.cpp index c927cddda..06db55369 100644 --- a/src/android/app/src/main/jni/emu_window/emu_window.cpp +++ b/src/android/app/src/main/jni/emu_window/emu_window.cpp @@ -5,6 +5,7 @@ #include "common/android/id_cache.h" #include "common/logging/log.h" +#include "input_common/drivers/android.h" #include "input_common/drivers/touch_screen.h" #include "input_common/drivers/virtual_amiibo.h" #include "input_common/drivers/virtual_gamepad.h" @@ -24,39 +25,18 @@ void EmuWindow_Android::OnSurfaceChanged(ANativeWindow* surface) { void EmuWindow_Android::OnTouchPressed(int id, float x, float y) { const auto [touch_x, touch_y] = MapToTouchScreen(x, y); - m_input_subsystem->GetTouchScreen()->TouchPressed(touch_x, touch_y, id); + EmulationSession::GetInstance().GetInputSubsystem().GetTouchScreen()->TouchPressed(touch_x, + touch_y, id); } void EmuWindow_Android::OnTouchMoved(int id, float x, float y) { const auto [touch_x, touch_y] = MapToTouchScreen(x, y); - m_input_subsystem->GetTouchScreen()->TouchMoved(touch_x, touch_y, id); + EmulationSession::GetInstance().GetInputSubsystem().GetTouchScreen()->TouchMoved(touch_x, + touch_y, id); } void EmuWindow_Android::OnTouchReleased(int id) { - m_input_subsystem->GetTouchScreen()->TouchReleased(id); -} - -void EmuWindow_Android::OnGamepadButtonEvent(int player_index, int button_id, bool pressed) { - m_input_subsystem->GetVirtualGamepad()->SetButtonState(player_index, button_id, pressed); -} - -void EmuWindow_Android::OnGamepadJoystickEvent(int player_index, int stick_id, float x, float y) { - m_input_subsystem->GetVirtualGamepad()->SetStickPosition(player_index, stick_id, x, y); -} - -void EmuWindow_Android::OnGamepadMotionEvent(int player_index, u64 delta_timestamp, float gyro_x, - float gyro_y, float gyro_z, float accel_x, - float accel_y, float accel_z) { - m_input_subsystem->GetVirtualGamepad()->SetMotionState( - player_index, delta_timestamp, gyro_x, gyro_y, gyro_z, accel_x, accel_y, accel_z); -} - -void EmuWindow_Android::OnReadNfcTag(std::span<u8> data) { - m_input_subsystem->GetVirtualAmiibo()->LoadAmiibo(data); -} - -void EmuWindow_Android::OnRemoveNfcTag() { - m_input_subsystem->GetVirtualAmiibo()->CloseAmiibo(); + EmulationSession::GetInstance().GetInputSubsystem().GetTouchScreen()->TouchReleased(id); } void EmuWindow_Android::OnFrameDisplayed() { @@ -67,10 +47,9 @@ void EmuWindow_Android::OnFrameDisplayed() { } } -EmuWindow_Android::EmuWindow_Android(InputCommon::InputSubsystem* input_subsystem, - ANativeWindow* surface, +EmuWindow_Android::EmuWindow_Android(ANativeWindow* surface, std::shared_ptr<Common::DynamicLibrary> driver_library) - : m_input_subsystem{input_subsystem}, m_driver_library{driver_library} { + : m_driver_library{driver_library} { LOG_INFO(Frontend, "initializing"); if (!surface) { @@ -80,10 +59,4 @@ EmuWindow_Android::EmuWindow_Android(InputCommon::InputSubsystem* input_subsyste OnSurfaceChanged(surface); window_info.type = Core::Frontend::WindowSystemType::Android; - - m_input_subsystem->Initialize(); -} - -EmuWindow_Android::~EmuWindow_Android() { - m_input_subsystem->Shutdown(); } diff --git a/src/android/app/src/main/jni/emu_window/emu_window.h b/src/android/app/src/main/jni/emu_window/emu_window.h index a34a0e479..d7b5fc6da 100644 --- a/src/android/app/src/main/jni/emu_window/emu_window.h +++ b/src/android/app/src/main/jni/emu_window/emu_window.h @@ -30,22 +30,17 @@ private: class EmuWindow_Android final : public Core::Frontend::EmuWindow { public: - EmuWindow_Android(InputCommon::InputSubsystem* input_subsystem, ANativeWindow* surface, + EmuWindow_Android(ANativeWindow* surface, std::shared_ptr<Common::DynamicLibrary> driver_library); - ~EmuWindow_Android(); + ~EmuWindow_Android() = default; void OnSurfaceChanged(ANativeWindow* surface); + void OnFrameDisplayed() override; + void OnTouchPressed(int id, float x, float y); void OnTouchMoved(int id, float x, float y); void OnTouchReleased(int id); - void OnGamepadButtonEvent(int player_index, int button_id, bool pressed); - void OnGamepadJoystickEvent(int player_index, int stick_id, float x, float y); - void OnGamepadMotionEvent(int player_index, u64 delta_timestamp, float gyro_x, float gyro_y, - float gyro_z, float accel_x, float accel_y, float accel_z); - void OnReadNfcTag(std::span<u8> data); - void OnRemoveNfcTag(); - void OnFrameDisplayed() override; std::unique_ptr<Core::Frontend::GraphicsContext> CreateSharedContext() const override { return {std::make_unique<GraphicsContext_Android>(m_driver_library)}; @@ -55,8 +50,6 @@ public: }; private: - InputCommon::InputSubsystem* m_input_subsystem{}; - float m_window_width{}; float m_window_height{}; diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp index a4d8454e8..50cef5d2a 100644 --- a/src/android/app/src/main/jni/native.cpp +++ b/src/android/app/src/main/jni/native.cpp @@ -88,6 +88,10 @@ FileSys::ManualContentProvider* EmulationSession::GetContentProvider() { return m_manual_provider.get(); } +InputCommon::InputSubsystem& EmulationSession::GetInputSubsystem() { + return m_input_subsystem; +} + const EmuWindow_Android& EmulationSession::Window() const { return *m_window; } @@ -198,6 +202,8 @@ void EmulationSession::InitializeSystem(bool reload) { Common::Log::Initialize(); Common::Log::SetColorConsoleBackendEnabled(true); Common::Log::Start(); + + m_input_subsystem.Initialize(); } // Initialize filesystem. @@ -222,8 +228,7 @@ Core::SystemResultStatus EmulationSession::InitializeEmulation(const std::string std::scoped_lock lock(m_mutex); // Create the render window. - m_window = - std::make_unique<EmuWindow_Android>(&m_input_subsystem, m_native_window, m_vulkan_library); + m_window = std::make_unique<EmuWindow_Android>(m_native_window, m_vulkan_library); // Initialize system. jauto android_keyboard = std::make_unique<Common::Android::SoftwareKeyboard::AndroidKeyboard>(); @@ -355,60 +360,6 @@ void EmulationSession::RunEmulation() { m_applet_id = static_cast<int>(Service::AM::AppletId::Application); } -bool EmulationSession::IsHandheldOnly() { - jconst npad_style_set = m_system.HIDCore().GetSupportedStyleTag(); - - if (npad_style_set.fullkey == 1) { - return false; - } - - if (npad_style_set.handheld == 0) { - return false; - } - - return !Settings::IsDockedMode(); -} - -void EmulationSession::SetDeviceType([[maybe_unused]] int index, int type) { - jauto controller = m_system.HIDCore().GetEmulatedControllerByIndex(index); - controller->SetNpadStyleIndex(static_cast<Core::HID::NpadStyleIndex>(type)); -} - -void EmulationSession::OnGamepadConnectEvent([[maybe_unused]] int index) { - jauto controller = m_system.HIDCore().GetEmulatedControllerByIndex(index); - - // Ensure that player1 is configured correctly and handheld disconnected - if (controller->GetNpadIdType() == Core::HID::NpadIdType::Player1) { - jauto handheld = m_system.HIDCore().GetEmulatedController(Core::HID::NpadIdType::Handheld); - - if (controller->GetNpadStyleIndex() == Core::HID::NpadStyleIndex::Handheld) { - handheld->SetNpadStyleIndex(Core::HID::NpadStyleIndex::Fullkey); - controller->SetNpadStyleIndex(Core::HID::NpadStyleIndex::Fullkey); - handheld->Disconnect(); - } - } - - // Ensure that handheld is configured correctly and player 1 disconnected - if (controller->GetNpadIdType() == Core::HID::NpadIdType::Handheld) { - jauto player1 = m_system.HIDCore().GetEmulatedController(Core::HID::NpadIdType::Player1); - - if (controller->GetNpadStyleIndex() != Core::HID::NpadStyleIndex::Handheld) { - player1->SetNpadStyleIndex(Core::HID::NpadStyleIndex::Handheld); - controller->SetNpadStyleIndex(Core::HID::NpadStyleIndex::Handheld); - player1->Disconnect(); - } - } - - if (!controller->IsConnected()) { - controller->Connect(); - } -} - -void EmulationSession::OnGamepadDisconnectEvent([[maybe_unused]] int index) { - jauto controller = m_system.HIDCore().GetEmulatedControllerByIndex(index); - controller->Disconnect(); -} - Common::Android::SoftwareKeyboard::AndroidKeyboard* EmulationSession::SoftwareKeyboard() { return m_software_keyboard; } @@ -574,14 +525,14 @@ jobjectArray Java_org_yuzu_yuzu_1emu_utils_GpuDriverHelper_getSystemDriverInfo( nullptr, nullptr, file_redirect_dir_, nullptr); auto driver_library = std::make_shared<Common::DynamicLibrary>(handle); InputCommon::InputSubsystem input_subsystem; - auto m_window = std::make_unique<EmuWindow_Android>( - &input_subsystem, ANativeWindow_fromSurface(env, j_surf), driver_library); + auto window = + std::make_unique<EmuWindow_Android>(ANativeWindow_fromSurface(env, j_surf), driver_library); Vulkan::vk::InstanceDispatch dld; Vulkan::vk::Instance vk_instance = Vulkan::CreateInstance( *driver_library, dld, VK_API_VERSION_1_1, Core::Frontend::WindowSystemType::Android); - auto surface = Vulkan::CreateSurface(vk_instance, m_window->GetWindowInfo()); + auto surface = Vulkan::CreateSurface(vk_instance, window->GetWindowInfo()); auto device = Vulkan::CreateDevice(vk_instance, dld, *surface); @@ -622,103 +573,6 @@ jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isPaused(JNIEnv* env, jclass claz return static_cast<jboolean>(EmulationSession::GetInstance().IsPaused()); } -jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isHandheldOnly(JNIEnv* env, jclass clazz) { - return EmulationSession::GetInstance().IsHandheldOnly(); -} - -jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_setDeviceType(JNIEnv* env, jclass clazz, - jint j_device, jint j_type) { - if (EmulationSession::GetInstance().IsRunning()) { - EmulationSession::GetInstance().SetDeviceType(j_device, j_type); - } - return static_cast<jboolean>(true); -} - -jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_onGamePadConnectEvent(JNIEnv* env, jclass clazz, - jint j_device) { - if (EmulationSession::GetInstance().IsRunning()) { - EmulationSession::GetInstance().OnGamepadConnectEvent(j_device); - } - return static_cast<jboolean>(true); -} - -jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_onGamePadDisconnectEvent(JNIEnv* env, jclass clazz, - jint j_device) { - if (EmulationSession::GetInstance().IsRunning()) { - EmulationSession::GetInstance().OnGamepadDisconnectEvent(j_device); - } - return static_cast<jboolean>(true); -} -jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_onGamePadButtonEvent(JNIEnv* env, jclass clazz, - jint j_device, jint j_button, - jint action) { - if (EmulationSession::GetInstance().IsRunning()) { - // Ensure gamepad is connected - EmulationSession::GetInstance().OnGamepadConnectEvent(j_device); - EmulationSession::GetInstance().Window().OnGamepadButtonEvent(j_device, j_button, - action != 0); - } - return static_cast<jboolean>(true); -} - -jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_onGamePadJoystickEvent(JNIEnv* env, jclass clazz, - jint j_device, jint stick_id, - jfloat x, jfloat y) { - if (EmulationSession::GetInstance().IsRunning()) { - EmulationSession::GetInstance().Window().OnGamepadJoystickEvent(j_device, stick_id, x, y); - } - return static_cast<jboolean>(true); -} - -jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_onGamePadMotionEvent( - JNIEnv* env, jclass clazz, jint j_device, jlong delta_timestamp, jfloat gyro_x, jfloat gyro_y, - jfloat gyro_z, jfloat accel_x, jfloat accel_y, jfloat accel_z) { - if (EmulationSession::GetInstance().IsRunning()) { - EmulationSession::GetInstance().Window().OnGamepadMotionEvent( - j_device, delta_timestamp, gyro_x, gyro_y, gyro_z, accel_x, accel_y, accel_z); - } - return static_cast<jboolean>(true); -} - -jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_onReadNfcTag(JNIEnv* env, jclass clazz, - jbyteArray j_data) { - jboolean isCopy{false}; - std::span<u8> data(reinterpret_cast<u8*>(env->GetByteArrayElements(j_data, &isCopy)), - static_cast<size_t>(env->GetArrayLength(j_data))); - - if (EmulationSession::GetInstance().IsRunning()) { - EmulationSession::GetInstance().Window().OnReadNfcTag(data); - } - return static_cast<jboolean>(true); -} - -jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_onRemoveNfcTag(JNIEnv* env, jclass clazz) { - if (EmulationSession::GetInstance().IsRunning()) { - EmulationSession::GetInstance().Window().OnRemoveNfcTag(); - } - return static_cast<jboolean>(true); -} - -void Java_org_yuzu_yuzu_1emu_NativeLibrary_onTouchPressed(JNIEnv* env, jclass clazz, jint id, - jfloat x, jfloat y) { - if (EmulationSession::GetInstance().IsRunning()) { - EmulationSession::GetInstance().Window().OnTouchPressed(id, x, y); - } -} - -void Java_org_yuzu_yuzu_1emu_NativeLibrary_onTouchMoved(JNIEnv* env, jclass clazz, jint id, - jfloat x, jfloat y) { - if (EmulationSession::GetInstance().IsRunning()) { - EmulationSession::GetInstance().Window().OnTouchMoved(id, x, y); - } -} - -void Java_org_yuzu_yuzu_1emu_NativeLibrary_onTouchReleased(JNIEnv* env, jclass clazz, jint id) { - if (EmulationSession::GetInstance().IsRunning()) { - EmulationSession::GetInstance().Window().OnTouchReleased(id); - } -} - void Java_org_yuzu_yuzu_1emu_NativeLibrary_initializeSystem(JNIEnv* env, jclass clazz, jboolean reload) { // Initialize the emulated system. @@ -759,6 +613,7 @@ jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getGpuDriver(JNIEnv* env, jobject void Java_org_yuzu_yuzu_1emu_NativeLibrary_applySettings(JNIEnv* env, jobject jobj) { EmulationSession::GetInstance().System().ApplySettings(); + EmulationSession::GetInstance().System().HIDCore().ReloadInputDevices(); } void Java_org_yuzu_yuzu_1emu_NativeLibrary_logSettings(JNIEnv* env, jobject jobj) { diff --git a/src/android/app/src/main/jni/native.h b/src/android/app/src/main/jni/native.h index 47936e305..6a4551ada 100644 --- a/src/android/app/src/main/jni/native.h +++ b/src/android/app/src/main/jni/native.h @@ -23,6 +23,7 @@ public: const Core::System& System() const; Core::System& System(); FileSys::ManualContentProvider* GetContentProvider(); + InputCommon::InputSubsystem& GetInputSubsystem(); const EmuWindow_Android& Window() const; EmuWindow_Android& Window(); @@ -50,10 +51,6 @@ public: const std::size_t program_index, const bool frontend_initiated); - bool IsHandheldOnly(); - void SetDeviceType([[maybe_unused]] int index, int type); - void OnGamepadConnectEvent([[maybe_unused]] int index); - void OnGamepadDisconnectEvent([[maybe_unused]] int index); Common::Android::SoftwareKeyboard::AndroidKeyboard* SoftwareKeyboard(); static void OnEmulationStarted(); diff --git a/src/android/app/src/main/jni/native_config.cpp b/src/android/app/src/main/jni/native_config.cpp index 8ae10fbc7..0b26280c6 100644 --- a/src/android/app/src/main/jni/native_config.cpp +++ b/src/android/app/src/main/jni/native_config.cpp @@ -3,7 +3,6 @@ #include <string> -#include <common/fs/fs_util.h> #include <jni.h> #include "android_config.h" @@ -425,4 +424,120 @@ void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setOverlayControlData( } } +jobjectArray Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getInputSettings(JNIEnv* env, jobject obj, + jboolean j_global) { + Settings::values.players.SetGlobal(static_cast<bool>(j_global)); + auto& players = Settings::values.players.GetValue(); + jobjectArray j_input_settings = + env->NewObjectArray(players.size(), Common::Android::GetPlayerInputClass(), nullptr); + for (size_t i = 0; i < players.size(); ++i) { + auto j_connected = static_cast<jboolean>(players[i].connected); + + jobjectArray j_buttons = env->NewObjectArray( + players[i].buttons.size(), Common::Android::GetStringClass(), env->NewStringUTF("")); + for (size_t j = 0; j < players[i].buttons.size(); ++j) { + env->SetObjectArrayElement(j_buttons, j, + Common::Android::ToJString(env, players[i].buttons[j])); + } + jobjectArray j_analogs = env->NewObjectArray( + players[i].analogs.size(), Common::Android::GetStringClass(), env->NewStringUTF("")); + for (size_t j = 0; j < players[i].analogs.size(); ++j) { + env->SetObjectArrayElement(j_analogs, j, + Common::Android::ToJString(env, players[i].analogs[j])); + } + jobjectArray j_motions = env->NewObjectArray( + players[i].motions.size(), Common::Android::GetStringClass(), env->NewStringUTF("")); + for (size_t j = 0; j < players[i].motions.size(); ++j) { + env->SetObjectArrayElement(j_motions, j, + Common::Android::ToJString(env, players[i].motions[j])); + } + + auto j_vibration_enabled = static_cast<jboolean>(players[i].vibration_enabled); + auto j_vibration_strength = static_cast<jint>(players[i].vibration_strength); + + auto j_body_color_left = static_cast<jlong>(players[i].body_color_left); + auto j_body_color_right = static_cast<jlong>(players[i].body_color_right); + auto j_button_color_left = static_cast<jlong>(players[i].button_color_left); + auto j_button_color_right = static_cast<jlong>(players[i].button_color_right); + + auto j_profile_name = Common::Android::ToJString(env, players[i].profile_name); + + auto j_use_system_vibrator = players[i].use_system_vibrator; + + jobject playerInput = env->NewObject( + Common::Android::GetPlayerInputClass(), Common::Android::GetPlayerInputConstructor(), + j_connected, j_buttons, j_analogs, j_motions, j_vibration_enabled, j_vibration_strength, + j_body_color_left, j_body_color_right, j_button_color_left, j_button_color_right, + j_profile_name, j_use_system_vibrator); + env->SetObjectArrayElement(j_input_settings, i, playerInput); + } + return j_input_settings; +} + +void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setInputSettings(JNIEnv* env, jobject obj, + jobjectArray j_value, + jboolean j_global) { + auto& players = Settings::values.players.GetValue(static_cast<bool>(j_global)); + int playersSize = env->GetArrayLength(j_value); + for (int i = 0; i < playersSize; ++i) { + jobject jplayer = env->GetObjectArrayElement(j_value, i); + + players[i].connected = static_cast<bool>( + env->GetBooleanField(jplayer, Common::Android::GetPlayerInputConnectedField())); + + auto j_buttons_array = static_cast<jobjectArray>( + env->GetObjectField(jplayer, Common::Android::GetPlayerInputButtonsField())); + int buttons_size = env->GetArrayLength(j_buttons_array); + for (int j = 0; j < buttons_size; ++j) { + auto button = static_cast<jstring>(env->GetObjectArrayElement(j_buttons_array, j)); + players[i].buttons[j] = Common::Android::GetJString(env, button); + } + auto j_analogs_array = static_cast<jobjectArray>( + env->GetObjectField(jplayer, Common::Android::GetPlayerInputAnalogsField())); + int analogs_size = env->GetArrayLength(j_analogs_array); + for (int j = 0; j < analogs_size; ++j) { + auto analog = static_cast<jstring>(env->GetObjectArrayElement(j_analogs_array, j)); + players[i].analogs[j] = Common::Android::GetJString(env, analog); + } + auto j_motions_array = static_cast<jobjectArray>( + env->GetObjectField(jplayer, Common::Android::GetPlayerInputMotionsField())); + int motions_size = env->GetArrayLength(j_motions_array); + for (int j = 0; j < motions_size; ++j) { + auto motion = static_cast<jstring>(env->GetObjectArrayElement(j_motions_array, j)); + players[i].motions[j] = Common::Android::GetJString(env, motion); + } + + players[i].vibration_enabled = static_cast<bool>( + env->GetBooleanField(jplayer, Common::Android::GetPlayerInputVibrationEnabledField())); + players[i].vibration_strength = static_cast<int>( + env->GetIntField(jplayer, Common::Android::GetPlayerInputVibrationStrengthField())); + + players[i].body_color_left = static_cast<u32>( + env->GetLongField(jplayer, Common::Android::GetPlayerInputBodyColorLeftField())); + players[i].body_color_right = static_cast<u32>( + env->GetLongField(jplayer, Common::Android::GetPlayerInputBodyColorRightField())); + players[i].button_color_left = static_cast<u32>( + env->GetLongField(jplayer, Common::Android::GetPlayerInputButtonColorLeftField())); + players[i].button_color_right = static_cast<u32>( + env->GetLongField(jplayer, Common::Android::GetPlayerInputButtonColorRightField())); + + auto profileName = static_cast<jstring>( + env->GetObjectField(jplayer, Common::Android::GetPlayerInputProfileNameField())); + players[i].profile_name = Common::Android::GetJString(env, profileName); + + players[i].use_system_vibrator = + env->GetBooleanField(jplayer, Common::Android::GetPlayerInputUseSystemVibratorField()); + } +} + +void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_saveControlPlayerValues(JNIEnv* env, jobject obj) { + Settings::values.players.SetGlobal(false); + + // Clear all controls from the config in case the user reverted back to globals + per_game_config->ClearControlPlayerValues(); + for (size_t index = 0; index < Settings::values.players.GetValue().size(); ++index) { + per_game_config->SaveAndroidControlPlayerValues(index); + } +} + } // extern "C" diff --git a/src/android/app/src/main/jni/native_input.cpp b/src/android/app/src/main/jni/native_input.cpp new file mode 100644 index 000000000..37a65f2b8 --- /dev/null +++ b/src/android/app/src/main/jni/native_input.cpp @@ -0,0 +1,629 @@ +// SPDX-FileCopyrightText: 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include <common/fs/fs.h> +#include <common/fs/path_util.h> +#include <common/settings.h> +#include <hid_core/hid_types.h> +#include <jni.h> + +#include "android_config.h" +#include "common/android/android_common.h" +#include "common/android/id_cache.h" +#include "hid_core/frontend/emulated_controller.h" +#include "hid_core/hid_core.h" +#include "input_common/drivers/android.h" +#include "input_common/drivers/touch_screen.h" +#include "input_common/drivers/virtual_amiibo.h" +#include "input_common/drivers/virtual_gamepad.h" +#include "native.h" + +std::unordered_map<std::string, std::unique_ptr<AndroidConfig>> map_profiles; + +bool IsHandheldOnly() { + const auto npad_style_set = + EmulationSession::GetInstance().System().HIDCore().GetSupportedStyleTag(); + + if (npad_style_set.fullkey == 1) { + return false; + } + + if (npad_style_set.handheld == 0) { + return false; + } + + return !Settings::IsDockedMode(); +} + +std::filesystem::path GetNameWithoutExtension(std::filesystem::path filename) { + return filename.replace_extension(); +} + +bool IsProfileNameValid(std::string_view profile_name) { + return profile_name.find_first_of("<>:;\"/\\|,.!?*") == std::string::npos; +} + +bool ProfileExistsInFilesystem(std::string_view profile_name) { + return Common::FS::Exists(Common::FS::GetYuzuPath(Common::FS::YuzuPath::ConfigDir) / "input" / + fmt::format("{}.ini", profile_name)); +} + +bool ProfileExistsInMap(const std::string& profile_name) { + return map_profiles.find(profile_name) != map_profiles.end(); +} + +bool SaveProfile(const std::string& profile_name, std::size_t player_index) { + if (!ProfileExistsInMap(profile_name)) { + return false; + } + + Settings::values.players.GetValue()[player_index].profile_name = profile_name; + map_profiles[profile_name]->SaveAndroidControlPlayerValues(player_index); + return true; +} + +bool LoadProfile(std::string& profile_name, std::size_t player_index) { + if (!ProfileExistsInMap(profile_name)) { + return false; + } + + if (!ProfileExistsInFilesystem(profile_name)) { + map_profiles.erase(profile_name); + return false; + } + + LOG_INFO(Config, "Loading input profile `{}`", profile_name); + + Settings::values.players.GetValue()[player_index].profile_name = profile_name; + map_profiles[profile_name]->ReadAndroidControlPlayerValues(player_index); + return true; +} + +void ApplyControllerConfig(size_t player_index, + const std::function<void(Core::HID::EmulatedController*)>& apply) { + auto& hid_core = EmulationSession::GetInstance().System().HIDCore(); + if (player_index == 0) { + auto* handheld = hid_core.GetEmulatedController(Core::HID::NpadIdType::Handheld); + auto* player_one = hid_core.GetEmulatedController(Core::HID::NpadIdType::Player1); + handheld->EnableConfiguration(); + player_one->EnableConfiguration(); + apply(handheld); + apply(player_one); + handheld->DisableConfiguration(); + player_one->DisableConfiguration(); + handheld->SaveCurrentConfig(); + player_one->SaveCurrentConfig(); + } else { + auto* controller = hid_core.GetEmulatedControllerByIndex(player_index); + controller->EnableConfiguration(); + apply(controller); + controller->DisableConfiguration(); + controller->SaveCurrentConfig(); + } +} + +void ConnectController(size_t player_index, bool connected) { + auto& hid_core = EmulationSession::GetInstance().System().HIDCore(); + if (player_index == 0) { + auto* handheld = hid_core.GetEmulatedController(Core::HID::NpadIdType::Handheld); + auto* player_one = hid_core.GetEmulatedController(Core::HID::NpadIdType::Player1); + handheld->EnableConfiguration(); + player_one->EnableConfiguration(); + if (player_one->GetNpadStyleIndex(true) == Core::HID::NpadStyleIndex::Handheld) { + if (connected) { + handheld->Connect(); + } else { + handheld->Disconnect(); + } + player_one->Disconnect(); + } else { + if (connected) { + player_one->Connect(); + } else { + player_one->Disconnect(); + } + handheld->Disconnect(); + } + handheld->DisableConfiguration(); + player_one->DisableConfiguration(); + handheld->SaveCurrentConfig(); + player_one->SaveCurrentConfig(); + } else { + auto* controller = hid_core.GetEmulatedControllerByIndex(player_index); + controller->EnableConfiguration(); + if (connected) { + controller->Connect(); + } else { + controller->Disconnect(); + } + controller->DisableConfiguration(); + controller->SaveCurrentConfig(); + } +} + +extern "C" { + +jboolean Java_org_yuzu_yuzu_1emu_features_input_NativeInput_isHandheldOnly(JNIEnv* env, + jobject j_obj) { + return IsHandheldOnly(); +} + +void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onGamePadButtonEvent( + JNIEnv* env, jobject j_obj, jstring j_guid, jint j_port, jint j_button_id, jint j_action) { + EmulationSession::GetInstance().GetInputSubsystem().GetAndroid()->SetButtonState( + Common::Android::GetJString(env, j_guid), j_port, j_button_id, j_action != 0); +} + +void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onGamePadAxisEvent( + JNIEnv* env, jobject j_obj, jstring j_guid, jint j_port, jint j_stick_id, jfloat j_value) { + EmulationSession::GetInstance().GetInputSubsystem().GetAndroid()->SetAxisPosition( + Common::Android::GetJString(env, j_guid), j_port, j_stick_id, j_value); +} + +void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onGamePadMotionEvent( + JNIEnv* env, jobject j_obj, jstring j_guid, jint j_port, jlong j_delta_timestamp, + jfloat j_x_gyro, jfloat j_y_gyro, jfloat j_z_gyro, jfloat j_x_accel, jfloat j_y_accel, + jfloat j_z_accel) { + EmulationSession::GetInstance().GetInputSubsystem().GetAndroid()->SetMotionState( + Common::Android::GetJString(env, j_guid), j_port, j_delta_timestamp, j_x_gyro, j_y_gyro, + j_z_gyro, j_x_accel, j_y_accel, j_z_accel); +} + +void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onReadNfcTag(JNIEnv* env, jobject j_obj, + jbyteArray j_data) { + jboolean isCopy{false}; + std::span<u8> data(reinterpret_cast<u8*>(env->GetByteArrayElements(j_data, &isCopy)), + static_cast<size_t>(env->GetArrayLength(j_data))); + + if (EmulationSession::GetInstance().IsRunning()) { + EmulationSession::GetInstance().GetInputSubsystem().GetVirtualAmiibo()->LoadAmiibo(data); + } +} + +void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onRemoveNfcTag(JNIEnv* env, jobject j_obj) { + if (EmulationSession::GetInstance().IsRunning()) { + EmulationSession::GetInstance().GetInputSubsystem().GetVirtualAmiibo()->CloseAmiibo(); + } +} + +void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onTouchPressed(JNIEnv* env, jobject j_obj, + jint j_id, jfloat j_x_axis, + jfloat j_y_axis) { + if (EmulationSession::GetInstance().IsRunning()) { + EmulationSession::GetInstance().Window().OnTouchPressed(j_id, j_x_axis, j_y_axis); + } +} + +void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onTouchMoved(JNIEnv* env, jobject j_obj, + jint j_id, jfloat j_x_axis, + jfloat j_y_axis) { + if (EmulationSession::GetInstance().IsRunning()) { + EmulationSession::GetInstance().Window().OnTouchMoved(j_id, j_x_axis, j_y_axis); + } +} + +void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onTouchReleased(JNIEnv* env, jobject j_obj, + jint j_id) { + if (EmulationSession::GetInstance().IsRunning()) { + EmulationSession::GetInstance().Window().OnTouchReleased(j_id); + } +} + +void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onOverlayButtonEventImpl( + JNIEnv* env, jobject j_obj, jint j_port, jint j_button_id, jint j_action) { + if (EmulationSession::GetInstance().IsRunning()) { + EmulationSession::GetInstance().GetInputSubsystem().GetVirtualGamepad()->SetButtonState( + j_port, j_button_id, j_action == 1); + } +} + +void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onOverlayJoystickEventImpl( + JNIEnv* env, jobject j_obj, jint j_port, jint j_stick_id, jfloat j_x_axis, jfloat j_y_axis) { + if (EmulationSession::GetInstance().IsRunning()) { + EmulationSession::GetInstance().GetInputSubsystem().GetVirtualGamepad()->SetStickPosition( + j_port, j_stick_id, j_x_axis, j_y_axis); + } +} + +void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onDeviceMotionEvent( + JNIEnv* env, jobject j_obj, jint j_port, jlong j_delta_timestamp, jfloat j_x_gyro, + jfloat j_y_gyro, jfloat j_z_gyro, jfloat j_x_accel, jfloat j_y_accel, jfloat j_z_accel) { + if (EmulationSession::GetInstance().IsRunning()) { + EmulationSession::GetInstance().GetInputSubsystem().GetVirtualGamepad()->SetMotionState( + j_port, j_delta_timestamp, j_x_gyro, j_y_gyro, j_z_gyro, j_x_accel, j_y_accel, + j_z_accel); + } +} + +void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_reloadInputDevices(JNIEnv* env, + jobject j_obj) { + EmulationSession::GetInstance().System().HIDCore().ReloadInputDevices(); +} + +void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_registerController(JNIEnv* env, + jobject j_obj, + jobject j_device) { + EmulationSession::GetInstance().GetInputSubsystem().GetAndroid()->RegisterController(j_device); +} + +jobjectArray Java_org_yuzu_yuzu_1emu_features_input_NativeInput_getInputDevices(JNIEnv* env, + jobject j_obj) { + auto devices = EmulationSession::GetInstance().GetInputSubsystem().GetInputDevices(); + jobjectArray jdevices = env->NewObjectArray(devices.size(), Common::Android::GetStringClass(), + Common::Android::ToJString(env, "")); + for (size_t i = 0; i < devices.size(); ++i) { + env->SetObjectArrayElement(jdevices, i, + Common::Android::ToJString(env, devices[i].Serialize())); + } + return jdevices; +} + +void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_loadInputProfiles(JNIEnv* env, + jobject j_obj) { + map_profiles.clear(); + const auto input_profile_loc = + Common::FS::GetYuzuPath(Common::FS::YuzuPath::ConfigDir) / "input"; + + if (Common::FS::IsDir(input_profile_loc)) { + Common::FS::IterateDirEntries( + input_profile_loc, + [&](const std::filesystem::path& full_path) { + const auto filename = full_path.filename(); + const auto name_without_ext = + Common::FS::PathToUTF8String(GetNameWithoutExtension(filename)); + + if (filename.extension() == ".ini" && IsProfileNameValid(name_without_ext)) { + map_profiles.insert_or_assign( + name_without_ext, std::make_unique<AndroidConfig>( + name_without_ext, Config::ConfigType::InputProfile)); + } + + return true; + }, + Common::FS::DirEntryFilter::File); + } +} + +jobjectArray Java_org_yuzu_yuzu_1emu_features_input_NativeInput_getInputProfileNames( + JNIEnv* env, jobject j_obj) { + std::vector<std::string> profile_names; + profile_names.reserve(map_profiles.size()); + + auto it = map_profiles.cbegin(); + while (it != map_profiles.cend()) { + const auto& [profile_name, config] = *it; + if (!ProfileExistsInFilesystem(profile_name)) { + it = map_profiles.erase(it); + continue; + } + + profile_names.push_back(profile_name); + ++it; + } + + std::stable_sort(profile_names.begin(), profile_names.end()); + + jobjectArray j_profile_names = + env->NewObjectArray(profile_names.size(), Common::Android::GetStringClass(), + Common::Android::ToJString(env, "")); + for (size_t i = 0; i < profile_names.size(); ++i) { + env->SetObjectArrayElement(j_profile_names, i, + Common::Android::ToJString(env, profile_names[i])); + } + + return j_profile_names; +} + +jboolean Java_org_yuzu_yuzu_1emu_features_input_NativeInput_isProfileNameValid(JNIEnv* env, + jobject j_obj, + jstring j_name) { + return Common::Android::GetJString(env, j_name).find_first_of("<>:;\"/\\|,.!?*") == + std::string::npos; +} + +jboolean Java_org_yuzu_yuzu_1emu_features_input_NativeInput_createProfile(JNIEnv* env, + jobject j_obj, + jstring j_name, + jint j_player_index) { + auto profile_name = Common::Android::GetJString(env, j_name); + if (ProfileExistsInMap(profile_name)) { + return false; + } + + map_profiles.insert_or_assign( + profile_name, + std::make_unique<AndroidConfig>(profile_name, Config::ConfigType::InputProfile)); + + return SaveProfile(profile_name, j_player_index); +} + +jboolean Java_org_yuzu_yuzu_1emu_features_input_NativeInput_deleteProfile(JNIEnv* env, + jobject j_obj, + jstring j_name, + jint j_player_index) { + auto profile_name = Common::Android::GetJString(env, j_name); + if (!ProfileExistsInMap(profile_name)) { + return false; + } + + if (!ProfileExistsInFilesystem(profile_name) || + Common::FS::RemoveFile(map_profiles[profile_name]->GetConfigFilePath())) { + map_profiles.erase(profile_name); + } + + Settings::values.players.GetValue()[j_player_index].profile_name = ""; + return !ProfileExistsInMap(profile_name) && !ProfileExistsInFilesystem(profile_name); +} + +jboolean Java_org_yuzu_yuzu_1emu_features_input_NativeInput_loadProfile(JNIEnv* env, jobject j_obj, + jstring j_name, + jint j_player_index) { + auto profile_name = Common::Android::GetJString(env, j_name); + return LoadProfile(profile_name, j_player_index); +} + +jboolean Java_org_yuzu_yuzu_1emu_features_input_NativeInput_saveProfile(JNIEnv* env, jobject j_obj, + jstring j_name, + jint j_player_index) { + auto profile_name = Common::Android::GetJString(env, j_name); + return SaveProfile(profile_name, j_player_index); +} + +void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_loadPerGameConfiguration( + JNIEnv* env, jobject j_obj, jint j_player_index, jint j_selected_index, + jstring j_selected_profile_name) { + static constexpr size_t HANDHELD_INDEX = 8; + + auto& hid_core = EmulationSession::GetInstance().System().HIDCore(); + Settings::values.players.SetGlobal(false); + + auto profile_name = Common::Android::GetJString(env, j_selected_profile_name); + auto* emulated_controller = hid_core.GetEmulatedControllerByIndex(j_player_index); + + if (j_selected_index == 0) { + Settings::values.players.GetValue()[j_player_index].profile_name = ""; + if (j_player_index == 0) { + Settings::values.players.GetValue()[HANDHELD_INDEX] = {}; + } + Settings::values.players.SetGlobal(true); + emulated_controller->ReloadFromSettings(); + return; + } + if (profile_name.empty()) { + return; + } + auto& player = Settings::values.players.GetValue()[j_player_index]; + auto& global_player = Settings::values.players.GetValue(true)[j_player_index]; + player.profile_name = profile_name; + global_player.profile_name = profile_name; + // Read from the profile into the custom player settings + LoadProfile(profile_name, j_player_index); + // Make sure the controller is connected + player.connected = true; + + emulated_controller->ReloadFromSettings(); + + if (j_player_index > 0) { + return; + } + // Handle Handheld cases + auto& handheld_player = Settings::values.players.GetValue()[HANDHELD_INDEX]; + auto* handheld_controller = hid_core.GetEmulatedController(Core::HID::NpadIdType::Handheld); + if (player.controller_type == Settings::ControllerType::Handheld) { + handheld_player = player; + } else { + handheld_player = {}; + } + handheld_controller->ReloadFromSettings(); +} + +void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_beginMapping(JNIEnv* env, jobject j_obj, + jint jtype) { + EmulationSession::GetInstance().GetInputSubsystem().BeginMapping( + static_cast<InputCommon::Polling::InputType>(jtype)); +} + +jstring Java_org_yuzu_yuzu_1emu_features_input_NativeInput_getNextInput(JNIEnv* env, + jobject j_obj) { + return Common::Android::ToJString( + env, EmulationSession::GetInstance().GetInputSubsystem().GetNextInput().Serialize()); +} + +void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_stopMapping(JNIEnv* env, jobject j_obj) { + EmulationSession::GetInstance().GetInputSubsystem().StopMapping(); +} + +void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_updateMappingsWithDefaultImpl( + JNIEnv* env, jobject j_obj, jint j_player_index, jstring j_device_params, + jstring j_display_name) { + auto& input_subsystem = EmulationSession::GetInstance().GetInputSubsystem(); + + // Clear all previous mappings + for (int button_id = 0; button_id < Settings::NativeButton::NumButtons; ++button_id) { + ApplyControllerConfig(j_player_index, [&](Core::HID::EmulatedController* controller) { + controller->SetButtonParam(button_id, {}); + }); + } + for (int analog_id = 0; analog_id < Settings::NativeAnalog::NumAnalogs; ++analog_id) { + ApplyControllerConfig(j_player_index, [&](Core::HID::EmulatedController* controller) { + controller->SetStickParam(analog_id, {}); + }); + } + + // Apply new mappings + auto device = Common::ParamPackage(Common::Android::GetJString(env, j_device_params)); + auto button_mappings = input_subsystem.GetButtonMappingForDevice(device); + auto analog_mappings = input_subsystem.GetAnalogMappingForDevice(device); + auto display_name = Common::Android::GetJString(env, j_display_name); + for (const auto& button_mapping : button_mappings) { + const std::size_t index = button_mapping.first; + auto named_mapping = button_mapping.second; + named_mapping.Set("display", display_name); + ApplyControllerConfig(j_player_index, [&](Core::HID::EmulatedController* controller) { + controller->SetButtonParam(index, named_mapping); + }); + } + for (const auto& analog_mapping : analog_mappings) { + const std::size_t index = analog_mapping.first; + auto named_mapping = analog_mapping.second; + named_mapping.Set("display", display_name); + ApplyControllerConfig(j_player_index, [&](Core::HID::EmulatedController* controller) { + controller->SetStickParam(index, named_mapping); + }); + } +} + +jstring Java_org_yuzu_yuzu_1emu_features_input_NativeInput_getButtonParamImpl(JNIEnv* env, + jobject j_obj, + jint j_player_index, + jint j_button) { + return Common::Android::ToJString(env, EmulationSession::GetInstance() + .System() + .HIDCore() + .GetEmulatedControllerByIndex(j_player_index) + ->GetButtonParam(j_button) + .Serialize()); +} + +void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_setButtonParamImpl( + JNIEnv* env, jobject j_obj, jint j_player_index, jint j_button_id, jstring j_param) { + ApplyControllerConfig(j_player_index, [&](Core::HID::EmulatedController* controller) { + controller->SetButtonParam(j_button_id, + Common::ParamPackage(Common::Android::GetJString(env, j_param))); + }); +} + +jstring Java_org_yuzu_yuzu_1emu_features_input_NativeInput_getStickParamImpl(JNIEnv* env, + jobject j_obj, + jint j_player_index, + jint j_stick) { + return Common::Android::ToJString(env, EmulationSession::GetInstance() + .System() + .HIDCore() + .GetEmulatedControllerByIndex(j_player_index) + ->GetStickParam(j_stick) + .Serialize()); +} + +void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_setStickParamImpl( + JNIEnv* env, jobject j_obj, jint j_player_index, jint j_stick_id, jstring j_param) { + ApplyControllerConfig(j_player_index, [&](Core::HID::EmulatedController* controller) { + controller->SetStickParam(j_stick_id, + Common::ParamPackage(Common::Android::GetJString(env, j_param))); + }); +} + +jint Java_org_yuzu_yuzu_1emu_features_input_NativeInput_getButtonNameImpl(JNIEnv* env, + jobject j_obj, + jstring j_param) { + return static_cast<jint>(EmulationSession::GetInstance().GetInputSubsystem().GetButtonName( + Common::ParamPackage(Common::Android::GetJString(env, j_param)))); +} + +jintArray Java_org_yuzu_yuzu_1emu_features_input_NativeInput_getSupportedStyleTagsImpl( + JNIEnv* env, jobject j_obj, jint j_player_index) { + auto& hid_core = EmulationSession::GetInstance().System().HIDCore(); + const auto npad_style_set = hid_core.GetSupportedStyleTag(); + std::vector<s32> supported_indexes; + if (npad_style_set.fullkey == 1) { + supported_indexes.push_back(static_cast<u32>(Core::HID::NpadStyleIndex::Fullkey)); + } + + if (npad_style_set.joycon_dual == 1) { + supported_indexes.push_back(static_cast<u32>(Core::HID::NpadStyleIndex::JoyconDual)); + } + + if (npad_style_set.joycon_left == 1) { + supported_indexes.push_back(static_cast<u32>(Core::HID::NpadStyleIndex::JoyconLeft)); + } + + if (npad_style_set.joycon_right == 1) { + supported_indexes.push_back(static_cast<u32>(Core::HID::NpadStyleIndex::JoyconRight)); + } + + if (j_player_index == 0 && npad_style_set.handheld == 1) { + supported_indexes.push_back(static_cast<u32>(Core::HID::NpadStyleIndex::Handheld)); + } + + if (npad_style_set.gamecube == 1) { + supported_indexes.push_back(static_cast<u32>(Core::HID::NpadStyleIndex::GameCube)); + } + + jintArray j_supported_indexes = env->NewIntArray(supported_indexes.size()); + env->SetIntArrayRegion(j_supported_indexes, 0, supported_indexes.size(), + supported_indexes.data()); + return j_supported_indexes; +} + +jint Java_org_yuzu_yuzu_1emu_features_input_NativeInput_getStyleIndexImpl(JNIEnv* env, + jobject j_obj, + jint j_player_index) { + return static_cast<s32>(EmulationSession::GetInstance() + .System() + .HIDCore() + .GetEmulatedControllerByIndex(j_player_index) + ->GetNpadStyleIndex(true)); +} + +void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_setStyleIndexImpl(JNIEnv* env, + jobject j_obj, + jint j_player_index, + jint j_style_index) { + auto& hid_core = EmulationSession::GetInstance().System().HIDCore(); + auto type = static_cast<Core::HID::NpadStyleIndex>(j_style_index); + ApplyControllerConfig(j_player_index, [&](Core::HID::EmulatedController* controller) { + controller->SetNpadStyleIndex(type); + }); + if (j_player_index == 0) { + auto* handheld = hid_core.GetEmulatedController(Core::HID::NpadIdType::Handheld); + auto* player_one = hid_core.GetEmulatedController(Core::HID::NpadIdType::Player1); + ConnectController(j_player_index, + player_one->IsConnected(true) || handheld->IsConnected(true)); + } +} + +jboolean Java_org_yuzu_yuzu_1emu_features_input_NativeInput_isControllerImpl(JNIEnv* env, + jobject j_obj, + jstring jparams) { + return static_cast<jint>(EmulationSession::GetInstance().GetInputSubsystem().IsController( + Common::ParamPackage(Common::Android::GetJString(env, jparams)))); +} + +jboolean Java_org_yuzu_yuzu_1emu_features_input_NativeInput_getIsConnected(JNIEnv* env, + jobject j_obj, + jint j_player_index) { + auto& hid_core = EmulationSession::GetInstance().System().HIDCore(); + auto* controller = hid_core.GetEmulatedControllerByIndex(static_cast<size_t>(j_player_index)); + if (j_player_index == 0 && + controller->GetNpadStyleIndex(true) == Core::HID::NpadStyleIndex::Handheld) { + return hid_core.GetEmulatedController(Core::HID::NpadIdType::Handheld)->IsConnected(true); + } + return controller->IsConnected(true); +} + +void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_connectControllersImpl( + JNIEnv* env, jobject j_obj, jbooleanArray j_connected) { + jboolean isCopy = false; + auto j_connected_array_size = env->GetArrayLength(j_connected); + jboolean* j_connected_array = env->GetBooleanArrayElements(j_connected, &isCopy); + for (int i = 0; i < j_connected_array_size; ++i) { + ConnectController(i, j_connected_array[i]); + } +} + +void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_resetControllerMappings( + JNIEnv* env, jobject j_obj, jint j_player_index) { + // Clear all previous mappings + for (int button_id = 0; button_id < Settings::NativeButton::NumButtons; ++button_id) { + ApplyControllerConfig(j_player_index, [&](Core::HID::EmulatedController* controller) { + controller->SetButtonParam(button_id, {}); + }); + } + for (int analog_id = 0; analog_id < Settings::NativeAnalog::NumAnalogs; ++analog_id) { + ApplyControllerConfig(j_player_index, [&](Core::HID::EmulatedController* controller) { + controller->SetStickParam(analog_id, {}); + }); + } +} + +} // extern "C" diff --git a/src/android/app/src/main/res/drawable/button_anim.xml b/src/android/app/src/main/res/drawable/button_anim.xml new file mode 100644 index 000000000..ccdc5ca6a --- /dev/null +++ b/src/android/app/src/main/res/drawable/button_anim.xml @@ -0,0 +1,142 @@ +<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:aapt="http://schemas.android.com/aapt"> + <aapt:attr name="android:drawable"> + <vector + android:width="1000dp" + android:height="1000dp" + android:viewportWidth="1000" + android:viewportHeight="1000"> + <group android:name="_R_G"> + <group + android:name="_R_G_L_0_G" + android:pivotX="100" + android:pivotY="100" + android:scaleX="4.5" + android:scaleY="4.5" + android:translateX="400" + android:translateY="400"> + <path + android:name="_R_G_L_0_G_D_0_P_0" + android:fillAlpha="1" + android:fillColor="?attr/colorSecondaryContainer" + android:fillType="nonZero" + android:pathData=" M198.56 100 C198.56,154.43 154.43,198.56 100,198.56 C45.57,198.56 1.44,154.43 1.44,100 C1.44,45.57 45.57,1.44 100,1.44 C154.43,1.44 198.56,45.57 198.56,100c " /> + <path + android:name="_R_G_L_0_G_D_2_P_0" + android:fillAlpha="0.8" + android:fillColor="?attr/colorOnSecondaryContainer" + android:fillType="nonZero" + android:pathData=" M50.14 151.21 C50.53,150.18 89.6,49.87 90.1,48.63 C90.1,48.63 90.67,47.2 90.67,47.2 C90.67,47.2 101.67,47.2 101.67,47.2 C101.67,47.2 112.67,47.2 112.67,47.2 C112.67,47.2 133.47,99.12 133.47,99.12 C144.91,127.68 154.32,151.17 154.38,151.33 C154.47,151.56 152.2,151.6 143.14,151.55 C143.14,151.55 131.79,151.48 131.79,151.48 C131.79,151.48 127.22,139.57 127.22,139.57 C127.22,139.57 122.65,127.66 122.65,127.66 C122.65,127.66 101.68,127.73 101.68,127.73 C101.68,127.73 80.71,127.8 80.71,127.8 C80.71,127.8 76.38,139.71 76.38,139.71 C76.38,139.71 72.06,151.62 72.06,151.62 C72.06,151.62 61.02,151.62 61.02,151.62 C50.61,151.62 50,151.55 50.14,151.22 C50.14,151.22 50.14,151.21 50.14,151.21c M115.86 110.06 C115.8,109.91 112.55,101.13 108.62,90.56 C104.7,80 101.42,71.43 101.34,71.53 C101.22,71.66 92.84,94.61 87.25,110.06 C87.17,110.29 90.13,110.34 101.56,110.34 C113,110.34 115.95,110.28 115.86,110.06c " /> + </group> + </group> + <group android:name="time_group" /> + </vector> + </aapt:attr> + <target android:name="_R_G_L_0_G"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="100" + android:propertyName="scaleX" + android:startOffset="0" + android:valueFrom="4.5" + android:valueTo="3.75" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="100" + android:propertyName="scaleY" + android:startOffset="0" + android:valueFrom="4.5" + android:valueTo="3.75" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="234" + android:propertyName="scaleX" + android:startOffset="100" + android:valueFrom="3.75" + android:valueTo="3.75" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="234" + android:propertyName="scaleY" + android:startOffset="100" + android:valueFrom="3.75" + android:valueTo="3.75" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="167" + android:propertyName="scaleX" + android:startOffset="334" + android:valueFrom="3.75" + android:valueTo="4.75" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="167" + android:propertyName="scaleY" + android:startOffset="334" + android:valueFrom="3.75" + android:valueTo="4.75" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="67" + android:propertyName="scaleX" + android:startOffset="501" + android:valueFrom="4.75" + android:valueTo="4.5" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="67" + android:propertyName="scaleY" + android:startOffset="501" + android:valueFrom="4.75" + android:valueTo="4.5" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="time_group"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="1034" + android:propertyName="translateX" + android:startOffset="0" + android:valueFrom="0" + android:valueTo="1" + android:valueType="floatType" /> + </set> + </aapt:attr> + </target> +</animated-vector> diff --git a/src/android/app/src/main/res/drawable/ic_controller_disconnected.xml b/src/android/app/src/main/res/drawable/ic_controller_disconnected.xml new file mode 100644 index 000000000..8e3c66f74 --- /dev/null +++ b/src/android/app/src/main/res/drawable/ic_controller_disconnected.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="960" + android:viewportHeight="960"> + <path + android:fillColor="?attr/colorControlNormal" + android:pathData="M700,480q-25,0 -42.5,-17.5T640,420q0,-25 17.5,-42.5T700,360q25,0 42.5,17.5T760,420q0,25 -17.5,42.5T700,480ZM366,480ZM280,600v-80h-80v-80h80v-80h80v80h80v80h-80v80h-80ZM160,720q-33,0 -56.5,-23.5T80,640v-320q0,-34 24,-57.5t58,-23.5h77l81,81L160,320v320h366L55,169l57,-57 736,736 -57,57 -185,-185L160,720ZM880,640q0,26 -14,46t-37,29l-29,-29v-366L434,320l-80,-80h446q33,0 56.5,23.5T880,320v320ZM617,503Z" /> +</vector> diff --git a/src/android/app/src/main/res/drawable/ic_more_vert.xml b/src/android/app/src/main/res/drawable/ic_more_vert.xml new file mode 100644 index 000000000..9f62ac595 --- /dev/null +++ b/src/android/app/src/main/res/drawable/ic_more_vert.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:height="24dp" + android:viewportHeight="24" + android:viewportWidth="24" + android:width="24dp"> + <path + android:fillColor="?attr/colorControlNormal" + android:pathData="M12,8c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM12,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM12,16c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z" /> +</vector> diff --git a/src/android/app/src/main/res/drawable/ic_new_label.xml b/src/android/app/src/main/res/drawable/ic_new_label.xml new file mode 100644 index 000000000..fac562c26 --- /dev/null +++ b/src/android/app/src/main/res/drawable/ic_new_label.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:fillColor="?attr/colorControlNormal" + android:pathData="M21,12l-4.37,6.16C16.26,18.68 15.65,19 15,19h-3l0,-6H9v-3H3V7c0,-1.1 0.9,-2 2,-2h10c0.65,0 1.26,0.31 1.63,0.84L21,12zM10,15H7v-3H5v3H2v2h3v3h2v-3h3V15z" /> +</vector> diff --git a/src/android/app/src/main/res/drawable/ic_overlay.xml b/src/android/app/src/main/res/drawable/ic_overlay.xml new file mode 100644 index 000000000..c7986c5a2 --- /dev/null +++ b/src/android/app/src/main/res/drawable/ic_overlay.xml @@ -0,0 +1,21 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:fillColor="?attr/colorControlNormal" + android:pathData="M21,5H3C1.9,5 1,5.9 1,7v10c0,1.1 0.9,2 2,2h18c1.1,0 2,-0.9 2,-2V7C23,5.9 22.1,5 21,5zM18,17H6V7h12V17z" /> + <path + android:fillColor="?attr/colorControlNormal" + android:pathData="M15,11.25h1.5v1.5h-1.5z" /> + <path + android:fillColor="?attr/colorControlNormal" + android:pathData="M12.5,11.25h1.5v1.5h-1.5z" /> + <path + android:fillColor="?attr/colorControlNormal" + android:pathData="M10,11.25h1.5v1.5h-1.5z" /> + <path + android:fillColor="?attr/colorControlNormal" + android:pathData="M7.5,11.25h1.5v1.5h-1.5z" /> +</vector> diff --git a/src/android/app/src/main/res/drawable/ic_share.xml b/src/android/app/src/main/res/drawable/ic_share.xml new file mode 100644 index 000000000..3fc2f3c99 --- /dev/null +++ b/src/android/app/src/main/res/drawable/ic_share.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:fillColor="?attr/colorControlNormal" + android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z" /> +</vector> diff --git a/src/android/app/src/main/res/drawable/stick_one_direction_anim.xml b/src/android/app/src/main/res/drawable/stick_one_direction_anim.xml new file mode 100644 index 000000000..a1da1316f --- /dev/null +++ b/src/android/app/src/main/res/drawable/stick_one_direction_anim.xml @@ -0,0 +1,118 @@ +<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:aapt="http://schemas.android.com/aapt"> + <aapt:attr name="android:drawable"> + <vector + android:width="1000dp" + android:height="1000dp" + android:viewportWidth="1000" + android:viewportHeight="1000"> + <group android:name="_R_G"> + <group + android:name="_R_G_L_1_G" + android:pivotX="100" + android:pivotY="100" + android:scaleX="5" + android:scaleY="5" + android:translateX="400" + android:translateY="400"> + <path + android:name="_R_G_L_1_G_D_0_P_0" + android:pathData=" M100 199.39 C59.8,199.39 23.56,175.17 8.18,138.04 C-7.2,100.9 1.3,58.15 29.73,29.72 C58.15,1.3 100.9,-7.21 138.04,8.18 C175.18,23.56 199.39,59.8 199.39,100 C199.33,154.87 154.87,199.33 100,199.39c " + android:strokeWidth="1" + android:strokeAlpha="0.6" + android:strokeColor="?attr/colorOutline" + android:strokeLineCap="round" + android:strokeLineJoin="round" /> + </group> + <group + android:name="_R_G_L_0_G_T_1" + android:scaleX="5" + android:scaleY="5" + android:translateX="500" + android:translateY="500"> + <group + android:name="_R_G_L_0_G" + android:translateX="-100" + android:translateY="-100"> + <path + android:name="_R_G_L_0_G_D_0_P_0" + android:fillAlpha="1" + android:fillColor="?attr/colorSecondaryContainer" + android:fillType="nonZero" + android:pathData=" M100.45 28.02 C140.63,28.02 173.2,60.59 173.2,100.77 C173.2,140.95 140.63,173.52 100.45,173.52 C60.27,173.52 27.7,140.95 27.7,100.77 C27.7,60.59 60.27,28.02 100.45,28.02c " /> + <path + android:name="_R_G_L_0_G_D_2_P_0" + android:fillAlpha="0.8" + android:fillColor="?attr/colorOnSecondaryContainer" + android:fillType="nonZero" + android:pathData=" M100.45 50.26 C128.62,50.26 151.46,73.1 151.46,101.28 C151.46,129.45 128.62,152.29 100.45,152.29 C72.27,152.29 49.43,129.45 49.43,101.28 C49.43,73.1 72.27,50.26 100.45,50.26c " /> + </group> + </group> + </group> + <group android:name="time_group" /> + </vector> + </aapt:attr> + <target android:name="_R_G_L_0_G_T_1"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="267" + android:pathData="M 500,500C 500,500 364,500 364,500" + android:propertyName="translateXY" + android:propertyXName="translateX" + android:propertyYName="translateY" + android:startOffset="0"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="234" + android:pathData="M 364,500C 364,500 364,500 364,500" + android:propertyName="translateXY" + android:propertyXName="translateX" + android:propertyYName="translateY" + android:startOffset="267"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0.333 0.667,0.667 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="133" + android:pathData="M 364,500C 364,500 525,500 525,500" + android:propertyName="translateXY" + android:propertyXName="translateX" + android:propertyYName="translateY" + android:startOffset="501"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="100" + android:pathData="M 525,500C 525,500 500,500 500,500" + android:propertyName="translateXY" + android:propertyXName="translateX" + android:propertyYName="translateY" + android:startOffset="634"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="time_group"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="968" + android:propertyName="translateX" + android:startOffset="0" + android:valueFrom="0" + android:valueTo="1" + android:valueType="floatType" /> + </set> + </aapt:attr> + </target> +</animated-vector> diff --git a/src/android/app/src/main/res/drawable/stick_two_direction_anim.xml b/src/android/app/src/main/res/drawable/stick_two_direction_anim.xml new file mode 100644 index 000000000..bc71adcbd --- /dev/null +++ b/src/android/app/src/main/res/drawable/stick_two_direction_anim.xml @@ -0,0 +1,173 @@ +<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:aapt="http://schemas.android.com/aapt"> + <aapt:attr name="android:drawable"> + <vector + android:width="1000dp" + android:height="1000dp" + android:viewportWidth="1000" + android:viewportHeight="1000"> + <group android:name="_R_G"> + <group + android:name="_R_G_L_1_G" + android:pivotX="100" + android:pivotY="100" + android:scaleX="5" + android:scaleY="5" + android:translateX="400" + android:translateY="400"> + <path + android:name="_R_G_L_1_G_D_0_P_0" + android:pathData=" M100 199.39 C59.8,199.39 23.56,175.17 8.18,138.04 C-7.2,100.9 1.3,58.15 29.73,29.72 C58.15,1.3 100.9,-7.21 138.04,8.18 C175.18,23.56 199.39,59.8 199.39,100 C199.33,154.87 154.87,199.33 100,199.39c " + android:strokeWidth="1" + android:strokeAlpha="0.6" + android:strokeColor="?attr/colorOutline" + android:strokeLineCap="round" + android:strokeLineJoin="round" /> + </group> + <group + android:name="_R_G_L_0_G_T_1" + android:scaleX="5" + android:scaleY="5" + android:translateX="500" + android:translateY="500"> + <group + android:name="_R_G_L_0_G" + android:translateX="-100" + android:translateY="-100"> + <path + android:name="_R_G_L_0_G_D_0_P_0" + android:fillAlpha="1" + android:fillColor="?attr/colorSecondaryContainer" + android:fillType="nonZero" + android:pathData=" M100.45 28.02 C140.63,28.02 173.2,60.59 173.2,100.77 C173.2,140.95 140.63,173.52 100.45,173.52 C60.27,173.52 27.7,140.95 27.7,100.77 C27.7,60.59 60.27,28.02 100.45,28.02c " /> + <path + android:name="_R_G_L_0_G_D_2_P_0" + android:fillAlpha="0.8" + android:fillColor="?attr/colorOnSecondaryContainer" + android:fillType="nonZero" + android:pathData=" M100.45 50.26 C128.62,50.26 151.46,73.1 151.46,101.28 C151.46,129.45 128.62,152.29 100.45,152.29 C72.27,152.29 49.43,129.45 49.43,101.28 C49.43,73.1 72.27,50.26 100.45,50.26c " /> + </group> + </group> + </group> + <group android:name="time_group" /> + </vector> + </aapt:attr> + <target android:name="_R_G_L_0_G_T_1"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="267" + android:pathData="M 500,500C 500,500 364,500 364,500" + android:propertyName="translateXY" + android:propertyXName="translateX" + android:propertyYName="translateY" + android:startOffset="0"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="234" + android:pathData="M 364,500C 364,500 364,500 364,500" + android:propertyName="translateXY" + android:propertyXName="translateX" + android:propertyYName="translateY" + android:startOffset="267"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0.333 0.667,0.667 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="133" + android:pathData="M 364,500C 364,500 525,500 525,500" + android:propertyName="translateXY" + android:propertyXName="translateX" + android:propertyYName="translateY" + android:startOffset="501"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="100" + android:pathData="M 525,500C 525,500 500,500 500,500" + android:propertyName="translateXY" + android:propertyXName="translateX" + android:propertyYName="translateY" + android:startOffset="634"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="400" + android:pathData="M 500,500C 500,500 500,500 500,500" + android:propertyName="translateXY" + android:propertyXName="translateX" + android:propertyYName="translateY" + android:startOffset="734"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0.333 0.667,0.667 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="267" + android:pathData="M 500,500C 500,500 500,364 500,364" + android:propertyName="translateXY" + android:propertyXName="translateX" + android:propertyYName="translateY" + android:startOffset="1134"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="234" + android:pathData="M 500,364C 500,364 500,364 500,364" + android:propertyName="translateXY" + android:propertyXName="translateX" + android:propertyYName="translateY" + android:startOffset="1401"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0.333 0.667,0.667 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="133" + android:pathData="M 500,364C 500,364 500,535 500,535" + android:propertyName="translateXY" + android:propertyXName="translateX" + android:propertyYName="translateY" + android:startOffset="1635"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="100" + android:pathData="M 500,535C 500,535 500,500 500,500" + android:propertyName="translateXY" + android:propertyXName="translateX" + android:propertyYName="translateY" + android:startOffset="1768"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="time_group"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="2269" + android:propertyName="translateX" + android:startOffset="0" + android:valueFrom="0" + android:valueTo="1" + android:valueType="floatType" /> + </set> + </aapt:attr> + </target> +</animated-vector> diff --git a/src/android/app/src/main/res/layout-ldrtl/list_item_setting_input.xml b/src/android/app/src/main/res/layout-ldrtl/list_item_setting_input.xml new file mode 100644 index 000000000..583620dc6 --- /dev/null +++ b/src/android/app/src/main/res/layout-ldrtl/list_item_setting_input.xml @@ -0,0 +1,63 @@ +<?xml version="1.0" encoding="utf-8"?> +<RelativeLayout 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/setting_body" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="?android:attr/selectableItemBackground" + android:clickable="true" + android:focusable="true" + android:gravity="center_vertical" + android:minHeight="72dp" + android:padding="16dp" + android:nextFocusLeft="@id/button_options"> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="center_vertical" + android:orientation="horizontal"> + + <LinearLayout + android:layout_width="0dp" + android:layout_height="wrap_content" + android:orientation="vertical" + android:layout_weight="1"> + + <com.google.android.material.textview.MaterialTextView + android:id="@+id/text_setting_name" + style="@style/TextAppearance.Material3.HeadlineMedium" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:textAlignment="viewStart" + android:textSize="17sp" + app:lineHeight="22dp" + tools:text="Setting Name" /> + + <com.google.android.material.textview.MaterialTextView + android:id="@+id/text_setting_value" + style="@style/TextAppearance.Material3.LabelMedium" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacing_small" + android:textAlignment="viewStart" + android:textStyle="bold" + android:textSize="13sp" + tools:text="1x" /> + + </LinearLayout> + + <Button + android:id="@+id/button_options" + style="?attr/materialIconButtonStyle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:nextFocusRight="@id/setting_body" + app:icon="@drawable/ic_more_vert" + app:iconSize="24dp" + app:iconTint="?attr/colorOnSurface" /> + + </LinearLayout> + +</RelativeLayout> diff --git a/src/android/app/src/main/res/layout/card_driver_option.xml b/src/android/app/src/main/res/layout/card_driver_option.xml index bda524f0f..09e26990b 100644 --- a/src/android/app/src/main/res/layout/card_driver_option.xml +++ b/src/android/app/src/main/res/layout/card_driver_option.xml @@ -39,10 +39,7 @@ style="@style/TextAppearance.Material3.TitleMedium" android:layout_width="match_parent" android:layout_height="wrap_content" - android:ellipsize="none" - android:marqueeRepeatLimit="marquee_forever" android:requiresFadingEdge="horizontal" - android:singleLine="true" android:textAlignment="viewStart" tools:text="@string/select_gpu_driver_default" /> @@ -52,10 +49,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="6dp" - android:ellipsize="none" - android:marqueeRepeatLimit="marquee_forever" android:requiresFadingEdge="horizontal" - android:singleLine="true" android:textAlignment="viewStart" tools:text="@string/install_gpu_driver_description" /> @@ -65,10 +59,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="6dp" - android:ellipsize="none" - android:marqueeRepeatLimit="marquee_forever" android:requiresFadingEdge="horizontal" - android:singleLine="true" android:textAlignment="viewStart" tools:text="@string/install_gpu_driver_description" /> diff --git a/src/android/app/src/main/res/layout/card_folder.xml b/src/android/app/src/main/res/layout/card_folder.xml index ed4a7ca8f..e3a5f1a86 100644 --- a/src/android/app/src/main/res/layout/card_folder.xml +++ b/src/android/app/src/main/res/layout/card_folder.xml @@ -21,10 +21,7 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_gravity="center_vertical|start" - android:ellipsize="none" - android:marqueeRepeatLimit="marquee_forever" android:requiresFadingEdge="horizontal" - android:singleLine="true" android:textAlignment="viewStart" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="@+id/button_layout" diff --git a/src/android/app/src/main/res/layout/card_game.xml b/src/android/app/src/main/res/layout/card_game.xml index 6340171ec..411b50315 100644 --- a/src/android/app/src/main/res/layout/card_game.xml +++ b/src/android/app/src/main/res/layout/card_game.xml @@ -40,10 +40,7 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginTop="8dp" - android:ellipsize="none" - android:marqueeRepeatLimit="marquee_forever" android:requiresFadingEdge="horizontal" - android:singleLine="true" android:textAlignment="center" android:textSize="14sp" app:layout_constraintEnd_toEndOf="@+id/image_game_screen" diff --git a/src/android/app/src/main/res/layout/card_simple_outlined.xml b/src/android/app/src/main/res/layout/card_simple_outlined.xml index b73930e7e..e29df6a2d 100644 --- a/src/android/app/src/main/res/layout/card_simple_outlined.xml +++ b/src/android/app/src/main/res/layout/card_simple_outlined.xml @@ -59,9 +59,6 @@ android:textAlignment="viewStart" android:textSize="14sp" android:textStyle="bold" - android:singleLine="true" - android:marqueeRepeatLimit="marquee_forever" - android:ellipsize="none" android:requiresFadingEdge="horizontal" android:layout_marginTop="6dp" android:visibility="gone" diff --git a/src/android/app/src/main/res/layout/dialog_input_profiles.xml b/src/android/app/src/main/res/layout/dialog_input_profiles.xml new file mode 100644 index 000000000..6ad76fe41 --- /dev/null +++ b/src/android/app/src/main/res/layout/dialog_input_profiles.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/list_profiles" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:fadeScrollbars="false" /> diff --git a/src/android/app/src/main/res/layout/dialog_mapping.xml b/src/android/app/src/main/res/layout/dialog_mapping.xml new file mode 100644 index 000000000..06190b8d2 --- /dev/null +++ b/src/android/app/src/main/res/layout/dialog_mapping.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + xmlns:tools="http://schemas.android.com/tools" + android:defaultFocusHighlightEnabled="false" + android:focusable="true" + android:focusableInTouchMode="true" + android:focusedByDefault="true" + android:orientation="horizontal" + android:gravity="center"> + + <ImageView + android:id="@+id/image_stick_animation" + android:layout_width="@dimen/mapping_anim_size" + android:layout_height="@dimen/mapping_anim_size" + tools:src="@drawable/stick_two_direction_anim" /> + + <ImageView + android:id="@+id/image_button_animation" + android:layout_width="@dimen/mapping_anim_size" + android:layout_height="@dimen/mapping_anim_size" + android:layout_marginStart="48dp" + tools:src="@drawable/button_anim" /> + +</LinearLayout> diff --git a/src/android/app/src/main/res/layout/fragment_game_properties.xml b/src/android/app/src/main/res/layout/fragment_game_properties.xml index 436ebd79d..5e3f3cf28 100644 --- a/src/android/app/src/main/res/layout/fragment_game_properties.xml +++ b/src/android/app/src/main/res/layout/fragment_game_properties.xml @@ -76,10 +76,7 @@ android:layout_marginTop="12dp" android:layout_marginBottom="12dp" android:layout_marginHorizontal="16dp" - android:ellipsize="none" - android:marqueeRepeatLimit="marquee_forever" android:requiresFadingEdge="horizontal" - android:singleLine="true" android:textAlignment="center" tools:text="deko_basic" /> diff --git a/src/android/app/src/main/res/layout/list_item_input_profile.xml b/src/android/app/src/main/res/layout/list_item_input_profile.xml new file mode 100644 index 000000000..a08dccf0c --- /dev/null +++ b/src/android/app/src/main/res/layout/list_item_input_profile.xml @@ -0,0 +1,74 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout 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:layout_width="match_parent" + android:layout_height="wrap_content" + android:focusable="false" + android:paddingHorizontal="20dp" + android:paddingVertical="16dp"> + + <com.google.android.material.textview.MaterialTextView + android:id="@+id/title" + style="@style/TextAppearance.Material3.HeadlineMedium" + android:layout_width="0dp" + android:layout_height="0dp" + android:textAlignment="viewStart" + android:gravity="start|center_vertical" + android:textSize="17sp" + android:layout_marginEnd="16dp" + app:layout_constraintBottom_toBottomOf="@+id/button_layout" + app:layout_constraintEnd_toStartOf="@+id/button_layout" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:lineHeight="28dp" + tools:text="My profile" /> + + <LinearLayout + android:id="@+id/button_layout" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:gravity="center_vertical" + android:orientation="horizontal" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="parent"> + + <Button + android:id="@+id/button_new" + style="@style/Widget.Material3.Button.IconButton.Filled.Tonal" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:contentDescription="@string/create_new_profile" + android:tooltipText="@string/create_new_profile" + app:icon="@drawable/ic_new_label" /> + + <Button + android:id="@+id/button_delete" + style="@style/Widget.Material3.Button.IconButton.Filled.Tonal" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:contentDescription="@string/delete" + android:tooltipText="@string/delete" + app:icon="@drawable/ic_delete" /> + + <Button + android:id="@+id/button_save" + style="@style/Widget.Material3.Button.IconButton.Filled.Tonal" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:contentDescription="@string/save" + android:tooltipText="@string/save" + app:icon="@drawable/ic_save" /> + + <Button + android:id="@+id/button_load" + style="@style/Widget.Material3.Button.IconButton.Filled.Tonal" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:contentDescription="@string/load" + android:tooltipText="@string/load" + app:icon="@drawable/ic_import" /> + + </LinearLayout> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/src/android/app/src/main/res/layout/list_item_setting_input.xml b/src/android/app/src/main/res/layout/list_item_setting_input.xml new file mode 100644 index 000000000..d67cbe245 --- /dev/null +++ b/src/android/app/src/main/res/layout/list_item_setting_input.xml @@ -0,0 +1,63 @@ +<?xml version="1.0" encoding="utf-8"?> +<RelativeLayout 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/setting_body" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="?android:attr/selectableItemBackground" + android:clickable="true" + android:focusable="true" + android:gravity="center_vertical" + android:minHeight="72dp" + android:padding="16dp" + android:nextFocusRight="@id/button_options"> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="center_vertical" + android:orientation="horizontal"> + + <LinearLayout + android:layout_width="0dp" + android:layout_height="wrap_content" + android:orientation="vertical" + android:layout_weight="1"> + + <com.google.android.material.textview.MaterialTextView + android:id="@+id/text_setting_name" + style="@style/TextAppearance.Material3.HeadlineMedium" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:textAlignment="viewStart" + android:textSize="17sp" + app:lineHeight="22dp" + tools:text="Setting Name" /> + + <com.google.android.material.textview.MaterialTextView + android:id="@+id/text_setting_value" + style="@style/TextAppearance.Material3.LabelMedium" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacing_small" + android:textAlignment="viewStart" + android:textStyle="bold" + android:textSize="13sp" + tools:text="1x" /> + + </LinearLayout> + + <Button + android:id="@+id/button_options" + style="?attr/materialIconButtonStyle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:nextFocusLeft="@id/setting_body" + app:icon="@drawable/ic_more_vert" + app:iconSize="24dp" + app:iconTint="?attr/colorOnSurface" /> + + </LinearLayout> + +</RelativeLayout> diff --git a/src/android/app/src/main/res/menu/menu_in_game.xml b/src/android/app/src/main/res/menu/menu_in_game.xml index eecb0563b..867197ebc 100644 --- a/src/android/app/src/main/res/menu/menu_in_game.xml +++ b/src/android/app/src/main/res/menu/menu_in_game.xml @@ -17,8 +17,13 @@ android:title="@string/per_game_settings" /> <item - android:id="@+id/menu_overlay_controls" + android:id="@+id/menu_controls" android:icon="@drawable/ic_controller" + android:title="@string/preferences_controls" /> + + <item + android:id="@+id/menu_overlay_controls" + android:icon="@drawable/ic_overlay" android:title="@string/emulation_input_overlay" /> <item diff --git a/src/android/app/src/main/res/menu/menu_input_options.xml b/src/android/app/src/main/res/menu/menu_input_options.xml new file mode 100644 index 000000000..81ea5043f --- /dev/null +++ b/src/android/app/src/main/res/menu/menu_input_options.xml @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + + <item + android:id="@+id/invert_axis" + android:title="@string/invert_axis" + android:visible="false" /> + + <item + android:id="@+id/invert_button" + android:title="@string/invert_button" + android:visible="false" /> + + <item + android:id="@+id/toggle_button" + android:title="@string/toggle_button" + android:visible="false" /> + + <item + android:id="@+id/turbo_button" + android:title="@string/turbo_button" + android:visible="false" /> + + <item + android:id="@+id/set_threshold" + android:title="@string/set_threshold" + android:visible="false" /> + + <item + android:id="@+id/toggle_axis" + android:title="@string/toggle_axis" + android:visible="false" /> + +</menu> diff --git a/src/android/app/src/main/res/navigation/settings_navigation.xml b/src/android/app/src/main/res/navigation/settings_navigation.xml index 1d87d36b3..e4c66e7d5 100644 --- a/src/android/app/src/main/res/navigation/settings_navigation.xml +++ b/src/android/app/src/main/res/navigation/settings_navigation.xml @@ -26,7 +26,7 @@ <fragment android:id="@+id/settingsSearchFragment" - android:name="org.yuzu.yuzu_emu.fragments.SettingsSearchFragment" + android:name="org.yuzu.yuzu_emu.features.settings.ui.SettingsSearchFragment" android:label="SettingsSearchFragment" /> </navigation> diff --git a/src/android/app/src/main/res/values-w600dp/dimens.xml b/src/android/app/src/main/res/values-w600dp/dimens.xml index 128319e27..0e2d40876 100644 --- a/src/android/app/src/main/res/values-w600dp/dimens.xml +++ b/src/android/app/src/main/res/values-w600dp/dimens.xml @@ -2,4 +2,6 @@ <resources> <dimen name="spacing_navigation">0dp</dimen> <dimen name="spacing_navigation_rail">80dp</dimen> + + <dimen name="mapping_anim_size">100dp</dimen> </resources> diff --git a/src/android/app/src/main/res/values/dimens.xml b/src/android/app/src/main/res/values/dimens.xml index 992b5ae44..bf733637f 100644 --- a/src/android/app/src/main/res/values/dimens.xml +++ b/src/android/app/src/main/res/values/dimens.xml @@ -18,4 +18,6 @@ <dimen name="dialog_margin">20dp</dimen> <dimen name="elevated_app_bar">3dp</dimen> + + <dimen name="mapping_anim_size">75dp</dimen> </resources> diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index 78a4c958a..6a631f664 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -255,6 +255,92 @@ <string name="audio_volume">Volume</string> <string name="audio_volume_description">Specifies the volume of audio output.</string> + <!-- Input strings --> + <string name="buttons">Buttons</string> + <string name="button_a">A</string> + <string name="button_b">B</string> + <string name="button_x">X</string> + <string name="button_y">Y</string> + <string name="button_plus">Plus</string> + <string name="button_minus">Minus</string> + <string name="button_home">Home</string> + <string name="button_capture">Capture</string> + <string name="start_pause">Start/Pause</string> + <string name="dpad">D-Pad</string> + <string name="up">Up</string> + <string name="down">Down</string> + <string name="left">Left</string> + <string name="right">Right</string> + <string name="left_stick">Left stick</string> + <string name="control_stick">Control stick</string> + <string name="right_stick">Right stick</string> + <string name="c_stick">C-Stick</string> + <string name="pressed">Pressed</string> + <string name="range">Range</string> + <string name="deadzone">Deadzone</string> + <string name="modifier">Modifier</string> + <string name="modifier_range">Modifier range</string> + <string name="triggers">Triggers</string> + <string name="button_l">L</string> + <string name="button_r">R</string> + <string name="button_zl">ZL</string> + <string name="button_zr">ZR</string> + <string name="button_sl_left">Left SL</string> + <string name="button_sr_left">Left SR</string> + <string name="button_sl_right">Right SL</string> + <string name="button_sr_right">Right SR</string> + <string name="button_z">Z</string> + <string name="invalid">Invalid</string> + <string name="not_set">Not set</string> + <string name="unknown">Unknown</string> + <string name="qualified_hat">%1$s%2$s%3$sHat %4$s</string> + <string name="qualified_button_stick_axis">%1$s%2$s%3$sAxis %4$s</string> + <string name="qualified_button">%1$s%2$s%3$sButton %4$s</string> + <string name="qualified_axis">Axis %1$s%2$s</string> + <string name="unused">Unused</string> + <string name="input_prompt">Move or press an input</string> + <string name="unsupported_input">Unsupported input type</string> + <string name="input_mapping_filter">Input mapping filter</string> + <string name="input_mapping_filter_description">Select a device to filter mapping inputs</string> + <string name="auto_map">Auto-map a controller</string> + <string name="auto_map_description">Select a device to attempt auto-mapping</string> + <string name="attempted_auto_map">Attempted auto-map with %1$s</string> + <string name="controller_type">Controller type</string> + <string name="pro_controller">Pro Controller</string> + <string name="handheld">Handheld</string> + <string name="dual_joycons">Dual Joycons</string> + <string name="left_joycon">Left Joycon</string> + <string name="right_joycon">Right Joycon</string> + <string name="gamecube_controller">GameCube Controller</string> + <string name="invert_axis">Invert axis</string> + <string name="invert_button">Invert button</string> + <string name="toggle_button">Toggle button</string> + <string name="turbo_button">Turbo button</string> + <string name="set_threshold">Set threshold</string> + <string name="toggle_axis">Toggle axis</string> + <string name="connected">Connected</string> + <string name="use_system_vibrator">Use system vibrator</string> + <string name="input_overlay">Input overlay</string> + <string name="vibration">Vibration</string> + <string name="vibration_strength">Vibration strength</string> + <string name="profile">Profile</string> + <string name="create_new_profile">Create new profile</string> + <string name="enter_profile_name">Enter profile name</string> + <string name="profile_name_already_exists">Profile name already exists</string> + <string name="invalid_profile_name">Invalid profile name</string> + <string name="use_global_input_configuration">Use global input configuration</string> + <string name="player_num_profile">Player %d profile</string> + <string name="delete_input_profile">Delete input profile</string> + <string name="delete_input_profile_description">Are you sure that you want to delete this profile? This is not recoverable.</string> + <string name="stick_map_description">Move a stick left and then up or press a button</string> + <string name="button_map_description">Press a button or move a trigger/stick</string> + <string name="map_dpad_direction">Map to D-Pad %1$s</string> + <string name="map_control">Map to %1$s</string> + <string name="failed_to_load_profile">Failed to load profile</string> + <string name="failed_to_save_profile">Failed to save profile</string> + <string name="reset_mapping">Reset mappings</string> + <string name="reset_mapping_description">Are you sure that you want to reset all mappings for this controller to default? This cannot be undone.</string> + <!-- Miscellaneous --> <string name="slider_default">Default</string> <string name="ini_saved">Saved settings</string> @@ -292,6 +378,10 @@ <string name="more_options">More options</string> <string name="use_global_setting">Use global setting</string> <string name="operation_completed_successfully">The operation completed successfully</string> + <string name="retry">Retry</string> + <string name="confirm">Confirm</string> + <string name="load">Load</string> + <string name="save">Save</string> <!-- GPU driver installation --> <string name="select_gpu_driver">Select GPU driver</string> @@ -313,6 +403,9 @@ <string name="preferences_graphics_description">Accuracy level, resolution, shader cache</string> <string name="preferences_audio">Audio</string> <string name="preferences_audio_description">Output engine, volume</string> + <string name="preferences_controls">Controls</string> + <string name="preferences_controls_description">Map controller input</string> + <string name="preferences_player">Player %d</string> <string name="preferences_theme">Theme and color</string> <string name="preferences_debug">Debug</string> <string name="preferences_debug_description">CPU/GPU debugging, graphics API, fastmem</string> diff --git a/src/common/android/id_cache.cpp b/src/common/android/id_cache.cpp index f39262db9..1145cbdf2 100644 --- a/src/common/android/id_cache.cpp +++ b/src/common/android/id_cache.cpp @@ -65,6 +65,30 @@ static jclass s_boolean_class; static jmethodID s_boolean_constructor; static jfieldID s_boolean_value_field; +static jclass s_player_input_class; +static jmethodID s_player_input_constructor; +static jfieldID s_player_input_connected_field; +static jfieldID s_player_input_buttons_field; +static jfieldID s_player_input_analogs_field; +static jfieldID s_player_input_motions_field; +static jfieldID s_player_input_vibration_enabled_field; +static jfieldID s_player_input_vibration_strength_field; +static jfieldID s_player_input_body_color_left_field; +static jfieldID s_player_input_body_color_right_field; +static jfieldID s_player_input_button_color_left_field; +static jfieldID s_player_input_button_color_right_field; +static jfieldID s_player_input_profile_name_field; +static jfieldID s_player_input_use_system_vibrator_field; + +static jclass s_yuzu_input_device_interface; +static jmethodID s_yuzu_input_device_get_name; +static jmethodID s_yuzu_input_device_get_guid; +static jmethodID s_yuzu_input_device_get_port; +static jmethodID s_yuzu_input_device_get_supports_vibration; +static jmethodID s_yuzu_input_device_vibrate; +static jmethodID s_yuzu_input_device_get_axes; +static jmethodID s_yuzu_input_device_has_keys; + static constexpr jint JNI_VERSION = JNI_VERSION_1_6; namespace Common::Android { @@ -276,6 +300,94 @@ jfieldID GetBooleanValueField() { return s_boolean_value_field; } +jclass GetPlayerInputClass() { + return s_player_input_class; +} + +jmethodID GetPlayerInputConstructor() { + return s_player_input_constructor; +} + +jfieldID GetPlayerInputConnectedField() { + return s_player_input_connected_field; +} + +jfieldID GetPlayerInputButtonsField() { + return s_player_input_buttons_field; +} + +jfieldID GetPlayerInputAnalogsField() { + return s_player_input_analogs_field; +} + +jfieldID GetPlayerInputMotionsField() { + return s_player_input_motions_field; +} + +jfieldID GetPlayerInputVibrationEnabledField() { + return s_player_input_vibration_enabled_field; +} + +jfieldID GetPlayerInputVibrationStrengthField() { + return s_player_input_vibration_strength_field; +} + +jfieldID GetPlayerInputBodyColorLeftField() { + return s_player_input_body_color_left_field; +} + +jfieldID GetPlayerInputBodyColorRightField() { + return s_player_input_body_color_right_field; +} + +jfieldID GetPlayerInputButtonColorLeftField() { + return s_player_input_button_color_left_field; +} + +jfieldID GetPlayerInputButtonColorRightField() { + return s_player_input_button_color_right_field; +} + +jfieldID GetPlayerInputProfileNameField() { + return s_player_input_profile_name_field; +} + +jfieldID GetPlayerInputUseSystemVibratorField() { + return s_player_input_use_system_vibrator_field; +} + +jclass GetYuzuInputDeviceInterface() { + return s_yuzu_input_device_interface; +} + +jmethodID GetYuzuDeviceGetName() { + return s_yuzu_input_device_get_name; +} + +jmethodID GetYuzuDeviceGetGUID() { + return s_yuzu_input_device_get_guid; +} + +jmethodID GetYuzuDeviceGetPort() { + return s_yuzu_input_device_get_port; +} + +jmethodID GetYuzuDeviceGetSupportsVibration() { + return s_yuzu_input_device_get_supports_vibration; +} + +jmethodID GetYuzuDeviceVibrate() { + return s_yuzu_input_device_vibrate; +} + +jmethodID GetYuzuDeviceGetAxes() { + return s_yuzu_input_device_get_axes; +} + +jmethodID GetYuzuDeviceHasKeys() { + return s_yuzu_input_device_has_keys; +} + #ifdef __cplusplus extern "C" { #endif @@ -387,6 +499,55 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) { s_boolean_value_field = env->GetFieldID(boolean_class, "value", "Z"); env->DeleteLocalRef(boolean_class); + const jclass player_input_class = + env->FindClass("org/yuzu/yuzu_emu/features/input/model/PlayerInput"); + s_player_input_class = reinterpret_cast<jclass>(env->NewGlobalRef(player_input_class)); + s_player_input_constructor = env->GetMethodID( + player_input_class, "<init>", + "(Z[Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/String;ZIJJJJLjava/lang/String;Z)V"); + s_player_input_connected_field = env->GetFieldID(player_input_class, "connected", "Z"); + s_player_input_buttons_field = + env->GetFieldID(player_input_class, "buttons", "[Ljava/lang/String;"); + s_player_input_analogs_field = + env->GetFieldID(player_input_class, "analogs", "[Ljava/lang/String;"); + s_player_input_motions_field = + env->GetFieldID(player_input_class, "motions", "[Ljava/lang/String;"); + s_player_input_vibration_enabled_field = + env->GetFieldID(player_input_class, "vibrationEnabled", "Z"); + s_player_input_vibration_strength_field = + env->GetFieldID(player_input_class, "vibrationStrength", "I"); + s_player_input_body_color_left_field = + env->GetFieldID(player_input_class, "bodyColorLeft", "J"); + s_player_input_body_color_right_field = + env->GetFieldID(player_input_class, "bodyColorRight", "J"); + s_player_input_button_color_left_field = + env->GetFieldID(player_input_class, "buttonColorLeft", "J"); + s_player_input_button_color_right_field = + env->GetFieldID(player_input_class, "buttonColorRight", "J"); + s_player_input_profile_name_field = + env->GetFieldID(player_input_class, "profileName", "Ljava/lang/String;"); + s_player_input_use_system_vibrator_field = + env->GetFieldID(player_input_class, "useSystemVibrator", "Z"); + env->DeleteLocalRef(player_input_class); + + const jclass yuzu_input_device_interface = + env->FindClass("org/yuzu/yuzu_emu/features/input/YuzuInputDevice"); + s_yuzu_input_device_interface = + reinterpret_cast<jclass>(env->NewGlobalRef(yuzu_input_device_interface)); + s_yuzu_input_device_get_name = + env->GetMethodID(yuzu_input_device_interface, "getName", "()Ljava/lang/String;"); + s_yuzu_input_device_get_guid = + env->GetMethodID(yuzu_input_device_interface, "getGUID", "()Ljava/lang/String;"); + s_yuzu_input_device_get_port = env->GetMethodID(yuzu_input_device_interface, "getPort", "()I"); + s_yuzu_input_device_get_supports_vibration = + env->GetMethodID(yuzu_input_device_interface, "getSupportsVibration", "()Z"); + s_yuzu_input_device_vibrate = env->GetMethodID(yuzu_input_device_interface, "vibrate", "(F)V"); + s_yuzu_input_device_get_axes = + env->GetMethodID(yuzu_input_device_interface, "getAxes", "()[Ljava/lang/Integer;"); + s_yuzu_input_device_has_keys = + env->GetMethodID(yuzu_input_device_interface, "hasKeys", "([I)[Z"); + env->DeleteLocalRef(yuzu_input_device_interface); + // Initialize Android Storage Common::FS::Android::RegisterCallbacks(env, s_native_library_class); @@ -416,6 +577,8 @@ void JNI_OnUnload(JavaVM* vm, void* reserved) { env->DeleteGlobalRef(s_double_class); env->DeleteGlobalRef(s_integer_class); env->DeleteGlobalRef(s_boolean_class); + env->DeleteGlobalRef(s_player_input_class); + env->DeleteGlobalRef(s_yuzu_input_device_interface); // UnInitialize applets SoftwareKeyboard::CleanupJNI(env); diff --git a/src/common/android/id_cache.h b/src/common/android/id_cache.h index 47802f96c..cd2844dcc 100644 --- a/src/common/android/id_cache.h +++ b/src/common/android/id_cache.h @@ -85,4 +85,28 @@ jclass GetBooleanClass(); jmethodID GetBooleanConstructor(); jfieldID GetBooleanValueField(); +jclass GetPlayerInputClass(); +jmethodID GetPlayerInputConstructor(); +jfieldID GetPlayerInputConnectedField(); +jfieldID GetPlayerInputButtonsField(); +jfieldID GetPlayerInputAnalogsField(); +jfieldID GetPlayerInputMotionsField(); +jfieldID GetPlayerInputVibrationEnabledField(); +jfieldID GetPlayerInputVibrationStrengthField(); +jfieldID GetPlayerInputBodyColorLeftField(); +jfieldID GetPlayerInputBodyColorRightField(); +jfieldID GetPlayerInputButtonColorLeftField(); +jfieldID GetPlayerInputButtonColorRightField(); +jfieldID GetPlayerInputProfileNameField(); +jfieldID GetPlayerInputUseSystemVibratorField(); + +jclass GetYuzuInputDeviceInterface(); +jmethodID GetYuzuDeviceGetName(); +jmethodID GetYuzuDeviceGetGUID(); +jmethodID GetYuzuDeviceGetPort(); +jmethodID GetYuzuDeviceGetSupportsVibration(); +jmethodID GetYuzuDeviceVibrate(); +jmethodID GetYuzuDeviceGetAxes(); +jmethodID GetYuzuDeviceHasKeys(); + } // namespace Common::Android diff --git a/src/common/settings_input.h b/src/common/settings_input.h index 53a95ef8f..a99bb0892 100644 --- a/src/common/settings_input.h +++ b/src/common/settings_input.h @@ -395,6 +395,10 @@ struct PlayerInput { u32 button_color_left; u32 button_color_right; std::string profile_name; + + // This is meant to tell the Android frontend whether to use a device's built-in vibration + // motor or a controller's vibrations. + bool use_system_vibrator; }; struct TouchscreenInput { diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index f70e093ca..fa5c4de37 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -489,12 +489,12 @@ add_library(core STATIC hle/service/apm/apm_controller.h hle/service/apm/apm_interface.cpp hle/service/apm/apm_interface.h - hle/service/audio/audctl.cpp - hle/service/audio/audctl.h hle/service/audio/audin_u.cpp hle/service/audio/audin_u.h hle/service/audio/audio.cpp hle/service/audio/audio.h + hle/service/audio/audio_controller.cpp + hle/service/audio/audio_controller.h hle/service/audio/audout_u.cpp hle/service/audio/audout_u.h hle/service/audio/audrec_a.cpp @@ -737,15 +737,48 @@ add_library(core STATIC hle/service/nim/nim.h hle/service/npns/npns.cpp hle/service/npns/npns.h - hle/service/ns/errors.h - hle/service/ns/iplatform_service_manager.cpp - hle/service/ns/iplatform_service_manager.h + hle/service/ns/account_proxy_interface.cpp + hle/service/ns/account_proxy_interface.h + hle/service/ns/application_manager_interface.cpp + hle/service/ns/application_manager_interface.h + hle/service/ns/application_version_interface.cpp + hle/service/ns/application_version_interface.h + hle/service/ns/content_management_interface.cpp + hle/service/ns/content_management_interface.h + hle/service/ns/develop_interface.cpp + hle/service/ns/develop_interface.h + hle/service/ns/document_interface.cpp + hle/service/ns/document_interface.h + hle/service/ns/download_task_interface.cpp + hle/service/ns/download_task_interface.h + hle/service/ns/dynamic_rights_interface.cpp + hle/service/ns/dynamic_rights_interface.h + hle/service/ns/ecommerce_interface.cpp + hle/service/ns/ecommerce_interface.h + hle/service/ns/factory_reset_interface.cpp + hle/service/ns/factory_reset_interface.h hle/service/ns/language.cpp hle/service/ns/language.h + hle/service/ns/ns_results.h + hle/service/ns/ns_types.h hle/service/ns/ns.cpp hle/service/ns/ns.h - hle/service/ns/pdm_qry.cpp - hle/service/ns/pdm_qry.h + hle/service/ns/platform_service_manager.cpp + hle/service/ns/platform_service_manager.h + hle/service/ns/query_service.cpp + hle/service/ns/query_service.h + hle/service/ns/read_only_application_control_data_interface.cpp + hle/service/ns/read_only_application_control_data_interface.h + hle/service/ns/read_only_application_record_interface.cpp + hle/service/ns/read_only_application_record_interface.h + hle/service/ns/service_getter_interface.cpp + hle/service/ns/service_getter_interface.h + hle/service/ns/system_update_control.cpp + hle/service/ns/system_update_control.h + hle/service/ns/system_update_interface.cpp + hle/service/ns/system_update_interface.h + hle/service/ns/vulnerability_manager_interface.cpp + hle/service/ns/vulnerability_manager_interface.h hle/service/nvdrv/core/container.cpp hle/service/nvdrv/core/container.h hle/service/nvdrv/core/heap_mapper.cpp diff --git a/src/core/file_sys/control_metadata.h b/src/core/file_sys/control_metadata.h index 555b9d8f7..667efbbab 100644 --- a/src/core/file_sys/control_metadata.h +++ b/src/core/file_sys/control_metadata.h @@ -64,8 +64,8 @@ struct RawNACP { u64_le cache_storage_size; u64_le cache_storage_journal_size; u64_le cache_storage_data_and_journal_max_size; - u64_le cache_storage_max_index; - INSERT_PADDING_BYTES(0xE70); + u16_le cache_storage_max_index; + INSERT_PADDING_BYTES(0xE76); }; static_assert(sizeof(RawNACP) == 0x4000, "RawNACP has incorrect size."); diff --git a/src/core/file_sys/system_archive/shared_font.cpp b/src/core/file_sys/system_archive/shared_font.cpp index deb52069d..9ea16aa59 100644 --- a/src/core/file_sys/system_archive/shared_font.cpp +++ b/src/core/file_sys/system_archive/shared_font.cpp @@ -9,7 +9,7 @@ #include "core/file_sys/system_archive/data/font_standard.h" #include "core/file_sys/system_archive/shared_font.h" #include "core/file_sys/vfs/vfs_vector.h" -#include "core/hle/service/ns/iplatform_service_manager.h" +#include "core/hle/service/ns/platform_service_manager.h" namespace FileSys::SystemArchive { diff --git a/src/core/hle/service/acc/profile_manager.cpp b/src/core/hle/service/acc/profile_manager.cpp index 29a10ad13..ee9795532 100644 --- a/src/core/hle/service/acc/profile_manager.cpp +++ b/src/core/hle/service/acc/profile_manager.cpp @@ -329,9 +329,8 @@ bool ProfileManager::GetProfileBaseAndData(const ProfileInfo& user, ProfileBase& /// Returns if the system is allowing user registrations or not bool ProfileManager::CanSystemRegisterUser() const { - return false; // TODO(ogniK): Games shouldn't have - // access to user registration, when we - // emulate qlaunch. Update this to dynamically change. + // TODO: Both games and applets can register users. Determine when this condition is not meet. + return true; } bool ProfileManager::RemoveUser(UUID uuid) { diff --git a/src/core/hle/service/am/applet_manager.cpp b/src/core/hle/service/am/applet_manager.cpp index 4c7266f89..2e109181d 100644 --- a/src/core/hle/service/am/applet_manager.cpp +++ b/src/core/hle/service/am/applet_manager.cpp @@ -35,6 +35,21 @@ AppletStorageChannel& InitializeFakeCallerApplet(Core::System& system, return applet->caller_applet_broker->GetInData(); } +void PushInShowQlaunch(Core::System& system, AppletStorageChannel& channel) { + const CommonArguments arguments{ + .arguments_version = CommonArgumentVersion::Version3, + .size = CommonArgumentSize::Version3, + .library_version = 0, + .theme_color = ThemeColor::BasicBlack, + .play_startup_sound = true, + .system_tick = system.CoreTiming().GetClockTicks(), + }; + + std::vector<u8> argument_data(sizeof(arguments)); + std::memcpy(argument_data.data(), &arguments, sizeof(arguments)); + channel.Push(std::make_shared<IStorage>(system, std::move(argument_data))); +} + void PushInShowAlbum(Core::System& system, AppletStorageChannel& channel) { const CommonArguments arguments{ .arguments_version = CommonArgumentVersion::Version3, @@ -284,6 +299,9 @@ void AppletManager::CreateAndInsertByFrontendAppletParameters( // Starting from frontend, some applets require input data. switch (applet->applet_id) { + case AppletId::QLaunch: + PushInShowQlaunch(m_system, InitializeFakeCallerApplet(m_system, applet)); + break; case AppletId::Cabinet: PushInShowCabinetData(m_system, InitializeFakeCallerApplet(m_system, applet)); break; diff --git a/src/core/hle/service/am/frontend/applet_web_browser.cpp b/src/core/hle/service/am/frontend/applet_web_browser.cpp index bb60260b4..835c20c4e 100644 --- a/src/core/hle/service/am/frontend/applet_web_browser.cpp +++ b/src/core/hle/service/am/frontend/applet_web_browser.cpp @@ -22,7 +22,7 @@ #include "core/hle/service/am/frontend/applet_web_browser.h" #include "core/hle/service/am/service/storage.h" #include "core/hle/service/filesystem/filesystem.h" -#include "core/hle/service/ns/iplatform_service_manager.h" +#include "core/hle/service/ns/platform_service_manager.h" #include "core/loader/loader.h" namespace Service::AM::Frontend { diff --git a/src/core/hle/service/am/library_applet_storage.cpp b/src/core/hle/service/am/library_applet_storage.cpp index 46e6c0111..0412c215d 100644 --- a/src/core/hle/service/am/library_applet_storage.cpp +++ b/src/core/hle/service/am/library_applet_storage.cpp @@ -70,7 +70,7 @@ public: Result Read(s64 offset, void* buffer, size_t size) override { R_TRY(ValidateOffset(offset, size, m_size)); - m_memory.ReadBlock(m_trmem->GetSourceAddress(), buffer, size); + m_memory.ReadBlock(m_trmem->GetSourceAddress() + offset, buffer, size); R_SUCCEED(); } @@ -79,7 +79,7 @@ public: R_UNLESS(m_is_writable, ResultUnknown); R_TRY(ValidateOffset(offset, size, m_size)); - m_memory.WriteBlock(m_trmem->GetSourceAddress(), buffer, size); + m_memory.WriteBlock(m_trmem->GetSourceAddress() + offset, buffer, size); R_SUCCEED(); } diff --git a/src/core/hle/service/am/service/application_functions.cpp b/src/core/hle/service/am/service/application_functions.cpp index b788fddd4..cb53b07e0 100644 --- a/src/core/hle/service/am/service/application_functions.cpp +++ b/src/core/hle/service/am/service/application_functions.cpp @@ -15,7 +15,9 @@ #include "core/hle/service/cmif_serialization.h" #include "core/hle/service/filesystem/filesystem.h" #include "core/hle/service/filesystem/save_data_controller.h" -#include "core/hle/service/ns/ns.h" +#include "core/hle/service/glue/glue_manager.h" +#include "core/hle/service/ns/application_manager_interface.h" +#include "core/hle/service/ns/service_getter_interface.h" #include "core/hle/service/sm/sm.h" namespace Service::AM { @@ -40,7 +42,7 @@ IApplicationFunctions::IApplicationFunctions(Core::System& system_, std::shared_ {26, D<&IApplicationFunctions::GetSaveDataSize>, "GetSaveDataSize"}, {27, D<&IApplicationFunctions::CreateCacheStorage>, "CreateCacheStorage"}, {28, D<&IApplicationFunctions::GetSaveDataSizeMax>, "GetSaveDataSizeMax"}, - {29, nullptr, "GetCacheStorageMax"}, + {29, D<&IApplicationFunctions::GetCacheStorageMax>, "GetCacheStorageMax"}, {30, D<&IApplicationFunctions::BeginBlockingHomeButtonShortAndLongPressed>, "BeginBlockingHomeButtonShortAndLongPressed"}, {31, D<&IApplicationFunctions::EndBlockingHomeButtonShortAndLongPressed>, "EndBlockingHomeButtonShortAndLongPressed"}, {32, D<&IApplicationFunctions::BeginBlockingHomeButton>, "BeginBlockingHomeButton"}, @@ -162,11 +164,13 @@ Result IApplicationFunctions::GetDesiredLanguage(Out<u64> out_language_code) { // Call IApplicationManagerInterface implementation. auto& service_manager = system.ServiceManager(); - auto ns_am2 = service_manager.GetService<NS::NS>("ns:am2"); - auto app_man = ns_am2->GetApplicationManagerInterface(); + auto ns_am2 = service_manager.GetService<NS::IServiceGetterInterface>("ns:am2"); + + std::shared_ptr<NS::IApplicationManagerInterface> app_man; + R_TRY(ns_am2->GetApplicationManagerInterface(&app_man)); // Get desired application language - u8 desired_language{}; + NS::ApplicationLanguage desired_language{}; R_TRY(app_man->GetApplicationDesiredLanguage(&desired_language, supported_languages)); // Convert to settings language code. @@ -267,6 +271,22 @@ Result IApplicationFunctions::GetSaveDataSizeMax(Out<u64> out_max_normal_size, R_SUCCEED(); } +Result IApplicationFunctions::GetCacheStorageMax(Out<u32> out_cache_storage_index_max, + Out<u64> out_max_journal_size) { + LOG_DEBUG(Service_AM, "called"); + + std::vector<u8> nacp; + R_TRY(system.GetARPManager().GetControlProperty(&nacp, m_applet->program_id)); + + auto raw_nacp = std::make_unique<FileSys::RawNACP>(); + std::memcpy(raw_nacp.get(), nacp.data(), std::min(sizeof(*raw_nacp), nacp.size())); + + *out_cache_storage_index_max = static_cast<u32>(raw_nacp->cache_storage_max_index); + *out_max_journal_size = static_cast<u64>(raw_nacp->cache_storage_data_and_journal_max_size); + + R_SUCCEED(); +} + Result IApplicationFunctions::BeginBlockingHomeButtonShortAndLongPressed(s64 unused) { LOG_WARNING(Service_AM, "(STUBBED) called"); diff --git a/src/core/hle/service/am/service/application_functions.h b/src/core/hle/service/am/service/application_functions.h index 3548202f8..10025a152 100644 --- a/src/core/hle/service/am/service/application_functions.h +++ b/src/core/hle/service/am/service/application_functions.h @@ -40,6 +40,7 @@ private: Result CreateCacheStorage(Out<u32> out_target_media, Out<u64> out_required_size, u16 index, u64 normal_size, u64 journal_size); Result GetSaveDataSizeMax(Out<u64> out_max_normal_size, Out<u64> out_max_journal_size); + Result GetCacheStorageMax(Out<u32> out_cache_storage_index_max, Out<u64> out_max_journal_size); Result BeginBlockingHomeButtonShortAndLongPressed(s64 unused); Result EndBlockingHomeButtonShortAndLongPressed(); Result BeginBlockingHomeButton(s64 timeout_ns); diff --git a/src/core/hle/service/am/service/library_applet_self_accessor.cpp b/src/core/hle/service/am/service/library_applet_self_accessor.cpp index 7a3a86e88..330eb26f0 100644 --- a/src/core/hle/service/am/service/library_applet_self_accessor.cpp +++ b/src/core/hle/service/am/service/library_applet_self_accessor.cpp @@ -14,7 +14,8 @@ #include "core/hle/service/cmif_serialization.h" #include "core/hle/service/filesystem/filesystem.h" #include "core/hle/service/glue/glue_manager.h" -#include "core/hle/service/ns/ns.h" +#include "core/hle/service/ns/application_manager_interface.h" +#include "core/hle/service/ns/service_getter_interface.h" #include "core/hle/service/sm/sm.h" namespace Service::AM { @@ -256,11 +257,13 @@ Result ILibraryAppletSelfAccessor::GetMainAppletApplicationDesiredLanguage( // Call IApplicationManagerInterface implementation. auto& service_manager = system.ServiceManager(); - auto ns_am2 = service_manager.GetService<NS::NS>("ns:am2"); - auto app_man = ns_am2->GetApplicationManagerInterface(); + auto ns_am2 = service_manager.GetService<NS::IServiceGetterInterface>("ns:am2"); + + std::shared_ptr<NS::IApplicationManagerInterface> app_man; + R_TRY(ns_am2->GetApplicationManagerInterface(&app_man)); // Get desired application language - u8 desired_language{}; + NS::ApplicationLanguage desired_language{}; R_TRY(app_man->GetApplicationDesiredLanguage(&desired_language, supported_languages)); // Convert to settings language code. @@ -284,17 +287,17 @@ Result ILibraryAppletSelfAccessor::GetCurrentApplicationId(Out<u64> out_applicat } Result ILibraryAppletSelfAccessor::GetMainAppletAvailableUsers( - Out<bool> out_no_users_available, Out<s32> out_users_count, + Out<bool> out_can_select_any_user, Out<s32> out_users_count, OutArray<Common::UUID, BufferAttr_HipcMapAlias> out_users) { const Service::Account::ProfileManager manager{}; - *out_no_users_available = true; + *out_can_select_any_user = false; *out_users_count = -1; LOG_INFO(Service_AM, "called"); if (manager.GetUserCount() > 0) { - *out_no_users_available = false; + *out_can_select_any_user = true; *out_users_count = static_cast<s32>(manager.GetUserCount()); const auto users = manager.GetAllUsers(); diff --git a/src/core/hle/service/am/service/library_applet_self_accessor.h b/src/core/hle/service/am/service/library_applet_self_accessor.h index a9743569f..3e60393c2 100644 --- a/src/core/hle/service/am/service/library_applet_self_accessor.h +++ b/src/core/hle/service/am/service/library_applet_self_accessor.h @@ -71,7 +71,7 @@ private: ErrorCode error_code, InLargeData<ErrorContext, BufferAttr_HipcMapAlias> error_context); Result GetMainAppletApplicationDesiredLanguage(Out<u64> out_desired_language); Result GetCurrentApplicationId(Out<u64> out_application_id); - Result GetMainAppletAvailableUsers(Out<bool> out_no_users_available, Out<s32> out_users_count, + Result GetMainAppletAvailableUsers(Out<bool> out_can_select_any_user, Out<s32> out_users_count, OutArray<Common::UUID, BufferAttr_HipcMapAlias> out_users); Result ShouldSetGpuTimeSliceManually(Out<bool> out_should_set_gpu_time_slice_manually); Result Cmd160(Out<u64> out_unknown0); diff --git a/src/core/hle/service/audio/audctl.cpp b/src/core/hle/service/audio/audctl.cpp deleted file mode 100644 index cf4bb4034..000000000 --- a/src/core/hle/service/audio/audctl.cpp +++ /dev/null @@ -1,201 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#include "common/logging/log.h" -#include "core/hle/service/audio/audctl.h" -#include "core/hle/service/ipc_helpers.h" -#include "core/hle/service/set/system_settings_server.h" -#include "core/hle/service/sm/sm.h" - -namespace Service::Audio { - -AudCtl::AudCtl(Core::System& system_) : ServiceFramework{system_, "audctl"} { - // clang-format off - static const FunctionInfo functions[] = { - {0, nullptr, "GetTargetVolume"}, - {1, nullptr, "SetTargetVolume"}, - {2, &AudCtl::GetTargetVolumeMin, "GetTargetVolumeMin"}, - {3, &AudCtl::GetTargetVolumeMax, "GetTargetVolumeMax"}, - {4, nullptr, "IsTargetMute"}, - {5, nullptr, "SetTargetMute"}, - {6, nullptr, "IsTargetConnected"}, - {7, nullptr, "SetDefaultTarget"}, - {8, nullptr, "GetDefaultTarget"}, - {9, &AudCtl::GetAudioOutputMode, "GetAudioOutputMode"}, - {10, &AudCtl::SetAudioOutputMode, "SetAudioOutputMode"}, - {11, nullptr, "SetForceMutePolicy"}, - {12, &AudCtl::GetForceMutePolicy, "GetForceMutePolicy"}, - {13, &AudCtl::GetOutputModeSetting, "GetOutputModeSetting"}, - {14, &AudCtl::SetOutputModeSetting, "SetOutputModeSetting"}, - {15, nullptr, "SetOutputTarget"}, - {16, nullptr, "SetInputTargetForceEnabled"}, - {17, &AudCtl::SetHeadphoneOutputLevelMode, "SetHeadphoneOutputLevelMode"}, - {18, &AudCtl::GetHeadphoneOutputLevelMode, "GetHeadphoneOutputLevelMode"}, - {19, nullptr, "AcquireAudioVolumeUpdateEventForPlayReport"}, - {20, nullptr, "AcquireAudioOutputDeviceUpdateEventForPlayReport"}, - {21, nullptr, "GetAudioOutputTargetForPlayReport"}, - {22, nullptr, "NotifyHeadphoneVolumeWarningDisplayedEvent"}, - {23, nullptr, "SetSystemOutputMasterVolume"}, - {24, nullptr, "GetSystemOutputMasterVolume"}, - {25, nullptr, "GetAudioVolumeDataForPlayReport"}, - {26, nullptr, "UpdateHeadphoneSettings"}, - {27, nullptr, "SetVolumeMappingTableForDev"}, - {28, nullptr, "GetAudioOutputChannelCountForPlayReport"}, - {29, nullptr, "BindAudioOutputChannelCountUpdateEventForPlayReport"}, - {30, &AudCtl::SetSpeakerAutoMuteEnabled, "SetSpeakerAutoMuteEnabled"}, - {31, &AudCtl::IsSpeakerAutoMuteEnabled, "IsSpeakerAutoMuteEnabled"}, - {32, nullptr, "GetActiveOutputTarget"}, - {33, nullptr, "GetTargetDeviceInfo"}, - {34, nullptr, "AcquireTargetNotification"}, - {35, nullptr, "SetHearingProtectionSafeguardTimerRemainingTimeForDebug"}, - {36, nullptr, "GetHearingProtectionSafeguardTimerRemainingTimeForDebug"}, - {37, nullptr, "SetHearingProtectionSafeguardEnabled"}, - {38, nullptr, "IsHearingProtectionSafeguardEnabled"}, - {39, nullptr, "IsHearingProtectionSafeguardMonitoringOutputForDebug"}, - {40, nullptr, "GetSystemInformationForDebug"}, - {41, nullptr, "SetVolumeButtonLongPressTime"}, - {42, nullptr, "SetNativeVolumeForDebug"}, - {10000, nullptr, "NotifyAudioOutputTargetForPlayReport"}, - {10001, nullptr, "NotifyAudioOutputChannelCountForPlayReport"}, - {10002, nullptr, "NotifyUnsupportedUsbOutputDeviceAttachedForPlayReport"}, - {10100, nullptr, "GetAudioVolumeDataForPlayReport"}, - {10101, nullptr, "BindAudioVolumeUpdateEventForPlayReport"}, - {10102, nullptr, "BindAudioOutputTargetUpdateEventForPlayReport"}, - {10103, nullptr, "GetAudioOutputTargetForPlayReport"}, - {10104, nullptr, "GetAudioOutputChannelCountForPlayReport"}, - {10105, nullptr, "BindAudioOutputChannelCountUpdateEventForPlayReport"}, - {10106, nullptr, "GetDefaultAudioOutputTargetForPlayReport"}, - {50000, nullptr, "SetAnalogInputBoostGainForPrototyping"}, - }; - // clang-format on - - RegisterHandlers(functions); - - m_set_sys = - system.ServiceManager().GetService<Service::Set::ISystemSettingsServer>("set:sys", true); -} - -AudCtl::~AudCtl() = default; - -void AudCtl::GetTargetVolumeMin(HLERequestContext& ctx) { - LOG_DEBUG(Audio, "called."); - - // This service function is currently hardcoded on the - // actual console to this value (as of 8.0.0). - constexpr s32 target_min_volume = 0; - - IPC::ResponseBuilder rb{ctx, 3}; - rb.Push(ResultSuccess); - rb.Push(target_min_volume); -} - -void AudCtl::GetTargetVolumeMax(HLERequestContext& ctx) { - LOG_DEBUG(Audio, "called."); - - // This service function is currently hardcoded on the - // actual console to this value (as of 8.0.0). - constexpr s32 target_max_volume = 15; - - IPC::ResponseBuilder rb{ctx, 3}; - rb.Push(ResultSuccess); - rb.Push(target_max_volume); -} - -void AudCtl::GetAudioOutputMode(HLERequestContext& ctx) { - IPC::RequestParser rp{ctx}; - const auto target{rp.PopEnum<Set::AudioOutputModeTarget>()}; - - Set::AudioOutputMode output_mode{}; - const auto result = m_set_sys->GetAudioOutputMode(&output_mode, target); - - LOG_INFO(Service_SET, "called, target={}, output_mode={}", target, output_mode); - - IPC::ResponseBuilder rb{ctx, 3}; - rb.Push(result); - rb.PushEnum(output_mode); -} - -void AudCtl::SetAudioOutputMode(HLERequestContext& ctx) { - IPC::RequestParser rp{ctx}; - const auto target{rp.PopEnum<Set::AudioOutputModeTarget>()}; - const auto output_mode{rp.PopEnum<Set::AudioOutputMode>()}; - - const auto result = m_set_sys->SetAudioOutputMode(target, output_mode); - - LOG_INFO(Service_SET, "called, target={}, output_mode={}", target, output_mode); - - IPC::ResponseBuilder rb{ctx, 2}; - rb.Push(result); -} - -void AudCtl::GetForceMutePolicy(HLERequestContext& ctx) { - LOG_WARNING(Audio, "(STUBBED) called"); - - IPC::ResponseBuilder rb{ctx, 3}; - rb.Push(ResultSuccess); - rb.PushEnum(ForceMutePolicy::Disable); -} - -void AudCtl::GetOutputModeSetting(HLERequestContext& ctx) { - IPC::RequestParser rp{ctx}; - const auto target{rp.PopEnum<Set::AudioOutputModeTarget>()}; - - LOG_WARNING(Audio, "(STUBBED) called, target={}", target); - - IPC::ResponseBuilder rb{ctx, 3}; - rb.Push(ResultSuccess); - rb.PushEnum(Set::AudioOutputMode::ch_7_1); -} - -void AudCtl::SetOutputModeSetting(HLERequestContext& ctx) { - IPC::RequestParser rp{ctx}; - const auto target{rp.PopEnum<Set::AudioOutputModeTarget>()}; - const auto output_mode{rp.PopEnum<Set::AudioOutputMode>()}; - - LOG_INFO(Service_SET, "called, target={}, output_mode={}", target, output_mode); - - IPC::ResponseBuilder rb{ctx, 2}; - rb.Push(ResultSuccess); -} - -void AudCtl::SetHeadphoneOutputLevelMode(HLERequestContext& ctx) { - LOG_WARNING(Audio, "(STUBBED) called"); - - IPC::ResponseBuilder rb{ctx, 2}; - rb.Push(ResultSuccess); -} - -void AudCtl::GetHeadphoneOutputLevelMode(HLERequestContext& ctx) { - LOG_WARNING(Audio, "(STUBBED) called"); - - IPC::ResponseBuilder rb{ctx, 3}; - rb.Push(ResultSuccess); - rb.PushEnum(HeadphoneOutputLevelMode::Normal); -} - -void AudCtl::SetSpeakerAutoMuteEnabled(HLERequestContext& ctx) { - IPC::RequestParser rp{ctx}; - const auto is_speaker_auto_mute_enabled{rp.Pop<bool>()}; - - LOG_WARNING(Audio, "(STUBBED) called, is_speaker_auto_mute_enabled={}", - is_speaker_auto_mute_enabled); - - const auto result = m_set_sys->SetSpeakerAutoMuteFlag(is_speaker_auto_mute_enabled); - - IPC::ResponseBuilder rb{ctx, 2}; - rb.Push(result); -} - -void AudCtl::IsSpeakerAutoMuteEnabled(HLERequestContext& ctx) { - bool is_speaker_auto_mute_enabled{}; - const auto result = m_set_sys->GetSpeakerAutoMuteFlag(&is_speaker_auto_mute_enabled); - - LOG_WARNING(Audio, "(STUBBED) called, is_speaker_auto_mute_enabled={}", - is_speaker_auto_mute_enabled); - - IPC::ResponseBuilder rb{ctx, 3}; - rb.Push(result); - rb.Push<u8>(is_speaker_auto_mute_enabled); -} - -} // namespace Service::Audio diff --git a/src/core/hle/service/audio/audctl.h b/src/core/hle/service/audio/audctl.h deleted file mode 100644 index 4c90ead70..000000000 --- a/src/core/hle/service/audio/audctl.h +++ /dev/null @@ -1,50 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#pragma once - -#include "core/hle/service/service.h" - -namespace Core { -class System; -} - -namespace Service::Set { -class ISystemSettingsServer; -} - -namespace Service::Audio { - -class AudCtl final : public ServiceFramework<AudCtl> { -public: - explicit AudCtl(Core::System& system_); - ~AudCtl() override; - -private: - enum class ForceMutePolicy { - Disable, - SpeakerMuteOnHeadphoneUnplugged, - }; - - enum class HeadphoneOutputLevelMode { - Normal, - HighPower, - }; - - void GetTargetVolumeMin(HLERequestContext& ctx); - void GetTargetVolumeMax(HLERequestContext& ctx); - void GetAudioOutputMode(HLERequestContext& ctx); - void SetAudioOutputMode(HLERequestContext& ctx); - void GetForceMutePolicy(HLERequestContext& ctx); - void GetOutputModeSetting(HLERequestContext& ctx); - void SetOutputModeSetting(HLERequestContext& ctx); - void SetHeadphoneOutputLevelMode(HLERequestContext& ctx); - void GetHeadphoneOutputLevelMode(HLERequestContext& ctx); - void SetSpeakerAutoMuteEnabled(HLERequestContext& ctx); - void IsSpeakerAutoMuteEnabled(HLERequestContext& ctx); - void AcquireTargetNotification(HLERequestContext& ctx); - - std::shared_ptr<Service::Set::ISystemSettingsServer> m_set_sys; -}; - -} // namespace Service::Audio diff --git a/src/core/hle/service/audio/audio.cpp b/src/core/hle/service/audio/audio.cpp index dccd16309..44af030eb 100644 --- a/src/core/hle/service/audio/audio.cpp +++ b/src/core/hle/service/audio/audio.cpp @@ -2,9 +2,9 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include "core/core.h" -#include "core/hle/service/audio/audctl.h" #include "core/hle/service/audio/audin_u.h" #include "core/hle/service/audio/audio.h" +#include "core/hle/service/audio/audio_controller.h" #include "core/hle/service/audio/audout_u.h" #include "core/hle/service/audio/audrec_a.h" #include "core/hle/service/audio/audrec_u.h" @@ -18,7 +18,7 @@ namespace Service::Audio { void LoopProcess(Core::System& system) { auto server_manager = std::make_unique<ServerManager>(system); - server_manager->RegisterNamedService("audctl", std::make_shared<AudCtl>(system)); + server_manager->RegisterNamedService("audctl", std::make_shared<IAudioController>(system)); server_manager->RegisterNamedService("audout:u", std::make_shared<AudOutU>(system)); server_manager->RegisterNamedService("audin:u", std::make_shared<AudInU>(system)); server_manager->RegisterNamedService("audrec:a", std::make_shared<AudRecA>(system)); diff --git a/src/core/hle/service/audio/audio_controller.cpp b/src/core/hle/service/audio/audio_controller.cpp new file mode 100644 index 000000000..a6da66d0f --- /dev/null +++ b/src/core/hle/service/audio/audio_controller.cpp @@ -0,0 +1,174 @@ +// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "common/logging/log.h" +#include "core/hle/service/audio/audio_controller.h" +#include "core/hle/service/cmif_serialization.h" +#include "core/hle/service/ipc_helpers.h" +#include "core/hle/service/set/system_settings_server.h" +#include "core/hle/service/sm/sm.h" + +namespace Service::Audio { + +IAudioController::IAudioController(Core::System& system_) + : ServiceFramework{system_, "audctl"}, service_context{system, "audctl"} { + // clang-format off + static const FunctionInfo functions[] = { + {0, nullptr, "GetTargetVolume"}, + {1, nullptr, "SetTargetVolume"}, + {2, C<&IAudioController::GetTargetVolumeMin>, "GetTargetVolumeMin"}, + {3, C<&IAudioController::GetTargetVolumeMax>, "GetTargetVolumeMax"}, + {4, nullptr, "IsTargetMute"}, + {5, nullptr, "SetTargetMute"}, + {6, nullptr, "IsTargetConnected"}, + {7, nullptr, "SetDefaultTarget"}, + {8, nullptr, "GetDefaultTarget"}, + {9, C<&IAudioController::GetAudioOutputMode>, "GetAudioOutputMode"}, + {10, C<&IAudioController::SetAudioOutputMode>, "SetAudioOutputMode"}, + {11, nullptr, "SetForceMutePolicy"}, + {12, C<&IAudioController::GetForceMutePolicy>, "GetForceMutePolicy"}, + {13, C<&IAudioController::GetOutputModeSetting>, "GetOutputModeSetting"}, + {14, C<&IAudioController::SetOutputModeSetting>, "SetOutputModeSetting"}, + {15, nullptr, "SetOutputTarget"}, + {16, nullptr, "SetInputTargetForceEnabled"}, + {17, C<&IAudioController::SetHeadphoneOutputLevelMode>, "SetHeadphoneOutputLevelMode"}, + {18, C<&IAudioController::GetHeadphoneOutputLevelMode>, "GetHeadphoneOutputLevelMode"}, + {19, nullptr, "AcquireAudioVolumeUpdateEventForPlayReport"}, + {20, nullptr, "AcquireAudioOutputDeviceUpdateEventForPlayReport"}, + {21, nullptr, "GetAudioOutputTargetForPlayReport"}, + {22, nullptr, "NotifyHeadphoneVolumeWarningDisplayedEvent"}, + {23, nullptr, "SetSystemOutputMasterVolume"}, + {24, nullptr, "GetSystemOutputMasterVolume"}, + {25, nullptr, "GetAudioVolumeDataForPlayReport"}, + {26, nullptr, "UpdateHeadphoneSettings"}, + {27, nullptr, "SetVolumeMappingTableForDev"}, + {28, nullptr, "GetAudioOutputChannelCountForPlayReport"}, + {29, nullptr, "BindAudioOutputChannelCountUpdateEventForPlayReport"}, + {30, C<&IAudioController::SetSpeakerAutoMuteEnabled>, "SetSpeakerAutoMuteEnabled"}, + {31, C<&IAudioController::IsSpeakerAutoMuteEnabled>, "IsSpeakerAutoMuteEnabled"}, + {32, nullptr, "GetActiveOutputTarget"}, + {33, nullptr, "GetTargetDeviceInfo"}, + {34, C<&IAudioController::AcquireTargetNotification>, "AcquireTargetNotification"}, + {35, nullptr, "SetHearingProtectionSafeguardTimerRemainingTimeForDebug"}, + {36, nullptr, "GetHearingProtectionSafeguardTimerRemainingTimeForDebug"}, + {37, nullptr, "SetHearingProtectionSafeguardEnabled"}, + {38, nullptr, "IsHearingProtectionSafeguardEnabled"}, + {39, nullptr, "IsHearingProtectionSafeguardMonitoringOutputForDebug"}, + {40, nullptr, "GetSystemInformationForDebug"}, + {41, nullptr, "SetVolumeButtonLongPressTime"}, + {42, nullptr, "SetNativeVolumeForDebug"}, + {10000, nullptr, "NotifyAudioOutputTargetForPlayReport"}, + {10001, nullptr, "NotifyAudioOutputChannelCountForPlayReport"}, + {10002, nullptr, "NotifyUnsupportedUsbOutputDeviceAttachedForPlayReport"}, + {10100, nullptr, "GetAudioVolumeDataForPlayReport"}, + {10101, nullptr, "BindAudioVolumeUpdateEventForPlayReport"}, + {10102, nullptr, "BindAudioOutputTargetUpdateEventForPlayReport"}, + {10103, nullptr, "GetAudioOutputTargetForPlayReport"}, + {10104, nullptr, "GetAudioOutputChannelCountForPlayReport"}, + {10105, nullptr, "BindAudioOutputChannelCountUpdateEventForPlayReport"}, + {10106, nullptr, "GetDefaultAudioOutputTargetForPlayReport"}, + {50000, nullptr, "SetAnalogInputBoostGainForPrototyping"}, + }; + // clang-format on + + RegisterHandlers(functions); + + m_set_sys = + system.ServiceManager().GetService<Service::Set::ISystemSettingsServer>("set:sys", true); + notification_event = service_context.CreateEvent("IAudioController:NotificationEvent"); +} + +IAudioController::~IAudioController() { + service_context.CloseEvent(notification_event); +}; + +Result IAudioController::GetTargetVolumeMin(Out<s32> out_target_min_volume) { + LOG_DEBUG(Audio, "called."); + + // This service function is currently hardcoded on the + // actual console to this value (as of 8.0.0). + *out_target_min_volume = 0; + R_SUCCEED(); +} + +Result IAudioController::GetTargetVolumeMax(Out<s32> out_target_max_volume) { + LOG_DEBUG(Audio, "called."); + + // This service function is currently hardcoded on the + // actual console to this value (as of 8.0.0). + *out_target_max_volume = 15; + R_SUCCEED(); +} + +Result IAudioController::GetAudioOutputMode(Out<Set::AudioOutputMode> out_output_mode, + Set::AudioOutputModeTarget target) { + const auto result = m_set_sys->GetAudioOutputMode(out_output_mode, target); + + LOG_INFO(Service_SET, "called, target={}, output_mode={}", target, *out_output_mode); + R_RETURN(result); +} + +Result IAudioController::SetAudioOutputMode(Set::AudioOutputModeTarget target, + Set::AudioOutputMode output_mode) { + LOG_INFO(Service_SET, "called, target={}, output_mode={}", target, output_mode); + + R_RETURN(m_set_sys->SetAudioOutputMode(target, output_mode)); +} + +Result IAudioController::GetForceMutePolicy(Out<ForceMutePolicy> out_mute_policy) { + LOG_WARNING(Audio, "(STUBBED) called"); + + // Removed on FW 13.2.1+ + *out_mute_policy = ForceMutePolicy::Disable; + R_SUCCEED(); +} + +Result IAudioController::GetOutputModeSetting(Out<Set::AudioOutputMode> out_output_mode, + Set::AudioOutputModeTarget target) { + LOG_WARNING(Audio, "(STUBBED) called, target={}", target); + + *out_output_mode = Set::AudioOutputMode::ch_7_1; + R_SUCCEED(); +} + +Result IAudioController::SetOutputModeSetting(Set::AudioOutputModeTarget target, + Set::AudioOutputMode output_mode) { + LOG_INFO(Service_SET, "called, target={}, output_mode={}", target, output_mode); + R_SUCCEED(); +} + +Result IAudioController::SetHeadphoneOutputLevelMode(HeadphoneOutputLevelMode output_level_mode) { + LOG_WARNING(Audio, "(STUBBED) called"); + R_SUCCEED(); +} + +Result IAudioController::GetHeadphoneOutputLevelMode( + Out<HeadphoneOutputLevelMode> out_output_level_mode) { + LOG_INFO(Audio, "called"); + + *out_output_level_mode = HeadphoneOutputLevelMode::Normal; + R_SUCCEED(); +} + +Result IAudioController::SetSpeakerAutoMuteEnabled(bool is_speaker_auto_mute_enabled) { + LOG_INFO(Audio, "called, is_speaker_auto_mute_enabled={}", is_speaker_auto_mute_enabled); + + R_RETURN(m_set_sys->SetSpeakerAutoMuteFlag(is_speaker_auto_mute_enabled)); +} + +Result IAudioController::IsSpeakerAutoMuteEnabled(Out<bool> out_is_speaker_auto_mute_enabled) { + const auto result = m_set_sys->GetSpeakerAutoMuteFlag(out_is_speaker_auto_mute_enabled); + + LOG_INFO(Audio, "called, is_speaker_auto_mute_enabled={}", *out_is_speaker_auto_mute_enabled); + R_RETURN(result); +} + +Result IAudioController::AcquireTargetNotification( + OutCopyHandle<Kernel::KReadableEvent> out_notification_event) { + LOG_WARNING(Service_AM, "(STUBBED) called"); + + *out_notification_event = ¬ification_event->GetReadableEvent(); + R_SUCCEED(); +} + +} // namespace Service::Audio diff --git a/src/core/hle/service/audio/audio_controller.h b/src/core/hle/service/audio/audio_controller.h new file mode 100644 index 000000000..9e8514373 --- /dev/null +++ b/src/core/hle/service/audio/audio_controller.h @@ -0,0 +1,58 @@ +// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "core/hle/service/cmif_types.h" +#include "core/hle/service/service.h" +#include "core/hle/service/set/settings_types.h" + +namespace Core { +class System; +} + +namespace Service::Set { +class ISystemSettingsServer; +} + +namespace Service::Audio { + +class IAudioController final : public ServiceFramework<IAudioController> { +public: + explicit IAudioController(Core::System& system_); + ~IAudioController() override; + +private: + enum class ForceMutePolicy { + Disable, + SpeakerMuteOnHeadphoneUnplugged, + }; + + enum class HeadphoneOutputLevelMode { + Normal, + HighPower, + }; + + Result GetTargetVolumeMin(Out<s32> out_target_min_volume); + Result GetTargetVolumeMax(Out<s32> out_target_max_volume); + Result GetAudioOutputMode(Out<Set::AudioOutputMode> out_output_mode, + Set::AudioOutputModeTarget target); + Result SetAudioOutputMode(Set::AudioOutputModeTarget target, Set::AudioOutputMode output_mode); + Result GetForceMutePolicy(Out<ForceMutePolicy> out_mute_policy); + Result GetOutputModeSetting(Out<Set::AudioOutputMode> out_output_mode, + Set::AudioOutputModeTarget target); + Result SetOutputModeSetting(Set::AudioOutputModeTarget target, + Set::AudioOutputMode output_mode); + Result SetHeadphoneOutputLevelMode(HeadphoneOutputLevelMode output_level_mode); + Result GetHeadphoneOutputLevelMode(Out<HeadphoneOutputLevelMode> out_output_level_mode); + Result SetSpeakerAutoMuteEnabled(bool is_speaker_auto_mute_enabled); + Result IsSpeakerAutoMuteEnabled(Out<bool> out_is_speaker_auto_mute_enabled); + Result AcquireTargetNotification(OutCopyHandle<Kernel::KReadableEvent> out_notification_event); + + KernelHelpers::ServiceContext service_context; + + Kernel::KEvent* notification_event; + std::shared_ptr<Service::Set::ISystemSettingsServer> m_set_sys; +}; + +} // namespace Service::Audio diff --git a/src/core/hle/service/caps/caps_a.cpp b/src/core/hle/service/caps/caps_a.cpp index 47ff072c5..52228b830 100644 --- a/src/core/hle/service/caps/caps_a.cpp +++ b/src/core/hle/service/caps/caps_a.cpp @@ -16,7 +16,7 @@ IAlbumAccessorService::IAlbumAccessorService(Core::System& system_, // clang-format off static const FunctionInfo functions[] = { {0, nullptr, "GetAlbumFileCount"}, - {1, nullptr, "GetAlbumFileList"}, + {1, C<&IAlbumAccessorService::GetAlbumFileList>, "GetAlbumFileList"}, {2, nullptr, "LoadAlbumFile"}, {3, C<&IAlbumAccessorService::DeleteAlbumFile>, "DeleteAlbumFile"}, {4, nullptr, "StorageCopyAlbumFile"}, @@ -62,6 +62,15 @@ IAlbumAccessorService::IAlbumAccessorService(Core::System& system_, IAlbumAccessorService::~IAlbumAccessorService() = default; +Result IAlbumAccessorService::GetAlbumFileList( + Out<u64> out_count, AlbumStorage storage, + OutArray<AlbumEntry, BufferAttr_HipcMapAlias> out_entries) { + LOG_INFO(Service_Capture, "called, storage={}", storage); + + const Result result = manager->GetAlbumFileList(out_entries, *out_count, storage, 0); + R_RETURN(TranslateResult(result)); +} + Result IAlbumAccessorService::DeleteAlbumFile(AlbumFileId file_id) { LOG_INFO(Service_Capture, "called, application_id=0x{:0x}, storage={}, type={}", file_id.application_id, file_id.storage, file_id.type); diff --git a/src/core/hle/service/caps/caps_a.h b/src/core/hle/service/caps/caps_a.h index 2cb9b4547..c7a5208e3 100644 --- a/src/core/hle/service/caps/caps_a.h +++ b/src/core/hle/service/caps/caps_a.h @@ -21,6 +21,9 @@ public: ~IAlbumAccessorService() override; private: + Result GetAlbumFileList(Out<u64> out_count, AlbumStorage storage, + OutArray<AlbumEntry, BufferAttr_HipcMapAlias> out_entries); + Result DeleteAlbumFile(AlbumFileId file_id); Result IsAlbumMounted(Out<bool> out_is_mounted, AlbumStorage storage); diff --git a/src/core/hle/service/erpt/erpt.cpp b/src/core/hle/service/erpt/erpt.cpp index 3ea862fad..39ae3a723 100644 --- a/src/core/hle/service/erpt/erpt.cpp +++ b/src/core/hle/service/erpt/erpt.cpp @@ -3,6 +3,8 @@ #include <memory> +#include "common/logging/log.h" +#include "core/hle/service/cmif_serialization.h" #include "core/hle/service/erpt/erpt.h" #include "core/hle/service/server_manager.h" #include "core/hle/service/service.h" @@ -15,7 +17,7 @@ public: explicit ErrorReportContext(Core::System& system_) : ServiceFramework{system_, "erpt:c"} { // clang-format off static const FunctionInfo functions[] = { - {0, nullptr, "SubmitContext"}, + {0, C<&ErrorReportContext::SubmitContext>, "SubmitContext"}, {1, nullptr, "CreateReportV0"}, {2, nullptr, "SetInitialLaunchSettingsCompletionTime"}, {3, nullptr, "ClearInitialLaunchSettingsCompletionTime"}, @@ -36,6 +38,14 @@ public: RegisterHandlers(functions); } + +private: + Result SubmitContext(InBuffer<BufferAttr_HipcMapAlias> buffer_a, + InBuffer<BufferAttr_HipcMapAlias> buffer_b) { + LOG_WARNING(Service_SET, "(STUBBED) called, buffer_a_size={}, buffer_b_size={}", + buffer_a.size(), buffer_b.size()); + R_SUCCEED(); + } }; class ErrorReportSession final : public ServiceFramework<ErrorReportSession> { diff --git a/src/core/hle/service/filesystem/fsp/fsp_srv.cpp b/src/core/hle/service/filesystem/fsp/fsp_srv.cpp index 63c2d3a58..2d49f30c8 100644 --- a/src/core/hle/service/filesystem/fsp/fsp_srv.cpp +++ b/src/core/hle/service/filesystem/fsp/fsp_srv.cpp @@ -336,7 +336,7 @@ FSP_SRV::FSP_SRV(Core::System& system_) {1012, nullptr, "GetFsStackUsage"}, {1013, nullptr, "UnsetSaveDataRootPath"}, {1014, nullptr, "OutputMultiProgramTagAccessLog"}, - {1016, nullptr, "FlushAccessLogOnSdCard"}, + {1016, &FSP_SRV::FlushAccessLogOnSdCard, "FlushAccessLogOnSdCard"}, {1017, nullptr, "OutputApplicationInfoAccessLog"}, {1018, nullptr, "SetDebugOption"}, {1019, nullptr, "UnsetDebugOption"}, @@ -706,6 +706,13 @@ void FSP_SRV::GetProgramIndexForAccessLog(HLERequestContext& ctx) { rb.Push(access_log_program_index); } +void FSP_SRV::FlushAccessLogOnSdCard(HLERequestContext& ctx) { + LOG_DEBUG(Service_FS, "(STUBBED) called"); + + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ResultSuccess); +} + void FSP_SRV::GetCacheStorageSize(HLERequestContext& ctx) { IPC::RequestParser rp{ctx}; const auto index{rp.Pop<s32>()}; diff --git a/src/core/hle/service/filesystem/fsp/fsp_srv.h b/src/core/hle/service/filesystem/fsp/fsp_srv.h index 26980af99..59406e6f9 100644 --- a/src/core/hle/service/filesystem/fsp/fsp_srv.h +++ b/src/core/hle/service/filesystem/fsp/fsp_srv.h @@ -58,6 +58,7 @@ private: void SetGlobalAccessLogMode(HLERequestContext& ctx); void GetGlobalAccessLogMode(HLERequestContext& ctx); void OutputAccessLogToSdCard(HLERequestContext& ctx); + void FlushAccessLogOnSdCard(HLERequestContext& ctx); void GetProgramIndexForAccessLog(HLERequestContext& ctx); void OpenMultiCommitManager(HLERequestContext& ctx); void GetCacheStorageSize(HLERequestContext& ctx); diff --git a/src/core/hle/service/ns/account_proxy_interface.cpp b/src/core/hle/service/ns/account_proxy_interface.cpp new file mode 100644 index 000000000..e5041af66 --- /dev/null +++ b/src/core/hle/service/ns/account_proxy_interface.cpp @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "core/hle/service/ns/account_proxy_interface.h" + +namespace Service::NS { + +IAccountProxyInterface::IAccountProxyInterface(Core::System& system_) + : ServiceFramework{system_, "IAccountProxyInterface"} { + // clang-format off + static const FunctionInfo functions[] = { + {0, nullptr, "CreateUserAccount"}, + }; + // clang-format on + + RegisterHandlers(functions); +} + +IAccountProxyInterface::~IAccountProxyInterface() = default; + +} // namespace Service::NS diff --git a/src/core/hle/service/ns/account_proxy_interface.h b/src/core/hle/service/ns/account_proxy_interface.h new file mode 100644 index 000000000..e944d2a75 --- /dev/null +++ b/src/core/hle/service/ns/account_proxy_interface.h @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "core/hle/service/service.h" + +namespace Service::NS { + +class IAccountProxyInterface final : public ServiceFramework<IAccountProxyInterface> { +public: + explicit IAccountProxyInterface(Core::System& system_); + ~IAccountProxyInterface() override; +}; + +} // namespace Service::NS diff --git a/src/core/hle/service/ns/application_manager_interface.cpp b/src/core/hle/service/ns/application_manager_interface.cpp new file mode 100644 index 000000000..2e3a44c0d --- /dev/null +++ b/src/core/hle/service/ns/application_manager_interface.cpp @@ -0,0 +1,519 @@ +// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "core/file_sys/nca_metadata.h" +#include "core/file_sys/registered_cache.h" +#include "core/hle/service/cmif_serialization.h" +#include "core/hle/service/filesystem/filesystem.h" +#include "core/hle/service/ns/application_manager_interface.h" +#include "core/hle/service/ns/content_management_interface.h" +#include "core/hle/service/ns/read_only_application_control_data_interface.h" + +namespace Service::NS { + +IApplicationManagerInterface::IApplicationManagerInterface(Core::System& system_) + : ServiceFramework{system_, "IApplicationManagerInterface"}, + service_context{system, "IApplicationManagerInterface"}, + record_update_system_event{service_context}, sd_card_mount_status_event{service_context}, + gamecard_update_detection_event{service_context}, + gamecard_mount_status_event{service_context}, gamecard_mount_failure_event{service_context} { + // clang-format off + static const FunctionInfo functions[] = { + {0, D<&IApplicationManagerInterface::ListApplicationRecord>, "ListApplicationRecord"}, + {1, nullptr, "GenerateApplicationRecordCount"}, + {2, D<&IApplicationManagerInterface::GetApplicationRecordUpdateSystemEvent>, "GetApplicationRecordUpdateSystemEvent"}, + {3, nullptr, "GetApplicationViewDeprecated"}, + {4, nullptr, "DeleteApplicationEntity"}, + {5, nullptr, "DeleteApplicationCompletely"}, + {6, nullptr, "IsAnyApplicationEntityRedundant"}, + {7, nullptr, "DeleteRedundantApplicationEntity"}, + {8, nullptr, "IsApplicationEntityMovable"}, + {9, nullptr, "MoveApplicationEntity"}, + {11, nullptr, "CalculateApplicationOccupiedSize"}, + {16, nullptr, "PushApplicationRecord"}, + {17, nullptr, "ListApplicationRecordContentMeta"}, + {19, nullptr, "LaunchApplicationOld"}, + {21, nullptr, "GetApplicationContentPath"}, + {22, nullptr, "TerminateApplication"}, + {23, nullptr, "ResolveApplicationContentPath"}, + {26, nullptr, "BeginInstallApplication"}, + {27, nullptr, "DeleteApplicationRecord"}, + {30, nullptr, "RequestApplicationUpdateInfo"}, + {31, nullptr, "Unknown31"}, + {32, nullptr, "CancelApplicationDownload"}, + {33, nullptr, "ResumeApplicationDownload"}, + {35, nullptr, "UpdateVersionList"}, + {36, nullptr, "PushLaunchVersion"}, + {37, nullptr, "ListRequiredVersion"}, + {38, D<&IApplicationManagerInterface::CheckApplicationLaunchVersion>, "CheckApplicationLaunchVersion"}, + {39, nullptr, "CheckApplicationLaunchRights"}, + {40, nullptr, "GetApplicationLogoData"}, + {41, nullptr, "CalculateApplicationDownloadRequiredSize"}, + {42, nullptr, "CleanupSdCard"}, + {43, D<&IApplicationManagerInterface::CheckSdCardMountStatus>, "CheckSdCardMountStatus"}, + {44, D<&IApplicationManagerInterface::GetSdCardMountStatusChangedEvent>, "GetSdCardMountStatusChangedEvent"}, + {45, nullptr, "GetGameCardAttachmentEvent"}, + {46, nullptr, "GetGameCardAttachmentInfo"}, + {47, nullptr, "GetTotalSpaceSize"}, + {48, D<&IApplicationManagerInterface::GetFreeSpaceSize>, "GetFreeSpaceSize"}, + {49, nullptr, "GetSdCardRemovedEvent"}, + {52, D<&IApplicationManagerInterface::GetGameCardUpdateDetectionEvent>, "GetGameCardUpdateDetectionEvent"}, + {53, nullptr, "DisableApplicationAutoDelete"}, + {54, nullptr, "EnableApplicationAutoDelete"}, + {55, D<&IApplicationManagerInterface::GetApplicationDesiredLanguage>, "GetApplicationDesiredLanguage"}, + {56, nullptr, "SetApplicationTerminateResult"}, + {57, nullptr, "ClearApplicationTerminateResult"}, + {58, nullptr, "GetLastSdCardMountUnexpectedResult"}, + {59, D<&IApplicationManagerInterface::ConvertApplicationLanguageToLanguageCode>, "ConvertApplicationLanguageToLanguageCode"}, + {60, nullptr, "ConvertLanguageCodeToApplicationLanguage"}, + {61, nullptr, "GetBackgroundDownloadStressTaskInfo"}, + {62, nullptr, "GetGameCardStopper"}, + {63, nullptr, "IsSystemProgramInstalled"}, + {64, nullptr, "StartApplyDeltaTask"}, + {65, nullptr, "GetRequestServerStopper"}, + {66, nullptr, "GetBackgroundApplyDeltaStressTaskInfo"}, + {67, nullptr, "CancelApplicationApplyDelta"}, + {68, nullptr, "ResumeApplicationApplyDelta"}, + {69, nullptr, "CalculateApplicationApplyDeltaRequiredSize"}, + {70, D<&IApplicationManagerInterface::ResumeAll>, "ResumeAll"}, + {71, D<&IApplicationManagerInterface::GetStorageSize>, "GetStorageSize"}, + {80, nullptr, "RequestDownloadApplication"}, + {81, nullptr, "RequestDownloadAddOnContent"}, + {82, nullptr, "DownloadApplication"}, + {83, nullptr, "CheckApplicationResumeRights"}, + {84, nullptr, "GetDynamicCommitEvent"}, + {85, nullptr, "RequestUpdateApplication2"}, + {86, nullptr, "EnableApplicationCrashReport"}, + {87, nullptr, "IsApplicationCrashReportEnabled"}, + {90, nullptr, "BoostSystemMemoryResourceLimit"}, + {91, nullptr, "DeprecatedLaunchApplication"}, + {92, nullptr, "GetRunningApplicationProgramId"}, + {93, nullptr, "GetMainApplicationProgramIndex"}, + {94, nullptr, "LaunchApplication"}, + {95, nullptr, "GetApplicationLaunchInfo"}, + {96, nullptr, "AcquireApplicationLaunchInfo"}, + {97, nullptr, "GetMainApplicationProgramIndexByApplicationLaunchInfo"}, + {98, nullptr, "EnableApplicationAllThreadDumpOnCrash"}, + {99, nullptr, "LaunchDevMenu"}, + {100, nullptr, "ResetToFactorySettings"}, + {101, nullptr, "ResetToFactorySettingsWithoutUserSaveData"}, + {102, nullptr, "ResetToFactorySettingsForRefurbishment"}, + {103, nullptr, "ResetToFactorySettingsWithPlatformRegion"}, + {104, nullptr, "ResetToFactorySettingsWithPlatformRegionAuthentication"}, + {105, nullptr, "RequestResetToFactorySettingsSecurely"}, + {106, nullptr, "RequestResetToFactorySettingsWithPlatformRegionAuthenticationSecurely"}, + {200, nullptr, "CalculateUserSaveDataStatistics"}, + {201, nullptr, "DeleteUserSaveDataAll"}, + {210, nullptr, "DeleteUserSystemSaveData"}, + {211, nullptr, "DeleteSaveData"}, + {220, nullptr, "UnregisterNetworkServiceAccount"}, + {221, nullptr, "UnregisterNetworkServiceAccountWithUserSaveDataDeletion"}, + {300, nullptr, "GetApplicationShellEvent"}, + {301, nullptr, "PopApplicationShellEventInfo"}, + {302, nullptr, "LaunchLibraryApplet"}, + {303, nullptr, "TerminateLibraryApplet"}, + {304, nullptr, "LaunchSystemApplet"}, + {305, nullptr, "TerminateSystemApplet"}, + {306, nullptr, "LaunchOverlayApplet"}, + {307, nullptr, "TerminateOverlayApplet"}, + {400, D<&IApplicationManagerInterface::GetApplicationControlData>, "GetApplicationControlData"}, + {401, nullptr, "InvalidateAllApplicationControlCache"}, + {402, nullptr, "RequestDownloadApplicationControlData"}, + {403, nullptr, "GetMaxApplicationControlCacheCount"}, + {404, nullptr, "InvalidateApplicationControlCache"}, + {405, nullptr, "ListApplicationControlCacheEntryInfo"}, + {406, nullptr, "GetApplicationControlProperty"}, + {407, nullptr, "ListApplicationTitle"}, + {408, nullptr, "ListApplicationIcon"}, + {502, nullptr, "RequestCheckGameCardRegistration"}, + {503, nullptr, "RequestGameCardRegistrationGoldPoint"}, + {504, nullptr, "RequestRegisterGameCard"}, + {505, D<&IApplicationManagerInterface::GetGameCardMountFailureEvent>, "GetGameCardMountFailureEvent"}, + {506, nullptr, "IsGameCardInserted"}, + {507, nullptr, "EnsureGameCardAccess"}, + {508, nullptr, "GetLastGameCardMountFailureResult"}, + {509, nullptr, "ListApplicationIdOnGameCard"}, + {510, nullptr, "GetGameCardPlatformRegion"}, + {600, nullptr, "CountApplicationContentMeta"}, + {601, nullptr, "ListApplicationContentMetaStatus"}, + {602, nullptr, "ListAvailableAddOnContent"}, + {603, nullptr, "GetOwnedApplicationContentMetaStatus"}, + {604, nullptr, "RegisterContentsExternalKey"}, + {605, nullptr, "ListApplicationContentMetaStatusWithRightsCheck"}, + {606, nullptr, "GetContentMetaStorage"}, + {607, nullptr, "ListAvailableAddOnContent"}, + {609, nullptr, "ListAvailabilityAssuredAddOnContent"}, + {610, nullptr, "GetInstalledContentMetaStorage"}, + {611, nullptr, "PrepareAddOnContent"}, + {700, nullptr, "PushDownloadTaskList"}, + {701, nullptr, "ClearTaskStatusList"}, + {702, nullptr, "RequestDownloadTaskList"}, + {703, nullptr, "RequestEnsureDownloadTask"}, + {704, nullptr, "ListDownloadTaskStatus"}, + {705, nullptr, "RequestDownloadTaskListData"}, + {800, nullptr, "RequestVersionList"}, + {801, nullptr, "ListVersionList"}, + {802, nullptr, "RequestVersionListData"}, + {900, nullptr, "GetApplicationRecord"}, + {901, nullptr, "GetApplicationRecordProperty"}, + {902, nullptr, "EnableApplicationAutoUpdate"}, + {903, nullptr, "DisableApplicationAutoUpdate"}, + {904, nullptr, "TouchApplication"}, + {905, nullptr, "RequestApplicationUpdate"}, + {906, D<&IApplicationManagerInterface::IsApplicationUpdateRequested>, "IsApplicationUpdateRequested"}, + {907, nullptr, "WithdrawApplicationUpdateRequest"}, + {908, nullptr, "ListApplicationRecordInstalledContentMeta"}, + {909, nullptr, "WithdrawCleanupAddOnContentsWithNoRightsRecommendation"}, + {910, nullptr, "HasApplicationRecord"}, + {911, nullptr, "SetPreInstalledApplication"}, + {912, nullptr, "ClearPreInstalledApplicationFlag"}, + {913, nullptr, "ListAllApplicationRecord"}, + {914, nullptr, "HideApplicationRecord"}, + {915, nullptr, "ShowApplicationRecord"}, + {916, nullptr, "IsApplicationAutoDeleteDisabled"}, + {1000, nullptr, "RequestVerifyApplicationDeprecated"}, + {1001, nullptr, "CorruptApplicationForDebug"}, + {1002, nullptr, "RequestVerifyAddOnContentsRights"}, + {1003, nullptr, "RequestVerifyApplication"}, + {1004, nullptr, "CorruptContentForDebug"}, + {1200, nullptr, "NeedsUpdateVulnerability"}, + {1300, D<&IApplicationManagerInterface::IsAnyApplicationEntityInstalled>, "IsAnyApplicationEntityInstalled"}, + {1301, nullptr, "DeleteApplicationContentEntities"}, + {1302, nullptr, "CleanupUnrecordedApplicationEntity"}, + {1303, nullptr, "CleanupAddOnContentsWithNoRights"}, + {1304, nullptr, "DeleteApplicationContentEntity"}, + {1305, nullptr, "TryDeleteRunningApplicationEntity"}, + {1306, nullptr, "TryDeleteRunningApplicationCompletely"}, + {1307, nullptr, "TryDeleteRunningApplicationContentEntities"}, + {1308, nullptr, "DeleteApplicationCompletelyForDebug"}, + {1309, nullptr, "CleanupUnavailableAddOnContents"}, + {1310, nullptr, "RequestMoveApplicationEntity"}, + {1311, nullptr, "EstimateSizeToMove"}, + {1312, nullptr, "HasMovableEntity"}, + {1313, nullptr, "CleanupOrphanContents"}, + {1314, nullptr, "CheckPreconditionSatisfiedToMove"}, + {1400, nullptr, "PrepareShutdown"}, + {1500, nullptr, "FormatSdCard"}, + {1501, nullptr, "NeedsSystemUpdateToFormatSdCard"}, + {1502, nullptr, "GetLastSdCardFormatUnexpectedResult"}, + {1504, nullptr, "InsertSdCard"}, + {1505, nullptr, "RemoveSdCard"}, + {1506, nullptr, "GetSdCardStartupStatus"}, + {1600, nullptr, "GetSystemSeedForPseudoDeviceId"}, + {1601, nullptr, "ResetSystemSeedForPseudoDeviceId"}, + {1700, nullptr, "ListApplicationDownloadingContentMeta"}, + {1701, D<&IApplicationManagerInterface::GetApplicationView>, "GetApplicationView"}, + {1702, nullptr, "GetApplicationDownloadTaskStatus"}, + {1703, nullptr, "GetApplicationViewDownloadErrorContext"}, + {1704, D<&IApplicationManagerInterface::GetApplicationViewWithPromotionInfo>, "GetApplicationViewWithPromotionInfo"}, + {1705, nullptr, "IsPatchAutoDeletableApplication"}, + {1800, nullptr, "IsNotificationSetupCompleted"}, + {1801, nullptr, "GetLastNotificationInfoCount"}, + {1802, nullptr, "ListLastNotificationInfo"}, + {1803, nullptr, "ListNotificationTask"}, + {1900, nullptr, "IsActiveAccount"}, + {1901, nullptr, "RequestDownloadApplicationPrepurchasedRights"}, + {1902, nullptr, "GetApplicationTicketInfo"}, + {1903, nullptr, "RequestDownloadApplicationPrepurchasedRightsForAccount"}, + {2000, nullptr, "GetSystemDeliveryInfo"}, + {2001, nullptr, "SelectLatestSystemDeliveryInfo"}, + {2002, nullptr, "VerifyDeliveryProtocolVersion"}, + {2003, nullptr, "GetApplicationDeliveryInfo"}, + {2004, nullptr, "HasAllContentsToDeliver"}, + {2005, nullptr, "CompareApplicationDeliveryInfo"}, + {2006, nullptr, "CanDeliverApplication"}, + {2007, nullptr, "ListContentMetaKeyToDeliverApplication"}, + {2008, nullptr, "NeedsSystemUpdateToDeliverApplication"}, + {2009, nullptr, "EstimateRequiredSize"}, + {2010, nullptr, "RequestReceiveApplication"}, + {2011, nullptr, "CommitReceiveApplication"}, + {2012, nullptr, "GetReceiveApplicationProgress"}, + {2013, nullptr, "RequestSendApplication"}, + {2014, nullptr, "GetSendApplicationProgress"}, + {2015, nullptr, "CompareSystemDeliveryInfo"}, + {2016, nullptr, "ListNotCommittedContentMeta"}, + {2017, nullptr, "CreateDownloadTask"}, + {2018, nullptr, "GetApplicationDeliveryInfoHash"}, + {2050, D<&IApplicationManagerInterface::GetApplicationRightsOnClient>, "GetApplicationRightsOnClient"}, + {2051, nullptr, "InvalidateRightsIdCache"}, + {2100, D<&IApplicationManagerInterface::GetApplicationTerminateResult>, "GetApplicationTerminateResult"}, + {2101, nullptr, "GetRawApplicationTerminateResult"}, + {2150, nullptr, "CreateRightsEnvironment"}, + {2151, nullptr, "DestroyRightsEnvironment"}, + {2152, nullptr, "ActivateRightsEnvironment"}, + {2153, nullptr, "DeactivateRightsEnvironment"}, + {2154, nullptr, "ForceActivateRightsContextForExit"}, + {2155, nullptr, "UpdateRightsEnvironmentStatus"}, + {2156, nullptr, "CreateRightsEnvironmentForMicroApplication"}, + {2160, nullptr, "AddTargetApplicationToRightsEnvironment"}, + {2161, nullptr, "SetUsersToRightsEnvironment"}, + {2170, nullptr, "GetRightsEnvironmentStatus"}, + {2171, nullptr, "GetRightsEnvironmentStatusChangedEvent"}, + {2180, nullptr, "RequestExtendRightsInRightsEnvironment"}, + {2181, nullptr, "GetResultOfExtendRightsInRightsEnvironment"}, + {2182, nullptr, "SetActiveRightsContextUsingStateToRightsEnvironment"}, + {2190, nullptr, "GetRightsEnvironmentHandleForApplication"}, + {2199, nullptr, "GetRightsEnvironmentCountForDebug"}, + {2200, nullptr, "GetGameCardApplicationCopyIdentifier"}, + {2201, nullptr, "GetInstalledApplicationCopyIdentifier"}, + {2250, nullptr, "RequestReportActiveELicence"}, + {2300, nullptr, "ListEventLog"}, + {2350, nullptr, "PerformAutoUpdateByApplicationId"}, + {2351, nullptr, "RequestNoDownloadRightsErrorResolution"}, + {2352, nullptr, "RequestResolveNoDownloadRightsError"}, + {2353, nullptr, "GetApplicationDownloadTaskInfo"}, + {2354, nullptr, "PrioritizeApplicationBackgroundTask"}, + {2355, nullptr, "PreferStorageEfficientUpdate"}, + {2356, nullptr, "RequestStorageEfficientUpdatePreferable"}, + {2357, nullptr, "EnableMultiCoreDownload"}, + {2358, nullptr, "DisableMultiCoreDownload"}, + {2359, nullptr, "IsMultiCoreDownloadEnabled"}, + {2400, nullptr, "GetPromotionInfo"}, + {2401, nullptr, "CountPromotionInfo"}, + {2402, nullptr, "ListPromotionInfo"}, + {2403, nullptr, "ImportPromotionJsonForDebug"}, + {2404, nullptr, "ClearPromotionInfoForDebug"}, + {2500, nullptr, "ConfirmAvailableTime"}, + {2510, nullptr, "CreateApplicationResource"}, + {2511, nullptr, "GetApplicationResource"}, + {2513, nullptr, "LaunchMicroApplication"}, + {2514, nullptr, "ClearTaskOfAsyncTaskManager"}, + {2515, nullptr, "CleanupAllPlaceHolderAndFragmentsIfNoTask"}, + {2516, nullptr, "EnsureApplicationCertificate"}, + {2517, nullptr, "CreateApplicationInstance"}, + {2518, nullptr, "UpdateQualificationForDebug"}, + {2519, nullptr, "IsQualificationTransitionSupported"}, + {2520, nullptr, "IsQualificationTransitionSupportedByProcessId"}, + {2521, nullptr, "GetRightsUserChangedEvent"}, + {2522, nullptr, "IsRomRedirectionAvailable"}, + {2800, nullptr, "GetApplicationIdOfPreomia"}, + {3000, nullptr, "RegisterDeviceLockKey"}, + {3001, nullptr, "UnregisterDeviceLockKey"}, + {3002, nullptr, "VerifyDeviceLockKey"}, + {3003, nullptr, "HideApplicationIcon"}, + {3004, nullptr, "ShowApplicationIcon"}, + {3005, nullptr, "HideApplicationTitle"}, + {3006, nullptr, "ShowApplicationTitle"}, + {3007, nullptr, "EnableGameCard"}, + {3008, nullptr, "DisableGameCard"}, + {3009, nullptr, "EnableLocalContentShare"}, + {3010, nullptr, "DisableLocalContentShare"}, + {3011, nullptr, "IsApplicationIconHidden"}, + {3012, nullptr, "IsApplicationTitleHidden"}, + {3013, nullptr, "IsGameCardEnabled"}, + {3014, nullptr, "IsLocalContentShareEnabled"}, + {3050, nullptr, "ListAssignELicenseTaskResult"}, + {9999, nullptr, "GetApplicationCertificate"}, + }; + // clang-format on + + RegisterHandlers(functions); +} + +IApplicationManagerInterface::~IApplicationManagerInterface() = default; + +Result IApplicationManagerInterface::GetApplicationControlData( + OutBuffer<BufferAttr_HipcMapAlias> out_buffer, Out<u32> out_actual_size, + ApplicationControlSource application_control_source, u64 application_id) { + LOG_DEBUG(Service_NS, "called"); + R_RETURN(IReadOnlyApplicationControlDataInterface(system).GetApplicationControlData( + out_buffer, out_actual_size, application_control_source, application_id)); +} + +Result IApplicationManagerInterface::GetApplicationDesiredLanguage( + Out<ApplicationLanguage> out_desired_language, u32 supported_languages) { + LOG_DEBUG(Service_NS, "called"); + R_RETURN(IReadOnlyApplicationControlDataInterface(system).GetApplicationDesiredLanguage( + out_desired_language, supported_languages)); +} + +Result IApplicationManagerInterface::ConvertApplicationLanguageToLanguageCode( + Out<u64> out_language_code, ApplicationLanguage application_language) { + LOG_DEBUG(Service_NS, "called"); + R_RETURN( + IReadOnlyApplicationControlDataInterface(system).ConvertApplicationLanguageToLanguageCode( + out_language_code, application_language)); +} + +Result IApplicationManagerInterface::ListApplicationRecord( + OutArray<ApplicationRecord, BufferAttr_HipcMapAlias> out_records, Out<s32> out_count, + s32 offset) { + const auto limit = out_records.size(); + + LOG_WARNING(Service_NS, "(STUBBED) called"); + const auto& cache = system.GetContentProviderUnion(); + const auto installed_games = cache.ListEntriesFilterOrigin( + std::nullopt, FileSys::TitleType::Application, FileSys::ContentRecordType::Program); + + size_t i = 0; + u8 ii = 24; + + for (const auto& [slot, game] : installed_games) { + if (i >= limit) { + break; + } + if (game.title_id == 0 || game.title_id < 0x0100000000001FFFull) { + continue; + } + if (offset > 0) { + offset--; + continue; + } + + ApplicationRecord record{}; + record.application_id = game.title_id; + record.type = ApplicationRecordType::Installed; + record.unknown = 0; // 2 = needs update + record.unknown2 = ii++; + + out_records[i++] = record; + } + + *out_count = static_cast<s32>(i); + R_SUCCEED(); +} + +Result IApplicationManagerInterface::GetApplicationRecordUpdateSystemEvent( + OutCopyHandle<Kernel::KReadableEvent> out_event) { + LOG_WARNING(Service_NS, "(STUBBED) called"); + + record_update_system_event.Signal(); + *out_event = record_update_system_event.GetHandle(); + + R_SUCCEED(); +} + +Result IApplicationManagerInterface::GetGameCardMountFailureEvent( + OutCopyHandle<Kernel::KReadableEvent> out_event) { + LOG_WARNING(Service_NS, "(STUBBED) called"); + *out_event = gamecard_mount_failure_event.GetHandle(); + R_SUCCEED(); +} + +Result IApplicationManagerInterface::IsAnyApplicationEntityInstalled( + Out<bool> out_is_any_application_entity_installed) { + LOG_WARNING(Service_NS, "(STUBBED) called"); + *out_is_any_application_entity_installed = true; + R_SUCCEED(); +} + +Result IApplicationManagerInterface::GetApplicationView( + OutArray<ApplicationView, BufferAttr_HipcMapAlias> out_application_views, + InArray<u64, BufferAttr_HipcMapAlias> application_ids) { + const auto size = std::min(out_application_views.size(), application_ids.size()); + LOG_WARNING(Service_NS, "(STUBBED) called, size={}", application_ids.size()); + + for (size_t i = 0; i < size; i++) { + ApplicationView view{}; + view.application_id = application_ids[i]; + view.unk = 0x70000; + view.flags = 0x401f17; + + out_application_views[i] = view; + } + + R_SUCCEED(); +} + +Result IApplicationManagerInterface::GetApplicationViewWithPromotionInfo( + OutArray<ApplicationViewWithPromotionInfo, BufferAttr_HipcMapAlias> out_application_views, + InArray<u64, BufferAttr_HipcMapAlias> application_ids) { + const auto size = std::min(out_application_views.size(), application_ids.size()); + LOG_WARNING(Service_NS, "(STUBBED) called, size={}", application_ids.size()); + + for (size_t i = 0; i < size; i++) { + ApplicationViewWithPromotionInfo view{}; + view.view.application_id = application_ids[i]; + view.view.unk = 0x70000; + view.view.flags = 0x401f17; + view.promotion = {}; + + out_application_views[i] = view; + } + + R_SUCCEED(); +} + +Result IApplicationManagerInterface::GetApplicationRightsOnClient( + OutArray<ApplicationRightsOnClient, BufferAttr_HipcMapAlias> out_rights, Out<u32> out_count, + Common::UUID account_id, u32 flags, u64 application_id) { + LOG_WARNING(Service_NS, "(STUBBED) called, flags={}, application_id={:016X}, account_id={}", + flags, application_id, account_id.FormattedString()); + + if (!out_rights.empty()) { + ApplicationRightsOnClient rights{}; + rights.application_id = application_id; + rights.uid = account_id; + rights.flags = 0; + rights.flags2 = 0; + + out_rights[0] = rights; + *out_count = 1; + } else { + *out_count = 0; + } + + R_SUCCEED(); +} + +Result IApplicationManagerInterface::CheckSdCardMountStatus() { + LOG_DEBUG(Service_NS, "called"); + R_RETURN(IContentManagementInterface(system).CheckSdCardMountStatus()); +} + +Result IApplicationManagerInterface::GetSdCardMountStatusChangedEvent( + OutCopyHandle<Kernel::KReadableEvent> out_event) { + LOG_WARNING(Service_NS, "(STUBBED) called"); + *out_event = sd_card_mount_status_event.GetHandle(); + R_SUCCEED(); +} + +Result IApplicationManagerInterface::GetFreeSpaceSize(Out<s64> out_free_space_size, + FileSys::StorageId storage_id) { + LOG_DEBUG(Service_NS, "called"); + R_RETURN(IContentManagementInterface(system).GetFreeSpaceSize(out_free_space_size, storage_id)); +} + +Result IApplicationManagerInterface::GetGameCardUpdateDetectionEvent( + OutCopyHandle<Kernel::KReadableEvent> out_event) { + LOG_WARNING(Service_NS, "(STUBBED) called"); + *out_event = gamecard_update_detection_event.GetHandle(); + R_SUCCEED(); +} + +Result IApplicationManagerInterface::ResumeAll() { + LOG_WARNING(Service_NS, "(STUBBED) called"); + R_SUCCEED(); +} + +Result IApplicationManagerInterface::GetStorageSize(Out<s64> out_total_space_size, + Out<s64> out_free_space_size, + FileSys::StorageId storage_id) { + LOG_INFO(Service_NS, "called, storage_id={}", storage_id); + *out_total_space_size = system.GetFileSystemController().GetTotalSpaceSize(storage_id); + *out_free_space_size = system.GetFileSystemController().GetFreeSpaceSize(storage_id); + R_SUCCEED(); +} + +Result IApplicationManagerInterface::IsApplicationUpdateRequested(Out<bool> out_update_required, + Out<u32> out_update_version, + u64 application_id) { + LOG_WARNING(Service_NS, "(STUBBED) called. application_id={:016X}", application_id); + *out_update_required = false; + *out_update_version = 0; + R_SUCCEED(); +} + +Result IApplicationManagerInterface::CheckApplicationLaunchVersion(u64 application_id) { + LOG_WARNING(Service_NS, "(STUBBED) called. application_id={:016X}", application_id); + R_SUCCEED(); +} + +Result IApplicationManagerInterface::GetApplicationTerminateResult(Out<Result> out_result, + u64 application_id) { + LOG_WARNING(Service_NS, "(STUBBED) called. application_id={:016X}", application_id); + *out_result = ResultSuccess; + R_SUCCEED(); +} + +} // namespace Service::NS diff --git a/src/core/hle/service/ns/application_manager_interface.h b/src/core/hle/service/ns/application_manager_interface.h new file mode 100644 index 000000000..350ec37ce --- /dev/null +++ b/src/core/hle/service/ns/application_manager_interface.h @@ -0,0 +1,62 @@ +// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "core/hle/service/cmif_types.h" +#include "core/hle/service/ns/language.h" +#include "core/hle/service/ns/ns_types.h" +#include "core/hle/service/os/event.h" +#include "core/hle/service/service.h" + +namespace Service::NS { + +class IApplicationManagerInterface final : public ServiceFramework<IApplicationManagerInterface> { +public: + explicit IApplicationManagerInterface(Core::System& system_); + ~IApplicationManagerInterface() override; + + Result GetApplicationControlData(OutBuffer<BufferAttr_HipcMapAlias> out_buffer, + Out<u32> out_actual_size, + ApplicationControlSource application_control_source, + u64 application_id); + Result GetApplicationDesiredLanguage(Out<ApplicationLanguage> out_desired_language, + u32 supported_languages); + Result ConvertApplicationLanguageToLanguageCode(Out<u64> out_language_code, + ApplicationLanguage application_language); + Result ListApplicationRecord(OutArray<ApplicationRecord, BufferAttr_HipcMapAlias> out_records, + Out<s32> out_count, s32 offset); + Result GetApplicationRecordUpdateSystemEvent(OutCopyHandle<Kernel::KReadableEvent> out_event); + Result GetGameCardMountFailureEvent(OutCopyHandle<Kernel::KReadableEvent> out_event); + Result IsAnyApplicationEntityInstalled(Out<bool> out_is_any_application_entity_installed); + Result GetApplicationView( + OutArray<ApplicationView, BufferAttr_HipcMapAlias> out_application_views, + InArray<u64, BufferAttr_HipcMapAlias> application_ids); + Result GetApplicationViewWithPromotionInfo( + OutArray<ApplicationViewWithPromotionInfo, BufferAttr_HipcMapAlias> out_application_views, + InArray<u64, BufferAttr_HipcMapAlias> application_ids); + Result GetApplicationRightsOnClient( + OutArray<ApplicationRightsOnClient, BufferAttr_HipcMapAlias> out_rights, Out<u32> out_count, + Common::UUID account_id, u32 flags, u64 application_id); + Result CheckSdCardMountStatus(); + Result GetSdCardMountStatusChangedEvent(OutCopyHandle<Kernel::KReadableEvent> out_event); + Result GetFreeSpaceSize(Out<s64> out_free_space_size, FileSys::StorageId storage_id); + Result GetGameCardUpdateDetectionEvent(OutCopyHandle<Kernel::KReadableEvent> out_event); + Result ResumeAll(); + Result GetStorageSize(Out<s64> out_total_space_size, Out<s64> out_free_space_size, + FileSys::StorageId storage_id); + Result IsApplicationUpdateRequested(Out<bool> out_update_required, Out<u32> out_update_version, + u64 application_id); + Result CheckApplicationLaunchVersion(u64 application_id); + Result GetApplicationTerminateResult(Out<Result> out_result, u64 application_id); + +private: + KernelHelpers::ServiceContext service_context; + Event record_update_system_event; + Event sd_card_mount_status_event; + Event gamecard_update_detection_event; + Event gamecard_mount_status_event; + Event gamecard_mount_failure_event; +}; + +} // namespace Service::NS diff --git a/src/core/hle/service/ns/application_version_interface.cpp b/src/core/hle/service/ns/application_version_interface.cpp new file mode 100644 index 000000000..b89e127db --- /dev/null +++ b/src/core/hle/service/ns/application_version_interface.cpp @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "core/hle/service/ns/application_version_interface.h" + +namespace Service::NS { + +IApplicationVersionInterface::IApplicationVersionInterface(Core::System& system_) + : ServiceFramework{system_, "IApplicationVersionInterface"} { + // clang-format off + static const FunctionInfo functions[] = { + {0, nullptr, "GetLaunchRequiredVersion"}, + {1, nullptr, "UpgradeLaunchRequiredVersion"}, + {35, nullptr, "UpdateVersionList"}, + {36, nullptr, "PushLaunchVersion"}, + {37, nullptr, "ListRequiredVersion"}, + {800, nullptr, "RequestVersionList"}, + {801, nullptr, "ListVersionList"}, + {802, nullptr, "RequestVersionListData"}, + {900, nullptr, "ImportAutoUpdatePolicyJsonForDebug"}, + {901, nullptr, "ListDefaultAutoUpdatePolicy"}, + {902, nullptr, "ListAutoUpdatePolicyForSpecificApplication"}, + {1000, nullptr, "PerformAutoUpdate"}, + {1001, nullptr, "ListAutoUpdateSchedule"}, + }; + // clang-format on + + RegisterHandlers(functions); +} + +IApplicationVersionInterface::~IApplicationVersionInterface() = default; + +} // namespace Service::NS diff --git a/src/core/hle/service/ns/application_version_interface.h b/src/core/hle/service/ns/application_version_interface.h new file mode 100644 index 000000000..b288cff1b --- /dev/null +++ b/src/core/hle/service/ns/application_version_interface.h @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "core/hle/service/service.h" + +namespace Service::NS { + +class IApplicationVersionInterface final : public ServiceFramework<IApplicationVersionInterface> { +public: + explicit IApplicationVersionInterface(Core::System& system_); + ~IApplicationVersionInterface() override; +}; + +} // namespace Service::NS diff --git a/src/core/hle/service/ns/content_management_interface.cpp b/src/core/hle/service/ns/content_management_interface.cpp new file mode 100644 index 000000000..69bb3f6e4 --- /dev/null +++ b/src/core/hle/service/ns/content_management_interface.cpp @@ -0,0 +1,72 @@ +// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "common/common_funcs.h" +#include "core/core.h" +#include "core/hle/service/cmif_serialization.h" +#include "core/hle/service/filesystem/filesystem.h" +#include "core/hle/service/ns/content_management_interface.h" +#include "core/hle/service/ns/ns_types.h" + +namespace Service::NS { + +IContentManagementInterface::IContentManagementInterface(Core::System& system_) + : ServiceFramework{system_, "IContentManagementInterface"} { + // clang-format off + static const FunctionInfo functions[] = { + {11, D<&IContentManagementInterface::CalculateApplicationOccupiedSize>, "CalculateApplicationOccupiedSize"}, + {43, D<&IContentManagementInterface::CheckSdCardMountStatus>, "CheckSdCardMountStatus"}, + {47, D<&IContentManagementInterface::GetTotalSpaceSize>, "GetTotalSpaceSize"}, + {48, D<&IContentManagementInterface::GetFreeSpaceSize>, "GetFreeSpaceSize"}, + {600, nullptr, "CountApplicationContentMeta"}, + {601, nullptr, "ListApplicationContentMetaStatus"}, + {605, nullptr, "ListApplicationContentMetaStatusWithRightsCheck"}, + {607, nullptr, "IsAnyApplicationRunning"}, + }; + // clang-format on + + RegisterHandlers(functions); +} + +IContentManagementInterface::~IContentManagementInterface() = default; + +Result IContentManagementInterface::CalculateApplicationOccupiedSize( + Out<ApplicationOccupiedSize> out_size, u64 application_id) { + LOG_WARNING(Service_NS, "(STUBBED) called, application_id={:016X}", application_id); + + using namespace Common::Literals; + + constexpr ApplicationOccupiedSizeEntity stub_entity{ + .storage_id = FileSys::StorageId::SdCard, + .app_size = 8_GiB, + .patch_size = 2_GiB, + .aoc_size = 12_MiB, + }; + + for (auto& entity : out_size->entities) { + entity = stub_entity; + } + + R_SUCCEED(); +} + +Result IContentManagementInterface::CheckSdCardMountStatus() { + LOG_WARNING(Service_NS, "(STUBBED) called"); + R_SUCCEED(); +} + +Result IContentManagementInterface::GetTotalSpaceSize(Out<s64> out_total_space_size, + FileSys::StorageId storage_id) { + LOG_INFO(Service_NS, "(STUBBED) called, storage_id={}", storage_id); + *out_total_space_size = system.GetFileSystemController().GetTotalSpaceSize(storage_id); + R_SUCCEED(); +} + +Result IContentManagementInterface::GetFreeSpaceSize(Out<s64> out_free_space_size, + FileSys::StorageId storage_id) { + LOG_INFO(Service_NS, "(STUBBED) called, storage_id={}", storage_id); + *out_free_space_size = system.GetFileSystemController().GetFreeSpaceSize(storage_id); + R_SUCCEED(); +} + +} // namespace Service::NS diff --git a/src/core/hle/service/ns/content_management_interface.h b/src/core/hle/service/ns/content_management_interface.h new file mode 100644 index 000000000..2894628e5 --- /dev/null +++ b/src/core/hle/service/ns/content_management_interface.h @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "core/hle/service/cmif_types.h" +#include "core/hle/service/ns/ns_types.h" +#include "core/hle/service/service.h" + +namespace Service::NS { + +class IContentManagementInterface final : public ServiceFramework<IContentManagementInterface> { +public: + explicit IContentManagementInterface(Core::System& system_); + ~IContentManagementInterface() override; + +public: + Result CalculateApplicationOccupiedSize(Out<ApplicationOccupiedSize> out_size, + u64 application_id); + Result CheckSdCardMountStatus(); + Result GetTotalSpaceSize(Out<s64> out_total_space_size, FileSys::StorageId storage_id); + Result GetFreeSpaceSize(Out<s64> out_free_space_size, FileSys::StorageId storage_id); +}; + +} // namespace Service::NS diff --git a/src/core/hle/service/ns/develop_interface.cpp b/src/core/hle/service/ns/develop_interface.cpp new file mode 100644 index 000000000..880bdbebb --- /dev/null +++ b/src/core/hle/service/ns/develop_interface.cpp @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "core/hle/service/ns/develop_interface.h" + +namespace Service::NS { + +IDevelopInterface::IDevelopInterface(Core::System& system_) : ServiceFramework{system_, "ns:dev"} { + // clang-format off + static const FunctionInfo functions[] = { + {0, nullptr, "LaunchProgram"}, + {1, nullptr, "TerminateProcess"}, + {2, nullptr, "TerminateProgram"}, + {4, nullptr, "GetShellEvent"}, + {5, nullptr, "GetShellEventInfo"}, + {6, nullptr, "TerminateApplication"}, + {7, nullptr, "PrepareLaunchProgramFromHost"}, + {8, nullptr, "LaunchApplicationFromHost"}, + {9, nullptr, "LaunchApplicationWithStorageIdForDevelop"}, + {10, nullptr, "IsSystemMemoryResourceLimitBoosted"}, + {11, nullptr, "GetRunningApplicationProcessIdForDevelop"}, + {12, nullptr, "SetCurrentApplicationRightsEnvironmentCanBeActiveForDevelop"}, + {13, nullptr, "CreateApplicationResourceForDevelop"}, + {14, nullptr, "IsPreomiaForDevelop"}, + {15, nullptr, "GetApplicationProgramIdFromHost"}, + {16, nullptr, "RefreshCachedDebugValues"}, + {17, nullptr, "PrepareLaunchApplicationFromHost"}, + {18, nullptr, "GetLaunchEvent"}, + {19, nullptr, "GetLaunchResult"}, + }; + // clang-format on + + RegisterHandlers(functions); +} + +IDevelopInterface::~IDevelopInterface() = default; + +} // namespace Service::NS diff --git a/src/core/hle/service/ns/develop_interface.h b/src/core/hle/service/ns/develop_interface.h new file mode 100644 index 000000000..a9f81ccd6 --- /dev/null +++ b/src/core/hle/service/ns/develop_interface.h @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "core/hle/service/service.h" + +namespace Service::NS { + +class IDevelopInterface final : public ServiceFramework<IDevelopInterface> { +public: + explicit IDevelopInterface(Core::System& system_); + ~IDevelopInterface() override; +}; + +} // namespace Service::NS diff --git a/src/core/hle/service/ns/document_interface.cpp b/src/core/hle/service/ns/document_interface.cpp new file mode 100644 index 000000000..51a1e46c0 --- /dev/null +++ b/src/core/hle/service/ns/document_interface.cpp @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "core/core.h" +#include "core/hle/service/cmif_serialization.h" +#include "core/hle/service/ns/document_interface.h" + +namespace Service::NS { + +IDocumentInterface::IDocumentInterface(Core::System& system_) + : ServiceFramework{system_, "IDocumentInterface"} { + // clang-format off + static const FunctionInfo functions[] = { + {21, nullptr, "GetApplicationContentPath"}, + {23, D<&IDocumentInterface::ResolveApplicationContentPath>, "ResolveApplicationContentPath"}, + {92, D<&IDocumentInterface::GetRunningApplicationProgramId>, "GetRunningApplicationProgramId"}, + }; + // clang-format on + + RegisterHandlers(functions); +} + +IDocumentInterface::~IDocumentInterface() = default; + +Result IDocumentInterface::ResolveApplicationContentPath(ContentPath content_path) { + LOG_WARNING(Service_NS, "(STUBBED) called, file_system_proxy_type={}, program_id={:016X}", + content_path.file_system_proxy_type, content_path.program_id); + R_SUCCEED(); +} + +Result IDocumentInterface::GetRunningApplicationProgramId(Out<u64> out_program_id, + u64 caller_program_id) { + LOG_WARNING(Service_NS, "(STUBBED) called, caller_program_id={:016X}", caller_program_id); + *out_program_id = system.GetApplicationProcessProgramID(); + R_SUCCEED(); +} + +} // namespace Service::NS diff --git a/src/core/hle/service/ns/document_interface.h b/src/core/hle/service/ns/document_interface.h new file mode 100644 index 000000000..cd461652c --- /dev/null +++ b/src/core/hle/service/ns/document_interface.h @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "core/hle/service/cmif_types.h" +#include "core/hle/service/ns/ns_types.h" +#include "core/hle/service/service.h" + +namespace Service::NS { + +class IDocumentInterface final : public ServiceFramework<IDocumentInterface> { +public: + explicit IDocumentInterface(Core::System& system_); + ~IDocumentInterface() override; + +private: + Result ResolveApplicationContentPath(ContentPath content_path); + Result GetRunningApplicationProgramId(Out<u64> out_program_id, u64 caller_program_id); +}; + +} // namespace Service::NS diff --git a/src/core/hle/service/ns/download_task_interface.cpp b/src/core/hle/service/ns/download_task_interface.cpp new file mode 100644 index 000000000..62dc7f187 --- /dev/null +++ b/src/core/hle/service/ns/download_task_interface.cpp @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "core/hle/service/cmif_serialization.h" +#include "core/hle/service/ns/download_task_interface.h" + +namespace Service::NS { + +IDownloadTaskInterface::IDownloadTaskInterface(Core::System& system_) + : ServiceFramework{system_, "IDownloadTaskInterface"} { + // clang-format off + static const FunctionInfo functions[] = { + {701, nullptr, "ClearTaskStatusList"}, + {702, nullptr, "RequestDownloadTaskList"}, + {703, nullptr, "RequestEnsureDownloadTask"}, + {704, nullptr, "ListDownloadTaskStatus"}, + {705, nullptr, "RequestDownloadTaskListData"}, + {706, nullptr, "TryCommitCurrentApplicationDownloadTask"}, + {707, D<&IDownloadTaskInterface::EnableAutoCommit>, "EnableAutoCommit"}, + {708, D<&IDownloadTaskInterface::DisableAutoCommit>, "DisableAutoCommit"}, + {709, nullptr, "TriggerDynamicCommitEvent"}, + }; + // clang-format on + + RegisterHandlers(functions); +} + +IDownloadTaskInterface::~IDownloadTaskInterface() = default; + +Result IDownloadTaskInterface::EnableAutoCommit() { + LOG_WARNING(Service_NS, "(STUBBED) called"); + R_SUCCEED(); +} +Result IDownloadTaskInterface::DisableAutoCommit() { + LOG_WARNING(Service_NS, "(STUBBED) called"); + R_SUCCEED(); +} + +} // namespace Service::NS diff --git a/src/core/hle/service/ns/download_task_interface.h b/src/core/hle/service/ns/download_task_interface.h new file mode 100644 index 000000000..b1cb69cb8 --- /dev/null +++ b/src/core/hle/service/ns/download_task_interface.h @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "core/hle/service/service.h" + +namespace Service::NS { + +class IDownloadTaskInterface final : public ServiceFramework<IDownloadTaskInterface> { +public: + explicit IDownloadTaskInterface(Core::System& system_); + ~IDownloadTaskInterface() override; + +private: + Result EnableAutoCommit(); + Result DisableAutoCommit(); +}; + +} // namespace Service::NS diff --git a/src/core/hle/service/ns/dynamic_rights_interface.cpp b/src/core/hle/service/ns/dynamic_rights_interface.cpp new file mode 100644 index 000000000..ce81e203f --- /dev/null +++ b/src/core/hle/service/ns/dynamic_rights_interface.cpp @@ -0,0 +1,62 @@ +// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "core/hle/service/cmif_serialization.h" +#include "core/hle/service/ns/dynamic_rights_interface.h" + +namespace Service::NS { + +IDynamicRightsInterface::IDynamicRightsInterface(Core::System& system_) + : ServiceFramework{system_, "DynamicRightsInterface"} { + // clang-format off + static const FunctionInfo functions[] = { + {0, nullptr, "RequestApplicationRightsOnServer"}, + {1, nullptr, "RequestAssignRights"}, + {4, nullptr, "DeprecatedRequestAssignRightsToResume"}, + {5, D<&IDynamicRightsInterface::VerifyActivatedRightsOwners>, "VerifyActivatedRightsOwners"}, + {6, nullptr, "DeprecatedGetApplicationRightsStatus"}, + {7, nullptr, "RequestPrefetchForDynamicRights"}, + {8, nullptr, "GetDynamicRightsState"}, + {9, nullptr, "RequestApplicationRightsOnServerToResume"}, + {10, nullptr, "RequestAssignRightsToResume"}, + {11, nullptr, "GetActivatedRightsUsers"}, + {12, nullptr, "GetApplicationRightsStatus"}, + {13, D<&IDynamicRightsInterface::GetRunningApplicationStatus>, "GetRunningApplicationStatus"}, + {14, nullptr, "SelectApplicationLicense"}, + {15, nullptr, "RequestContentsAuthorizationToken"}, + {16, nullptr, "QualifyUser"}, + {17, nullptr, "QualifyUserWithProcessId"}, + {18, D<&IDynamicRightsInterface::NotifyApplicationRightsCheckStart>, "NotifyApplicationRightsCheckStart"}, + {19, nullptr, "UpdateUserList"}, + {20, nullptr, "IsRightsLostUser"}, + {21, nullptr, "SetRequiredAddOnContentsOnContentsAvailabilityTransition"}, + {22, nullptr, "GetLimitedApplicationLicense"}, + {23, nullptr, "GetLimitedApplicationLicenseUpgradableEvent"}, + {24, nullptr, "NotifyLimitedApplicationLicenseUpgradableEventForDebug"}, + {25, nullptr, "RequestProceedDynamicRightsState"}, + }; + // clang-format on + + RegisterHandlers(functions); +} + +IDynamicRightsInterface::~IDynamicRightsInterface() = default; + +Result IDynamicRightsInterface::NotifyApplicationRightsCheckStart() { + LOG_WARNING(Service_NS, "(STUBBED) called"); + R_SUCCEED(); +} + +Result IDynamicRightsInterface::GetRunningApplicationStatus(Out<u32> out_status, + u64 rights_handle) { + LOG_WARNING(Service_NS, "(STUBBED) called, rights_handle={:#x}", rights_handle); + *out_status = 0; + R_SUCCEED(); +} + +Result IDynamicRightsInterface::VerifyActivatedRightsOwners(u64 rights_handle) { + LOG_WARNING(Service_NS, "(STUBBED) called, rights_handle={:#x}", rights_handle); + R_SUCCEED(); +} + +} // namespace Service::NS diff --git a/src/core/hle/service/ns/dynamic_rights_interface.h b/src/core/hle/service/ns/dynamic_rights_interface.h new file mode 100644 index 000000000..877e009b0 --- /dev/null +++ b/src/core/hle/service/ns/dynamic_rights_interface.h @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "core/hle/service/cmif_types.h" +#include "core/hle/service/service.h" + +namespace Service::NS { + +class IDynamicRightsInterface final : public ServiceFramework<IDynamicRightsInterface> { +public: + explicit IDynamicRightsInterface(Core::System& system_); + ~IDynamicRightsInterface() override; + +private: + Result NotifyApplicationRightsCheckStart(); + Result GetRunningApplicationStatus(Out<u32> out_status, u64 rights_handle); + Result VerifyActivatedRightsOwners(u64 rights_handle); +}; + +} // namespace Service::NS diff --git a/src/core/hle/service/ns/ecommerce_interface.cpp b/src/core/hle/service/ns/ecommerce_interface.cpp new file mode 100644 index 000000000..76fc425f0 --- /dev/null +++ b/src/core/hle/service/ns/ecommerce_interface.cpp @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "core/hle/service/ns/ecommerce_interface.h" + +namespace Service::NS { + +IECommerceInterface::IECommerceInterface(Core::System& system_) + : ServiceFramework{system_, "IECommerceInterface"} { + // clang-format off + static const FunctionInfo functions[] = { + {0, nullptr, "RequestLinkDevice"}, + {1, nullptr, "RequestCleanupAllPreInstalledApplications"}, + {2, nullptr, "RequestCleanupPreInstalledApplication"}, + {3, nullptr, "RequestSyncRights"}, + {4, nullptr, "RequestUnlinkDevice"}, + {5, nullptr, "RequestRevokeAllELicense"}, + {6, nullptr, "RequestSyncRightsBasedOnAssignedELicenses"}, + }; + // clang-format on + + RegisterHandlers(functions); +} + +IECommerceInterface::~IECommerceInterface() = default; + +} // namespace Service::NS diff --git a/src/core/hle/service/ns/ecommerce_interface.h b/src/core/hle/service/ns/ecommerce_interface.h new file mode 100644 index 000000000..4352101f4 --- /dev/null +++ b/src/core/hle/service/ns/ecommerce_interface.h @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "core/hle/service/service.h" + +namespace Service::NS { + +class IECommerceInterface final : public ServiceFramework<IECommerceInterface> { +public: + explicit IECommerceInterface(Core::System& system_); + ~IECommerceInterface() override; +}; + +} // namespace Service::NS diff --git a/src/core/hle/service/ns/factory_reset_interface.cpp b/src/core/hle/service/ns/factory_reset_interface.cpp new file mode 100644 index 000000000..fd5cf7e1f --- /dev/null +++ b/src/core/hle/service/ns/factory_reset_interface.cpp @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "core/hle/service/ns/factory_reset_interface.h" + +namespace Service::NS { + +IFactoryResetInterface::IFactoryResetInterface(Core::System& system_) + : ServiceFramework{system_, "IFactoryResetInterface"} { + // clang-format off + static const FunctionInfo functions[] = { + {100, nullptr, "ResetToFactorySettings"}, + {101, nullptr, "ResetToFactorySettingsWithoutUserSaveData"}, + {102, nullptr, "ResetToFactorySettingsForRefurbishment"}, + {103, nullptr, "ResetToFactorySettingsWithPlatformRegion"}, + {104, nullptr, "ResetToFactorySettingsWithPlatformRegionAuthentication"}, + {105, nullptr, "RequestResetToFactorySettingsSecurely"}, + {106, nullptr, "RequestResetToFactorySettingsWithPlatformRegionAuthenticationSecurely"}, + }; + // clang-format on + + RegisterHandlers(functions); +} + +IFactoryResetInterface::~IFactoryResetInterface() = default; + +} // namespace Service::NS diff --git a/src/core/hle/service/ns/factory_reset_interface.h b/src/core/hle/service/ns/factory_reset_interface.h new file mode 100644 index 000000000..50d125123 --- /dev/null +++ b/src/core/hle/service/ns/factory_reset_interface.h @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "core/hle/service/service.h" + +namespace Service::NS { + +class IFactoryResetInterface final : public ServiceFramework<IFactoryResetInterface> { +public: + explicit IFactoryResetInterface(Core::System& system_); + ~IFactoryResetInterface() override; +}; + +} // namespace Service::NS diff --git a/src/core/hle/service/ns/ns.cpp b/src/core/hle/service/ns/ns.cpp index 19c3ff01b..8402e83cb 100644 --- a/src/core/hle/service/ns/ns.cpp +++ b/src/core/hle/service/ns/ns.cpp @@ -1,893 +1,38 @@ // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later -#include "common/logging/log.h" -#include "common/settings.h" -#include "core/arm/debug.h" -#include "core/core.h" -#include "core/file_sys/control_metadata.h" -#include "core/file_sys/patch_manager.h" -#include "core/file_sys/vfs/vfs.h" -#include "core/hle/service/filesystem/filesystem.h" -#include "core/hle/service/glue/glue_manager.h" -#include "core/hle/service/ipc_helpers.h" -#include "core/hle/service/ns/errors.h" -#include "core/hle/service/ns/iplatform_service_manager.h" -#include "core/hle/service/ns/language.h" +#include "core/hle/service/ns/develop_interface.h" #include "core/hle/service/ns/ns.h" -#include "core/hle/service/ns/pdm_qry.h" +#include "core/hle/service/ns/platform_service_manager.h" +#include "core/hle/service/ns/query_service.h" +#include "core/hle/service/ns/service_getter_interface.h" +#include "core/hle/service/ns/system_update_interface.h" +#include "core/hle/service/ns/vulnerability_manager_interface.h" #include "core/hle/service/server_manager.h" -#include "core/hle/service/set/settings_server.h" namespace Service::NS { -IAccountProxyInterface::IAccountProxyInterface(Core::System& system_) - : ServiceFramework{system_, "IAccountProxyInterface"} { - // clang-format off - static const FunctionInfo functions[] = { - {0, nullptr, "CreateUserAccount"}, - }; - // clang-format on - - RegisterHandlers(functions); -} - -IAccountProxyInterface::~IAccountProxyInterface() = default; - -IApplicationManagerInterface::IApplicationManagerInterface(Core::System& system_) - : ServiceFramework{system_, "IApplicationManagerInterface"} { - // clang-format off - static const FunctionInfo functions[] = { - {0, nullptr, "ListApplicationRecord"}, - {1, nullptr, "GenerateApplicationRecordCount"}, - {2, nullptr, "GetApplicationRecordUpdateSystemEvent"}, - {3, nullptr, "GetApplicationViewDeprecated"}, - {4, nullptr, "DeleteApplicationEntity"}, - {5, nullptr, "DeleteApplicationCompletely"}, - {6, nullptr, "IsAnyApplicationEntityRedundant"}, - {7, nullptr, "DeleteRedundantApplicationEntity"}, - {8, nullptr, "IsApplicationEntityMovable"}, - {9, nullptr, "MoveApplicationEntity"}, - {11, nullptr, "CalculateApplicationOccupiedSize"}, - {16, nullptr, "PushApplicationRecord"}, - {17, nullptr, "ListApplicationRecordContentMeta"}, - {19, nullptr, "LaunchApplicationOld"}, - {21, nullptr, "GetApplicationContentPath"}, - {22, nullptr, "TerminateApplication"}, - {23, nullptr, "ResolveApplicationContentPath"}, - {26, nullptr, "BeginInstallApplication"}, - {27, nullptr, "DeleteApplicationRecord"}, - {30, nullptr, "RequestApplicationUpdateInfo"}, - {31, nullptr, "Unknown31"}, - {32, nullptr, "CancelApplicationDownload"}, - {33, nullptr, "ResumeApplicationDownload"}, - {35, nullptr, "UpdateVersionList"}, - {36, nullptr, "PushLaunchVersion"}, - {37, nullptr, "ListRequiredVersion"}, - {38, nullptr, "CheckApplicationLaunchVersion"}, - {39, nullptr, "CheckApplicationLaunchRights"}, - {40, nullptr, "GetApplicationLogoData"}, - {41, nullptr, "CalculateApplicationDownloadRequiredSize"}, - {42, nullptr, "CleanupSdCard"}, - {43, nullptr, "CheckSdCardMountStatus"}, - {44, nullptr, "GetSdCardMountStatusChangedEvent"}, - {45, nullptr, "GetGameCardAttachmentEvent"}, - {46, nullptr, "GetGameCardAttachmentInfo"}, - {47, nullptr, "GetTotalSpaceSize"}, - {48, nullptr, "GetFreeSpaceSize"}, - {49, nullptr, "GetSdCardRemovedEvent"}, - {52, nullptr, "GetGameCardUpdateDetectionEvent"}, - {53, nullptr, "DisableApplicationAutoDelete"}, - {54, nullptr, "EnableApplicationAutoDelete"}, - {55, &IApplicationManagerInterface::GetApplicationDesiredLanguage, "GetApplicationDesiredLanguage"}, - {56, nullptr, "SetApplicationTerminateResult"}, - {57, nullptr, "ClearApplicationTerminateResult"}, - {58, nullptr, "GetLastSdCardMountUnexpectedResult"}, - {59, &IApplicationManagerInterface::ConvertApplicationLanguageToLanguageCode, "ConvertApplicationLanguageToLanguageCode"}, - {60, nullptr, "ConvertLanguageCodeToApplicationLanguage"}, - {61, nullptr, "GetBackgroundDownloadStressTaskInfo"}, - {62, nullptr, "GetGameCardStopper"}, - {63, nullptr, "IsSystemProgramInstalled"}, - {64, nullptr, "StartApplyDeltaTask"}, - {65, nullptr, "GetRequestServerStopper"}, - {66, nullptr, "GetBackgroundApplyDeltaStressTaskInfo"}, - {67, nullptr, "CancelApplicationApplyDelta"}, - {68, nullptr, "ResumeApplicationApplyDelta"}, - {69, nullptr, "CalculateApplicationApplyDeltaRequiredSize"}, - {70, nullptr, "ResumeAll"}, - {71, nullptr, "GetStorageSize"}, - {80, nullptr, "RequestDownloadApplication"}, - {81, nullptr, "RequestDownloadAddOnContent"}, - {82, nullptr, "DownloadApplication"}, - {83, nullptr, "CheckApplicationResumeRights"}, - {84, nullptr, "GetDynamicCommitEvent"}, - {85, nullptr, "RequestUpdateApplication2"}, - {86, nullptr, "EnableApplicationCrashReport"}, - {87, nullptr, "IsApplicationCrashReportEnabled"}, - {90, nullptr, "BoostSystemMemoryResourceLimit"}, - {91, nullptr, "DeprecatedLaunchApplication"}, - {92, nullptr, "GetRunningApplicationProgramId"}, - {93, nullptr, "GetMainApplicationProgramIndex"}, - {94, nullptr, "LaunchApplication"}, - {95, nullptr, "GetApplicationLaunchInfo"}, - {96, nullptr, "AcquireApplicationLaunchInfo"}, - {97, nullptr, "GetMainApplicationProgramIndexByApplicationLaunchInfo"}, - {98, nullptr, "EnableApplicationAllThreadDumpOnCrash"}, - {99, nullptr, "LaunchDevMenu"}, - {100, nullptr, "ResetToFactorySettings"}, - {101, nullptr, "ResetToFactorySettingsWithoutUserSaveData"}, - {102, nullptr, "ResetToFactorySettingsForRefurbishment"}, - {103, nullptr, "ResetToFactorySettingsWithPlatformRegion"}, - {104, nullptr, "ResetToFactorySettingsWithPlatformRegionAuthentication"}, - {105, nullptr, "RequestResetToFactorySettingsSecurely"}, - {106, nullptr, "RequestResetToFactorySettingsWithPlatformRegionAuthenticationSecurely"}, - {200, nullptr, "CalculateUserSaveDataStatistics"}, - {201, nullptr, "DeleteUserSaveDataAll"}, - {210, nullptr, "DeleteUserSystemSaveData"}, - {211, nullptr, "DeleteSaveData"}, - {220, nullptr, "UnregisterNetworkServiceAccount"}, - {221, nullptr, "UnregisterNetworkServiceAccountWithUserSaveDataDeletion"}, - {300, nullptr, "GetApplicationShellEvent"}, - {301, nullptr, "PopApplicationShellEventInfo"}, - {302, nullptr, "LaunchLibraryApplet"}, - {303, nullptr, "TerminateLibraryApplet"}, - {304, nullptr, "LaunchSystemApplet"}, - {305, nullptr, "TerminateSystemApplet"}, - {306, nullptr, "LaunchOverlayApplet"}, - {307, nullptr, "TerminateOverlayApplet"}, - {400, &IApplicationManagerInterface::GetApplicationControlData, "GetApplicationControlData"}, - {401, nullptr, "InvalidateAllApplicationControlCache"}, - {402, nullptr, "RequestDownloadApplicationControlData"}, - {403, nullptr, "GetMaxApplicationControlCacheCount"}, - {404, nullptr, "InvalidateApplicationControlCache"}, - {405, nullptr, "ListApplicationControlCacheEntryInfo"}, - {406, nullptr, "GetApplicationControlProperty"}, - {407, nullptr, "ListApplicationTitle"}, - {408, nullptr, "ListApplicationIcon"}, - {502, nullptr, "RequestCheckGameCardRegistration"}, - {503, nullptr, "RequestGameCardRegistrationGoldPoint"}, - {504, nullptr, "RequestRegisterGameCard"}, - {505, nullptr, "GetGameCardMountFailureEvent"}, - {506, nullptr, "IsGameCardInserted"}, - {507, nullptr, "EnsureGameCardAccess"}, - {508, nullptr, "GetLastGameCardMountFailureResult"}, - {509, nullptr, "ListApplicationIdOnGameCard"}, - {510, nullptr, "GetGameCardPlatformRegion"}, - {600, nullptr, "CountApplicationContentMeta"}, - {601, nullptr, "ListApplicationContentMetaStatus"}, - {602, nullptr, "ListAvailableAddOnContent"}, - {603, nullptr, "GetOwnedApplicationContentMetaStatus"}, - {604, nullptr, "RegisterContentsExternalKey"}, - {605, nullptr, "ListApplicationContentMetaStatusWithRightsCheck"}, - {606, nullptr, "GetContentMetaStorage"}, - {607, nullptr, "ListAvailableAddOnContent"}, - {609, nullptr, "ListAvailabilityAssuredAddOnContent"}, - {610, nullptr, "GetInstalledContentMetaStorage"}, - {611, nullptr, "PrepareAddOnContent"}, - {700, nullptr, "PushDownloadTaskList"}, - {701, nullptr, "ClearTaskStatusList"}, - {702, nullptr, "RequestDownloadTaskList"}, - {703, nullptr, "RequestEnsureDownloadTask"}, - {704, nullptr, "ListDownloadTaskStatus"}, - {705, nullptr, "RequestDownloadTaskListData"}, - {800, nullptr, "RequestVersionList"}, - {801, nullptr, "ListVersionList"}, - {802, nullptr, "RequestVersionListData"}, - {900, nullptr, "GetApplicationRecord"}, - {901, nullptr, "GetApplicationRecordProperty"}, - {902, nullptr, "EnableApplicationAutoUpdate"}, - {903, nullptr, "DisableApplicationAutoUpdate"}, - {904, nullptr, "TouchApplication"}, - {905, nullptr, "RequestApplicationUpdate"}, - {906, nullptr, "IsApplicationUpdateRequested"}, - {907, nullptr, "WithdrawApplicationUpdateRequest"}, - {908, nullptr, "ListApplicationRecordInstalledContentMeta"}, - {909, nullptr, "WithdrawCleanupAddOnContentsWithNoRightsRecommendation"}, - {910, nullptr, "HasApplicationRecord"}, - {911, nullptr, "SetPreInstalledApplication"}, - {912, nullptr, "ClearPreInstalledApplicationFlag"}, - {913, nullptr, "ListAllApplicationRecord"}, - {914, nullptr, "HideApplicationRecord"}, - {915, nullptr, "ShowApplicationRecord"}, - {916, nullptr, "IsApplicationAutoDeleteDisabled"}, - {1000, nullptr, "RequestVerifyApplicationDeprecated"}, - {1001, nullptr, "CorruptApplicationForDebug"}, - {1002, nullptr, "RequestVerifyAddOnContentsRights"}, - {1003, nullptr, "RequestVerifyApplication"}, - {1004, nullptr, "CorruptContentForDebug"}, - {1200, nullptr, "NeedsUpdateVulnerability"}, - {1300, nullptr, "IsAnyApplicationEntityInstalled"}, - {1301, nullptr, "DeleteApplicationContentEntities"}, - {1302, nullptr, "CleanupUnrecordedApplicationEntity"}, - {1303, nullptr, "CleanupAddOnContentsWithNoRights"}, - {1304, nullptr, "DeleteApplicationContentEntity"}, - {1305, nullptr, "TryDeleteRunningApplicationEntity"}, - {1306, nullptr, "TryDeleteRunningApplicationCompletely"}, - {1307, nullptr, "TryDeleteRunningApplicationContentEntities"}, - {1308, nullptr, "DeleteApplicationCompletelyForDebug"}, - {1309, nullptr, "CleanupUnavailableAddOnContents"}, - {1310, nullptr, "RequestMoveApplicationEntity"}, - {1311, nullptr, "EstimateSizeToMove"}, - {1312, nullptr, "HasMovableEntity"}, - {1313, nullptr, "CleanupOrphanContents"}, - {1314, nullptr, "CheckPreconditionSatisfiedToMove"}, - {1400, nullptr, "PrepareShutdown"}, - {1500, nullptr, "FormatSdCard"}, - {1501, nullptr, "NeedsSystemUpdateToFormatSdCard"}, - {1502, nullptr, "GetLastSdCardFormatUnexpectedResult"}, - {1504, nullptr, "InsertSdCard"}, - {1505, nullptr, "RemoveSdCard"}, - {1506, nullptr, "GetSdCardStartupStatus"}, - {1600, nullptr, "GetSystemSeedForPseudoDeviceId"}, - {1601, nullptr, "ResetSystemSeedForPseudoDeviceId"}, - {1700, nullptr, "ListApplicationDownloadingContentMeta"}, - {1701, nullptr, "GetApplicationView"}, - {1702, nullptr, "GetApplicationDownloadTaskStatus"}, - {1703, nullptr, "GetApplicationViewDownloadErrorContext"}, - {1704, nullptr, "GetApplicationViewWithPromotionInfo"}, - {1705, nullptr, "IsPatchAutoDeletableApplication"}, - {1800, nullptr, "IsNotificationSetupCompleted"}, - {1801, nullptr, "GetLastNotificationInfoCount"}, - {1802, nullptr, "ListLastNotificationInfo"}, - {1803, nullptr, "ListNotificationTask"}, - {1900, nullptr, "IsActiveAccount"}, - {1901, nullptr, "RequestDownloadApplicationPrepurchasedRights"}, - {1902, nullptr, "GetApplicationTicketInfo"}, - {1903, nullptr, "RequestDownloadApplicationPrepurchasedRightsForAccount"}, - {2000, nullptr, "GetSystemDeliveryInfo"}, - {2001, nullptr, "SelectLatestSystemDeliveryInfo"}, - {2002, nullptr, "VerifyDeliveryProtocolVersion"}, - {2003, nullptr, "GetApplicationDeliveryInfo"}, - {2004, nullptr, "HasAllContentsToDeliver"}, - {2005, nullptr, "CompareApplicationDeliveryInfo"}, - {2006, nullptr, "CanDeliverApplication"}, - {2007, nullptr, "ListContentMetaKeyToDeliverApplication"}, - {2008, nullptr, "NeedsSystemUpdateToDeliverApplication"}, - {2009, nullptr, "EstimateRequiredSize"}, - {2010, nullptr, "RequestReceiveApplication"}, - {2011, nullptr, "CommitReceiveApplication"}, - {2012, nullptr, "GetReceiveApplicationProgress"}, - {2013, nullptr, "RequestSendApplication"}, - {2014, nullptr, "GetSendApplicationProgress"}, - {2015, nullptr, "CompareSystemDeliveryInfo"}, - {2016, nullptr, "ListNotCommittedContentMeta"}, - {2017, nullptr, "CreateDownloadTask"}, - {2018, nullptr, "GetApplicationDeliveryInfoHash"}, - {2050, nullptr, "GetApplicationRightsOnClient"}, - {2051, nullptr, "InvalidateRightsIdCache"}, - {2100, nullptr, "GetApplicationTerminateResult"}, - {2101, nullptr, "GetRawApplicationTerminateResult"}, - {2150, nullptr, "CreateRightsEnvironment"}, - {2151, nullptr, "DestroyRightsEnvironment"}, - {2152, nullptr, "ActivateRightsEnvironment"}, - {2153, nullptr, "DeactivateRightsEnvironment"}, - {2154, nullptr, "ForceActivateRightsContextForExit"}, - {2155, nullptr, "UpdateRightsEnvironmentStatus"}, - {2156, nullptr, "CreateRightsEnvironmentForMicroApplication"}, - {2160, nullptr, "AddTargetApplicationToRightsEnvironment"}, - {2161, nullptr, "SetUsersToRightsEnvironment"}, - {2170, nullptr, "GetRightsEnvironmentStatus"}, - {2171, nullptr, "GetRightsEnvironmentStatusChangedEvent"}, - {2180, nullptr, "RequestExtendRightsInRightsEnvironment"}, - {2181, nullptr, "GetResultOfExtendRightsInRightsEnvironment"}, - {2182, nullptr, "SetActiveRightsContextUsingStateToRightsEnvironment"}, - {2190, nullptr, "GetRightsEnvironmentHandleForApplication"}, - {2199, nullptr, "GetRightsEnvironmentCountForDebug"}, - {2200, nullptr, "GetGameCardApplicationCopyIdentifier"}, - {2201, nullptr, "GetInstalledApplicationCopyIdentifier"}, - {2250, nullptr, "RequestReportActiveELicence"}, - {2300, nullptr, "ListEventLog"}, - {2350, nullptr, "PerformAutoUpdateByApplicationId"}, - {2351, nullptr, "RequestNoDownloadRightsErrorResolution"}, - {2352, nullptr, "RequestResolveNoDownloadRightsError"}, - {2353, nullptr, "GetApplicationDownloadTaskInfo"}, - {2354, nullptr, "PrioritizeApplicationBackgroundTask"}, - {2355, nullptr, "PreferStorageEfficientUpdate"}, - {2356, nullptr, "RequestStorageEfficientUpdatePreferable"}, - {2357, nullptr, "EnableMultiCoreDownload"}, - {2358, nullptr, "DisableMultiCoreDownload"}, - {2359, nullptr, "IsMultiCoreDownloadEnabled"}, - {2400, nullptr, "GetPromotionInfo"}, - {2401, nullptr, "CountPromotionInfo"}, - {2402, nullptr, "ListPromotionInfo"}, - {2403, nullptr, "ImportPromotionJsonForDebug"}, - {2404, nullptr, "ClearPromotionInfoForDebug"}, - {2500, nullptr, "ConfirmAvailableTime"}, - {2510, nullptr, "CreateApplicationResource"}, - {2511, nullptr, "GetApplicationResource"}, - {2513, nullptr, "LaunchMicroApplication"}, - {2514, nullptr, "ClearTaskOfAsyncTaskManager"}, - {2515, nullptr, "CleanupAllPlaceHolderAndFragmentsIfNoTask"}, - {2516, nullptr, "EnsureApplicationCertificate"}, - {2517, nullptr, "CreateApplicationInstance"}, - {2518, nullptr, "UpdateQualificationForDebug"}, - {2519, nullptr, "IsQualificationTransitionSupported"}, - {2520, nullptr, "IsQualificationTransitionSupportedByProcessId"}, - {2521, nullptr, "GetRightsUserChangedEvent"}, - {2522, nullptr, "IsRomRedirectionAvailable"}, - {2800, nullptr, "GetApplicationIdOfPreomia"}, - {3000, nullptr, "RegisterDeviceLockKey"}, - {3001, nullptr, "UnregisterDeviceLockKey"}, - {3002, nullptr, "VerifyDeviceLockKey"}, - {3003, nullptr, "HideApplicationIcon"}, - {3004, nullptr, "ShowApplicationIcon"}, - {3005, nullptr, "HideApplicationTitle"}, - {3006, nullptr, "ShowApplicationTitle"}, - {3007, nullptr, "EnableGameCard"}, - {3008, nullptr, "DisableGameCard"}, - {3009, nullptr, "EnableLocalContentShare"}, - {3010, nullptr, "DisableLocalContentShare"}, - {3011, nullptr, "IsApplicationIconHidden"}, - {3012, nullptr, "IsApplicationTitleHidden"}, - {3013, nullptr, "IsGameCardEnabled"}, - {3014, nullptr, "IsLocalContentShareEnabled"}, - {3050, nullptr, "ListAssignELicenseTaskResult"}, - {9999, nullptr, "GetApplicationCertificate"}, - }; - // clang-format on - - RegisterHandlers(functions); -} - -IApplicationManagerInterface::~IApplicationManagerInterface() = default; - -void IApplicationManagerInterface::GetApplicationControlData(HLERequestContext& ctx) { - IPC::RequestParser rp{ctx}; - const auto flag = rp.PopRaw<u64>(); - LOG_DEBUG(Service_NS, "called with flag={:016X}", flag); - - const auto title_id = rp.PopRaw<u64>(); - - const auto size = ctx.GetWriteBufferSize(); - - const FileSys::PatchManager pm{title_id, system.GetFileSystemController(), - system.GetContentProvider()}; - const auto control = pm.GetControlMetadata(); - - std::vector<u8> out; - - if (control.first != nullptr) { - if (size < 0x4000) { - LOG_ERROR(Service_NS, - "output buffer is too small! (actual={:016X}, expected_min=0x4000)", size); - IPC::ResponseBuilder rb{ctx, 2}; - // TODO(DarkLordZach): Find a better error code for this. - rb.Push(ResultUnknown); - return; - } - - out.resize(0x4000); - const auto bytes = control.first->GetRawBytes(); - std::memcpy(out.data(), bytes.data(), bytes.size()); - } else { - LOG_WARNING(Service_NS, "missing NACP data for title_id={:016X}, defaulting to zeros.", - title_id); - out.resize(std::min<u64>(0x4000, size)); - } - - if (control.second != nullptr) { - if (size < 0x4000 + control.second->GetSize()) { - LOG_ERROR(Service_NS, - "output buffer is too small! (actual={:016X}, expected_min={:016X})", size, - 0x4000 + control.second->GetSize()); - IPC::ResponseBuilder rb{ctx, 2}; - // TODO(DarkLordZach): Find a better error code for this. - rb.Push(ResultUnknown); - return; - } - - out.resize(0x4000 + control.second->GetSize()); - control.second->Read(out.data() + 0x4000, control.second->GetSize()); - } else { - LOG_WARNING(Service_NS, "missing icon data for title_id={:016X}, defaulting to zeros.", - title_id); - } - - ctx.WriteBuffer(out); - - IPC::ResponseBuilder rb{ctx, 3}; - rb.Push(ResultSuccess); - rb.Push<u32>(static_cast<u32>(out.size())); -} - -void IApplicationManagerInterface::GetApplicationDesiredLanguage(HLERequestContext& ctx) { - IPC::RequestParser rp{ctx}; - const auto supported_languages = rp.Pop<u32>(); - - u8 desired_language{}; - const auto res = GetApplicationDesiredLanguage(&desired_language, supported_languages); - if (res == ResultSuccess) { - IPC::ResponseBuilder rb{ctx, 3}; - rb.Push(ResultSuccess); - rb.Push<u32>(desired_language); - } else { - IPC::ResponseBuilder rb{ctx, 2}; - rb.Push(res); - } -} - -Result IApplicationManagerInterface::GetApplicationDesiredLanguage(u8* out_desired_language, - const u32 supported_languages) { - LOG_DEBUG(Service_NS, "called with supported_languages={:08X}", supported_languages); - - // Get language code from settings - const auto language_code = - Set::GetLanguageCodeFromIndex(static_cast<s32>(Settings::values.language_index.GetValue())); - - // Convert to application language, get priority list - const auto application_language = ConvertToApplicationLanguage(language_code); - if (application_language == std::nullopt) { - LOG_ERROR(Service_NS, "Could not convert application language! language_code={}", - language_code); - return Service::NS::ResultApplicationLanguageNotFound; - } - const auto priority_list = GetApplicationLanguagePriorityList(*application_language); - if (!priority_list) { - LOG_ERROR(Service_NS, - "Could not find application language priorities! application_language={}", - *application_language); - return Service::NS::ResultApplicationLanguageNotFound; - } - - // Try to find a valid language. - for (const auto lang : *priority_list) { - const auto supported_flag = GetSupportedLanguageFlag(lang); - if (supported_languages == 0 || (supported_languages & supported_flag) == supported_flag) { - *out_desired_language = static_cast<u8>(lang); - return ResultSuccess; - } - } - - LOG_ERROR(Service_NS, "Could not find a valid language! supported_languages={:08X}", - supported_languages); - return Service::NS::ResultApplicationLanguageNotFound; -} - -void IApplicationManagerInterface::ConvertApplicationLanguageToLanguageCode( - HLERequestContext& ctx) { - IPC::RequestParser rp{ctx}; - const auto application_language = rp.Pop<u8>(); - - u64 language_code{}; - const auto res = ConvertApplicationLanguageToLanguageCode(&language_code, application_language); - if (res == ResultSuccess) { - IPC::ResponseBuilder rb{ctx, 4}; - rb.Push(ResultSuccess); - rb.Push(language_code); - } else { - IPC::ResponseBuilder rb{ctx, 2}; - rb.Push(res); - } -} - -Result IApplicationManagerInterface::ConvertApplicationLanguageToLanguageCode( - u64* out_language_code, u8 application_language) { - const auto language_code = - ConvertToLanguageCode(static_cast<ApplicationLanguage>(application_language)); - if (language_code == std::nullopt) { - LOG_ERROR(Service_NS, "Language not found! application_language={}", application_language); - return Service::NS::ResultApplicationLanguageNotFound; - } - - *out_language_code = static_cast<u64>(*language_code); - return ResultSuccess; -} - -IApplicationVersionInterface::IApplicationVersionInterface(Core::System& system_) - : ServiceFramework{system_, "IApplicationVersionInterface"} { - // clang-format off - static const FunctionInfo functions[] = { - {0, nullptr, "GetLaunchRequiredVersion"}, - {1, nullptr, "UpgradeLaunchRequiredVersion"}, - {35, nullptr, "UpdateVersionList"}, - {36, nullptr, "PushLaunchVersion"}, - {37, nullptr, "ListRequiredVersion"}, - {800, nullptr, "RequestVersionList"}, - {801, nullptr, "ListVersionList"}, - {802, nullptr, "RequestVersionListData"}, - {900, nullptr, "ImportAutoUpdatePolicyJsonForDebug"}, - {901, nullptr, "ListDefaultAutoUpdatePolicy"}, - {902, nullptr, "ListAutoUpdatePolicyForSpecificApplication"}, - {1000, nullptr, "PerformAutoUpdate"}, - {1001, nullptr, "ListAutoUpdateSchedule"}, - }; - // clang-format on - - RegisterHandlers(functions); -} - -IApplicationVersionInterface::~IApplicationVersionInterface() = default; - -IContentManagementInterface::IContentManagementInterface(Core::System& system_) - : ServiceFramework{system_, "IContentManagementInterface"} { - // clang-format off - static const FunctionInfo functions[] = { - {11, nullptr, "CalculateApplicationOccupiedSize"}, - {43, nullptr, "CheckSdCardMountStatus"}, - {47, &IContentManagementInterface::GetTotalSpaceSize, "GetTotalSpaceSize"}, - {48, &IContentManagementInterface::GetFreeSpaceSize, "GetFreeSpaceSize"}, - {600, nullptr, "CountApplicationContentMeta"}, - {601, nullptr, "ListApplicationContentMetaStatus"}, - {605, nullptr, "ListApplicationContentMetaStatusWithRightsCheck"}, - {607, nullptr, "IsAnyApplicationRunning"}, - }; - // clang-format on - - RegisterHandlers(functions); -} - -IContentManagementInterface::~IContentManagementInterface() = default; - -void IContentManagementInterface::GetTotalSpaceSize(HLERequestContext& ctx) { - IPC::RequestParser rp{ctx}; - const auto storage{rp.PopEnum<FileSys::StorageId>()}; - - LOG_INFO(Service_Capture, "called, storage={}", storage); - - IPC::ResponseBuilder rb{ctx, 4}; - rb.Push(ResultSuccess); - rb.Push<u64>(system.GetFileSystemController().GetTotalSpaceSize(storage)); -} - -void IContentManagementInterface::GetFreeSpaceSize(HLERequestContext& ctx) { - IPC::RequestParser rp{ctx}; - const auto storage{rp.PopEnum<FileSys::StorageId>()}; - - LOG_INFO(Service_Capture, "called, storage={}", storage); - - IPC::ResponseBuilder rb{ctx, 4}; - rb.Push(ResultSuccess); - rb.Push<u64>(system.GetFileSystemController().GetFreeSpaceSize(storage)); -} - -IDocumentInterface::IDocumentInterface(Core::System& system_) - : ServiceFramework{system_, "IDocumentInterface"} { - // clang-format off - static const FunctionInfo functions[] = { - {21, nullptr, "GetApplicationContentPath"}, - {23, &IDocumentInterface::ResolveApplicationContentPath, "ResolveApplicationContentPath"}, - {92, &IDocumentInterface::GetRunningApplicationProgramId, "GetRunningApplicationProgramId"}, - }; - // clang-format on - - RegisterHandlers(functions); -} - -IDocumentInterface::~IDocumentInterface() = default; - -void IDocumentInterface::ResolveApplicationContentPath(HLERequestContext& ctx) { - struct ContentPath { - u8 file_system_proxy_type; - u64 program_id; - }; - static_assert(sizeof(ContentPath) == 0x10, "ContentPath has wrong size"); - - IPC::RequestParser rp{ctx}; - auto content_path = rp.PopRaw<ContentPath>(); - LOG_WARNING(Service_NS, "(STUBBED) called, file_system_proxy_type={}, program_id={:016X}", - content_path.file_system_proxy_type, content_path.program_id); - - IPC::ResponseBuilder rb{ctx, 2}; - rb.Push(ResultSuccess); -} - -void IDocumentInterface::GetRunningApplicationProgramId(HLERequestContext& ctx) { - IPC::RequestParser rp{ctx}; - const auto caller_program_id = rp.PopRaw<u64>(); - LOG_WARNING(Service_NS, "(STUBBED) called, caller_program_id={:016X}", caller_program_id); - - IPC::ResponseBuilder rb{ctx, 4}; - rb.Push(ResultSuccess); - rb.Push<u64>(system.GetApplicationProcessProgramID()); -} - -IDownloadTaskInterface::IDownloadTaskInterface(Core::System& system_) - : ServiceFramework{system_, "IDownloadTaskInterface"} { - // clang-format off - static const FunctionInfo functions[] = { - {701, nullptr, "ClearTaskStatusList"}, - {702, nullptr, "RequestDownloadTaskList"}, - {703, nullptr, "RequestEnsureDownloadTask"}, - {704, nullptr, "ListDownloadTaskStatus"}, - {705, nullptr, "RequestDownloadTaskListData"}, - {706, nullptr, "TryCommitCurrentApplicationDownloadTask"}, - {707, nullptr, "EnableAutoCommit"}, - {708, nullptr, "DisableAutoCommit"}, - {709, nullptr, "TriggerDynamicCommitEvent"}, - }; - // clang-format on - - RegisterHandlers(functions); -} - -IDownloadTaskInterface::~IDownloadTaskInterface() = default; - -IECommerceInterface::IECommerceInterface(Core::System& system_) - : ServiceFramework{system_, "IECommerceInterface"} { - // clang-format off - static const FunctionInfo functions[] = { - {0, nullptr, "RequestLinkDevice"}, - {1, nullptr, "RequestCleanupAllPreInstalledApplications"}, - {2, nullptr, "RequestCleanupPreInstalledApplication"}, - {3, nullptr, "RequestSyncRights"}, - {4, nullptr, "RequestUnlinkDevice"}, - {5, nullptr, "RequestRevokeAllELicense"}, - {6, nullptr, "RequestSyncRightsBasedOnAssignedELicenses"}, - }; - // clang-format on - - RegisterHandlers(functions); -} - -IECommerceInterface::~IECommerceInterface() = default; - -IFactoryResetInterface::IFactoryResetInterface(Core::System& system_) - : ServiceFramework{system_, "IFactoryResetInterface"} { - // clang-format off - static const FunctionInfo functions[] = { - {100, nullptr, "ResetToFactorySettings"}, - {101, nullptr, "ResetToFactorySettingsWithoutUserSaveData"}, - {102, nullptr, "ResetToFactorySettingsForRefurbishment"}, - {103, nullptr, "ResetToFactorySettingsWithPlatformRegion"}, - {104, nullptr, "ResetToFactorySettingsWithPlatformRegionAuthentication"}, - {105, nullptr, "RequestResetToFactorySettingsSecurely"}, - {106, nullptr, "RequestResetToFactorySettingsWithPlatformRegionAuthenticationSecurely"}, - }; - // clang-format on - - RegisterHandlers(functions); -} - -IFactoryResetInterface::~IFactoryResetInterface() = default; - -IReadOnlyApplicationRecordInterface::IReadOnlyApplicationRecordInterface(Core::System& system_) - : ServiceFramework{system_, "IReadOnlyApplicationRecordInterface"} { - static const FunctionInfo functions[] = { - {0, &IReadOnlyApplicationRecordInterface::HasApplicationRecord, "HasApplicationRecord"}, - {1, nullptr, "NotifyApplicationFailure"}, - {2, &IReadOnlyApplicationRecordInterface::IsDataCorruptedResult, "IsDataCorruptedResult"}, - }; - // clang-format on - - RegisterHandlers(functions); -} - -IReadOnlyApplicationRecordInterface::~IReadOnlyApplicationRecordInterface() = default; - -void IReadOnlyApplicationRecordInterface::HasApplicationRecord(HLERequestContext& ctx) { - IPC::RequestParser rp{ctx}; - const u64 program_id = rp.PopRaw<u64>(); - LOG_WARNING(Service_NS, "(STUBBED) called, program_id={:X}", program_id); - - IPC::ResponseBuilder rb{ctx, 3}; - rb.Push(ResultSuccess); - rb.Push<u8>(1); -} - -void IReadOnlyApplicationRecordInterface::IsDataCorruptedResult(HLERequestContext& ctx) { - IPC::RequestParser rp{ctx}; - const auto result = rp.PopRaw<Result>(); - LOG_WARNING(Service_NS, "(STUBBED) called, result={:#x}", result.GetInnerValue()); - - IPC::ResponseBuilder rb{ctx, 3}; - rb.Push(ResultSuccess); - rb.Push<u8>(0); -} - -IReadOnlyApplicationControlDataInterface::IReadOnlyApplicationControlDataInterface( - Core::System& system_) - : ServiceFramework{system_, "IReadOnlyApplicationControlDataInterface"} { - // clang-format off - static const FunctionInfo functions[] = { - {0, &IReadOnlyApplicationControlDataInterface::GetApplicationControlData, "GetApplicationControlData"}, - {1, nullptr, "GetApplicationDesiredLanguage"}, - {2, nullptr, "ConvertApplicationLanguageToLanguageCode"}, - {3, nullptr, "ConvertLanguageCodeToApplicationLanguage"}, - {4, nullptr, "SelectApplicationDesiredLanguage"}, - }; - // clang-format on - - RegisterHandlers(functions); -} - -IReadOnlyApplicationControlDataInterface::~IReadOnlyApplicationControlDataInterface() = default; - -void IReadOnlyApplicationControlDataInterface::GetApplicationControlData(HLERequestContext& ctx) { - enum class ApplicationControlSource : u8 { - CacheOnly, - Storage, - StorageOnly, - }; - - struct RequestParameters { - ApplicationControlSource source; - u64 application_id; - }; - static_assert(sizeof(RequestParameters) == 0x10, "RequestParameters has incorrect size."); - - IPC::RequestParser rp{ctx}; - std::vector<u8> nacp_data{}; - const auto parameters{rp.PopRaw<RequestParameters>()}; - const auto result = - system.GetARPManager().GetControlProperty(&nacp_data, parameters.application_id); - - if (result == ResultSuccess) { - ctx.WriteBuffer(nacp_data.data(), nacp_data.size()); - } - - IPC::ResponseBuilder rb{ctx, 2}; - rb.Push(result); -} - -NS::NS(const char* name, Core::System& system_) : ServiceFramework{system_, name} { - // clang-format off - static const FunctionInfo functions[] = { - {7988, nullptr, "GetDynamicRightsInterface"}, - {7989, &NS::PushInterface<IReadOnlyApplicationControlDataInterface>, "GetReadOnlyApplicationControlDataInterface"}, - {7991, &NS::PushInterface<IReadOnlyApplicationRecordInterface>, "GetReadOnlyApplicationRecordInterface"}, - {7992, &NS::PushInterface<IECommerceInterface>, "GetECommerceInterface"}, - {7993, &NS::PushInterface<IApplicationVersionInterface>, "GetApplicationVersionInterface"}, - {7994, &NS::PushInterface<IFactoryResetInterface>, "GetFactoryResetInterface"}, - {7995, &NS::PushInterface<IAccountProxyInterface>, "GetAccountProxyInterface"}, - {7996, &NS::PushIApplicationManagerInterface, "GetApplicationManagerInterface"}, - {7997, &NS::PushInterface<IDownloadTaskInterface>, "GetDownloadTaskInterface"}, - {7998, &NS::PushInterface<IContentManagementInterface>, "GetContentManagementInterface"}, - {7999, &NS::PushInterface<IDocumentInterface>, "GetDocumentInterface"}, - }; - // clang-format on - - RegisterHandlers(functions); -} - -NS::~NS() = default; - -std::shared_ptr<IApplicationManagerInterface> NS::GetApplicationManagerInterface() const { - return GetInterface<IApplicationManagerInterface>(system); -} - -class NS_DEV final : public ServiceFramework<NS_DEV> { -public: - explicit NS_DEV(Core::System& system_) : ServiceFramework{system_, "ns:dev"} { - // clang-format off - static const FunctionInfo functions[] = { - {0, nullptr, "LaunchProgram"}, - {1, nullptr, "TerminateProcess"}, - {2, nullptr, "TerminateProgram"}, - {4, nullptr, "GetShellEvent"}, - {5, nullptr, "GetShellEventInfo"}, - {6, nullptr, "TerminateApplication"}, - {7, nullptr, "PrepareLaunchProgramFromHost"}, - {8, nullptr, "LaunchApplicationFromHost"}, - {9, nullptr, "LaunchApplicationWithStorageIdForDevelop"}, - {10, nullptr, "IsSystemMemoryResourceLimitBoosted"}, - {11, nullptr, "GetRunningApplicationProcessIdForDevelop"}, - {12, nullptr, "SetCurrentApplicationRightsEnvironmentCanBeActiveForDevelop"}, - {13, nullptr, "CreateApplicationResourceForDevelop"}, - {14, nullptr, "IsPreomiaForDevelop"}, - {15, nullptr, "GetApplicationProgramIdFromHost"}, - {16, nullptr, "RefreshCachedDebugValues"}, - {17, nullptr, "PrepareLaunchApplicationFromHost"}, - {18, nullptr, "GetLaunchEvent"}, - {19, nullptr, "GetLaunchResult"}, - }; - // clang-format on - - RegisterHandlers(functions); - } -}; - -class ISystemUpdateControl final : public ServiceFramework<ISystemUpdateControl> { -public: - explicit ISystemUpdateControl(Core::System& system_) - : ServiceFramework{system_, "ISystemUpdateControl"} { - // clang-format off - static const FunctionInfo functions[] = { - {0, nullptr, "HasDownloaded"}, - {1, nullptr, "RequestCheckLatestUpdate"}, - {2, nullptr, "RequestDownloadLatestUpdate"}, - {3, nullptr, "GetDownloadProgress"}, - {4, nullptr, "ApplyDownloadedUpdate"}, - {5, nullptr, "RequestPrepareCardUpdate"}, - {6, nullptr, "GetPrepareCardUpdateProgress"}, - {7, nullptr, "HasPreparedCardUpdate"}, - {8, nullptr, "ApplyCardUpdate"}, - {9, nullptr, "GetDownloadedEulaDataSize"}, - {10, nullptr, "GetDownloadedEulaData"}, - {11, nullptr, "SetupCardUpdate"}, - {12, nullptr, "GetPreparedCardUpdateEulaDataSize"}, - {13, nullptr, "GetPreparedCardUpdateEulaData"}, - {14, nullptr, "SetupCardUpdateViaSystemUpdater"}, - {15, nullptr, "HasReceived"}, - {16, nullptr, "RequestReceiveSystemUpdate"}, - {17, nullptr, "GetReceiveProgress"}, - {18, nullptr, "ApplyReceivedUpdate"}, - {19, nullptr, "GetReceivedEulaDataSize"}, - {20, nullptr, "GetReceivedEulaData"}, - {21, nullptr, "SetupToReceiveSystemUpdate"}, - {22, nullptr, "RequestCheckLatestUpdateIncludesRebootlessUpdate"}, - }; - // clang-format on - - RegisterHandlers(functions); - } -}; - -class NS_SU final : public ServiceFramework<NS_SU> { -public: - explicit NS_SU(Core::System& system_) : ServiceFramework{system_, "ns:su"} { - // clang-format off - static const FunctionInfo functions[] = { - {0, nullptr, "GetBackgroundNetworkUpdateState"}, - {1, &NS_SU::OpenSystemUpdateControl, "OpenSystemUpdateControl"}, - {2, nullptr, "NotifyExFatDriverRequired"}, - {3, nullptr, "ClearExFatDriverStatusForDebug"}, - {4, nullptr, "RequestBackgroundNetworkUpdate"}, - {5, nullptr, "NotifyBackgroundNetworkUpdate"}, - {6, nullptr, "NotifyExFatDriverDownloadedForDebug"}, - {9, nullptr, "GetSystemUpdateNotificationEventForContentDelivery"}, - {10, nullptr, "NotifySystemUpdateForContentDelivery"}, - {11, nullptr, "PrepareShutdown"}, - {12, nullptr, "Unknown12"}, - {13, nullptr, "Unknown13"}, - {14, nullptr, "Unknown14"}, - {15, nullptr, "Unknown15"}, - {16, nullptr, "DestroySystemUpdateTask"}, - {17, nullptr, "RequestSendSystemUpdate"}, - {18, nullptr, "GetSendSystemUpdateProgress"}, - }; - // clang-format on - - RegisterHandlers(functions); - } - -private: - void OpenSystemUpdateControl(HLERequestContext& ctx) { - LOG_DEBUG(Service_NS, "called"); - - IPC::ResponseBuilder rb{ctx, 2, 0, 1}; - rb.Push(ResultSuccess); - rb.PushIpcInterface<ISystemUpdateControl>(system); - } -}; - -class NS_VM final : public ServiceFramework<NS_VM> { -public: - explicit NS_VM(Core::System& system_) : ServiceFramework{system_, "ns:vm"} { - // clang-format off - static const FunctionInfo functions[] = { - {1200, &NS_VM::NeedsUpdateVulnerability, "NeedsUpdateVulnerability"}, - {1201, nullptr, "UpdateSafeSystemVersionForDebug"}, - {1202, nullptr, "GetSafeSystemVersion"}, - }; - // clang-format on - - RegisterHandlers(functions); - } - -private: - void NeedsUpdateVulnerability(HLERequestContext& ctx) { - LOG_WARNING(Service_NS, "(STUBBED) called"); - - IPC::ResponseBuilder rb{ctx, 3}; - rb.Push(ResultSuccess); - rb.Push(false); - } -}; - void LoopProcess(Core::System& system) { auto server_manager = std::make_unique<ServerManager>(system); - server_manager->RegisterNamedService("ns:am2", std::make_shared<NS>("ns:am2", system)); - server_manager->RegisterNamedService("ns:ec", std::make_shared<NS>("ns:ec", system)); - server_manager->RegisterNamedService("ns:rid", std::make_shared<NS>("ns:rid", system)); - server_manager->RegisterNamedService("ns:rt", std::make_shared<NS>("ns:rt", system)); - server_manager->RegisterNamedService("ns:web", std::make_shared<NS>("ns:web", system)); - server_manager->RegisterNamedService("ns:ro", std::make_shared<NS>("ns:ro", system)); - - server_manager->RegisterNamedService("ns:dev", std::make_shared<NS_DEV>(system)); - server_manager->RegisterNamedService("ns:su", std::make_shared<NS_SU>(system)); - server_manager->RegisterNamedService("ns:vm", std::make_shared<NS_VM>(system)); - server_manager->RegisterNamedService("pdm:qry", std::make_shared<PDM_QRY>(system)); + server_manager->RegisterNamedService( + "ns:am2", std::make_shared<IServiceGetterInterface>(system, "ns:am2")); + server_manager->RegisterNamedService( + "ns:ec", std::make_shared<IServiceGetterInterface>(system, "ns:ec")); + server_manager->RegisterNamedService( + "ns:rid", std::make_shared<IServiceGetterInterface>(system, "ns:rid")); + server_manager->RegisterNamedService( + "ns:rt", std::make_shared<IServiceGetterInterface>(system, "ns:rt")); + server_manager->RegisterNamedService( + "ns:web", std::make_shared<IServiceGetterInterface>(system, "ns:web")); + server_manager->RegisterNamedService( + "ns:ro", std::make_shared<IServiceGetterInterface>(system, "ns:ro")); + + server_manager->RegisterNamedService("ns:dev", std::make_shared<IDevelopInterface>(system)); + server_manager->RegisterNamedService("ns:su", std::make_shared<ISystemUpdateInterface>(system)); + server_manager->RegisterNamedService("ns:vm", + std::make_shared<IVulnerabilityManagerInterface>(system)); + server_manager->RegisterNamedService("pdm:qry", std::make_shared<IQueryService>(system)); server_manager->RegisterNamedService("pl:s", std::make_shared<IPlatformServiceManager>(system, "pl:s")); diff --git a/src/core/hle/service/ns/ns.h b/src/core/hle/service/ns/ns.h index 9ee306ef9..f79b4ae3d 100644 --- a/src/core/hle/service/ns/ns.h +++ b/src/core/hle/service/ns/ns.h @@ -3,141 +3,12 @@ #pragma once -#include "core/hle/service/service.h" - namespace Core { class System; } -namespace Service { - -namespace FileSystem { -class FileSystemController; -} // namespace FileSystem - -namespace NS { - -class IAccountProxyInterface final : public ServiceFramework<IAccountProxyInterface> { -public: - explicit IAccountProxyInterface(Core::System& system_); - ~IAccountProxyInterface() override; -}; - -class IApplicationManagerInterface final : public ServiceFramework<IApplicationManagerInterface> { -public: - explicit IApplicationManagerInterface(Core::System& system_); - ~IApplicationManagerInterface() override; - - Result GetApplicationDesiredLanguage(u8* out_desired_language, u32 supported_languages); - Result ConvertApplicationLanguageToLanguageCode(u64* out_language_code, - u8 application_language); - -private: - void GetApplicationControlData(HLERequestContext& ctx); - void GetApplicationDesiredLanguage(HLERequestContext& ctx); - void ConvertApplicationLanguageToLanguageCode(HLERequestContext& ctx); -}; - -class IApplicationVersionInterface final : public ServiceFramework<IApplicationVersionInterface> { -public: - explicit IApplicationVersionInterface(Core::System& system_); - ~IApplicationVersionInterface() override; -}; - -class IContentManagementInterface final : public ServiceFramework<IContentManagementInterface> { -public: - explicit IContentManagementInterface(Core::System& system_); - ~IContentManagementInterface() override; - -private: - void GetTotalSpaceSize(HLERequestContext& ctx); - void GetFreeSpaceSize(HLERequestContext& ctx); -}; - -class IDocumentInterface final : public ServiceFramework<IDocumentInterface> { -public: - explicit IDocumentInterface(Core::System& system_); - ~IDocumentInterface() override; - -private: - void ResolveApplicationContentPath(HLERequestContext& ctx); - void GetRunningApplicationProgramId(HLERequestContext& ctx); -}; - -class IDownloadTaskInterface final : public ServiceFramework<IDownloadTaskInterface> { -public: - explicit IDownloadTaskInterface(Core::System& system_); - ~IDownloadTaskInterface() override; -}; - -class IECommerceInterface final : public ServiceFramework<IECommerceInterface> { -public: - explicit IECommerceInterface(Core::System& system_); - ~IECommerceInterface() override; -}; - -class IFactoryResetInterface final : public ServiceFramework<IFactoryResetInterface> { -public: - explicit IFactoryResetInterface(Core::System& system_); - ~IFactoryResetInterface() override; -}; - -class IReadOnlyApplicationRecordInterface final - : public ServiceFramework<IReadOnlyApplicationRecordInterface> { -public: - explicit IReadOnlyApplicationRecordInterface(Core::System& system_); - ~IReadOnlyApplicationRecordInterface() override; - -private: - void HasApplicationRecord(HLERequestContext& ctx); - void IsDataCorruptedResult(HLERequestContext& ctx); -}; - -class IReadOnlyApplicationControlDataInterface final - : public ServiceFramework<IReadOnlyApplicationControlDataInterface> { -public: - explicit IReadOnlyApplicationControlDataInterface(Core::System& system_); - ~IReadOnlyApplicationControlDataInterface() override; - -private: - void GetApplicationControlData(HLERequestContext& ctx); -}; - -class NS final : public ServiceFramework<NS> { -public: - explicit NS(const char* name, Core::System& system_); - ~NS() override; - - std::shared_ptr<IApplicationManagerInterface> GetApplicationManagerInterface() const; - -private: - template <typename T, typename... Args> - void PushInterface(HLERequestContext& ctx) { - LOG_DEBUG(Service_NS, "called"); - - IPC::ResponseBuilder rb{ctx, 2, 0, 1}; - rb.Push(ResultSuccess); - rb.PushIpcInterface<T>(system); - } - - void PushIApplicationManagerInterface(HLERequestContext& ctx) { - LOG_DEBUG(Service_NS, "called"); - - IPC::ResponseBuilder rb{ctx, 2, 0, 1}; - rb.Push(ResultSuccess); - rb.PushIpcInterface<IApplicationManagerInterface>(system); - } - - template <typename T, typename... Args> - std::shared_ptr<T> GetInterface(Args&&... args) const { - static_assert(std::is_base_of_v<SessionRequestHandler, T>, - "Not a base of ServiceFrameworkBase"); - - return std::make_shared<T>(std::forward<Args>(args)...); - } -}; +namespace Service::NS { void LoopProcess(Core::System& system); -} // namespace NS -} // namespace Service +} // namespace Service::NS diff --git a/src/core/hle/service/ns/errors.h b/src/core/hle/service/ns/ns_results.h index 16d2ea6f7..16d2ea6f7 100644 --- a/src/core/hle/service/ns/errors.h +++ b/src/core/hle/service/ns/ns_results.h diff --git a/src/core/hle/service/ns/ns_types.h b/src/core/hle/service/ns/ns_types.h new file mode 100644 index 000000000..38421b0f4 --- /dev/null +++ b/src/core/hle/service/ns/ns_types.h @@ -0,0 +1,111 @@ +// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "common/common_funcs.h" +#include "common/uuid.h" +#include "core/file_sys/romfs_factory.h" + +namespace Service::NS { + +enum class ApplicationRecordType : u8 { + Installing = 2, + Installed = 3, + GameCardNotInserted = 5, + Archived = 11, + GameCard = 16, +}; + +enum class ApplicationControlSource : u8 { + CacheOnly = 0, + Storage = 1, + StorageOnly = 2, +}; + +enum class BackgroundNetworkUpdateState : u8 { + None, + InProgress, + Ready, +}; + +struct ApplicationRecord { + u64 application_id; + ApplicationRecordType type; + u8 unknown; + INSERT_PADDING_BYTES_NOINIT(0x6); + u8 unknown2; + INSERT_PADDING_BYTES_NOINIT(0x7); +}; +static_assert(sizeof(ApplicationRecord) == 0x18, "ApplicationRecord has incorrect size."); + +/// ApplicationView +struct ApplicationView { + u64 application_id; ///< ApplicationId. + u32 unk; ///< Unknown. + u32 flags; ///< Flags. + std::array<u8, 0x10> unk_x10; ///< Unknown. + u32 unk_x20; ///< Unknown. + u16 unk_x24; ///< Unknown. + std::array<u8, 0x2> unk_x26; ///< Unknown. + std::array<u8, 0x8> unk_x28; ///< Unknown. + std::array<u8, 0x10> unk_x30; ///< Unknown. + u32 unk_x40; ///< Unknown. + u8 unk_x44; ///< Unknown. + std::array<u8, 0xb> unk_x45; ///< Unknown. +}; +static_assert(sizeof(ApplicationView) == 0x50, "ApplicationView has incorrect size."); + +struct ApplicationRightsOnClient { + u64 application_id; + Common::UUID uid; + u8 flags; + u8 flags2; + INSERT_PADDING_BYTES_NOINIT(0x6); +}; +static_assert(sizeof(ApplicationRightsOnClient) == 0x20, + "ApplicationRightsOnClient has incorrect size."); + +/// NsPromotionInfo +struct PromotionInfo { + u64 start_timestamp; ///< POSIX timestamp for the promotion start. + u64 end_timestamp; ///< POSIX timestamp for the promotion end. + s64 remaining_time; ///< Remaining time until the promotion ends, in nanoseconds + ///< ({end_timestamp - current_time} converted to nanoseconds). + INSERT_PADDING_BYTES_NOINIT(0x4); + u8 flags; ///< Flags. Bit0: whether the PromotionInfo is valid (including bit1). Bit1 clear: + ///< remaining_time is set. + INSERT_PADDING_BYTES_NOINIT(0x3); +}; +static_assert(sizeof(PromotionInfo) == 0x20, "PromotionInfo has incorrect size."); + +/// NsApplicationViewWithPromotionInfo +struct ApplicationViewWithPromotionInfo { + ApplicationView view; ///< \ref NsApplicationView + PromotionInfo promotion; ///< \ref NsPromotionInfo +}; +static_assert(sizeof(ApplicationViewWithPromotionInfo) == 0x70, + "ApplicationViewWithPromotionInfo has incorrect size."); + +struct ApplicationOccupiedSizeEntity { + FileSys::StorageId storage_id; + u64 app_size; + u64 patch_size; + u64 aoc_size; +}; +static_assert(sizeof(ApplicationOccupiedSizeEntity) == 0x20, + "ApplicationOccupiedSizeEntity has incorrect size."); + +struct ApplicationOccupiedSize { + std::array<ApplicationOccupiedSizeEntity, 4> entities; +}; +static_assert(sizeof(ApplicationOccupiedSize) == 0x80, + "ApplicationOccupiedSize has incorrect size."); + +struct ContentPath { + u8 file_system_proxy_type; + u64 program_id; +}; +static_assert(sizeof(ContentPath) == 0x10, "ContentPath has incorrect size."); + +} // namespace Service::NS diff --git a/src/core/hle/service/ns/pdm_qry.cpp b/src/core/hle/service/ns/pdm_qry.cpp deleted file mode 100644 index ce0ee30e0..000000000 --- a/src/core/hle/service/ns/pdm_qry.cpp +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#include <memory> - -#include "common/logging/log.h" -#include "common/uuid.h" -#include "core/hle/service/ipc_helpers.h" -#include "core/hle/service/ns/pdm_qry.h" -#include "core/hle/service/service.h" - -namespace Service::NS { - -PDM_QRY::PDM_QRY(Core::System& system_) : ServiceFramework{system_, "pdm:qry"} { - // clang-format off - static const FunctionInfo functions[] = { - {0, nullptr, "QueryAppletEvent"}, - {1, nullptr, "QueryPlayStatistics"}, - {2, nullptr, "QueryPlayStatisticsByUserAccountId"}, - {3, nullptr, "QueryPlayStatisticsByNetworkServiceAccountId"}, - {4, nullptr, "QueryPlayStatisticsByApplicationId"}, - {5, &PDM_QRY::QueryPlayStatisticsByApplicationIdAndUserAccountId, "QueryPlayStatisticsByApplicationIdAndUserAccountId"}, - {6, nullptr, "QueryPlayStatisticsByApplicationIdAndNetworkServiceAccountId"}, - {7, nullptr, "QueryLastPlayTimeV0"}, - {8, nullptr, "QueryPlayEvent"}, - {9, nullptr, "GetAvailablePlayEventRange"}, - {10, nullptr, "QueryAccountEvent"}, - {11, nullptr, "QueryAccountPlayEvent"}, - {12, nullptr, "GetAvailableAccountPlayEventRange"}, - {13, nullptr, "QueryApplicationPlayStatisticsForSystemV0"}, - {14, nullptr, "QueryRecentlyPlayedApplication"}, - {15, nullptr, "GetRecentlyPlayedApplicationUpdateEvent"}, - {16, nullptr, "QueryApplicationPlayStatisticsByUserAccountIdForSystemV0"}, - {17, nullptr, "QueryLastPlayTime"}, - {18, nullptr, "QueryApplicationPlayStatisticsForSystem"}, - {19, nullptr, "QueryApplicationPlayStatisticsByUserAccountIdForSystem"}, - }; - // clang-format on - - RegisterHandlers(functions); -} - -PDM_QRY::~PDM_QRY() = default; - -void PDM_QRY::QueryPlayStatisticsByApplicationIdAndUserAccountId(HLERequestContext& ctx) { - IPC::RequestParser rp{ctx}; - const auto unknown = rp.Pop<bool>(); - rp.Pop<u8>(); // Padding - const auto application_id = rp.Pop<u64>(); - const auto user_account_uid = rp.PopRaw<Common::UUID>(); - - // TODO(German77): Read statistics of the game - PlayStatistics statistics{ - .application_id = application_id, - .total_launches = 1, - }; - - LOG_WARNING(Service_NS, - "(STUBBED) called. unknown={}. application_id=0x{:016X}, user_account_uid=0x{}", - unknown, application_id, user_account_uid.RawString()); - - IPC::ResponseBuilder rb{ctx, 12}; - rb.Push(ResultSuccess); - rb.PushRaw(statistics); -} - -} // namespace Service::NS diff --git a/src/core/hle/service/ns/iplatform_service_manager.cpp b/src/core/hle/service/ns/platform_service_manager.cpp index 46268be95..23cf05005 100644 --- a/src/core/hle/service/ns/iplatform_service_manager.cpp +++ b/src/core/hle/service/ns/platform_service_manager.cpp @@ -18,9 +18,9 @@ #include "core/hle/kernel/k_shared_memory.h" #include "core/hle/kernel/kernel.h" #include "core/hle/kernel/physical_memory.h" +#include "core/hle/service/cmif_serialization.h" #include "core/hle/service/filesystem/filesystem.h" -#include "core/hle/service/ipc_helpers.h" -#include "core/hle/service/ns/iplatform_service_manager.h" +#include "core/hle/service/ns/platform_service_manager.h" namespace Service::NS { @@ -37,11 +37,6 @@ constexpr u32 EXPECTED_MAGIC{0x36f81a1e}; // What we expect the encrypted bfttf constexpr u64 SHARED_FONT_MEM_SIZE{0x1100000}; constexpr FontRegion EMPTY_REGION{0, 0}; -enum class LoadState : u32 { - Loading = 0, - Done = 1, -}; - static void DecryptSharedFont(const std::vector<u32>& input, Kernel::PhysicalMemory& output, std::size_t& offset) { ASSERT_MSG(offset + (input.size() * sizeof(u32)) < SHARED_FONT_MEM_SIZE, @@ -138,13 +133,13 @@ IPlatformServiceManager::IPlatformServiceManager(Core::System& system_, const ch : ServiceFramework{system_, service_name_}, impl{std::make_unique<Impl>()} { // clang-format off static const FunctionInfo functions[] = { - {0, &IPlatformServiceManager::RequestLoad, "RequestLoad"}, - {1, &IPlatformServiceManager::GetLoadState, "GetLoadState"}, - {2, &IPlatformServiceManager::GetSize, "GetSize"}, - {3, &IPlatformServiceManager::GetSharedMemoryAddressOffset, "GetSharedMemoryAddressOffset"}, - {4, &IPlatformServiceManager::GetSharedMemoryNativeHandle, "GetSharedMemoryNativeHandle"}, - {5, &IPlatformServiceManager::GetSharedFontInOrderOfPriority, "GetSharedFontInOrderOfPriority"}, - {6, &IPlatformServiceManager::GetSharedFontInOrderOfPriority, "GetSharedFontInOrderOfPriorityForSystem"}, + {0, D<&IPlatformServiceManager::RequestLoad>, "RequestLoad"}, + {1, D<&IPlatformServiceManager::GetLoadState>, "GetLoadState"}, + {2, D<&IPlatformServiceManager::GetSize>, "GetSize"}, + {3, D<&IPlatformServiceManager::GetSharedMemoryAddressOffset>, "GetSharedMemoryAddressOffset"}, + {4, D<&IPlatformServiceManager::GetSharedMemoryNativeHandle>, "GetSharedMemoryNativeHandle"}, + {5, D<&IPlatformServiceManager::GetSharedFontInOrderOfPriority>, "GetSharedFontInOrderOfPriority"}, + {6, D<&IPlatformServiceManager::GetSharedFontInOrderOfPriority>, "GetSharedFontInOrderOfPriorityForSystem"}, {100, nullptr, "RequestApplicationFunctionAuthorization"}, {101, nullptr, "RequestApplicationFunctionAuthorizationByProcessId"}, {102, nullptr, "RequestApplicationFunctionAuthorizationByApplicationId"}, @@ -208,47 +203,33 @@ IPlatformServiceManager::IPlatformServiceManager(Core::System& system_, const ch IPlatformServiceManager::~IPlatformServiceManager() = default; -void IPlatformServiceManager::RequestLoad(HLERequestContext& ctx) { - IPC::RequestParser rp{ctx}; - const u32 shared_font_type{rp.Pop<u32>()}; +Result IPlatformServiceManager::RequestLoad(SharedFontType type) { // Games don't call this so all fonts should be loaded - LOG_DEBUG(Service_NS, "called, shared_font_type={}", shared_font_type); - - IPC::ResponseBuilder rb{ctx, 2}; - rb.Push(ResultSuccess); + LOG_DEBUG(Service_NS, "called, shared_font_type={}", type); + R_SUCCEED(); } -void IPlatformServiceManager::GetLoadState(HLERequestContext& ctx) { - IPC::RequestParser rp{ctx}; - const u32 font_id{rp.Pop<u32>()}; - LOG_DEBUG(Service_NS, "called, font_id={}", font_id); - - IPC::ResponseBuilder rb{ctx, 3}; - rb.Push(ResultSuccess); - rb.Push<u32>(static_cast<u32>(LoadState::Done)); +Result IPlatformServiceManager::GetLoadState(Out<LoadState> out_load_state, SharedFontType type) { + LOG_DEBUG(Service_NS, "called, shared_font_type={}", type); + *out_load_state = LoadState::Loaded; + R_SUCCEED(); } -void IPlatformServiceManager::GetSize(HLERequestContext& ctx) { - IPC::RequestParser rp{ctx}; - const u32 font_id{rp.Pop<u32>()}; - LOG_DEBUG(Service_NS, "called, font_id={}", font_id); - - IPC::ResponseBuilder rb{ctx, 3}; - rb.Push(ResultSuccess); - rb.Push<u32>(impl->GetSharedFontRegion(font_id).size); +Result IPlatformServiceManager::GetSize(Out<u32> out_size, SharedFontType type) { + LOG_DEBUG(Service_NS, "called, shared_font_type={}", type); + *out_size = impl->GetSharedFontRegion(static_cast<size_t>(type)).size; + R_SUCCEED(); } -void IPlatformServiceManager::GetSharedMemoryAddressOffset(HLERequestContext& ctx) { - IPC::RequestParser rp{ctx}; - const u32 font_id{rp.Pop<u32>()}; - LOG_DEBUG(Service_NS, "called, font_id={}", font_id); - - IPC::ResponseBuilder rb{ctx, 3}; - rb.Push(ResultSuccess); - rb.Push<u32>(impl->GetSharedFontRegion(font_id).offset); +Result IPlatformServiceManager::GetSharedMemoryAddressOffset(Out<u32> out_shared_memory_offset, + SharedFontType type) { + LOG_DEBUG(Service_NS, "called, shared_font_type={}", type); + *out_shared_memory_offset = impl->GetSharedFontRegion(static_cast<size_t>(type)).offset; + R_SUCCEED(); } -void IPlatformServiceManager::GetSharedMemoryNativeHandle(HLERequestContext& ctx) { +Result IPlatformServiceManager::GetSharedMemoryNativeHandle( + OutCopyHandle<Kernel::KSharedMemory> out_shared_memory_native_handle) { // Map backing memory for the font data LOG_DEBUG(Service_NS, "called"); @@ -256,50 +237,37 @@ void IPlatformServiceManager::GetSharedMemoryNativeHandle(HLERequestContext& ctx std::memcpy(kernel.GetFontSharedMem().GetPointer(), impl->shared_font->data(), impl->shared_font->size()); - IPC::ResponseBuilder rb{ctx, 2, 1}; - rb.Push(ResultSuccess); - rb.PushCopyObjects(&kernel.GetFontSharedMem()); + // FIXME: this shouldn't belong to the kernel + *out_shared_memory_native_handle = &kernel.GetFontSharedMem(); + R_SUCCEED(); } -void IPlatformServiceManager::GetSharedFontInOrderOfPriority(HLERequestContext& ctx) { +Result IPlatformServiceManager::GetSharedFontInOrderOfPriority( + OutArray<u32, BufferAttr_HipcMapAlias> out_font_codes, + OutArray<u32, BufferAttr_HipcMapAlias> out_font_offsets, + OutArray<u32, BufferAttr_HipcMapAlias> out_font_sizes, Out<bool> out_fonts_are_loaded, + Out<u32> out_font_count, Set::LanguageCode language_code) { + LOG_DEBUG(Service_NS, "called, language_code={:#x}", language_code); + // The maximum number of elements that can be returned is 6. Regardless of the available fonts // or buffer size. - constexpr std::size_t MaxElementCount = 6; - IPC::RequestParser rp{ctx}; - const u64 language_code{rp.Pop<u64>()}; // TODO(ogniK): Find out what this is used for - const std::size_t font_codes_count = - std::min(MaxElementCount, ctx.GetWriteBufferNumElements<u32>(0)); - const std::size_t font_offsets_count = - std::min(MaxElementCount, ctx.GetWriteBufferNumElements<u32>(1)); - const std::size_t font_sizes_count = - std::min(MaxElementCount, ctx.GetWriteBufferNumElements<u32>(2)); - LOG_DEBUG(Service_NS, "called, language_code={:X}", language_code); - - IPC::ResponseBuilder rb{ctx, 4}; - std::vector<u32> font_codes; - std::vector<u32> font_offsets; - std::vector<u32> font_sizes; + constexpr size_t MaxElementCount = 6; // TODO(ogniK): Have actual priority order - for (std::size_t i = 0; i < impl->shared_font_regions.size(); i++) { - font_codes.push_back(static_cast<u32>(i)); - auto region = impl->GetSharedFontRegion(i); - font_offsets.push_back(region.offset); - font_sizes.push_back(region.size); - } + const auto max_size = std::min({MaxElementCount, out_font_codes.size(), out_font_offsets.size(), + out_font_sizes.size(), impl->shared_font_regions.size()}); - // Resize buffers if game requests smaller size output - font_codes.resize(std::min(font_codes.size(), font_codes_count)); - font_offsets.resize(std::min(font_offsets.size(), font_offsets_count)); - font_sizes.resize(std::min(font_sizes.size(), font_sizes_count)); + for (size_t i = 0; i < max_size; i++) { + auto region = impl->GetSharedFontRegion(i); - ctx.WriteBuffer(font_codes, 0); - ctx.WriteBuffer(font_offsets, 1); - ctx.WriteBuffer(font_sizes, 2); + out_font_codes[i] = static_cast<u32>(i); + out_font_offsets[i] = region.offset; + out_font_sizes[i] = region.size; + } - rb.Push(ResultSuccess); - rb.Push<u8>(static_cast<u8>(LoadState::Done)); // Fonts Loaded - rb.Push<u32>(static_cast<u32>(font_codes.size())); + *out_fonts_are_loaded = true; + *out_font_count = static_cast<u32>(max_size); + R_SUCCEED(); } } // namespace Service::NS diff --git a/src/core/hle/service/ns/iplatform_service_manager.h b/src/core/hle/service/ns/platform_service_manager.h index 03071e02b..b82c385a6 100644 --- a/src/core/hle/service/ns/iplatform_service_manager.h +++ b/src/core/hle/service/ns/platform_service_manager.h @@ -5,7 +5,9 @@ #include <memory> #include <vector> +#include "core/hle/service/cmif_types.h" #include "core/hle/service/service.h" +#include "core/hle/service/set/settings_types.h" namespace Service { @@ -23,6 +25,20 @@ enum class FontArchives : u64 { ChineseSimple = 0x0100000000000814, }; +enum class SharedFontType : u32 { + JapanUSEuropeStandard = 0, + ChineseSimplified = 1, + ExtendedChineseSimplified = 2, + ChineseTraditional = 3, + KoreanHangul = 4, + NintendoExtended = 5, +}; + +enum class LoadState : u32 { + Loading = 0, + Loaded = 1, +}; + constexpr std::array<std::pair<FontArchives, const char*>, 7> SHARED_FONTS{ std::make_pair(FontArchives::Standard, "nintendo_udsg-r_std_003.bfttf"), std::make_pair(FontArchives::ChineseSimple, "nintendo_udsg-r_org_zh-cn_003.bfttf"), @@ -42,12 +58,17 @@ public: ~IPlatformServiceManager() override; private: - void RequestLoad(HLERequestContext& ctx); - void GetLoadState(HLERequestContext& ctx); - void GetSize(HLERequestContext& ctx); - void GetSharedMemoryAddressOffset(HLERequestContext& ctx); - void GetSharedMemoryNativeHandle(HLERequestContext& ctx); - void GetSharedFontInOrderOfPriority(HLERequestContext& ctx); + Result RequestLoad(SharedFontType type); + Result GetLoadState(Out<LoadState> out_load_state, SharedFontType type); + Result GetSize(Out<u32> out_size, SharedFontType type); + Result GetSharedMemoryAddressOffset(Out<u32> out_shared_memory_offset, SharedFontType type); + Result GetSharedMemoryNativeHandle( + OutCopyHandle<Kernel::KSharedMemory> out_shared_memory_native_handle); + Result GetSharedFontInOrderOfPriority(OutArray<u32, BufferAttr_HipcMapAlias> out_font_codes, + OutArray<u32, BufferAttr_HipcMapAlias> out_font_offsets, + OutArray<u32, BufferAttr_HipcMapAlias> out_font_sizes, + Out<bool> out_fonts_are_loaded, Out<u32> out_font_count, + Set::LanguageCode language_code); struct Impl; std::unique_ptr<Impl> impl; diff --git a/src/core/hle/service/ns/query_service.cpp b/src/core/hle/service/ns/query_service.cpp new file mode 100644 index 000000000..946b7fa23 --- /dev/null +++ b/src/core/hle/service/ns/query_service.cpp @@ -0,0 +1,57 @@ +// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "common/logging/log.h" +#include "common/uuid.h" +#include "core/hle/service/cmif_serialization.h" +#include "core/hle/service/ns/query_service.h" +#include "core/hle/service/service.h" + +namespace Service::NS { + +IQueryService::IQueryService(Core::System& system_) : ServiceFramework{system_, "pdm:qry"} { + // clang-format off + static const FunctionInfo functions[] = { + {0, nullptr, "QueryAppletEvent"}, + {1, nullptr, "QueryPlayStatistics"}, + {2, nullptr, "QueryPlayStatisticsByUserAccountId"}, + {3, nullptr, "QueryPlayStatisticsByNetworkServiceAccountId"}, + {4, nullptr, "QueryPlayStatisticsByApplicationId"}, + {5, D<&IQueryService::QueryPlayStatisticsByApplicationIdAndUserAccountId>, "QueryPlayStatisticsByApplicationIdAndUserAccountId"}, + {6, nullptr, "QueryPlayStatisticsByApplicationIdAndNetworkServiceAccountId"}, + {7, nullptr, "QueryLastPlayTimeV0"}, + {8, nullptr, "QueryPlayEvent"}, + {9, nullptr, "GetAvailablePlayEventRange"}, + {10, nullptr, "QueryAccountEvent"}, + {11, nullptr, "QueryAccountPlayEvent"}, + {12, nullptr, "GetAvailableAccountPlayEventRange"}, + {13, nullptr, "QueryApplicationPlayStatisticsForSystemV0"}, + {14, nullptr, "QueryRecentlyPlayedApplication"}, + {15, nullptr, "GetRecentlyPlayedApplicationUpdateEvent"}, + {16, nullptr, "QueryApplicationPlayStatisticsByUserAccountIdForSystemV0"}, + {17, nullptr, "QueryLastPlayTime"}, + {18, nullptr, "QueryApplicationPlayStatisticsForSystem"}, + {19, nullptr, "QueryApplicationPlayStatisticsByUserAccountIdForSystem"}, + }; + // clang-format on + + RegisterHandlers(functions); +} + +IQueryService::~IQueryService() = default; + +Result IQueryService::QueryPlayStatisticsByApplicationIdAndUserAccountId( + Out<PlayStatistics> out_play_statistics, bool unknown, Common::UUID account_id, + u64 application_id) { + // TODO(German77): Read statistics of the game + *out_play_statistics = { + .application_id = application_id, + .total_launches = 1, + }; + + LOG_WARNING(Service_NS, "(STUBBED) called. unknown={}. application_id={:016X}, account_id={}", + unknown, application_id, account_id.FormattedString()); + R_SUCCEED(); +} + +} // namespace Service::NS diff --git a/src/core/hle/service/ns/pdm_qry.h b/src/core/hle/service/ns/query_service.h index c98e01660..6cdbfa277 100644 --- a/src/core/hle/service/ns/pdm_qry.h +++ b/src/core/hle/service/ns/query_service.h @@ -3,6 +3,8 @@ #pragma once +#include "common/uuid.h" +#include "core/hle/service/cmif_types.h" #include "core/hle/service/service.h" namespace Service::NS { @@ -20,13 +22,15 @@ struct PlayStatistics { }; static_assert(sizeof(PlayStatistics) == 0x28, "PlayStatistics is an invalid size"); -class PDM_QRY final : public ServiceFramework<PDM_QRY> { +class IQueryService final : public ServiceFramework<IQueryService> { public: - explicit PDM_QRY(Core::System& system_); - ~PDM_QRY() override; + explicit IQueryService(Core::System& system_); + ~IQueryService() override; private: - void QueryPlayStatisticsByApplicationIdAndUserAccountId(HLERequestContext& ctx); + Result QueryPlayStatisticsByApplicationIdAndUserAccountId( + Out<PlayStatistics> out_play_statistics, bool unknown, Common::UUID account_id, + u64 application_id); }; } // namespace Service::NS diff --git a/src/core/hle/service/ns/read_only_application_control_data_interface.cpp b/src/core/hle/service/ns/read_only_application_control_data_interface.cpp new file mode 100644 index 000000000..9b2ca94a4 --- /dev/null +++ b/src/core/hle/service/ns/read_only_application_control_data_interface.cpp @@ -0,0 +1,122 @@ +// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "common/settings.h" +#include "core/file_sys/control_metadata.h" +#include "core/file_sys/patch_manager.h" +#include "core/file_sys/vfs/vfs.h" +#include "core/hle/service/cmif_serialization.h" +#include "core/hle/service/ns/language.h" +#include "core/hle/service/ns/ns_results.h" +#include "core/hle/service/ns/read_only_application_control_data_interface.h" +#include "core/hle/service/set/settings_server.h" + +namespace Service::NS { + +IReadOnlyApplicationControlDataInterface::IReadOnlyApplicationControlDataInterface( + Core::System& system_) + : ServiceFramework{system_, "IReadOnlyApplicationControlDataInterface"} { + // clang-format off + static const FunctionInfo functions[] = { + {0, D<&IReadOnlyApplicationControlDataInterface::GetApplicationControlData>, "GetApplicationControlData"}, + {1, D<&IReadOnlyApplicationControlDataInterface::GetApplicationDesiredLanguage>, "GetApplicationDesiredLanguage"}, + {2, D<&IReadOnlyApplicationControlDataInterface::ConvertApplicationLanguageToLanguageCode>, "ConvertApplicationLanguageToLanguageCode"}, + {3, nullptr, "ConvertLanguageCodeToApplicationLanguage"}, + {4, nullptr, "SelectApplicationDesiredLanguage"}, + }; + // clang-format on + + RegisterHandlers(functions); +} + +IReadOnlyApplicationControlDataInterface::~IReadOnlyApplicationControlDataInterface() = default; + +Result IReadOnlyApplicationControlDataInterface::GetApplicationControlData( + OutBuffer<BufferAttr_HipcMapAlias> out_buffer, Out<u32> out_actual_size, + ApplicationControlSource application_control_source, u64 application_id) { + LOG_INFO(Service_NS, "called with control_source={}, application_id={:016X}", + application_control_source, application_id); + + const FileSys::PatchManager pm{application_id, system.GetFileSystemController(), + system.GetContentProvider()}; + const auto control = pm.GetControlMetadata(); + const auto size = out_buffer.size(); + + const auto icon_size = control.second ? control.second->GetSize() : 0; + const auto total_size = sizeof(FileSys::RawNACP) + icon_size; + + if (size < total_size) { + LOG_ERROR(Service_NS, "output buffer is too small! (actual={:016X}, expected_min=0x4000)", + size); + R_THROW(ResultUnknown); + } + + if (control.first != nullptr) { + const auto bytes = control.first->GetRawBytes(); + std::memcpy(out_buffer.data(), bytes.data(), bytes.size()); + } else { + LOG_WARNING(Service_NS, "missing NACP data for application_id={:016X}, defaulting to zero", + application_id); + std::memset(out_buffer.data(), 0, sizeof(FileSys::RawNACP)); + } + + if (control.second != nullptr) { + control.second->Read(out_buffer.data() + sizeof(FileSys::RawNACP), icon_size); + } else { + LOG_WARNING(Service_NS, "missing icon data for application_id={:016X}", application_id); + } + + *out_actual_size = static_cast<u32>(total_size); + R_SUCCEED(); +} + +Result IReadOnlyApplicationControlDataInterface::GetApplicationDesiredLanguage( + Out<ApplicationLanguage> out_desired_language, u32 supported_languages) { + LOG_INFO(Service_NS, "called with supported_languages={:08X}", supported_languages); + + // Get language code from settings + const auto language_code = + Set::GetLanguageCodeFromIndex(static_cast<s32>(Settings::values.language_index.GetValue())); + + // Convert to application language, get priority list + const auto application_language = ConvertToApplicationLanguage(language_code); + if (application_language == std::nullopt) { + LOG_ERROR(Service_NS, "Could not convert application language! language_code={}", + language_code); + R_THROW(Service::NS::ResultApplicationLanguageNotFound); + } + const auto priority_list = GetApplicationLanguagePriorityList(*application_language); + if (!priority_list) { + LOG_ERROR(Service_NS, + "Could not find application language priorities! application_language={}", + *application_language); + R_THROW(Service::NS::ResultApplicationLanguageNotFound); + } + + // Try to find a valid language. + for (const auto lang : *priority_list) { + const auto supported_flag = GetSupportedLanguageFlag(lang); + if (supported_languages == 0 || (supported_languages & supported_flag) == supported_flag) { + *out_desired_language = lang; + R_SUCCEED(); + } + } + + LOG_ERROR(Service_NS, "Could not find a valid language! supported_languages={:08X}", + supported_languages); + R_THROW(Service::NS::ResultApplicationLanguageNotFound); +} + +Result IReadOnlyApplicationControlDataInterface::ConvertApplicationLanguageToLanguageCode( + Out<u64> out_language_code, ApplicationLanguage application_language) { + const auto language_code = ConvertToLanguageCode(application_language); + if (language_code == std::nullopt) { + LOG_ERROR(Service_NS, "Language not found! application_language={}", application_language); + R_THROW(Service::NS::ResultApplicationLanguageNotFound); + } + + *out_language_code = static_cast<u64>(*language_code); + R_SUCCEED(); +} + +} // namespace Service::NS diff --git a/src/core/hle/service/ns/read_only_application_control_data_interface.h b/src/core/hle/service/ns/read_only_application_control_data_interface.h new file mode 100644 index 000000000..ac099435a --- /dev/null +++ b/src/core/hle/service/ns/read_only_application_control_data_interface.h @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "core/hle/service/cmif_types.h" +#include "core/hle/service/ns/language.h" +#include "core/hle/service/ns/ns_types.h" +#include "core/hle/service/service.h" + +namespace Service::NS { + +class IReadOnlyApplicationControlDataInterface final + : public ServiceFramework<IReadOnlyApplicationControlDataInterface> { +public: + explicit IReadOnlyApplicationControlDataInterface(Core::System& system_); + ~IReadOnlyApplicationControlDataInterface() override; + +public: + Result GetApplicationControlData(OutBuffer<BufferAttr_HipcMapAlias> out_buffer, + Out<u32> out_actual_size, + ApplicationControlSource application_control_source, + u64 application_id); + Result GetApplicationDesiredLanguage(Out<ApplicationLanguage> out_desired_language, + u32 supported_languages); + Result ConvertApplicationLanguageToLanguageCode(Out<u64> out_language_code, + ApplicationLanguage application_language); +}; + +} // namespace Service::NS diff --git a/src/core/hle/service/ns/read_only_application_record_interface.cpp b/src/core/hle/service/ns/read_only_application_record_interface.cpp new file mode 100644 index 000000000..816a1e1dc --- /dev/null +++ b/src/core/hle/service/ns/read_only_application_record_interface.cpp @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "core/hle/service/cmif_serialization.h" +#include "core/hle/service/ns/read_only_application_record_interface.h" + +namespace Service::NS { + +IReadOnlyApplicationRecordInterface::IReadOnlyApplicationRecordInterface(Core::System& system_) + : ServiceFramework{system_, "IReadOnlyApplicationRecordInterface"} { + static const FunctionInfo functions[] = { + {0, D<&IReadOnlyApplicationRecordInterface::HasApplicationRecord>, "HasApplicationRecord"}, + {1, nullptr, "NotifyApplicationFailure"}, + {2, D<&IReadOnlyApplicationRecordInterface::IsDataCorruptedResult>, + "IsDataCorruptedResult"}, + }; + // clang-format on + + RegisterHandlers(functions); +} + +IReadOnlyApplicationRecordInterface::~IReadOnlyApplicationRecordInterface() = default; + +Result IReadOnlyApplicationRecordInterface::HasApplicationRecord( + Out<bool> out_has_application_record, u64 program_id) { + LOG_WARNING(Service_NS, "(STUBBED) called, program_id={:016X}", program_id); + *out_has_application_record = true; + R_SUCCEED(); +} + +Result IReadOnlyApplicationRecordInterface::IsDataCorruptedResult( + Out<bool> out_is_data_corrupted_result, Result result) { + LOG_WARNING(Service_NS, "(STUBBED) called, result={:#x}", result.GetInnerValue()); + *out_is_data_corrupted_result = false; + R_SUCCEED(); +} + +} // namespace Service::NS diff --git a/src/core/hle/service/ns/read_only_application_record_interface.h b/src/core/hle/service/ns/read_only_application_record_interface.h new file mode 100644 index 000000000..d06e8f5e6 --- /dev/null +++ b/src/core/hle/service/ns/read_only_application_record_interface.h @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "core/hle/service/cmif_types.h" +#include "core/hle/service/service.h" + +namespace Service::NS { + +class IReadOnlyApplicationRecordInterface final + : public ServiceFramework<IReadOnlyApplicationRecordInterface> { +public: + explicit IReadOnlyApplicationRecordInterface(Core::System& system_); + ~IReadOnlyApplicationRecordInterface() override; + +private: + Result HasApplicationRecord(Out<bool> out_has_application_record, u64 program_id); + Result IsDataCorruptedResult(Out<bool> out_is_data_corrupted_result, Result result); +}; + +} // namespace Service::NS diff --git a/src/core/hle/service/ns/service_getter_interface.cpp b/src/core/hle/service/ns/service_getter_interface.cpp new file mode 100644 index 000000000..1a3dd7166 --- /dev/null +++ b/src/core/hle/service/ns/service_getter_interface.cpp @@ -0,0 +1,120 @@ +// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "core/hle/service/cmif_serialization.h" +#include "core/hle/service/ns/account_proxy_interface.h" +#include "core/hle/service/ns/application_manager_interface.h" +#include "core/hle/service/ns/application_version_interface.h" +#include "core/hle/service/ns/content_management_interface.h" +#include "core/hle/service/ns/document_interface.h" +#include "core/hle/service/ns/download_task_interface.h" +#include "core/hle/service/ns/dynamic_rights_interface.h" +#include "core/hle/service/ns/ecommerce_interface.h" +#include "core/hle/service/ns/factory_reset_interface.h" +#include "core/hle/service/ns/read_only_application_control_data_interface.h" +#include "core/hle/service/ns/read_only_application_record_interface.h" +#include "core/hle/service/ns/service_getter_interface.h" + +namespace Service::NS { + +IServiceGetterInterface::IServiceGetterInterface(Core::System& system_, const char* name) + : ServiceFramework{system_, name} { + // clang-format off + static const FunctionInfo functions[] = { + {7988, D<&IServiceGetterInterface::GetDynamicRightsInterface>, "GetDynamicRightsInterface"}, + {7989, D<&IServiceGetterInterface::GetReadOnlyApplicationControlDataInterface>, "GetReadOnlyApplicationControlDataInterface"}, + {7991, D<&IServiceGetterInterface::GetReadOnlyApplicationRecordInterface>, "GetReadOnlyApplicationRecordInterface"}, + {7992, D<&IServiceGetterInterface::GetECommerceInterface>, "GetECommerceInterface"}, + {7993, D<&IServiceGetterInterface::GetApplicationVersionInterface>, "GetApplicationVersionInterface"}, + {7994, D<&IServiceGetterInterface::GetFactoryResetInterface>, "GetFactoryResetInterface"}, + {7995, D<&IServiceGetterInterface::GetAccountProxyInterface>, "GetAccountProxyInterface"}, + {7996, D<&IServiceGetterInterface::GetApplicationManagerInterface>, "GetApplicationManagerInterface"}, + {7997, D<&IServiceGetterInterface::GetDownloadTaskInterface>, "GetDownloadTaskInterface"}, + {7998, D<&IServiceGetterInterface::GetContentManagementInterface>, "GetContentManagementInterface"}, + {7999, D<&IServiceGetterInterface::GetDocumentInterface>, "GetDocumentInterface"}, + }; + // clang-format on + + RegisterHandlers(functions); +} + +IServiceGetterInterface::~IServiceGetterInterface() = default; + +Result IServiceGetterInterface::GetDynamicRightsInterface( + Out<SharedPointer<IDynamicRightsInterface>> out_interface) { + LOG_DEBUG(Service_NS, "called"); + *out_interface = std::make_shared<IDynamicRightsInterface>(system); + R_SUCCEED(); +} + +Result IServiceGetterInterface::GetReadOnlyApplicationControlDataInterface( + Out<SharedPointer<IReadOnlyApplicationControlDataInterface>> out_interface) { + LOG_DEBUG(Service_NS, "called"); + *out_interface = std::make_shared<IReadOnlyApplicationControlDataInterface>(system); + R_SUCCEED(); +} + +Result IServiceGetterInterface::GetReadOnlyApplicationRecordInterface( + Out<SharedPointer<IReadOnlyApplicationRecordInterface>> out_interface) { + LOG_DEBUG(Service_NS, "called"); + *out_interface = std::make_shared<IReadOnlyApplicationRecordInterface>(system); + R_SUCCEED(); +} + +Result IServiceGetterInterface::GetECommerceInterface( + Out<SharedPointer<IECommerceInterface>> out_interface) { + LOG_DEBUG(Service_NS, "called"); + *out_interface = std::make_shared<IECommerceInterface>(system); + R_SUCCEED(); +} + +Result IServiceGetterInterface::GetApplicationVersionInterface( + Out<SharedPointer<IApplicationVersionInterface>> out_interface) { + LOG_DEBUG(Service_NS, "called"); + *out_interface = std::make_shared<IApplicationVersionInterface>(system); + R_SUCCEED(); +} + +Result IServiceGetterInterface::GetFactoryResetInterface( + Out<SharedPointer<IFactoryResetInterface>> out_interface) { + LOG_DEBUG(Service_NS, "called"); + *out_interface = std::make_shared<IFactoryResetInterface>(system); + R_SUCCEED(); +} + +Result IServiceGetterInterface::GetAccountProxyInterface( + Out<SharedPointer<IAccountProxyInterface>> out_interface) { + LOG_DEBUG(Service_NS, "called"); + *out_interface = std::make_shared<IAccountProxyInterface>(system); + R_SUCCEED(); +} + +Result IServiceGetterInterface::GetApplicationManagerInterface( + Out<SharedPointer<IApplicationManagerInterface>> out_interface) { + LOG_DEBUG(Service_NS, "called"); + *out_interface = std::make_shared<IApplicationManagerInterface>(system); + R_SUCCEED(); +} + +Result IServiceGetterInterface::GetDownloadTaskInterface( + Out<SharedPointer<IDownloadTaskInterface>> out_interface) { + LOG_DEBUG(Service_NS, "called"); + *out_interface = std::make_shared<IDownloadTaskInterface>(system); + R_SUCCEED(); +} + +Result IServiceGetterInterface::GetContentManagementInterface( + Out<SharedPointer<IContentManagementInterface>> out_interface) { + LOG_DEBUG(Service_NS, "called"); + *out_interface = std::make_shared<IContentManagementInterface>(system); + R_SUCCEED(); +} + +Result IServiceGetterInterface::GetDocumentInterface( + Out<SharedPointer<IDocumentInterface>> out_interface) { + LOG_DEBUG(Service_NS, "called"); + *out_interface = std::make_shared<IDocumentInterface>(system); + R_SUCCEED(); +} + +} // namespace Service::NS diff --git a/src/core/hle/service/ns/service_getter_interface.h b/src/core/hle/service/ns/service_getter_interface.h new file mode 100644 index 000000000..bbc18d444 --- /dev/null +++ b/src/core/hle/service/ns/service_getter_interface.h @@ -0,0 +1,47 @@ +// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "core/hle/service/cmif_types.h" +#include "core/hle/service/service.h" + +namespace Service::NS { + +class IDynamicRightsInterface; +class IReadOnlyApplicationControlDataInterface; +class IReadOnlyApplicationRecordInterface; +class IECommerceInterface; +class IApplicationVersionInterface; +class IFactoryResetInterface; +class IAccountProxyInterface; +class IApplicationManagerInterface; +class IDownloadTaskInterface; +class IContentManagementInterface; +class IDocumentInterface; + +class IServiceGetterInterface : public ServiceFramework<IServiceGetterInterface> { +public: + explicit IServiceGetterInterface(Core::System& system_, const char* name); + ~IServiceGetterInterface() override; + +public: + Result GetDynamicRightsInterface(Out<SharedPointer<IDynamicRightsInterface>> out_interface); + Result GetReadOnlyApplicationControlDataInterface( + Out<SharedPointer<IReadOnlyApplicationControlDataInterface>> out_interface); + Result GetReadOnlyApplicationRecordInterface( + Out<SharedPointer<IReadOnlyApplicationRecordInterface>> out_interface); + Result GetECommerceInterface(Out<SharedPointer<IECommerceInterface>> out_interface); + Result GetApplicationVersionInterface( + Out<SharedPointer<IApplicationVersionInterface>> out_interface); + Result GetFactoryResetInterface(Out<SharedPointer<IFactoryResetInterface>> out_interface); + Result GetAccountProxyInterface(Out<SharedPointer<IAccountProxyInterface>> out_interface); + Result GetApplicationManagerInterface( + Out<SharedPointer<IApplicationManagerInterface>> out_interface); + Result GetDownloadTaskInterface(Out<SharedPointer<IDownloadTaskInterface>> out_interface); + Result GetContentManagementInterface( + Out<SharedPointer<IContentManagementInterface>> out_interface); + Result GetDocumentInterface(Out<SharedPointer<IDocumentInterface>> out_interface); +}; + +} // namespace Service::NS diff --git a/src/core/hle/service/ns/system_update_control.cpp b/src/core/hle/service/ns/system_update_control.cpp new file mode 100644 index 000000000..f5f5cfd90 --- /dev/null +++ b/src/core/hle/service/ns/system_update_control.cpp @@ -0,0 +1,44 @@ +// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "core/hle/service/cmif_serialization.h" +#include "core/hle/service/ns/system_update_control.h" + +namespace Service::NS { + +ISystemUpdateControl::ISystemUpdateControl(Core::System& system_) + : ServiceFramework{system_, "ISystemUpdateControl"} { + // clang-format off + static const FunctionInfo functions[] = { + {0, nullptr, "HasDownloaded"}, + {1, nullptr, "RequestCheckLatestUpdate"}, + {2, nullptr, "RequestDownloadLatestUpdate"}, + {3, nullptr, "GetDownloadProgress"}, + {4, nullptr, "ApplyDownloadedUpdate"}, + {5, nullptr, "RequestPrepareCardUpdate"}, + {6, nullptr, "GetPrepareCardUpdateProgress"}, + {7, nullptr, "HasPreparedCardUpdate"}, + {8, nullptr, "ApplyCardUpdate"}, + {9, nullptr, "GetDownloadedEulaDataSize"}, + {10, nullptr, "GetDownloadedEulaData"}, + {11, nullptr, "SetupCardUpdate"}, + {12, nullptr, "GetPreparedCardUpdateEulaDataSize"}, + {13, nullptr, "GetPreparedCardUpdateEulaData"}, + {14, nullptr, "SetupCardUpdateViaSystemUpdater"}, + {15, nullptr, "HasReceived"}, + {16, nullptr, "RequestReceiveSystemUpdate"}, + {17, nullptr, "GetReceiveProgress"}, + {18, nullptr, "ApplyReceivedUpdate"}, + {19, nullptr, "GetReceivedEulaDataSize"}, + {20, nullptr, "GetReceivedEulaData"}, + {21, nullptr, "SetupToReceiveSystemUpdate"}, + {22, nullptr, "RequestCheckLatestUpdateIncludesRebootlessUpdate"}, + }; + // clang-format on + + RegisterHandlers(functions); +} + +ISystemUpdateControl::~ISystemUpdateControl() = default; + +} // namespace Service::NS diff --git a/src/core/hle/service/ns/system_update_control.h b/src/core/hle/service/ns/system_update_control.h new file mode 100644 index 000000000..a30a09000 --- /dev/null +++ b/src/core/hle/service/ns/system_update_control.h @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "core/hle/service/service.h" + +namespace Service::NS { + +class ISystemUpdateControl final : public ServiceFramework<ISystemUpdateControl> { +public: + explicit ISystemUpdateControl(Core::System& system_); + ~ISystemUpdateControl() override; +}; + +} // namespace Service::NS diff --git a/src/core/hle/service/ns/system_update_interface.cpp b/src/core/hle/service/ns/system_update_interface.cpp new file mode 100644 index 000000000..7e22ca3db --- /dev/null +++ b/src/core/hle/service/ns/system_update_interface.cpp @@ -0,0 +1,61 @@ +// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "core/hle/service/cmif_serialization.h" +#include "core/hle/service/ns/system_update_control.h" +#include "core/hle/service/ns/system_update_interface.h" + +namespace Service::NS { + +ISystemUpdateInterface::ISystemUpdateInterface(Core::System& system_) + : ServiceFramework{system_, "ns:su"}, service_context{system_, "ns:su"}, + update_notification_event{service_context} { + // clang-format off + static const FunctionInfo functions[] = { + {0, D<&ISystemUpdateInterface::GetBackgroundNetworkUpdateState>, "GetBackgroundNetworkUpdateState"}, + {1, D<&ISystemUpdateInterface::OpenSystemUpdateControl>, "OpenSystemUpdateControl"}, + {2, nullptr, "NotifyExFatDriverRequired"}, + {3, nullptr, "ClearExFatDriverStatusForDebug"}, + {4, nullptr, "RequestBackgroundNetworkUpdate"}, + {5, nullptr, "NotifyBackgroundNetworkUpdate"}, + {6, nullptr, "NotifyExFatDriverDownloadedForDebug"}, + {9, D<&ISystemUpdateInterface::GetSystemUpdateNotificationEventForContentDelivery>, "GetSystemUpdateNotificationEventForContentDelivery"}, + {10, nullptr, "NotifySystemUpdateForContentDelivery"}, + {11, nullptr, "PrepareShutdown"}, + {12, nullptr, "Unknown12"}, + {13, nullptr, "Unknown13"}, + {14, nullptr, "Unknown14"}, + {15, nullptr, "Unknown15"}, + {16, nullptr, "DestroySystemUpdateTask"}, + {17, nullptr, "RequestSendSystemUpdate"}, + {18, nullptr, "GetSendSystemUpdateProgress"}, + }; + // clang-format on + + RegisterHandlers(functions); +} + +ISystemUpdateInterface::~ISystemUpdateInterface() = default; + +Result ISystemUpdateInterface::GetBackgroundNetworkUpdateState( + Out<BackgroundNetworkUpdateState> out_background_network_update_state) { + LOG_WARNING(Service_AM, "(STUBBED) called"); + *out_background_network_update_state = BackgroundNetworkUpdateState::None; + R_SUCCEED(); +} + +Result ISystemUpdateInterface::OpenSystemUpdateControl( + Out<SharedPointer<ISystemUpdateControl>> out_system_update_control) { + LOG_WARNING(Service_NS, "(STUBBED) called"); + *out_system_update_control = std::make_shared<ISystemUpdateControl>(system); + R_SUCCEED(); +} + +Result ISystemUpdateInterface::GetSystemUpdateNotificationEventForContentDelivery( + OutCopyHandle<Kernel::KReadableEvent> out_event) { + LOG_WARNING(Service_NS, "(STUBBED) called"); + *out_event = update_notification_event.GetHandle(); + R_SUCCEED(); +} + +} // namespace Service::NS diff --git a/src/core/hle/service/ns/system_update_interface.h b/src/core/hle/service/ns/system_update_interface.h new file mode 100644 index 000000000..36a2880ec --- /dev/null +++ b/src/core/hle/service/ns/system_update_interface.h @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "core/hle/service/cmif_types.h" +#include "core/hle/service/kernel_helpers.h" +#include "core/hle/service/ns/ns_types.h" +#include "core/hle/service/os/event.h" +#include "core/hle/service/service.h" + +namespace Kernel { +class KReadableEvent; +} + +namespace Service::NS { + +class ISystemUpdateControl; + +class ISystemUpdateInterface final : public ServiceFramework<ISystemUpdateInterface> { +public: + explicit ISystemUpdateInterface(Core::System& system_); + ~ISystemUpdateInterface() override; + +private: + Result GetBackgroundNetworkUpdateState( + Out<BackgroundNetworkUpdateState> out_background_network_update_state); + Result OpenSystemUpdateControl( + Out<SharedPointer<ISystemUpdateControl>> out_system_update_control); + Result GetSystemUpdateNotificationEventForContentDelivery( + OutCopyHandle<Kernel::KReadableEvent> out_event); + +private: + KernelHelpers::ServiceContext service_context; + Event update_notification_event; +}; + +} // namespace Service::NS diff --git a/src/core/hle/service/ns/vulnerability_manager_interface.cpp b/src/core/hle/service/ns/vulnerability_manager_interface.cpp new file mode 100644 index 000000000..69c21fb89 --- /dev/null +++ b/src/core/hle/service/ns/vulnerability_manager_interface.cpp @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "core/hle/service/cmif_serialization.h" +#include "core/hle/service/ns/vulnerability_manager_interface.h" + +namespace Service::NS { + +IVulnerabilityManagerInterface::IVulnerabilityManagerInterface(Core::System& system_) + : ServiceFramework{system_, "ns:vm"} { + // clang-format off + static const FunctionInfo functions[] = { + {1200, D<&IVulnerabilityManagerInterface::NeedsUpdateVulnerability>, "NeedsUpdateVulnerability"}, + {1201, nullptr, "UpdateSafeSystemVersionForDebug"}, + {1202, nullptr, "GetSafeSystemVersion"}, + }; + // clang-format on + + RegisterHandlers(functions); +} + +IVulnerabilityManagerInterface::~IVulnerabilityManagerInterface() = default; + +Result IVulnerabilityManagerInterface::NeedsUpdateVulnerability( + Out<bool> out_needs_update_vulnerability) { + LOG_WARNING(Service_NS, "(STUBBED) called"); + *out_needs_update_vulnerability = false; + R_SUCCEED(); +} + +} // namespace Service::NS diff --git a/src/core/hle/service/ns/vulnerability_manager_interface.h b/src/core/hle/service/ns/vulnerability_manager_interface.h new file mode 100644 index 000000000..c689cf7ec --- /dev/null +++ b/src/core/hle/service/ns/vulnerability_manager_interface.h @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "core/hle/service/cmif_types.h" +#include "core/hle/service/service.h" + +namespace Service::NS { + +class IVulnerabilityManagerInterface final + : public ServiceFramework<IVulnerabilityManagerInterface> { +public: + explicit IVulnerabilityManagerInterface(Core::System& system_); + ~IVulnerabilityManagerInterface() override; + +private: + Result NeedsUpdateVulnerability(Out<bool> out_needs_update_vulnerability); +}; + +} // namespace Service::NS diff --git a/src/core/memory/cheat_engine.cpp b/src/core/memory/cheat_engine.cpp index b84b57d92..d8921e565 100644 --- a/src/core/memory/cheat_engine.cpp +++ b/src/core/memory/cheat_engine.cpp @@ -117,9 +117,9 @@ bool StandardVmCallbacks::IsAddressInRange(VAddr in) const { (in < metadata.heap_extents.base || in >= metadata.heap_extents.base + metadata.heap_extents.size) && (in < metadata.alias_extents.base || - in >= metadata.heap_extents.base + metadata.alias_extents.size) && + in >= metadata.alias_extents.base + metadata.alias_extents.size) && (in < metadata.aslr_extents.base || - in >= metadata.heap_extents.base + metadata.aslr_extents.size)) { + in >= metadata.aslr_extents.base + metadata.aslr_extents.size)) { LOG_DEBUG(CheatEngine, "Cheat attempting to access memory at invalid address={:016X}, if this " "persists, " diff --git a/src/frontend_common/config.cpp b/src/frontend_common/config.cpp index 2bebfeef9..95f8c8c36 100644 --- a/src/frontend_common/config.cpp +++ b/src/frontend_common/config.cpp @@ -138,6 +138,7 @@ void Config::ReadPlayerValues(const std::size_t player_index) { if (profile_name.empty()) { // Use the global input config player = Settings::values.players.GetValue(true)[player_index]; + player.profile_name = ""; return; } player.profile_name = profile_name; diff --git a/src/frontend_common/content_manager.h b/src/frontend_common/content_manager.h index f3efe3465..c4e97a47b 100644 --- a/src/frontend_common/content_manager.h +++ b/src/frontend_common/content_manager.h @@ -251,11 +251,12 @@ inline InstallResult InstallNCA(FileSys::VfsFilesystem& vfs, const std::string& * \param callback Callback to report the progress of the installation. The first size_t * parameter is the total size of the installed contents and the second is the current progress. If * you return true to the callback, it will cancel the installation as soon as possible. + * \param firmware_only Set to true to only scan system nand NCAs (firmware), post firmware install. * \return A list of entries that failed to install. Returns an empty vector if successful. */ inline std::vector<std::string> VerifyInstalledContents( Core::System& system, FileSys::ManualContentProvider& provider, - const std::function<bool(size_t, size_t)>& callback) { + const std::function<bool(size_t, size_t)>& callback, bool firmware_only = false) { // Get content registries. auto bis_contents = system.GetFileSystemController().GetSystemNANDContents(); auto user_contents = system.GetFileSystemController().GetUserNANDContents(); @@ -264,7 +265,7 @@ inline std::vector<std::string> VerifyInstalledContents( if (bis_contents) { content_providers.push_back(bis_contents); } - if (user_contents) { + if (user_contents && !firmware_only) { content_providers.push_back(user_contents); } diff --git a/src/hid_core/frontend/emulated_controller.cpp b/src/hid_core/frontend/emulated_controller.cpp index 819460eb5..d9d278fa3 100644 --- a/src/hid_core/frontend/emulated_controller.cpp +++ b/src/hid_core/frontend/emulated_controller.cpp @@ -174,18 +174,25 @@ void EmulatedController::LoadDevices() { // Only map virtual devices to the first controller if (npad_id_type == NpadIdType::Player1 || npad_id_type == NpadIdType::Handheld) { camera_params[1] = Common::ParamPackage{"engine:camera,camera:1"}; - ring_params[1] = Common::ParamPackage{"engine:joycon,axis_x:100,axis_y:101"}; nfc_params[0] = Common::ParamPackage{"engine:virtual_amiibo,nfc:1"}; +#ifdef HAVE_LIBUSB + ring_params[1] = Common::ParamPackage{"engine:joycon,axis_x:100,axis_y:101"}; +#endif +#ifdef ANDROID + android_params = Common::ParamPackage{"engine:android,port:100"}; +#endif } output_params[LeftIndex] = left_joycon; output_params[RightIndex] = right_joycon; output_params[2] = camera_params[1]; output_params[3] = nfc_params[0]; + output_params[4] = android_params; output_params[LeftIndex].Set("output", true); output_params[RightIndex].Set("output", true); output_params[2].Set("output", true); output_params[3].Set("output", true); + output_params[4].Set("output", true); LoadTASParams(); LoadVirtualGamepadParams(); @@ -578,6 +585,9 @@ void EmulatedController::DisableConfiguration() { // Get Joycon colors before turning on the controller for (const auto& color_device : color_devices) { + if (color_device == nullptr) { + continue; + } color_device->ForceUpdate(); } @@ -1277,6 +1287,10 @@ bool EmulatedController::SetVibration(DeviceIndex device_index, const VibrationV .high_frequency = vibration.high_frequency, .type = type, }; + + // Send vibrations to Android's input overlay + output_devices[4]->SetVibration(status); + return output_devices[index]->SetVibration(status) == Common::Input::DriverResult::Success; } diff --git a/src/hid_core/frontend/emulated_controller.h b/src/hid_core/frontend/emulated_controller.h index 701b38300..ab3c6fcd3 100644 --- a/src/hid_core/frontend/emulated_controller.h +++ b/src/hid_core/frontend/emulated_controller.h @@ -21,7 +21,7 @@ namespace Core::HID { const std::size_t max_emulated_controllers = 2; -const std::size_t output_devices_size = 4; +const std::size_t output_devices_size = 5; struct ControllerMotionInfo { Common::Input::MotionStatus raw_status{}; MotionInput emulated{}; @@ -597,6 +597,7 @@ private: CameraParams camera_params; RingAnalogParams ring_params; NfcParams nfc_params; + Common::ParamPackage android_params; OutputParams output_params; ButtonDevices button_devices; diff --git a/src/input_common/CMakeLists.txt b/src/input_common/CMakeLists.txt index d0a71a15b..d455323e0 100644 --- a/src/input_common/CMakeLists.txt +++ b/src/input_common/CMakeLists.txt @@ -2,8 +2,6 @@ # SPDX-License-Identifier: GPL-2.0-or-later add_library(input_common STATIC - drivers/android.cpp - drivers/android.h drivers/camera.cpp drivers/camera.h drivers/keyboard.cpp @@ -94,3 +92,11 @@ target_link_libraries(input_common PUBLIC hid_core PRIVATE common Boost::headers if (YUZU_USE_PRECOMPILED_HEADERS) target_precompile_headers(input_common PRIVATE precompiled_headers.h) endif() + +if (ANDROID) + target_sources(input_common PRIVATE + drivers/android.cpp + drivers/android.h + ) + target_link_libraries(input_common PRIVATE android) +endif() diff --git a/src/input_common/drivers/android.cpp b/src/input_common/drivers/android.cpp index b6a03fdc0..e859cc538 100644 --- a/src/input_common/drivers/android.cpp +++ b/src/input_common/drivers/android.cpp @@ -1,30 +1,47 @@ // SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later +#include <set> +#include <common/settings_input.h> +#include <jni.h> +#include "common/android/android_common.h" +#include "common/android/id_cache.h" #include "input_common/drivers/android.h" namespace InputCommon { Android::Android(std::string input_engine_) : InputEngine(std::move(input_engine_)) {} -void Android::RegisterController(std::size_t controller_number) { - PreSetController(GetIdentifier(controller_number)); +void Android::RegisterController(jobject j_input_device) { + auto env = Common::Android::GetEnvForThread(); + const std::string guid = Common::Android::GetJString( + env, static_cast<jstring>( + env->CallObjectMethod(j_input_device, Common::Android::GetYuzuDeviceGetGUID()))); + const s32 port = env->CallIntMethod(j_input_device, Common::Android::GetYuzuDeviceGetPort()); + const auto identifier = GetIdentifier(guid, static_cast<size_t>(port)); + PreSetController(identifier); + + if (input_devices.find(identifier) != input_devices.end()) { + env->DeleteGlobalRef(input_devices[identifier]); + } + auto new_device = env->NewGlobalRef(j_input_device); + input_devices[identifier] = new_device; } -void Android::SetButtonState(std::size_t controller_number, int button_id, bool value) { - const auto identifier = GetIdentifier(controller_number); +void Android::SetButtonState(std::string guid, size_t port, int button_id, bool value) { + const auto identifier = GetIdentifier(guid, port); SetButton(identifier, button_id, value); } -void Android::SetAxisState(std::size_t controller_number, int axis_id, float value) { - const auto identifier = GetIdentifier(controller_number); +void Android::SetAxisPosition(std::string guid, size_t port, int axis_id, float value) { + const auto identifier = GetIdentifier(guid, port); SetAxis(identifier, axis_id, value); } -void Android::SetMotionState(std::size_t controller_number, u64 delta_timestamp, float gyro_x, +void Android::SetMotionState(std::string guid, size_t port, u64 delta_timestamp, float gyro_x, float gyro_y, float gyro_z, float accel_x, float accel_y, float accel_z) { - const auto identifier = GetIdentifier(controller_number); + const auto identifier = GetIdentifier(guid, port); const BasicMotion motion_data{ .gyro_x = gyro_x, .gyro_y = gyro_y, @@ -37,10 +54,295 @@ void Android::SetMotionState(std::size_t controller_number, u64 delta_timestamp, SetMotion(identifier, 0, motion_data); } -PadIdentifier Android::GetIdentifier(std::size_t controller_number) const { +Common::Input::DriverResult Android::SetVibration( + [[maybe_unused]] const PadIdentifier& identifier, + [[maybe_unused]] const Common::Input::VibrationStatus& vibration) { + auto device = input_devices.find(identifier); + if (device != input_devices.end()) { + Common::Android::RunJNIOnFiber<void>([&](JNIEnv* env) { + float average_intensity = + static_cast<float>((vibration.high_amplitude + vibration.low_amplitude) / 2.0); + env->CallVoidMethod(device->second, Common::Android::GetYuzuDeviceVibrate(), + average_intensity); + }); + return Common::Input::DriverResult::Success; + } + return Common::Input::DriverResult::NotSupported; +} + +bool Android::IsVibrationEnabled([[maybe_unused]] const PadIdentifier& identifier) { + auto device = input_devices.find(identifier); + if (device != input_devices.end()) { + return Common::Android::RunJNIOnFiber<bool>([&](JNIEnv* env) { + return static_cast<bool>(env->CallBooleanMethod( + device->second, Common::Android::GetYuzuDeviceGetSupportsVibration())); + }); + } + return false; +} + +std::vector<Common::ParamPackage> Android::GetInputDevices() const { + std::vector<Common::ParamPackage> devices; + auto env = Common::Android::GetEnvForThread(); + for (const auto& [key, value] : input_devices) { + auto name_object = static_cast<jstring>( + env->CallObjectMethod(value, Common::Android::GetYuzuDeviceGetName())); + const std::string name = + fmt::format("{} {}", Common::Android::GetJString(env, name_object), key.port); + devices.emplace_back(Common::ParamPackage{ + {"engine", GetEngineName()}, + {"display", std::move(name)}, + {"guid", key.guid.RawString()}, + {"port", std::to_string(key.port)}, + }); + } + return devices; +} + +std::set<s32> Android::GetDeviceAxes(JNIEnv* env, jobject& j_device) const { + auto j_axes = static_cast<jobjectArray>( + env->CallObjectMethod(j_device, Common::Android::GetYuzuDeviceGetAxes())); + std::set<s32> axes; + for (int i = 0; i < env->GetArrayLength(j_axes); ++i) { + jobject axis = env->GetObjectArrayElement(j_axes, i); + axes.insert(env->GetIntField(axis, Common::Android::GetIntegerValueField())); + } + return axes; +} + +Common::ParamPackage Android::BuildParamPackageForAnalog(PadIdentifier identifier, int axis_x, + int axis_y) const { + Common::ParamPackage params; + params.Set("engine", GetEngineName()); + params.Set("port", static_cast<int>(identifier.port)); + params.Set("guid", identifier.guid.RawString()); + params.Set("axis_x", axis_x); + params.Set("axis_y", axis_y); + params.Set("offset_x", 0); + params.Set("offset_y", 0); + params.Set("invert_x", "+"); + + // Invert Y-Axis by default + params.Set("invert_y", "-"); + return params; +} + +Common::ParamPackage Android::BuildAnalogParamPackageForButton(PadIdentifier identifier, s32 axis, + bool invert) const { + Common::ParamPackage params{}; + params.Set("engine", GetEngineName()); + params.Set("port", static_cast<int>(identifier.port)); + params.Set("guid", identifier.guid.RawString()); + params.Set("axis", axis); + params.Set("threshold", "0.5"); + params.Set("invert", invert ? "-" : "+"); + return params; +} + +Common::ParamPackage Android::BuildButtonParamPackageForButton(PadIdentifier identifier, + s32 button) const { + Common::ParamPackage params{}; + params.Set("engine", GetEngineName()); + params.Set("port", static_cast<int>(identifier.port)); + params.Set("guid", identifier.guid.RawString()); + params.Set("button", button); + return params; +} + +bool Android::MatchVID(Common::UUID device, const std::vector<std::string>& vids) const { + for (size_t i = 0; i < vids.size(); ++i) { + auto fucker = device.RawString(); + if (fucker.find(vids[i]) != std::string::npos) { + return true; + } + } + return false; +} + +AnalogMapping Android::GetAnalogMappingForDevice(const Common::ParamPackage& params) { + if (!params.Has("guid") || !params.Has("port")) { + return {}; + } + + auto identifier = + GetIdentifier(params.Get("guid", ""), static_cast<size_t>(params.Get("port", 0))); + auto& j_device = input_devices[identifier]; + if (j_device == nullptr) { + return {}; + } + + auto env = Common::Android::GetEnvForThread(); + std::set<s32> axes = GetDeviceAxes(env, j_device); + if (axes.size() == 0) { + return {}; + } + + AnalogMapping mapping = {}; + if (axes.find(AXIS_X) != axes.end() && axes.find(AXIS_Y) != axes.end()) { + mapping.insert_or_assign(Settings::NativeAnalog::LStick, + BuildParamPackageForAnalog(identifier, AXIS_X, AXIS_Y)); + } + + if (axes.find(AXIS_RX) != axes.end() && axes.find(AXIS_RY) != axes.end()) { + mapping.insert_or_assign(Settings::NativeAnalog::RStick, + BuildParamPackageForAnalog(identifier, AXIS_RX, AXIS_RY)); + } else if (axes.find(AXIS_Z) != axes.end() && axes.find(AXIS_RZ) != axes.end()) { + mapping.insert_or_assign(Settings::NativeAnalog::RStick, + BuildParamPackageForAnalog(identifier, AXIS_Z, AXIS_RZ)); + } + return mapping; +} + +ButtonMapping Android::GetButtonMappingForDevice(const Common::ParamPackage& params) { + if (!params.Has("guid") || !params.Has("port")) { + return {}; + } + + auto identifier = + GetIdentifier(params.Get("guid", ""), static_cast<size_t>(params.Get("port", 0))); + auto& j_device = input_devices[identifier]; + if (j_device == nullptr) { + return {}; + } + + auto env = Common::Android::GetEnvForThread(); + jintArray j_keys = env->NewIntArray(static_cast<int>(keycode_ids.size())); + env->SetIntArrayRegion(j_keys, 0, static_cast<int>(keycode_ids.size()), keycode_ids.data()); + auto j_has_keys_object = static_cast<jbooleanArray>( + env->CallObjectMethod(j_device, Common::Android::GetYuzuDeviceHasKeys(), j_keys)); + jboolean isCopy = false; + jboolean* j_has_keys = env->GetBooleanArrayElements(j_has_keys_object, &isCopy); + + std::set<s32> available_keys; + for (size_t i = 0; i < keycode_ids.size(); ++i) { + if (j_has_keys[i]) { + available_keys.insert(keycode_ids[i]); + } + } + + // Some devices use axes instead of buttons for certain controls so we need all the axes here + std::set<s32> axes = GetDeviceAxes(env, j_device); + + ButtonMapping mapping = {}; + if (axes.find(AXIS_HAT_X) != axes.end() && axes.find(AXIS_HAT_Y) != axes.end()) { + mapping.insert_or_assign(Settings::NativeButton::DUp, + BuildAnalogParamPackageForButton(identifier, AXIS_HAT_Y, true)); + mapping.insert_or_assign(Settings::NativeButton::DDown, + BuildAnalogParamPackageForButton(identifier, AXIS_HAT_Y, false)); + mapping.insert_or_assign(Settings::NativeButton::DLeft, + BuildAnalogParamPackageForButton(identifier, AXIS_HAT_X, true)); + mapping.insert_or_assign(Settings::NativeButton::DRight, + BuildAnalogParamPackageForButton(identifier, AXIS_HAT_X, false)); + } else if (available_keys.find(KEYCODE_DPAD_UP) != available_keys.end() && + available_keys.find(KEYCODE_DPAD_DOWN) != available_keys.end() && + available_keys.find(KEYCODE_DPAD_LEFT) != available_keys.end() && + available_keys.find(KEYCODE_DPAD_RIGHT) != available_keys.end()) { + mapping.insert_or_assign(Settings::NativeButton::DUp, + BuildButtonParamPackageForButton(identifier, KEYCODE_DPAD_UP)); + mapping.insert_or_assign(Settings::NativeButton::DDown, + BuildButtonParamPackageForButton(identifier, KEYCODE_DPAD_DOWN)); + mapping.insert_or_assign(Settings::NativeButton::DLeft, + BuildButtonParamPackageForButton(identifier, KEYCODE_DPAD_LEFT)); + mapping.insert_or_assign(Settings::NativeButton::DRight, + BuildButtonParamPackageForButton(identifier, KEYCODE_DPAD_RIGHT)); + } + + if (axes.find(AXIS_LTRIGGER) != axes.end()) { + mapping.insert_or_assign(Settings::NativeButton::ZL, BuildAnalogParamPackageForButton( + identifier, AXIS_LTRIGGER, false)); + } else if (available_keys.find(KEYCODE_BUTTON_L2) != available_keys.end()) { + mapping.insert_or_assign(Settings::NativeButton::ZL, + BuildButtonParamPackageForButton(identifier, KEYCODE_BUTTON_L2)); + } + + if (axes.find(AXIS_RTRIGGER) != axes.end()) { + mapping.insert_or_assign(Settings::NativeButton::ZR, BuildAnalogParamPackageForButton( + identifier, AXIS_RTRIGGER, false)); + } else if (available_keys.find(KEYCODE_BUTTON_R2) != available_keys.end()) { + mapping.insert_or_assign(Settings::NativeButton::ZR, + BuildButtonParamPackageForButton(identifier, KEYCODE_BUTTON_R2)); + } + + if (available_keys.find(KEYCODE_BUTTON_A) != available_keys.end()) { + if (MatchVID(identifier.guid, flipped_ab_vids)) { + mapping.insert_or_assign(Settings::NativeButton::B, BuildButtonParamPackageForButton( + identifier, KEYCODE_BUTTON_A)); + } else { + mapping.insert_or_assign(Settings::NativeButton::A, BuildButtonParamPackageForButton( + identifier, KEYCODE_BUTTON_A)); + } + } + if (available_keys.find(KEYCODE_BUTTON_B) != available_keys.end()) { + if (MatchVID(identifier.guid, flipped_ab_vids)) { + mapping.insert_or_assign(Settings::NativeButton::A, BuildButtonParamPackageForButton( + identifier, KEYCODE_BUTTON_B)); + } else { + mapping.insert_or_assign(Settings::NativeButton::B, BuildButtonParamPackageForButton( + identifier, KEYCODE_BUTTON_B)); + } + } + if (available_keys.find(KEYCODE_BUTTON_X) != available_keys.end()) { + if (MatchVID(identifier.guid, flipped_xy_vids)) { + mapping.insert_or_assign(Settings::NativeButton::Y, BuildButtonParamPackageForButton( + identifier, KEYCODE_BUTTON_X)); + } else { + mapping.insert_or_assign(Settings::NativeButton::X, BuildButtonParamPackageForButton( + identifier, KEYCODE_BUTTON_X)); + } + } + if (available_keys.find(KEYCODE_BUTTON_Y) != available_keys.end()) { + if (MatchVID(identifier.guid, flipped_xy_vids)) { + mapping.insert_or_assign(Settings::NativeButton::X, BuildButtonParamPackageForButton( + identifier, KEYCODE_BUTTON_Y)); + } else { + mapping.insert_or_assign(Settings::NativeButton::Y, BuildButtonParamPackageForButton( + identifier, KEYCODE_BUTTON_Y)); + } + } + + if (available_keys.find(KEYCODE_BUTTON_L1) != available_keys.end()) { + mapping.insert_or_assign(Settings::NativeButton::L, + BuildButtonParamPackageForButton(identifier, KEYCODE_BUTTON_L1)); + } + if (available_keys.find(KEYCODE_BUTTON_R1) != available_keys.end()) { + mapping.insert_or_assign(Settings::NativeButton::R, + BuildButtonParamPackageForButton(identifier, KEYCODE_BUTTON_R1)); + } + + if (available_keys.find(KEYCODE_BUTTON_THUMBL) != available_keys.end()) { + mapping.insert_or_assign( + Settings::NativeButton::LStick, + BuildButtonParamPackageForButton(identifier, KEYCODE_BUTTON_THUMBL)); + } + if (available_keys.find(KEYCODE_BUTTON_THUMBR) != available_keys.end()) { + mapping.insert_or_assign( + Settings::NativeButton::RStick, + BuildButtonParamPackageForButton(identifier, KEYCODE_BUTTON_THUMBR)); + } + + if (available_keys.find(KEYCODE_BUTTON_START) != available_keys.end()) { + mapping.insert_or_assign( + Settings::NativeButton::Plus, + BuildButtonParamPackageForButton(identifier, KEYCODE_BUTTON_START)); + } + if (available_keys.find(KEYCODE_BUTTON_SELECT) != available_keys.end()) { + mapping.insert_or_assign( + Settings::NativeButton::Minus, + BuildButtonParamPackageForButton(identifier, KEYCODE_BUTTON_SELECT)); + } + + return mapping; +} + +Common::Input::ButtonNames Android::GetUIName( + [[maybe_unused]] const Common::ParamPackage& params) const { + return Common::Input::ButtonNames::Value; +} + +PadIdentifier Android::GetIdentifier(const std::string& guid, size_t port) const { return { - .guid = Common::UUID{}, - .port = controller_number, + .guid = Common::UUID{guid}, + .port = port, .pad = 0, }; } diff --git a/src/input_common/drivers/android.h b/src/input_common/drivers/android.h index 3f01817f6..8a386c1b1 100644 --- a/src/input_common/drivers/android.h +++ b/src/input_common/drivers/android.h @@ -3,6 +3,8 @@ #pragma once +#include <set> +#include <jni.h> #include "input_common/input_engine.h" namespace InputCommon { @@ -15,40 +17,122 @@ public: explicit Android(std::string input_engine_); /** - * Registers controller number to accept new inputs - * @param controller_number the controller number that will take this action + * Registers controller number to accept new inputs. + * @param j_input_device YuzuInputDevice object from the Android frontend to register. */ - void RegisterController(std::size_t controller_number); + void RegisterController(jobject j_input_device); /** - * Sets the status of all buttons bound with the key to pressed - * @param controller_number the controller number that will take this action - * @param button_id the id of the button - * @param value indicates if the button is pressed or not + * Sets the status of a button on a specific controller. + * @param guid 32 character hexadecimal string consisting of the controller's PID+VID. + * @param port Port determined by controller connection order. + * @param button_id The Android Keycode corresponding to this event. + * @param value Whether the button is pressed or not. */ - void SetButtonState(std::size_t controller_number, int button_id, bool value); + void SetButtonState(std::string guid, size_t port, int button_id, bool value); /** - * Sets the status of a analog input to a specific player index - * @param controller_number the controller number that will take this action - * @param axis_id the id of the axis to move - * @param value the analog position of the axis + * Sets the status of an axis on a specific controller. + * @param guid 32 character hexadecimal string consisting of the controller's PID+VID. + * @param port Port determined by controller connection order. + * @param axis_id The Android axis ID corresponding to this event. + * @param value Value along the given axis. */ - void SetAxisState(std::size_t controller_number, int axis_id, float value); + void SetAxisPosition(std::string guid, size_t port, int axis_id, float value); /** - * Sets the status of the motion sensor to a specific player index - * @param controller_number the controller number that will take this action - * @param delta_timestamp time passed since last reading - * @param gyro_x,gyro_y,gyro_z the gyro sensor readings - * @param accel_x,accel_y,accel_z the accelerometer reading + * Sets the status of the motion sensor on a specific controller + * @param guid 32 character hexadecimal string consisting of the controller's PID+VID. + * @param port Port determined by controller connection order. + * @param delta_timestamp Time passed since the last read. + * @param gyro_x,gyro_y,gyro_z Gyro sensor readings. + * @param accel_x,accel_y,accel_z Accelerometer sensor readings. */ - void SetMotionState(std::size_t controller_number, u64 delta_timestamp, float gyro_x, + void SetMotionState(std::string guid, size_t port, u64 delta_timestamp, float gyro_x, float gyro_y, float gyro_z, float accel_x, float accel_y, float accel_z); + Common::Input::DriverResult SetVibration( + const PadIdentifier& identifier, const Common::Input::VibrationStatus& vibration) override; + + bool IsVibrationEnabled(const PadIdentifier& identifier) override; + + std::vector<Common::ParamPackage> GetInputDevices() const override; + + /** + * Gets the axes reported by the YuzuInputDevice. + * @param env JNI environment pointer. + * @param j_device YuzuInputDevice from the Android frontend. + * @return Set of the axes reported by the underlying Android InputDevice + */ + std::set<s32> GetDeviceAxes(JNIEnv* env, jobject& j_device) const; + + Common::ParamPackage BuildParamPackageForAnalog(PadIdentifier identifier, int axis_x, + int axis_y) const; + + Common::ParamPackage BuildAnalogParamPackageForButton(PadIdentifier identifier, s32 axis, + bool invert) const; + + Common::ParamPackage BuildButtonParamPackageForButton(PadIdentifier identifier, + s32 button) const; + + bool MatchVID(Common::UUID device, const std::vector<std::string>& vids) const; + + AnalogMapping GetAnalogMappingForDevice(const Common::ParamPackage& params) override; + + ButtonMapping GetButtonMappingForDevice(const Common::ParamPackage& params) override; + + Common::Input::ButtonNames GetUIName(const Common::ParamPackage& params) const override; + private: + std::unordered_map<PadIdentifier, jobject> input_devices; + /// Returns the correct identifier corresponding to the player index - PadIdentifier GetIdentifier(std::size_t controller_number) const; + PadIdentifier GetIdentifier(const std::string& guid, size_t port) const; + + static constexpr s32 AXIS_X = 0; + static constexpr s32 AXIS_Y = 1; + static constexpr s32 AXIS_Z = 11; + static constexpr s32 AXIS_RX = 12; + static constexpr s32 AXIS_RY = 13; + static constexpr s32 AXIS_RZ = 14; + static constexpr s32 AXIS_HAT_X = 15; + static constexpr s32 AXIS_HAT_Y = 16; + static constexpr s32 AXIS_LTRIGGER = 17; + static constexpr s32 AXIS_RTRIGGER = 18; + + static constexpr s32 KEYCODE_DPAD_UP = 19; + static constexpr s32 KEYCODE_DPAD_DOWN = 20; + static constexpr s32 KEYCODE_DPAD_LEFT = 21; + static constexpr s32 KEYCODE_DPAD_RIGHT = 22; + static constexpr s32 KEYCODE_BUTTON_A = 96; + static constexpr s32 KEYCODE_BUTTON_B = 97; + static constexpr s32 KEYCODE_BUTTON_X = 99; + static constexpr s32 KEYCODE_BUTTON_Y = 100; + static constexpr s32 KEYCODE_BUTTON_L1 = 102; + static constexpr s32 KEYCODE_BUTTON_R1 = 103; + static constexpr s32 KEYCODE_BUTTON_L2 = 104; + static constexpr s32 KEYCODE_BUTTON_R2 = 105; + static constexpr s32 KEYCODE_BUTTON_THUMBL = 106; + static constexpr s32 KEYCODE_BUTTON_THUMBR = 107; + static constexpr s32 KEYCODE_BUTTON_START = 108; + static constexpr s32 KEYCODE_BUTTON_SELECT = 109; + const std::vector<s32> keycode_ids{ + KEYCODE_DPAD_UP, KEYCODE_DPAD_DOWN, KEYCODE_DPAD_LEFT, KEYCODE_DPAD_RIGHT, + KEYCODE_BUTTON_A, KEYCODE_BUTTON_B, KEYCODE_BUTTON_X, KEYCODE_BUTTON_Y, + KEYCODE_BUTTON_L1, KEYCODE_BUTTON_R1, KEYCODE_BUTTON_L2, KEYCODE_BUTTON_R2, + KEYCODE_BUTTON_THUMBL, KEYCODE_BUTTON_THUMBR, KEYCODE_BUTTON_START, KEYCODE_BUTTON_SELECT, + }; + + const std::string sony_vid{"054c"}; + const std::string nintendo_vid{"057e"}; + const std::string razer_vid{"1532"}; + const std::string redmagic_vid{"3537"}; + const std::string backbone_labs_vid{"358a"}; + const std::string xbox_vid{"045e"}; + const std::vector<std::string> flipped_ab_vids{sony_vid, nintendo_vid, razer_vid, + redmagic_vid, backbone_labs_vid, xbox_vid}; + const std::vector<std::string> flipped_xy_vids{sony_vid, razer_vid, redmagic_vid, + backbone_labs_vid, xbox_vid}; }; } // namespace InputCommon diff --git a/src/input_common/main.cpp b/src/input_common/main.cpp index f8749ebbf..62a7ae40f 100644 --- a/src/input_common/main.cpp +++ b/src/input_common/main.cpp @@ -4,7 +4,6 @@ #include <memory> #include "common/input.h" #include "common/param_package.h" -#include "input_common/drivers/android.h" #include "input_common/drivers/camera.h" #include "input_common/drivers/keyboard.h" #include "input_common/drivers/mouse.h" @@ -28,6 +27,10 @@ #include "input_common/drivers/sdl_driver.h" #endif +#ifdef ANDROID +#include "input_common/drivers/android.h" +#endif + namespace InputCommon { /// Dummy engine to get periodic updates @@ -79,7 +82,9 @@ struct InputSubsystem::Impl { RegisterEngine("cemuhookudp", udp_client); RegisterEngine("tas", tas_input); RegisterEngine("camera", camera); +#ifdef ANDROID RegisterEngine("android", android); +#endif RegisterEngine("virtual_amiibo", virtual_amiibo); RegisterEngine("virtual_gamepad", virtual_gamepad); #ifdef HAVE_SDL2 @@ -111,7 +116,9 @@ struct InputSubsystem::Impl { UnregisterEngine(udp_client); UnregisterEngine(tas_input); UnregisterEngine(camera); +#ifdef ANDROID UnregisterEngine(android); +#endif UnregisterEngine(virtual_amiibo); UnregisterEngine(virtual_gamepad); #ifdef HAVE_SDL2 @@ -128,12 +135,16 @@ struct InputSubsystem::Impl { Common::ParamPackage{{"display", "Any"}, {"engine", "any"}}, }; +#ifndef ANDROID auto keyboard_devices = keyboard->GetInputDevices(); devices.insert(devices.end(), keyboard_devices.begin(), keyboard_devices.end()); auto mouse_devices = mouse->GetInputDevices(); devices.insert(devices.end(), mouse_devices.begin(), mouse_devices.end()); +#endif +#ifdef ANDROID auto android_devices = android->GetInputDevices(); devices.insert(devices.end(), android_devices.begin(), android_devices.end()); +#endif #ifdef HAVE_LIBUSB auto gcadapter_devices = gcadapter->GetInputDevices(); devices.insert(devices.end(), gcadapter_devices.begin(), gcadapter_devices.end()); @@ -162,9 +173,11 @@ struct InputSubsystem::Impl { if (engine == mouse->GetEngineName()) { return mouse; } +#ifdef ANDROID if (engine == android->GetEngineName()) { return android; } +#endif #ifdef HAVE_LIBUSB if (engine == gcadapter->GetEngineName()) { return gcadapter; @@ -245,9 +258,11 @@ struct InputSubsystem::Impl { if (engine == mouse->GetEngineName()) { return true; } +#ifdef ANDROID if (engine == android->GetEngineName()) { return true; } +#endif #ifdef HAVE_LIBUSB if (engine == gcadapter->GetEngineName()) { return true; @@ -276,7 +291,9 @@ struct InputSubsystem::Impl { void BeginConfiguration() { keyboard->BeginConfiguration(); mouse->BeginConfiguration(); +#ifdef ANDROID android->BeginConfiguration(); +#endif #ifdef HAVE_LIBUSB gcadapter->BeginConfiguration(); #endif @@ -290,7 +307,9 @@ struct InputSubsystem::Impl { void EndConfiguration() { keyboard->EndConfiguration(); mouse->EndConfiguration(); +#ifdef ANDROID android->EndConfiguration(); +#endif #ifdef HAVE_LIBUSB gcadapter->EndConfiguration(); #endif @@ -321,7 +340,6 @@ struct InputSubsystem::Impl { std::shared_ptr<TasInput::Tas> tas_input; std::shared_ptr<CemuhookUDP::UDPClient> udp_client; std::shared_ptr<Camera> camera; - std::shared_ptr<Android> android; std::shared_ptr<VirtualAmiibo> virtual_amiibo; std::shared_ptr<VirtualGamepad> virtual_gamepad; @@ -333,6 +351,10 @@ struct InputSubsystem::Impl { std::shared_ptr<SDLDriver> sdl; std::shared_ptr<Joycons> joycon; #endif + +#ifdef ANDROID + std::shared_ptr<Android> android; +#endif }; InputSubsystem::InputSubsystem() : impl{std::make_unique<Impl>()} {} @@ -387,6 +409,7 @@ const Camera* InputSubsystem::GetCamera() const { return impl->camera.get(); } +#ifdef ANDROID Android* InputSubsystem::GetAndroid() { return impl->android.get(); } @@ -394,6 +417,7 @@ Android* InputSubsystem::GetAndroid() { const Android* InputSubsystem::GetAndroid() const { return impl->android.get(); } +#endif VirtualAmiibo* InputSubsystem::GetVirtualAmiibo() { return impl->virtual_amiibo.get(); diff --git a/src/yuzu/configuration/qt_config.cpp b/src/yuzu/configuration/qt_config.cpp index 1051031f2..37951b9c8 100644 --- a/src/yuzu/configuration/qt_config.cpp +++ b/src/yuzu/configuration/qt_config.cpp @@ -90,6 +90,7 @@ void QtConfig::ReadQtPlayerValues(const std::size_t player_index) { if (profile_name.empty()) { // Use the global input config player = Settings::values.players.GetValue(true)[player_index]; + player.profile_name = ""; return; } } diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp index dfa50006a..0d16bfd65 100644 --- a/src/yuzu/main.cpp +++ b/src/yuzu/main.cpp @@ -1603,6 +1603,7 @@ void GMainWindow::ConnectMenuEvents() { // Help connect_menu(ui->action_Open_yuzu_Folder, &GMainWindow::OnOpenYuzuFolder); connect_menu(ui->action_Verify_installed_contents, &GMainWindow::OnVerifyInstalledContents); + connect_menu(ui->action_Install_Firmware, &GMainWindow::OnInstallFirmware); connect_menu(ui->action_About, &GMainWindow::OnAbout); } @@ -1631,6 +1632,8 @@ void GMainWindow::UpdateMenuState() { action->setEnabled(emulation_running); } + ui->action_Install_Firmware->setEnabled(!emulation_running); + for (QAction* action : applet_actions) { action->setEnabled(is_firmware_available && !emulation_running); } @@ -4150,6 +4153,146 @@ void GMainWindow::OnVerifyInstalledContents() { } } +void GMainWindow::OnInstallFirmware() { + // Don't do this while emulation is running, that'd probably be a bad idea. + if (emu_thread != nullptr && emu_thread->IsRunning()) { + return; + } + + // Check for installed keys, error out, suggest restart? + if (!ContentManager::AreKeysPresent()) { + QMessageBox::information( + this, tr("Keys not installed"), + tr("Install decryption keys and restart yuzu before attempting to install firmware.")); + return; + } + + QString firmware_source_location = + QFileDialog::getExistingDirectory(this, tr("Select Dumped Firmware Source Location"), + QString::fromStdString(""), QFileDialog::ShowDirsOnly); + if (firmware_source_location.isEmpty()) { + return; + } + + QProgressDialog progress(tr("Installing Firmware..."), tr("Cancel"), 0, 100, this); + progress.setWindowModality(Qt::WindowModal); + progress.setMinimumDuration(100); + progress.setAutoClose(false); + progress.setAutoReset(false); + progress.show(); + + // Declare progress callback. + auto QtProgressCallback = [&](size_t total_size, size_t processed_size) { + progress.setValue(static_cast<int>((processed_size * 100) / total_size)); + return progress.wasCanceled(); + }; + + LOG_INFO(Frontend, "Installing firmware from {}", firmware_source_location.toStdString()); + + // Check for a reasonable number of .nca files (don't hardcode them, just see if there's some in + // there.) + std::filesystem::path firmware_source_path = firmware_source_location.toStdString(); + if (!Common::FS::IsDir(firmware_source_path)) { + progress.close(); + return; + } + + std::vector<std::filesystem::path> out; + const Common::FS::DirEntryCallable callback = + [&out](const std::filesystem::directory_entry& entry) { + if (entry.path().has_extension() && entry.path().extension() == ".nca") + out.emplace_back(entry.path()); + + return true; + }; + + QtProgressCallback(100, 10); + + Common::FS::IterateDirEntries(firmware_source_path, callback, Common::FS::DirEntryFilter::File); + if (out.size() <= 0) { + progress.close(); + QMessageBox::warning(this, tr("Firmware install failed"), + tr("Unable to locate potential firmware NCA files")); + return; + } + + // Locate and erase the content of nand/system/Content/registered/*.nca, if any. + auto sysnand_content_vdir = system->GetFileSystemController().GetSystemNANDContentDirectory(); + if (!sysnand_content_vdir->CleanSubdirectoryRecursive("registered")) { + progress.close(); + QMessageBox::critical(this, tr("Firmware install failed"), + tr("Failed to delete one or more firmware file.")); + return; + } + + LOG_INFO(Frontend, + "Cleaned nand/system/Content/registered folder in preparation for new firmware."); + + QtProgressCallback(100, 20); + + auto firmware_vdir = sysnand_content_vdir->GetDirectoryRelative("registered"); + + bool success = true; + bool cancelled = false; + int i = 0; + for (const auto& firmware_src_path : out) { + i++; + auto firmware_src_vfile = + vfs->OpenFile(firmware_src_path.generic_string(), FileSys::OpenMode::Read); + auto firmware_dst_vfile = + firmware_vdir->CreateFileRelative(firmware_src_path.filename().string()); + + if (!VfsRawCopy(firmware_src_vfile, firmware_dst_vfile)) { + LOG_ERROR(Frontend, "Failed to copy firmware file {} to {} in registered folder!", + firmware_src_path.generic_string(), firmware_src_path.filename().string()); + success = false; + } + + if (QtProgressCallback(100, 20 + (int)(((float)(i) / (float)out.size()) * 70.0))) { + success = false; + cancelled = true; + break; + } + } + + if (!success && !cancelled) { + progress.close(); + QMessageBox::critical(this, tr("Firmware install failed"), + tr("One or more firmware files failed to copy into NAND.")); + return; + } else if (cancelled) { + progress.close(); + QMessageBox::warning(this, tr("Firmware install failed"), + tr("Firmware installation cancelled, firmware may be in bad state, " + "restart yuzu or re-install firmware.")); + return; + } + + // Re-scan VFS for the newly placed firmware files. + system->GetFileSystemController().CreateFactories(*vfs); + + auto VerifyFirmwareCallback = [&](size_t total_size, size_t processed_size) { + progress.setValue(90 + static_cast<int>((processed_size * 10) / total_size)); + return progress.wasCanceled(); + }; + + auto result = + ContentManager::VerifyInstalledContents(*system, *provider, VerifyFirmwareCallback, true); + + if (result.size() > 0) { + const auto failed_names = + QString::fromStdString(fmt::format("{}", fmt::join(result, "\n"))); + progress.close(); + QMessageBox::critical( + this, tr("Firmware integrity verification failed!"), + tr("Verification failed for the following files:\n\n%1").arg(failed_names)); + return; + } + + progress.close(); + OnCheckFirmwareDecryption(); +} + void GMainWindow::OnAbout() { AboutDialog aboutDialog(this); aboutDialog.exec(); diff --git a/src/yuzu/main.h b/src/yuzu/main.h index aba61e388..1f0e35c67 100644 --- a/src/yuzu/main.h +++ b/src/yuzu/main.h @@ -380,6 +380,7 @@ private slots: void OnLoadAmiibo(); void OnOpenYuzuFolder(); void OnVerifyInstalledContents(); + void OnInstallFirmware(); void OnAbout(); void OnToggleFilterBar(); void OnToggleStatusBar(); diff --git a/src/yuzu/main.ui b/src/yuzu/main.ui index 6a6b0821f..6ff444a22 100644 --- a/src/yuzu/main.ui +++ b/src/yuzu/main.ui @@ -25,7 +25,16 @@ </property> <widget class="QWidget" name="centralwidget"> <layout class="QHBoxLayout" name="horizontalLayout"> - <property name="margin" stdset="0"> + <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> </layout> @@ -156,8 +165,8 @@ <addaction name="separator"/> <addaction name="action_Configure_Tas"/> </widget> - <addaction name="action_Rederive"/> <addaction name="action_Verify_installed_contents"/> + <addaction name="action_Install_Firmware"/> <addaction name="separator"/> <addaction name="menu_cabinet_applet"/> <addaction name="action_Load_Album"/> @@ -455,6 +464,11 @@ <string>Open &Controller Menu</string> </property> </action> + <action name="action_Install_Firmware"> + <property name="text"> + <string>Install Firmware</string> + </property> + </action> </widget> <resources> <include location="yuzu.qrc"/> diff --git a/src/yuzu_cmd/sdl_config.cpp b/src/yuzu_cmd/sdl_config.cpp index 995114510..6e0f254b6 100644 --- a/src/yuzu_cmd/sdl_config.cpp +++ b/src/yuzu_cmd/sdl_config.cpp @@ -103,6 +103,7 @@ void SdlConfig::ReadSdlPlayerValues(const std::size_t player_index) { if (profile_name.empty()) { // Use the global input config player = Settings::values.players.GetValue(true)[player_index]; + player.profile_name = ""; return; } } |