Android: Add patch code (cheat) import

This commit is contained in:
Connor McLaughlin 2020-11-07 22:04:07 +10:00
parent fcca1fa3f7
commit 8014e66c6f
8 changed files with 161 additions and 38 deletions

View File

@ -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<u32>(std::max(m_settings_interface.GetIntValue("CPU", "Overclock", 100), 1));
const u32 overclock_percent =
static_cast<u32>(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<CheatList>(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<jclass>(env->NewGlobalRef(s_AndroidHostInterface_class))) ==
nullptr ||
(s_CheatCode_class = env->FindClass("com/github/stenzek/duckstation/CheatCode")) == nullptr ||
(s_CheatCode_class = static_cast<jclass>(env->NewGlobalRef(s_CheatCode_class))) == nullptr)
(s_PatchCode_class = env->FindClass("com/github/stenzek/duckstation/PatchCode")) == nullptr ||
(s_PatchCode_class = static_cast<jclass>(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, "<init>", "(ILjava/lang/String;Z)V")) == nullptr)
(s_PatchCode_constructor = env->GetMethodID(s_PatchCode_class, "<init>", "(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<jint>(i),
jobject java_cc = env->NewObject(s_PatchCode_class, s_PatchCode_constructor, static_cast<jint>(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;

View File

@ -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;

View File

@ -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);

View File

@ -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
*/

View File

@ -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();
}
}

View File

@ -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() {

View File

@ -143,7 +143,7 @@
</string-array>
<string-array name="emulation_more_menu">
<item>Reset</item>
<item>Patches</item>
<item>Patch Codes</item>
<item>Change Disc</item>
<item>Change Touchscreen Controller</item>
<item>Settings</item>

View File

@ -41,9 +41,9 @@
app:iconSpaceReserved="false" />
<SwitchPreferenceCompat
app:key="Main/AutoLoadCheats"
app:title="Load Cheats"
app:title="Load Patch Codes"
app:defaultValue="false"
app:summary="Loads cheats from cheats/&lt;game name&gt;.cht in PCSXR format. Cheats can be toggled while ingame."
app:summary="Loads patch codes from cheats/&lt;game name&gt;.cht in PCSXR format. Codes can be toggled while ingame."
app:iconSpaceReserved="false" />
<SwitchPreferenceCompat
app:key="Display/VSync"