mirror of
https://github.com/dolphin-emu/dolphin.git
synced 2025-02-12 23:48:58 +01:00
Merge pull request #10092 from JosJuice/android-cheats
Android: Add cheat GUI
This commit is contained in:
commit
7379450633
@ -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.
|
||||
|
@ -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"
|
||||
|
@ -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));
|
||||
|
||||
|
@ -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);
|
||||
}
|
@ -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);
|
||||
}
|
@ -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);
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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);
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
22
Source/Android/app/src/main/res/layout/activity_cheats.xml
Normal file
22
Source/Android/app/src/main/res/layout/activity_cheats.xml
Normal 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>
|
@ -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>
|
@ -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>
|
@ -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>
|
37
Source/Android/app/src/main/res/layout/list_item_cheat.xml
Normal file
37
Source/Android/app/src/main/res/layout/list_item_cheat.xml
Normal 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>
|
@ -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>
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
182
Source/Android/jni/Cheats/ARCheat.cpp
Normal file
182
Source/Android/jni/Cheats/ARCheat.cpp
Normal 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);
|
||||
}
|
||||
}
|
13
Source/Android/jni/Cheats/Cheats.h
Normal file
13
Source/Android/jni/Cheats/Cheats.h
Normal 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
|
212
Source/Android/jni/Cheats/GeckoCheat.cpp
Normal file
212
Source/Android/jni/Cheats/GeckoCheat.cpp
Normal 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;
|
||||
}
|
||||
}
|
169
Source/Android/jni/Cheats/PatchCheat.cpp
Normal file
169
Source/Android/jni/Cheats/PatchCheat.cpp
Normal 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);
|
||||
}
|
||||
}
|
@ -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.
|
||||
|
@ -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);
|
||||
|
Loading…
x
Reference in New Issue
Block a user