mirror of
https://github.com/rafaelvcaetano/melonDS-android.git
synced 2024-11-23 13:49:43 +00:00
Add base support for RetroAchievements hardcore mode
This commit is contained in:
parent
4bca90ee42
commit
c2658424b3
@ -2,7 +2,7 @@
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 4,
|
||||
"identityHash": "31374048dc97dc955a845a6920edacaa",
|
||||
"identityHash": "afbfe1a8a5ee33a9307efcca89acecbd",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "cheat_database",
|
||||
@ -365,7 +365,7 @@
|
||||
},
|
||||
{
|
||||
"tableName": "ra_user_achievement",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`game_id` INTEGER NOT NULL, `achievement_id` INTEGER NOT NULL, `is_unlocked` INTEGER NOT NULL, PRIMARY KEY(`game_id`, `achievement_id`))",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`game_id` INTEGER NOT NULL, `achievement_id` INTEGER NOT NULL, `is_unlocked` INTEGER NOT NULL, `is_hardcore` INTEGER NOT NULL, PRIMARY KEY(`game_id`, `achievement_id`, `is_hardcore`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "gameId",
|
||||
@ -384,13 +384,20 @@
|
||||
"columnName": "is_unlocked",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isHardcore",
|
||||
"columnName": "is_hardcore",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"game_id",
|
||||
"achievement_id"
|
||||
"achievement_id",
|
||||
"is_hardcore"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
@ -398,7 +405,7 @@
|
||||
},
|
||||
{
|
||||
"tableName": "ra_game_set_metadata",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`game_id` INTEGER NOT NULL, `last_achievement_set_updated` INTEGER, `last_user_data_updated` INTEGER, PRIMARY KEY(`game_id`))",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`game_id` INTEGER NOT NULL, `last_achievement_set_updated` INTEGER, `last_user_data_updated` INTEGER, `last_hardcore_user_data_updated` INTEGER, PRIMARY KEY(`game_id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "gameId",
|
||||
@ -413,10 +420,16 @@
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastUserDataUpdated",
|
||||
"fieldPath": "lastSoftcoreUserDataUpdated",
|
||||
"columnName": "last_user_data_updated",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastHardcoreUserDataUpdated",
|
||||
"columnName": "last_hardcore_user_data_updated",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
@ -491,7 +504,7 @@
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '31374048dc97dc955a845a6920edacaa')"
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'afbfe1a8a5ee33a9307efcca89acecbd')"
|
||||
]
|
||||
}
|
||||
}
|
@ -42,8 +42,8 @@ abstract class RAAchievementsDao {
|
||||
@Query("SELECT * FROM ra_game WHERE game_id = :gameId")
|
||||
abstract suspend fun getGame(gameId: Long): RAGameEntity?
|
||||
|
||||
@Query("SELECT * FROM ra_user_achievement WHERE game_id = :gameId AND is_unlocked = 1")
|
||||
abstract suspend fun getGameUserUnlockedAchievements(gameId: Long): List<RAUserAchievementEntity>
|
||||
@Query("SELECT * FROM ra_user_achievement WHERE game_id = :gameId AND is_hardcore = :forHardcoreMode AND is_unlocked = 1")
|
||||
abstract suspend fun getGameUserUnlockedAchievements(gameId: Long, forHardcoreMode: Boolean): List<RAUserAchievementEntity>
|
||||
|
||||
@Query("DELETE FROM ra_user_achievement WHERE game_id = :gameId")
|
||||
protected abstract suspend fun deleteGameUserUnlockedAchievements(gameId: Long)
|
||||
|
@ -9,5 +9,6 @@ import java.time.Instant
|
||||
data class RAGameSetMetadata(
|
||||
@PrimaryKey @ColumnInfo(name = "game_id") val gameId: Long,
|
||||
@ColumnInfo(name = "last_achievement_set_updated") val lastAchievementSetUpdated: Instant?,
|
||||
@ColumnInfo(name = "last_user_data_updated") val lastUserDataUpdated: Instant?,
|
||||
@ColumnInfo(name = "last_user_data_updated") val lastSoftcoreUserDataUpdated: Instant?,
|
||||
@ColumnInfo(name = "last_hardcore_user_data_updated") val lastHardcoreUserDataUpdated: Instant?,
|
||||
)
|
||||
|
@ -5,10 +5,11 @@ import androidx.room.Entity
|
||||
|
||||
@Entity(
|
||||
tableName = "ra_user_achievement",
|
||||
primaryKeys = ["game_id", "achievement_id"],
|
||||
primaryKeys = ["game_id", "achievement_id", "is_hardcore"],
|
||||
)
|
||||
data class RAUserAchievementEntity(
|
||||
@ColumnInfo(name = "game_id") val gameId: Long,
|
||||
@ColumnInfo(name = "achievement_id") val achievementId: Long,
|
||||
@ColumnInfo(name = "is_unlocked") val isUnlocked: Boolean,
|
||||
@ColumnInfo(name = "is_hardcore") val isHardcore: Boolean,
|
||||
)
|
||||
|
@ -5,4 +5,22 @@ import me.magnum.rcheevosapi.model.RAAchievement
|
||||
data class RAUserAchievement(
|
||||
val achievement: RAAchievement,
|
||||
val isUnlocked: Boolean,
|
||||
)
|
||||
val forHardcoreMode: Boolean,
|
||||
) {
|
||||
|
||||
/**
|
||||
* Returns the number of points that this achievement is worth for the user in the current state. This depends on the actual number of points that the achievements awards,
|
||||
* whether the user has unlocked it or not and if it was unlocked in hardcore or softcore mode.
|
||||
*/
|
||||
fun pointsWorth(): Int {
|
||||
return if (isUnlocked) {
|
||||
if (forHardcoreMode) {
|
||||
achievement.points * 2
|
||||
} else {
|
||||
achievement.points
|
||||
}
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
}
|
@ -9,10 +9,10 @@ interface RetroAchievementsRepository {
|
||||
suspend fun getUserAuthentication(): RAUserAuth?
|
||||
suspend fun login(username: String, password: String): Result<Unit>
|
||||
suspend fun logout()
|
||||
suspend fun getGameUserAchievements(gameHash: String): Result<List<RAUserAchievement>>
|
||||
suspend fun getGameUserAchievements(gameHash: String, forHardcoreMode: Boolean): Result<List<RAUserAchievement>>
|
||||
suspend fun getGameRichPresencePatch(gameHash: String): String?
|
||||
suspend fun getAchievement(achievementId: Long): Result<RAAchievement?>
|
||||
suspend fun awardAchievement(achievement: RAAchievement)
|
||||
suspend fun awardAchievement(achievement: RAAchievement, forHardcoreMode: Boolean)
|
||||
suspend fun submitPendingAchievements(): Result<Unit>
|
||||
suspend fun startSession(gameHash: String)
|
||||
suspend fun sendSessionHeartbeat(gameHash: String, richPresenceDescription: String?)
|
||||
|
@ -51,6 +51,7 @@ interface SettingsRepository {
|
||||
fun getSoftInputOpacity(): Int
|
||||
|
||||
fun isRetroAchievementsRichPresenceEnabled(): Boolean
|
||||
fun isRetroAchievementsHardcoreEnabled(): Boolean
|
||||
|
||||
fun areCheatsEnabled(): Boolean
|
||||
|
||||
|
@ -55,7 +55,7 @@ class AndroidRetroAchievementsRepository(
|
||||
raUserAuthStore.clearUserAuth()
|
||||
}
|
||||
|
||||
override suspend fun getGameUserAchievements(gameHash: String): Result<List<RAUserAchievement>> {
|
||||
override suspend fun getGameUserAchievements(gameHash: String, forHardcoreMode: Boolean): Result<List<RAUserAchievement>> {
|
||||
val gameIdResult = getGameIdFromGameHash(gameHash)
|
||||
if (gameIdResult.isFailure) {
|
||||
return Result.failure(gameIdResult.exceptionOrNull()!!)
|
||||
@ -74,7 +74,7 @@ class AndroidRetroAchievementsRepository(
|
||||
return Result.failure(gameAchievementsResult.exceptionOrNull()!!)
|
||||
}
|
||||
|
||||
val userUnlocksResult = fetchGameUserUnlockedAchievements(gameId, currentMetadata)
|
||||
val userUnlocksResult = fetchGameUserUnlockedAchievements(gameId, forHardcoreMode, currentMetadata)
|
||||
if (userUnlocksResult.isFailure) {
|
||||
return Result.failure(userUnlocksResult.exceptionOrNull()!!)
|
||||
}
|
||||
@ -86,6 +86,7 @@ class AndroidRetroAchievementsRepository(
|
||||
RAUserAchievement(
|
||||
achievement = it,
|
||||
isUnlocked = userUnlocks.contains(it.id),
|
||||
forHardcoreMode = forHardcoreMode,
|
||||
)
|
||||
}
|
||||
return Result.success(userAchievements)
|
||||
@ -108,14 +109,16 @@ class AndroidRetroAchievementsRepository(
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun awardAchievement(achievement: RAAchievement) {
|
||||
submitAchievementAward(achievement.id, achievement.gameId, false, true)
|
||||
override suspend fun awardAchievement(achievement: RAAchievement, forHardcoreMode: Boolean) {
|
||||
submitAchievementAward(achievement.id, achievement.gameId, forHardcoreMode).onFailure {
|
||||
scheduleAchievementSubmissionJob()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun submitPendingAchievements(): Result<Unit> {
|
||||
achievementsDao.getPendingAchievementSubmissions().forEach {
|
||||
// Do not schedule resubmission if this fails. The current submission job should schedule another attempt
|
||||
val submissionResult = submitAchievementAward(it.achievementId, RAGameId(it.gameId), it.forHardcoreMode, false)
|
||||
val submissionResult = submitAchievementAward(it.achievementId, RAGameId(it.gameId), it.forHardcoreMode)
|
||||
if (submissionResult.isFailure) {
|
||||
return submissionResult
|
||||
}
|
||||
@ -136,9 +139,14 @@ class AndroidRetroAchievementsRepository(
|
||||
raApi.sendPing(gameId, richPresenceDescription)
|
||||
}
|
||||
|
||||
private suspend fun submitAchievementAward(achievementId: Long, gameId: RAGameId, forHardcoreMode: Boolean, scheduleResubmissionOnFailure: Boolean): Result<Unit> {
|
||||
private suspend fun submitAchievementAward(achievementId: Long, gameId: RAGameId, forHardcoreMode: Boolean): Result<Unit> {
|
||||
// Award the achievement immediately locally
|
||||
val userAchievement = RAUserAchievementEntity(gameId.id, achievementId, true)
|
||||
val userAchievement = RAUserAchievementEntity(
|
||||
gameId = gameId.id,
|
||||
achievementId = achievementId,
|
||||
isUnlocked = true,
|
||||
isHardcore = forHardcoreMode,
|
||||
)
|
||||
achievementsDao.addUserAchievement(userAchievement)
|
||||
|
||||
return raApi.awardAchievement(achievementId, forHardcoreMode).onFailure {
|
||||
@ -149,9 +157,6 @@ class AndroidRetroAchievementsRepository(
|
||||
forHardcoreMode = forHardcoreMode,
|
||||
)
|
||||
achievementsDao.addPendingAchievementSubmission(pendingAchievementSubmissionEntity)
|
||||
if (scheduleResubmissionOnFailure) {
|
||||
scheduleAchievementSubmissionJob()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -217,24 +222,25 @@ class AndroidRetroAchievementsRepository(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchGameUserUnlockedAchievements(gameId: RAGameId, gameSetMetadata: CurrentGameSetMetadata): Result<List<Long>> {
|
||||
return if (mustRefreshUserData(gameSetMetadata.currentMetadata)) {
|
||||
raApi.getUserUnlockedAchievements(gameId, false).onSuccess { userUnlocks ->
|
||||
private suspend fun fetchGameUserUnlockedAchievements(gameId: RAGameId, forHardcoreMode: Boolean, gameSetMetadata: CurrentGameSetMetadata): Result<List<Long>> {
|
||||
return if (mustRefreshUserData(gameSetMetadata.currentMetadata, forHardcoreMode)) {
|
||||
raApi.getUserUnlockedAchievements(gameId, forHardcoreMode).onSuccess { userUnlocks ->
|
||||
val userAchievementEntities = userUnlocks.map {
|
||||
RAUserAchievementEntity(
|
||||
gameId.id,
|
||||
it,
|
||||
true,
|
||||
gameId = gameId.id,
|
||||
achievementId = it,
|
||||
isUnlocked = true,
|
||||
isHardcore = forHardcoreMode,
|
||||
)
|
||||
}
|
||||
|
||||
val newMetadata = gameSetMetadata.withNewUserAchievementsUpdate()
|
||||
val newMetadata = gameSetMetadata.withNewUserAchievementsUpdate(forHardcoreMode)
|
||||
achievementsDao.updateGameUserUnlockedAchievements(gameId.id, userAchievementEntities)
|
||||
achievementsDao.updateGameSetMetadata(newMetadata)
|
||||
}.recoverCatching { exception ->
|
||||
if (gameSetMetadata.isUserAchievementDataKnown()) {
|
||||
if (gameSetMetadata.isUserAchievementDataKnown(forHardcoreMode)) {
|
||||
// Load DB data because we know that it was previously loaded
|
||||
achievementsDao.getGameUserUnlockedAchievements(gameId.id).map {
|
||||
achievementsDao.getGameUserUnlockedAchievements(gameId.id, forHardcoreMode).map {
|
||||
it.achievementId
|
||||
}
|
||||
} else {
|
||||
@ -244,7 +250,7 @@ class AndroidRetroAchievementsRepository(
|
||||
}
|
||||
} else {
|
||||
runCatching {
|
||||
achievementsDao.getGameUserUnlockedAchievements(gameId.id).map {
|
||||
achievementsDao.getGameUserUnlockedAchievements(gameId.id, forHardcoreMode).map {
|
||||
it.achievementId
|
||||
}
|
||||
}
|
||||
@ -268,13 +274,19 @@ class AndroidRetroAchievementsRepository(
|
||||
return Duration.between(gameSetMetadata.lastAchievementSetUpdated, Instant.now()) >= Duration.ofDays(7)
|
||||
}
|
||||
|
||||
private fun mustRefreshUserData(gameSetMetadata: RAGameSetMetadata?): Boolean {
|
||||
if (gameSetMetadata?.lastUserDataUpdated == null) {
|
||||
private fun mustRefreshUserData(gameSetMetadata: RAGameSetMetadata?, forHardcoreMode: Boolean): Boolean {
|
||||
val lastUserDataUpdateTimestamp = if (forHardcoreMode) {
|
||||
gameSetMetadata?.lastHardcoreUserDataUpdated
|
||||
} else {
|
||||
gameSetMetadata?.lastSoftcoreUserDataUpdated
|
||||
}
|
||||
|
||||
if (lastUserDataUpdateTimestamp == null) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Sync user achievement data once a day
|
||||
return Duration.between(gameSetMetadata.lastUserDataUpdated, Instant.now()) >= Duration.ofDays(1)
|
||||
return Duration.between(lastUserDataUpdateTimestamp, Instant.now()) >= Duration.ofDays(1)
|
||||
}
|
||||
|
||||
private fun scheduleAchievementSubmissionJob() {
|
||||
@ -296,14 +308,20 @@ class AndroidRetroAchievementsRepository(
|
||||
private set
|
||||
|
||||
fun withNewAchievementSetUpdate(): RAGameSetMetadata {
|
||||
return (currentMetadata?.copy(lastAchievementSetUpdated = Instant.now()) ?: RAGameSetMetadata(gameId.id, Instant.now(), null)).also {
|
||||
return (currentMetadata?.copy(lastAchievementSetUpdated = Instant.now()) ?: RAGameSetMetadata(gameId.id, Instant.now(), null, null)).also {
|
||||
currentMetadata = it
|
||||
}
|
||||
}
|
||||
|
||||
fun withNewUserAchievementsUpdate(): RAGameSetMetadata {
|
||||
return (currentMetadata?.copy(lastUserDataUpdated = Instant.now()) ?: RAGameSetMetadata(gameId.id, null, Instant.now())).also {
|
||||
currentMetadata = it
|
||||
fun withNewUserAchievementsUpdate(forHardcoreMode: Boolean): RAGameSetMetadata {
|
||||
return if (forHardcoreMode) {
|
||||
currentMetadata?.copy(lastHardcoreUserDataUpdated = Instant.now()) ?: RAGameSetMetadata(gameId.id, null, null, Instant.now()).also {
|
||||
currentMetadata = it
|
||||
}
|
||||
} else {
|
||||
currentMetadata?.copy(lastSoftcoreUserDataUpdated = Instant.now()) ?: RAGameSetMetadata(gameId.id, null, Instant.now(), null).also {
|
||||
currentMetadata = it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -311,8 +329,12 @@ class AndroidRetroAchievementsRepository(
|
||||
return currentMetadata?.lastAchievementSetUpdated != null
|
||||
}
|
||||
|
||||
fun isUserAchievementDataKnown(): Boolean {
|
||||
return currentMetadata?.lastUserDataUpdated != null
|
||||
fun isUserAchievementDataKnown(forHardcoreMode: Boolean): Boolean {
|
||||
return if (forHardcoreMode) {
|
||||
currentMetadata?.lastHardcoreUserDataUpdated != null
|
||||
} else {
|
||||
currentMetadata?.lastSoftcoreUserDataUpdated != null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -370,6 +370,10 @@ class SharedPreferencesSettingsRepository(
|
||||
return preferences.getBoolean("ra_rich_presence", true)
|
||||
}
|
||||
|
||||
override fun isRetroAchievementsHardcoreEnabled(): Boolean {
|
||||
return preferences.getBoolean("ra_hardcore_enabled", false)
|
||||
}
|
||||
|
||||
override fun areCheatsEnabled(): Boolean {
|
||||
return preferences.getBoolean("cheats_enabled", false)
|
||||
}
|
||||
|
@ -290,7 +290,7 @@ class EmulatorViewModel @Inject constructor(
|
||||
|
||||
viewModelScope.launch {
|
||||
if (retroAchievementsRepository.isUserAuthenticated()) {
|
||||
val achievementData = retroAchievementsRepository.getGameUserAchievements(rom.retroAchievementsHash).map { achievements ->
|
||||
val achievementData = retroAchievementsRepository.getGameUserAchievements(rom.retroAchievementsHash, false).map { achievements ->
|
||||
achievements.filter { !it.isUnlocked }.map { RASimpleAchievement(it.achievement.id, it.achievement.memoryAddress) }
|
||||
}.fold(
|
||||
onSuccess = {
|
||||
@ -315,7 +315,7 @@ class EmulatorViewModel @Inject constructor(
|
||||
.onSuccess {
|
||||
if (it != null) {
|
||||
_achievementTriggeredEvent.emit(it)
|
||||
retroAchievementsRepository.awardAchievement(it)
|
||||
retroAchievementsRepository.awardAchievement(it, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -40,7 +40,7 @@ class RomRetroAchievementsViewModel @Inject constructor(
|
||||
private fun loadAchievements() {
|
||||
viewModelScope.launch {
|
||||
if (retroAchievementsRepository.isUserAuthenticated()) {
|
||||
retroAchievementsRepository.getGameUserAchievements(rom.retroAchievementsHash).fold(
|
||||
retroAchievementsRepository.getGameUserAchievements(rom.retroAchievementsHash, false).fold(
|
||||
onSuccess = { achievements ->
|
||||
val sortedAchievements = achievements.sortedBy {
|
||||
// Display unlocked achievements first
|
||||
@ -82,7 +82,7 @@ class RomRetroAchievementsViewModel @Inject constructor(
|
||||
return RomAchievementsSummary(
|
||||
totalAchievements = userAchievements.size,
|
||||
completedAchievements = userAchievements.count { it.isUnlocked },
|
||||
totalPoints = userAchievements.sumOf { if (it.isUnlocked) it.achievement.points else 0 },
|
||||
totalPoints = userAchievements.sumOf { it.pointsWorth() },
|
||||
)
|
||||
}
|
||||
}
|
@ -191,6 +191,7 @@ fun PreviewRomAchievementUi() {
|
||||
userAchievement = RAUserAchievement(
|
||||
achievement = mockRAAchievementPreview(),
|
||||
isUnlocked = true,
|
||||
forHardcoreMode = false,
|
||||
),
|
||||
onViewAchievement = {},
|
||||
)
|
||||
|
@ -293,8 +293,8 @@ private fun PreviewContent() {
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
content = RomRetroAchievementsUiState.Ready(
|
||||
listOf(
|
||||
RAUserAchievement(mockRAAchievementPreview(id = 1), false),
|
||||
RAUserAchievement(mockRAAchievementPreview(id = 2, title = "This is another amazing achievement", description = "But this one cannot be missed."), false),
|
||||
RAUserAchievement(mockRAAchievementPreview(id = 1), false, false),
|
||||
RAUserAchievement(mockRAAchievementPreview(id = 2, title = "This is another amazing achievement", description = "But this one cannot be missed."), false, false),
|
||||
),
|
||||
RomAchievementsSummary(50, 20, 85),
|
||||
),
|
||||
|
Loading…
Reference in New Issue
Block a user