[Android] Finally add Amiibo load support to Android (#2845)
Co-authored-by: Ribbit <ribbit@placeholder.com> Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/2845 Reviewed-by: crueter <crueter@eden-emu.dev> Reviewed-by: Maufeat <sahyno1996@gmail.com> Co-authored-by: Ribbit <ribbit@eden-emu.dev> Co-committed-by: Ribbit <ribbit@eden-emu.dev>
This commit is contained in:
parent
c788dbb3ef
commit
683c2834aa
|
|
@ -549,6 +549,21 @@ object NativeLibrary {
|
|||
*/
|
||||
external fun clearFilesystemProvider()
|
||||
|
||||
/**
|
||||
* Gets the current virtual amiibo state reported by the core.
|
||||
*
|
||||
* @return Native enum value for the current amiibo state.
|
||||
*/
|
||||
external fun getVirtualAmiiboState(): Int
|
||||
|
||||
/**
|
||||
* Loads amiibo data into the currently running emulation session.
|
||||
*
|
||||
* @param data Raw amiibo file contents.
|
||||
* @return Native enum value representing the load result.
|
||||
*/
|
||||
external fun loadAmiibo(data: ByteArray): Int
|
||||
|
||||
/**
|
||||
* Checks if all necessary keys are present for decryption
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -35,6 +35,8 @@ import android.widget.FrameLayout
|
|||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.core.graphics.Insets
|
||||
|
|
@ -60,6 +62,7 @@ import org.yuzu.yuzu_emu.R
|
|||
import org.yuzu.yuzu_emu.activities.EmulationActivity
|
||||
import org.yuzu.yuzu_emu.databinding.DialogOverlayAdjustBinding
|
||||
import org.yuzu.yuzu_emu.databinding.FragmentEmulationBinding
|
||||
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
|
||||
|
|
@ -121,6 +124,39 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
|||
|
||||
private var perfStatsRunnable: Runnable? = null
|
||||
private var socRunnable: Runnable? = null
|
||||
private var isAmiiboPickerOpen = false
|
||||
|
||||
private val loadAmiiboLauncher =
|
||||
registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
|
||||
isAmiiboPickerOpen = false
|
||||
val binding = _binding ?: return@registerForActivityResult
|
||||
binding.inGameMenu.requestFocus()
|
||||
|
||||
if (!isAdded || uri == null) {
|
||||
return@registerForActivityResult
|
||||
}
|
||||
|
||||
val data = try {
|
||||
requireContext().contentResolver.openInputStream(uri)?.use { it.readBytes() }
|
||||
} catch (e: Exception) {
|
||||
Log.error("[EmulationFragment] Failed to read amiibo: ${e.message}")
|
||||
showAmiiboDialog(R.string.amiibo_unknown_error)
|
||||
return@registerForActivityResult
|
||||
}
|
||||
|
||||
val amiiboData = data ?: run {
|
||||
showAmiiboDialog(R.string.amiibo_not_valid)
|
||||
return@registerForActivityResult
|
||||
}
|
||||
|
||||
if (amiiboData.isEmpty()) {
|
||||
showAmiiboDialog(R.string.amiibo_not_valid)
|
||||
return@registerForActivityResult
|
||||
}
|
||||
|
||||
val result = NativeLibrary.loadAmiibo(amiiboData)
|
||||
handleAmiiboLoadResult(result)
|
||||
}
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
|
|
@ -623,6 +659,8 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
|||
true
|
||||
}
|
||||
|
||||
R.id.menu_load_amiibo -> handleLoadAmiiboSelection()
|
||||
|
||||
R.id.menu_controls -> {
|
||||
val action = HomeNavigationDirections.actionGlobalSettingsActivity(
|
||||
null,
|
||||
|
|
@ -893,6 +931,70 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
|||
}
|
||||
}
|
||||
|
||||
private fun handleLoadAmiiboSelection(): Boolean {
|
||||
val binding = _binding ?: return true
|
||||
|
||||
binding.inGameMenu.requestFocus()
|
||||
|
||||
if (!NativeLibrary.isRunning()) {
|
||||
showAmiiboDialog(R.string.amiibo_wrong_state)
|
||||
return true
|
||||
}
|
||||
|
||||
when (AmiiboState.fromValue(NativeLibrary.getVirtualAmiiboState())) {
|
||||
AmiiboState.TagNearby -> {
|
||||
NativeInput.onRemoveNfcTag()
|
||||
showAmiiboDialog(R.string.amiibo_removed_message)
|
||||
}
|
||||
|
||||
AmiiboState.WaitingForAmiibo -> {
|
||||
if (isAmiiboPickerOpen) {
|
||||
return true
|
||||
}
|
||||
|
||||
isAmiiboPickerOpen = true
|
||||
binding.drawerLayout.close()
|
||||
loadAmiiboLauncher.launch(AMIIBO_MIME_TYPES)
|
||||
}
|
||||
|
||||
else -> showAmiiboDialog(R.string.amiibo_wrong_state)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private fun handleAmiiboLoadResult(result: Int) {
|
||||
when (AmiiboLoadResult.fromValue(result)) {
|
||||
AmiiboLoadResult.Success -> {
|
||||
if (!isAdded) {
|
||||
return
|
||||
}
|
||||
Toast.makeText(
|
||||
requireContext(),
|
||||
getString(R.string.amiibo_load_success),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
|
||||
AmiiboLoadResult.UnableToLoad -> showAmiiboDialog(R.string.amiibo_in_use)
|
||||
AmiiboLoadResult.NotAnAmiibo -> showAmiiboDialog(R.string.amiibo_not_valid)
|
||||
AmiiboLoadResult.WrongDeviceState -> showAmiiboDialog(R.string.amiibo_wrong_state)
|
||||
AmiiboLoadResult.Unknown -> showAmiiboDialog(R.string.amiibo_unknown_error)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showAmiiboDialog(@StringRes messageRes: Int) {
|
||||
if (!isAdded) {
|
||||
return
|
||||
}
|
||||
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.amiibo_title)
|
||||
.setMessage(messageRes)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
if (this::emulationState.isInitialized) {
|
||||
if (emulationState.isRunning && emulationActivity?.isInPictureInPictureMode != true) {
|
||||
|
|
@ -906,6 +1008,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
|||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
_binding = null
|
||||
isAmiiboPickerOpen = false
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
|
|
@ -1739,7 +1842,34 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
|||
}
|
||||
}
|
||||
|
||||
private enum class AmiiboState(val value: Int) {
|
||||
Disabled(0),
|
||||
Initialized(1),
|
||||
WaitingForAmiibo(2),
|
||||
TagNearby(3);
|
||||
|
||||
companion object {
|
||||
fun fromValue(value: Int): AmiiboState =
|
||||
values().firstOrNull { it.value == value } ?: Disabled
|
||||
}
|
||||
}
|
||||
|
||||
private enum class AmiiboLoadResult(val value: Int) {
|
||||
Success(0),
|
||||
UnableToLoad(1),
|
||||
NotAnAmiibo(2),
|
||||
WrongDeviceState(3),
|
||||
Unknown(4);
|
||||
|
||||
companion object {
|
||||
fun fromValue(value: Int): AmiiboLoadResult =
|
||||
values().firstOrNull { it.value == value } ?: Unknown
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val AMIIBO_MIME_TYPES =
|
||||
arrayOf("application/octet-stream", "application/x-binary", "*/*")
|
||||
private val perfStatsUpdateHandler = Handler(Looper.myLooper()!!)
|
||||
private val socUpdateHandler = Handler(Looper.myLooper()!!)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,8 +7,10 @@
|
|||
|
||||
#include <codecvt>
|
||||
#include <locale>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
#include <dlfcn.h>
|
||||
|
||||
#ifdef ARCHITECTURE_arm64
|
||||
|
|
@ -67,6 +69,7 @@
|
|||
#include "hid_core/frontend/emulated_controller.h"
|
||||
#include "hid_core/hid_core.h"
|
||||
#include "hid_core/hid_types.h"
|
||||
#include "input_common/drivers/virtual_amiibo.h"
|
||||
#include "jni/native.h"
|
||||
#include "video_core/renderer_base.h"
|
||||
#include "video_core/renderer_vulkan/renderer_vulkan.h"
|
||||
|
|
@ -1005,6 +1008,44 @@ jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_areKeysPresent(JNIEnv* env, jobje
|
|||
return ContentManager::AreKeysPresent();
|
||||
}
|
||||
|
||||
jint Java_org_yuzu_yuzu_1emu_NativeLibrary_getVirtualAmiiboState(JNIEnv* env, jobject jobj) {
|
||||
if (!EmulationSession::GetInstance().IsRunning()) {
|
||||
return static_cast<jint>(InputCommon::VirtualAmiibo::State::Disabled);
|
||||
}
|
||||
|
||||
auto* virtual_amiibo =
|
||||
EmulationSession::GetInstance().GetInputSubsystem().GetVirtualAmiibo();
|
||||
if (virtual_amiibo == nullptr) {
|
||||
return static_cast<jint>(InputCommon::VirtualAmiibo::State::Disabled);
|
||||
}
|
||||
|
||||
return static_cast<jint>(virtual_amiibo->GetCurrentState());
|
||||
}
|
||||
|
||||
jint Java_org_yuzu_yuzu_1emu_NativeLibrary_loadAmiibo(JNIEnv* env, jobject jobj,
|
||||
jbyteArray jdata) {
|
||||
if (!EmulationSession::GetInstance().IsRunning() || jdata == nullptr) {
|
||||
return static_cast<jint>(InputCommon::VirtualAmiibo::Info::WrongDeviceState);
|
||||
}
|
||||
|
||||
auto* virtual_amiibo =
|
||||
EmulationSession::GetInstance().GetInputSubsystem().GetVirtualAmiibo();
|
||||
if (virtual_amiibo == nullptr) {
|
||||
return static_cast<jint>(InputCommon::VirtualAmiibo::Info::Unknown);
|
||||
}
|
||||
|
||||
const jsize length = env->GetArrayLength(jdata);
|
||||
std::vector<u8> bytes(static_cast<std::size_t>(length));
|
||||
if (length > 0) {
|
||||
env->GetByteArrayRegion(jdata, 0, length,
|
||||
reinterpret_cast<jbyte*>(bytes.data()));
|
||||
}
|
||||
|
||||
const auto info =
|
||||
virtual_amiibo->LoadAmiibo(std::span<u8>(bytes.data(), bytes.size()));
|
||||
return static_cast<jint>(info);
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_org_yuzu_yuzu_1emu_NativeLibrary_initMultiplayer(
|
||||
JNIEnv* env, [[maybe_unused]] jobject obj) {
|
||||
|
|
|
|||
|
|
@ -33,6 +33,11 @@
|
|||
android:icon="@drawable/ic_two_users"
|
||||
android:title="@string/multiplayer" />
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_load_amiibo"
|
||||
android:icon="@drawable/ic_nfc"
|
||||
android:title="@string/load_amiibo" />
|
||||
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_overlay_controls"
|
||||
|
|
|
|||
|
|
@ -803,11 +803,21 @@
|
|||
<string name="emulation_pause">Pause emulation</string>
|
||||
<string name="emulation_unpause">Unpause emulation</string>
|
||||
<string name="emulation_input_overlay">Overlay options</string>
|
||||
<string name="load_amiibo">Load Amiibo</string>
|
||||
<string name="touchscreen">Touchscreen</string>
|
||||
<string name="lock_drawer">Lock drawer</string>
|
||||
<string name="unlock_drawer">Unlock drawer</string>
|
||||
<string name="reset">Reset</string>
|
||||
|
||||
<!-- Amiibo -->
|
||||
<string name="amiibo_title">Amiibo</string>
|
||||
<string name="amiibo_removed_message">The current amiibo has been removed</string>
|
||||
<string name="amiibo_wrong_state">The current game is not looking for amiibo</string>
|
||||
<string name="amiibo_not_valid">The selected file is not a valid amiibo</string>
|
||||
<string name="amiibo_in_use">The selected file is already in use</string>
|
||||
<string name="amiibo_unknown_error">An unknown error occurred</string>
|
||||
<string name="amiibo_load_success">Amiibo loaded</string>
|
||||
|
||||
|
||||
<!-- Software keyboard -->
|
||||
<string name="software_keyboard">Software keyboard</string>
|
||||
|
|
|
|||
Loading…
Reference in New Issue