From 906199a517e7d396716188a2804d32a15f87831f Mon Sep 17 00:00:00 2001 From: Azalea <22280294+hykilpikonna@users.noreply.github.com> Date: Tue, 19 Mar 2024 19:58:00 -0400 Subject: [PATCH] [+] Change in-game settings --- src/main/java/ext/Ext.kt | 8 ++- .../java/icu/samnyan/aqua/net/SettingsApi.kt | 17 ++--- .../java/icu/samnyan/aqua/net/games/Chusan.kt | 12 +++- .../icu/samnyan/aqua/net/games/GameHelper.kt | 61 ++--------------- .../icu/samnyan/aqua/net/games/Maimai2.kt | 9 ++- .../java/icu/samnyan/aqua/net/games/Models.kt | 68 +++++++++++++++++-- .../java/icu/samnyan/aqua/net/games/Ongeki.kt | 9 ++- 7 files changed, 110 insertions(+), 74 deletions(-) diff --git a/src/main/java/ext/Ext.kt b/src/main/java/ext/Ext.kt index b7ea7a6b..c475f793 100644 --- a/src/main/java/ext/Ext.kt +++ b/src/main/java/ext/Ext.kt @@ -47,7 +47,13 @@ annotation class SettingField(val name: Str, val desc: Str) // Reflection @Suppress("UNCHECKED_CAST") fun KClass.vars() = memberProperties.mapNotNull { it as? KMutableProperty1 } - +@Suppress("UNCHECKED_CAST") +fun KMutableProperty1.setCast(obj: C, value: String) = set(obj, when (returnType.classifier) { + String::class -> value + Int::class -> value.toInt() + Boolean::class -> value.toBoolean() + else -> 400 - "Invalid field type $returnType" +} as T) // Make it easier to throw a ResponseStatusException operator fun HttpStatus.invoke(message: String? = null): Nothing = throw ApiException(value(), message ?: this.reasonPhrase) diff --git a/src/main/java/icu/samnyan/aqua/net/SettingsApi.kt b/src/main/java/icu/samnyan/aqua/net/SettingsApi.kt index 0857ae27..94324ce3 100644 --- a/src/main/java/icu/samnyan/aqua/net/SettingsApi.kt +++ b/src/main/java/icu/samnyan/aqua/net/SettingsApi.kt @@ -40,14 +40,15 @@ class SettingsApi( userRepo.save(u.apply { gameOptions = it }) } // Check field type - val type = field.returnType - val newValue = when (type.classifier) { - String::class -> value - Int::class -> value.toInt() - Boolean::class -> value.toBoolean() - else -> (400 - "Invalid field type $type") - } - field.set(options, newValue) +// val type = field.returnType +// val newValue = when (type.classifier) { +// String::class -> value +// Int::class -> value.toInt() +// Boolean::class -> value.toBoolean() +// else -> (400 - "Invalid field type $type") +// } +// field.set(options, newValue) + field.setCast(options, value) goRepo.save(options) } } \ No newline at end of file diff --git a/src/main/java/icu/samnyan/aqua/net/games/Chusan.kt b/src/main/java/icu/samnyan/aqua/net/games/Chusan.kt index 2915b657..fd978520 100644 --- a/src/main/java/icu/samnyan/aqua/net/games/Chusan.kt +++ b/src/main/java/icu/samnyan/aqua/net/games/Chusan.kt @@ -3,9 +3,11 @@ package icu.samnyan.aqua.net.games import ext.API import ext.RP import ext.Str +import ext.minus import icu.samnyan.aqua.net.db.AquaUserServices import icu.samnyan.aqua.net.utils.* import icu.samnyan.aqua.sega.chusan.model.* +import icu.samnyan.aqua.sega.chusan.model.userdata.UserData import org.springframework.web.bind.annotation.RestController @RestController @@ -14,9 +16,8 @@ class Chusan( override val us: AquaUserServices, override val playlogRepo: Chu3UserPlaylogRepo, override val userDataRepo: Chu3UserDataRepo, - val userGeneralDataRepository: Chu3UserGeneralDataRepo -): GameApiController("chu3") -{ + val userGeneralDataRepository: Chu3UserGeneralDataRepo, +): GameApiController("chu3") { override suspend fun trend(@RP username: Str): List = us.cardByName(username) { card -> findTrend(playlogRepo.findByUserCardExtId(card.extId) .map { TrendLog(it.playDate.toString(), it.playerRating) }) @@ -24,6 +25,11 @@ class Chusan( // Only show > AAA rank override val shownRanks = chu3Scores.filter { it.first >= 95 * 10000 } + override val settableFields: Map Unit> = mapOf( + "name" to { u, v -> u.setUserName(v) + if (!v.all { it in USERNAME_CHARS }) { 400 - "Invalid character in username" } + }, + ) override suspend fun userSummary(@RP username: Str) = us.cardByName(username) { card -> // Summary values: total plays, player rating, server-wide ranking diff --git a/src/main/java/icu/samnyan/aqua/net/games/GameHelper.kt b/src/main/java/icu/samnyan/aqua/net/games/GameHelper.kt index 62d10103..fe99ee9f 100644 --- a/src/main/java/icu/samnyan/aqua/net/games/GameHelper.kt +++ b/src/main/java/icu/samnyan/aqua/net/games/GameHelper.kt @@ -1,10 +1,13 @@ package icu.samnyan.aqua.net.games import ext.isoDate -import ext.minus -import icu.samnyan.aqua.sega.general.model.Card import java.time.LocalDate -import java.util.* + +const val LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + + "abcdefghijklmnopqrstuvwxyz" + + "0123456789" +const val SYMBOLS = "・:;?!~/+-×÷=♂♀∀#&*@☆○◎◇□△▽♪†‡ΣαβγθφψωДё$()._␣" +const val USERNAME_CHARS = LETTERS + SYMBOLS data class TrendLog(val date: String, val rating: Int) @@ -41,55 +44,3 @@ fun findTrend(log: List): List { } fun List.acc() = if (isEmpty()) 0.0 else sumOf { it.achievement }.toDouble() / size / 10000.0 - -fun GameApiController.genericUserSummary( - card: Card, - ratingComposition: Map, -): GenericGameSummary { - // Summary values: total plays, player rating, server-wide ranking - // number of each rank, max combo, number of full combo, number of all perfect - val user = userDataRepo.findByCard(card) ?: (404 - "Game data not found") - val plays = playlogRepo.findByUserCardExtId(card.extId) - - // Detailed ranks: Find the number of each rank in each level category - // map> - val rankMap = shownRanks.associate { (_, v) -> v to 0 } - val detailedRanks = HashMap>() - plays.forEach { play -> - val lvl = musicMapping[play.musicId]?.notes?.getOrNull(if (play.level == 10) 0 else play.level)?.lv ?: return@forEach - shownRanks.find { (s, _) -> play.achievement > s }?.let { (_, v) -> - val ranks = detailedRanks.getOrPut(lvl.toInt()) { rankMap.toMutableMap() } - ranks[v] = ranks[v]!! + 1 - } - } - - // Collapse detailed ranks to get non-detailed ranks map - val ranks = shownRanks.associate { (_, v) -> v to 0 }.toMutableMap().also { ranks -> - plays.forEach { play -> - shownRanks.find { (s, _) -> play.achievement > s }?.let { (_, v) -> ranks[v] = ranks[v]!! + 1 } - } - } - - return GenericGameSummary( - name = user.userName, - iconId = user.iconId, - aquaUser = card.aquaUser?.publicFields, - serverRank = userDataRepo.getRanking(user.playerRating), - accuracy = plays.acc(), - rating = user.playerRating, - ratingHighest = user.highestRating, - ranks = ranks.map { (k, v) -> RankCount(k, v) }, - detailedRanks = detailedRanks, - maxCombo = plays.maxOfOrNull { it.maxCombo } ?: 0, - fullCombo = plays.count { it.isFullCombo }, - allPerfect = plays.count { it.isAllPerfect }, - totalScore = user.totalScore, - plays = plays.size, - totalPlayTime = plays.count() * 3L, // TODO: Give a better estimate - joined = user.firstPlayDate.toString(), - lastSeen = user.lastPlayDate.toString(), - lastVersion = user.lastRomVersion, - ratingComposition = ratingComposition, - recent = plays.sortedBy { it.userPlayDate.toString() }.takeLast(15).reversed() - ) -} diff --git a/src/main/java/icu/samnyan/aqua/net/games/Maimai2.kt b/src/main/java/icu/samnyan/aqua/net/games/Maimai2.kt index 9d3b16a0..56d1f9c2 100644 --- a/src/main/java/icu/samnyan/aqua/net/games/Maimai2.kt +++ b/src/main/java/icu/samnyan/aqua/net/games/Maimai2.kt @@ -8,6 +8,7 @@ import icu.samnyan.aqua.api.model.resp.sega.maimai2.external.Maimai2DataExport import icu.samnyan.aqua.net.db.AquaUserServices import icu.samnyan.aqua.net.utils.* import icu.samnyan.aqua.sega.maimai2.model.* +import icu.samnyan.aqua.sega.maimai2.model.userdata.UserDetail import org.springframework.web.bind.annotation.RestController import java.lang.reflect.Field import java.util.* @@ -23,8 +24,7 @@ class Maimai2( override val userDataRepo: Mai2UserDataRepo, val userGeneralDataRepository: Mai2UserGeneralDataRepo, val repos: Mai2Repos -): GameApiController("mai2") -{ +): GameApiController("mai2") { override suspend fun trend(@RP username: Str): List = us.cardByName(username) { card -> findTrend(playlogRepo.findByUserCardExtId(card.extId) .map { TrendLog(it.playDate, it.afterRating) }) @@ -32,6 +32,11 @@ class Maimai2( // Only show > S rank override val shownRanks = mai2Scores.filter { it.first >= 97 * 10000 } + override val settableFields: Map Unit> = mapOf( + "name" to { u, v -> u.userName = v + if (!v.all { it in USERNAME_CHARS }) { 400 - "Invalid character in username" } + }, + ) override suspend fun userSummary(@RP username: Str) = us.cardByName(username) { card -> val extra = userGeneralDataRepository.findByUser_Card_ExtId(card.extId) diff --git a/src/main/java/icu/samnyan/aqua/net/games/Models.kt b/src/main/java/icu/samnyan/aqua/net/games/Models.kt index dfe40174..fd6f2134 100644 --- a/src/main/java/icu/samnyan/aqua/net/games/Models.kt +++ b/src/main/java/icu/samnyan/aqua/net/games/Models.kt @@ -3,8 +3,6 @@ package icu.samnyan.aqua.net.games import ext.* import icu.samnyan.aqua.net.db.AquaUserServices import icu.samnyan.aqua.net.utils.* -import icu.samnyan.aqua.sega.chusan.model.userdata.UserData -import icu.samnyan.aqua.sega.chusan.model.userdata.UserPlaylog import icu.samnyan.aqua.sega.general.model.Card import kotlinx.serialization.Serializable import org.springframework.data.domain.Page @@ -116,16 +114,17 @@ interface GenericPlaylogRepo : JpaRepository { fun findByUserCardExtId(extId: Long, page: Pageable): Page } -abstract class GameApiController(name: String) { +abstract class GameApiController(name: String) { val musicMapping = resJson>("/meta/$name/music.json") ?.mapKeys { it.key.toInt() } ?: emptyMap() val itemMapping = resJson>>("/meta/$name/items.json") ?: emptyMap() abstract val us: AquaUserServices - abstract val userDataRepo: GenericUserDataRepo<*> + abstract val userDataRepo: GenericUserDataRepo abstract val playlogRepo: GenericPlaylogRepo<*> abstract val shownRanks: List> + abstract val settableFields: Map Unit> @API("trend") abstract suspend fun trend(@RP username: String): List @@ -165,4 +164,65 @@ abstract class GameApiController(name: String) { @API("playlog") fun playlog(@RP id: Long): IGenericGamePlaylog = playlogRepo.findById(id).getOrNull() ?: (404 - "Playlog not found") + + @API("user-setting") + suspend fun userSetting(@RP username: String, @RP field: String, @RP value: String): Any { + val prop = settableFields[field] ?: (400 - "Invalid field $field") + + return us.cardByName(username) { card -> + val user = userDataRepo.findByCard(card) ?: (404 - "User not found") + prop(user, value) + userDataRepo.save(user) + SUCCESS + } + } + + fun genericUserSummary(card: Card, ratingComp: Map): GenericGameSummary { + // Summary values: total plays, player rating, server-wide ranking + // number of each rank, max combo, number of full combo, number of all perfect + val user = userDataRepo.findByCard(card) ?: (404 - "Game data not found") + val plays = playlogRepo.findByUserCardExtId(card.extId) + + // Detailed ranks: Find the number of each rank in each level category + // map> + val rankMap = shownRanks.associate { (_, v) -> v to 0 } + val detailedRanks = HashMap>() + plays.forEach { play -> + val lvl = musicMapping[play.musicId]?.notes?.getOrNull(if (play.level == 10) 0 else play.level)?.lv ?: return@forEach + shownRanks.find { (s, _) -> play.achievement > s }?.let { (_, v) -> + val ranks = detailedRanks.getOrPut(lvl.toInt()) { rankMap.toMutableMap() } + ranks[v] = ranks[v]!! + 1 + } + } + + // Collapse detailed ranks to get non-detailed ranks map + val ranks = shownRanks.associate { (_, v) -> v to 0 }.toMutableMap().also { ranks -> + plays.forEach { play -> + shownRanks.find { (s, _) -> play.achievement > s }?.let { (_, v) -> ranks[v] = ranks[v]!! + 1 } + } + } + + return GenericGameSummary( + name = user.userName, + iconId = user.iconId, + aquaUser = card.aquaUser?.publicFields, + serverRank = userDataRepo.getRanking(user.playerRating), + accuracy = plays.acc(), + rating = user.playerRating, + ratingHighest = user.highestRating, + ranks = ranks.map { (k, v) -> RankCount(k, v) }, + detailedRanks = detailedRanks, + maxCombo = plays.maxOfOrNull { it.maxCombo } ?: 0, + fullCombo = plays.count { it.isFullCombo }, + allPerfect = plays.count { it.isAllPerfect }, + totalScore = user.totalScore, + plays = plays.size, + totalPlayTime = plays.count() * 3L, // TODO: Give a better estimate + joined = user.firstPlayDate.toString(), + lastSeen = user.lastPlayDate.toString(), + lastVersion = user.lastRomVersion, + ratingComposition = ratingComp, + recent = plays.sortedBy { it.userPlayDate.toString() }.takeLast(15).reversed() + ) + } } \ No newline at end of file diff --git a/src/main/java/icu/samnyan/aqua/net/games/Ongeki.kt b/src/main/java/icu/samnyan/aqua/net/games/Ongeki.kt index 1b132445..218c92f8 100644 --- a/src/main/java/icu/samnyan/aqua/net/games/Ongeki.kt +++ b/src/main/java/icu/samnyan/aqua/net/games/Ongeki.kt @@ -1,11 +1,13 @@ package icu.samnyan.aqua.net.games import ext.API +import ext.minus import icu.samnyan.aqua.net.db.AquaUserServices import icu.samnyan.aqua.net.utils.* import icu.samnyan.aqua.sega.ongeki.dao.userdata.UserDataRepository import icu.samnyan.aqua.sega.ongeki.dao.userdata.UserGeneralDataRepository import icu.samnyan.aqua.sega.ongeki.dao.userdata.UserPlaylogRepository +import icu.samnyan.aqua.sega.ongeki.model.userdata.UserData import org.springframework.web.bind.annotation.RestController @RestController @@ -15,13 +17,18 @@ class Ongeki( override val playlogRepo: UserPlaylogRepository, override val userDataRepo: UserDataRepository, val userGeneralDataRepository: UserGeneralDataRepository -): GameApiController("ongeki") { +): GameApiController("ongeki") { override suspend fun trend(username: String) = us.cardByName(username) { card -> findTrend(playlogRepo.findByUser_Card_ExtId(card.extId) .map { TrendLog(it.playDate, it.playerRating) }) } override val shownRanks = ongekiScores.filter { it.first >= 950000 } + override val settableFields: Map Unit> = mapOf( + "name" to { u, v -> u.setUserName(v) + if (!v.all { it in USERNAME_CHARS }) { 400 - "Invalid character in username" } + }, + ) override suspend fun userSummary(username: String) = us.cardByName(username) { card -> // val extra = userGeneralDataRepository.findByUser_Card_ExtId(u.ghostCard.extId)