From de33c7fa2b2b97ca8c5fa9abec8190283afe55d6 Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Tue, 6 Oct 2020 23:07:49 +1000 Subject: [PATCH] Android: Implement cheats --- .../app/src/cpp/android_host_interface.cpp | 69 +++++++++---- .../src/cpp/android_settings_interface.cpp | 3 +- .../duckstation/AndroidHostInterface.java | 3 + .../github/stenzek/duckstation/CheatCode.java | 17 ++++ .../duckstation/EmulationActivity.java | 99 ++++++++++++++++--- android/app/src/main/res/values/arrays.xml | 9 +- .../app/src/main/res/xml/root_preferences.xml | 6 ++ 7 files changed, 170 insertions(+), 36 deletions(-) create mode 100644 android/app/src/main/java/com/github/stenzek/duckstation/CheatCode.java diff --git a/android/app/src/cpp/android_host_interface.cpp b/android/app/src/cpp/android_host_interface.cpp index 37013605b..1bf479e96 100644 --- a/android/app/src/cpp/android_host_interface.cpp +++ b/android/app/src/cpp/android_host_interface.cpp @@ -4,6 +4,7 @@ #include "common/log.h" #include "common/string.h" #include "common/timestamp.h" +#include "core/cheats.h" #include "core/controller.h" #include "core/gpu.h" #include "core/host_display.h" @@ -28,6 +29,8 @@ static jmethodID s_EmulationActivity_method_reportMessage; static jmethodID s_EmulationActivity_method_onEmulationStarted; static jmethodID s_EmulationActivity_method_onEmulationStopped; static jmethodID s_EmulationActivity_method_onGameTitleChanged; +static jclass s_CheatCode_class; +static jmethodID s_CheatCode_constructor; namespace AndroidHelpers { // helper for retrieving the current per-thread jni environment @@ -186,9 +189,7 @@ bool AndroidHostInterface::StartEmulationThread(jobject emulation_activity, ANat void AndroidHostInterface::PauseEmulationThread(bool paused) { Assert(IsEmulationThreadRunning()); - RunOnEmulationThread([this, paused]() { - PauseSystem(paused); - }); + RunOnEmulationThread([this, paused]() { PauseSystem(paused); }); } void AndroidHostInterface::StopEmulationThread() @@ -291,8 +292,10 @@ void AndroidHostInterface::EmulationThreadLoop() // run any events { std::unique_lock lock(m_mutex); - for (;;) { - while (!m_callback_queue.empty()) { + for (;;) + { + while (!m_callback_queue.empty()) + { auto callback = std::move(m_callback_queue.front()); m_callback_queue.pop_front(); lock.unlock(); @@ -303,10 +306,13 @@ void AndroidHostInterface::EmulationThreadLoop() if (m_emulation_thread_stop_request.load()) return; - if (System::IsPaused()) { + if (System::IsPaused()) + { // paused, wait for us to resume m_sleep_cv.wait(lock); - } else { + } + else + { // done with callbacks, run the frame break; } @@ -511,21 +517,19 @@ extern "C" jint JNI_OnLoad(JavaVM* vm, void* reserved) Log::SetDebugOutputParams(true, nullptr, LOGLEVEL_DEV); s_jvm = vm; + // Create global reference so it doesn't get cleaned up. JNIEnv* env = AndroidHelpers::GetJNIEnv(); - if ((s_AndroidHostInterface_class = env->FindClass("com/github/stenzek/duckstation/AndroidHostInterface")) == nullptr) + if ((s_AndroidHostInterface_class = env->FindClass("com/github/stenzek/duckstation/AndroidHostInterface")) == + nullptr || + (s_AndroidHostInterface_class = static_cast(env->NewGlobalRef(s_AndroidHostInterface_class))) == + nullptr || + (s_CheatCode_class = env->FindClass("com/github/stenzek/duckstation/CheatCode")) == nullptr || + (s_CheatCode_class = static_cast(env->NewGlobalRef(s_CheatCode_class))) == nullptr) { Log_ErrorPrint("AndroidHostInterface class lookup failed"); return -1; } - // Create global reference so it doesn't get cleaned up. - s_AndroidHostInterface_class = static_cast(env->NewGlobalRef(s_AndroidHostInterface_class)); - if (!s_AndroidHostInterface_class) - { - Log_ErrorPrint("Failed to get reference to AndroidHostInterface"); - return -1; - } - jclass emulation_activity_class; if ((s_AndroidHostInterface_constructor = env->GetMethodID(s_AndroidHostInterface_class, "", "(Landroid/content/Context;)V")) == nullptr || @@ -545,7 +549,8 @@ extern "C" jint JNI_OnLoad(JavaVM* vm, void* reserved) (s_EmulationActivity_method_onEmulationStopped = env->GetMethodID(emulation_activity_class, "onEmulationStopped", "()V")) == nullptr || (s_EmulationActivity_method_onGameTitleChanged = - env->GetMethodID(emulation_activity_class, "onGameTitleChanged", "(Ljava/lang/String;)V")) == nullptr) + env->GetMethodID(emulation_activity_class, "onGameTitleChanged", "(Ljava/lang/String;)V")) == nullptr || + (s_CheatCode_constructor = env->GetMethodID(s_CheatCode_class, "", "(ILjava/lang/String;Z)V")) == nullptr) { Log_ErrorPrint("AndroidHostInterface lookups failed"); return -1; @@ -770,7 +775,8 @@ DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_saveResumeState, jobject obj, DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_setDisplayAlignment, jobject obj, jint alignment) { AndroidHostInterface* hi = AndroidHelpers::GetNativeClass(env, obj); - hi->RunOnEmulationThread([hi, alignment]() { hi->GetDisplay()->SetDisplayAlignment(static_cast(alignment)); }); + hi->RunOnEmulationThread( + [hi, alignment]() { hi->GetDisplay()->SetDisplayAlignment(static_cast(alignment)); }); } DEFINE_JNI_ARGS_METHOD(bool, AndroidHostInterface_hasSurface, jobject obj) @@ -795,5 +801,32 @@ DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_pauseEmulationThread, jobject hi->PauseEmulationThread(paused); } +DEFINE_JNI_ARGS_METHOD(jobject, AndroidHostInterface_getCheatList, jobject obj) +{ + if (!System::IsValid() || !System::HasCheatList()) + return nullptr; + CheatList* cl = System::GetCheatList(); + const u32 count = cl->GetCodeCount(); + jobjectArray arr = env->NewObjectArray(count, s_CheatCode_class, nullptr); + for (u32 i = 0; i < count; i++) + { + const CheatCode& cc = cl->GetCode(i); + + jobject java_cc = env->NewObject(s_CheatCode_class, s_CheatCode_constructor, static_cast(i), + env->NewStringUTF(cc.description.c_str()), cc.enabled); + env->SetObjectArrayElement(arr, i, java_cc); + } + + return arr; +} + +DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_setCheatEnabled, jobject obj, jint index, jboolean enabled) +{ + if (!System::IsValid() || !System::HasCheatList()) + return; + + AndroidHostInterface* hi = AndroidHelpers::GetNativeClass(env, obj); + hi->RunOnEmulationThread([index, enabled, hi]() { hi->SetCheatCodeState(static_cast(index), enabled, true); }); +} diff --git a/android/app/src/cpp/android_settings_interface.cpp b/android/app/src/cpp/android_settings_interface.cpp index 53d801019..7bef4379c 100644 --- a/android/app/src/cpp/android_settings_interface.cpp +++ b/android/app/src/cpp/android_settings_interface.cpp @@ -141,7 +141,8 @@ void AndroidSettingsInterface::DeleteValue(const char* section, const char* key) std::vector AndroidSettingsInterface::GetStringList(const char* section, const char* key) { JNIEnv* env = AndroidHelpers::GetJNIEnv(); - jobject values_set = env->CallObjectMethod(m_java_shared_preferences, m_get_string_set, env->NewStringUTF(GetSettingKey(section, key)), nullptr); + jobject values_set = env->CallObjectMethod(m_java_shared_preferences, m_get_string_set, + env->NewStringUTF(GetSettingKey(section, key)), nullptr); if (!values_set) return {}; diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/AndroidHostInterface.java b/android/app/src/main/java/com/github/stenzek/duckstation/AndroidHostInterface.java index d6a015a7b..9e93f1c49 100644 --- a/android/app/src/main/java/com/github/stenzek/duckstation/AndroidHostInterface.java +++ b/android/app/src/main/java/com/github/stenzek/duckstation/AndroidHostInterface.java @@ -71,6 +71,9 @@ public class AndroidHostInterface { public native void setDisplayAlignment(int alignment); + public native CheatCode[] getCheatList(); + public native void setCheatEnabled(int index, boolean enabled); + static { System.loadLibrary("duckstation-native"); } diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/CheatCode.java b/android/app/src/main/java/com/github/stenzek/duckstation/CheatCode.java new file mode 100644 index 000000000..d40a5f4e9 --- /dev/null +++ b/android/app/src/main/java/com/github/stenzek/duckstation/CheatCode.java @@ -0,0 +1,17 @@ +package com.github.stenzek.duckstation; + +public class CheatCode { + private int mIndex; + private String mName; + private boolean mEnabled; + + public CheatCode(int index, String name, boolean enabled) { + mIndex = index; + mName = name; + mEnabled = enabled; + } + + public int getIndex() { return mIndex; } + public String getName() { return mName; } + public boolean isEnabled() { return mEnabled; } +} diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/EmulationActivity.java b/android/app/src/main/java/com/github/stenzek/duckstation/EmulationActivity.java index 0a98cdc85..b9374bcaf 100644 --- a/android/app/src/main/java/com/github/stenzek/duckstation/EmulationActivity.java +++ b/android/app/src/main/java/com/github/stenzek/duckstation/EmulationActivity.java @@ -280,18 +280,7 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde return; } - case 2: // Reset - { - AndroidHostInterface.getInstance().resetSystem(); - return; - } - - case 3: // Change Disc - { - return; - } - - case 4: // Toggle Speed Limiter + case 2: // Toggle Speed Limiter { boolean newSetting = !getBooleanSetting("Main/SpeedLimiterEnabled", true); setBooleanSetting("Main/SpeedLimiterEnabled", newSetting); @@ -299,13 +288,63 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde return; } - case 5: // Toggle Touchscreen Controller + case 3: // More Options + { + showMoreMenu(); + return; + } + + case 4: // Quit + { + finish(); + return; + } + } + } + }); + builder.setOnDismissListener(new DialogInterface.OnDismissListener() { + @Override + public void onDismiss(DialogInterface dialogInterface) { + enableFullscreenImmersive(); + } + }); + builder.create().show(); + } + + private void showMoreMenu() { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + if (mGameTitle != null && !mGameTitle.isEmpty()) + builder.setTitle(mGameTitle); + + builder.setItems(R.array.emulation_more_menu, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + switch (i) + { + case 0: // Reset + { + AndroidHostInterface.getInstance().resetSystem(); + return; + } + + case 1: // Cheats + { + showCheatsMenu(); + return; + } + + case 2: // Change Disc + { + return; + } + + case 3: // Toggle Touchscreen Controller { setTouchscreenControllerVisibility(!mTouchscreenControllerVisible); return; } - case 6: // Settings + case 4: // Settings { Intent intent = new Intent(EmulationActivity.this, SettingsActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); @@ -313,7 +352,7 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde return; } - case 7: // Quit + case 5: // Quit { finish(); return; @@ -330,6 +369,36 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde builder.create().show(); } + private void showCheatsMenu() { + final CheatCode[] cheats = AndroidHostInterface.getInstance().getCheatList(); + if (cheats == null) { + Toast.makeText(this, "No cheats are loaded.", Toast.LENGTH_LONG); + return; + } + + AlertDialog.Builder builder = new AlertDialog.Builder(this); + + CharSequence[] items = new CharSequence[cheats.length]; + for (int i = 0; i < cheats.length; i++) { + final CheatCode cc = cheats[i]; + items[i] = String.format("%s %s", cc.isEnabled() ? "(ON)" : "(OFF)", cc.getName()); + } + + builder.setItems(items, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + AndroidHostInterface.getInstance().setCheatEnabled(i, !cheats[i].isEnabled()); + } + }); + builder.setOnDismissListener(new DialogInterface.OnDismissListener() { + @Override + public void onDismiss(DialogInterface dialogInterface) { + enableFullscreenImmersive(); + } + }); + builder.create().show(); + } + /** * Touchscreen controller overlay */ diff --git a/android/app/src/main/res/values/arrays.xml b/android/app/src/main/res/values/arrays.xml index eaf0bc386..13e3b9376 100644 --- a/android/app/src/main/res/values/arrays.xml +++ b/android/app/src/main/res/values/arrays.xml @@ -128,9 +128,14 @@ Quick Load Quick Save - Reset - Change Disc Toggle Speed Limiter + More Options + Quit + + + Reset + Cheats + Change Disc Toggle Touch Controller Settings Quit diff --git a/android/app/src/main/res/xml/root_preferences.xml b/android/app/src/main/res/xml/root_preferences.xml index 2bb0fd050..0fa2bc5cd 100644 --- a/android/app/src/main/res/xml/root_preferences.xml +++ b/android/app/src/main/res/xml/root_preferences.xml @@ -30,6 +30,12 @@ app:defaultValue="true" app:summary="Automatically saves the emulator state when powering down or exiting. You can then resume directly from where you left off next time." app:iconSpaceReserved="false" /> +