Android: Add Gecko code downloading

This commit is contained in:
JosJuice 2021-08-11 17:17:30 +02:00
parent 47efd3317d
commit 53ae1a0725
13 changed files with 173 additions and 16 deletions

View File

@ -29,7 +29,8 @@ public class GamePropertiesDialog extends DialogFragment
{ {
public static final String TAG = "GamePropertiesDialog"; public static final String TAG = "GamePropertiesDialog";
private static final String ARG_PATH = "path"; 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"; public static final String ARG_REVISION = "revision";
private static final String ARG_PLATFORM = "platform"; private static final String ARG_PLATFORM = "platform";
private static final String ARG_SHOULD_ALLOW_CONVERSION = "should_allow_conversion"; private static final String ARG_SHOULD_ALLOW_CONVERSION = "should_allow_conversion";
@ -40,7 +41,8 @@ public class GamePropertiesDialog extends DialogFragment
Bundle arguments = new Bundle(); Bundle arguments = new Bundle();
arguments.putString(ARG_PATH, gameFile.getPath()); 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_REVISION, gameFile.getRevision());
arguments.putInt(ARG_PLATFORM, gameFile.getPlatform()); arguments.putInt(ARG_PLATFORM, gameFile.getPlatform());
arguments.putBoolean(ARG_SHOULD_ALLOW_CONVERSION, gameFile.shouldAllowConversion()); arguments.putBoolean(ARG_SHOULD_ALLOW_CONVERSION, gameFile.shouldAllowConversion());
@ -54,7 +56,8 @@ public class GamePropertiesDialog extends DialogFragment
public Dialog onCreateDialog(Bundle savedInstanceState) public Dialog onCreateDialog(Bundle savedInstanceState)
{ {
final String path = requireArguments().getString(ARG_PATH); 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 revision = requireArguments().getInt(ARG_REVISION);
final int platform = requireArguments().getInt(ARG_PLATFORM); final int platform = requireArguments().getInt(ARG_PLATFORM);
final boolean shouldAllowConversion = final boolean shouldAllowConversion =
@ -93,7 +96,7 @@ public class GamePropertiesDialog extends DialogFragment
SettingsActivity.launch(getContext(), MenuTag.SETTINGS, gameId, revision, isWii)); SettingsActivity.launch(getContext(), MenuTag.SETTINGS, gameId, revision, isWii));
itemsBuilder.add(R.string.properties_edit_cheats, (dialog, i) -> itemsBuilder.add(R.string.properties_edit_cheats, (dialog, i) ->
CheatsActivity.launch(getContext(), gameId, revision, isWii)); CheatsActivity.launch(getContext(), gameId, gameTdbId, revision, isWii));
itemsBuilder.add(R.string.properties_clear_game_settings, (dialog, i) -> itemsBuilder.add(R.string.properties_clear_game_settings, (dialog, i) ->
clearGameSettings(gameId)); clearGameSettings(gameId));

View File

@ -21,6 +21,7 @@ public class CheatsViewModel extends ViewModel
private final MutableLiveData<Integer> mCheatAddedEvent = new MutableLiveData<>(null); private final MutableLiveData<Integer> mCheatAddedEvent = new MutableLiveData<>(null);
private final MutableLiveData<Integer> mCheatChangedEvent = new MutableLiveData<>(null); private final MutableLiveData<Integer> mCheatChangedEvent = new MutableLiveData<>(null);
private final MutableLiveData<Integer> mCheatDeletedEvent = 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 final MutableLiveData<Boolean> mOpenDetailsViewEvent = new MutableLiveData<>(false);
private ArrayList<PatchCheat> mPatchCheats; private ArrayList<PatchCheat> mPatchCheats;
@ -236,6 +237,38 @@ public class CheatsViewModel extends ViewModel
mCheatDeletedEvent.setValue(null); 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() public LiveData<Boolean> getOpenDetailsViewEvent()
{ {
return mOpenDetailsViewEvent; return mOpenDetailsViewEvent;

View File

@ -4,6 +4,7 @@ package org.dolphinemu.dolphinemu.features.cheats.model;
import androidx.annotation.Keep; import androidx.annotation.Keep;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public class GeckoCheat extends AbstractCheat public class GeckoCheat extends AbstractCheat
{ {
@ -26,6 +27,12 @@ public class GeckoCheat extends AbstractCheat
private native long createNew(); private native long createNew();
@Override
public boolean equals(@Nullable Object obj)
{
return obj != null && getClass() == obj.getClass() && equalsImpl((GeckoCheat) obj);
}
public boolean supportsCreator() public boolean supportsCreator()
{ {
return true; return true;
@ -52,6 +59,8 @@ public class GeckoCheat extends AbstractCheat
public native boolean getEnabled(); public native boolean getEnabled();
public native boolean equalsImpl(@NonNull GeckoCheat other);
@Override @Override
protected native int trySetImpl(@NonNull String name, @NonNull String creator, protected native int trySetImpl(@NonNull String name, @NonNull String creator,
@NonNull String notes, @NonNull String code); @NonNull String notes, @NonNull String code);
@ -63,4 +72,7 @@ public class GeckoCheat extends AbstractCheat
public static native GeckoCheat[] loadCodes(String gameId, int revision); public static native GeckoCheat[] loadCodes(String gameId, int revision);
public static native void saveCodes(String gameId, int revision, GeckoCheat[] codes); public static native void saveCodes(String gameId, int revision, GeckoCheat[] codes);
@Nullable
public static native GeckoCheat[] downloadCodes(String gameTdbId);
} }

View File

@ -6,6 +6,7 @@ import android.view.View;
import android.widget.TextView; import android.widget.TextView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.lifecycle.ViewModelProvider;
import org.dolphinemu.dolphinemu.R; import org.dolphinemu.dolphinemu.R;
import org.dolphinemu.dolphinemu.features.cheats.model.ARCheat; import org.dolphinemu.dolphinemu.features.cheats.model.ARCheat;
@ -17,6 +18,7 @@ public class ActionViewHolder extends CheatItemViewHolder implements View.OnClic
{ {
private final TextView mName; private final TextView mName;
private CheatsActivity mActivity;
private CheatsViewModel mViewModel; private CheatsViewModel mViewModel;
private int mString; private int mString;
private int mPosition; private int mPosition;
@ -30,9 +32,10 @@ public class ActionViewHolder extends CheatItemViewHolder implements View.OnClic
itemView.setOnClickListener(this); itemView.setOnClickListener(this);
} }
public void bind(CheatsViewModel viewModel, CheatItem item, int position) public void bind(CheatsActivity activity, CheatItem item, int position)
{ {
mViewModel = viewModel; mActivity = activity;
mViewModel = new ViewModelProvider(activity).get(CheatsViewModel.class);
mString = item.getString(); mString = item.getString();
mPosition = position; mPosition = position;
@ -56,5 +59,9 @@ public class ActionViewHolder extends CheatItemViewHolder implements View.OnClic
mViewModel.startAddingCheat(new PatchCheat(), mPosition); mViewModel.startAddingCheat(new PatchCheat(), mPosition);
mViewModel.openDetailsView(); mViewModel.openDetailsView();
} }
else if (mString == R.string.cheats_download_gecko)
{
mActivity.downloadGeckoCodes();
}
} }
} }

View File

@ -16,5 +16,5 @@ public abstract class CheatItemViewHolder extends RecyclerView.ViewHolder
super(itemView); super(itemView);
} }
public abstract void bind(CheatsViewModel viewModel, CheatItem item, int position); public abstract void bind(CheatsActivity activity, CheatItem item, int position);
} }

View File

@ -8,6 +8,7 @@ import android.widget.CompoundButton;
import android.widget.TextView; import android.widget.TextView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.RecyclerView.ViewHolder; import androidx.recyclerview.widget.RecyclerView.ViewHolder;
import org.dolphinemu.dolphinemu.R; import org.dolphinemu.dolphinemu.R;
@ -34,11 +35,11 @@ public class CheatViewHolder extends CheatItemViewHolder
mCheckbox = itemView.findViewById(R.id.checkbox); mCheckbox = itemView.findViewById(R.id.checkbox);
} }
public void bind(CheatsViewModel viewModel, CheatItem item, int position) public void bind(CheatsActivity activity, CheatItem item, int position)
{ {
mCheckbox.setOnCheckedChangeListener(null); mCheckbox.setOnCheckedChangeListener(null);
mViewModel = viewModel; mViewModel = new ViewModelProvider(activity).get(CheatsViewModel.class);
mCheat = item.getCheat(); mCheat = item.getCheat();
mPosition = position; mPosition = position;

View File

@ -9,6 +9,7 @@ import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.core.view.ViewCompat; import androidx.core.view.ViewCompat;
import androidx.lifecycle.ViewModelProvider; import androidx.lifecycle.ViewModelProvider;
@ -17,6 +18,7 @@ import androidx.slidingpanelayout.widget.SlidingPaneLayout;
import org.dolphinemu.dolphinemu.R; import org.dolphinemu.dolphinemu.R;
import org.dolphinemu.dolphinemu.features.cheats.model.Cheat; import org.dolphinemu.dolphinemu.features.cheats.model.Cheat;
import org.dolphinemu.dolphinemu.features.cheats.model.CheatsViewModel; 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.features.settings.model.Settings;
import org.dolphinemu.dolphinemu.ui.TwoPaneOnBackPressedCallback; import org.dolphinemu.dolphinemu.ui.TwoPaneOnBackPressedCallback;
import org.dolphinemu.dolphinemu.ui.main.MainPresenter; import org.dolphinemu.dolphinemu.ui.main.MainPresenter;
@ -25,10 +27,12 @@ public class CheatsActivity extends AppCompatActivity
implements SlidingPaneLayout.PanelSlideListener implements SlidingPaneLayout.PanelSlideListener
{ {
private static final String ARG_GAME_ID = "game_id"; 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_REVISION = "revision";
private static final String ARG_IS_WII = "is_wii"; private static final String ARG_IS_WII = "is_wii";
private String mGameId; private String mGameId;
private String mGameTdbId;
private int mRevision; private int mRevision;
private boolean mIsWii; private boolean mIsWii;
private CheatsViewModel mViewModel; private CheatsViewModel mViewModel;
@ -40,10 +44,12 @@ public class CheatsActivity extends AppCompatActivity
private View mCheatListLastFocus; private View mCheatListLastFocus;
private View mCheatDetailsLastFocus; private View mCheatDetailsLastFocus;
public static void launch(Context context, String gameId, int revision, boolean isWii) public static void launch(Context context, String gameId, String gameTdbId, int revision,
boolean isWii)
{ {
Intent intent = new Intent(context, CheatsActivity.class); Intent intent = new Intent(context, CheatsActivity.class);
intent.putExtra(ARG_GAME_ID, gameId); intent.putExtra(ARG_GAME_ID, gameId);
intent.putExtra(ARG_GAMETDB_ID, gameTdbId);
intent.putExtra(ARG_REVISION, revision); intent.putExtra(ARG_REVISION, revision);
intent.putExtra(ARG_IS_WII, isWii); intent.putExtra(ARG_IS_WII, isWii);
context.startActivity(intent); context.startActivity(intent);
@ -58,6 +64,7 @@ public class CheatsActivity extends AppCompatActivity
Intent intent = getIntent(); Intent intent = getIntent();
mGameId = intent.getStringExtra(ARG_GAME_ID); mGameId = intent.getStringExtra(ARG_GAME_ID);
mGameTdbId = intent.getStringExtra(ARG_GAMETDB_ID);
mRevision = intent.getIntExtra(ARG_REVISION, 0); mRevision = intent.getIntExtra(ARG_REVISION, 0);
mIsWii = intent.getBooleanExtra(ARG_IS_WII, true); mIsWii = intent.getBooleanExtra(ARG_IS_WII, true);
@ -161,6 +168,49 @@ public class CheatsActivity extends AppCompatActivity
return settings; 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, public static void setOnFocusChangeListenerRecursively(@NonNull View view,
View.OnFocusChangeListener listener) View.OnFocusChangeListener listener)
{ {

View File

@ -45,6 +45,16 @@ public class CheatsAdapter extends RecyclerView.Adapter<CheatItemViewHolder>
if (position != null) if (position != null)
notifyItemRemoved(position); 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 @NonNull
@ -75,14 +85,14 @@ public class CheatsAdapter extends RecyclerView.Adapter<CheatItemViewHolder>
@Override @Override
public void onBindViewHolder(@NonNull CheatItemViewHolder holder, int position) public void onBindViewHolder(@NonNull CheatItemViewHolder holder, int position)
{ {
holder.bind(mViewModel, getItemAt(position), position); holder.bind(mActivity, getItemAt(position), position);
} }
@Override @Override
public int getItemCount() public int getItemCount()
{ {
return mViewModel.getARCheats().size() + mViewModel.getGeckoCheats().size() + return mViewModel.getARCheats().size() + mViewModel.getGeckoCheats().size() +
mViewModel.getPatchCheats().size() + 6; mViewModel.getPatchCheats().size() + 7;
} }
@Override @Override
@ -144,6 +154,9 @@ public class CheatsAdapter extends RecyclerView.Adapter<CheatItemViewHolder>
return new CheatItem(CheatItem.TYPE_ACTION, R.string.cheats_add_gecko); return new CheatItem(CheatItem.TYPE_ACTION, R.string.cheats_add_gecko);
position -= 1; position -= 1;
if (position == 0)
return new CheatItem(CheatItem.TYPE_ACTION, R.string.cheats_download_gecko);
throw new IndexOutOfBoundsException(); throw new IndexOutOfBoundsException();
} }
} }

View File

@ -21,7 +21,7 @@ public class HeaderViewHolder extends CheatItemViewHolder
mHeaderName = itemView.findViewById(R.id.text_header_name); mHeaderName = itemView.findViewById(R.id.text_header_name);
} }
public void bind(CheatsViewModel viewModel, CheatItem item, int position) public void bind(CheatsActivity activity, CheatItem item, int position)
{ {
mHeaderName.setText(item.getString()); mHeaderName.setText(item.getString());
} }

View File

@ -395,6 +395,7 @@
<string name="cheats_add_ar">Add New AR Code</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_gecko">Add New Gecko Code</string>
<string name="cheats_add_patch">Add New Patch</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_name">Name</string>
<string name="cheats_creator">Creator</string> <string name="cheats_creator">Creator</string>
<string name="cheats_notes">Notes</string> <string name="cheats_notes">Notes</string>
@ -406,6 +407,10 @@
<string name="cheats_error_no_code_lines">Code 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_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_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_disabled_warning">Dolphin\'s cheat system is currently disabled.</string>
<string name="cheats_open_settings">Settings</string> <string name="cheats_open_settings">Settings</string>

View File

@ -92,6 +92,13 @@ Java_org_dolphinemu_dolphinemu_features_cheats_model_GeckoCheat_getEnabled(JNIEn
return static_cast<jboolean>(GetPointer(env, obj)->enabled); 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( 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) JNIEnv* env, jobject obj, jstring name, jstring creator, jstring notes, jstring code_string)
{ {
@ -180,4 +187,26 @@ JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_features_cheats_model_Geck
Gecko::SaveCodes(game_ini_local, vector); Gecko::SaveCodes(game_ini_local, vector);
game_ini_local.Save(ini_path); game_ini_local.Save(ini_path);
} }
JNIEXPORT jobjectArray JNICALL
Java_org_dolphinemu_dolphinemu_features_cheats_model_GeckoCheat_downloadCodes(JNIEnv* env, jclass,
jstring jGameTdbId)
{
const std::string gametdb_id = GetJString(env, jGameTdbId);
bool success = true;
const std::vector<Gecko::GeckoCode> codes = Gecko::DownloadCodes(gametdb_id, &success, false);
if (!success)
return nullptr;
const jobjectArray array =
env->NewObjectArray(static_cast<jsize>(codes.size()), IDCache::GetGeckoCheatClass(), nullptr);
jsize i = 0;
for (const Gecko::GeckoCode& code : codes)
env->SetObjectArrayElement(array, i++, GeckoCheatToJava(env, code));
return array;
}
} }

View File

@ -17,10 +17,13 @@
namespace Gecko 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. // 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; Common::HttpRequest http;
// The server always redirects once to the same location. // The server always redirects once to the same location.

View File

@ -14,7 +14,8 @@ class IniFile;
namespace Gecko namespace Gecko
{ {
std::vector<GeckoCode> LoadCodes(const IniFile& globalIni, const IniFile& localIni); 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); void SaveCodes(IniFile& inifile, const std::vector<GeckoCode>& gcodes);
std::optional<GeckoCode::Code> DeserializeLine(const std::string& line); std::optional<GeckoCode::Code> DeserializeLine(const std::string& line);