| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| // SPDX-License-Identifier: GPL-2.0-or-later | ||
|
|
||
| package org.dolphinemu.dolphinemu.features.riivolution.ui; | ||
|
|
||
| import android.content.Context; | ||
| import android.content.Intent; | ||
| import android.os.Bundle; | ||
| import android.widget.Button; | ||
| import android.widget.TextView; | ||
|
|
||
| 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; | ||
| import org.dolphinemu.dolphinemu.utils.DirectoryInitialization; | ||
|
|
||
| public class RiivolutionBootActivity extends AppCompatActivity | ||
| { | ||
| private static final String ARG_GAME_PATH = "game_path"; | ||
| private static final String ARG_GAME_ID = "game_id"; | ||
| 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) | ||
| { | ||
| Intent launcher = new Intent(context, RiivolutionBootActivity.class); | ||
| launcher.putExtra(ARG_GAME_PATH, gamePath); | ||
| launcher.putExtra(ARG_GAME_ID, gameId); | ||
| launcher.putExtra(ARG_REVISION, revision); | ||
| launcher.putExtra(ARG_DISC_NUMBER, discNumber); | ||
| context.startActivity(launcher); | ||
| } | ||
|
|
||
| @Override | ||
| protected void onCreate(Bundle savedInstanceState) | ||
| { | ||
| super.onCreate(savedInstanceState); | ||
|
|
||
| setContentView(R.layout.activity_riivolution_boot); | ||
|
|
||
| Intent intent = getIntent(); | ||
|
|
||
| String path = getIntent().getStringExtra(ARG_GAME_PATH); | ||
| String gameId = intent.getStringExtra(ARG_GAME_ID); | ||
| int revision = intent.getIntExtra(ARG_REVISION, -1); | ||
| int discNumber = intent.getIntExtra(ARG_DISC_NUMBER, -1); | ||
|
|
||
| TextView textSdRoot = findViewById(R.id.text_sd_root); | ||
| String riivolutionPath = DirectoryInitialization.getUserDirectory() + "/Load/Riivolution"; | ||
| textSdRoot.setText(getString(R.string.riivolution_sd_root, riivolutionPath)); | ||
|
|
||
| Button buttonStart = findViewById(R.id.button_start); | ||
| 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)); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<String> 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) | ||
| { | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| <?xml version="1.0" encoding="utf-8"?> | ||
| <androidx.constraintlayout.widget.ConstraintLayout | ||
| xmlns:android="http://schemas.android.com/apk/res/android" | ||
| xmlns:tools="http://schemas.android.com/tools" | ||
| xmlns:app="http://schemas.android.com/apk/res-auto" | ||
| android:id="@+id/root" | ||
| android:layout_width="match_parent" | ||
| android:layout_height="match_parent"> | ||
|
|
||
| <TextView | ||
| android:id="@+id/text_sd_root" | ||
| android:layout_width="match_parent" | ||
| android:layout_height="wrap_content" | ||
| android:layout_margin="@dimen/spacing_large" | ||
| tools:text="@string/riivolution_sd_root" | ||
| app:layout_constraintStart_toStartOf="parent" | ||
| app:layout_constraintEnd_toEndOf="parent" | ||
| app:layout_constraintTop_toTopOf="parent" | ||
| app:layout_constraintBottom_toTopOf="@id/divider" /> | ||
|
|
||
| <View | ||
| android:id="@+id/divider" | ||
| android:layout_width="0dp" | ||
| android:layout_height="1dp" | ||
| android:background="#1F000000" | ||
| android:layout_marginHorizontal="@dimen/spacing_large" | ||
| android:layout_marginVertical="@dimen/spacing_small" | ||
| app:layout_constraintStart_toStartOf="parent" | ||
| app:layout_constraintEnd_toEndOf="parent" | ||
| app:layout_constraintTop_toBottomOf="@id/text_sd_root" | ||
| app:layout_constraintBottom_toTopOf="@id/scroll_view" /> | ||
|
|
||
| <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_toBottomOf="@id/divider" | ||
| app:layout_constraintBottom_toTopOf="@id/button_start"> | ||
|
|
||
| <androidx.recyclerview.widget.RecyclerView | ||
| android:id="@+id/recycler_view" | ||
| android:layout_width="match_parent" | ||
| android:layout_height="0dp" /> | ||
|
|
||
| </ScrollView> | ||
|
|
||
| <Button | ||
| android:id="@+id/button_start" | ||
| android:layout_width="0dp" | ||
| android:layout_height="wrap_content" | ||
| android:layout_margin="@dimen/spacing_large" | ||
| android:text="@string/riivolution_start" | ||
| app:layout_constraintStart_toStartOf="parent" | ||
| app:layout_constraintEnd_toEndOf="parent" | ||
| app:layout_constraintTop_toBottomOf="@id/scroll_view" | ||
| app:layout_constraintBottom_toBottomOf="parent" /> | ||
|
|
||
| </androidx.constraintlayout.widget.ConstraintLayout> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| <?xml version="1.0" encoding="utf-8"?> | ||
| <TextView xmlns:android="http://schemas.android.com/apk/res/android" | ||
| xmlns:tools="http://schemas.android.com/tools" | ||
| android:layout_width="match_parent" | ||
| android:layout_height="wrap_content" | ||
| android:id="@+id/text_name" | ||
| tools:text="Example Section" | ||
| android:paddingHorizontal="@dimen/spacing_large" | ||
| android:paddingVertical="@dimen/spacing_medlarge" /> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| <?xml version="1.0" encoding="utf-8"?> | ||
| <androidx.constraintlayout.widget.ConstraintLayout | ||
| xmlns:android="http://schemas.android.com/apk/res/android" | ||
| xmlns:app="http://schemas.android.com/apk/res-auto" | ||
| xmlns:tools="http://schemas.android.com/tools" | ||
| android:layout_width="match_parent" | ||
| android:layout_height="wrap_content"> | ||
|
|
||
| <TextView | ||
| android:layout_width="0dp" | ||
| android:layout_height="wrap_content" | ||
| android:id="@+id/text_name" | ||
| tools:text="Example Option" | ||
| android:layout_marginHorizontal="@dimen/spacing_large" | ||
| android:layout_marginVertical="@dimen/spacing_medlarge" | ||
| app:layout_constraintStart_toStartOf="parent" | ||
| app:layout_constraintEnd_toStartOf="@id/spinner_choice" | ||
| app:layout_constraintTop_toTopOf="parent" | ||
| app:layout_constraintBottom_toBottomOf="parent"/> | ||
|
|
||
| <Spinner | ||
| android:layout_width="0dp" | ||
| android:layout_height="wrap_content" | ||
| android:id="@+id/spinner_choice" | ||
| app:layout_constraintStart_toEndOf="@id/text_name" | ||
| app:layout_constraintEnd_toEndOf="parent" | ||
| app:layout_constraintTop_toTopOf="parent" | ||
| app:layout_constraintBottom_toBottomOf="parent"/> | ||
|
|
||
| </androidx.constraintlayout.widget.ConstraintLayout> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,193 @@ | ||
| // Copyright 2021 Dolphin Emulator Project | ||
| // SPDX-License-Identifier: GPL-2.0-or-later | ||
|
|
||
| #include <string> | ||
| #include <string_view> | ||
| #include <vector> | ||
|
|
||
| #include <fmt/format.h> | ||
| #include <jni.h> | ||
|
|
||
| #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<DiscIO::Riivolution::Disc>* GetPointer(JNIEnv* env, jobject obj) | ||
| { | ||
| return reinterpret_cast<std::vector<DiscIO::Riivolution::Disc>*>( | ||
| env->GetLongField(obj, IDCache::GetRiivolutionPatchesPointer())); | ||
| } | ||
|
|
||
| static std::vector<DiscIO::Riivolution::Disc>& 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<jlong>(new std::vector<DiscIO::Riivolution::Disc>); | ||
| } | ||
|
|
||
| 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<DiscIO::Riivolution::Config> 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); | ||
| } | ||
| } |