4 Commits

Author SHA1 Message Date
Menci
d996fba291 [+] Fedy for AquaGameOptions 2025-12-13 10:21:42 +09:00
Menci
0cb2a95ff3 [F] Fix inheritance in KClass<T>.vars() 2025-12-13 10:21:42 +09:00
Menci
be5220fd51 [O] Use Iterable<T>.mapApply() 2025-12-13 10:21:42 +09:00
Menci
f23c0d6fe1 [RF] Re-organize game options 2025-12-13 10:21:42 +09:00
11 changed files with 153 additions and 85 deletions

View File

@@ -1,7 +1,6 @@
<script> <script>
import { fade } from "svelte/transition"; import { fade } from "svelte/transition";
import { FADE_IN, FADE_OUT } from "../../libs/config"; import { FADE_IN, FADE_OUT } from "../../libs/config";
import GameSettingFields from "./GameSettingFields.svelte";
import { t, ts } from "../../libs/i18n"; import { t, ts } from "../../libs/i18n";
import useLocalStorage from "../../libs/hooks/useLocalStorage.svelte"; import useLocalStorage from "../../libs/hooks/useLocalStorage.svelte";
import RegionSelector from "./RegionSelector.svelte"; import RegionSelector from "./RegionSelector.svelte";
@@ -13,7 +12,6 @@
<blockquote> <blockquote>
{ts("settings.gameNotice")} {ts("settings.gameNotice")}
</blockquote> </blockquote>
<GameSettingFields game="general"/>
<div class="field"> <div class="field">
<div class="bool"> <div class="bool">
<input id="rounding" type="checkbox" bind:checked={rounding.value}/> <input id="rounding" type="checkbox" bind:checked={rounding.value}/>

View File

@@ -149,18 +149,30 @@ export const EN_REF_SETTINGS = {
'settings.tabs.mai2': 'Mai', 'settings.tabs.mai2': 'Mai',
'settings.tabs.ongeki': 'Ongeki', 'settings.tabs.ongeki': 'Ongeki',
'settings.tabs.wacca': 'Wacca', 'settings.tabs.wacca': 'Wacca',
'settings.fields.unlockMusic.name': 'Unlock All Music', 'settings.fields.mai2UnlockMusic.name': 'Unlock All Music',
'settings.fields.unlockMusic.desc': 'Unlock all music and master difficulty in game.', 'settings.fields.mai2UnlockMusic.desc': 'Unlock all music and master difficulty.',
'settings.fields.unlockChara.name': 'Unlock All Characters', 'settings.fields.mai2UnlockChara.name': 'Unlock All Characters',
'settings.fields.unlockChara.desc': 'Unlock all characters, voices, and partners in game.', 'settings.fields.mai2UnlockChara.desc': 'Unlock all characters (new characters start at level 1).',
'settings.fields.unlockCollectables.name': 'Unlock All Collectables', 'settings.fields.mai2UnlockCharaMaxLevel.name': 'Max Character Level',
'settings.fields.unlockCollectables.desc': 'Unlock all collectables (nameplate, title, icon, frame) in game.', 'settings.fields.mai2UnlockCharaMaxLevel.desc': 'Set all characters to max level.',
'settings.fields.unlockTickets.name': 'Unlock All Tickets', 'settings.fields.mai2UnlockPartners.name': 'Unlock All Partners',
'settings.fields.unlockTickets.desc': 'Infinite map/ex tickets (Note: maimai still limits which tickets can be used).', 'settings.fields.mai2UnlockPartners.desc': 'Unlock all partners.',
'settings.fields.waccaInfiniteWp.name': 'Wacca: Infinite WP', 'settings.fields.mai2UnlockCollectables.name': 'Unlock All Collectables',
'settings.fields.waccaInfiniteWp.desc': 'Set WP to 999999', 'settings.fields.mai2UnlockCollectables.desc': 'Unlock all collectables (nameplate, title, icon, frame).',
'settings.fields.waccaAlwaysVip.name': 'Wacca: Always VIP', 'settings.fields.mai2UnlockTickets.name': 'Unlock All Tickets',
'settings.fields.waccaAlwaysVip.desc': 'Set VIP expiration date to 2077-01-01', 'settings.fields.mai2UnlockTickets.desc': 'Infinite tickets (Note: client still limits which tickets can be used).',
'settings.fields.waccaUnlockMusic.name': 'Unlock All Music',
'settings.fields.waccaUnlockMusic.desc': 'Unlock all music.',
'settings.fields.waccaUnlockPlates.name': 'Unlock All Plates',
'settings.fields.waccaUnlockPlates.desc': 'Unlock all plates.',
'settings.fields.waccaUnlockCollectables.name': 'Unlock All Collectables',
'settings.fields.waccaUnlockCollectables.desc': 'Unlock all collectables (icon, trophy).',
'settings.fields.waccaUnlockTickets.name': 'Infinite Tickets',
'settings.fields.waccaUnlockTickets.desc': 'Infinite tickets.',
'settings.fields.waccaInfiniteWp.name': 'Infinite WP',
'settings.fields.waccaInfiniteWp.desc': 'Set WP to 999999.',
'settings.fields.waccaAlwaysVip.name': 'Always VIP',
'settings.fields.waccaAlwaysVip.desc': 'Set VIP expiration date to 2077-01-01.',
'settings.fields.chusanTeamName.name': 'Team Name', 'settings.fields.chusanTeamName.name': 'Team Name',
'settings.fields.chusanTeamName.desc': 'Customize the text displayed on the top of your profile.', 'settings.fields.chusanTeamName.desc': 'Customize the text displayed on the top of your profile.',
'settings.fields.chusanInfinitePenguins.name': 'Infinite Penguins', 'settings.fields.chusanInfinitePenguins.name': 'Infinite Penguins',

View File

@@ -161,18 +161,30 @@ const zhSettings: typeof EN_REF_SETTINGS = {
'settings.tabs.mai2': '舞萌', 'settings.tabs.mai2': '舞萌',
'settings.tabs.ongeki': '音击', 'settings.tabs.ongeki': '音击',
'settings.tabs.wacca': '华卡', 'settings.tabs.wacca': '华卡',
'settings.fields.unlockMusic.name': '解锁谱面', 'settings.fields.mai2UnlockMusic.name': '解锁谱面',
'settings.fields.unlockMusic.desc': '在游戏中解锁所有曲目和大师难度谱面。', 'settings.fields.mai2UnlockMusic.desc': '解锁所有曲目和大师难度谱面。',
'settings.fields.unlockChara.name': '解锁角色', 'settings.fields.mai2UnlockChara.name': '解锁角色',
'settings.fields.unlockChara.desc': '在游戏中解锁所有角色、语音和伙伴。', 'settings.fields.mai2UnlockChara.desc': '解锁所有角色(新角色从 1 级开始)。',
'settings.fields.unlockCollectables.name': '解锁收藏品', 'settings.fields.mai2UnlockCharaMaxLevel.name': '角色满级',
'settings.fields.unlockCollectables.desc': '在游戏中解锁所有收藏品(名牌、称号、图标、背景图)。', 'settings.fields.mai2UnlockCharaMaxLevel.desc': '将所有角色设置为满级。',
'settings.fields.unlockTickets.name': '解锁游戏券', 'settings.fields.mai2UnlockPartners.name': '解锁搭档',
'settings.fields.unlockTickets.desc': '无限跑图券/解锁券maimai 客户端仍限制一些券不能使用)。', 'settings.fields.mai2UnlockPartners.desc': '解锁所有搭档。',
'settings.fields.waccaInfiniteWp.name': '华卡:无限 WP', 'settings.fields.mai2UnlockCollectables.name': '解锁收藏品',
'settings.fields.waccaInfiniteWp.desc': '将 WP 设置为 999999', 'settings.fields.mai2UnlockCollectables.desc': '解锁所有收藏品(姓名框、称号、头像、背景)。',
'settings.fields.waccaAlwaysVip.name': '华卡:永久会员', 'settings.fields.mai2UnlockTickets.name': '解锁功能票',
'settings.fields.waccaAlwaysVip.desc': '将 VIP 到期时间设置为 2077-01-01', 'settings.fields.mai2UnlockTickets.desc': '无限功能票(注:客户端仍限制一些功能票不能使用)。',
'settings.fields.waccaUnlockMusic.name': '解锁谱面',
'settings.fields.waccaUnlockMusic.desc': '解锁所有曲目。',
'settings.fields.waccaUnlockPlates.name': '解锁铭牌',
'settings.fields.waccaUnlockPlates.desc': '解锁所有铭牌。',
'settings.fields.waccaUnlockCollectables.name': '解锁收藏品',
'settings.fields.waccaUnlockCollectables.desc': '解锁所有收藏品。',
'settings.fields.waccaUnlockTickets.name': '无限解锁券',
'settings.fields.waccaUnlockTickets.desc': '无限解锁券。',
'settings.fields.waccaInfiniteWp.name': '无限 WP',
'settings.fields.waccaInfiniteWp.desc': '将 WP 设置为 999999。',
'settings.fields.waccaAlwaysVip.name': '永久会员',
'settings.fields.waccaAlwaysVip.desc': '将 VIP 到期时间设置为 2077-01-01。',
'settings.fields.chusanTeamName.name': '队伍名称', 'settings.fields.chusanTeamName.name': '队伍名称',
'settings.fields.chusanTeamName.desc': '自定义显示在个人资料顶部的文本。', 'settings.fields.chusanTeamName.desc': '自定义显示在个人资料顶部的文本。',
'settings.fields.chusanInfinitePenguins.name': '我是桐谷遥', 'settings.fields.chusanInfinitePenguins.name': '我是桐谷遥',

View File

@@ -35,8 +35,10 @@ import java.util.concurrent.locks.Lock
import kotlin.reflect.KCallable import kotlin.reflect.KCallable
import kotlin.reflect.KClass import kotlin.reflect.KClass
import kotlin.reflect.KMutableProperty1 import kotlin.reflect.KMutableProperty1
import kotlin.reflect.full.declaredMemberProperties
import kotlin.reflect.full.isSubclassOf import kotlin.reflect.full.isSubclassOf
import kotlin.reflect.full.memberProperties import kotlin.reflect.full.memberProperties
import kotlin.reflect.jvm.javaField
import kotlin.reflect.jvm.jvmErasure import kotlin.reflect.jvm.jvmErasure
typealias RP = RequestParam typealias RP = RequestParam
@@ -81,7 +83,9 @@ annotation class SettingField(
// Reflection // Reflection
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
fun <T : Any> KClass<T>.vars() = memberProperties.mapNotNull { it as? Var<T, Any> } fun <T : Any> KClass<T>.ownVars() = declaredMemberProperties.sortedBy { it.javaField?.declaringClass?.declaredFields?.indexOf(it.javaField) ?: Int.MAX_VALUE }.mapNotNull { it as? Var<T, Any> }
@Suppress("UNCHECKED_CAST")
fun <T : Any> KClass<T>.vars(): List<Var<T, Any>> = supertypes.mapNotNull { it.classifier as? KClass<*> }.filter { !it.java.isInterface }.flatMap{ it.vars() as List<Var<T, Any>> } + ownVars()
fun <T : Any> KClass<T>.varsMap() = vars().associateBy { it.name } fun <T : Any> KClass<T>.varsMap() = vars().associateBy { it.name }
fun <T : Any> KClass<T>.getters() = java.methods.filter { it.name.startsWith("get") } fun <T : Any> KClass<T>.getters() = java.methods.filter { it.name.startsWith("get") }
fun <T : Any> KClass<T>.gettersMap() = getters().associateBy { it.name.removePrefix("get").firstCharLower() } fun <T : Any> KClass<T>.gettersMap() = getters().associateBy { it.name.removePrefix("get").firstCharLower() }

View File

@@ -8,6 +8,7 @@ import org.springframework.web.bind.annotation.RestController
import java.security.MessageDigest import java.security.MessageDigest
import icu.samnyan.aqua.net.utils.SUCCESS import icu.samnyan.aqua.net.utils.SUCCESS
import icu.samnyan.aqua.net.components.JWT import icu.samnyan.aqua.net.components.JWT
import icu.samnyan.aqua.net.db.AquaGameOptions
import icu.samnyan.aqua.net.db.AquaNetUser import icu.samnyan.aqua.net.db.AquaNetUser
import icu.samnyan.aqua.net.db.AquaUserServices import icu.samnyan.aqua.net.db.AquaUserServices
import icu.samnyan.aqua.net.games.mai2.Mai2Import import icu.samnyan.aqua.net.games.mai2.Mai2Import
@@ -47,7 +48,7 @@ data class UserProfilePicture(val url: Str, val updatedAtMs: Long)
data class UserBasicInfo( data class UserBasicInfo(
val auId: Long, val ghostExtId: Long, val registrationTimeMs: Long, val auId: Long, val ghostExtId: Long, val registrationTimeMs: Long,
val username: Str, val displayName: Str, val email: Str, val passwordHash: Str, val profileBio: Str, val username: Str, val displayName: Str, val email: Str, val passwordHash: Str, val profileBio: Str,
val profilePicture: UserProfilePicture?, val profilePicture: UserProfilePicture?, val gameOptions: Map<Str, Any?>?,
) )
private data class UserUpdatedEvent(val user: UserBasicInfo, val isNewlyCreated: Bool) private data class UserUpdatedEvent(val user: UserBasicInfo, val isNewlyCreated: Bool)
@@ -131,7 +132,7 @@ class Fedy(
} caught { UserRegisterRes(error = it) } } caught { UserRegisterRes(error = it) }
} }
data class UserUpdateReq(val auId: Long, val fields: Map<Str, Str?>?) data class UserUpdateReq(val auId: Long, val fields: Map<Str, Str?>?, val gameOptions: Map<Str, Any?>?)
data class UserUpdateRes(val error: FedyErr? = null, val user: UserBasicInfo? = null) data class UserUpdateRes(val error: FedyErr? = null, val user: UserBasicInfo? = null)
@API("/user/update") @API("/user/update")
fun handleUserUpdate(@RH(KEY_HEADER) key: Str, @RT(REQ_PART) req: UserUpdateReq, @RT(PFP_PART) pfpFile: MultipartFile?): UserUpdateRes = handleFedy(key) { fun handleUserUpdate(@RH(KEY_HEADER) key: Str, @RT(REQ_PART) req: UserUpdateReq, @RT(PFP_PART) pfpFile: MultipartFile?): UserUpdateRes = handleFedy(key) {
@@ -142,12 +143,16 @@ class Fedy(
if (k == "email") { ru.email = us.validateEmail(v) } if (k == "email") { ru.email = us.validateEmail(v) }
else us.update(ru, k, v) else us.update(ru, k, v)
} }
pfpFile?.run { pfpFile?.apply {
val mime = TIKA.detect(pfpFile.bytes).takeIf { it.startsWith("image/") } ?: (400 - "Invalid file type") val mime = TIKA.detect(pfpFile.bytes).takeIf { it.startsWith("image/") } ?: (400 - "Invalid file type")
val name = "${ru.auId}${MIMES.forName(mime)?.extension ?: ".jpg"}" val name = "${ru.auId}${MIMES.forName(mime)?.extension ?: ".jpg"}"
(paths.aquaNetPortrait.path() / name).writeBytes(bytes) (paths.aquaNetPortrait.path() / name).writeBytes(bytes)
ru.profilePicture = name ru.profilePicture = name
} }
req.gameOptions?.apply {
val options = ru.gameOptions ?: AquaGameOptions().also { ru.gameOptions = it }
forEach { (k, v) -> v?.let { GAME_OPTIONS_FIELDS[k]?.set(options, it) } }
}
us.userRepo.save(ru) us.userRepo.save(ru)
if (fields.containsKey("pwHash") ?: false) { us.clearAllSessions(ru) } if (fields.containsKey("pwHash") ?: false) { us.clearAllSessions(ru) }
UserUpdateRes(user = ru.fedyBasicInfo()) UserUpdateRes(user = ru.fedyBasicInfo())
@@ -162,7 +167,8 @@ class Fedy(
?.let { UserProfilePicture( ?.let { UserProfilePicture(
url = "/uploads/net/portrait/${profilePicture}", url = "/uploads/net/portrait/${profilePicture}",
updatedAtMs = it.getLastModifiedTime().toMillis() updatedAtMs = it.getLastModifiedTime().toMillis()
) } ) },
gameOptions?.let { o -> GAME_OPTIONS_FIELDS.mapValues { it.value.get(o) } }
) )
data class DataPullReq(val extId: Long, val game: Str, val createdAtMs: Long, val updatedAtMs: Long, val exportOptions: ExportOptions) data class DataPullReq(val extId: Long, val game: Str, val createdAtMs: Long, val updatedAtMs: Long, val exportOptions: ExportOptions)
@@ -286,17 +292,16 @@ class Fedy(
log.info("Fedy /card/unlink : Unlinked card ${card.id} (${card.luid}) from user ${cu.auId} (${cu.username})") log.info("Fedy /card/unlink : Unlinked card ${card.id} (${card.luid}) from user ${cu.auId} (${cu.username})")
} }
fun onUserUpdated(u: AquaNetUser, isNew: Bool = false) = maybeNotifyAsync(FedyEvent(userUpdated = UserUpdatedEvent(u.fedyBasicInfo(), isNew))) fun onUserUpdated(u: AquaNetUser, isNew: Bool = false) = maybeNotifyAsync { FedyEvent(userUpdated = UserUpdatedEvent(u.fedyBasicInfo(), isNew)) }
fun onCardCreated(luid: Str, extId: Long) = maybeNotifyAsync(FedyEvent(cardCreated = CardCreatedEvent(luid, extId))) fun onCardCreated(luid: Str, extId: Long) = maybeNotifyAsync { FedyEvent(cardCreated = CardCreatedEvent(luid, extId)) }
fun onCardLinked(luid: Str, oldExtId: Long?, ghostExtId: Long, migratedGames: List<Str>) = maybeNotifyAsync(FedyEvent(cardLinked = CardLinkedEvent(luid, oldExtId, ghostExtId, migratedGames))) fun onCardLinked(luid: Str, oldExtId: Long?, ghostExtId: Long, migratedGames: List<Str>) = maybeNotifyAsync { FedyEvent(cardLinked = CardLinkedEvent(luid, oldExtId, ghostExtId, migratedGames)) }
fun onCardUnlinked(luid: Str) = maybeNotifyAsync(FedyEvent(cardUnlinked = CardUnlinkedEvent(luid))) fun onCardUnlinked(luid: Str) = maybeNotifyAsync { FedyEvent(cardUnlinked = CardUnlinkedEvent(luid)) }
fun onDataUpdated(extId: Long, game: Str, removeOldData: Bool) = maybeNotifyAsync({ fun onDataUpdated(extId: Long, game: Str, removeOldData: Bool) = maybeNotifyAsync {
val card = cardRepo.findByExtId(extId).orElse(null) ?: return@maybeNotifyAsync null // Card not found, nothing to do val card = cardRepo.findByExtId(extId).orElse(null) ?: return@maybeNotifyAsync null // Card not found, nothing to do
FedyEvent(dataUpdated = DataUpdatedEvent(extId, card.isGhost, game, removeOldData)) FedyEvent(dataUpdated = DataUpdatedEvent(extId, card.isGhost, game, removeOldData))
}) }
private fun maybeNotifyAsync(event: FedyEvent) = maybeNotifyAsync({ event }) private fun maybeNotifyAsync(getEvent: () -> FedyEvent?) = if (!props.enabled || suppressEvents.get()) {} else CompletableFuture.runAsync {
private fun maybeNotifyAsync(getEvent: () -> FedyEvent?) = if (!props.enabled && !suppressEvents.get()) {} else CompletableFuture.runAsync {
var event: FedyEvent? = null var event: FedyEvent? = null
try { try {
event = getEvent() event = getEvent()
@@ -355,6 +360,12 @@ class Fedy(
const val KEY_HEADER = "X-Fedy-Key" const val KEY_HEADER = "X-Fedy-Key"
const val REQ_PART = "request" const val REQ_PART = "request"
const val PFP_PART = "profilePicture" const val PFP_PART = "profilePicture"
@Suppress("UNCHECKED_CAST")
val GAME_OPTIONS_FIELDS = listOf(
O::mai2UnlockMusic, O::mai2UnlockChara, O::mai2UnlockCharaMaxLevel, O::mai2UnlockPartners, O::mai2UnlockCollectables, O::mai2UnlockTickets
).map { it as Var<O, Any?> }.associateBy { it.name }
val log = logger() val log = logger()
} }
} }
typealias O = AquaGameOptions

View File

@@ -14,7 +14,8 @@ import kotlin.reflect.jvm.jvmErasure
class SettingsApi( class SettingsApi(
val us: AquaUserServices, val us: AquaUserServices,
val userRepo: AquaNetUserRepo, val userRepo: AquaNetUserRepo,
val goRepo: AquaGameOptionsRepo val goRepo: AquaGameOptionsRepo,
val fedy: Fedy
) { ) {
// Get all params with SettingField annotation // Get all params with SettingField annotation
val fields = AquaGameOptions::class.vars() val fields = AquaGameOptions::class.vars()
@@ -41,6 +42,6 @@ class SettingsApi(
} }
// Check field type // Check field type
field.setCast(options, value) field.setCast(options, value)
goRepo.save(options) goRepo.save(options).also { fedy.onUserUpdated(u) }
} }
} }

View File

@@ -2,10 +2,7 @@ package icu.samnyan.aqua.net.db
import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonIgnore
import ext.SettingField import ext.SettingField
import jakarta.persistence.Entity import jakarta.persistence.*
import jakarta.persistence.GeneratedValue
import jakarta.persistence.GenerationType
import jakarta.persistence.Id
import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.JpaRepository
@Entity @Entity
@@ -14,21 +11,29 @@ class AquaGameOptions(
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Long = 0, var id: Long = 0,
@SettingField("general") @SettingField("mai2") @Column(name = "mai2_unlock_music")
var unlockMusic: Boolean = false, var mai2UnlockMusic: Boolean = false,
@SettingField("mai2") @Column(name = "mai2_unlock_chara")
@SettingField("general") var mai2UnlockChara: Boolean = false,
var unlockChara: Boolean = false, @SettingField("mai2") @Column(name = "mai2_unlock_chara_max_level")
var mai2UnlockCharaMaxLevel: Boolean = false,
@SettingField("general") @SettingField("mai2") @Column(name = "mai2_unlock_partners")
var unlockCollectables: Boolean = false, var mai2UnlockPartners: Boolean = false,
@SettingField("mai2") @Column(name = "mai2_unlock_collectables")
@SettingField("general") var mai2UnlockCollectables: Boolean = false,
var unlockTickets: Boolean = false, @SettingField("mai2") @Column(name = "mai2_unlock_tickets")
var mai2UnlockTickets: Boolean = false,
@SettingField("wacca")
var waccaUnlockMusic: Boolean = false,
@SettingField("wacca")
var waccaUnlockPlates: Boolean = false,
@SettingField("wacca")
var waccaUnlockCollectables: Boolean = false,
@SettingField("wacca")
var waccaUnlockTickets: Boolean = false,
@SettingField("wacca") @SettingField("wacca")
var waccaInfiniteWp: Boolean = false, var waccaInfiniteWp: Boolean = false,
@SettingField("wacca") @SettingField("wacca")
var waccaAlwaysVip: Boolean = false, var waccaAlwaysVip: Boolean = false,

View File

@@ -1,11 +1,14 @@
package icu.samnyan.aqua.sega.maimai2.handler package icu.samnyan.aqua.sega.maimai2.handler
import ext.int
import ext.logger import ext.logger
import ext.mapApply
import icu.samnyan.aqua.net.games.mai2.Maimai2 import icu.samnyan.aqua.net.games.mai2.Maimai2
import icu.samnyan.aqua.sega.general.BaseHandler import icu.samnyan.aqua.sega.general.BaseHandler
import icu.samnyan.aqua.sega.general.dao.CardRepository import icu.samnyan.aqua.sega.general.dao.CardRepository
import icu.samnyan.aqua.sega.maimai2.model.Mai2Repos import icu.samnyan.aqua.sega.maimai2.model.Mai2Repos
import icu.samnyan.aqua.sega.maimai2.model.userdata.Mai2ItemKind import icu.samnyan.aqua.sega.maimai2.model.userdata.Mai2ItemKind
import icu.samnyan.aqua.sega.maimai2.model.userdata.Mai2UserCharacter
import org.springframework.stereotype.Component import org.springframework.stereotype.Component
import kotlin.jvm.optionals.getOrNull import kotlin.jvm.optionals.getOrNull
@@ -15,34 +18,25 @@ class GetUserCharacterHandler(
val maimai2: Maimai2, val maimai2: Maimai2,
val cardRepo: CardRepository, val cardRepo: CardRepository,
) : BaseHandler { ) : BaseHandler {
val itemUnlock = maimai2.itemMapping[Mai2ItemKind.chara.name]?.map { mapOf( val charaIds = maimai2.itemMapping[Mai2ItemKind.chara.name]?.map { it.key.int() } ?: emptyList()
"characterId" to it.key,
"level" to 9999,
"awakening" to 1,
"useCount" to 0
) }
init { init {
if (itemUnlock.isNullOrEmpty()) logger.warn("Mai2 item info is empty") if (charaIds.isEmpty()) logger.warn("Mai2 item info is empty")
} }
override fun handle(request: Map<String, Any>): Any { override fun handle(request: Map<String, Any>): Any {
val userId = (request["userId"] as Number).toLong() val userId = (request["userId"] as Number).toLong()
val gameOptions = cardRepo.findByExtId(userId).getOrNull()?.aquaUser?.gameOptions
// Aqua Net game unlock feature val userCharacterList = repos.userCharacter.findByUser_Card_ExtId(userId)
cardRepo.findByExtId(userId).getOrNull()?.aquaUser?.gameOptions?.let { opt -> .let { if (gameOptions?.mai2UnlockChara != true) it else (
if (!opt.unlockChara or itemUnlock.isNullOrEmpty()) return@let charaIds.associateWith { Mai2UserCharacter().apply { characterId = it; level = 1 } } +
it.associateBy { it.characterId }
logger.info("Response: ${itemUnlock!!.size} Characters - All unlock") ).values }
return mapOf( .let { if (gameOptions?.mai2UnlockCharaMaxLevel != true) it else it.mapApply { level = 9999 } }
"userId" to userId,
"userCharacterList" to itemUnlock
)
}
return mapOf( return mapOf(
"userId" to userId, "userId" to userId,
"userCharacterList" to repos.userCharacter.findByUser_Card_ExtId(userId) "userCharacterList" to userCharacterList
) )
} }

View File

@@ -50,10 +50,11 @@ class GetUserItemHandler(
// Aqua Net game unlock feature // Aqua Net game unlock feature
cardRepo.findByExtId(userId).getOrNull()?.aquaUser?.gameOptions?.let { opt -> cardRepo.findByExtId(userId).getOrNull()?.aquaUser?.gameOptions?.let { opt ->
val items = when { val items = when {
(kind in 5..8) && opt.unlockMusic -> musicUnlock.getValue(kind) (kind in 5..8) && opt.mai2UnlockMusic -> musicUnlock.getValue(kind)
(kind in 1..3 || kind == 11) && opt.unlockCollectables -> itemUnlock[kind] (kind in 1..3 || kind == 11) && opt.mai2UnlockCollectables -> itemUnlock[kind]
(kind == 12) && opt.unlockTickets -> itemUnlock[kind] (kind == 12) && opt.mai2UnlockTickets -> itemUnlock[kind]
(kind in 9..10) && opt.unlockChara -> itemUnlock[kind] (kind == 9) && opt.mai2UnlockChara -> itemUnlock[kind]
(kind == 10) && opt.mai2UnlockPartners -> itemUnlock[kind]
else -> emptyList() else -> emptyList()
} }

View File

@@ -205,19 +205,19 @@ fun WaccaServer.init() {
val go = u.card?.aquaUser?.gameOptions ?: AquaGameOptions() val go = u.card?.aquaUser?.gameOptions ?: AquaGameOptions()
// All unlock // All unlock
if (go.unlockMusic && wacca.musicMapping.isNotEmpty()) { if (go.waccaUnlockMusic && wacca.musicMapping.isNotEmpty()) {
items[MUSIC_UNLOCK()] = wacca.musicMapping.map { (id, v) -> MUSIC_UNLOCK(u, id, p1 = v.notes.size.long() - 1) } items[MUSIC_UNLOCK()] = wacca.musicMapping.map { (id, v) -> MUSIC_UNLOCK(u, id, p1 = v.notes.size.long() - 1) }
} }
if (go.unlockTickets) { if (go.waccaUnlockTickets) {
var i = 0 var i = 0
items[TICKET()] = enabledTickets.flatMap { (1..5).map { TICKET(u, it).apply { id = (i++).toLong() } } } items[TICKET()] = enabledTickets.flatMap { (1..5).map { TICKET(u, it).apply { id = (i++).toLong() } } }
} }
if (go.unlockChara) { if (go.waccaUnlockPlates) {
wacca.itemMapping["plates"]?.let { items[USER_PLATE()] = it.map { (k, _) -> USER_PLATE(u, k.int()) } } wacca.itemMapping["plates"]?.let { items[USER_PLATE()] = it.map { (k, _) -> USER_PLATE(u, k.int()) } }
} }
if (go.unlockCollectables) { if (go.waccaUnlockCollectables) {
// TODO: Add titles // TODO: Add titles
mapOf("icon" to ICON, "plates" to USER_PLATE, "trophy" to TROPHY).map { (name, type) -> mapOf("icon" to ICON, "trophy" to TROPHY).map { (name, type) ->
wacca.itemMapping[name]?.let { items[type()] = it.map { (k, _) -> type(u, k.int()) } } wacca.itemMapping[name]?.let { items[type()] = it.map { (k, _) -> type(u, k.int()) } }
} }
} }

View File

@@ -0,0 +1,30 @@
-- Add new unlock columns
ALTER TABLE aqua_game_options ADD COLUMN mai2_unlock_music BIT NOT NULL DEFAULT 0;
ALTER TABLE aqua_game_options ADD COLUMN mai2_unlock_chara BIT NOT NULL DEFAULT 0;
ALTER TABLE aqua_game_options ADD COLUMN mai2_unlock_chara_max_level BIT NOT NULL DEFAULT 0;
ALTER TABLE aqua_game_options ADD COLUMN mai2_unlock_partners BIT NOT NULL DEFAULT 0;
ALTER TABLE aqua_game_options ADD COLUMN mai2_unlock_collectables BIT NOT NULL DEFAULT 0;
ALTER TABLE aqua_game_options ADD COLUMN mai2_unlock_tickets BIT NOT NULL DEFAULT 0;
ALTER TABLE aqua_game_options ADD COLUMN wacca_unlock_music BIT NOT NULL DEFAULT 0;
ALTER TABLE aqua_game_options ADD COLUMN wacca_unlock_plates BIT NOT NULL DEFAULT 0;
ALTER TABLE aqua_game_options ADD COLUMN wacca_unlock_collectables BIT NOT NULL DEFAULT 0;
ALTER TABLE aqua_game_options ADD COLUMN wacca_unlock_tickets BIT NOT NULL DEFAULT 0;
-- Migrate data
UPDATE aqua_game_options SET
mai2_unlock_music = unlock_music,
mai2_unlock_chara = unlock_chara,
mai2_unlock_chara_max_level = unlock_chara,
mai2_unlock_partners = unlock_chara,
mai2_unlock_collectables = unlock_collectables,
mai2_unlock_tickets = unlock_tickets,
wacca_unlock_music = unlock_music,
wacca_unlock_plates = unlock_chara | unlock_collectables,
wacca_unlock_collectables = unlock_collectables,
wacca_unlock_tickets = unlock_tickets;
-- Drop old columns
ALTER TABLE aqua_game_options DROP COLUMN unlock_music;
ALTER TABLE aqua_game_options DROP COLUMN unlock_chara;
ALTER TABLE aqua_game_options DROP COLUMN unlock_collectables;
ALTER TABLE aqua_game_options DROP COLUMN unlock_tickets;