Merge pull request #10092 from JosJuice/android-cheats

Android: Add cheat GUI
This commit is contained in:
Léo Lam 2021-09-16 19:21:33 +02:00 committed by GitHub
commit 7379450633
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 2678 additions and 15 deletions

View File

@ -90,6 +90,8 @@ dependencies {
implementation 'androidx.recyclerview:recyclerview:1.2.1'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'androidx.lifecycle:lifecycle-viewmodel:2.3.1'
implementation 'androidx.fragment:fragment:1.3.6'
implementation "androidx.slidingpanelayout:slidingpanelayout:1.2.0-alpha03"
implementation 'com.google.android.material:material:1.4.0'
// Android TV UI libraries.

View File

@ -76,6 +76,12 @@
android:theme="@style/DolphinSettingsBase"
android:label="@string/settings"/>
<activity
android:name=".features.cheats.ui.CheatsActivity"
android:exported="false"
android:theme="@style/DolphinSettingsBase"
android:label="@string/cheats"/>
<activity
android:name=".activities.EmulationActivity"
android:exported="false"

View File

@ -12,6 +12,7 @@ import androidx.fragment.app.DialogFragment;
import org.dolphinemu.dolphinemu.R;
import org.dolphinemu.dolphinemu.activities.ConvertActivity;
import org.dolphinemu.dolphinemu.features.cheats.ui.CheatsActivity;
import org.dolphinemu.dolphinemu.features.settings.model.Settings;
import org.dolphinemu.dolphinemu.features.settings.model.StringSetting;
import org.dolphinemu.dolphinemu.features.settings.ui.MenuTag;
@ -28,7 +29,8 @@ public class GamePropertiesDialog extends DialogFragment
{
public static final String TAG = "GamePropertiesDialog";
private static final String ARG_PATH = "path";
private static final String ARG_GAMEID = "game_id";
private static final String ARG_GAME_ID = "game_id";
private static final String ARG_GAMETDB_ID = "gametdb_id";
public static final String ARG_REVISION = "revision";
private static final String ARG_PLATFORM = "platform";
private static final String ARG_SHOULD_ALLOW_CONVERSION = "should_allow_conversion";
@ -39,7 +41,8 @@ public class GamePropertiesDialog extends DialogFragment
Bundle arguments = new Bundle();
arguments.putString(ARG_PATH, gameFile.getPath());
arguments.putString(ARG_GAMEID, gameFile.getGameId());
arguments.putString(ARG_GAME_ID, gameFile.getGameId());
arguments.putString(ARG_GAMETDB_ID, gameFile.getGameTdbId());
arguments.putInt(ARG_REVISION, gameFile.getRevision());
arguments.putInt(ARG_PLATFORM, gameFile.getPlatform());
arguments.putBoolean(ARG_SHOULD_ALLOW_CONVERSION, gameFile.shouldAllowConversion());
@ -53,7 +56,8 @@ public class GamePropertiesDialog extends DialogFragment
public Dialog onCreateDialog(Bundle savedInstanceState)
{
final String path = requireArguments().getString(ARG_PATH);
final String gameId = requireArguments().getString(ARG_GAMEID);
final String gameId = requireArguments().getString(ARG_GAME_ID);
final String gameTdbId = requireArguments().getString(ARG_GAMETDB_ID);
final int revision = requireArguments().getInt(ARG_REVISION);
final int platform = requireArguments().getInt(ARG_PLATFORM);
final boolean shouldAllowConversion =
@ -91,6 +95,9 @@ public class GamePropertiesDialog extends DialogFragment
itemsBuilder.add(R.string.properties_edit_game_settings, (dialog, i) ->
SettingsActivity.launch(getContext(), MenuTag.SETTINGS, gameId, revision, isWii));
itemsBuilder.add(R.string.properties_edit_cheats, (dialog, i) ->
CheatsActivity.launch(getContext(), gameId, gameTdbId, revision, isWii));
itemsBuilder.add(R.string.properties_clear_game_settings, (dialog, i) ->
clearGameSettings(gameId));

View File

@ -0,0 +1,60 @@
// SPDX-License-Identifier: GPL-2.0-or-later
package org.dolphinemu.dolphinemu.features.cheats.model;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
public class ARCheat extends AbstractCheat
{
@Keep
private final long mPointer;
public ARCheat()
{
mPointer = createNew();
}
@Keep
private ARCheat(long pointer)
{
mPointer = pointer;
}
@Override
public native void finalize();
private native long createNew();
public boolean supportsCreator()
{
return false;
}
public boolean supportsNotes()
{
return false;
}
@NonNull
public native String getName();
@NonNull
public native String getCode();
public native boolean getUserDefined();
public native boolean getEnabled();
@Override
protected native int trySetImpl(@NonNull String name, @NonNull String creator,
@NonNull String notes, @NonNull String code);
@Override
protected native void setEnabledImpl(boolean enabled);
@NonNull
public static native ARCheat[] loadCodes(String gameId, int revision);
public static native void saveCodes(String gameId, int revision, ARCheat[] codes);
}

View File

@ -0,0 +1,62 @@
// SPDX-License-Identifier: GPL-2.0-or-later
package org.dolphinemu.dolphinemu.features.cheats.model;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public abstract class AbstractCheat implements Cheat
{
private Runnable mChangedCallback = null;
public int trySet(@NonNull String name, @NonNull String creator, @NonNull String notes,
@NonNull String code)
{
if (!code.isEmpty() && code.charAt(0) == '$')
{
int firstLineEnd = code.indexOf('\n');
if (firstLineEnd == -1)
{
name = code.substring(1);
code = "";
}
else
{
name = code.substring(1, firstLineEnd);
code = code.substring(firstLineEnd + 1);
}
}
if (name.isEmpty())
return TRY_SET_FAIL_NO_NAME;
int result = trySetImpl(name, creator, notes, code);
if (result == TRY_SET_SUCCESS)
onChanged();
return result;
}
public void setEnabled(boolean enabled)
{
setEnabledImpl(enabled);
onChanged();
}
public void setChangedCallback(@Nullable Runnable callback)
{
mChangedCallback = callback;
}
protected void onChanged()
{
if (mChangedCallback != null)
mChangedCallback.run();
}
protected abstract int trySetImpl(@NonNull String name, @NonNull String creator,
@NonNull String notes, @NonNull String code);
protected abstract void setEnabledImpl(boolean enabled);
}

View File

@ -0,0 +1,48 @@
// SPDX-License-Identifier: GPL-2.0-or-later
package org.dolphinemu.dolphinemu.features.cheats.model;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public interface Cheat
{
int TRY_SET_FAIL_CODE_MIXED_ENCRYPTION = -3;
int TRY_SET_FAIL_NO_CODE_LINES = -2;
int TRY_SET_FAIL_NO_NAME = -1;
int TRY_SET_SUCCESS = 0;
// Result codes greater than 0 represent an error on the corresponding code line (one-indexed)
boolean supportsCreator();
boolean supportsNotes();
@NonNull
String getName();
@NonNull
default String getCreator()
{
return "";
}
@NonNull
default String getNotes()
{
return "";
}
@NonNull
String getCode();
int trySet(@NonNull String name, @NonNull String creator, @NonNull String notes,
@NonNull String code);
boolean getUserDefined();
boolean getEnabled();
void setEnabled(boolean enabled);
void setChangedCallback(@Nullable Runnable callback);
}

View File

@ -0,0 +1,297 @@
// SPDX-License-Identifier: GPL-2.0-or-later
package org.dolphinemu.dolphinemu.features.cheats.model;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import java.util.ArrayList;
import java.util.Collections;
public class CheatsViewModel extends ViewModel
{
private boolean mLoaded = false;
private int mSelectedCheatPosition = -1;
private final MutableLiveData<Cheat> mSelectedCheat = new MutableLiveData<>(null);
private final MutableLiveData<Boolean> mIsAdding = new MutableLiveData<>(false);
private final MutableLiveData<Boolean> mIsEditing = new MutableLiveData<>(false);
private final MutableLiveData<Integer> mCheatAddedEvent = new MutableLiveData<>(null);
private final MutableLiveData<Integer> mCheatChangedEvent = new MutableLiveData<>(null);
private final MutableLiveData<Integer> mCheatDeletedEvent = new MutableLiveData<>(null);
private final MutableLiveData<Integer> mGeckoCheatsDownloadedEvent = new MutableLiveData<>(null);
private final MutableLiveData<Boolean> mOpenDetailsViewEvent = new MutableLiveData<>(false);
private ArrayList<PatchCheat> mPatchCheats;
private ArrayList<ARCheat> mARCheats;
private ArrayList<GeckoCheat> mGeckoCheats;
private boolean mPatchCheatsNeedSaving = false;
private boolean mARCheatsNeedSaving = false;
private boolean mGeckoCheatsNeedSaving = false;
public void load(String gameID, int revision)
{
if (mLoaded)
return;
mPatchCheats = new ArrayList<>();
Collections.addAll(mPatchCheats, PatchCheat.loadCodes(gameID, revision));
mARCheats = new ArrayList<>();
Collections.addAll(mARCheats, ARCheat.loadCodes(gameID, revision));
mGeckoCheats = new ArrayList<>();
Collections.addAll(mGeckoCheats, GeckoCheat.loadCodes(gameID, revision));
for (PatchCheat cheat : mPatchCheats)
{
cheat.setChangedCallback(() -> mPatchCheatsNeedSaving = true);
}
for (ARCheat cheat : mARCheats)
{
cheat.setChangedCallback(() -> mARCheatsNeedSaving = true);
}
for (GeckoCheat cheat : mGeckoCheats)
{
cheat.setChangedCallback(() -> mGeckoCheatsNeedSaving = true);
}
mLoaded = true;
}
public void saveIfNeeded(String gameID, int revision)
{
if (mPatchCheatsNeedSaving)
{
PatchCheat.saveCodes(gameID, revision, mPatchCheats.toArray(new PatchCheat[0]));
mPatchCheatsNeedSaving = false;
}
if (mARCheatsNeedSaving)
{
ARCheat.saveCodes(gameID, revision, mARCheats.toArray(new ARCheat[0]));
mARCheatsNeedSaving = false;
}
if (mGeckoCheatsNeedSaving)
{
GeckoCheat.saveCodes(gameID, revision, mGeckoCheats.toArray(new GeckoCheat[0]));
mGeckoCheatsNeedSaving = false;
}
}
public LiveData<Cheat> getSelectedCheat()
{
return mSelectedCheat;
}
public void setSelectedCheat(Cheat cheat, int position)
{
if (mIsEditing.getValue())
setIsEditing(false);
mSelectedCheat.setValue(cheat);
mSelectedCheatPosition = position;
}
public LiveData<Boolean> getIsAdding()
{
return mIsAdding;
}
public void startAddingCheat(Cheat cheat, int position)
{
mSelectedCheat.setValue(cheat);
mSelectedCheatPosition = position;
mIsAdding.setValue(true);
mIsEditing.setValue(true);
}
public void finishAddingCheat()
{
if (!mIsAdding.getValue())
throw new IllegalStateException();
mIsAdding.setValue(false);
mIsEditing.setValue(false);
Cheat cheat = mSelectedCheat.getValue();
if (cheat instanceof PatchCheat)
{
mPatchCheats.add((PatchCheat) mSelectedCheat.getValue());
cheat.setChangedCallback(() -> mPatchCheatsNeedSaving = true);
mPatchCheatsNeedSaving = true;
}
else if (cheat instanceof ARCheat)
{
mARCheats.add((ARCheat) mSelectedCheat.getValue());
cheat.setChangedCallback(() -> mPatchCheatsNeedSaving = true);
mARCheatsNeedSaving = true;
}
else if (cheat instanceof GeckoCheat)
{
mGeckoCheats.add((GeckoCheat) mSelectedCheat.getValue());
cheat.setChangedCallback(() -> mGeckoCheatsNeedSaving = true);
mGeckoCheatsNeedSaving = true;
}
else
{
throw new UnsupportedOperationException();
}
notifyCheatAdded();
}
public LiveData<Boolean> getIsEditing()
{
return mIsEditing;
}
public void setIsEditing(boolean isEditing)
{
mIsEditing.setValue(isEditing);
if (mIsAdding.getValue() && !isEditing)
{
mIsAdding.setValue(false);
setSelectedCheat(null, -1);
}
}
/**
* When a cheat is added, the integer stored in the returned LiveData
* changes to the position of that cheat, then changes back to null.
*/
public LiveData<Integer> getCheatAddedEvent()
{
return mCheatAddedEvent;
}
private void notifyCheatAdded()
{
mCheatAddedEvent.setValue(mSelectedCheatPosition);
mCheatAddedEvent.setValue(null);
}
/**
* When a cheat is edited, the integer stored in the returned LiveData
* changes to the position of that cheat, then changes back to null.
*/
public LiveData<Integer> getCheatChangedEvent()
{
return mCheatChangedEvent;
}
/**
* Notifies that an edit has been made to the contents of the currently selected cheat.
*/
public void notifySelectedCheatChanged()
{
notifyCheatChanged(mSelectedCheatPosition);
}
/**
* Notifies that an edit has been made to the contents of the cheat at the given position.
*/
public void notifyCheatChanged(int position)
{
mCheatChangedEvent.setValue(position);
mCheatChangedEvent.setValue(null);
}
/**
* When a cheat is deleted, the integer stored in the returned LiveData
* changes to the position of that cheat, then changes back to null.
*/
public LiveData<Integer> getCheatDeletedEvent()
{
return mCheatDeletedEvent;
}
public void deleteSelectedCheat()
{
Cheat cheat = mSelectedCheat.getValue();
int position = mSelectedCheatPosition;
setSelectedCheat(null, -1);
if (mPatchCheats.remove(cheat))
mPatchCheatsNeedSaving = true;
if (mARCheats.remove(cheat))
mARCheatsNeedSaving = true;
if (mGeckoCheats.remove(cheat))
mGeckoCheatsNeedSaving = true;
notifyCheatDeleted(position);
}
/**
* Notifies that the cheat at the given position has been deleted.
*/
private void notifyCheatDeleted(int position)
{
mCheatDeletedEvent.setValue(position);
mCheatDeletedEvent.setValue(null);
}
/**
* When Gecko cheats are downloaded, the integer stored in the returned LiveData
* changes to the number of cheats added, then changes back to null.
*/
public LiveData<Integer> getGeckoCheatsDownloadedEvent()
{
return mGeckoCheatsDownloadedEvent;
}
public int addDownloadedGeckoCodes(GeckoCheat[] cheats)
{
int cheatsAdded = 0;
for (GeckoCheat cheat : cheats)
{
if (!mGeckoCheats.contains(cheat))
{
mGeckoCheats.add(cheat);
cheatsAdded++;
}
}
if (cheatsAdded != 0)
{
mGeckoCheatsNeedSaving = true;
mGeckoCheatsDownloadedEvent.setValue(cheatsAdded);
mGeckoCheatsDownloadedEvent.setValue(null);
}
return cheatsAdded;
}
public LiveData<Boolean> getOpenDetailsViewEvent()
{
return mOpenDetailsViewEvent;
}
public void openDetailsView()
{
mOpenDetailsViewEvent.setValue(true);
mOpenDetailsViewEvent.setValue(false);
}
public ArrayList<PatchCheat> getPatchCheats()
{
return mPatchCheats;
}
public ArrayList<ARCheat> getARCheats()
{
return mARCheats;
}
public ArrayList<GeckoCheat> getGeckoCheats()
{
return mGeckoCheats;
}
}

View File

@ -0,0 +1,78 @@
// SPDX-License-Identifier: GPL-2.0-or-later
package org.dolphinemu.dolphinemu.features.cheats.model;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public class GeckoCheat extends AbstractCheat
{
@Keep
private final long mPointer;
public GeckoCheat()
{
mPointer = createNew();
}
@Keep
private GeckoCheat(long pointer)
{
mPointer = pointer;
}
@Override
public native void finalize();
private native long createNew();
@Override
public boolean equals(@Nullable Object obj)
{
return obj != null && getClass() == obj.getClass() && equalsImpl((GeckoCheat) obj);
}
public boolean supportsCreator()
{
return true;
}
public boolean supportsNotes()
{
return true;
}
@NonNull
public native String getName();
@NonNull
public native String getCreator();
@NonNull
public native String getNotes();
@NonNull
public native String getCode();
public native boolean getUserDefined();
public native boolean getEnabled();
public native boolean equalsImpl(@NonNull GeckoCheat other);
@Override
protected native int trySetImpl(@NonNull String name, @NonNull String creator,
@NonNull String notes, @NonNull String code);
@Override
protected native void setEnabledImpl(boolean enabled);
@NonNull
public static native GeckoCheat[] loadCodes(String gameId, int revision);
public static native void saveCodes(String gameId, int revision, GeckoCheat[] codes);
@Nullable
public static native GeckoCheat[] downloadCodes(String gameTdbId);
}

View File

@ -0,0 +1,60 @@
// SPDX-License-Identifier: GPL-2.0-or-later
package org.dolphinemu.dolphinemu.features.cheats.model;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
public class PatchCheat extends AbstractCheat
{
@Keep
private final long mPointer;
public PatchCheat()
{
mPointer = createNew();
}
@Keep
private PatchCheat(long pointer)
{
mPointer = pointer;
}
@Override
public native void finalize();
private native long createNew();
public boolean supportsCreator()
{
return false;
}
public boolean supportsNotes()
{
return false;
}
@NonNull
public native String getName();
@NonNull
public native String getCode();
public native boolean getUserDefined();
public native boolean getEnabled();
@Override
protected native int trySetImpl(@NonNull String name, @NonNull String creator,
@NonNull String notes, @NonNull String code);
@Override
protected native void setEnabledImpl(boolean enabled);
@NonNull
public static native PatchCheat[] loadCodes(String gameId, int revision);
public static native void saveCodes(String gameId, int revision, PatchCheat[] codes);
}

View File

@ -0,0 +1,67 @@
// SPDX-License-Identifier: GPL-2.0-or-later
package org.dolphinemu.dolphinemu.features.cheats.ui;
import android.view.View;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.lifecycle.ViewModelProvider;
import org.dolphinemu.dolphinemu.R;
import org.dolphinemu.dolphinemu.features.cheats.model.ARCheat;
import org.dolphinemu.dolphinemu.features.cheats.model.CheatsViewModel;
import org.dolphinemu.dolphinemu.features.cheats.model.GeckoCheat;
import org.dolphinemu.dolphinemu.features.cheats.model.PatchCheat;
public class ActionViewHolder extends CheatItemViewHolder implements View.OnClickListener
{
private final TextView mName;
private CheatsActivity mActivity;
private CheatsViewModel mViewModel;
private int mString;
private int mPosition;
public ActionViewHolder(@NonNull View itemView)
{
super(itemView);
mName = itemView.findViewById(R.id.text_setting_name);
itemView.setOnClickListener(this);
}
public void bind(CheatsActivity activity, CheatItem item, int position)
{
mActivity = activity;
mViewModel = new ViewModelProvider(activity).get(CheatsViewModel.class);
mString = item.getString();
mPosition = position;
mName.setText(mString);
}
public void onClick(View root)
{
if (mString == R.string.cheats_add_ar)
{
mViewModel.startAddingCheat(new ARCheat(), mPosition);
mViewModel.openDetailsView();
}
else if (mString == R.string.cheats_add_gecko)
{
mViewModel.startAddingCheat(new GeckoCheat(), mPosition);
mViewModel.openDetailsView();
}
else if (mString == R.string.cheats_add_patch)
{
mViewModel.startAddingCheat(new PatchCheat(), mPosition);
mViewModel.openDetailsView();
}
else if (mString == R.string.cheats_download_gecko)
{
mActivity.downloadGeckoCodes();
}
}
}

View File

@ -0,0 +1,198 @@
// SPDX-License-Identifier: GPL-2.0-or-later
package org.dolphinemu.dolphinemu.features.cheats.ui;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ScrollView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.ViewModelProvider;
import org.dolphinemu.dolphinemu.R;
import org.dolphinemu.dolphinemu.features.cheats.model.Cheat;
import org.dolphinemu.dolphinemu.features.cheats.model.CheatsViewModel;
public class CheatDetailsFragment extends Fragment
{
private View mRoot;
private ScrollView mScrollView;
private TextView mLabelName;
private EditText mEditName;
private TextView mLabelCreator;
private EditText mEditCreator;
private TextView mLabelNotes;
private EditText mEditNotes;
private EditText mEditCode;
private Button mButtonDelete;
private Button mButtonEdit;
private Button mButtonCancel;
private Button mButtonOk;
private CheatsViewModel mViewModel;
private Cheat mCheat;
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState)
{
return inflater.inflate(R.layout.fragment_cheat_details, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState)
{
mRoot = view.findViewById(R.id.root);
mScrollView = view.findViewById(R.id.scroll_view);
mLabelName = view.findViewById(R.id.label_name);
mEditName = view.findViewById(R.id.edit_name);
mLabelCreator = view.findViewById(R.id.label_creator);
mEditCreator = view.findViewById(R.id.edit_creator);
mLabelNotes = view.findViewById(R.id.label_notes);
mEditNotes = view.findViewById(R.id.edit_notes);
mEditCode = view.findViewById(R.id.edit_code);
mButtonDelete = view.findViewById(R.id.button_delete);
mButtonEdit = view.findViewById(R.id.button_edit);
mButtonCancel = view.findViewById(R.id.button_cancel);
mButtonOk = view.findViewById(R.id.button_ok);
CheatsActivity activity = (CheatsActivity) requireActivity();
mViewModel = new ViewModelProvider(activity).get(CheatsViewModel.class);
mViewModel.getSelectedCheat().observe(getViewLifecycleOwner(), this::onSelectedCheatUpdated);
mViewModel.getIsEditing().observe(getViewLifecycleOwner(), this::onIsEditingUpdated);
mButtonDelete.setOnClickListener(this::onDeleteClicked);
mButtonEdit.setOnClickListener(this::onEditClicked);
mButtonCancel.setOnClickListener(this::onCancelClicked);
mButtonOk.setOnClickListener(this::onOkClicked);
CheatsActivity.setOnFocusChangeListenerRecursively(view,
(v, hasFocus) -> activity.onDetailsViewFocusChange(hasFocus));
}
private void clearEditErrors()
{
mEditName.setError(null);
mEditCode.setError(null);
}
private void onDeleteClicked(View view)
{
AlertDialog.Builder builder =
new AlertDialog.Builder(requireContext(), R.style.DolphinDialogBase);
builder.setMessage(getString(R.string.cheats_delete_confirmation, mCheat.getName()));
builder.setPositiveButton(R.string.yes, (dialog, i) -> mViewModel.deleteSelectedCheat());
builder.setNegativeButton(R.string.no, null);
builder.show();
}
private void onEditClicked(View view)
{
mViewModel.setIsEditing(true);
mButtonOk.requestFocus();
}
private void onCancelClicked(View view)
{
mViewModel.setIsEditing(false);
onSelectedCheatUpdated(mCheat);
mButtonDelete.requestFocus();
}
private void onOkClicked(View view)
{
clearEditErrors();
int result = mCheat.trySet(mEditName.getText().toString(), mEditCreator.getText().toString(),
mEditNotes.getText().toString(), mEditCode.getText().toString());
switch (result)
{
case Cheat.TRY_SET_SUCCESS:
if (mViewModel.getIsAdding().getValue())
{
mViewModel.finishAddingCheat();
onSelectedCheatUpdated(mCheat);
}
else
{
mViewModel.notifySelectedCheatChanged();
mViewModel.setIsEditing(false);
}
mButtonEdit.requestFocus();
break;
case Cheat.TRY_SET_FAIL_NO_NAME:
mEditName.setError(getString(R.string.cheats_error_no_name));
mScrollView.smoothScrollTo(0, mLabelName.getTop());
break;
case Cheat.TRY_SET_FAIL_NO_CODE_LINES:
mEditCode.setError(getString(R.string.cheats_error_no_code_lines));
mScrollView.smoothScrollTo(0, mEditCode.getBottom());
break;
case Cheat.TRY_SET_FAIL_CODE_MIXED_ENCRYPTION:
mEditCode.setError(getString(R.string.cheats_error_mixed_encryption));
mScrollView.smoothScrollTo(0, mEditCode.getBottom());
break;
default:
mEditCode.setError(getString(R.string.cheats_error_on_line, result));
mScrollView.smoothScrollTo(0, mEditCode.getBottom());
break;
}
}
private void onSelectedCheatUpdated(@Nullable Cheat cheat)
{
clearEditErrors();
mRoot.setVisibility(cheat == null ? View.GONE : View.VISIBLE);
int creatorVisibility = cheat != null && cheat.supportsCreator() ? View.VISIBLE : View.GONE;
int notesVisibility = cheat != null && cheat.supportsNotes() ? View.VISIBLE : View.GONE;
mLabelCreator.setVisibility(creatorVisibility);
mEditCreator.setVisibility(creatorVisibility);
mLabelNotes.setVisibility(notesVisibility);
mEditNotes.setVisibility(notesVisibility);
boolean userDefined = cheat != null && cheat.getUserDefined();
mButtonDelete.setEnabled(userDefined);
mButtonEdit.setEnabled(userDefined);
// If the fragment was recreated while editing a cheat, it's vital that we
// don't repopulate the fields, otherwise the user's changes will be lost
boolean isEditing = mViewModel.getIsEditing().getValue();
if (!isEditing && cheat != null)
{
mEditName.setText(cheat.getName());
mEditCreator.setText(cheat.getCreator());
mEditNotes.setText(cheat.getNotes());
mEditCode.setText(cheat.getCode());
}
mCheat = cheat;
}
private void onIsEditingUpdated(boolean isEditing)
{
mEditName.setEnabled(isEditing);
mEditCreator.setEnabled(isEditing);
mEditNotes.setEnabled(isEditing);
mEditCode.setEnabled(isEditing);
mButtonDelete.setVisibility(isEditing ? View.GONE : View.VISIBLE);
mButtonEdit.setVisibility(isEditing ? View.GONE : View.VISIBLE);
mButtonCancel.setVisibility(isEditing ? View.VISIBLE : View.GONE);
mButtonOk.setVisibility(isEditing ? View.VISIBLE : View.GONE);
}
}

View File

@ -0,0 +1,49 @@
// SPDX-License-Identifier: GPL-2.0-or-later
package org.dolphinemu.dolphinemu.features.cheats.ui;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.dolphinemu.dolphinemu.features.cheats.model.Cheat;
public class CheatItem
{
public static final int TYPE_CHEAT = 0;
public static final int TYPE_HEADER = 1;
public static final int TYPE_ACTION = 2;
private final @Nullable Cheat mCheat;
private final int mString;
private final int mType;
public CheatItem(@NonNull Cheat cheat)
{
mCheat = cheat;
mString = 0;
mType = TYPE_CHEAT;
}
public CheatItem(int type, int string)
{
mCheat = null;
mString = string;
mType = type;
}
@Nullable
public Cheat getCheat()
{
return mCheat;
}
public int getString()
{
return mString;
}
public int getType()
{
return mType;
}
}

View File

@ -0,0 +1,20 @@
// SPDX-License-Identifier: GPL-2.0-or-later
package org.dolphinemu.dolphinemu.features.cheats.ui;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import org.dolphinemu.dolphinemu.features.cheats.model.CheatsViewModel;
public abstract class CheatItemViewHolder extends RecyclerView.ViewHolder
{
public CheatItemViewHolder(@NonNull View itemView)
{
super(itemView);
}
public abstract void bind(CheatsActivity activity, CheatItem item, int position);
}

View File

@ -0,0 +1,43 @@
// SPDX-License-Identifier: GPL-2.0-or-later
package org.dolphinemu.dolphinemu.features.cheats.ui;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import org.dolphinemu.dolphinemu.R;
import org.dolphinemu.dolphinemu.features.cheats.model.CheatsViewModel;
import org.dolphinemu.dolphinemu.ui.DividerItemDecoration;
public class CheatListFragment extends Fragment
{
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState)
{
return inflater.inflate(R.layout.fragment_cheat_list, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState)
{
RecyclerView recyclerView = view.findViewById(R.id.cheat_list);
CheatsActivity activity = (CheatsActivity) requireActivity();
CheatsViewModel viewModel = new ViewModelProvider(activity).get(CheatsViewModel.class);
recyclerView.setAdapter(new CheatsAdapter(activity, viewModel));
recyclerView.setLayoutManager(new LinearLayoutManager(activity));
recyclerView.addItemDecoration(new DividerItemDecoration(activity, null));
}
}

View File

@ -0,0 +1,64 @@
// SPDX-License-Identifier: GPL-2.0-or-later
package org.dolphinemu.dolphinemu.features.cheats.ui;
import android.view.View;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.RecyclerView.ViewHolder;
import org.dolphinemu.dolphinemu.R;
import org.dolphinemu.dolphinemu.features.cheats.model.Cheat;
import org.dolphinemu.dolphinemu.features.cheats.model.CheatsViewModel;
public class CheatViewHolder extends CheatItemViewHolder
implements View.OnClickListener, CompoundButton.OnCheckedChangeListener
{
private final View mRoot;
private final TextView mName;
private final CheckBox mCheckbox;
private CheatsViewModel mViewModel;
private Cheat mCheat;
private int mPosition;
public CheatViewHolder(@NonNull View itemView)
{
super(itemView);
mRoot = itemView.findViewById(R.id.root);
mName = itemView.findViewById(R.id.text_name);
mCheckbox = itemView.findViewById(R.id.checkbox);
}
public void bind(CheatsActivity activity, CheatItem item, int position)
{
mCheckbox.setOnCheckedChangeListener(null);
mViewModel = new ViewModelProvider(activity).get(CheatsViewModel.class);
mCheat = item.getCheat();
mPosition = position;
mName.setText(mCheat.getName());
mCheckbox.setChecked(mCheat.getEnabled());
mRoot.setOnClickListener(this);
mCheckbox.setOnCheckedChangeListener(this);
}
public void onClick(View root)
{
mViewModel.setSelectedCheat(mCheat, mPosition);
mViewModel.openDetailsView();
}
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked)
{
mCheat.setEnabled(isChecked);
mViewModel.notifyCheatChanged(mPosition);
}
}

View File

@ -0,0 +1,63 @@
// SPDX-License-Identifier: GPL-2.0-or-later
package org.dolphinemu.dolphinemu.features.cheats.ui;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import org.dolphinemu.dolphinemu.R;
import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting;
import org.dolphinemu.dolphinemu.features.settings.model.Settings;
import org.dolphinemu.dolphinemu.features.settings.ui.MenuTag;
import org.dolphinemu.dolphinemu.features.settings.ui.SettingsActivity;
public class CheatWarningFragment extends Fragment implements View.OnClickListener
{
private View mView;
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState)
{
return inflater.inflate(R.layout.fragment_cheat_warning, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState)
{
mView = view;
Button settingsButton = view.findViewById(R.id.button_settings);
settingsButton.setOnClickListener(this);
CheatsActivity activity = (CheatsActivity) requireActivity();
CheatsActivity.setOnFocusChangeListenerRecursively(view,
(v, hasFocus) -> activity.onListViewFocusChange(hasFocus));
}
@Override
public void onResume()
{
super.onResume();
CheatsActivity activity = (CheatsActivity) requireActivity();
try (Settings settings = activity.loadGameSpecificSettings())
{
boolean cheatsEnabled = BooleanSetting.MAIN_ENABLE_CHEATS.getBoolean(settings);
mView.setVisibility(cheatsEnabled ? View.GONE : View.VISIBLE);
}
}
public void onClick(View view)
{
SettingsActivity.launch(requireContext(), MenuTag.CONFIG_GENERAL);
}
}

View File

@ -0,0 +1,253 @@
// SPDX-License-Identifier: GPL-2.0-or-later
package org.dolphinemu.dolphinemu.features.cheats.ui;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.view.ViewCompat;
import androidx.lifecycle.ViewModelProvider;
import androidx.slidingpanelayout.widget.SlidingPaneLayout;
import org.dolphinemu.dolphinemu.R;
import org.dolphinemu.dolphinemu.features.cheats.model.Cheat;
import org.dolphinemu.dolphinemu.features.cheats.model.CheatsViewModel;
import org.dolphinemu.dolphinemu.features.cheats.model.GeckoCheat;
import org.dolphinemu.dolphinemu.features.settings.model.Settings;
import org.dolphinemu.dolphinemu.ui.TwoPaneOnBackPressedCallback;
import org.dolphinemu.dolphinemu.ui.main.MainPresenter;
public class CheatsActivity extends AppCompatActivity
implements SlidingPaneLayout.PanelSlideListener
{
private static final String ARG_GAME_ID = "game_id";
private static final String ARG_GAMETDB_ID = "gametdb_id";
private static final String ARG_REVISION = "revision";
private static final String ARG_IS_WII = "is_wii";
private String mGameId;
private String mGameTdbId;
private int mRevision;
private boolean mIsWii;
private CheatsViewModel mViewModel;
private SlidingPaneLayout mSlidingPaneLayout;
private View mCheatList;
private View mCheatDetails;
private View mCheatListLastFocus;
private View mCheatDetailsLastFocus;
public static void launch(Context context, String gameId, String gameTdbId, int revision,
boolean isWii)
{
Intent intent = new Intent(context, CheatsActivity.class);
intent.putExtra(ARG_GAME_ID, gameId);
intent.putExtra(ARG_GAMETDB_ID, gameTdbId);
intent.putExtra(ARG_REVISION, revision);
intent.putExtra(ARG_IS_WII, isWii);
context.startActivity(intent);
}
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
MainPresenter.skipRescanningLibrary();
Intent intent = getIntent();
mGameId = intent.getStringExtra(ARG_GAME_ID);
mGameTdbId = intent.getStringExtra(ARG_GAMETDB_ID);
mRevision = intent.getIntExtra(ARG_REVISION, 0);
mIsWii = intent.getBooleanExtra(ARG_IS_WII, true);
setTitle(getString(R.string.cheats_with_game_id, mGameId));
mViewModel = new ViewModelProvider(this).get(CheatsViewModel.class);
mViewModel.load(mGameId, mRevision);
setContentView(R.layout.activity_cheats);
mSlidingPaneLayout = findViewById(R.id.sliding_pane_layout);
mCheatList = findViewById(R.id.cheat_list);
mCheatDetails = findViewById(R.id.cheat_details);
mCheatListLastFocus = mCheatList;
mCheatDetailsLastFocus = mCheatDetails;
mSlidingPaneLayout.addPanelSlideListener(this);
getOnBackPressedDispatcher().addCallback(this,
new TwoPaneOnBackPressedCallback(mSlidingPaneLayout));
mViewModel.getSelectedCheat().observe(this, this::onSelectedCheatChanged);
onSelectedCheatChanged(mViewModel.getSelectedCheat().getValue());
mViewModel.getOpenDetailsViewEvent().observe(this, this::openDetailsView);
}
@Override
public boolean onCreateOptionsMenu(Menu menu)
{
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.menu_settings, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item)
{
if (item.getItemId() == R.id.menu_save_exit)
{
finish();
return true;
}
return false;
}
@Override
protected void onStop()
{
super.onStop();
mViewModel.saveIfNeeded(mGameId, mRevision);
}
@Override
public void onPanelSlide(@NonNull View panel, float slideOffset)
{
}
@Override
public void onPanelOpened(@NonNull View panel)
{
boolean rtl = ViewCompat.getLayoutDirection(panel) == ViewCompat.LAYOUT_DIRECTION_RTL;
mCheatDetailsLastFocus.requestFocus(rtl ? View.FOCUS_LEFT : View.FOCUS_RIGHT);
}
@Override
public void onPanelClosed(@NonNull View panel)
{
boolean rtl = ViewCompat.getLayoutDirection(panel) == ViewCompat.LAYOUT_DIRECTION_RTL;
mCheatListLastFocus.requestFocus(rtl ? View.FOCUS_RIGHT : View.FOCUS_LEFT);
}
private void onSelectedCheatChanged(Cheat selectedCheat)
{
boolean cheatSelected = selectedCheat != null;
if (!cheatSelected && mSlidingPaneLayout.isOpen())
mSlidingPaneLayout.close();
mSlidingPaneLayout.setLockMode(cheatSelected ?
SlidingPaneLayout.LOCK_MODE_UNLOCKED : SlidingPaneLayout.LOCK_MODE_LOCKED_CLOSED);
}
public void onListViewFocusChange(boolean hasFocus)
{
if (hasFocus)
{
mCheatListLastFocus = mCheatList.findFocus();
if (mCheatListLastFocus == null)
throw new NullPointerException();
mSlidingPaneLayout.close();
}
}
public void onDetailsViewFocusChange(boolean hasFocus)
{
if (hasFocus)
{
mCheatDetailsLastFocus = mCheatDetails.findFocus();
if (mCheatDetailsLastFocus == null)
throw new NullPointerException();
mSlidingPaneLayout.open();
}
}
private void openDetailsView(boolean open)
{
if (open)
mSlidingPaneLayout.open();
}
public Settings loadGameSpecificSettings()
{
Settings settings = new Settings();
settings.loadSettings(null, mGameId, mRevision, mIsWii);
return settings;
}
public void downloadGeckoCodes()
{
AlertDialog progressDialog = new AlertDialog.Builder(this, R.style.DolphinDialogBase).create();
progressDialog.setTitle(R.string.cheats_downloading);
progressDialog.setCancelable(false);
progressDialog.show();
new Thread(() ->
{
GeckoCheat[] codes = GeckoCheat.downloadCodes(mGameTdbId);
runOnUiThread(() ->
{
progressDialog.dismiss();
if (codes == null)
{
new AlertDialog.Builder(this, R.style.DolphinDialogBase)
.setMessage(getString(R.string.cheats_download_failed))
.setPositiveButton(R.string.ok, null)
.show();
}
else if (codes.length == 0)
{
new AlertDialog.Builder(this, R.style.DolphinDialogBase)
.setMessage(getString(R.string.cheats_download_empty))
.setPositiveButton(R.string.ok, null)
.show();
}
else
{
int cheatsAdded = mViewModel.addDownloadedGeckoCodes(codes);
String message = getString(R.string.cheats_download_succeeded, codes.length, cheatsAdded);
new AlertDialog.Builder(this, R.style.DolphinDialogBase)
.setMessage(message)
.setPositiveButton(R.string.ok, null)
.show();
}
});
}).start();
}
public static void setOnFocusChangeListenerRecursively(@NonNull View view,
View.OnFocusChangeListener listener)
{
view.setOnFocusChangeListener(listener);
if (view instanceof ViewGroup)
{
ViewGroup viewGroup = (ViewGroup) view;
for (int i = 0; i < viewGroup.getChildCount(); i++)
{
View child = viewGroup.getChildAt(i);
setOnFocusChangeListenerRecursively(child, listener);
}
}
}
}

View File

@ -0,0 +1,162 @@
// SPDX-License-Identifier: GPL-2.0-or-later
package org.dolphinemu.dolphinemu.features.cheats.ui;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.lifecycle.LifecycleOwner;
import androidx.recyclerview.widget.RecyclerView;
import org.dolphinemu.dolphinemu.R;
import org.dolphinemu.dolphinemu.features.cheats.model.ARCheat;
import org.dolphinemu.dolphinemu.features.cheats.model.CheatsViewModel;
import org.dolphinemu.dolphinemu.features.cheats.model.GeckoCheat;
import org.dolphinemu.dolphinemu.features.cheats.model.PatchCheat;
import java.util.ArrayList;
public class CheatsAdapter extends RecyclerView.Adapter<CheatItemViewHolder>
{
private final CheatsActivity mActivity;
private final CheatsViewModel mViewModel;
public CheatsAdapter(CheatsActivity activity, CheatsViewModel viewModel)
{
mActivity = activity;
mViewModel = viewModel;
mViewModel.getCheatAddedEvent().observe(activity, (position) ->
{
if (position != null)
notifyItemInserted(position);
});
mViewModel.getCheatChangedEvent().observe(activity, (position) ->
{
if (position != null)
notifyItemChanged(position);
});
mViewModel.getCheatDeletedEvent().observe(activity, (position) ->
{
if (position != null)
notifyItemRemoved(position);
});
mViewModel.getGeckoCheatsDownloadedEvent().observe(activity, (cheatsAdded) ->
{
if (cheatsAdded != null)
{
int positionEnd = getItemCount() - 2; // Skip "Add Gecko Code" and "Download Gecko Codes"
int positionStart = positionEnd - cheatsAdded;
notifyItemRangeInserted(positionStart, cheatsAdded);
}
});
}
@NonNull
@Override
public CheatItemViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType)
{
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
switch (viewType)
{
case CheatItem.TYPE_CHEAT:
View cheatView = inflater.inflate(R.layout.list_item_cheat, parent, false);
addViewListeners(cheatView);
return new CheatViewHolder(cheatView);
case CheatItem.TYPE_HEADER:
View headerView = inflater.inflate(R.layout.list_item_header, parent, false);
addViewListeners(headerView);
return new HeaderViewHolder(headerView);
case CheatItem.TYPE_ACTION:
View actionView = inflater.inflate(R.layout.list_item_submenu, parent, false);
addViewListeners(actionView);
return new ActionViewHolder(actionView);
default:
throw new UnsupportedOperationException();
}
}
@Override
public void onBindViewHolder(@NonNull CheatItemViewHolder holder, int position)
{
holder.bind(mActivity, getItemAt(position), position);
}
@Override
public int getItemCount()
{
return mViewModel.getARCheats().size() + mViewModel.getGeckoCheats().size() +
mViewModel.getPatchCheats().size() + 7;
}
@Override
public int getItemViewType(int position)
{
return getItemAt(position).getType();
}
private void addViewListeners(View view)
{
CheatsActivity.setOnFocusChangeListenerRecursively(view,
(v, hasFocus) -> mActivity.onListViewFocusChange(hasFocus));
}
private CheatItem getItemAt(int position)
{
// Patches
if (position == 0)
return new CheatItem(CheatItem.TYPE_HEADER, R.string.cheats_header_patch);
position -= 1;
ArrayList<PatchCheat> patchCheats = mViewModel.getPatchCheats();
if (position < patchCheats.size())
return new CheatItem(patchCheats.get(position));
position -= patchCheats.size();
if (position == 0)
return new CheatItem(CheatItem.TYPE_ACTION, R.string.cheats_add_patch);
position -= 1;
// AR codes
if (position == 0)
return new CheatItem(CheatItem.TYPE_HEADER, R.string.cheats_header_ar);
position -= 1;
ArrayList<ARCheat> arCheats = mViewModel.getARCheats();
if (position < arCheats.size())
return new CheatItem(arCheats.get(position));
position -= arCheats.size();
if (position == 0)
return new CheatItem(CheatItem.TYPE_ACTION, R.string.cheats_add_ar);
position -= 1;
// Gecko codes
if (position == 0)
return new CheatItem(CheatItem.TYPE_HEADER, R.string.cheats_header_gecko);
position -= 1;
ArrayList<GeckoCheat> geckoCheats = mViewModel.getGeckoCheats();
if (position < geckoCheats.size())
return new CheatItem(geckoCheats.get(position));
position -= geckoCheats.size();
if (position == 0)
return new CheatItem(CheatItem.TYPE_ACTION, R.string.cheats_add_gecko);
position -= 1;
if (position == 0)
return new CheatItem(CheatItem.TYPE_ACTION, R.string.cheats_download_gecko);
throw new IndexOutOfBoundsException();
}
}

View File

@ -0,0 +1,28 @@
// SPDX-License-Identifier: GPL-2.0-or-later
package org.dolphinemu.dolphinemu.features.cheats.ui;
import android.view.View;
import android.widget.TextView;
import androidx.annotation.NonNull;
import org.dolphinemu.dolphinemu.R;
import org.dolphinemu.dolphinemu.features.cheats.model.CheatsViewModel;
public class HeaderViewHolder extends CheatItemViewHolder
{
private TextView mHeaderName;
public HeaderViewHolder(@NonNull View itemView)
{
super(itemView);
mHeaderName = itemView.findViewById(R.id.text_header_name);
}
public void bind(CheatsActivity activity, CheatItem item, int position)
{
mHeaderName.setText(item.getString());
}
}

View File

@ -14,6 +14,7 @@ public enum BooleanSetting implements AbstractBooleanSetting
MAIN_FASTMEM(Settings.FILE_DOLPHIN, Settings.SECTION_INI_CORE, "Fastmem", true),
MAIN_CPU_THREAD(Settings.FILE_DOLPHIN, Settings.SECTION_INI_CORE, "CPUThread", true),
MAIN_SYNC_ON_SKIP_IDLE(Settings.FILE_DOLPHIN, Settings.SECTION_INI_CORE, "SyncOnSkipIdle", true),
MAIN_ENABLE_CHEATS(Settings.FILE_DOLPHIN, Settings.SECTION_INI_CORE, "EnableCheats", false),
MAIN_OVERRIDE_REGION_SETTINGS(Settings.FILE_DOLPHIN, Settings.SECTION_INI_CORE,
"OverrideRegionSettings", false),
MAIN_AUDIO_STRETCH(Settings.FILE_DOLPHIN, Settings.SECTION_INI_CORE, "AudioStretch", false),
@ -198,6 +199,7 @@ public enum BooleanSetting implements AbstractBooleanSetting
private static final BooleanSetting[] NOT_RUNTIME_EDITABLE_ARRAY = new BooleanSetting[]{
MAIN_DSP_HLE,
MAIN_CPU_THREAD,
MAIN_ENABLE_CHEATS,
MAIN_OVERRIDE_REGION_SETTINGS,
MAIN_WII_SD_CARD, // Can actually be changed, but specific code is required
MAIN_DSP_JIT

View File

@ -18,9 +18,7 @@ import androidx.recyclerview.widget.RecyclerView;
import org.dolphinemu.dolphinemu.R;
import org.dolphinemu.dolphinemu.dialogs.MotionAlertDialog;
import org.dolphinemu.dolphinemu.features.settings.model.AdHocBooleanSetting;
import org.dolphinemu.dolphinemu.features.settings.model.Settings;
import org.dolphinemu.dolphinemu.features.settings.model.StringSetting;
import org.dolphinemu.dolphinemu.features.settings.model.view.CheckBoxSetting;
import org.dolphinemu.dolphinemu.features.settings.model.view.FilePicker;
import org.dolphinemu.dolphinemu.features.settings.model.view.FloatSliderSetting;
@ -43,7 +41,6 @@ import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.SettingViewHold
import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.SingleChoiceViewHolder;
import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.SliderViewHolder;
import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.SubmenuViewHolder;
import org.dolphinemu.dolphinemu.ui.main.MainPresenter;
import org.dolphinemu.dolphinemu.utils.DirectoryInitialization;
import org.dolphinemu.dolphinemu.utils.FileBrowserHelper;
import org.dolphinemu.dolphinemu.utils.Log;
@ -51,10 +48,7 @@ import org.dolphinemu.dolphinemu.utils.Log;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.security.InvalidParameterException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Map;
public final class SettingsAdapter extends RecyclerView.Adapter<SettingViewHolder>
implements DialogInterface.OnClickListener, SeekBar.OnSeekBarChangeListener
@ -87,7 +81,7 @@ public final class SettingsAdapter extends RecyclerView.Adapter<SettingViewHolde
switch (viewType)
{
case SettingsItem.TYPE_HEADER:
view = inflater.inflate(R.layout.list_item_settings_header, parent, false);
view = inflater.inflate(R.layout.list_item_header, parent, false);
return new HeaderViewHolder(view, this);
case SettingsItem.TYPE_CHECKBOX:
@ -105,7 +99,7 @@ public final class SettingsAdapter extends RecyclerView.Adapter<SettingViewHolde
return new SliderViewHolder(view, this, mContext);
case SettingsItem.TYPE_SUBMENU:
view = inflater.inflate(R.layout.list_item_setting_submenu, parent, false);
view = inflater.inflate(R.layout.list_item_submenu, parent, false);
return new SubmenuViewHolder(view, this);
case SettingsItem.TYPE_INPUT_BINDING:

View File

@ -258,6 +258,8 @@ public final class SettingsFragmentPresenter
{
sl.add(new CheckBoxSetting(mContext, BooleanSetting.MAIN_CPU_THREAD, R.string.dual_core,
R.string.dual_core_description));
sl.add(new CheckBoxSetting(mContext, BooleanSetting.MAIN_ENABLE_CHEATS, R.string.enable_cheats,
0));
sl.add(new CheckBoxSetting(mContext, BooleanSetting.MAIN_OVERRIDE_REGION_SETTINGS,
R.string.override_region_settings, 0));
sl.add(new CheckBoxSetting(mContext, BooleanSetting.MAIN_AUTO_DISC_CHANGE,

View File

@ -0,0 +1,46 @@
// SPDX-License-Identifier: GPL-2.0-or-later
package org.dolphinemu.dolphinemu.ui;
import android.view.View;
import androidx.activity.OnBackPressedCallback;
import androidx.annotation.NonNull;
import androidx.slidingpanelayout.widget.SlidingPaneLayout;
public class TwoPaneOnBackPressedCallback extends OnBackPressedCallback
implements SlidingPaneLayout.PanelSlideListener
{
private final SlidingPaneLayout mSlidingPaneLayout;
public TwoPaneOnBackPressedCallback(@NonNull SlidingPaneLayout slidingPaneLayout)
{
super(slidingPaneLayout.isSlideable() && slidingPaneLayout.isOpen());
mSlidingPaneLayout = slidingPaneLayout;
slidingPaneLayout.addPanelSlideListener(this);
}
@Override
public void handleOnBackPressed()
{
mSlidingPaneLayout.close();
}
@Override
public void onPanelSlide(@NonNull View panel, float slideOffset)
{
}
@Override
public void onPanelOpened(@NonNull View panel)
{
setEnabled(true);
}
@Override
public void onPanelClosed(@NonNull View panel)
{
setEnabled(false);
}
}

View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/root"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:focusable="true"
android:nextFocusLeft="@id/checkbox">
<TextView
android:id="@+id/text_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
style="@style/TextAppearance.AppCompat.Headline"
android:textSize="16sp"
tools:text="Hyrule Field Speed Hack"
android:layout_margin="@dimen/spacing_large"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/checkbox"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
<CheckBox
android:id="@+id/checkbox"
android:layout_width="48dp"
android:layout_height="64dp"
app:layout_constraintStart_toEndOf="@id/text_name"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:gravity="center"
android:focusable="true"
android:nextFocusRight="@id/root" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.slidingpanelayout.widget.SlidingPaneLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/sliding_pane_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.fragment.app.FragmentContainerView
android:layout_width="320dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:id="@+id/cheat_list"
android:name="org.dolphinemu.dolphinemu.features.cheats.ui.CheatListFragment" />
<androidx.fragment.app.FragmentContainerView
android:layout_width="320dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:id="@+id/cheat_details"
android:name="org.dolphinemu.dolphinemu.features.cheats.ui.CheatDetailsFragment" />
</androidx.slidingpanelayout.widget.SlidingPaneLayout>

View File

@ -0,0 +1,190 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/root"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ScrollView
android:id="@+id/scroll_view"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/barrier">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/label_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/TextAppearance.AppCompat.Headline"
android:textSize="18sp"
android:text="@string/cheats_name"
android:layout_margin="@dimen/spacing_large"
android:labelFor="@id/edit_name"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/edit_name" />
<EditText
android:id="@+id/edit_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="48dp"
android:layout_marginHorizontal="@dimen/spacing_large"
android:importantForAutofill="no"
android:inputType="text"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/label_name"
app:layout_constraintBottom_toTopOf="@id/label_creator"
tools:text="Hyrule Field Speed Hack" />
<TextView
android:id="@+id/label_creator"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/TextAppearance.AppCompat.Headline"
android:textSize="18sp"
android:text="@string/cheats_creator"
android:layout_margin="@dimen/spacing_large"
android:labelFor="@id/edit_creator"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/edit_name"
app:layout_constraintBottom_toTopOf="@id/edit_creator" />
<EditText
android:id="@+id/edit_creator"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="48dp"
android:layout_marginHorizontal="@dimen/spacing_large"
android:importantForAutofill="no"
android:inputType="text"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/label_creator"
app:layout_constraintBottom_toTopOf="@id/label_notes" />
<TextView
android:id="@+id/label_notes"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/TextAppearance.AppCompat.Headline"
android:textSize="18sp"
android:text="@string/cheats_notes"
android:layout_margin="@dimen/spacing_large"
android:labelFor="@id/edit_notes"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/edit_creator"
app:layout_constraintBottom_toTopOf="@id/edit_notes" />
<EditText
android:id="@+id/edit_notes"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="48dp"
android:layout_marginHorizontal="@dimen/spacing_large"
android:importantForAutofill="no"
android:inputType="textMultiLine"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/label_notes"
app:layout_constraintBottom_toTopOf="@id/label_code" />
<TextView
android:id="@+id/label_code"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/TextAppearance.AppCompat.Headline"
android:textSize="18sp"
android:text="@string/cheats_code"
android:layout_margin="@dimen/spacing_large"
android:labelFor="@id/edit_code"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/edit_notes"
app:layout_constraintBottom_toTopOf="@id/edit_code" />
<EditText
android:id="@+id/edit_code"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="108sp"
android:layout_marginHorizontal="@dimen/spacing_large"
android:importantForAutofill="no"
android:inputType="textMultiLine"
android:typeface="monospace"
android:gravity="start"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/label_code"
app:layout_constraintBottom_toBottomOf="parent"
tools:text="0x8003d63c:dword:0x60000000\n0x8003d658:dword:0x60000000" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
<androidx.constraintlayout.widget.Barrier
android:id="@+id/barrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="top"
app:constraint_referenced_ids="button_delete,button_edit,button_cancel,button_ok" />
<Button
android:id="@+id/button_delete"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="@dimen/spacing_large"
android:text="@string/cheats_delete"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/button_edit"
app:layout_constraintTop_toBottomOf="@id/barrier"
app:layout_constraintBottom_toBottomOf="parent" />
<Button
android:id="@+id/button_edit"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="@dimen/spacing_large"
android:text="@string/cheats_edit"
app:layout_constraintStart_toEndOf="@id/button_delete"
app:layout_constraintEnd_toStartOf="@id/button_cancel"
app:layout_constraintTop_toBottomOf="@id/barrier"
app:layout_constraintBottom_toBottomOf="parent" />
<Button
android:id="@+id/button_cancel"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="@dimen/spacing_large"
android:text="@string/cancel"
app:layout_constraintStart_toEndOf="@id/button_edit"
app:layout_constraintEnd_toStartOf="@id/button_ok"
app:layout_constraintTop_toBottomOf="@id/barrier"
app:layout_constraintBottom_toBottomOf="parent" />
<Button
android:id="@+id/button_ok"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="@dimen/spacing_large"
android:text="@string/ok"
app:layout_constraintStart_toEndOf="@id/button_cancel"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/barrier"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/cheat_warning"
android:name="org.dolphinemu.dolphinemu.features.cheats.ui.CheatWarningFragment"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/cheat_list" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/cheat_list"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/cheat_warning"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/text_warning"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_margin="@dimen/spacing_large"
android:text="@string/cheats_disabled_warning"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/button_settings"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
<Button
android:id="@+id/button_settings"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/spacing_small"
android:text="@string/cheats_open_settings"
app:layout_constraintStart_toEndOf="@id/text_warning"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/root"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:focusable="true"
android:nextFocusRight="@id/checkbox">
<TextView
android:id="@+id/text_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
style="@style/TextAppearance.AppCompat.Headline"
android:textSize="16sp"
tools:text="Hyrule Field Speed Hack"
android:layout_margin="@dimen/spacing_large"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/checkbox"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
<CheckBox
android:id="@+id/checkbox"
android:layout_width="48dp"
android:layout_height="64dp"
app:layout_constraintStart_toEndOf="@id/text_name"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:gravity="center"
android:focusable="true"
android:nextFocusLeft="@id/root" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -135,6 +135,7 @@
<string name="general_submenu">General</string>
<string name="dual_core">Dual Core</string>
<string name="dual_core_description">Split workload to two CPU cores instead of one. Increases speed.</string>
<string name="enable_cheats">Enable Cheats</string>
<string name="speed_limit">Speed Limit (0% = Unlimited)</string>
<string name="overclock_warning">WARNING: Changing this from the default (100%) WILL break games and cause glitches. Please do not report bugs that occur with a non-default clock.</string>
<string name="gamecube_submenu">GameCube</string>
@ -361,7 +362,8 @@
<string name="properties_convert">Convert File</string>
<string name="properties_set_default_iso">Set as Default ISO</string>
<string name="properties_edit_game_settings">Edit Game Settings</string>
<string name="properties_clear_game_settings">Clear Game Settings</string>
<string name="properties_edit_cheats">Edit Cheats</string>
<string name="properties_clear_game_settings">Clear Game Settings and Cheats</string>
<string name="properties_clear_success">Cleared settings for %1$s</string>
<string name="properties_clear_failure">Unable to clear settings for %1$s</string>
<string name="properties_clear_missing">No game settings to delete</string>
@ -371,6 +373,8 @@
<string name="preferences_extensions">Extension Bindings</string>
<string name="game_ini_junk_title">Junk Data Found</string>
<string name="game_ini_junk_question">The settings file for this game contains extraneous data added by an old version of Dolphin. This will likely prevent global settings from working as intended.\n\nWould you like to fix this by deleting the settings file for this game? All game-specific settings and cheats that you have added will be removed. This cannot be undone.</string>
<!-- Game Details Screen -->
<string name="game_details_country">Country</string>
<string name="game_details_company">Company</string>
<string name="game_details_game_id">Game ID</string>
@ -382,6 +386,34 @@
<string name="game_details_no_compression">No Compression</string>
<string name="game_details_size_and_format">%1$s (%2$s)</string>
<!-- Cheats Screen -->
<string name="cheats">Cheats</string>
<string name="cheats_with_game_id">Cheats: %1$s</string>
<string name="cheats_header_ar">AR Codes</string>
<string name="cheats_header_gecko">Gecko Codes</string>
<string name="cheats_header_patch">Patches</string>
<string name="cheats_add_ar">Add New AR Code</string>
<string name="cheats_add_gecko">Add New Gecko Code</string>
<string name="cheats_add_patch">Add New Patch</string>
<string name="cheats_download_gecko">Download Gecko Codes</string>
<string name="cheats_name">Name</string>
<string name="cheats_creator">Creator</string>
<string name="cheats_notes">Notes</string>
<string name="cheats_code">Code</string>
<string name="cheats_edit">Edit</string>
<string name="cheats_delete">Delete</string>
<string name="cheats_delete_confirmation">Are you sure you want to delete "%1$s"?</string>
<string name="cheats_error_no_name">Name can\'t be empty</string>
<string name="cheats_error_no_code_lines">Code can\'t be empty</string>
<string name="cheats_error_on_line">Error on line %1$d</string>
<string name="cheats_error_mixed_encryption">Lines must either be all encrypted or all decrypted</string>
<string name="cheats_downloading">Downloading...</string>
<string name="cheats_download_failed">Failed to download codes.</string>
<string name="cheats_download_empty">File contained no codes.</string>
<string name="cheats_download_succeeded">Downloaded %1$d codes. (added %2$d)</string>
<string name="cheats_disabled_warning">Dolphin\'s cheat system is currently disabled.</string>
<string name="cheats_open_settings">Settings</string>
<!-- Convert Screen -->
<string name="convert_format">Format</string>
<string name="convert_block_size">Block Size</string>

View File

@ -58,6 +58,18 @@ static jmethodID s_network_helper_get_network_gateway;
static jclass s_boolean_supplier_class;
static jmethodID s_boolean_supplier_get;
static jclass s_ar_cheat_class;
static jfieldID s_ar_cheat_pointer;
static jmethodID s_ar_cheat_constructor;
static jclass s_gecko_cheat_class;
static jfieldID s_gecko_cheat_pointer;
static jmethodID s_gecko_cheat_constructor;
static jclass s_patch_cheat_class;
static jfieldID s_patch_cheat_pointer;
static jmethodID s_patch_cheat_constructor;
namespace IDCache
{
JNIEnv* GetEnvForThread()
@ -268,6 +280,51 @@ jmethodID GetBooleanSupplierGet()
return s_boolean_supplier_get;
}
jclass GetARCheatClass()
{
return s_ar_cheat_class;
}
jfieldID GetARCheatPointer()
{
return s_ar_cheat_pointer;
}
jmethodID GetARCheatConstructor()
{
return s_ar_cheat_constructor;
}
jclass GetGeckoCheatClass()
{
return s_gecko_cheat_class;
}
jfieldID GetGeckoCheatPointer()
{
return s_gecko_cheat_pointer;
}
jmethodID GetGeckoCheatConstructor()
{
return s_gecko_cheat_constructor;
}
jclass GetPatchCheatClass()
{
return s_patch_cheat_class;
}
jfieldID GetPatchCheatPointer()
{
return s_patch_cheat_pointer;
}
jmethodID GetPatchCheatConstructor()
{
return s_patch_cheat_constructor;
}
} // namespace IDCache
extern "C" {
@ -376,6 +433,27 @@ JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved)
s_boolean_supplier_get = env->GetMethodID(s_boolean_supplier_class, "get", "()Z");
env->DeleteLocalRef(boolean_supplier_class);
const jclass ar_cheat_class =
env->FindClass("org/dolphinemu/dolphinemu/features/cheats/model/ARCheat");
s_ar_cheat_class = reinterpret_cast<jclass>(env->NewGlobalRef(ar_cheat_class));
s_ar_cheat_pointer = env->GetFieldID(ar_cheat_class, "mPointer", "J");
s_ar_cheat_constructor = env->GetMethodID(ar_cheat_class, "<init>", "(J)V");
env->DeleteLocalRef(ar_cheat_class);
const jclass gecko_cheat_class =
env->FindClass("org/dolphinemu/dolphinemu/features/cheats/model/GeckoCheat");
s_gecko_cheat_class = reinterpret_cast<jclass>(env->NewGlobalRef(gecko_cheat_class));
s_gecko_cheat_pointer = env->GetFieldID(gecko_cheat_class, "mPointer", "J");
s_gecko_cheat_constructor = env->GetMethodID(gecko_cheat_class, "<init>", "(J)V");
env->DeleteLocalRef(gecko_cheat_class);
const jclass patch_cheat_class =
env->FindClass("org/dolphinemu/dolphinemu/features/cheats/model/PatchCheat");
s_patch_cheat_class = reinterpret_cast<jclass>(env->NewGlobalRef(patch_cheat_class));
s_patch_cheat_pointer = env->GetFieldID(patch_cheat_class, "mPointer", "J");
s_patch_cheat_constructor = env->GetMethodID(patch_cheat_class, "<init>", "(J)V");
env->DeleteLocalRef(patch_cheat_class);
return JNI_VERSION;
}
@ -396,5 +474,8 @@ JNIEXPORT void JNI_OnUnload(JavaVM* vm, void* reserved)
env->DeleteGlobalRef(s_content_handler_class);
env->DeleteGlobalRef(s_network_helper_class);
env->DeleteGlobalRef(s_boolean_supplier_class);
env->DeleteGlobalRef(s_ar_cheat_class);
env->DeleteGlobalRef(s_gecko_cheat_class);
env->DeleteGlobalRef(s_patch_cheat_class);
}
}

View File

@ -57,4 +57,16 @@ jmethodID GetNetworkHelperGetNetworkGateway();
jmethodID GetBooleanSupplierGet();
jclass GetARCheatClass();
jfieldID GetARCheatPointer();
jmethodID GetARCheatConstructor();
jclass GetGeckoCheatClass();
jfieldID GetGeckoCheatPointer();
jmethodID GetGeckoCheatConstructor();
jclass GetPatchCheatClass();
jfieldID GetPatchCheatPointer();
jmethodID GetPatchCheatConstructor();
} // namespace IDCache

View File

@ -1,4 +1,8 @@
add_library(main SHARED
Cheats/ARCheat.cpp
Cheats/Cheats.h
Cheats/GeckoCheat.cpp
Cheats/PatchCheat.cpp
Config/NativeConfig.cpp
Config/PostProcessing.cpp
GameList/GameFile.cpp

View File

@ -0,0 +1,182 @@
// Copyright 2021 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <string>
#include <vector>
#include <jni.h>
#include "Common/FileUtil.h"
#include "Common/IniFile.h"
#include "Common/StringUtil.h"
#include "Core/ARDecrypt.h"
#include "Core/ActionReplay.h"
#include "Core/ConfigManager.h"
#include "jni/AndroidCommon/AndroidCommon.h"
#include "jni/AndroidCommon/IDCache.h"
#include "jni/Cheats/Cheats.h"
static ActionReplay::ARCode* GetPointer(JNIEnv* env, jobject obj)
{
return reinterpret_cast<ActionReplay::ARCode*>(
env->GetLongField(obj, IDCache::GetARCheatPointer()));
}
jobject ARCheatToJava(JNIEnv* env, const ActionReplay::ARCode& code)
{
return env->NewObject(IDCache::GetARCheatClass(), IDCache::GetARCheatConstructor(),
reinterpret_cast<jlong>(new ActionReplay::ARCode(code)));
}
extern "C" {
JNIEXPORT void JNICALL
Java_org_dolphinemu_dolphinemu_features_cheats_model_ARCheat_finalize(JNIEnv* env, jobject obj)
{
delete GetPointer(env, obj);
}
JNIEXPORT jlong JNICALL
Java_org_dolphinemu_dolphinemu_features_cheats_model_ARCheat_createNew(JNIEnv* env, jobject obj)
{
auto* code = new ActionReplay::ARCode;
code->user_defined = true;
return reinterpret_cast<jlong>(code);
}
JNIEXPORT jstring JNICALL
Java_org_dolphinemu_dolphinemu_features_cheats_model_ARCheat_getName(JNIEnv* env, jobject obj)
{
return ToJString(env, GetPointer(env, obj)->name);
}
JNIEXPORT jstring JNICALL
Java_org_dolphinemu_dolphinemu_features_cheats_model_ARCheat_getCode(JNIEnv* env, jobject obj)
{
ActionReplay::ARCode* code = GetPointer(env, obj);
std::string code_string;
for (size_t i = 0; i < code->ops.size(); ++i)
{
if (i != 0)
code_string += '\n';
code_string += ActionReplay::SerializeLine(code->ops[i]);
}
return ToJString(env, code_string);
}
JNIEXPORT jboolean JNICALL
Java_org_dolphinemu_dolphinemu_features_cheats_model_ARCheat_getUserDefined(JNIEnv* env,
jobject obj)
{
return static_cast<jboolean>(GetPointer(env, obj)->user_defined);
}
JNIEXPORT jboolean JNICALL
Java_org_dolphinemu_dolphinemu_features_cheats_model_ARCheat_getEnabled(JNIEnv* env, jobject obj)
{
return static_cast<jboolean>(GetPointer(env, obj)->enabled);
}
JNIEXPORT jint JNICALL Java_org_dolphinemu_dolphinemu_features_cheats_model_ARCheat_trySetImpl(
JNIEnv* env, jobject obj, jstring name, jstring creator, jstring notes, jstring code_string)
{
ActionReplay::ARCode* code = GetPointer(env, obj);
std::vector<ActionReplay::AREntry> entries;
std::vector<std::string> encrypted_lines;
std::vector<std::string> lines = SplitString(GetJString(env, code_string), '\n');
for (size_t i = 0; i < lines.size(); i++)
{
const std::string& line = lines[i];
if (line.empty())
continue;
auto parse_result = ActionReplay::DeserializeLine(line);
if (std::holds_alternative<ActionReplay::AREntry>(parse_result))
entries.emplace_back(std::get<ActionReplay::AREntry>(std::move(parse_result)));
else if (std::holds_alternative<ActionReplay::EncryptedLine>(parse_result))
encrypted_lines.emplace_back(std::get<ActionReplay::EncryptedLine>(std::move(parse_result)));
else
return i + 1; // Parse error on line i
}
if (!encrypted_lines.empty())
{
if (!entries.empty())
return Cheats::TRY_SET_FAIL_CODE_MIXED_ENCRYPTION;
ActionReplay::DecryptARCode(encrypted_lines, &entries);
}
if (entries.empty())
return Cheats::TRY_SET_FAIL_NO_CODE_LINES;
code->name = GetJString(env, name);
code->ops = std::move(entries);
return Cheats::TRY_SET_SUCCESS;
}
JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_features_cheats_model_ARCheat_setEnabledImpl(
JNIEnv* env, jobject obj, jboolean enabled)
{
GetPointer(env, obj)->enabled = static_cast<bool>(enabled);
}
JNIEXPORT jobjectArray JNICALL
Java_org_dolphinemu_dolphinemu_features_cheats_model_ARCheat_loadCodes(JNIEnv* env, jclass,
jstring jGameID,
jint revision)
{
const std::string game_id = GetJString(env, jGameID);
IniFile game_ini_local;
// We don't use LoadLocalGameIni() here because user cheat codes that are installed via the UI
// will always be stored in GS/${GAMEID}.ini
game_ini_local.Load(File::GetUserPath(D_GAMESETTINGS_IDX) + game_id + ".ini");
const IniFile game_ini_default = SConfig::LoadDefaultGameIni(game_id, revision);
const std::vector<ActionReplay::ARCode> codes =
ActionReplay::LoadCodes(game_ini_default, game_ini_local);
const jobjectArray array =
env->NewObjectArray(static_cast<jsize>(codes.size()), IDCache::GetARCheatClass(), nullptr);
jsize i = 0;
for (const ActionReplay::ARCode& code : codes)
env->SetObjectArrayElement(array, i++, ARCheatToJava(env, code));
return array;
}
JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_features_cheats_model_ARCheat_saveCodes(
JNIEnv* env, jclass, jstring jGameID, jint revision, jobjectArray jCodes)
{
const jsize size = env->GetArrayLength(jCodes);
std::vector<ActionReplay::ARCode> vector;
vector.reserve(size);
for (jsize i = 0; i < size; ++i)
{
jobject code = reinterpret_cast<jstring>(env->GetObjectArrayElement(jCodes, i));
vector.emplace_back(*GetPointer(env, code));
env->DeleteLocalRef(code);
}
const std::string game_id = GetJString(env, jGameID);
const std::string ini_path = File::GetUserPath(D_GAMESETTINGS_IDX) + game_id + ".ini";
IniFile game_ini_local;
game_ini_local.Load(ini_path);
ActionReplay::SaveCodes(&game_ini_local, vector);
game_ini_local.Save(ini_path);
}
}

View File

@ -0,0 +1,13 @@
// Copyright 2021 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
namespace Cheats
{
constexpr int TRY_SET_FAIL_CODE_MIXED_ENCRYPTION = -3;
constexpr int TRY_SET_FAIL_NO_CODE_LINES = -2;
constexpr int TRY_SET_FAIL_NO_NAME = -1;
constexpr int TRY_SET_SUCCESS = 0;
// Result codes greater than 0 represent an error on the corresponding code line (one-indexed)
} // namespace Cheats

View File

@ -0,0 +1,212 @@
// Copyright 2021 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <string>
#include <vector>
#include <jni.h>
#include "Common/FileUtil.h"
#include "Common/IniFile.h"
#include "Core/ConfigManager.h"
#include "Core/GeckoCode.h"
#include "Core/GeckoCodeConfig.h"
#include "jni/AndroidCommon/AndroidCommon.h"
#include "jni/AndroidCommon/IDCache.h"
#include "jni/Cheats/Cheats.h"
static Gecko::GeckoCode* GetPointer(JNIEnv* env, jobject obj)
{
return reinterpret_cast<Gecko::GeckoCode*>(
env->GetLongField(obj, IDCache::GetGeckoCheatPointer()));
}
jobject GeckoCheatToJava(JNIEnv* env, const Gecko::GeckoCode& code)
{
return env->NewObject(IDCache::GetGeckoCheatClass(), IDCache::GetGeckoCheatConstructor(),
reinterpret_cast<jlong>(new Gecko::GeckoCode(code)));
}
extern "C" {
JNIEXPORT void JNICALL
Java_org_dolphinemu_dolphinemu_features_cheats_model_GeckoCheat_finalize(JNIEnv* env, jobject obj)
{
delete GetPointer(env, obj);
}
JNIEXPORT jlong JNICALL
Java_org_dolphinemu_dolphinemu_features_cheats_model_GeckoCheat_createNew(JNIEnv* env, jobject obj)
{
auto* code = new Gecko::GeckoCode;
code->user_defined = true;
return reinterpret_cast<jlong>(code);
}
JNIEXPORT jstring JNICALL
Java_org_dolphinemu_dolphinemu_features_cheats_model_GeckoCheat_getName(JNIEnv* env, jobject obj)
{
return ToJString(env, GetPointer(env, obj)->name);
}
JNIEXPORT jstring JNICALL
Java_org_dolphinemu_dolphinemu_features_cheats_model_GeckoCheat_getCreator(JNIEnv* env, jobject obj)
{
return ToJString(env, GetPointer(env, obj)->creator);
}
JNIEXPORT jstring JNICALL
Java_org_dolphinemu_dolphinemu_features_cheats_model_GeckoCheat_getNotes(JNIEnv* env, jobject obj)
{
return ToJString(env, JoinStrings(GetPointer(env, obj)->notes, "\n"));
}
JNIEXPORT jstring JNICALL
Java_org_dolphinemu_dolphinemu_features_cheats_model_GeckoCheat_getCode(JNIEnv* env, jobject obj)
{
Gecko::GeckoCode* code = GetPointer(env, obj);
std::string code_string;
for (size_t i = 0; i < code->codes.size(); ++i)
{
if (i != 0)
code_string += '\n';
code_string += code->codes[i].original_line;
}
return ToJString(env, code_string);
}
JNIEXPORT jboolean JNICALL
Java_org_dolphinemu_dolphinemu_features_cheats_model_GeckoCheat_getUserDefined(JNIEnv* env,
jobject obj)
{
return static_cast<jboolean>(GetPointer(env, obj)->user_defined);
}
JNIEXPORT jboolean JNICALL
Java_org_dolphinemu_dolphinemu_features_cheats_model_GeckoCheat_getEnabled(JNIEnv* env, jobject obj)
{
return static_cast<jboolean>(GetPointer(env, obj)->enabled);
}
JNIEXPORT jboolean JNICALL
Java_org_dolphinemu_dolphinemu_features_cheats_model_GeckoCheat_equalsImpl(JNIEnv* env, jobject obj,
jobject other)
{
return *GetPointer(env, obj) == *GetPointer(env, other);
}
JNIEXPORT jint JNICALL Java_org_dolphinemu_dolphinemu_features_cheats_model_GeckoCheat_trySetImpl(
JNIEnv* env, jobject obj, jstring name, jstring creator, jstring notes, jstring code_string)
{
Gecko::GeckoCode* code = GetPointer(env, obj);
std::vector<Gecko::GeckoCode::Code> entries;
std::vector<std::string> lines = SplitString(GetJString(env, code_string), '\n');
for (size_t i = 0; i < lines.size(); i++)
{
const std::string& line = lines[i];
if (line.empty())
continue;
if (std::optional<Gecko::GeckoCode::Code> c = Gecko::DeserializeLine(line))
entries.emplace_back(*std::move(c));
else
return i + 1; // Parse error on line i
}
if (entries.empty())
return Cheats::TRY_SET_FAIL_NO_CODE_LINES;
code->name = GetJString(env, name);
code->creator = GetJString(env, creator);
code->notes = SplitString(GetJString(env, notes), '\n');
code->codes = std::move(entries);
return Cheats::TRY_SET_SUCCESS;
}
JNIEXPORT void JNICALL
Java_org_dolphinemu_dolphinemu_features_cheats_model_GeckoCheat_setEnabledImpl(JNIEnv* env,
jobject obj,
jboolean enabled)
{
GetPointer(env, obj)->enabled = static_cast<bool>(enabled);
}
JNIEXPORT jobjectArray JNICALL
Java_org_dolphinemu_dolphinemu_features_cheats_model_GeckoCheat_loadCodes(JNIEnv* env, jclass,
jstring jGameID,
jint revision)
{
const std::string game_id = GetJString(env, jGameID);
IniFile game_ini_local;
// We don't use LoadLocalGameIni() here because user cheat codes that are installed via the UI
// will always be stored in GS/${GAMEID}.ini
game_ini_local.Load(File::GetUserPath(D_GAMESETTINGS_IDX) + game_id + ".ini");
const IniFile game_ini_default = SConfig::LoadDefaultGameIni(game_id, revision);
const std::vector<Gecko::GeckoCode> codes = Gecko::LoadCodes(game_ini_default, game_ini_local);
const jobjectArray array =
env->NewObjectArray(static_cast<jsize>(codes.size()), IDCache::GetGeckoCheatClass(), nullptr);
jsize i = 0;
for (const Gecko::GeckoCode& code : codes)
env->SetObjectArrayElement(array, i++, GeckoCheatToJava(env, code));
return array;
}
JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_features_cheats_model_GeckoCheat_saveCodes(
JNIEnv* env, jclass, jstring jGameID, jint revision, jobjectArray jCodes)
{
const jsize size = env->GetArrayLength(jCodes);
std::vector<Gecko::GeckoCode> vector;
vector.reserve(size);
for (jsize i = 0; i < size; ++i)
{
jobject code = reinterpret_cast<jstring>(env->GetObjectArrayElement(jCodes, i));
vector.emplace_back(*GetPointer(env, code));
env->DeleteLocalRef(code);
}
const std::string game_id = GetJString(env, jGameID);
const std::string ini_path = File::GetUserPath(D_GAMESETTINGS_IDX) + game_id + ".ini";
IniFile game_ini_local;
game_ini_local.Load(ini_path);
Gecko::SaveCodes(game_ini_local, vector);
game_ini_local.Save(ini_path);
}
JNIEXPORT jobjectArray JNICALL
Java_org_dolphinemu_dolphinemu_features_cheats_model_GeckoCheat_downloadCodes(JNIEnv* env, jclass,
jstring jGameTdbId)
{
const std::string gametdb_id = GetJString(env, jGameTdbId);
bool success = true;
const std::vector<Gecko::GeckoCode> codes = Gecko::DownloadCodes(gametdb_id, &success, false);
if (!success)
return nullptr;
const jobjectArray array =
env->NewObjectArray(static_cast<jsize>(codes.size()), IDCache::GetGeckoCheatClass(), nullptr);
jsize i = 0;
for (const Gecko::GeckoCode& code : codes)
env->SetObjectArrayElement(array, i++, GeckoCheatToJava(env, code));
return array;
}
}

View File

@ -0,0 +1,169 @@
// Copyright 2021 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <string>
#include <vector>
#include <jni.h>
#include "Common/FileUtil.h"
#include "Common/IniFile.h"
#include "Core/ConfigManager.h"
#include "Core/PatchEngine.h"
#include "jni/AndroidCommon/AndroidCommon.h"
#include "jni/AndroidCommon/IDCache.h"
#include "jni/Cheats/Cheats.h"
static PatchEngine::Patch* GetPointer(JNIEnv* env, jobject obj)
{
return reinterpret_cast<PatchEngine::Patch*>(
env->GetLongField(obj, IDCache::GetPatchCheatPointer()));
}
jobject PatchCheatToJava(JNIEnv* env, const PatchEngine::Patch& patch)
{
return env->NewObject(IDCache::GetPatchCheatClass(), IDCache::GetPatchCheatConstructor(),
reinterpret_cast<jlong>(new PatchEngine::Patch(patch)));
}
extern "C" {
JNIEXPORT void JNICALL
Java_org_dolphinemu_dolphinemu_features_cheats_model_PatchCheat_finalize(JNIEnv* env, jobject obj)
{
delete GetPointer(env, obj);
}
JNIEXPORT jlong JNICALL
Java_org_dolphinemu_dolphinemu_features_cheats_model_PatchCheat_createNew(JNIEnv* env, jobject obj)
{
auto* patch = new PatchEngine::Patch;
patch->user_defined = true;
return reinterpret_cast<jlong>(patch);
}
JNIEXPORT jstring JNICALL
Java_org_dolphinemu_dolphinemu_features_cheats_model_PatchCheat_getName(JNIEnv* env, jobject obj)
{
return ToJString(env, GetPointer(env, obj)->name);
}
JNIEXPORT jstring JNICALL
Java_org_dolphinemu_dolphinemu_features_cheats_model_PatchCheat_getCode(JNIEnv* env, jobject obj)
{
PatchEngine::Patch* patch = GetPointer(env, obj);
std::string code_string;
for (size_t i = 0; i < patch->entries.size(); ++i)
{
if (i != 0)
code_string += '\n';
code_string += PatchEngine::SerializeLine(patch->entries[i]);
}
return ToJString(env, code_string);
}
JNIEXPORT jboolean JNICALL
Java_org_dolphinemu_dolphinemu_features_cheats_model_PatchCheat_getUserDefined(JNIEnv* env,
jobject obj)
{
return static_cast<jboolean>(GetPointer(env, obj)->user_defined);
}
JNIEXPORT jboolean JNICALL
Java_org_dolphinemu_dolphinemu_features_cheats_model_PatchCheat_getEnabled(JNIEnv* env, jobject obj)
{
return static_cast<jboolean>(GetPointer(env, obj)->enabled);
}
JNIEXPORT jint JNICALL Java_org_dolphinemu_dolphinemu_features_cheats_model_PatchCheat_trySetImpl(
JNIEnv* env, jobject obj, jstring name, jstring creator, jstring notes, jstring code_string)
{
PatchEngine::Patch* patch = GetPointer(env, obj);
std::vector<PatchEngine::PatchEntry> entries;
std::vector<std::string> lines = SplitString(GetJString(env, code_string), '\n');
for (size_t i = 0; i < lines.size(); i++)
{
const std::string& line = lines[i];
if (line.empty())
continue;
if (std::optional<PatchEngine::PatchEntry> entry = PatchEngine::DeserializeLine(line))
entries.emplace_back(*std::move(entry));
else
return i + 1; // Parse error on line i
}
if (entries.empty())
return Cheats::TRY_SET_FAIL_NO_CODE_LINES;
patch->name = GetJString(env, name);
patch->entries = std::move(entries);
return Cheats::TRY_SET_SUCCESS;
}
JNIEXPORT void JNICALL
Java_org_dolphinemu_dolphinemu_features_cheats_model_PatchCheat_setEnabledImpl(JNIEnv* env,
jobject obj,
jboolean enabled)
{
GetPointer(env, obj)->enabled = static_cast<bool>(enabled);
}
JNIEXPORT jobjectArray JNICALL
Java_org_dolphinemu_dolphinemu_features_cheats_model_PatchCheat_loadCodes(JNIEnv* env, jclass,
jstring jGameID,
jint revision)
{
const std::string game_id = GetJString(env, jGameID);
IniFile game_ini_local;
// We don't use LoadLocalGameIni() here because user cheat codes that are installed via the UI
// will always be stored in GS/${GAMEID}.ini
game_ini_local.Load(File::GetUserPath(D_GAMESETTINGS_IDX) + game_id + ".ini");
const IniFile game_ini_default = SConfig::LoadDefaultGameIni(game_id, revision);
std::vector<PatchEngine::Patch> patches;
PatchEngine::LoadPatchSection("OnFrame", &patches, game_ini_default, game_ini_local);
const jobjectArray array = env->NewObjectArray(static_cast<jsize>(patches.size()),
IDCache::GetPatchCheatClass(), nullptr);
jsize i = 0;
for (const PatchEngine::Patch& patch : patches)
env->SetObjectArrayElement(array, i++, PatchCheatToJava(env, patch));
return array;
}
JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_features_cheats_model_PatchCheat_saveCodes(
JNIEnv* env, jclass, jstring jGameID, jint revision, jobjectArray jCodes)
{
const jsize size = env->GetArrayLength(jCodes);
std::vector<PatchEngine::Patch> vector;
vector.reserve(size);
for (jsize i = 0; i < size; ++i)
{
jobject code = reinterpret_cast<jstring>(env->GetObjectArrayElement(jCodes, i));
vector.emplace_back(*GetPointer(env, code));
env->DeleteLocalRef(code);
}
const std::string game_id = GetJString(env, jGameID);
const std::string ini_path = File::GetUserPath(D_GAMESETTINGS_IDX) + game_id + ".ini";
IniFile game_ini_local;
game_ini_local.Load(ini_path);
PatchEngine::SavePatchSection(&game_ini_local, vector);
game_ini_local.Save(ini_path);
}
}

View File

@ -17,10 +17,13 @@
namespace Gecko
{
std::vector<GeckoCode> DownloadCodes(std::string gametdb_id, bool* succeeded)
std::vector<GeckoCode> DownloadCodes(std::string gametdb_id, bool* succeeded, bool use_https)
{
// TODO: Fix https://bugs.dolphin-emu.org/issues/11772 so we don't need this workaround
const std::string protocol = use_https ? "https://" : "http://";
// codes.rc24.xyz is a mirror of the now defunct geckocodes.org.
std::string endpoint{"https://codes.rc24.xyz/txt.php?txt=" + gametdb_id};
std::string endpoint{protocol + "codes.rc24.xyz/txt.php?txt=" + gametdb_id};
Common::HttpRequest http;
// The server always redirects once to the same location.

View File

@ -14,7 +14,8 @@ class IniFile;
namespace Gecko
{
std::vector<GeckoCode> LoadCodes(const IniFile& globalIni, const IniFile& localIni);
std::vector<GeckoCode> DownloadCodes(std::string gametdb_id, bool* succeeded);
std::vector<GeckoCode> DownloadCodes(std::string gametdb_id, bool* succeeded,
bool use_https = true);
void SaveCodes(IniFile& inifile, const std::vector<GeckoCode>& gcodes);
std::optional<GeckoCode::Code> DeserializeLine(const std::string& line);