diff --git a/src/android/app/src/main/java/org/citra/citra_emu/applets/MiiSelector.java b/src/android/app/src/main/java/org/citra/citra_emu/applets/MiiSelector.java new file mode 100644 index 000000000..858eab1c9 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/applets/MiiSelector.java @@ -0,0 +1,122 @@ +// Copyright 2020 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +package org.citra.citra_emu.applets; + +import android.app.Activity; +import android.app.Dialog; +import android.content.DialogInterface; +import android.os.Bundle; + +import org.citra.citra_emu.NativeLibrary; +import org.citra.citra_emu.R; +import org.citra.citra_emu.activities.EmulationActivity; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Objects; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.DialogFragment; + +public final class MiiSelector { + public static class MiiSelectorConfig implements java.io.Serializable { + public boolean enable_cancel_button; + public String title; + public long initially_selected_mii_index; + // List of Miis to display + public String[] mii_names; + } + + public static class MiiSelectorData { + public long return_code; + public int index; + + private MiiSelectorData(long return_code, int index) { + this.return_code = return_code; + this.index = index; + } + } + + public static class MiiSelectorDialogFragment extends DialogFragment { + static MiiSelectorDialogFragment newInstance(MiiSelectorConfig config) { + MiiSelectorDialogFragment frag = new MiiSelectorDialogFragment(); + Bundle args = new Bundle(); + args.putSerializable("config", config); + frag.setArguments(args); + return frag; + } + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final Activity emulationActivity = Objects.requireNonNull(getActivity()); + + MiiSelectorConfig config = + Objects.requireNonNull((MiiSelectorConfig)Objects.requireNonNull(getArguments()) + .getSerializable("config")); + + // Note: we intentionally leave out the Standard Mii in the native code so that + // the string can get translated + ArrayList list = new ArrayList<>(); + list.add(emulationActivity.getString(R.string.standard_mii)); + list.addAll(Arrays.asList(config.mii_names)); + + final int initialIndex = config.initially_selected_mii_index < list.size() + ? (int)config.initially_selected_mii_index + : 0; + data.index = initialIndex; + AlertDialog.Builder builder = + new AlertDialog.Builder(emulationActivity) + .setTitle(config.title.isEmpty() + ? emulationActivity.getString(R.string.mii_selector) + : config.title) + .setSingleChoiceItems(list.toArray(new String[] {}), initialIndex, + (dialog, which) -> { data.index = which; }) + .setPositiveButton(android.R.string.ok, (dialog, which) -> { + data.return_code = 0; + synchronized (finishLock) { + finishLock.notifyAll(); + } + }); + if (config.enable_cancel_button) { + builder.setNegativeButton(android.R.string.cancel, (dialog, which) -> { + data.return_code = 1; + synchronized (finishLock) { + finishLock.notifyAll(); + } + }); + } + setCancelable(false); + return builder.create(); + } + } + + private static MiiSelectorData data; + private static final Object finishLock = new Object(); + + private static void ExecuteImpl(MiiSelectorConfig config) { + final EmulationActivity emulationActivity = NativeLibrary.sEmulationActivity.get(); + + data = new MiiSelectorData(0, 0); + + MiiSelectorDialogFragment fragment = MiiSelectorDialogFragment.newInstance(config); + fragment.show(emulationActivity.getSupportFragmentManager(), "mii_selector"); + } + + public static MiiSelectorData Execute(MiiSelectorConfig config) { + NativeLibrary.sEmulationActivity.get().runOnUiThread(() -> ExecuteImpl(config)); + + synchronized (finishLock) { + try { + finishLock.wait(); + } catch (Exception ignored) { + } + } + + return data; + } +} diff --git a/src/android/app/src/main/jni/CMakeLists.txt b/src/android/app/src/main/jni/CMakeLists.txt index c86d1a686..8fe02f210 100644 --- a/src/android/app/src/main/jni/CMakeLists.txt +++ b/src/android/app/src/main/jni/CMakeLists.txt @@ -1,4 +1,6 @@ add_library(main SHARED + applets/mii_selector.cpp + applets/mii_selector.h applets/swkbd.cpp applets/swkbd.h button_manager.cpp diff --git a/src/android/app/src/main/jni/applets/mii_selector.cpp b/src/android/app/src/main/jni/applets/mii_selector.cpp new file mode 100644 index 000000000..95490c7a2 --- /dev/null +++ b/src/android/app/src/main/jni/applets/mii_selector.cpp @@ -0,0 +1,89 @@ +// Copyright 2020 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include "common/string_util.h" +#include "jni/applets/mii_selector.h" +#include "jni/id_cache.h" + +static jclass s_mii_selector_class; +static jclass s_mii_selector_config_class; +static jclass s_mii_selector_data_class; +static jmethodID s_mii_selector_execute; + +namespace MiiSelector { + +AndroidMiiSelector::~AndroidMiiSelector() = default; + +void AndroidMiiSelector::Setup(const Frontend::MiiSelectorConfig& config) { + JNIEnv* env = IDCache::GetEnvForThread(); + auto miis = Frontend::LoadMiis(); + + // Create the Java MiiSelectorConfig object + jobject java_config = env->AllocObject(s_mii_selector_config_class); + env->SetBooleanField(java_config, + env->GetFieldID(s_mii_selector_config_class, "enable_cancel_button", "Z"), + static_cast(config.enable_cancel_button)); + env->SetObjectField(java_config, + env->GetFieldID(s_mii_selector_config_class, "title", "Ljava/lang/String;"), + env->NewStringUTF(config.title.c_str())); + env->SetLongField( + java_config, + env->GetFieldID(s_mii_selector_config_class, "initially_selected_mii_index", "J"), + static_cast(config.initially_selected_mii_index)); + + // List mii names + // The 'Standard Mii' is not included here as we need Java side to translate it + const jclass string_class = + reinterpret_cast(env->NewGlobalRef(env->FindClass("java/lang/String"))); + const jobjectArray array = + env->NewObjectArray(static_cast(miis.size()), string_class, nullptr); + for (std::size_t i = 0; i < miis.size(); ++i) { + const auto name = Common::UTF16BufferToUTF8(miis[i].mii_name); + env->SetObjectArrayElement(array, static_cast(i), env->NewStringUTF(name.c_str())); + } + env->SetObjectField( + java_config, + env->GetFieldID(s_mii_selector_config_class, "mii_names", "[Ljava/lang/String;"), array); + env->DeleteGlobalRef(string_class); + + // Invoke backend Execute method + jobject data = + env->CallStaticObjectMethod(s_mii_selector_class, s_mii_selector_execute, java_config); + + const u32 return_code = static_cast( + env->GetLongField(data, env->GetFieldID(s_mii_selector_data_class, "return_code", "J"))); + if (return_code == 1) { + Finalize(return_code, HLE::Applets::MiiData{}); + return; + } + + const int index = static_cast( + env->GetIntField(data, env->GetFieldID(s_mii_selector_data_class, "index", "I"))); + ASSERT_MSG(index >= 0 && index <= miis.size(), "Index returned is out of bound"); + Finalize(return_code, index == 0 + ? HLE::Applets::MiiSelector::GetStandardMiiResult().selected_mii_data + : miis.at(static_cast(index - 1))); +} + +void InitJNI(JNIEnv* env) { + s_mii_selector_class = reinterpret_cast( + env->NewGlobalRef(env->FindClass("org/citra/citra_emu/applets/MiiSelector"))); + s_mii_selector_config_class = reinterpret_cast(env->NewGlobalRef( + env->FindClass("org/citra/citra_emu/applets/MiiSelector$MiiSelectorConfig"))); + s_mii_selector_data_class = reinterpret_cast(env->NewGlobalRef( + env->FindClass("org/citra/citra_emu/applets/MiiSelector$MiiSelectorData"))); + + s_mii_selector_execute = + env->GetStaticMethodID(s_mii_selector_class, "Execute", + "(Lorg/citra/citra_emu/applets/MiiSelector$MiiSelectorConfig;)Lorg/" + "citra/citra_emu/applets/MiiSelector$MiiSelectorData;"); +} + +void CleanupJNI(JNIEnv* env) { + env->DeleteGlobalRef(s_mii_selector_class); + env->DeleteGlobalRef(s_mii_selector_config_class); + env->DeleteGlobalRef(s_mii_selector_data_class); +} + +} // namespace MiiSelector diff --git a/src/android/app/src/main/jni/applets/mii_selector.h b/src/android/app/src/main/jni/applets/mii_selector.h new file mode 100644 index 000000000..f33d1cb8d --- /dev/null +++ b/src/android/app/src/main/jni/applets/mii_selector.h @@ -0,0 +1,25 @@ +// Copyright 2020 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include "core/frontend/applets/mii_selector.h" + +namespace MiiSelector { + +class AndroidMiiSelector final : public Frontend::MiiSelector { +public: + ~AndroidMiiSelector(); + + void Setup(const Frontend::MiiSelectorConfig& config) override; +}; + +// Should be called in JNI_Load +void InitJNI(JNIEnv* env); + +// Should be called in JNI_Unload +void CleanupJNI(JNIEnv* env); + +} // namespace MiiSelector diff --git a/src/android/app/src/main/jni/id_cache.cpp b/src/android/app/src/main/jni/id_cache.cpp index 9f0130fb2..2ed4bed57 100644 --- a/src/android/app/src/main/jni/id_cache.cpp +++ b/src/android/app/src/main/jni/id_cache.cpp @@ -7,6 +7,7 @@ #include "common/logging/filter.h" #include "common/logging/log.h" #include "core/settings.h" +#include "jni/applets/mii_selector.h" #include "jni/applets/swkbd.h" #include "jni/camera/still_image_camera.h" #include "jni/id_cache.h" @@ -120,6 +121,7 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) { s_request_camera_permission = env->GetStaticMethodID(s_native_library_class, "RequestCameraPermission", "()Z"); + MiiSelector::InitJNI(env); SoftwareKeyboard::InitJNI(env); Camera::StillImage::InitJNI(env); @@ -133,6 +135,7 @@ void JNI_OnUnload(JavaVM* vm, void* reserved) { } env->DeleteGlobalRef(s_native_library_class); + MiiSelector::CleanupJNI(env); SoftwareKeyboard::CleanupJNI(env); Camera::StillImage::CleanupJNI(env); } diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp index b86717cc9..b6b75cd1d 100644 --- a/src/android/app/src/main/jni/native.cpp +++ b/src/android/app/src/main/jni/native.cpp @@ -21,6 +21,7 @@ #include "core/hle/service/am/am.h" #include "core/hle/service/nfc/nfc.h" #include "core/settings.h" +#include "jni/applets/mii_selector.h" #include "jni/applets/swkbd.h" #include "jni/button_manager.h" #include "jni/camera/ndk_camera.h" @@ -135,6 +136,7 @@ static Core::System::ResultStatus RunCitra(const std::string& filepath) { // Register frontend applets Frontend::RegisterDefaultApplets(); + system.RegisterMiiSelector(std::make_shared()); system.RegisterSoftwareKeyboard(std::make_shared()); InputManager::Init(); diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index b220de67e..e9ee26bb6 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -168,6 +168,10 @@ Blank input is not allowed Empty input is not allowed + + Mii Selector + Standard Mii + Select Image Camera diff --git a/src/citra_qt/applets/mii_selector.cpp b/src/citra_qt/applets/mii_selector.cpp index 3ee25805f..2099e675e 100644 --- a/src/citra_qt/applets/mii_selector.cpp +++ b/src/citra_qt/applets/mii_selector.cpp @@ -8,11 +8,7 @@ #include #include #include "citra_qt/applets/mii_selector.h" -#include "common/file_util.h" #include "common/string_util.h" -#include "core/file_sys/archive_extsavedata.h" -#include "core/file_sys/file_backend.h" -#include "core/hle/service/ptm/ptm.h" QtMiiSelectorDialog::QtMiiSelectorDialog(QWidget* parent, QtMiiSelector* mii_selector_) : QDialog(parent), mii_selector(mii_selector_) { @@ -33,37 +29,9 @@ QtMiiSelectorDialog::QtMiiSelectorDialog(QWidget* parent, QtMiiSelector* mii_sel miis.push_back(HLE::Applets::MiiSelector::GetStandardMiiResult().selected_mii_data); combobox->addItem(tr("Standard Mii")); - - std::string nand_directory{FileUtil::GetUserPath(FileUtil::UserPath::NANDDir)}; - FileSys::ArchiveFactory_ExtSaveData extdata_archive_factory(nand_directory, true); - - auto archive_result = extdata_archive_factory.Open(Service::PTM::ptm_shared_extdata_id, 0); - if (archive_result.Succeeded()) { - auto archive = std::move(archive_result).Unwrap(); - - FileSys::Path file_path = "/CFL_DB.dat"; - FileSys::Mode mode{}; - mode.read_flag.Assign(1); - - auto file_result = archive->OpenFile(file_path, mode); - if (file_result.Succeeded()) { - auto file = std::move(file_result).Unwrap(); - - u32 saved_miis_offset = 0x8; - // The Mii Maker has a 100 Mii limit on the 3ds - for (int i = 0; i < 100; ++i) { - HLE::Applets::MiiData mii; - std::array mii_raw; - file->Read(saved_miis_offset, sizeof(mii), mii_raw.data()); - std::memcpy(&mii, mii_raw.data(), sizeof(mii)); - if (mii.mii_id != 0) { - std::string name = Common::UTF16BufferToUTF8(mii.mii_name); - miis.push_back(mii); - combobox->addItem(QString::fromStdString(name)); - } - saved_miis_offset += sizeof(mii); - } - } + for (const auto& mii : Frontend::LoadMiis()) { + miis.push_back(mii); + combobox->addItem(QString::fromStdString(Common::UTF16BufferToUTF8(mii.mii_name))); } if (combobox->count() > static_cast(config.initially_selected_mii_index)) { diff --git a/src/core/frontend/applets/mii_selector.cpp b/src/core/frontend/applets/mii_selector.cpp index 2ca23f1db..2fdfe3049 100644 --- a/src/core/frontend/applets/mii_selector.cpp +++ b/src/core/frontend/applets/mii_selector.cpp @@ -2,7 +2,12 @@ // Licensed under GPLv2 or any later version // Refer to the license.txt file included. +#include "common/file_util.h" +#include "common/string_util.h" +#include "core/file_sys/archive_extsavedata.h" +#include "core/file_sys/file_backend.h" #include "core/frontend/applets/mii_selector.h" +#include "core/hle/service/ptm/ptm.h" namespace Frontend { @@ -10,6 +15,42 @@ void MiiSelector::Finalize(u32 return_code, HLE::Applets::MiiData mii) { data = {return_code, mii}; } +std::vector LoadMiis() { + std::vector miis; + + std::string nand_directory{FileUtil::GetUserPath(FileUtil::UserPath::NANDDir)}; + FileSys::ArchiveFactory_ExtSaveData extdata_archive_factory(nand_directory, true); + + auto archive_result = extdata_archive_factory.Open(Service::PTM::ptm_shared_extdata_id, 0); + if (archive_result.Succeeded()) { + auto archive = std::move(archive_result).Unwrap(); + + FileSys::Path file_path = "/CFL_DB.dat"; + FileSys::Mode mode{}; + mode.read_flag.Assign(1); + + auto file_result = archive->OpenFile(file_path, mode); + if (file_result.Succeeded()) { + auto file = std::move(file_result).Unwrap(); + + u32 saved_miis_offset = 0x8; + // The Mii Maker has a 100 Mii limit on the 3ds + for (int i = 0; i < 100; ++i) { + HLE::Applets::MiiData mii; + std::array mii_raw; + file->Read(saved_miis_offset, sizeof(mii), mii_raw.data()); + std::memcpy(&mii, mii_raw.data(), sizeof(mii)); + if (mii.mii_id != 0) { + miis.push_back(mii); + } + saved_miis_offset += sizeof(mii); + } + } + } + + return miis; +} + void DefaultMiiSelector::Setup(const Frontend::MiiSelectorConfig& config) { MiiSelector::Setup(config); Finalize(0, HLE::Applets::MiiSelector::GetStandardMiiResult().selected_mii_data); diff --git a/src/core/frontend/applets/mii_selector.h b/src/core/frontend/applets/mii_selector.h index ae63e359a..0886fe513 100644 --- a/src/core/frontend/applets/mii_selector.h +++ b/src/core/frontend/applets/mii_selector.h @@ -49,6 +49,8 @@ protected: MiiSelectorData data; }; +std::vector LoadMiis(); + class DefaultMiiSelector final : public MiiSelector { public: void Setup(const MiiSelectorConfig& config) override;