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";
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";
@ -40,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());
@ -54,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 =
@ -93,7 +96,7 @@ public class GamePropertiesDialog extends DialogFragment
SettingsActivity.launch(getContext(), MenuTag.SETTINGS, gameId, revision, isWii));
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) ->
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> 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;
@ -236,6 +237,38 @@ public class CheatsViewModel extends ViewModel
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;

View File

@ -4,6 +4,7 @@ package org.dolphinemu.dolphinemu.features.cheats.model;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public class GeckoCheat extends AbstractCheat
{
@ -26,6 +27,12 @@ public class GeckoCheat extends AbstractCheat
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;
@ -52,6 +59,8 @@ public class GeckoCheat extends AbstractCheat
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);
@ -63,4 +72,7 @@ public class GeckoCheat extends AbstractCheat
public static native GeckoCheat[] loadCodes(String gameId, int revision);
public static native void saveCodes(String gameId, int revision, GeckoCheat[] codes);
@Nullable
public static native GeckoCheat[] downloadCodes(String gameTdbId);
}

View File

@ -6,6 +6,7 @@ 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;
@ -17,6 +18,7 @@ public class ActionViewHolder extends CheatItemViewHolder implements View.OnClic
{
private final TextView mName;
private CheatsActivity mActivity;
private CheatsViewModel mViewModel;
private int mString;
private int mPosition;
@ -30,9 +32,10 @@ public class ActionViewHolder extends CheatItemViewHolder implements View.OnClic
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();
mPosition = position;
@ -56,5 +59,9 @@ public class ActionViewHolder extends CheatItemViewHolder implements View.OnClic
mViewModel.startAddingCheat(new PatchCheat(), mPosition);
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);
}
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 androidx.annotation.NonNull;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.RecyclerView.ViewHolder;
import org.dolphinemu.dolphinemu.R;
@ -34,11 +35,11 @@ public class CheatViewHolder extends CheatItemViewHolder
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);
mViewModel = viewModel;
mViewModel = new ViewModelProvider(activity).get(CheatsViewModel.class);
mCheat = item.getCheat();
mPosition = position;

View File

@ -9,6 +9,7 @@ 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;
@ -17,6 +18,7 @@ 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;
@ -25,10 +27,12 @@ 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;
@ -40,10 +44,12 @@ public class CheatsActivity extends AppCompatActivity
private View mCheatListLastFocus;
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.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);
@ -58,6 +64,7 @@ public class CheatsActivity extends AppCompatActivity
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);
@ -161,6 +168,49 @@ public class CheatsActivity extends AppCompatActivity
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 File

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

View File

@ -21,7 +21,7 @@ public class HeaderViewHolder extends CheatItemViewHolder
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());
}

View File

@ -395,6 +395,7 @@
<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>
@ -406,6 +407,10 @@
<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>

View File

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

View File

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

View File

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