| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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(); | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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)); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); | ||
| } | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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()); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); | ||
| } | ||
| } | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); | ||
| } | ||
| } |