diff options
| author | JosJuice <josjuice@gmail.com> | 2025-11-22 17:00:03 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-11-22 17:00:03 +0100 |
| commit | f4f7424a9f059a25ec09b22a280d03fcf0e751eb (patch) | |
| tree | da9b25179aad7e305bada139bb2062951e68542d | |
| parent | e526c1ffde96e619bc764167f6fc337db74551ed (diff) | |
| parent | ddced1a070848d935bd1d140eed549a4cae89566 (diff) | |
Merge pull request #13583 from LillyJadeKatrin/retroachievements-android
Android Support for RetroAchievements
18 files changed, 473 insertions, 3 deletions
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/AchievementModel.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/AchievementModel.kt new file mode 100644 index 0000000000..1ea53ddbec --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/AchievementModel.kt @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.features.settings.model + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +object AchievementModel { + @JvmStatic + external fun init() + + suspend fun asyncLogin(password: String): Boolean { + return withContext(Dispatchers.IO) { + login(password) + } + } + + @JvmStatic + private external fun login(password: String): Boolean + + @JvmStatic + external fun logout() + + @JvmStatic + external fun isHardcoreModeActive(): Boolean + + @JvmStatic + external fun shutdown() +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/BooleanSetting.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/BooleanSetting.kt index 0b6945ff13..1f8ef6e94b 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/BooleanSetting.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/BooleanSetting.kt @@ -883,6 +883,42 @@ enum class BooleanSetting( Settings.SECTION_LOGGER_OPTIONS, "WriteToFile", false + ), + ACHIEVEMENTS_ENABLED( + Settings.FILE_ACHIEVEMENTS, + Settings.SECTION_ACHIEVEMENTS, + "Enabled", + false + ), + ACHIEVEMENTS_HARDCORE_ENABLED( + Settings.FILE_ACHIEVEMENTS, + Settings.SECTION_ACHIEVEMENTS, + "HardcoreEnabled", + false + ), + ACHIEVEMENTS_UNOFFICIAL_ENABLED( + Settings.FILE_ACHIEVEMENTS, + Settings.SECTION_ACHIEVEMENTS, + "UnofficialEnabled", + false + ), + ACHIEVEMENTS_ENCORE_ENABLED( + Settings.FILE_ACHIEVEMENTS, + Settings.SECTION_ACHIEVEMENTS, + "EncoreEnabled", + false + ), + ACHIEVEMENTS_SPECTATOR_ENABLED( + Settings.FILE_ACHIEVEMENTS, + Settings.SECTION_ACHIEVEMENTS, + "SpectatorEnabled", + false + ), + ACHIEVEMENTS_PROGRESS_ENABLED( + Settings.FILE_ACHIEVEMENTS, + Settings.SECTION_ACHIEVEMENTS, + "ProgressEnabled", + true ); override val isOverridden: Boolean @@ -943,7 +979,12 @@ enum class BooleanSetting( MAIN_TIME_TRACKING, MAIN_EMULATE_SKYLANDER_PORTAL, MAIN_EMULATE_INFINITY_BASE, - MAIN_EMULATE_WII_SPEAK + MAIN_EMULATE_WII_SPEAK, + ACHIEVEMENTS_ENABLED, + ACHIEVEMENTS_HARDCORE_ENABLED, + ACHIEVEMENTS_UNOFFICIAL_ENABLED, + ACHIEVEMENTS_ENCORE_ENABLED, + ACHIEVEMENTS_SPECTATOR_ENABLED ) private val NOT_RUNTIME_EDITABLE: Set<BooleanSetting> = HashSet(listOf(*NOT_RUNTIME_EDITABLE_ARRAY)) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/Settings.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/Settings.kt index 32ad11b3e1..9f91a4244c 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/Settings.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/Settings.kt @@ -104,6 +104,7 @@ class Settings : Closeable { const val FILE_GFX = "GFX" const val FILE_LOGGER = "Logger" const val FILE_WIIMOTE = "WiimoteNew" + const val FILE_ACHIEVEMENTS = "RetroAchievements" const val FILE_GAME_SETTINGS_ONLY = "GameSettingsOnly" const val SECTION_INI_ANDROID = "Android" const val SECTION_INI_ANDROID_OVERLAY_BUTTONS = "AndroidOverlayButtons" @@ -122,5 +123,6 @@ class Settings : Closeable { const val SECTION_EMULATED_USB_DEVICES = "EmulatedUSBDevices" const val SECTION_STEREOSCOPY = "Stereoscopy" const val SECTION_ANALYTICS = "Analytics" + const val SECTION_ACHIEVEMENTS = "Achievements" } } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/StringSetting.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/StringSetting.kt index b150c845e1..29640aa92f 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/StringSetting.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/StringSetting.kt @@ -80,6 +80,18 @@ enum class StringSetting( Settings.SECTION_GFX_SETTINGS, "DriverLibName", "" + ), + ACHIEVEMENTS_USERNAME( + Settings.FILE_ACHIEVEMENTS, + Settings.SECTION_ACHIEVEMENTS, + "Username", + "" + ), + ACHIEVEMENTS_API_TOKEN( + Settings.FILE_ACHIEVEMENTS, + Settings.SECTION_ACHIEVEMENTS, + "ApiToken", + "" ); override val isOverridden: Boolean diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/LoginDialog.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/LoginDialog.kt new file mode 100644 index 0000000000..9f1d3efd5f --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/LoginDialog.kt @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.features.settings.ui + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.DialogFragment +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.launch +import org.dolphinemu.dolphinemu.databinding.DialogLoginBinding +import org.dolphinemu.dolphinemu.dialogs.AlertMessage +import org.dolphinemu.dolphinemu.features.settings.model.AchievementModel.asyncLogin +import org.dolphinemu.dolphinemu.features.settings.model.NativeConfig +import org.dolphinemu.dolphinemu.features.settings.model.StringSetting + +class LoginDialog(val parent: SettingsFragmentPresenter) : DialogFragment() { + private var _binding: DialogLoginBinding? = null + private val binding get() = _binding!! + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = DialogLoginBinding.inflate(inflater, container, false) + binding.usernameInput.setText( + StringSetting.ACHIEVEMENTS_USERNAME.string + ) + return binding.getRoot() + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + binding.buttonCancel.setOnClickListener { onCancelClicked() } + binding.buttonLogin.setOnClickListener { onLoginClicked() } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + private fun onCancelClicked() { + dismiss() + } + + private fun onLoginClicked() { + binding.loginFailed.visibility = View.GONE + binding.loginInProgress.visibility = View.VISIBLE + StringSetting.ACHIEVEMENTS_USERNAME.setString(NativeConfig.LAYER_BASE_OR_CURRENT, + binding.usernameInput.text.toString()) + lifecycleScope.launch { + if (asyncLogin(binding.passwordInput.text.toString())) { + parent.loadSettingsList() + dismiss() + } else { + binding.loginInProgress.visibility = View.GONE + binding.loginFailed.visibility = View.VISIBLE + } + } + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/MenuTag.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/MenuTag.kt index 0bfcb96d6c..74556b07d9 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/MenuTag.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/MenuTag.kt @@ -14,6 +14,7 @@ enum class MenuTag { CONFIG_GAME_CUBE("config_gamecube"), CONFIG_SERIALPORT1("config_serialport1"), CONFIG_WII("config_wii"), + CONFIG_ACHIEVEMENTS("config_achievements"), CONFIG_ADVANCED("config_advanced"), CONFIG_LOG("config_log"), DEBUG("debug"), diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragment.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragment.kt index 2992dd3211..1289d30f61 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragment.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragment.kt @@ -268,6 +268,7 @@ class SettingsFragment : Fragment(), SettingsFragmentView { titles[MenuTag.CONFIG_GAME_CUBE] = R.string.gamecube_submenu titles[MenuTag.CONFIG_SERIALPORT1] = R.string.serialport1_submenu titles[MenuTag.CONFIG_WII] = R.string.wii_submenu + titles[MenuTag.CONFIG_ACHIEVEMENTS] = R.string.achievements_submenu titles[MenuTag.CONFIG_ADVANCED] = R.string.advanced_submenu titles[MenuTag.DEBUG] = R.string.debug_submenu titles[MenuTag.GRAPHICS] = R.string.graphics_settings diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt index a47901debb..a4327b9645 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt @@ -31,6 +31,7 @@ import org.dolphinemu.dolphinemu.features.input.ui.ProfileDialog import org.dolphinemu.dolphinemu.features.input.ui.ProfileDialogPresenter import org.dolphinemu.dolphinemu.features.settings.model.* import org.dolphinemu.dolphinemu.features.settings.model.view.* +import org.dolphinemu.dolphinemu.features.settings.model.AchievementModel.logout import org.dolphinemu.dolphinemu.model.GpuDriverMetadata import org.dolphinemu.dolphinemu.utils.* import kotlin.collections.ArrayList @@ -98,7 +99,7 @@ class SettingsFragmentPresenter( } } - private fun loadSettingsList() { + fun loadSettingsList() { val sl = ArrayList<SettingsItem>() when (menuTag) { MenuTag.SETTINGS -> addTopLevelSettings(sl) @@ -109,6 +110,7 @@ class SettingsFragmentPresenter( MenuTag.CONFIG_PATHS -> addPathsSettings(sl) MenuTag.CONFIG_GAME_CUBE -> addGameCubeSettings(sl) MenuTag.CONFIG_WII -> addWiiSettings(sl) + MenuTag.CONFIG_ACHIEVEMENTS -> addAchievementSettings(sl); MenuTag.CONFIG_ADVANCED -> addAdvancedSettings(sl) MenuTag.GRAPHICS -> addGraphicsSettings(sl) MenuTag.CONFIG_SERIALPORT1 -> addSerialPortSubSettings(sl, serialPort1Type) @@ -198,6 +200,7 @@ class SettingsFragmentPresenter( sl.add(SubmenuSetting(context, R.string.paths_submenu, MenuTag.CONFIG_PATHS)) sl.add(SubmenuSetting(context, R.string.gamecube_submenu, MenuTag.CONFIG_GAME_CUBE)) sl.add(SubmenuSetting(context, R.string.wii_submenu, MenuTag.CONFIG_WII)) + sl.add(SubmenuSetting(context, R.string.achievements_submenu, MenuTag.CONFIG_ACHIEVEMENTS)) sl.add(SubmenuSetting(context, R.string.advanced_submenu, MenuTag.CONFIG_ADVANCED)) sl.add(SubmenuSetting(context, R.string.log_submenu, MenuTag.CONFIG_LOG)) sl.add(SubmenuSetting(context, R.string.debug_submenu, MenuTag.DEBUG)) @@ -246,7 +249,7 @@ class SettingsFragmentPresenter( FloatSetting.MAIN_EMULATION_SPEED, R.string.speed_limit, 0, - 0f, + if (AchievementModel.isHardcoreModeActive()) 100f else 0f, 200f, "%", 1f, @@ -912,6 +915,113 @@ class SettingsFragmentPresenter( ) } + private fun addAchievementSettings(sl: ArrayList<SettingsItem>) { + val achievementsEnabledSetting: AbstractBooleanSetting = object : AbstractBooleanSetting { + override val boolean: Boolean + get() = BooleanSetting.ACHIEVEMENTS_ENABLED.boolean + + override fun setBoolean(settings: Settings, newValue: Boolean) { + BooleanSetting.ACHIEVEMENTS_ENABLED.setBoolean(settings, newValue) + if (newValue) + AchievementModel.init() + else + AchievementModel.shutdown() + loadSettingsList() + } + + override val isOverridden: Boolean + get() = BooleanSetting.ACHIEVEMENTS_ENABLED.isOverridden + + override val isRuntimeEditable: Boolean + get() = BooleanSetting.ACHIEVEMENTS_ENABLED.isRuntimeEditable + + override fun delete(settings: Settings): Boolean { + val result = BooleanSetting.ACHIEVEMENTS_ENABLED.delete(settings) + AchievementModel.shutdown() + loadSettingsList() + return result + } + } + + sl.add( + SwitchSetting( + context, + achievementsEnabledSetting, + R.string.achievements_enabled, + 0 + ) + ) + if (BooleanSetting.ACHIEVEMENTS_ENABLED.boolean) { + if (StringSetting.ACHIEVEMENTS_API_TOKEN.string == "") { + sl.add( + RunRunnable( + context, + R.string.achievements_login, + 0, + 0, + 0, + false + ) { + fragmentView.showDialogFragment(LoginDialog(this)) + loadSettingsList() + }) + } else { + sl.add( + RunRunnable( + context, + R.string.achievements_logout, + 0, + 0, + 0, + false + ) { + logout() + loadSettingsList() + }) + } + sl.add( + SwitchSetting( + context, + BooleanSetting.ACHIEVEMENTS_HARDCORE_ENABLED, + R.string.achievements_hardcore_enabled, + 0 + ) + ) + sl.add( + SwitchSetting( + context, + BooleanSetting.ACHIEVEMENTS_UNOFFICIAL_ENABLED, + R.string.achievements_unofficial_enabled, + 0 + ) + ) + sl.add( + SwitchSetting( + context, + BooleanSetting.ACHIEVEMENTS_ENCORE_ENABLED, + R.string.achievements_encore_enabled, + 0 + ) + ) + sl.add( + SwitchSetting( + context, + BooleanSetting.ACHIEVEMENTS_SPECTATOR_ENABLED, + R.string.achievements_spectator_enabled, + 0 + ) + ) + sl.add( + SwitchSetting( + context, + BooleanSetting.ACHIEVEMENTS_PROGRESS_ENABLED, + R.string.achievements_progress_enabled, + 0 + ) + ) + } + } + private fun addAdvancedSettings(sl: ArrayList<SettingsItem>) { val SYNC_GPU_NEVER = 0 val SYNC_GPU_ON_IDLE_SKIP = 1 diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt index ddcf1d39b5..2907b948b6 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt @@ -32,6 +32,9 @@ class SwitchSettingViewHolder( binding.textSettingName.text = item.name binding.textSettingDescription.text = item.description + // Make sure we don't trigger any listener set earlier + binding.settingSwitch.setOnCheckedChangeListener(null) + binding.settingSwitch.isChecked = setting.isChecked binding.settingSwitch.isEnabled = setting.isEditable diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/MenuFragment.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/MenuFragment.kt index b668e08da8..6bf9c7c147 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/MenuFragment.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/MenuFragment.kt @@ -19,6 +19,7 @@ import org.dolphinemu.dolphinemu.NativeLibrary import org.dolphinemu.dolphinemu.R import org.dolphinemu.dolphinemu.activities.EmulationActivity import org.dolphinemu.dolphinemu.databinding.FragmentIngameMenuBinding +import org.dolphinemu.dolphinemu.features.settings.model.AchievementModel import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting import org.dolphinemu.dolphinemu.features.settings.model.IntSetting import org.dolphinemu.dolphinemu.utils.InsetsHelper @@ -114,11 +115,16 @@ class MenuFragment : Fragment(), View.OnClickListener { override fun onResume() { super.onResume() val savestatesEnabled = BooleanSetting.MAIN_ENABLE_SAVESTATES.boolean + val hardcoreEnabled = AchievementModel.isHardcoreModeActive() val savestateVisibility = if (savestatesEnabled) View.VISIBLE else View.GONE binding.menuQuicksave.visibility = savestateVisibility binding.menuQuickload.visibility = savestateVisibility binding.menuEmulationSaveRoot.visibility = savestateVisibility binding.menuEmulationLoadRoot.visibility = savestateVisibility + // While technically the option is still enabled, AchievementManager + // will block the load and send a message to the screen. + binding.menuQuickload.paint.isStrikeThruText = hardcoreEnabled + binding.menuEmulationLoadRoot.paint.isStrikeThruText = hardcoreEnabled } override fun onDestroyView() { diff --git a/Source/Android/app/src/main/res/layout/dialog_login.xml b/Source/Android/app/src/main/res/layout/dialog_login.xml new file mode 100644 index 0000000000..320aed0e6a --- /dev/null +++ b/Source/Android/app/src/main/res/layout/dialog_login.xml @@ -0,0 +1,110 @@ +<?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:id="@+id/root" + android:minWidth="320dp" + android:layout_width="wrap_content" + android:layout_height="wrap_content"> + + <com.google.android.material.textfield.TextInputLayout + android:id="@+id/username" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginHorizontal="@dimen/spacing_large" + android:layout_marginVertical="@dimen/spacing_small" + android:paddingTop="@dimen/spacing_medlarge" + app:layout_constraintBottom_toTopOf="@id/password" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent"> + + <com.google.android.material.textfield.TextInputEditText + android:id="@+id/username_input" + android:hint="@string/achievements_username" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:importantForAutofill="no" + android:inputType="text" + android:minHeight="48dp" + android:textAlignment="viewStart" /> + + </com.google.android.material.textfield.TextInputLayout> + + <com.google.android.material.textfield.TextInputLayout + android:id="@+id/password" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginHorizontal="@dimen/spacing_large" + app:layout_constraintBottom_toTopOf="@id/login_in_progress" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/username"> + + <com.google.android.material.textfield.TextInputEditText + android:id="@+id/password_input" + android:hint="@string/achievements_password" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:minHeight="48dp" + android:importantForAutofill="no" + android:inputType="textPassword" + android:textAlignment="viewStart" /> + + </com.google.android.material.textfield.TextInputLayout> + + <TextView + android:id="@+id/login_in_progress" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_gravity="bottom" + android:text="@string/achievements_login_in_progress" + android:textAlignment="center" + android:textColor="?attr/colorOnErrorContainer" + android:visibility="invisible" + android:clickable="false" + android:focusable="false" + app:layout_constraintBottom_toTopOf="@id/login_failed" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/password"/> + + <TextView + android:id="@+id/login_failed" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_gravity="bottom" + android:text="@string/achievements_login_failed" + android:textAlignment="center" + android:textColor="?attr/colorOnErrorContainer" + android:visibility="invisible" + android:clickable="false" + android:focusable="false" + app:layout_constraintBottom_toTopOf="@id/button_cancel" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/login_in_progress"/> + + <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_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toStartOf="@id/button_login" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/login_failed" /> + + <Button + android:id="@+id/button_login" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_margin="@dimen/spacing_large" + android:text="@string/ok" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@id/button_cancel" + app:layout_constraintTop_toBottomOf="@id/login_failed" /> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/Source/Android/app/src/main/res/values/strings.xml b/Source/Android/app/src/main/res/values/strings.xml index 6342e35cb0..c94073a213 100644 --- a/Source/Android/app/src/main/res/values/strings.xml +++ b/Source/Android/app/src/main/res/values/strings.xml @@ -75,6 +75,7 @@ <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="achievements_submenu">RetroAchievements</string> <string name="gamecube_submenu">GameCube</string> <string name="ipl_settings">IPL Settings</string> <string name="skip_main_menu">Skip Main Menu</string> @@ -953,4 +954,18 @@ It can efficiently compress both junk data and encrypted Wii data. <string name="mute_wii_speak">Mute Wii Speak</string> <string name="wii_speak_permission_warning">Missing Microphone Permission</string> <string name="wii_speak_permission_warning_description">Wii Speak emulation requires microphone permission. You might need to restart the game for the permission to be effective.</string> + + <!-- Achievements --> + <string name="achievements_enabled">Enable Achievements</string> + <string name="achievements_hardcore_enabled">Enable Hardcore Mode</string> + <string name="achievements_unofficial_enabled">Enable Unofficial Achievements</string> + <string name="achievements_encore_enabled">Enable Encore Mode</string> + <string name="achievements_spectator_enabled">Enable Spectator Mode</string> + <string name="achievements_progress_enabled">Enable Progress Notifications</string> + <string name="achievements_username">Username</string> + <string name="achievements_password">Password</string> + <string name="achievements_login">Log In</string> + <string name="achievements_logout">Log Out</string> + <string name="achievements_login_in_progress">Logging In</string> + <string name="achievements_login_failed">Login Failed</string> </resources> diff --git a/Source/Android/jni/AchievementAdapter.cpp b/Source/Android/jni/AchievementAdapter.cpp new file mode 100644 index 0000000000..d5df74cfdc --- /dev/null +++ b/Source/Android/jni/AchievementAdapter.cpp @@ -0,0 +1,58 @@ +// Copyright 2025 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include <jni.h> + +#include <latch> +#include "Common/Event.h" +#include "Common/HookableEvent.h" +#include "Core/AchievementManager.h" +#include "jni/AndroidCommon/AndroidCommon.h" +#include "jni/AndroidCommon/IDCache.h" + +extern "C" { + +JNIEXPORT void JNICALL +Java_org_dolphinemu_dolphinemu_features_settings_model_AchievementModel_init(JNIEnv* env, jclass) +{ + AchievementManager::GetInstance().Init(nullptr); +} + +JNIEXPORT jboolean JNICALL +Java_org_dolphinemu_dolphinemu_features_settings_model_AchievementModel_login(JNIEnv* env, jclass, + jstring password) +{ + auto& instance = AchievementManager::GetInstance(); + bool success; + std::latch login_complete_event{1}; + Common::EventHook login_hook = + instance.login_event.Register([&login_complete_event, &success](int result) { + success = (result == RC_OK); + login_complete_event.count_down(); + }); + instance.Login(GetJString(env, password)); + login_complete_event.wait(); + return success; +} + +JNIEXPORT void JNICALL +Java_org_dolphinemu_dolphinemu_features_settings_model_AchievementModel_logout(JNIEnv* env, jclass) +{ + AchievementManager::GetInstance().Logout(); +} + +JNIEXPORT jboolean JNICALL +Java_org_dolphinemu_dolphinemu_features_settings_model_AchievementModel_isHardcoreModeActive( + JNIEnv* env, jclass) +{ + return AchievementManager::GetInstance().IsHardcoreModeActive(); +} + +JNIEXPORT void JNICALL +Java_org_dolphinemu_dolphinemu_features_settings_model_AchievementModel_shutdown(JNIEnv* env, + jclass) +{ + AchievementManager::GetInstance().Shutdown(); +} + +} // extern "C" diff --git a/Source/Android/jni/CMakeLists.txt b/Source/Android/jni/CMakeLists.txt index c6d542bf08..4f9c015102 100644 --- a/Source/Android/jni/CMakeLists.txt +++ b/Source/Android/jni/CMakeLists.txt @@ -1,4 +1,5 @@ add_library(main SHARED + AchievementAdapter.cpp ActivityTracker.cpp Cheats/ARCheat.cpp Cheats/Cheats.h diff --git a/Source/Android/jni/Config/NativeConfig.cpp b/Source/Android/jni/Config/NativeConfig.cpp index f367c5e162..8478174dae 100644 --- a/Source/Android/jni/Config/NativeConfig.cpp +++ b/Source/Android/jni/Config/NativeConfig.cpp @@ -48,6 +48,10 @@ static Config::Location GetLocation(JNIEnv* env, jstring file, jstring section, { system = Config::System::GameSettingsOnly; } + else if (decoded_file == "RetroAchievements") + { + system = Config::System::Achievements; + } else { ASSERT(false); diff --git a/Source/Android/jni/MainAndroid.cpp b/Source/Android/jni/MainAndroid.cpp index 707e81e336..7c78f49045 100644 --- a/Source/Android/jni/MainAndroid.cpp +++ b/Source/Android/jni/MainAndroid.cpp @@ -33,6 +33,7 @@ #include "Common/Version.h" #include "Common/WindowSystemInfo.h" +#include "Core/AchievementManager.h" #include "Core/Boot/Boot.h" #include "Core/BootManager.h" #include "Core/CommonTitles.h" @@ -559,6 +560,8 @@ JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_Initialize(J WiimoteReal::InitAdapterClass(); UICommon::Init(); UICommon::InitControllers(WindowSystemInfo(WindowSystemType::Android, nullptr, nullptr, nullptr)); + + AchievementManager::GetInstance().Init(nullptr); } JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_ReportStartToAnalytics(JNIEnv*, diff --git a/Source/Core/Core/AchievementManager.cpp b/Source/Core/Core/AchievementManager.cpp index 7a41d57c84..4dc581d125 100644 --- a/Source/Core/Core/AchievementManager.cpp +++ b/Source/Core/Core/AchievementManager.cpp @@ -49,8 +49,13 @@ #include <shlwapi.h> #endif // RC_CLIENT_SUPPORTS_RAINTEGRATION +#ifdef ANDROID +static const Common::HttpRequest::Headers USER_AGENT_HEADER = { + {"User-Agent", Common::GetUserAgentStr() + " (Android)"}}; +#else // ANDROID static const Common::HttpRequest::Headers USER_AGENT_HEADER = { {"User-Agent", Common::GetUserAgentStr()}}; +#endif // ANDROID AchievementManager& AchievementManager::GetInstance() { @@ -911,6 +916,7 @@ void AchievementManager::LoginCallback(int result, const char* error_message, rc Config::Get(Config::RA_USERNAME)); Config::SetBaseOrCurrent(Config::RA_API_TOKEN, ""); instance.update_event.Trigger({.failed_login_code = result}); + instance.login_event.Trigger(result); return; } @@ -923,6 +929,7 @@ void AchievementManager::LoginCallback(int result, const char* error_message, rc { WARN_LOG_FMT(ACHIEVEMENTS, "Failed to retrieve user information from client."); instance.update_event.Trigger({.failed_login_code = RC_INVALID_STATE}); + instance.login_event.Trigger(RC_INVALID_STATE); return; } @@ -942,9 +949,12 @@ void AchievementManager::LoginCallback(int result, const char* error_message, rc user->username, Config::Get(Config::RA_USERNAME)); rc_client_logout(client); instance.update_event.Trigger({.failed_login_code = RC_INVALID_STATE}); + instance.login_event.Trigger(RC_INVALID_STATE); return; } } + instance.login_event.Trigger(RC_OK); + INFO_LOG_FMT(ACHIEVEMENTS, "Successfully logged in {} to RetroAchievements server.", user->username); std::lock_guard lg{instance.GetLock()}; diff --git a/Source/Core/Core/AchievementManager.h b/Source/Core/Core/AchievementManager.h index b666b3f862..0c486deeba 100644 --- a/Source/Core/Core/AchievementManager.h +++ b/Source/Core/Core/AchievementManager.h @@ -121,6 +121,7 @@ public: int failed_login_code = 0; }; Common::HookableEvent<const UpdatedItems&> update_event; + Common::HookableEvent<int> login_event; static AchievementManager& GetInstance(); void Init(void* hwnd); |
