From 22a1f3422cd99fd88c2671d05955a4860dc6e959 Mon Sep 17 00:00:00 2001 From: JosJuice Date: Sun, 24 Oct 2021 21:06:59 +0200 Subject: [PATCH] Android: Add Riivolution patch configuration --- .../riivolution/model/RiivolutionPatches.java | 86 ++++++++ .../riivolution/ui/RiivolutionAdapter.java | 84 ++++++++ .../ui/RiivolutionBootActivity.java | 41 +++- .../riivolution/ui/RiivolutionItem.java | 40 ++++ .../riivolution/ui/RiivolutionViewHolder.java | 84 ++++++++ .../res/layout/activity_riivolution_boot.xml | 3 +- .../layout/list_item_riivolution_header.xml | 9 + .../layout/list_item_riivolution_option.xml | 30 +++ .../app/src/main/res/values/strings.xml | 1 + Source/Android/jni/AndroidCommon/IDCache.cpp | 21 ++ Source/Android/jni/AndroidCommon/IDCache.h | 3 + Source/Android/jni/CMakeLists.txt | 1 + Source/Android/jni/RiivolutionPatches.cpp | 193 ++++++++++++++++++ 13 files changed, 594 insertions(+), 2 deletions(-) create mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/riivolution/model/RiivolutionPatches.java create mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/riivolution/ui/RiivolutionAdapter.java create mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/riivolution/ui/RiivolutionItem.java create mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/riivolution/ui/RiivolutionViewHolder.java create mode 100644 Source/Android/app/src/main/res/layout/list_item_riivolution_header.xml create mode 100644 Source/Android/app/src/main/res/layout/list_item_riivolution_option.xml create mode 100644 Source/Android/jni/RiivolutionPatches.cpp diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/riivolution/model/RiivolutionPatches.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/riivolution/model/RiivolutionPatches.java new file mode 100644 index 0000000000..6d2ff51b6b --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/riivolution/model/RiivolutionPatches.java @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.features.riivolution.model; + +import androidx.annotation.Keep; + +public class RiivolutionPatches +{ + private String mGameId; + private int mRevision; + private int mDiscNumber; + + private boolean mUnsavedChanges = false; + + @Keep + private long mPointer; + + public RiivolutionPatches(String gameId, int revision, int discNumber) + { + mGameId = gameId; + mRevision = revision; + mDiscNumber = discNumber; + + mPointer = initialize(); + } + + @Override + public native void finalize(); + + private static native long initialize(); + + public native int getDiscCount(); + + public native String getDiscName(int discIndex); + + public native int getSectionCount(int discIndex); + + public native String getSectionName(int discIndex, int sectionIndex); + + public native int getOptionCount(int discIndex, int sectionIndex); + + public native String getOptionName(int discIndex, int sectionIndex, int optionIndex); + + public native int getChoiceCount(int discIndex, int sectionIndex, int optionIndex); + + public native String getChoiceName(int discIndex, int sectionIndex, int optionIndex, + int choiceIndex); + + /** + * @return 0 if no choice is selected, otherwise the index of the selected choice plus one. + */ + public native int getSelectedChoice(int discIndex, int sectionIndex, int optionIndex); + + /** + * @param choiceIndex 0 to select no choice, otherwise the choice index plus one. + */ + public void setSelectedChoice(int discIndex, int sectionIndex, int optionIndex, int choiceIndex) + { + mUnsavedChanges = true; + setSelectedChoiceImpl(discIndex, sectionIndex, optionIndex, choiceIndex); + } + + /** + * @param choiceIndex 0 to select no choice, otherwise the choice index plus one. + */ + private native void setSelectedChoiceImpl(int discIndex, int sectionIndex, int optionIndex, + int choiceIndex); + + public void loadConfig() + { + loadConfigImpl(mGameId, mRevision, mDiscNumber); + } + + private native void loadConfigImpl(String gameId, int revision, int discNumber); + + public void saveConfig() + { + if (mUnsavedChanges) + { + mUnsavedChanges = false; + saveConfigImpl(mGameId); + } + } + + private native void saveConfigImpl(String gameId); +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/riivolution/ui/RiivolutionAdapter.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/riivolution/ui/RiivolutionAdapter.java new file mode 100644 index 0000000000..d1fe8ca5b3 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/riivolution/ui/RiivolutionAdapter.java @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.features.riivolution.ui; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import org.dolphinemu.dolphinemu.R; +import org.dolphinemu.dolphinemu.features.riivolution.model.RiivolutionPatches; + +import java.util.ArrayList; + +public class RiivolutionAdapter extends RecyclerView.Adapter +{ + private final Context mContext; + private final RiivolutionPatches mPatches; + private final ArrayList mItems = new ArrayList<>(); + + public RiivolutionAdapter(Context context, RiivolutionPatches patches) + { + mContext = context; + mPatches = patches; + + int discCount = mPatches.getDiscCount(); + for (int i = 0; i < discCount; i++) + { + mItems.add(new RiivolutionItem(i)); + + int sectionCount = mPatches.getSectionCount(i); + for (int j = 0; j < sectionCount; j++) + { + mItems.add(new RiivolutionItem(i, j)); + + int optionCount = mPatches.getOptionCount(i, j); + for (int k = 0; k < optionCount; k++) + { + mItems.add(new RiivolutionItem(i, j, k)); + } + } + } + } + + @NonNull @Override + public RiivolutionViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) + { + LayoutInflater inflater = LayoutInflater.from(parent.getContext()); + + switch (viewType) + { + case RiivolutionViewHolder.TYPE_HEADER: + View headerView = inflater.inflate(R.layout.list_item_riivolution_header, parent, false); + return new RiivolutionViewHolder(headerView); + case RiivolutionViewHolder.TYPE_OPTION: + View optionView = inflater.inflate(R.layout.list_item_riivolution_option, parent, false); + return new RiivolutionViewHolder(optionView); + default: + throw new UnsupportedOperationException(); + } + } + + @Override + public void onBindViewHolder(@NonNull RiivolutionViewHolder holder, int position) + { + holder.bind(mContext, mPatches, mItems.get(position)); + } + + @Override + public int getItemCount() + { + return mItems.size(); + } + + @Override + public int getItemViewType(int position) + { + return mItems.get(position).mOptionIndex != -1 ? + RiivolutionViewHolder.TYPE_OPTION : RiivolutionViewHolder.TYPE_HEADER; + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/riivolution/ui/RiivolutionBootActivity.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/riivolution/ui/RiivolutionBootActivity.java index 1b6e55df98..37921c4af8 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/riivolution/ui/RiivolutionBootActivity.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/riivolution/ui/RiivolutionBootActivity.java @@ -7,10 +7,15 @@ import android.content.Intent; import android.os.Bundle; import android.widget.Button; +import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; import org.dolphinemu.dolphinemu.R; import org.dolphinemu.dolphinemu.activities.EmulationActivity; +import org.dolphinemu.dolphinemu.features.riivolution.model.RiivolutionPatches; +import org.dolphinemu.dolphinemu.ui.DividerItemDecoration; public class RiivolutionBootActivity extends AppCompatActivity { @@ -19,6 +24,8 @@ public class RiivolutionBootActivity extends AppCompatActivity private static final String ARG_REVISION = "revision"; private static final String ARG_DISC_NUMBER = "disc_number"; + private RiivolutionPatches mPatches; + public static void launch(Context context, String gamePath, String gameId, int revision, int discNumber) { @@ -45,6 +52,38 @@ public class RiivolutionBootActivity extends AppCompatActivity int discNumber = intent.getIntExtra(ARG_DISC_NUMBER, -1); Button buttonStart = findViewById(R.id.button_start); - buttonStart.setOnClickListener((v) -> EmulationActivity.launch(this, path, true)); + buttonStart.setOnClickListener((v) -> + { + if (mPatches != null) + mPatches.saveConfig(); + + EmulationActivity.launch(this, path, true); + }); + + new Thread(() -> + { + RiivolutionPatches patches = new RiivolutionPatches(gameId, revision, discNumber); + patches.loadConfig(); + runOnUiThread(() -> populateList(patches)); + }).start(); + } + + @Override + protected void onStop() + { + super.onStop(); + + if (mPatches != null) + mPatches.saveConfig(); + } + + private void populateList(RiivolutionPatches patches) + { + mPatches = patches; + + RecyclerView recyclerView = findViewById(R.id.recycler_view); + + recyclerView.setAdapter(new RiivolutionAdapter(this, patches)); + recyclerView.setLayoutManager(new LinearLayoutManager(this)); } } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/riivolution/ui/RiivolutionItem.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/riivolution/ui/RiivolutionItem.java new file mode 100644 index 0000000000..8d2ef17e5b --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/riivolution/ui/RiivolutionItem.java @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.features.riivolution.ui; + +public class RiivolutionItem +{ + public final int mDiscIndex; + public final int mSectionIndex; + public final int mOptionIndex; + + /** + * Constructor for a disc. + */ + public RiivolutionItem(int discIndex) + { + mDiscIndex = discIndex; + mSectionIndex = -1; + mOptionIndex = -1; + } + + /** + * Constructor for a section. + */ + public RiivolutionItem(int discIndex, int sectionIndex) + { + mDiscIndex = discIndex; + mSectionIndex = sectionIndex; + mOptionIndex = -1; + } + + /** + * Constructor for an option. + */ + public RiivolutionItem(int discIndex, int sectionIndex, int optionIndex) + { + mDiscIndex = discIndex; + mSectionIndex = sectionIndex; + mOptionIndex = optionIndex; + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/riivolution/ui/RiivolutionViewHolder.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/riivolution/ui/RiivolutionViewHolder.java new file mode 100644 index 0000000000..e51a575262 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/riivolution/ui/RiivolutionViewHolder.java @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.features.riivolution.ui; + +import android.content.Context; +import android.view.View; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.Spinner; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.RecyclerView; + +import org.dolphinemu.dolphinemu.R; +import org.dolphinemu.dolphinemu.features.riivolution.model.RiivolutionPatches; + +public class RiivolutionViewHolder extends RecyclerView.ViewHolder + implements AdapterView.OnItemSelectedListener +{ + public static final int TYPE_HEADER = 0; + public static final int TYPE_OPTION = 1; + + private final TextView mTextView; + private final Spinner mSpinner; + + private RiivolutionPatches mPatches; + private RiivolutionItem mItem; + + public RiivolutionViewHolder(@NonNull View itemView) + { + super(itemView); + + mTextView = itemView.findViewById(R.id.text_name); + mSpinner = itemView.findViewById(R.id.spinner_choice); + } + + public void bind(Context context, RiivolutionPatches patches, RiivolutionItem item) + { + String text; + if (item.mOptionIndex != -1) + text = patches.getOptionName(item.mDiscIndex, item.mSectionIndex, item.mOptionIndex); + else if (item.mSectionIndex != -1) + text = patches.getSectionName(item.mDiscIndex, item.mSectionIndex); + else + text = patches.getDiscName(item.mDiscIndex); + mTextView.setText(text); + + if (item.mOptionIndex != -1) + { + mPatches = patches; + mItem = item; + + ArrayAdapter adapter = new ArrayAdapter<>(context, + R.layout.list_item_riivolution_header); + + int choiceCount = patches.getChoiceCount(mItem.mDiscIndex, mItem.mSectionIndex, + mItem.mOptionIndex); + adapter.add(context.getString(R.string.riivolution_disabled)); + for (int i = 0; i < choiceCount; i++) + { + adapter.add(patches.getChoiceName(mItem.mDiscIndex, mItem.mSectionIndex, mItem.mOptionIndex, + i)); + } + + mSpinner.setAdapter(adapter); + mSpinner.setSelection(patches.getSelectedChoice(mItem.mDiscIndex, mItem.mSectionIndex, + mItem.mOptionIndex)); + mSpinner.setOnItemSelectedListener(this); + } + } + + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) + { + mPatches.setSelectedChoice(mItem.mDiscIndex, mItem.mSectionIndex, mItem.mOptionIndex, position); + } + + @Override + public void onNothingSelected(AdapterView parent) + { + } +} diff --git a/Source/Android/app/src/main/res/layout/activity_riivolution_boot.xml b/Source/Android/app/src/main/res/layout/activity_riivolution_boot.xml index 6ca71f5b1f..3d136367d5 100644 --- a/Source/Android/app/src/main/res/layout/activity_riivolution_boot.xml +++ b/Source/Android/app/src/main/res/layout/activity_riivolution_boot.xml @@ -18,7 +18,8 @@ + android:layout_height="0dp" + android:layout_marginTop="@dimen/spacing_medlarge" /> diff --git a/Source/Android/app/src/main/res/layout/list_item_riivolution_header.xml b/Source/Android/app/src/main/res/layout/list_item_riivolution_header.xml new file mode 100644 index 0000000000..b31ee13c05 --- /dev/null +++ b/Source/Android/app/src/main/res/layout/list_item_riivolution_header.xml @@ -0,0 +1,9 @@ + + diff --git a/Source/Android/app/src/main/res/layout/list_item_riivolution_option.xml b/Source/Android/app/src/main/res/layout/list_item_riivolution_option.xml new file mode 100644 index 0000000000..a778c3bfea --- /dev/null +++ b/Source/Android/app/src/main/res/layout/list_item_riivolution_option.xml @@ -0,0 +1,30 @@ + + + + + + + + diff --git a/Source/Android/app/src/main/res/values/strings.xml b/Source/Android/app/src/main/res/values/strings.xml index 2a74122e6e..56feb15e21 100644 --- a/Source/Android/app/src/main/res/values/strings.xml +++ b/Source/Android/app/src/main/res/values/strings.xml @@ -482,6 +482,7 @@ It can efficiently compress both junk data and encrypted Wii data. + Disabled Start diff --git a/Source/Android/jni/AndroidCommon/IDCache.cpp b/Source/Android/jni/AndroidCommon/IDCache.cpp index d0a7710e1a..a5a52a4f36 100644 --- a/Source/Android/jni/AndroidCommon/IDCache.cpp +++ b/Source/Android/jni/AndroidCommon/IDCache.cpp @@ -70,6 +70,9 @@ static jclass s_patch_cheat_class; static jfieldID s_patch_cheat_pointer; static jmethodID s_patch_cheat_constructor; +static jclass s_riivolution_patches_class; +static jfieldID s_riivolution_patches_pointer; + namespace IDCache { JNIEnv* GetEnvForThread() @@ -325,6 +328,16 @@ jmethodID GetPatchCheatConstructor() return s_patch_cheat_constructor; } +jclass GetRiivolutionPatchesClass() +{ + return s_riivolution_patches_class; +} + +jfieldID GetRiivolutionPatchesPointer() +{ + return s_riivolution_patches_pointer; +} + } // namespace IDCache extern "C" { @@ -454,6 +467,13 @@ JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) s_patch_cheat_constructor = env->GetMethodID(patch_cheat_class, "", "(J)V"); env->DeleteLocalRef(patch_cheat_class); + const jclass riivolution_patches_class = + env->FindClass("org/dolphinemu/dolphinemu/features/riivolution/model/RiivolutionPatches"); + s_riivolution_patches_class = + reinterpret_cast(env->NewGlobalRef(riivolution_patches_class)); + s_riivolution_patches_pointer = env->GetFieldID(riivolution_patches_class, "mPointer", "J"); + env->DeleteLocalRef(riivolution_patches_class); + return JNI_VERSION; } @@ -477,5 +497,6 @@ JNIEXPORT void JNI_OnUnload(JavaVM* vm, void* reserved) env->DeleteGlobalRef(s_ar_cheat_class); env->DeleteGlobalRef(s_gecko_cheat_class); env->DeleteGlobalRef(s_patch_cheat_class); + env->DeleteGlobalRef(s_riivolution_patches_class); } } diff --git a/Source/Android/jni/AndroidCommon/IDCache.h b/Source/Android/jni/AndroidCommon/IDCache.h index 3268c842c3..8dc93d60ba 100644 --- a/Source/Android/jni/AndroidCommon/IDCache.h +++ b/Source/Android/jni/AndroidCommon/IDCache.h @@ -69,4 +69,7 @@ jclass GetPatchCheatClass(); jfieldID GetPatchCheatPointer(); jmethodID GetPatchCheatConstructor(); +jclass GetRiivolutionPatchesClass(); +jfieldID GetRiivolutionPatchesPointer(); + } // namespace IDCache diff --git a/Source/Android/jni/CMakeLists.txt b/Source/Android/jni/CMakeLists.txt index 6d28a334cf..74d9d90e03 100644 --- a/Source/Android/jni/CMakeLists.txt +++ b/Source/Android/jni/CMakeLists.txt @@ -10,6 +10,7 @@ add_library(main SHARED GameList/GameFileCache.cpp IniFile.cpp MainAndroid.cpp + RiivolutionPatches.cpp WiiUtils.cpp ) diff --git a/Source/Android/jni/RiivolutionPatches.cpp b/Source/Android/jni/RiivolutionPatches.cpp new file mode 100644 index 0000000000..6d088fe029 --- /dev/null +++ b/Source/Android/jni/RiivolutionPatches.cpp @@ -0,0 +1,193 @@ +// Copyright 2021 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include + +#include +#include + +#include "Common/FileSearch.h" +#include "Common/FileUtil.h" +#include "Common/StringUtil.h" +#include "DiscIO/RiivolutionParser.h" +#include "jni/AndroidCommon/AndroidCommon.h" +#include "jni/AndroidCommon/IDCache.h" + +static std::vector* GetPointer(JNIEnv* env, jobject obj) +{ + return reinterpret_cast*>( + env->GetLongField(obj, IDCache::GetRiivolutionPatchesPointer())); +} + +static std::vector& GetReference(JNIEnv* env, jobject obj) +{ + return *GetPointer(env, obj); +} + +extern "C" { + +JNIEXPORT jlong JNICALL +Java_org_dolphinemu_dolphinemu_features_riivolution_model_RiivolutionPatches_initialize(JNIEnv* env, + jclass obj) +{ + return reinterpret_cast(new std::vector); +} + +JNIEXPORT void JNICALL +Java_org_dolphinemu_dolphinemu_features_riivolution_model_RiivolutionPatches_finalize(JNIEnv* env, + jobject obj) +{ + delete GetPointer(env, obj); +} + +JNIEXPORT jint JNICALL +Java_org_dolphinemu_dolphinemu_features_riivolution_model_RiivolutionPatches_getDiscCount( + JNIEnv* env, jobject obj) +{ + return GetReference(env, obj).size(); +} + +JNIEXPORT jstring JNICALL +Java_org_dolphinemu_dolphinemu_features_riivolution_model_RiivolutionPatches_getDiscName( + JNIEnv* env, jobject obj, jint disc_index) +{ + std::string filename, extension; + SplitPath(GetReference(env, obj)[disc_index].m_xml_path, nullptr, &filename, &extension); + return ToJString(env, filename + extension); +} + +JNIEXPORT jint JNICALL +Java_org_dolphinemu_dolphinemu_features_riivolution_model_RiivolutionPatches_getSectionCount( + JNIEnv* env, jobject obj, jint disc_index) +{ + return GetReference(env, obj)[disc_index].m_sections.size(); +} + +JNIEXPORT jstring JNICALL +Java_org_dolphinemu_dolphinemu_features_riivolution_model_RiivolutionPatches_getSectionName( + JNIEnv* env, jobject obj, jint disc_index, jint section_index) +{ + return ToJString(env, GetReference(env, obj)[disc_index].m_sections[section_index].m_name); +} + +JNIEXPORT jint JNICALL +Java_org_dolphinemu_dolphinemu_features_riivolution_model_RiivolutionPatches_getOptionCount( + JNIEnv* env, jobject obj, jint disc_index, jint section_index) +{ + return GetReference(env, obj)[disc_index].m_sections[section_index].m_options.size(); +} + +JNIEXPORT jstring JNICALL +Java_org_dolphinemu_dolphinemu_features_riivolution_model_RiivolutionPatches_getOptionName( + JNIEnv* env, jobject obj, jint disc_index, jint section_index, jint option_index) +{ + return ToJString( + env, + GetReference(env, obj)[disc_index].m_sections[section_index].m_options[option_index].m_name); +} + +JNIEXPORT jint JNICALL +Java_org_dolphinemu_dolphinemu_features_riivolution_model_RiivolutionPatches_getChoiceCount( + JNIEnv* env, jobject obj, jint disc_index, jint section_index, jint option_index) +{ + return GetReference(env, obj)[disc_index] + .m_sections[section_index] + .m_options[option_index] + .m_choices.size(); +} + +JNIEXPORT jstring JNICALL +Java_org_dolphinemu_dolphinemu_features_riivolution_model_RiivolutionPatches_getChoiceName( + JNIEnv* env, jobject obj, jint disc_index, jint section_index, jint option_index, + jint choice_index) +{ + return ToJString(env, GetReference(env, obj)[disc_index] + .m_sections[section_index] + .m_options[option_index] + .m_choices[choice_index] + .m_name); +} + +JNIEXPORT jint JNICALL +Java_org_dolphinemu_dolphinemu_features_riivolution_model_RiivolutionPatches_getSelectedChoice( + JNIEnv* env, jobject obj, jint disc_index, jint section_index, jint option_index) +{ + return GetReference(env, obj)[disc_index] + .m_sections[section_index] + .m_options[option_index] + .m_selected_choice; +} + +JNIEXPORT void JNICALL +Java_org_dolphinemu_dolphinemu_features_riivolution_model_RiivolutionPatches_setSelectedChoiceImpl( + JNIEnv* env, jobject obj, jint disc_index, jint section_index, jint option_index, + jint choice_index) +{ + GetReference(env, obj)[disc_index] + .m_sections[section_index] + .m_options[option_index] + .m_selected_choice = choice_index; +} + +static std::optional LoadConfigXML(const std::string& root_directory, + std::string_view game_id) +{ + // The way Riivolution stores settings only makes sense for standard game IDs. + if (!(game_id.size() == 4 || game_id.size() == 6)) + return std::nullopt; + + return DiscIO::Riivolution::ParseConfigFile( + fmt::format("{}/riivolution/config/{}.xml", root_directory, game_id.substr(0, 4))); +} + +JNIEXPORT void JNICALL +Java_org_dolphinemu_dolphinemu_features_riivolution_model_RiivolutionPatches_loadConfigImpl( + JNIEnv* env, jobject obj, jstring j_game_id, jint revision, jint disc_number) +{ + const std::string game_id = GetJString(env, j_game_id); + auto& discs = GetReference(env, obj); + + const std::string& riivolution_dir = File::GetUserPath(D_RIIVOLUTION_IDX); + const auto config = LoadConfigXML(riivolution_dir, game_id); + + discs.clear(); + for (const std::string& path : Common::DoFileSearch({riivolution_dir + "riivolution"}, {".xml"})) + { + auto parsed = DiscIO::Riivolution::ParseFile(path); + if (!parsed || !parsed->IsValidForGame(game_id, revision, disc_number)) + continue; + if (config) + DiscIO::Riivolution::ApplyConfigDefaults(&*parsed, *config); + discs.emplace_back(std::move(*parsed)); + } +} + +JNIEXPORT void JNICALL +Java_org_dolphinemu_dolphinemu_features_riivolution_model_RiivolutionPatches_saveConfigImpl( + JNIEnv* env, jobject obj, jstring j_game_id) +{ + const std::string game_id = GetJString(env, j_game_id); + if (!(game_id.size() == 4 || game_id.size() == 6)) + return; + + DiscIO::Riivolution::Config config; + for (const auto& disc : GetReference(env, obj)) + { + for (const auto& section : disc.m_sections) + { + for (const auto& option : section.m_options) + { + std::string id = option.m_id.empty() ? (section.m_name + option.m_name) : option.m_id; + config.m_options.emplace_back( + DiscIO::Riivolution::ConfigOption{std::move(id), option.m_selected_choice}); + } + } + } + + const std::string& root = File::GetUserPath(D_RIIVOLUTION_IDX); + DiscIO::Riivolution::WriteConfigFile( + fmt::format("{}/riivolution/config/{}.xml", root, game_id.substr(0, 4)), config); +} +}