diff --git a/src/main/java/ext/Ext.kt b/src/main/java/ext/Ext.kt index f80b91d5..b7ea7a6b 100644 --- a/src/main/java/ext/Ext.kt +++ b/src/main/java/ext/Ext.kt @@ -22,6 +22,9 @@ import java.nio.file.Path import java.time.LocalDate import java.time.LocalDateTime import java.time.format.DateTimeFormatter +import kotlin.reflect.KClass +import kotlin.reflect.KMutableProperty1 +import kotlin.reflect.full.memberProperties typealias RP = RequestParam typealias RB = RequestBody @@ -30,17 +33,22 @@ typealias API = RequestMapping typealias Str = String typealias Bool = Boolean -@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY) +@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY, AnnotationTarget.PROPERTY_GETTER) @Retention(AnnotationRetention.RUNTIME) annotation class Doc( val desc: String, val ret: String = "" ) -@Target(AnnotationTarget.VALUE_PARAMETER) +@Target(AnnotationTarget.PROPERTY) @Retention(AnnotationRetention.RUNTIME) annotation class SettingField(val name: Str, val desc: Str) +// Reflection +@Suppress("UNCHECKED_CAST") +fun KClass.vars() = memberProperties.mapNotNull { it as? KMutableProperty1 } + + // Make it easier to throw a ResponseStatusException operator fun HttpStatus.invoke(message: String? = null): Nothing = throw ApiException(value(), message ?: this.reasonPhrase) operator fun Int.minus(message: String): Nothing { diff --git a/src/main/java/icu/samnyan/aqua/net/SettingsApi.kt b/src/main/java/icu/samnyan/aqua/net/SettingsApi.kt new file mode 100644 index 00000000..2aea70c8 --- /dev/null +++ b/src/main/java/icu/samnyan/aqua/net/SettingsApi.kt @@ -0,0 +1,53 @@ +package icu.samnyan.aqua.net + +import ext.* +import icu.samnyan.aqua.net.db.AquaGameOptions +import icu.samnyan.aqua.net.db.AquaNetUserRepo +import icu.samnyan.aqua.net.db.AquaUserServices +import org.springframework.web.bind.annotation.RestController +import kotlin.reflect.full.findAnnotation +import kotlin.reflect.full.primaryConstructor + +@RestController +@API("/api/v2/settings") +class SettingsApi( + val us: AquaUserServices, + val userRepo: AquaNetUserRepo +) { + // Get all params with SettingField annotation + // Kotlin by default put all the @Annotations in the constructor param + val params = AquaGameOptions::class.primaryConstructor!!.parameters + .mapNotNull { it.findAnnotation()?.let { an -> it to an } } + val fields = AquaGameOptions::class.vars().associateBy { it.name } + val fieldMap = params.associate { (key, _) -> key.name to fields[key.name]!! } + val fieldDesc = params.map { (key, an) -> mapOf( + "key" to key.name, + "name" to an.name, + "desc" to an.desc + ) } + + @API("get") + @Doc("Get the game options of the logged in user") + fun getSettings(@RP token: String) = us.jwt.auth(token) { u -> + val go = u.gameOptions ?: AquaGameOptions() + fieldDesc.map { it + ("value" to fieldMap[it["key"]]!!.get(go)) } + } + + @API("set") + @Doc("Set a field in the game options") + fun setField(@RP token: String, @RP key: String, value: String) = us.jwt.auth(token) { u -> + val field = fieldMap[key] ?: error("Field not found") + val options = u.gameOptions ?: AquaGameOptions().also { + userRepo.save(u.apply { gameOptions = it }) + } + // Check field type + val type = field.returnType + val newValue = when { + type == String::class -> value + type == Int::class -> value.toInt() + type == Boolean::class -> value.toBoolean() + else -> error("Unsupported type") + } + field.set(options, newValue) + } +} \ No newline at end of file diff --git a/src/main/java/icu/samnyan/aqua/net/db/AquaGameOptions.kt b/src/main/java/icu/samnyan/aqua/net/db/AquaGameOptions.kt index bba24c16..0d370fe7 100644 --- a/src/main/java/icu/samnyan/aqua/net/db/AquaGameOptions.kt +++ b/src/main/java/icu/samnyan/aqua/net/db/AquaGameOptions.kt @@ -16,20 +16,20 @@ class AquaGameOptions( @SettingField("Unlock All Music", "Unlock all music and master difficulty in game") - val unlockMusic: Boolean = false, + var unlockMusic: Boolean = false, @SettingField("Unlock All Chara", "Unlock all characters and partners in game") - val unlockChara: Boolean = false, + var unlockChara: Boolean = false, @SettingField("Unlock All Collectables", "Unlock all collectables (nameplate, title, icon, frame) in game. " + "This setting is not relevant in chusan because in-game user box is disabled in chusan.") - val unlockCollectables: Boolean = false, + var unlockCollectables: Boolean = false, @SettingField("Unlock All Tickets" , "Unlock all map tickets (the game still limits which tickets can be used)") - val unlockTickets: Boolean = false, + var unlockTickets: Boolean = false, ) interface AquaGameOptionsRepository : JpaRepository \ No newline at end of file