From 74942443d3106c87259c0cd7ca1b684898126993 Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Sat, 7 Nov 2020 22:04:07 +1000 Subject: [PATCH] Android: Add patch code (cheat) import --- .../app/src/cpp/android_host_interface.cpp | 73 +++++++++++++++---- android/app/src/cpp/android_host_interface.h | 2 + .../duckstation/AndroidHostInterface.java | 6 +- .../duckstation/EmulationActivity.java | 61 ++++++++++++---- .../github/stenzek/duckstation/FileUtil.java | 39 ++++++++++ .../{CheatCode.java => PatchCode.java} | 12 +-- android/app/src/main/res/values/arrays.xml | 2 +- .../src/main/res/xml/general_preferences.xml | 4 +- 8 files changed, 161 insertions(+), 38 deletions(-) rename android/app/src/main/java/com/github/stenzek/duckstation/{CheatCode.java => PatchCode.java} (55%) diff --git a/android/app/src/cpp/android_host_interface.cpp b/android/app/src/cpp/android_host_interface.cpp index 66e58623f..b110196c3 100644 --- a/android/app/src/cpp/android_host_interface.cpp +++ b/android/app/src/cpp/android_host_interface.cpp @@ -38,8 +38,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; +static jclass s_PatchCode_class; +static jmethodID s_PatchCode_constructor; namespace AndroidHelpers { // helper for retrieving the current per-thread jni environment @@ -178,7 +178,8 @@ void AndroidHostInterface::LoadAndConvertSettings() g_settings.gpu_per_sample_shading = StringUtil::EndsWith(msaa_str, "-ssaa"); // turn percentage into fraction for overclock - const u32 overclock_percent = static_cast(std::max(m_settings_interface.GetIntValue("CPU", "Overclock", 100), 1)); + const u32 overclock_percent = + static_cast(std::max(m_settings_interface.GetIntValue("CPU", "Overclock", 100), 1)); Settings::CPUOverclockPercentToFraction(overclock_percent, &g_settings.cpu_overclock_numerator, &g_settings.cpu_overclock_denominator); g_settings.cpu_overclock_enable = (overclock_percent != 100); @@ -329,8 +330,7 @@ void AndroidHostInterface::EmulationThreadLoop() lock.unlock(); callback(); lock.lock(); - } - while (!m_callback_queue.empty()); + } while (!m_callback_queue.empty()); m_callbacks_outstanding.store(false); } @@ -583,6 +583,34 @@ void AndroidHostInterface::ApplySettings(bool display_osd_messages) CheckForSettingsChanges(old_settings); } +bool AndroidHostInterface::ImportPatchCodesFromString(const std::string& str) +{ + CheatList* cl = new CheatList(); + if (!cl->LoadFromString(str, CheatList::Format::Autodetect) || cl->GetCodeCount() == 0) + return false; + + RunOnEmulationThread([this, cl]() { + u32 imported_count; + if (!System::HasCheatList()) + { + imported_count = cl->GetCodeCount(); + System::SetCheatList(std::unique_ptr(cl)); + } + else + { + const u32 old_count = System::GetCheatList()->GetCodeCount(); + System::GetCheatList()->MergeList(*cl); + imported_count = System::GetCheatList()->GetCodeCount() - old_count; + delete cl; + } + + AddFormattedOSDMessage(20.0f, "Imported %u patch codes.", imported_count); + CommonHostInterface::SaveCheatList(); + }); + + return true; +} + extern "C" jint JNI_OnLoad(JavaVM* vm, void* reserved) { Log::SetDebugOutputParams(true, nullptr, LOGLEVEL_DEV); @@ -594,8 +622,8 @@ extern "C" jint JNI_OnLoad(JavaVM* vm, void* reserved) 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) + (s_PatchCode_class = env->FindClass("com/github/stenzek/duckstation/PatchCode")) == nullptr || + (s_PatchCode_class = static_cast(env->NewGlobalRef(s_PatchCode_class))) == nullptr) { Log_ErrorPrint("AndroidHostInterface class lookup failed"); return -1; @@ -621,7 +649,7 @@ extern "C" jint JNI_OnLoad(JavaVM* vm, void* reserved) env->GetMethodID(emulation_activity_class, "onEmulationStopped", "()V")) == nullptr || (s_EmulationActivity_method_onGameTitleChanged = 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) + (s_PatchCode_constructor = env->GetMethodID(s_PatchCode_class, "", "(ILjava/lang/String;Z)V")) == nullptr) { Log_ErrorPrint("AndroidHostInterface lookups failed"); return -1; @@ -878,20 +906,30 @@ DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_pauseEmulationThread, jobject hi->PauseEmulationThread(paused); } -DEFINE_JNI_ARGS_METHOD(jobject, AndroidHostInterface_getCheatList, jobject obj) +DEFINE_JNI_ARGS_METHOD(jobject, AndroidHostInterface_getPatchCodeList, jobject obj) { - if (!System::IsValid() || !System::HasCheatList()) + if (!System::IsValid()) + return nullptr; + + AndroidHostInterface* hi = AndroidHelpers::GetNativeClass(env, obj); + if (!System::HasCheatList() && !g_settings.auto_load_cheats) + { + // Hopefully this won't deadlock... + hi->RunOnEmulationThread([hi]() { hi->LoadCheatListFromGameTitle(); }, true); + } + + if (!System::HasCheatList()) return nullptr; CheatList* cl = System::GetCheatList(); const u32 count = cl->GetCodeCount(); - jobjectArray arr = env->NewObjectArray(count, s_CheatCode_class, nullptr); + jobjectArray arr = env->NewObjectArray(count, s_PatchCode_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), + jobject java_cc = env->NewObject(s_PatchCode_class, s_PatchCode_constructor, static_cast(i), env->NewStringUTF(cc.description.c_str()), cc.enabled); env->SetObjectArrayElement(arr, i, java_cc); } @@ -899,7 +937,16 @@ DEFINE_JNI_ARGS_METHOD(jobject, AndroidHostInterface_getCheatList, jobject obj) return arr; } -DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_setCheatEnabled, jobject obj, jint index, jboolean enabled) +DEFINE_JNI_ARGS_METHOD(jboolean, AndroidHostInterface_importPatchCodesFromString, jobject obj, jstring str) +{ + if (!System::IsValid()) + return false; + + AndroidHostInterface* hi = AndroidHelpers::GetNativeClass(env, obj); + return hi->ImportPatchCodesFromString(AndroidHelpers::JStringToString(env, str)); +} + +DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_setPatchCodeEnabled, jobject obj, jint index, jboolean enabled) { if (!System::IsValid() || !System::HasCheatList()) return; diff --git a/android/app/src/cpp/android_host_interface.h b/android/app/src/cpp/android_host_interface.h index 224f36fe0..6ad39d297 100644 --- a/android/app/src/cpp/android_host_interface.h +++ b/android/app/src/cpp/android_host_interface.h @@ -54,6 +54,8 @@ public: void RefreshGameList(bool invalidate_cache, bool invalidate_database, ProgressCallback* progress_callback); void ApplySettings(bool display_osd_messages); + bool ImportPatchCodesFromString(const std::string& str); + protected: void SetUserDirectory() override; void LoadSettings() override; 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 292be5237..f73fb0557 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 @@ -70,9 +70,9 @@ public class AndroidHostInterface { public native void setDisplayAlignment(int alignment); - public native CheatCode[] getCheatList(); - - public native void setCheatEnabled(int index, boolean enabled); + public native PatchCode[] getPatchCodeList(); + public native void setPatchCodeEnabled(int index, boolean enabled); + public native boolean importPatchCodesFromString(String str); public native void addOSDMessage(String message, float duration); 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 26c4a0379..413c9ecd6 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 @@ -5,6 +5,7 @@ import android.content.Intent; import android.content.SharedPreferences; import android.content.res.Configuration; import android.hardware.input.InputManager; +import android.net.Uri; import android.os.Bundle; import android.util.Log; import android.view.SurfaceHolder; @@ -54,6 +55,19 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde editor.apply(); } + private void reportErrorOnUIThread(String message) { + // Toast.makeText(this, message, Toast.LENGTH_LONG); + new AlertDialog.Builder(this) + .setTitle("Error") + .setMessage(message) + .setPositiveButton("OK", (dialog, button) -> { + dialog.dismiss(); + enableFullscreenImmersive(); + }) + .create() + .show(); + } + public void reportError(String message) { Log.e("EmulationActivity", message); @@ -65,6 +79,7 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde .setMessage(message) .setPositiveButton("OK", (dialog, button) -> { dialog.dismiss(); + enableFullscreenImmersive(); synchronized (lock) { lock.notify(); } @@ -133,7 +148,6 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { // Once we get a surface, we can boot. if (AndroidHostInterface.getInstance().isEmulationThreadRunning()) { - final boolean hadSurface = AndroidHostInterface.getInstance().hasSurface(); AndroidHostInterface.getInstance().surfaceChanged(holder.getSurface(), format, width, height); updateOrientation(); @@ -217,6 +231,9 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde if (requestCode == REQUEST_CODE_SETTINGS) { if (AndroidHostInterface.getInstance().isEmulationThreadRunning()) applySettings(); + } else if (requestCode == REQUEST_IMPORT_PATCH_CODES) { + if (data != null) + importPatchesFromFile(data.getData()); } } @@ -261,6 +278,7 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde } private static final int REQUEST_CODE_SETTINGS = 0; + private static final int REQUEST_IMPORT_PATCH_CODES = 1; private void showMenu() { AlertDialog.Builder builder = new AlertDialog.Builder(this); @@ -331,7 +349,7 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde return; } - case 1: // Patches + case 1: // Patch Codes { showPatchesMenu(); return; @@ -373,25 +391,42 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde } private void showPatchesMenu() { - final CheatCode[] cheats = AndroidHostInterface.getInstance().getCheatList(); - if (cheats == null) { - AndroidHostInterface.getInstance().addOSDMessage("No patches are loaded.", 5.0f); - return; - } + final PatchCode[] codes = AndroidHostInterface.getInstance().getPatchCodeList(); 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()); + CharSequence[] items = new CharSequence[(codes != null) ? (codes.length + 1) : 1]; + items[0] = "Import Patch Codes..."; + if (codes != null) { + for (int i = 0; i < codes.length; i++) { + final PatchCode cc = codes[i]; + items[i + 1] = String.format("%s %s", cc.isEnabled() ? "(ON)" : "(OFF)", cc.getDescription()); + } } - builder.setItems(items, (dialogInterface, i) -> AndroidHostInterface.getInstance().setCheatEnabled(i, !cheats[i].isEnabled())); - builder.setOnDismissListener(dialogInterface -> enableFullscreenImmersive()); + builder.setItems(items, (dialogInterface, i) -> { + if (i > 0) { + AndroidHostInterface.getInstance().setPatchCodeEnabled(i - 1, !codes[i - 1].isEnabled()); + enableFullscreenImmersive(); + } else { + Intent intent = new Intent(Intent.ACTION_GET_CONTENT); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + intent.setType("*/*"); + intent.addCategory(Intent.CATEGORY_OPENABLE); + startActivityForResult(Intent.createChooser(intent, "Choose Patch Code File"), REQUEST_IMPORT_PATCH_CODES); + } + }); + builder.setOnCancelListener(dialogInterface -> enableFullscreenImmersive()); builder.create().show(); } + private void importPatchesFromFile(Uri uri) { + String str = FileUtil.readFileFromUri(this, uri, 512 * 1024); + if (str == null || !AndroidHostInterface.getInstance().importPatchCodesFromString(str)) { + reportErrorOnUIThread("Failed to import patch codes. Make sure you selected a PCSXR or Libretro format file."); + } + } + /** * Touchscreen controller overlay */ diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/FileUtil.java b/android/app/src/main/java/com/github/stenzek/duckstation/FileUtil.java index 429da3992..15ae7c1f1 100644 --- a/android/app/src/main/java/com/github/stenzek/duckstation/FileUtil.java +++ b/android/app/src/main/java/com/github/stenzek/duckstation/FileUtil.java @@ -9,12 +9,23 @@ import android.net.Uri; import android.os.Build; import android.os.storage.StorageManager; import android.provider.DocumentsContract; +import android.widget.Toast; import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.StringWriter; import java.lang.reflect.Array; import java.lang.reflect.Method; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; public final class FileUtil { static String TAG = "TAG"; @@ -138,4 +149,32 @@ public final class FileUtil { return null; } } + + public static String readFileFromUri(final Context context, final Uri uri, int maxSize) { + InputStream stream = null; + try { + stream = context.getContentResolver().openInputStream(uri); + } catch (FileNotFoundException e) { + return null; + } + + StringBuilder os = new StringBuilder(); + try { + char[] buffer = new char[1024]; + InputStreamReader reader = new InputStreamReader(stream, Charset.forName(StandardCharsets.UTF_8.name())); + int len; + while ((len = reader.read(buffer)) > 0) { + os.append(buffer, 0, len); + if (os.length() > maxSize) + return null; + } + } catch (IOException e) { + return null; + } + + if (os.length() == 0) + return null; + + return os.toString(); + } } \ No newline at end of file diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/CheatCode.java b/android/app/src/main/java/com/github/stenzek/duckstation/PatchCode.java similarity index 55% rename from android/app/src/main/java/com/github/stenzek/duckstation/CheatCode.java rename to android/app/src/main/java/com/github/stenzek/duckstation/PatchCode.java index 8a457473c..d22157ae1 100644 --- a/android/app/src/main/java/com/github/stenzek/duckstation/CheatCode.java +++ b/android/app/src/main/java/com/github/stenzek/duckstation/PatchCode.java @@ -1,13 +1,13 @@ package com.github.stenzek.duckstation; -public class CheatCode { +public class PatchCode { private int mIndex; - private String mName; + private String mDescription; private boolean mEnabled; - public CheatCode(int index, String name, boolean enabled) { + public PatchCode(int index, String description, boolean enabled) { mIndex = index; - mName = name; + mDescription = description; mEnabled = enabled; } @@ -15,8 +15,8 @@ public class CheatCode { return mIndex; } - public String getName() { - return mName; + public String getDescription() { + return mDescription; } public boolean isEnabled() { diff --git a/android/app/src/main/res/values/arrays.xml b/android/app/src/main/res/values/arrays.xml index 95f0ff825..6551dc298 100644 --- a/android/app/src/main/res/values/arrays.xml +++ b/android/app/src/main/res/values/arrays.xml @@ -143,7 +143,7 @@ Reset - Patches + Patch Codes Change Disc Change Touchscreen Controller Settings diff --git a/android/app/src/main/res/xml/general_preferences.xml b/android/app/src/main/res/xml/general_preferences.xml index af0a0d157..89db87275 100644 --- a/android/app/src/main/res/xml/general_preferences.xml +++ b/android/app/src/main/res/xml/general_preferences.xml @@ -41,9 +41,9 @@ app:iconSpaceReserved="false" />