UNFINISHED - Achievements List in Android

This commit is contained in:
LillyJadeKatrin 2025-12-08 19:26:29 -05:00
parent 887c68eed4
commit f9f3b7da2d
21 changed files with 811 additions and 0 deletions

View File

@ -91,6 +91,12 @@
android:theme="@style/Theme.Dolphin.Main"
android:label="@string/cheats"/>
<activity
android:name=".features.achievements.ui.AchievementsActivity"
android:exported="false"
android:theme="@style/Theme.Dolphin.Main"
android:label="@string/achievements"/>
<activity
android:name=".activities.EmulationActivity"
android:exported="false"

View File

@ -36,6 +36,7 @@ import org.dolphinemu.dolphinemu.R
import org.dolphinemu.dolphinemu.databinding.ActivityEmulationBinding
import org.dolphinemu.dolphinemu.databinding.DialogInputAdjustBinding
import org.dolphinemu.dolphinemu.databinding.DialogNfcFiguresManagerBinding
import org.dolphinemu.dolphinemu.features.achievements.ui.AchievementsActivity
import org.dolphinemu.dolphinemu.features.infinitybase.InfinityConfig
import org.dolphinemu.dolphinemu.features.infinitybase.model.Figure
import org.dolphinemu.dolphinemu.features.infinitybase.ui.FigureSlot
@ -515,6 +516,7 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider {
MENU_SET_IR_MODE -> setIRMode()
MENU_ACTION_CHOOSE_DOUBLETAP -> chooseDoubleTapButton()
MENU_ACTION_SETTINGS -> SettingsActivity.launch(this, MenuTag.SETTINGS)
MENU_ACTION_ACHIEVEMENTS -> AchievementsActivity.launch(this)
MENU_ACTION_SKYLANDERS -> showSkylanderPortalSettings()
MENU_ACTION_INFINITY_BASE -> showInfinityBaseSettings()
MENU_ACTION_EXIT -> emulationFragment!!.stopEmulation()
@ -1077,6 +1079,7 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider {
const val MENU_ACTION_SKYLANDERS = 36
const val MENU_ACTION_INFINITY_BASE = 37
const val MENU_ACTION_LATCHING_CONTROLS = 38
const val MENU_ACTION_ACHIEVEMENTS = 39
init {
buttonsActionsMap.apply {

View File

@ -0,0 +1,23 @@
package org.dolphinemu.dolphinemu.features.achievements.model
import androidx.annotation.Keep
@Keep
class Achievement(
var title: String = "",
var description: String = "",
var badgeName: String = "",
var measuredProgress: String = "",
var measuredPercent: Float = 0F,
var id: Int = 0,
var points: Int = 0,
var unlockTime: String = "",
var state: Int = 0,
var category: Int = 0,
var bucket: Int = 0,
var unlocked: Int = 0,
var rarity: Float = 0F,
var rarityHardcore: Float = 0F,
var type: Int = 0,
var badgeUrl: String = "",
var badgeLockedUrl: String = "")

View File

@ -0,0 +1,25 @@
package org.dolphinemu.dolphinemu.features.achievements.model
import androidx.annotation.Keep
@Keep
class AchievementBucket(
var numAchievements: Int) {
var achievements: Array<Achievement> = Array(numAchievements) {Achievement()}
var label: String = ""
var subsetId: Int = 0
var bucketType: Int = 0
companion object {
const val RC_CLIENT_ACHIEVEMENT_BUCKET_UNKNOWN = 0
const val RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED = 1
const val RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED = 2
const val RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED = 3
const val RC_CLIENT_ACHIEVEMENT_BUCKET_UNOFFICIAL = 4
const val RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED = 5
const val RC_CLIENT_ACHIEVEMENT_BUCKET_ACTIVE_CHALLENGE = 6
const val RC_CLIENT_ACHIEVEMENT_BUCKET_ALMOST_THERE = 7
const val RC_CLIENT_ACHIEVEMENT_BUCKET_UNSYNCED = 8
const val NUM_RC_CLIENT_ACHIEVEMENT_BUCKETS = 9
}
}

View File

@ -0,0 +1,19 @@
package org.dolphinemu.dolphinemu.features.achievements.model
import androidx.lifecycle.ViewModel
class AchievementProgressViewModel : ViewModel() {
var buckets: ArrayList<AchievementBucket> = ArrayList()
fun load() {
buckets.addAll(fetchProgress())
}
companion object {
@JvmStatic
external fun isGameLoaded(): Boolean
@JvmStatic
external fun fetchProgress(): Array<AchievementBucket>
}
}

View File

@ -0,0 +1,15 @@
// SPDX-License-Identifier: GPL-2.0-or-later
package org.dolphinemu.dolphinemu.features.achievements.ui
import android.widget.TextView
import org.dolphinemu.dolphinemu.databinding.ListItemHeaderBinding
import org.dolphinemu.dolphinemu.features.achievements.ui.AchievementsActivity
class AchievementHeaderViewHolder(binding: ListItemHeaderBinding) : AchievementProgressItemViewHolder(binding.root) {
private val headerName: TextView = binding.textHeaderName
override fun bind(activity: AchievementsActivity, item: AchievementProgressItem, position: Int) {
headerName.setText(item.string)
}
}

View File

@ -0,0 +1,56 @@
// SPDX-License-Identifier: GPL-2.0-or-later
package org.dolphinemu.dolphinemu.features.achievements.ui
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import org.dolphinemu.dolphinemu.databinding.ListItemAchievementProgressBinding
import org.dolphinemu.dolphinemu.databinding.ListItemHeaderBinding
import org.dolphinemu.dolphinemu.features.achievements.model.AchievementProgressViewModel
class AchievementProgressAdapter(
private val activity: AchievementsActivity,
private val viewModel: AchievementProgressViewModel
) : RecyclerView.Adapter<AchievementProgressItemViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AchievementProgressItemViewHolder {
val inflater = LayoutInflater.from(parent.context)
return when (viewType) {
AchievementProgressItem.TYPE_ACHIEVEMENT -> {
val listItemAchievementProgressBinding = ListItemAchievementProgressBinding.inflate(inflater, parent, false)
AchievementProgressViewHolder(listItemAchievementProgressBinding)
}
AchievementProgressItem.TYPE_HEADER -> {
val listItemHeaderBinding = ListItemHeaderBinding.inflate(inflater, parent, false)
AchievementHeaderViewHolder(listItemHeaderBinding)
}
else -> throw UnsupportedOperationException()
}
}
override fun onBindViewHolder(holder: AchievementProgressItemViewHolder, position: Int) {
holder.bind(activity, getItemAt(position), position)
}
override fun getItemCount(): Int {
return viewModel.buckets.size + viewModel.buckets.sumOf { bucket -> bucket.achievements.size }
}
override fun getItemViewType(position: Int): Int {
return getItemAt(position).type
}
private fun getItemAt(position: Int): AchievementProgressItem {
var itemPosition = position
viewModel.buckets.forEach { bucket ->
when (itemPosition) {
0 -> return AchievementProgressItem(bucket.label)
in 1..bucket.achievements.size -> return AchievementProgressItem(
bucket.achievements[itemPosition]
)
else -> itemPosition -= bucket.achievements.size
}
}
throw IndexOutOfBoundsException()
}
}

View File

@ -0,0 +1,28 @@
// SPDX-License-Identifier: GPL-2.0-or-later
package org.dolphinemu.dolphinemu.features.achievements.ui
import org.dolphinemu.dolphinemu.features.achievements.model.Achievement
class AchievementProgressItem {
val achievement: Achievement?
val string: String
val type: Int
constructor(achievement: Achievement) {
this.achievement = achievement
string = ""
type = TYPE_ACHIEVEMENT
}
constructor(string: String) {
achievement = null
this.string = string
this.type = TYPE_HEADER
}
companion object {
const val TYPE_HEADER = 0
const val TYPE_ACHIEVEMENT = 1
}
}

View File

@ -0,0 +1,11 @@
// SPDX-License-Identifier: GPL-2.0-or-later
package org.dolphinemu.dolphinemu.features.achievements.ui
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import org.dolphinemu.dolphinemu.features.achievements.ui.AchievementsActivity
abstract class AchievementProgressItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
abstract fun bind(activity: AchievementsActivity, item: AchievementProgressItem, position: Int)
}

View File

@ -0,0 +1,62 @@
// SPDX-License-Identifier: GPL-2.0-or-later
package org.dolphinemu.dolphinemu.features.achievements.ui
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import org.dolphinemu.dolphinemu.R
import org.dolphinemu.dolphinemu.databinding.FragmentAchievementsListBinding
import org.dolphinemu.dolphinemu.features.achievements.model.AchievementProgressViewModel
import org.dolphinemu.dolphinemu.features.settings.ui.SettingsDividerItemDecoration
class AchievementProgressListFragment : Fragment() {
private var _binding: FragmentAchievementsListBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentAchievementsListBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val activity = requireActivity() as AchievementsActivity
val viewModel = ViewModelProvider(activity)[AchievementProgressViewModel::class.java]
binding.achievementsList.adapter = AchievementProgressAdapter(activity, viewModel)
binding.achievementsList.layoutManager = LinearLayoutManager(activity)
val divider = SettingsDividerItemDecoration(requireActivity())
binding.achievementsList.addItemDecoration(divider)
setInsets()
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
private fun setInsets() {
ViewCompat.setOnApplyWindowInsetsListener(binding.achievementsList) { v: View, windowInsets: WindowInsetsCompat ->
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(
0,
0,
0,
insets.bottom + resources.getDimensionPixelSize(R.dimen.spacing_xtralarge)
)
windowInsets
}
}
}

View File

@ -0,0 +1,25 @@
// SPDX-License-Identifier: GPL-2.0-or-later
package org.dolphinemu.dolphinemu.features.achievements.ui
import android.view.View
import android.widget.CompoundButton
import androidx.lifecycle.ViewModelProvider
import org.dolphinemu.dolphinemu.databinding.ListItemAchievementProgressBinding
import org.dolphinemu.dolphinemu.features.achievements.model.AchievementProgressViewModel
import org.dolphinemu.dolphinemu.features.achievements.model.Achievement
import org.dolphinemu.dolphinemu.features.achievements.ui.AchievementsActivity
class AchievementProgressViewHolder(private val binding: ListItemAchievementProgressBinding) :
AchievementProgressItemViewHolder(binding.getRoot()) {
private lateinit var achievement: Achievement
override fun bind(activity: AchievementsActivity, item: AchievementProgressItem, position: Int) {
achievement = item.achievement!!
binding.achievementTitle.text = achievement.title
binding.achievementDescription.text = achievement.description
binding.achievementScore.text = achievement.points.toString()
binding.achievementStatus.text = if (achievement.unlocked == 0) "Locked" else "Unlocked"
binding.achievementProgress.text = achievement.measuredProgress
}
}

View File

@ -0,0 +1,73 @@
// SPDX-License-Identifier: GPL-2.0-or-later
package org.dolphinemu.dolphinemu.features.achievements.ui
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.activity.enableEdgeToEdge
import androidx.annotation.ColorInt
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.ViewModelProvider
import com.google.android.material.color.MaterialColors
import com.google.android.material.elevation.ElevationOverlayProvider
import org.dolphinemu.dolphinemu.R
import org.dolphinemu.dolphinemu.databinding.ActivityAchievementsBinding
import org.dolphinemu.dolphinemu.features.achievements.model.AchievementProgressViewModel
import org.dolphinemu.dolphinemu.ui.TwoPaneOnBackPressedCallback
import org.dolphinemu.dolphinemu.ui.main.MainPresenter
import org.dolphinemu.dolphinemu.utils.ThemeHelper
class AchievementsActivity : AppCompatActivity() {
private lateinit var viewModel: AchievementProgressViewModel
private lateinit var binding: ActivityAchievementsBinding
override fun onCreate(savedInstanceState: Bundle?) {
ThemeHelper.setTheme(this)
enableEdgeToEdge()
super.onCreate(savedInstanceState)
MainPresenter.skipRescanningLibrary()
title = getString(R.string.achievements_progress)
viewModel = ViewModelProvider(this)[AchievementProgressViewModel::class.java]
viewModel.load()
binding = ActivityAchievementsBinding.inflate(layoutInflater)
setContentView(binding.root)
onBackPressedDispatcher.addCallback(
this,
TwoPaneOnBackPressedCallback(binding.slidingPaneLayout)
)
setSupportActionBar(binding.toolbarAchievements)
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
@ColorInt val color =
ElevationOverlayProvider(binding.toolbarAchievements.context).compositeOverlay(
MaterialColors.getColor(binding.toolbarAchievements, R.attr.colorSurface),
resources.getDimensionPixelSize(R.dimen.elevated_app_bar).toFloat()
)
binding.toolbarAchievements.setBackgroundColor(color)
ThemeHelper.setStatusBarColor(this, color)
}
override fun onSupportNavigateUp(): Boolean {
onBackPressed()
return true
}
companion object {
@JvmStatic
fun launch(
context: Context
) {
val intent = Intent(context, AchievementsActivity::class.java)
context.startActivity(intent)
}
}
}

View File

@ -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.achievements.model.AchievementProgressViewModel
import org.dolphinemu.dolphinemu.features.settings.model.AchievementModel
import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting
import org.dolphinemu.dolphinemu.features.settings.model.IntSetting
@ -115,6 +116,7 @@ class MenuFragment : Fragment(), View.OnClickListener {
override fun onResume() {
super.onResume()
val savestatesEnabled = BooleanSetting.MAIN_ENABLE_SAVESTATES.boolean
val hasAchievements = AchievementProgressViewModel.isGameLoaded()
val hardcoreEnabled = AchievementModel.isHardcoreModeActive()
val savestateVisibility = if (savestatesEnabled) View.VISIBLE else View.GONE
binding.menuQuicksave.visibility = savestateVisibility
@ -125,6 +127,7 @@ class MenuFragment : Fragment(), View.OnClickListener {
// will block the load and send a message to the screen.
binding.menuQuickload.paint.isStrikeThruText = hardcoreEnabled
binding.menuEmulationLoadRoot.paint.isStrikeThruText = hardcoreEnabled
binding.menuAchievements.visibility = if (hasAchievements) View.VISIBLE else View.GONE
}
override fun onDestroyView() {
@ -186,6 +189,10 @@ class MenuFragment : Fragment(), View.OnClickListener {
R.id.menu_emulation_load_root,
EmulationActivity.MENU_ACTION_LOAD_ROOT
)
buttonsActionsMap.append(
R.id.menu_achievements,
EmulationActivity.MENU_ACTION_ACHIEVEMENTS
)
buttonsActionsMap.append(
R.id.menu_overlay_controls,
EmulationActivity.MENU_ACTION_OVERLAY_CONTROLS

View File

@ -0,0 +1,70 @@
<?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="match_parent">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/coordinator_main"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorSurface"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar_achievements"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:elevation="0dp"
app:liftOnScroll="false">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar_achievements"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorSurface" />
</com.google.android.material.appbar.AppBarLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<androidx.slidingpanelayout.widget.SlidingPaneLayout
android:id="@+id/sliding_pane_layout"
android:layout_width="match_parent"
android:layout_height="0dp"
android:background="?attr/colorSurface"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/coordinator_main">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/achievement_list"
android:name="org.dolphinemu.dolphinemu.features.achievements.ui.AchievementProgressListFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:layout="@layout/fragment_achievements_list" />
</androidx.slidingpanelayout.widget.SlidingPaneLayout>
<!-- We have to set the layout height at 1px because when a device forces fullscreen mode,
inset callbacks are not triggered and using 0dp will make this view cover the entire
display since this activity uses a constraint layout. Now, even if the callback isn't
triggered, this view will remain at 1px height. -->
<View
android:id="@+id/workaround_view"
android:layout_width="match_parent"
android:layout_height="1px"
android:layout_gravity="bottom"
android:background="@android:color/transparent"
android:clickable="true"
android:focusable="false"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,19 @@
<?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="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/achievements_list"
android:layout_width="match_parent"
android:layout_height="0dp"
android:clipToPadding="false"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -81,6 +81,12 @@
android:text="@string/emulation_loadstate"
android:visibility="gone" />
<Button
android:id="@+id/menu_achievements"
style="@style/InGameMenuOption"
android:text="@string/emulation_achievements"
android:visibility="gone" />
<Button
android:id="@+id/menu_settings"
style="@style/InGameMenuOption"

View File

@ -0,0 +1,82 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/root"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:focusable="true"
android:clickable="true"
android:background="?android:attr/selectableItemBackground">
<TextView
android:id="@+id/achievement_title"
style="@style/TextAppearance.MaterialComponents.Headline5"
android:layout_width="wrap_content"
android:layout_height="76dp"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:layout_marginStart="@dimen/spacing_large"
android:layout_marginEnd="@dimen/spacing_large"
android:gravity="center_vertical|start"
android:textSize="16sp"
android:textAlignment="viewStart"
tools:text="Achievement" />
<TextView
android:id="@+id/achievement_description"
style="@style/TextAppearance.MaterialComponents.Headline5"
android:layout_width="wrap_content"
android:layout_height="76dp"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:layout_marginStart="@dimen/spacing_large"
android:layout_marginEnd="@dimen/spacing_large"
android:gravity="center_vertical|start"
android:textSize="16sp"
android:textAlignment="viewStart"
tools:text="Achievement" />
<TextView
android:id="@+id/achievement_score"
style="@style/TextAppearance.MaterialComponents.Headline5"
android:layout_width="wrap_content"
android:layout_height="76dp"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:layout_marginStart="@dimen/spacing_large"
android:layout_marginEnd="@dimen/spacing_large"
android:gravity="center_vertical|start"
android:textSize="16sp"
android:textAlignment="viewStart"
tools:text="Achievement" />
<TextView
android:id="@+id/achievement_status"
style="@style/TextAppearance.MaterialComponents.Headline5"
android:layout_width="wrap_content"
android:layout_height="76dp"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:layout_marginStart="@dimen/spacing_large"
android:layout_marginEnd="@dimen/spacing_large"
android:gravity="center_vertical|start"
android:textSize="16sp"
android:textAlignment="viewStart"
tools:text="Achievement" />
<TextView
android:id="@+id/achievement_progress"
style="@style/TextAppearance.MaterialComponents.Headline5"
android:layout_width="wrap_content"
android:layout_height="76dp"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:layout_marginStart="@dimen/spacing_large"
android:layout_marginEnd="@dimen/spacing_large"
android:gravity="center_vertical|start"
android:textSize="16sp"
android:textAlignment="viewStart"
tools:text="Achievement" />
</RelativeLayout>

View File

@ -604,6 +604,7 @@ It can efficiently compress both junk data and encrypted Wii data.
<string name="emulation_screenshot">Take Screenshot</string>
<string name="emulation_savestate">Save State</string>
<string name="emulation_loadstate">Load State</string>
<string name="emulation_achievements">Achievements</string>
<string name="emulation_exit">Exit Emulation</string>
<string name="emulation_state_slot">Slot %1$d\n\n%2$s</string>
<string name="emulation_state_slot_empty">Slot %1$d\n\nEmpty</string>
@ -956,6 +957,7 @@ It can efficiently compress both junk data and encrypted Wii data.
<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">Achievements</string>
<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>
@ -970,4 +972,5 @@ It can efficiently compress both junk data and encrypted Wii data.
<string name="achievements_logout">Log Out</string>
<string name="achievements_login_in_progress">Logging In</string>
<string name="achievements_login_failed">Login Failed</string>
<string name="achievements_progress">Achievements</string>
</resources>

View File

@ -55,4 +55,64 @@ Java_org_dolphinemu_dolphinemu_features_settings_model_AchievementModel_shutdown
AchievementManager::GetInstance().Shutdown();
}
JNIEXPORT jboolean JNICALL
Java_org_dolphinemu_dolphinemu_features_achievements_model_AchievementProgressViewModel_isGameLoaded(
JNIEnv* env, jclass)
{
return AchievementManager::GetInstance().IsGameLoaded();
}
extern "C"
JNIEXPORT jobjectArray JNICALL
Java_org_dolphinemu_dolphinemu_features_achievements_model_AchievementProgressViewModel_fetchProgress(
JNIEnv *env, jclass clazz)
{
auto& instance = AchievementManager::GetInstance();
if (!instance.IsGameLoaded())
return env->NewObjectArray(0, IDCache::GetAchievementBucketClass(), nullptr);
auto* client = instance.GetClient();
auto* achievement_list =
rc_client_create_achievement_list(client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE_AND_UNOFFICIAL,
RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS);
if (!achievement_list)
return env->NewObjectArray(0, IDCache::GetAchievementBucketClass(), nullptr);
auto progress = env->NewObjectArray(achievement_list->num_buckets, IDCache::GetAchievementBucketClass(), nullptr);
for (u32 ix = 0; ix < achievement_list->num_buckets; ix++)
{
auto bucket = env->NewObject(IDCache::GetAchievementBucketClass(), IDCache::GetAchievementBucketConstructor(), achievement_list->buckets[ix].num_achievements);
env->SetObjectArrayElement(progress, ix, bucket);
env->SetObjectField(bucket, IDCache::GetAchievementBucketLabel(), ToJString(env, achievement_list->buckets[ix].label));
env->SetIntField(bucket, IDCache::GetAchievementBucketSubsetId(), achievement_list->buckets[ix].subset_id);
env->SetIntField(bucket, IDCache::GetAchievementBucketBucketType(), achievement_list->buckets[ix].bucket_type);
auto bucket_achievements = static_cast<jobjectArray>(env->GetObjectField(bucket, IDCache::GetAchievementBucketAchievements()));
for (u32 jx = 0; jx < achievement_list->buckets[ix].num_achievements; jx++)
{
auto achievement = env->GetObjectArrayElement(bucket_achievements, jx);
env->SetObjectField(achievement, IDCache::GetAchievementTitle(), ToJString(env, achievement_list->buckets[ix].achievements[jx]->title));
env->SetObjectField(achievement, IDCache::GetAchievementDescription(), ToJString(env, achievement_list->buckets[ix].achievements[jx]->description));
env->SetObjectField(achievement, IDCache::GetAchievementBadgeName(), ToJString(env, achievement_list->buckets[ix].achievements[jx]->badge_name));
env->SetObjectField(achievement, IDCache::GetAchievementMeasuredProgress(), ToJString(env, achievement_list->buckets[ix].achievements[jx]->measured_progress));
env->SetFloatField(achievement, IDCache::GetAchievementMeasuredPercent(), achievement_list->buckets[ix].achievements[jx]->measured_percent);
env->SetIntField(achievement, IDCache::GetAchievementId(), achievement_list->buckets[ix].achievements[jx]->id);
env->SetIntField(achievement, IDCache::GetAchievementPoints(), achievement_list->buckets[ix].achievements[jx]->points);
// TODO: Convert to string? Or something.
//env->SetObjectField(achievement, IDCache::GetAchievementUnlockTime(), ToJString(env, achievement_list->buckets[ix].achievements[jx]->unlock_time));
env->SetIntField(achievement, IDCache::GetAchievementState(), achievement_list->buckets[ix].achievements[jx]->state);
env->SetIntField(achievement, IDCache::GetAchievementCategory(), achievement_list->buckets[ix].achievements[jx]->category);
env->SetIntField(achievement, IDCache::GetAchievementBucket(), achievement_list->buckets[ix].achievements[jx]->bucket);
env->SetIntField(achievement, IDCache::GetAchievementUnlocked(), achievement_list->buckets[ix].achievements[jx]->unlocked);
env->SetFloatField(achievement, IDCache::GetAchievementRarity(), achievement_list->buckets[ix].achievements[jx]->rarity);
env->SetFloatField(achievement, IDCache::GetAchievementRarityHardcore(), achievement_list->buckets[ix].achievements[jx]->rarity_hardcore);
env->SetIntField(achievement, IDCache::GetAchievementType(), achievement_list->buckets[ix].achievements[jx]->type);
env->SetObjectField(achievement, IDCache::GetAchievementBadgeUrl(), ToJString(env, achievement_list->buckets[ix].achievements[jx]->badge_url));
env->SetObjectField(achievement, IDCache::GetAchievementBadgeLockedUrl(), ToJString(env, achievement_list->buckets[ix].achievements[jx]->badge_locked_url));
env->DeleteLocalRef(achievement);
}
env->DeleteLocalRef(bucket);
}
rc_client_destroy_achievement_list(achievement_list);
return progress;
}
} // extern "C"

View File

@ -125,6 +125,33 @@ static jclass s_audio_utils_class;
static jmethodID s_audio_utils_get_sample_rate;
static jmethodID s_audio_utils_get_frames_per_buffer;
static jclass s_achievement_bucket_class;
static jfieldID s_achievement_bucket_label;
static jfieldID s_achievement_bucket_subset_id;
static jfieldID s_achievement_bucket_bucket_type;
static jfieldID s_achievement_bucket_achievements;
static jmethodID s_achievement_bucket_constructor;
static jclass s_achievement_class;
static jfieldID s_achievement_title;
static jfieldID s_achievement_description;
static jfieldID s_achievement_badge_name;
static jfieldID s_achievement_measured_progress;
static jfieldID s_achievement_measured_percent;
static jfieldID s_achievement_id;
static jfieldID s_achievement_points;
static jfieldID s_achievement_unlock_time;
static jfieldID s_achievement_state;
static jfieldID s_achievement_category;
static jfieldID s_achievement_bucket;
static jfieldID s_achievement_unlocked;
static jfieldID s_achievement_rarity;
static jfieldID s_achievement_rarity_hardcore;
static jfieldID s_achievement_type;
static jfieldID s_achievement_badge_url;
static jfieldID s_achievement_badge_locked_url;
static jmethodID s_achievement_constructor;
namespace IDCache
{
JNIEnv* GetEnvForThread()
@ -575,6 +602,131 @@ jmethodID GetAudioUtilsGetFramesPerBuffer()
return s_audio_utils_get_frames_per_buffer;
}
jclass GetAchievementBucketClass()
{
return s_achievement_bucket_class;
}
jfieldID GetAchievementBucketLabel()
{
return s_achievement_bucket_label;
}
jfieldID GetAchievementBucketSubsetId()
{
return s_achievement_bucket_subset_id;
}
jfieldID GetAchievementBucketBucketType()
{
return s_achievement_bucket_bucket_type;
}
jfieldID GetAchievementBucketAchievements()
{
return s_achievement_bucket_achievements;
}
jmethodID GetAchievementBucketConstructor()
{
return s_achievement_bucket_constructor;
}
jclass GetAchievementClass()
{
return s_achievement_class;
}
jfieldID GetAchievementTitle()
{
return s_achievement_title;
}
jfieldID GetAchievementDescription()
{
return s_achievement_description;
}
jfieldID GetAchievementBadgeName()
{
return s_achievement_badge_name;
}
jfieldID GetAchievementMeasuredProgress()
{
return s_achievement_measured_progress;
}
jfieldID GetAchievementMeasuredPercent()
{
return s_achievement_measured_percent;
}
jfieldID GetAchievementId()
{
return s_achievement_id;
}
jfieldID GetAchievementPoints()
{
return s_achievement_points;
}
jfieldID GetAchievementUnlockTime()
{
return s_achievement_unlock_time;
}
jfieldID GetAchievementState()
{
return s_achievement_state;
}
jfieldID GetAchievementCategory()
{
return s_achievement_category;
}
jfieldID GetAchievementBucket()
{
return s_achievement_bucket;
}
jfieldID GetAchievementUnlocked()
{
return s_achievement_unlocked;
}
jfieldID GetAchievementRarity()
{
return s_achievement_rarity;
}
jfieldID GetAchievementRarityHardcore()
{
return s_achievement_rarity_hardcore;
}
jfieldID GetAchievementType()
{
return s_achievement_type;
}
jfieldID GetAchievementBadgeUrl()
{
return s_achievement_badge_url;
}
jfieldID GetAchievementBadgeLockedUrl()
{
return s_achievement_badge_locked_url;
}
jmethodID GetAchievementConstructor()
{
return s_achievement_constructor;
}
} // namespace IDCache
extern "C" {
@ -816,6 +968,43 @@ JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved)
env->GetStaticMethodID(audio_utils_class, "getFramesPerBuffer", "()I");
env->DeleteLocalRef(audio_utils_class);
const jclass achievement_bucket_class =
env->FindClass("org/dolphinemu/dolphinemu/features/achievements/model/AchievementBucket");
s_achievement_bucket_class =
reinterpret_cast<jclass>(env->NewGlobalRef(achievement_bucket_class));
s_achievement_bucket_label = env->GetFieldID(IDCache::GetAchievementBucketClass(), "label", "Ljava/lang/String;");
s_achievement_bucket_subset_id = env->GetFieldID(IDCache::GetAchievementBucketClass(), "subsetId", "I");
s_achievement_bucket_bucket_type = env->GetFieldID(IDCache::GetAchievementBucketClass(), "bucketType", "I");
s_achievement_bucket_achievements = env->GetFieldID(IDCache::GetAchievementBucketClass(), "achievements", "[Lorg/dolphinemu/dolphinemu/features/achievements/model/Achievement;");
s_achievement_bucket_constructor =
env->GetMethodID(achievement_bucket_class, "<init>", "(I)V");
env->DeleteLocalRef(achievement_bucket_class);
const jclass achievement_class =
env->FindClass("org/dolphinemu/dolphinemu/features/achievements/model/Achievement");
s_achievement_class =
reinterpret_cast<jclass>(env->NewGlobalRef(achievement_class));
s_achievement_title = env->GetFieldID(IDCache::GetAchievementClass(), "title", "Ljava/lang/String;");
s_achievement_description = env->GetFieldID(IDCache::GetAchievementClass(), "description", "Ljava/lang/String;");
s_achievement_badge_name = env->GetFieldID(IDCache::GetAchievementClass(), "badgeName", "Ljava/lang/String;");
s_achievement_measured_progress = env->GetFieldID(IDCache::GetAchievementClass(), "measuredProgress", "Ljava/lang/String;");
s_achievement_measured_percent = env->GetFieldID(IDCache::GetAchievementClass(), "measuredPercent", "F");
s_achievement_id = env->GetFieldID(IDCache::GetAchievementClass(), "id", "I");
s_achievement_points = env->GetFieldID(IDCache::GetAchievementClass(), "points", "I");
s_achievement_unlock_time = env->GetFieldID(IDCache::GetAchievementClass(), "unlockTime", "Ljava/lang/String;");
s_achievement_state = env->GetFieldID(IDCache::GetAchievementClass(), "state", "I");
s_achievement_category = env->GetFieldID(IDCache::GetAchievementClass(), "category", "I");
s_achievement_bucket = env->GetFieldID(IDCache::GetAchievementClass(), "bucket", "I");
s_achievement_unlocked = env->GetFieldID(IDCache::GetAchievementClass(), "unlocked", "I");
s_achievement_rarity = env->GetFieldID(IDCache::GetAchievementClass(), "rarity", "F");
s_achievement_rarity_hardcore = env->GetFieldID(IDCache::GetAchievementClass(), "rarityHardcore", "F");
s_achievement_type = env->GetFieldID(IDCache::GetAchievementClass(), "type", "I");
s_achievement_badge_url = env->GetFieldID(IDCache::GetAchievementClass(), "badgeUrl", "Ljava/lang/String;");
s_achievement_badge_locked_url = env->GetFieldID(IDCache::GetAchievementClass(), "badgeLockedUrl", "Ljava/lang/String;");
s_achievement_constructor =
env->GetMethodID(achievement_class, "<init>","()V");
env->DeleteLocalRef(achievement_class);
return JNI_VERSION;
}
@ -853,5 +1042,7 @@ JNIEXPORT void JNI_OnUnload(JavaVM* vm, void* reserved)
env->DeleteGlobalRef(s_input_detector_class);
env->DeleteGlobalRef(s_permission_handler_class);
env->DeleteGlobalRef(s_audio_utils_class);
env->DeleteGlobalRef(s_achievement_bucket_class);
env->DeleteGlobalRef(s_achievement_class);
}
}

View File

@ -124,4 +124,31 @@ jclass GetAudioUtilsClass();
jmethodID GetAudioUtilsGetSampleRate();
jmethodID GetAudioUtilsGetFramesPerBuffer();
jclass GetAchievementBucketClass();
jfieldID GetAchievementBucketLabel();
jfieldID GetAchievementBucketSubsetId();
jfieldID GetAchievementBucketBucketType();
jfieldID GetAchievementBucketAchievements();
jmethodID GetAchievementBucketConstructor();
jclass GetAchievementClass();
jfieldID GetAchievementTitle();
jfieldID GetAchievementDescription();
jfieldID GetAchievementBadgeName();
jfieldID GetAchievementMeasuredProgress();
jfieldID GetAchievementMeasuredPercent();
jfieldID GetAchievementId();
jfieldID GetAchievementPoints();
jfieldID GetAchievementUnlockTime();
jfieldID GetAchievementState();
jfieldID GetAchievementCategory();
jfieldID GetAchievementBucket();
jfieldID GetAchievementUnlocked();
jfieldID GetAchievementRarity();
jfieldID GetAchievementRarityHardcore();
jfieldID GetAchievementType();
jfieldID GetAchievementBadgeUrl();
jfieldID GetAchievementBadgeLockedUrl();
jmethodID GetAchievementConstructor();
} // namespace IDCache