diff --git a/comet-core/src/main/kotlin/ren/natsuyuk1/comet/commands/ProjectSekaiCommand.kt b/comet-core/src/main/kotlin/ren/natsuyuk1/comet/commands/ProjectSekaiCommand.kt index 8e6bd6780..cd8c96e77 100644 --- a/comet-core/src/main/kotlin/ren/natsuyuk1/comet/commands/ProjectSekaiCommand.kt +++ b/comet-core/src/main/kotlin/ren/natsuyuk1/comet/commands/ProjectSekaiCommand.kt @@ -3,9 +3,7 @@ package ren.natsuyuk1.comet.commands import moe.sdl.yac.core.subcommands import moe.sdl.yac.parameters.arguments.argument import moe.sdl.yac.parameters.arguments.default -import moe.sdl.yac.parameters.options.flag import moe.sdl.yac.parameters.options.option -import moe.sdl.yac.parameters.types.int import moe.sdl.yac.parameters.types.long import ren.natsuyuk1.comet.api.Comet import ren.natsuyuk1.comet.api.command.* @@ -14,21 +12,16 @@ import ren.natsuyuk1.comet.api.message.asImage import ren.natsuyuk1.comet.api.message.buildMessageWrapper import ren.natsuyuk1.comet.api.user.CometUser import ren.natsuyuk1.comet.commands.service.ProjectSekaiService -import ren.natsuyuk1.comet.consts.cometClient -import ren.natsuyuk1.comet.network.thirdparty.projectsekai.ProjectSekaiAPI.getCheerPredictData import ren.natsuyuk1.comet.network.thirdparty.projectsekai.objects.MusicDifficulty -import ren.natsuyuk1.comet.network.thirdparty.projectsekai.objects.kit33.toMessageWrapper import ren.natsuyuk1.comet.network.thirdparty.projectsekai.toMessageWrapper import ren.natsuyuk1.comet.objects.config.FeatureConfig import ren.natsuyuk1.comet.objects.pjsk.ProjectSekaiUserData -import ren.natsuyuk1.comet.objects.pjsk.local.PJSKProfileMusic import ren.natsuyuk1.comet.objects.pjsk.local.ProjectSekaiMusic import ren.natsuyuk1.comet.service.image.ProjectSekaiImageService import ren.natsuyuk1.comet.util.pjsk.pjskFolder import ren.natsuyuk1.comet.util.toMessageWrapper import ren.natsuyuk1.comet.utils.file.isBlank import ren.natsuyuk1.comet.utils.file.isType -import ren.natsuyuk1.comet.utils.math.NumberUtil.formatDigests import java.io.File val PROJECTSEKAI by lazy { @@ -37,7 +30,6 @@ val PROJECTSEKAI by lazy { listOf("pjsk", "啤酒烧烤"), "查询 Project Sekai: Colorful Stage 相关信息", " /pjsk bind -i [账号 ID] - 绑定账号\n" + - "/pjsk event (排名) 查询当前活动信息\n" + "/pjsk pred 查询当前活动结束预测分数\n" + "/pjsk info 查询账号信息\n" + "/pjsk chart 查询歌曲谱面\n" + @@ -57,10 +49,7 @@ class ProjectSekaiCommand( if (FeatureConfig.data.projectSekaiSetting.enable) { subcommands( Bind(subject, sender, user), - Event(subject, sender, user), - Prediction(subject, sender, user), Info(subject, sender, user), - Best30(subject, sender, user), Chart(subject, sender, user), Music(subject, sender, user), ) @@ -74,11 +63,7 @@ class ProjectSekaiCommand( } if (currentContext.invokedSubcommand == null) { - if (ProjectSekaiUserData.isBound(user.id.value)) { - subject.sendMessage(ProjectSekaiService.queryUserEventInfo(user, 0)) - } else { - subject.sendMessage(property.helpText.toMessageWrapper()) - } + subject.sendMessage(property.helpText.toMessageWrapper()) } } @@ -144,75 +129,6 @@ class ProjectSekaiCommand( } } - class Event( - override val subject: PlatformCommandSender, - override val sender: PlatformCommandSender, - override val user: CometUser, - ) : CometSubCommand(subject, sender, user, EVENT) { - - companion object { - val EVENT = SubCommandProperty( - "event", - listOf("活动排名", "活排"), - PROJECTSEKAI, - ) - } - - private val position by argument("排名位置", "欲查询的指定排名").int().default(0) - - override suspend fun run() { - subject.sendMessage(ProjectSekaiService.queryUserEventInfo(user, position)) - } - } - - class Prediction( - override val subject: PlatformCommandSender, - override val sender: PlatformCommandSender, - override val user: CometUser, - ) : CometSubCommand(subject, sender, user, PREDICTION) { - - companion object { - val PREDICTION = SubCommandProperty( - "pred", - listOf("prediction", "预测", "预测线"), - PROJECTSEKAI, - ) - } - - private val event by option("-e", "--event").flag(default = false) - - override suspend fun run() { - if (event) { - cometClient.getCheerPredictData().toMessageWrapper().takeIf { - !it.isEmpty() - }?.let { - subject.sendMessage(it) - } - } else { - subject.sendMessage(ProjectSekaiService.fetchPrediction()) - } - } - } - - class Best30( - override val subject: PlatformCommandSender, - override val sender: PlatformCommandSender, - override val user: CometUser, - ) : CometSubCommand(subject, sender, user, BEST30) { - - companion object { - val BEST30 = SubCommandProperty( - "best30", - listOf("b30"), - PROJECTSEKAI, - ) - } - - override suspend fun run() { - subject.sendMessage(ProjectSekaiService.b30(user)) - } - } - class Chart( override val subject: PlatformCommandSender, override val sender: PlatformCommandSender, @@ -249,8 +165,6 @@ class ProjectSekaiCommand( FeatureConfig.data.projectSekaiSetting.minSimilarity, ) - val extraInfo = musicInfo?.id?.let { PJSKProfileMusic.getMusicInfo(it) } - if (musicInfo == null) { subject.sendMessage("找不到你想要搜索的歌曲哦".toMessageWrapper()) return @@ -273,18 +187,6 @@ class ProjectSekaiCommand( subject.sendMessage( buildMessageWrapper { appendTextln("搜索准确度: $sim") - if (extraInfo != null) { - val bpmText = if (!extraInfo.bpms.isNullOrEmpty()) { - buildString { - extraInfo.bpms.forEach { bi -> - append("${bi.bpm.formatDigests(0)} - ") - } - }.removeSuffix(" - ") - } else { - extraInfo.bpm.formatDigests(0) - } - appendTextln("BPM: $bpmText") - } appendElement(chartFile.asImage()) }, ) diff --git a/comet-core/src/main/kotlin/ren/natsuyuk1/comet/commands/service/ProjectSekaiService.kt b/comet-core/src/main/kotlin/ren/natsuyuk1/comet/commands/service/ProjectSekaiService.kt index 62902b176..9d4a94821 100644 --- a/comet-core/src/main/kotlin/ren/natsuyuk1/comet/commands/service/ProjectSekaiService.kt +++ b/comet-core/src/main/kotlin/ren/natsuyuk1/comet/commands/service/ProjectSekaiService.kt @@ -3,24 +3,17 @@ package ren.natsuyuk1.comet.commands.service import ren.natsuyuk1.comet.api.message.MessageWrapper import ren.natsuyuk1.comet.api.user.CometUser import ren.natsuyuk1.comet.consts.cometClient -import ren.natsuyuk1.comet.network.thirdparty.projectsekai.ProjectSekaiAPI.getRankSeasonInfo -import ren.natsuyuk1.comet.network.thirdparty.projectsekai.ProjectSekaiAPI.getSpecificRankInfo -import ren.natsuyuk1.comet.network.thirdparty.projectsekai.ProjectSekaiAPI.getUserEventInfo import ren.natsuyuk1.comet.network.thirdparty.projectsekai.ProjectSekaiAPI.getUserInfo -import ren.natsuyuk1.comet.network.thirdparty.projectsekai.objects.SekaiEventStatus -import ren.natsuyuk1.comet.network.thirdparty.projectsekai.objects.kit33.toMessageWrapper import ren.natsuyuk1.comet.network.thirdparty.projectsekai.objects.toMessageWrapper -import ren.natsuyuk1.comet.network.thirdparty.projectsekai.toMessageWrapper -import ren.natsuyuk1.comet.service.ProjectSekaiManager -import ren.natsuyuk1.comet.service.image.ProjectSekaiImageService.drawEventInfo import ren.natsuyuk1.comet.util.toMessageWrapper -import ren.natsuyuk1.comet.utils.skiko.SkikoHelper -import ren.natsuyuk1.comet.objects.pjsk.ProjectSekaiData as pjskData import ren.natsuyuk1.comet.objects.pjsk.ProjectSekaiUserData as pjskUserData -import ren.natsuyuk1.comet.service.ProjectSekaiManager as pjskHelper object ProjectSekaiService { fun bindAccount(user: CometUser, userID: Long): MessageWrapper { + if (pjskUserData.hasUserID(userID)) { + return "该账号已被绑定,如果是你的账号被错误绑定请联系管理员".toMessageWrapper() + } + return if (pjskUserData.isBound(user.id.value)) { pjskUserData.updateID(user.id.value, userID) "已更改你绑定的账号 ID!".toMessageWrapper() @@ -30,75 +23,10 @@ object ProjectSekaiService { } } - suspend fun queryUserEventInfo(user: CometUser, position: Int): MessageWrapper { - val userData = pjskUserData.getUserPJSKData(user.id.value) - val currentEventId = pjskData.getEventId() ?: return "获取当前活动信息失败, 请稍后再试".toMessageWrapper() - - if (position == 0 && userData == null) { - return "你还没有绑定过世界计划账号, 使用 /pjsk bind -i [你的ID] 绑定".toMessageWrapper() - } - - return when (pjskHelper.getCurrentEventStatus()) { - SekaiEventStatus.ONGOING, SekaiEventStatus.END -> { - if (position == 0 && userData != null) { - val cur = cometClient.getUserEventInfo(currentEventId, userData.userID) - if (SkikoHelper.isSkikoLoaded()) { - cur.drawEventInfo(currentEventId, userData) - } else { - cur.toMessageWrapper(userData, currentEventId) - } - } else { - val cur = cometClient.getSpecificRankInfo(currentEventId, position) - - if (SkikoHelper.isSkikoLoaded()) { - cur.drawEventInfo(currentEventId) - } else { - cur.toMessageWrapper(null, currentEventId) - } - } - } - - SekaiEventStatus.COUNTING -> { - "活动数据统计中, 请耐心等待~".toMessageWrapper() - } - - else -> { - "获取当前活动信息失败, 请稍后再试".toMessageWrapper() - } - } - } - - fun fetchPrediction(): MessageWrapper { - val pred = pjskData.getCurrentPredictionInfo() - val predUpdateTime = pjskData.getPredictionInfoTime() - - if (pred == null || predUpdateTime == null) { - return "活动预测线信息暂未获取, 稍等片刻哦~".toMessageWrapper() - } - - return pred.toMessageWrapper(predUpdateTime) - } - suspend fun queryUserInfo(userID: Long): MessageWrapper { - val rankSeason = ProjectSekaiManager.getLatestRankSeason() ?: return "查询排位数据时出现异常".toMessageWrapper() - - val rankInfo = cometClient.getRankSeasonInfo(userID, rankSeason) - return cometClient.getUserInfo(userID).toMessageWrapper().apply { appendLine() appendLine() - appendText(rankInfo.getRankInfo()) } } - - suspend fun b30(user: CometUser): MessageWrapper { - if (!SkikoHelper.isSkikoLoaded()) { - return "Comet 的图像生成库还没加载, 生成不了图片捏".toMessageWrapper() - } - - val userData = pjskUserData.getUserPJSKData(user.id.value) - ?: return "你还没有绑定过世界计划账号, 使用 /pjsk bind -i [你的ID] 绑定".toMessageWrapper() - - return cometClient.getUserInfo(userData.userID).generateBest30() - } } diff --git a/comet-core/src/main/kotlin/ren/natsuyuk1/comet/consts/PersistStuff.kt b/comet-core/src/main/kotlin/ren/natsuyuk1/comet/consts/PersistStuff.kt index 3e3182ac2..9999f9d74 100644 --- a/comet-core/src/main/kotlin/ren/natsuyuk1/comet/consts/PersistStuff.kt +++ b/comet-core/src/main/kotlin/ren/natsuyuk1/comet/consts/PersistStuff.kt @@ -63,8 +63,6 @@ val defaultCommands: List> = PROJECTSEKAI, listOf( ProjectSekaiCommand.Bind.BIND, - ProjectSekaiCommand.Event.EVENT, - ProjectSekaiCommand.Prediction.PREDICTION, ProjectSekaiCommand.Info.INFO, ProjectSekaiCommand.Chart.CHART, ProjectSekaiCommand.Music.MUSIC, diff --git a/comet-core/src/main/kotlin/ren/natsuyuk1/comet/network/thirdparty/projectsekai/ProjectSekaiAPI.kt b/comet-core/src/main/kotlin/ren/natsuyuk1/comet/network/thirdparty/projectsekai/ProjectSekaiAPI.kt index 6674c3de5..65193eb53 100644 --- a/comet-core/src/main/kotlin/ren/natsuyuk1/comet/network/thirdparty/projectsekai/ProjectSekaiAPI.kt +++ b/comet-core/src/main/kotlin/ren/natsuyuk1/comet/network/thirdparty/projectsekai/ProjectSekaiAPI.kt @@ -20,9 +20,6 @@ import ren.natsuyuk1.comet.network.CometClient import ren.natsuyuk1.comet.network.thirdparty.projectsekai.objects.ProjectSekaiEventList import ren.natsuyuk1.comet.network.thirdparty.projectsekai.objects.ProjectSekaiRankSeasonInfo import ren.natsuyuk1.comet.network.thirdparty.projectsekai.objects.ProjectSekaiUserInfo -import ren.natsuyuk1.comet.network.thirdparty.projectsekai.objects.SekaiProfileEventInfo -import ren.natsuyuk1.comet.network.thirdparty.projectsekai.objects.kit33.PJSKCheerfulPreditionInfo -import ren.natsuyuk1.comet.network.thirdparty.projectsekai.objects.kit33.PJSKEventPredictionInfo import ren.natsuyuk1.comet.utils.json.serializeTo private val logger = mu.KotlinLogging.logger {} @@ -48,12 +45,6 @@ object ProjectSekaiAPI { */ private const val PJSEKAI_URL = "https://api.pjsek.ai/database/master" - /** - * 33 Kit - * https://3-3.dev/ - */ - private const val THREE3KIT_URL = "https://33.dsml.hk/pred" - private suspend fun CometClient.profileRequest( param: String, builder: URLBuilder.(URLBuilder) -> Unit = {}, @@ -79,34 +70,6 @@ object ProjectSekaiAPI { } } - suspend fun CometClient.getUserEventInfo(eventID: Int, userID: Long): SekaiProfileEventInfo { - logger.debug { "Fetching project sekai event $eventID rank for user $userID" } - - val resp = profileRequest("/api/user/%7Buser_id%7D/event/$eventID/ranking") { - parameters.append("targetUserId", userID.toString()) - } - - if (resp.status != HttpStatusCode.OK) { - error("API return code isn't OK (${resp.status}), raw request url: ${resp.call.request.url}") - } - - return json.decodeFromString(resp.bodyAsText().also { logger.debug { "Raw content: $it" } }) - } - - suspend fun CometClient.getSpecificRankInfo(eventID: Int, rankPosition: Int): SekaiProfileEventInfo { - logger.debug { "Fetching project sekai event $eventID rank position at $rankPosition" } - - val resp = profileRequest("/api/user/%7Buser_id%7D/event/$eventID/ranking") { - parameters.append("targetRank", rankPosition.toString()) - } - - if (resp.status != HttpStatusCode.OK) { - error("API return code isn't OK (${resp.status}), raw request url: ${resp.call.request.url}") - } - - return json.decodeFromString(resp.bodyAsText().also { logger.debug { "Raw content: $it" } }) - } - suspend fun CometClient.getEventList(limit: Int = 12, startAt: Int = -1, skip: Int = 0): ProjectSekaiEventList { logger.debug { "Fetching project sekai event list" } @@ -121,12 +84,6 @@ object ProjectSekaiAPI { }.bodyAsText().serializeTo(json) } - suspend fun CometClient.getRankPredictionInfo(): PJSKEventPredictionInfo { - logger.debug { "Fetching project sekai rank prediction info" } - - return client.get(THREE3KIT_URL).bodyAsText().serializeTo(json) - } - suspend fun CometClient.getUserInfo(id: Long): ProjectSekaiUserInfo { logger.debug { "Fetching project sekai user info for $id" } @@ -152,16 +109,4 @@ object ProjectSekaiAPI { return json.decodeFromString(resp.bodyAsText().also { logger.debug { "Raw content: $it" } }) } - - suspend fun CometClient.getCheerPredictData(): PJSKCheerfulPreditionInfo { - logger.debug { "Fetching project sekai cheerful event predict info" } - - val resp = client.get("https://33.dsml.hk/cheer-pred") - - if (resp.status != HttpStatusCode.OK) { - error("API return code isn't OK (${resp.status}), raw request url: ${resp.call.request.url}") - } - - return json.decodeFromString(resp.bodyAsText().also { logger.debug { "Raw content: $it" } }) - } } diff --git a/comet-core/src/main/kotlin/ren/natsuyuk1/comet/network/thirdparty/projectsekai/ProjectSekaiHelper.kt b/comet-core/src/main/kotlin/ren/natsuyuk1/comet/network/thirdparty/projectsekai/ProjectSekaiHelper.kt index 43bc43c08..9243c9a56 100644 --- a/comet-core/src/main/kotlin/ren/natsuyuk1/comet/network/thirdparty/projectsekai/ProjectSekaiHelper.kt +++ b/comet-core/src/main/kotlin/ren/natsuyuk1/comet/network/thirdparty/projectsekai/ProjectSekaiHelper.kt @@ -10,186 +10,18 @@ package ren.natsuyuk1.comet.network.thirdparty.projectsekai import kotlinx.datetime.Clock -import ren.natsuyuk1.comet.api.message.MessageWrapper import ren.natsuyuk1.comet.api.message.asImage import ren.natsuyuk1.comet.api.message.buildMessageWrapper -import ren.natsuyuk1.comet.consts.cometClient -import ren.natsuyuk1.comet.network.thirdparty.projectsekai.ProjectSekaiAPI.getSpecificRankInfo +import ren.natsuyuk1.comet.network.thirdparty.projectsekai.objects.MusicDifficulty import ren.natsuyuk1.comet.network.thirdparty.projectsekai.objects.ProjectSekaiMusicInfo -import ren.natsuyuk1.comet.network.thirdparty.projectsekai.objects.SekaiEventStatus -import ren.natsuyuk1.comet.network.thirdparty.projectsekai.objects.SekaiProfileEventInfo -import ren.natsuyuk1.comet.objects.pjsk.ProjectSekaiData -import ren.natsuyuk1.comet.objects.pjsk.ProjectSekaiUserData -import ren.natsuyuk1.comet.objects.pjsk.local.PJSKProfileMusic -import ren.natsuyuk1.comet.objects.pjsk.local.ProjectSekaiI18N import ren.natsuyuk1.comet.objects.pjsk.local.ProjectSekaiMusic import ren.natsuyuk1.comet.objects.pjsk.local.ProjectSekaiMusicDifficulty -import ren.natsuyuk1.comet.service.ProjectSekaiManager -import ren.natsuyuk1.comet.util.toMessageWrapper import ren.natsuyuk1.comet.utils.datetime.format -import ren.natsuyuk1.comet.utils.datetime.toFriendly -import ren.natsuyuk1.comet.utils.math.NumberUtil.getBetterNumber import ren.natsuyuk1.comet.utils.math.NumberUtil.toInstant import ren.natsuyuk1.comet.utils.time.yyMMddWithTimeZonePattern -import java.util.concurrent.TimeUnit -import kotlin.math.absoluteValue -import kotlin.time.Duration.Companion.seconds - -private val rankPosition = - listOf( - 1, - 2, - 3, - 4, - 5, - 10, - 50, - 100, - 200, - 500, - 1000, - 2000, - 3000, - 4000, - 5000, - 10000, - 20000, - 50000, - 100000, - ) - -internal suspend fun SekaiProfileEventInfo.toMessageWrapper( - userData: ProjectSekaiUserData?, - eventId: Int, -): MessageWrapper { - if (rankings.isEmpty()) { - return "你还没打这期活动捏".toMessageWrapper() - } - - val now = Clock.System.now() - val profile = rankings.first() - val (ahead, behind) = profile.rank.getSurroundingRank() - val eventInfo = ProjectSekaiData.getCurrentEventInfo() ?: return "查询失败, 活动信息未加载".toMessageWrapper() - val eventStatus = ProjectSekaiManager.getCurrentEventStatus() - - return buildMessageWrapper { - appendTextln("${profile.name} - ${profile.userId}") - appendLine() - appendTextln("当前活动 ${eventInfo.name}") - if (eventStatus == SekaiEventStatus.ONGOING) { - appendTextln( - "离活动结束还有 ${ - (eventInfo.aggregateTime.toInstant(true) - now) - .toFriendly(TimeUnit.SECONDS) - }", - ) - } - - if (profile.userCheerfulCarnival.cheerfulCarnivalTeamId != null) { - val teamName = - ProjectSekaiI18N.getCarnivalTeamName(profile.userCheerfulCarnival.cheerfulCarnivalTeamId) - - if (teamName != null) { - appendTextln("当前队伍为 $teamName") - appendLine() - } - } - - appendTextln("分数 ${profile.score} | 排名 ${profile.rank}") - appendLine() - - if (userData != null) { - if (userData.lastQueryScore != 0L && userData.lastQueryPosition != 0) { - val scoreDiff = profile.score - userData.lastQueryScore - val rankDiff = userData.lastQueryPosition - profile.rank - - if (scoreDiff != 0L) { - appendText("+ $scoreDiff 分") - } - - if (rankDiff != 0) { - appendText( - (if (profile.rank < userData.lastQueryPosition) " ↑ 上升" else " ↓ 下降") + - " ${rankDiff.absoluteValue} 名", - ) - } - - appendLine() - } - - // Refresh user pjsk score and rank - userData.updateInfo(profile.score, profile.rank) - } - - appendLine() - - if (ahead != 0) { - val aheadEventStatus = cometClient.getSpecificRankInfo(eventId, ahead) - val aheadScore = aheadEventStatus.getScore() - - if (aheadScore != -1L) { - val aheadScoreStr = aheadScore.getBetterNumber() - val delta = (aheadScore - profile.score).getBetterNumber() - appendTextln("上一档排名 $ahead 的分数为 $aheadScoreStr, 相差 $delta") - } else { - appendTextln("上一档排名 $ahead 暂无数据") - } - } - - if (behind in 200..100000) { - val behindEventStatus = cometClient.getSpecificRankInfo(eventId, behind) - val behindScore = behindEventStatus.getScore() - - if (behindScore != -1L) { - val targetScore = behindScore.getBetterNumber() - val deltaScore = (profile.score - behindScore).getBetterNumber() - appendTextln("下一档排名 $behind 的分数为 $targetScore, 相差 $deltaScore") - } else { - appendTextln("下一档排名 $behind 暂无数据") - } - } - - appendLine() - - appendText("数据来自 PJSK Profile | Unibot API | 33Kit") - } -} - -internal fun Int.getSurroundingRank(): Pair { - if (this <= rankPosition.first()) { - return Pair(0, rankPosition.first()) - } - - var before: Int - var after: Int - - for (i in rankPosition.indices) { - if (i == rankPosition.size - 1) { - break - } - - before = rankPosition[i] - after = rankPosition[i + 1] - - if (this in before + 1..after) { - if (before == this && i != 0) { - before = rankPosition[i - 1] - } else if (after == this && i + 1 != rankPosition.size - 1) { - after = rankPosition[i + 2] - } - - return Pair(before, after) - } - } - - return Pair(rankPosition.last(), 1000001) -} - internal suspend fun ProjectSekaiMusicInfo.toMessageWrapper() = buildMessageWrapper { val musicInfo = this@toMessageWrapper - val diff = ProjectSekaiMusicDifficulty.getMusicDifficulty(musicInfo.id) - val extraInfo = PJSKProfileMusic.getMusicInfo(musicInfo.id) if (musicInfo.publishedAt.toInstant(true) > Clock.System.now()) { appendTextln("⚠ 该内容为未公开剧透内容") @@ -202,21 +34,13 @@ internal suspend fun ProjectSekaiMusicInfo.toMessageWrapper() = appendTextln("作词 ${musicInfo.lyricist}") appendTextln("作曲 ${musicInfo.composer}") appendTextln("编曲 ${musicInfo.arranger}") - if (extraInfo != null) { - appendTextln("时长 ${extraInfo.duration.seconds.toFriendly()}") - } appendTextln("上线时间 ${musicInfo.publishedAt.toInstant(true).format(yyMMddWithTimeZonePattern)}") appendLine() appendTextln("难度信息 >") - if (diff.isEmpty()) { - appendText("最新歌曲暂无难度信息") - } else { - diff.forEach { - appendTextln( - "${it.musicDifficulty}[${it.playLevel}] | ${it.totalNoteCount}", - ) - } + MusicDifficulty.values().forEach { + val diff = ProjectSekaiMusicDifficulty.getDifficulty(id, it) ?: return@forEach + appendTextln("$it[${diff.playLevel}] | ${diff.totalNoteCount}") } trim() diff --git a/comet-core/src/main/kotlin/ren/natsuyuk1/comet/network/thirdparty/projectsekai/objects/PJSKMusicDifficultyInfo.kt b/comet-core/src/main/kotlin/ren/natsuyuk1/comet/network/thirdparty/projectsekai/objects/PJSKMusicDifficultyInfo.kt index d549af6a1..ae20b1342 100644 --- a/comet-core/src/main/kotlin/ren/natsuyuk1/comet/network/thirdparty/projectsekai/objects/PJSKMusicDifficultyInfo.kt +++ b/comet-core/src/main/kotlin/ren/natsuyuk1/comet/network/thirdparty/projectsekai/objects/PJSKMusicDifficultyInfo.kt @@ -3,9 +3,7 @@ package ren.natsuyuk1.comet.network.thirdparty.projectsekai.objects import kotlinx.serialization.Serializable /** - * Project Sekai Profile Music Data - * - * 有删减 + * Project Sekai Music Data */ @Serializable data class PJSKMusicDifficultyInfo( @@ -14,7 +12,4 @@ data class PJSKMusicDifficultyInfo( val musicDifficulty: MusicDifficulty, val playLevel: Int, val totalNoteCount: Int, - val playLevelAdjust: Double = 0.0, - val fullComboAdjust: Double = 0.0, - val fullPerfectAdjust: Double = 0.0, ) diff --git a/comet-core/src/main/kotlin/ren/natsuyuk1/comet/network/thirdparty/projectsekai/objects/ProjectSekaiUserInfo.kt b/comet-core/src/main/kotlin/ren/natsuyuk1/comet/network/thirdparty/projectsekai/objects/ProjectSekaiUserInfo.kt index 2bec03e26..73b15586c 100644 --- a/comet-core/src/main/kotlin/ren/natsuyuk1/comet/network/thirdparty/projectsekai/objects/ProjectSekaiUserInfo.kt +++ b/comet-core/src/main/kotlin/ren/natsuyuk1/comet/network/thirdparty/projectsekai/objects/ProjectSekaiUserInfo.kt @@ -5,48 +5,6 @@ import kotlinx.serialization.Serializable import ren.natsuyuk1.comet.api.message.MessageWrapper import ren.natsuyuk1.comet.api.message.buildMessageWrapper import ren.natsuyuk1.comet.objects.pjsk.local.ProjectSekaiMusic -import ren.natsuyuk1.comet.objects.pjsk.local.ProjectSekaiMusicDifficulty -import ren.natsuyuk1.comet.service.ProjectSekaiManager -import ren.natsuyuk1.comet.service.image.ProjectSekaiImageService -import ren.natsuyuk1.comet.util.toMessageWrapper - -fun ProjectSekaiUserInfo.toMessageWrapper(): MessageWrapper = buildMessageWrapper { - appendTextln("${user.userGameData.name} | ${user.userGameData.level} 级") - appendLine() - if (userProfile.bio.isNotBlank()) { - appendTextln(userProfile.bio) - appendLine() - } - appendTextln("歌曲游玩情况 >>") - - appendText( - "EXPERT | Clear ${ - getSpecificLevelMusicCount( - MusicDifficulty.EXPERT, - MusicPlayResult.CLEAR, - ) - }/${ProjectSekaiMusic.musicDatabase.size} / FC ${ - getSpecificLevelMusicCount( - MusicDifficulty.EXPERT, - MusicPlayResult.FULL_COMBO, - ) - } / AP ${getSpecificLevelMusicCount(MusicDifficulty.EXPERT, MusicPlayResult.ALL_PERFECT)}", - ) - appendLine() - appendText( - "MASTER | Clear ${ - getSpecificLevelMusicCount( - MusicDifficulty.MASTER, - MusicPlayResult.CLEAR, - ) - }/${ProjectSekaiMusic.musicDatabase.size} / FC ${ - getSpecificLevelMusicCount( - MusicDifficulty.MASTER, - MusicPlayResult.FULL_COMBO, - ) - } / AP ${getSpecificLevelMusicCount(MusicDifficulty.MASTER, MusicPlayResult.ALL_PERFECT)}", - ) -} /** * [ProjectSekaiUserInfo] @@ -55,72 +13,34 @@ fun ProjectSekaiUserInfo.toMessageWrapper(): MessageWrapper = buildMessageWrappe */ @Serializable data class ProjectSekaiUserInfo( + // 当前综合力数据 + val totalPower: TotalPower, + // 用户信息 val user: UserGameData, val userProfile: UserProfile, - val userDecks: List, + // 用户编队信息 + val userDeck: UserDeck, + // 用户卡面信息 val userCards: List, - val userMusics: List, - val userMusicResults: List, + // 用户当前佩戴徽章信息 + val userHonors: List, + @SerialName("userMusicDifficultyClearCount") val userMusicDifficultyClearInfo: List, ) { - fun getBest30Songs(): List = userMusicResults.filter { - it.musicDifficulty >= MusicDifficulty.EXPERT && (it.isAllPerfect || it.isFullCombo) - }.sortedBy { - ProjectSekaiManager.getSongAdjustedLevel(it.musicId, it.musicDifficulty, it.playResult) - }.asReversed().distinctBy { - it.musicId - }.take(30) - - suspend fun generateBest30(): MessageWrapper { - if (ProjectSekaiMusicDifficulty.musicDiffDatabase.isEmpty() || ProjectSekaiMusic.musicDatabase.isEmpty()) { - return "Project Sekai 歌曲数据还没有加载好噢".toMessageWrapper() - } - - return buildMessageWrapper { - appendElement( - ProjectSekaiImageService.drawBest30(this@ProjectSekaiUserInfo, getBest30Songs()), - ) - } - } - - fun getSpecificLevelMusicCount(difficulty: MusicDifficulty, playResult: MusicPlayResult): Int { - var counter = 0 - - userMusics.forEach { - counter += it.musicStatus.count { ms -> - ms.userMusicResult.any { umr -> - umr.musicDifficulty == difficulty && umr.playResult >= playResult - } - } - } - - return counter - } - @Serializable - data class MusicResult( - val userId: Long, - val musicId: Int, - val musicDifficulty: MusicDifficulty, - val playType: String, - val playResult: MusicPlayResult, - val highScore: Int, - @SerialName("fullComboFlg") val isFullCombo: Boolean, - @SerialName("fullPerfectFlg") val isAllPerfect: Boolean, - val mvpCount: Int, - val superStarCount: Int, + data class TotalPower( + val areaItemBonus: Int, + val basicCardTotalPower: Int, + val characterRankBonus: Int, + val honorBonus: Int, + val totalPower: Int, ) @Serializable data class UserGameData( - @SerialName("userGamedata") val userGameData: Data, - ) { - @Serializable - data class Data( - @SerialName("userId") val userID: Long, - val name: String, - @SerialName("rank") val level: Int, - ) - } + @SerialName("userId") val userID: Long, + val name: String, + @SerialName("rank") val level: Int, + ) @Serializable data class UserProfile( @@ -130,46 +50,45 @@ data class ProjectSekaiUserInfo( ) @Serializable - data class UserMusic( - @SerialName("userId") val userID: Long, - @SerialName("musicId") val musicID: Int, - @SerialName("userMusicDifficultyStatuses") val musicStatus: List, - ) { - @Serializable - data class MusicStatus( - @SerialName("musicId") val musicID: Int, - val musicDifficulty: String, - val musicDifficultyStatus: String, - @SerialName("userMusicResults") val userMusicResult: List, - ) { - @Serializable - data class UserMusicResult( - // easy, normal, hard, expert, master - val musicDifficulty: MusicDifficulty, - // clear, full_combo, all_perfect - val playResult: MusicPlayResult, - ) - } - } + data class MusicDifficultyClearInfo( + val fullCombo: Int, + val liveClear: Int, + val musicDifficultyType: MusicDifficulty, + ) @Serializable data class UserDeck( + // 队伍编号 + val deckId: Int, + // 领队 val leader: Int, + // 领队 + val member1: Int, + val member2: Int, + val member3: Int, + val member4: Int, + val member5: Int, + // 队伍名 + val name: String, + // 等价于 member2 val subLeader: Int, - @SerialName("member3") val third: Int, - @SerialName("member4") val fourth: Int, - @SerialName("member5") val fifth: Int, + val userId: Long, ) @Serializable data class UserCard( val cardId: Int, val defaultImage: String, - // val episodes val level: Int, val masterRank: Int, val specialTrainingStatus: String, ) + + @Serializable + data class UserHonor( + val honorId: Int, + val level: Int, + ) } /** @@ -195,19 +114,31 @@ enum class MusicDifficulty { MASTER, } -/** - * [MusicPlayResult] - * - * 代表玩家游玩过曲目的状态. - */ -@Serializable -enum class MusicPlayResult { - @SerialName("clear") - CLEAR, +fun ProjectSekaiUserInfo.toMessageWrapper(): MessageWrapper = buildMessageWrapper { + appendTextln("${user.name} | ${user.level} 级") + appendLine() + if (userProfile.bio.isNotBlank()) { + appendTextln(userProfile.bio) + appendLine() + } + appendTextln("歌曲游玩情况 >>") - @SerialName("full_combo") - FULL_COMBO, + val ex = userMusicDifficultyClearInfo.find { it.musicDifficultyType == MusicDifficulty.EXPERT } + val ma = userMusicDifficultyClearInfo.find { it.musicDifficultyType == MusicDifficulty.MASTER } - @SerialName("full_perfect") - ALL_PERFECT, + appendText( + "EXPERT | Clear ${ + ex?.liveClear + }/${ProjectSekaiMusic.musicDatabase.size} / FC ${ + ex?.fullCombo + }", + ) + appendLine() + appendText( + "MASTER | Clear ${ + ma?.liveClear + }/${ProjectSekaiMusic.musicDatabase.size} / FC ${ + ma?.fullCombo + }", + ) } diff --git a/comet-core/src/main/kotlin/ren/natsuyuk1/comet/objects/pjsk/ProjectSekaiData.kt b/comet-core/src/main/kotlin/ren/natsuyuk1/comet/objects/pjsk/ProjectSekaiData.kt index 5e4c39cf9..e7bb2602e 100644 --- a/comet-core/src/main/kotlin/ren/natsuyuk1/comet/objects/pjsk/ProjectSekaiData.kt +++ b/comet-core/src/main/kotlin/ren/natsuyuk1/comet/objects/pjsk/ProjectSekaiData.kt @@ -9,10 +9,6 @@ package ren.natsuyuk1.comet.objects.pjsk -import kotlinx.datetime.Clock -import kotlinx.datetime.Instant -import kotlinx.serialization.decodeFromString -import kotlinx.serialization.encodeToString import mu.KotlinLogging import org.jetbrains.exposed.dao.Entity import org.jetbrains.exposed.dao.EntityClass @@ -22,18 +18,13 @@ import org.jetbrains.exposed.dao.id.EntityID import org.jetbrains.exposed.dao.id.IdTable import org.jetbrains.exposed.sql.Column import org.jetbrains.exposed.sql.insert -import org.jetbrains.exposed.sql.kotlin.datetime.timestamp import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.update import ren.natsuyuk1.comet.consts.cometClient -import ren.natsuyuk1.comet.consts.json import ren.natsuyuk1.comet.network.thirdparty.projectsekai.ProjectSekaiAPI.getEventList -import ren.natsuyuk1.comet.network.thirdparty.projectsekai.ProjectSekaiAPI.getRankPredictionInfo -import ren.natsuyuk1.comet.network.thirdparty.projectsekai.objects.kit33.PJSKEventPredictionInfo import ren.natsuyuk1.comet.objects.pjsk.local.ProjectSekaiEvent import java.util.* -import kotlin.time.Duration.Companion.hours private val logger = KotlinLogging.logger {} @@ -44,8 +35,6 @@ object ProjectSekaiDataTable : IdTable("pjsk_data") { val aggregateTime: Column = long("aggregate_at") val endTime: Column = long("end_time") val name = text("name") - val eventPredictionData = text("event_prediction_data") - val eventPredictionUpdateTime = timestamp("event_prediction_update_time") val eventType = text("event_type").default("marathon") } @@ -53,8 +42,8 @@ class ProjectSekaiData(id: EntityID) : Entity(id) { companion object : EntityClass(ProjectSekaiDataTable) { private suspend fun initData() { kotlin.runCatching { - Pair(cometClient.getEventList(1).data.first(), cometClient.getRankPredictionInfo()) - }.onSuccess { (eventInfo, pred) -> + cometClient.getEventList(1).data.first() + }.onSuccess { eventInfo -> transaction { new(0) { currentEventID = eventInfo.id @@ -62,8 +51,6 @@ class ProjectSekaiData(id: EntityID) : Entity(id) { aggregateTime = eventInfo.aggregateAt endTime = eventInfo.closedAt name = eventInfo.name - eventPredictionData = json.encodeToString(pred) - eventPredictionUpdateTime = Clock.System.now() eventType = eventInfo.eventType } } @@ -115,52 +102,9 @@ class ProjectSekaiData(id: EntityID) : Entity(id) { } } - suspend fun updatePredictionData() { - var init = false - newSuspendedTransaction { - if (ProjectSekaiData.all().empty()) { - initData() - init = true - } - } - - if (init) return - - val pjskData = transaction { ProjectSekaiData.all().first() } - - val now = Clock.System.now() - - if ((now - pjskData.eventPredictionUpdateTime) >= 2.hours) { - kotlin.runCatching { - cometClient.getRankPredictionInfo() - }.onSuccess { pred -> - transaction { - pjskData.apply { - eventPredictionData = json.encodeToString(pred) - eventPredictionUpdateTime = now - } - } - }.onFailure { - logger.warn(it) { "获取 Project Sekai 活动积分预测信息失败, 可能是上游服务器异常" } - } - } - } - fun getCurrentEventInfo(): ProjectSekaiData? = transaction { ProjectSekaiData.all().firstOrNull() } fun getEventId(): Int? = transaction { ProjectSekaiData.all().firstOrNull()?.currentEventID } - - fun getCurrentPredictionInfo(): PJSKEventPredictionInfo? = - transaction { - getCurrentEventInfo()?.eventPredictionData?.let { - json.decodeFromString(it) - } - } - - fun getPredictionInfoTime(): Instant? = - transaction { - getCurrentEventInfo()?.eventPredictionUpdateTime - } } var currentEventID by ProjectSekaiDataTable.currentEventID @@ -168,8 +112,6 @@ class ProjectSekaiData(id: EntityID) : Entity(id) { var aggregateTime by ProjectSekaiDataTable.aggregateTime var endTime by ProjectSekaiDataTable.endTime var name by ProjectSekaiDataTable.name - var eventPredictionData by ProjectSekaiDataTable.eventPredictionData - var eventPredictionUpdateTime by ProjectSekaiDataTable.eventPredictionUpdateTime var eventType by ProjectSekaiDataTable.eventType } @@ -225,5 +167,9 @@ class ProjectSekaiUserData(id: EntityID) : UUIDEntity(id) { fun getUserPJSKData(uuid: UUID) = transaction { find { ProjectSekaiUserDataTable.id eq uuid }.firstOrNull() } + + fun hasUserID(userID: Long) = transaction { + !find { ProjectSekaiUserDataTable.userID eq userID }.empty() + } } } diff --git a/comet-core/src/main/kotlin/ren/natsuyuk1/comet/objects/pjsk/local/LocalFiles.kt b/comet-core/src/main/kotlin/ren/natsuyuk1/comet/objects/pjsk/local/LocalFiles.kt index 349270066..10807abe2 100644 --- a/comet-core/src/main/kotlin/ren/natsuyuk1/comet/objects/pjsk/local/LocalFiles.kt +++ b/comet-core/src/main/kotlin/ren/natsuyuk1/comet/objects/pjsk/local/LocalFiles.kt @@ -1,11 +1,9 @@ package ren.natsuyuk1.comet.objects.pjsk.local -internal val pjskLocal = mutableListOf( +internal val pjskLocal: List = listOf( ProjectSekaiCard, - PJSKProfileMusic, ProjectSekaiI18N, - ProjectSekaiMusicDifficulty, - ProjectSekaiRank, ProjectSekaiMusicAlias, ProjectSekaiMusic, + ProjectSekaiMusicDifficulty, ) diff --git a/comet-core/src/main/kotlin/ren/natsuyuk1/comet/objects/pjsk/local/PJSKProfileMusic.kt b/comet-core/src/main/kotlin/ren/natsuyuk1/comet/objects/pjsk/local/PJSKProfileMusic.kt deleted file mode 100644 index accf4dafd..000000000 --- a/comet-core/src/main/kotlin/ren/natsuyuk1/comet/objects/pjsk/local/PJSKProfileMusic.kt +++ /dev/null @@ -1,63 +0,0 @@ -package ren.natsuyuk1.comet.objects.pjsk.local - -import kotlinx.serialization.builtins.ListSerializer -import mu.KotlinLogging -import ren.natsuyuk1.comet.consts.cometClient -import ren.natsuyuk1.comet.consts.json -import ren.natsuyuk1.comet.network.thirdparty.projectsekai.objects.ProfileMusicInfo -import ren.natsuyuk1.comet.util.pjsk.pjskFolder -import ren.natsuyuk1.comet.utils.file.isBlank -import ren.natsuyuk1.comet.utils.file.readTextBuffered -import ren.natsuyuk1.comet.utils.file.touch -import ren.natsuyuk1.comet.utils.ktor.DownloadStatus -import ren.natsuyuk1.comet.utils.ktor.downloadFile -import kotlin.collections.set -import kotlin.time.Duration.Companion.days - -private val logger = KotlinLogging.logger {} - -object PJSKProfileMusic : ProjectSekaiLocalFile( - pjskFolder.resolve("musics_pjsekai.json"), - 2.days, -) { - private const val url = "https://musics.pjsekai.moe/musics.json" - - private val musicDatabase = mutableMapOf() - - override suspend fun load() { - musicDatabase.clear() - - try { - val content = file.readTextBuffered() - if (content.isBlank()) { - logger.warn { "加载 Project Sekai 歌曲数据失败, 文件为空" } - } else { - val music = json.decodeFromString( - ListSerializer(ProfileMusicInfo.serializer()), - content, - ) - - music.forEach { - musicDatabase[it.id] = it - } - } - } catch (e: Exception) { - logger.warn(e) { "解析 歌曲数据时出现问题" } - } - } - - override suspend fun update(): Boolean { - file.touch() - - if (file.isBlank() || isOutdated()) { - if (cometClient.client.downloadFile(url, file) == DownloadStatus.OK) { - logger.info { "成功更新音乐数据" } - return true - } - } - - return false - } - - fun getMusicInfo(id: Int): ProfileMusicInfo? = musicDatabase[id] -} diff --git a/comet-core/src/main/kotlin/ren/natsuyuk1/comet/objects/pjsk/local/ProjectSekaiMusicDifficulty.kt b/comet-core/src/main/kotlin/ren/natsuyuk1/comet/objects/pjsk/local/ProjectSekaiMusicDifficulty.kt index f1afa574d..23cd9efdd 100644 --- a/comet-core/src/main/kotlin/ren/natsuyuk1/comet/objects/pjsk/local/ProjectSekaiMusicDifficulty.kt +++ b/comet-core/src/main/kotlin/ren/natsuyuk1/comet/objects/pjsk/local/ProjectSekaiMusicDifficulty.kt @@ -1,12 +1,16 @@ package ren.natsuyuk1.comet.objects.pjsk.local -import kotlinx.serialization.builtins.ListSerializer +import kotlinx.datetime.Instant +import kotlinx.serialization.decodeFromString import mu.KotlinLogging import ren.natsuyuk1.comet.consts.cometClient import ren.natsuyuk1.comet.consts.json +import ren.natsuyuk1.comet.network.thirdparty.github.GitHubApi +import ren.natsuyuk1.comet.network.thirdparty.projectsekai.objects.MusicDifficulty import ren.natsuyuk1.comet.network.thirdparty.projectsekai.objects.PJSKMusicDifficultyInfo +import ren.natsuyuk1.comet.util.pjsk.getSekaiBestResourceURL import ren.natsuyuk1.comet.util.pjsk.pjskFolder -import ren.natsuyuk1.comet.utils.file.isBlank +import ren.natsuyuk1.comet.utils.file.lastModifiedTime import ren.natsuyuk1.comet.utils.file.readTextBuffered import ren.natsuyuk1.comet.utils.file.touch import ren.natsuyuk1.comet.utils.ktor.DownloadStatus @@ -17,42 +21,51 @@ private val logger = KotlinLogging.logger {} object ProjectSekaiMusicDifficulty : ProjectSekaiLocalFile( pjskFolder.resolve("musicDifficulties.json"), - 1.days, + 5.days, ) { - internal val musicDiffDatabase = mutableListOf() + private val musicDifficulties = mutableListOf() override suspend fun load() { - musicDiffDatabase.clear() - try { - musicDiffDatabase.addAll( - json.decodeFromString( - ListSerializer(PJSKMusicDifficultyInfo.serializer()), - file.readTextBuffered(), - ), - ) + val content = file.readTextBuffered() + if (content.isBlank()) { + logger.warn { "加载 Project Sekai 歌曲数据失败, 文件为空" } + } else { + musicDifficulties.clear() + musicDifficulties.addAll(json.decodeFromString(content)) + } } catch (e: Exception) { - logger.warn(e) { "解析 Project Sekai 音乐等级偏差值数据时出现问题" } + logger.warn(e) { "解析歌曲别名数据时出现问题" } } } override suspend fun update(): Boolean { file.touch() - if (file.isBlank() || isOutdated()) { - if (cometClient.client.downloadFile( - "https://gitlab.com/pjsekai/database/musics/-/raw/main/musicDifficulties.json", - file, - ) == DownloadStatus.OK - ) { - logger.info { "成功更新音乐等级偏差值数据" } + GitHubApi.getSpecificFileCommits("StarWishsama", "comet-resource-database", "projectsekai/music_title.json") + .onSuccess { + val commitTime = Instant.parse(it.first().commit.committer.date) + val lastModified = file.lastModifiedTime() - return true + file.touch() + + if (file.length() == 0L || commitTime > lastModified) { + if (cometClient.client.downloadFile( + getSekaiBestResourceURL(file.name), + file, + ) == DownloadStatus.OK + ) { + logger.info { "成功更新歌曲别名数据" } + return true + } + } + }.onFailure { + logger.warn(it) { "加载 Project Sekai 歌曲难度失败!" } } - } return false } - fun getMusicDifficulty(musicId: Int) = musicDiffDatabase.filter { it.musicId == musicId } + fun getDifficulty(musicId: Int, difficulty: MusicDifficulty) = + musicDifficulties.find { it.id == musicId && it.musicDifficulty == difficulty } } diff --git a/comet-core/src/main/kotlin/ren/natsuyuk1/comet/objects/pjsk/local/ProjectSekaiRank.kt b/comet-core/src/main/kotlin/ren/natsuyuk1/comet/objects/pjsk/local/ProjectSekaiRank.kt deleted file mode 100644 index ad7b524b7..000000000 --- a/comet-core/src/main/kotlin/ren/natsuyuk1/comet/objects/pjsk/local/ProjectSekaiRank.kt +++ /dev/null @@ -1,68 +0,0 @@ -package ren.natsuyuk1.comet.objects.pjsk.local - -import kotlinx.datetime.Instant -import kotlinx.serialization.builtins.ListSerializer -import mu.KotlinLogging -import ren.natsuyuk1.comet.consts.cometClient -import ren.natsuyuk1.comet.consts.json -import ren.natsuyuk1.comet.network.thirdparty.github.GitHubApi -import ren.natsuyuk1.comet.network.thirdparty.projectsekai.objects.PJSKRankSeasonInfo -import ren.natsuyuk1.comet.service.RateLimitAPI -import ren.natsuyuk1.comet.service.RateLimitService -import ren.natsuyuk1.comet.util.pjsk.getSekaiBestResourceURL -import ren.natsuyuk1.comet.util.pjsk.pjskFolder -import ren.natsuyuk1.comet.utils.file.lastModifiedTime -import ren.natsuyuk1.comet.utils.file.readTextBuffered -import ren.natsuyuk1.comet.utils.file.touch -import ren.natsuyuk1.comet.utils.ktor.DownloadStatus -import ren.natsuyuk1.comet.utils.ktor.downloadFile -import kotlin.time.Duration.Companion.days - -private val logger = KotlinLogging.logger {} - -object ProjectSekaiRank : ProjectSekaiLocalFile( - pjskFolder.resolve("rankMatchSeasons.json"), - 30.days, -) { - internal val rankSeasonInfo = mutableListOf() - - override suspend fun load() { - rankSeasonInfo.clear() - - try { - rankSeasonInfo.addAll( - json.decodeFromString( - ListSerializer(PJSKRankSeasonInfo.serializer()), - file.readTextBuffered(), - ), - ) - } catch (e: Exception) { - logger.warn(e) { "解析排位数据时出现问题" } - } - } - - override suspend fun update(): Boolean { - if (RateLimitService.isRateLimit(RateLimitAPI.GITHUB)) { - return false - } - - GitHubApi.getSpecificFileCommits("Sekai-World", "sekai-master-db-diff", file.name).onSuccess { - val current = file.lastModifiedTime() - val lastUpdate = Instant.parse(it.first().commit.committer.date) - - if (!file.exists() || lastUpdate > current) { - file.touch() - if (cometClient.client.downloadFile( - getSekaiBestResourceURL(file.name), - file, - ) == DownloadStatus.OK - ) { - logger.info { "成功更新排位数据" } - return true - } - } - } - - return false - } -} diff --git a/comet-core/src/main/kotlin/ren/natsuyuk1/comet/service/ProjectSekaiManager.kt b/comet-core/src/main/kotlin/ren/natsuyuk1/comet/service/ProjectSekaiManager.kt index a02a40878..503569a6a 100644 --- a/comet-core/src/main/kotlin/ren/natsuyuk1/comet/service/ProjectSekaiManager.kt +++ b/comet-core/src/main/kotlin/ren/natsuyuk1/comet/service/ProjectSekaiManager.kt @@ -5,12 +5,8 @@ import kotlinx.coroutines.launch import mu.KotlinLogging import ren.natsuyuk1.comet.api.task.TaskManager import ren.natsuyuk1.comet.consts.cometClient -import ren.natsuyuk1.comet.network.thirdparty.projectsekai.objects.MusicDifficulty -import ren.natsuyuk1.comet.network.thirdparty.projectsekai.objects.MusicPlayResult import ren.natsuyuk1.comet.network.thirdparty.projectsekai.objects.SekaiEventStatus import ren.natsuyuk1.comet.objects.pjsk.ProjectSekaiData -import ren.natsuyuk1.comet.objects.pjsk.local.ProjectSekaiMusicDifficulty.musicDiffDatabase -import ren.natsuyuk1.comet.objects.pjsk.local.ProjectSekaiRank.rankSeasonInfo import ren.natsuyuk1.comet.objects.pjsk.local.pjskLocal import ren.natsuyuk1.comet.util.pjsk.pjskFolder import ren.natsuyuk1.comet.utils.coroutine.ModuleScope @@ -18,8 +14,6 @@ import ren.natsuyuk1.comet.utils.file.touch import ren.natsuyuk1.comet.utils.ktor.downloadFile import java.io.File import kotlin.coroutines.CoroutineContext -import kotlin.time.DurationUnit -import kotlin.time.toDuration private val logger = KotlinLogging.logger {} @@ -36,19 +30,13 @@ object ProjectSekaiManager { loadPJSKDatabase() scope.launch { refreshEvent() } - scope.launch { refreshCache() } scope.launch { loadBest30Image() } TaskManager.registerTask("pjsk_event", "0 6 * * *", ::refreshEvent) - TaskManager.registerTaskDelayed(3.toDuration(DurationUnit.HOURS), ::refreshCache) logger.info { "Project Sekai 管理器加载完成" } } - private suspend fun refreshCache() { - ProjectSekaiData.updatePredictionData() - } - private suspend fun refreshEvent() { ProjectSekaiData.updateEventInfo() } @@ -97,26 +85,6 @@ object ProjectSekaiManager { } } - fun getSongLevel(songId: Int, difficulty: MusicDifficulty): Int? { - val diffInfo = - musicDiffDatabase.find { it.musicId == songId && it.musicDifficulty == difficulty } ?: return null - - return diffInfo.playLevel - } - - fun getSongAdjustedLevel(songId: Int, difficulty: MusicDifficulty, playResult: MusicPlayResult): Double? { - val diffInfo = - musicDiffDatabase.find { it.musicId == songId && it.musicDifficulty == difficulty } ?: return null - - return diffInfo.playLevel + when (playResult) { - MusicPlayResult.CLEAR -> diffInfo.playLevelAdjust - MusicPlayResult.FULL_COMBO -> diffInfo.fullComboAdjust - MusicPlayResult.ALL_PERFECT -> diffInfo.fullPerfectAdjust - } - } - - fun getLatestRankSeason(): Int? = rankSeasonInfo.lastOrNull()?.id - private suspend fun downloadCardImage(assetBundleName: String, trainingStatus: String) { val suffix = if (trainingStatus == "done") "after_training" else "normal" /* ktlint-disable max-line-length */ diff --git a/comet-core/src/main/kotlin/ren/natsuyuk1/comet/service/image/ProjectSekaiImageService.kt b/comet-core/src/main/kotlin/ren/natsuyuk1/comet/service/image/ProjectSekaiImageService.kt index d3833b536..6085986e1 100644 --- a/comet-core/src/main/kotlin/ren/natsuyuk1/comet/service/image/ProjectSekaiImageService.kt +++ b/comet-core/src/main/kotlin/ren/natsuyuk1/comet/service/image/ProjectSekaiImageService.kt @@ -1,620 +1,38 @@ package ren.natsuyuk1.comet.service.image -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import kotlinx.datetime.Clock -import moe.sdl.yac.core.PrintMessage import org.jetbrains.skia.* -import org.jetbrains.skia.paragraph.* -import ren.natsuyuk1.comet.api.message.MessageWrapper -import ren.natsuyuk1.comet.api.message.asImage -import ren.natsuyuk1.comet.api.message.buildMessageWrapper +import org.jetbrains.skia.paragraph.Alignment +import org.jetbrains.skia.paragraph.ParagraphBuilder +import org.jetbrains.skia.paragraph.ParagraphStyle import ren.natsuyuk1.comet.api.task.TaskManager -import ren.natsuyuk1.comet.consts.cometClient -import ren.natsuyuk1.comet.network.thirdparty.projectsekai.ProjectSekaiAPI.getSpecificRankInfo -import ren.natsuyuk1.comet.network.thirdparty.projectsekai.getSurroundingRank -import ren.natsuyuk1.comet.network.thirdparty.projectsekai.objects.* -import ren.natsuyuk1.comet.objects.pjsk.ProjectSekaiData -import ren.natsuyuk1.comet.objects.pjsk.ProjectSekaiUserData +import ren.natsuyuk1.comet.network.thirdparty.projectsekai.objects.MusicDifficulty +import ren.natsuyuk1.comet.network.thirdparty.projectsekai.objects.ProfileMusicInfo +import ren.natsuyuk1.comet.network.thirdparty.projectsekai.objects.ProjectSekaiMusicInfo import ren.natsuyuk1.comet.objects.pjsk.local.* -import ren.natsuyuk1.comet.service.ProjectSekaiManager -import ren.natsuyuk1.comet.util.pjsk.pjskFolder -import ren.natsuyuk1.comet.util.toMessageWrapper -import ren.natsuyuk1.comet.utils.datetime.toFriendly import ren.natsuyuk1.comet.utils.file.isBlank import ren.natsuyuk1.comet.utils.file.isType import ren.natsuyuk1.comet.utils.file.touch import ren.natsuyuk1.comet.utils.ktor.DownloadStatus -import ren.natsuyuk1.comet.utils.math.NumberUtil.fixDisplay -import ren.natsuyuk1.comet.utils.math.NumberUtil.getBetterNumber -import ren.natsuyuk1.comet.utils.math.NumberUtil.toInstant import ren.natsuyuk1.comet.utils.skiko.FontUtil -import ren.natsuyuk1.comet.utils.skiko.FontUtil.gloryFontSetting import ren.natsuyuk1.comet.utils.skiko.addTextln import ren.natsuyuk1.comet.utils.skiko.changeStyle -import ren.natsuyuk1.comet.utils.string.StringUtil.limit import java.awt.Color import java.io.File -import java.util.concurrent.TimeUnit -import kotlin.math.absoluteValue /** * 负责绘制 Project Sekai 部分功能的图片 */ object ProjectSekaiImageService { - /** - * 代表绘制图片的通用宽度 - */ - private const val WIDTH = 550 - /** * 代表图片内容的填充大小 */ private const val DEFAULT_PADDING = 20 - /** - * 代表角色卡图片的大小 - * 原图为 128x128 - */ - private const val AVATAR_SIZE = 100 - - /** - * 代表歌曲封面的大小 - */ - private const val COVER_SIZE = 50 - /** * 谱面背景色 */ private val BETTER_GRAY_RGB: Int by lazy { Color(220, 220, 220).rgb } - /** - * Skia 绘制质量参数, 使用较低值以压缩大小 - */ - private const val QUALITY = 90 - - private const val BEST30_HEIGHT = 1900 - private const val BEST30_WIDTH = 940 - - /** - * 绘制一张玩家的 30 首最佳歌曲表 - * - * @param user 从 API 获得的 [ProjectSekaiUserInfo] - * @param b30 表现最佳的 30 首歌 - * - * @return 渲染后图片 [Image] - */ - suspend fun drawBest30( - user: ProjectSekaiUserInfo, - b30: List, - ): ren.natsuyuk1.comet.api.message.Image { - val cardId = user.userDecks.first().leader - val avatarBundleName = ProjectSekaiCard.getAssetBundleName(cardId) ?: throw PrintMessage("加载卡面数据失败") - val status = - user.userCards.find { it.cardId == cardId }?.specialTrainingStatus ?: throw PrintMessage("加载卡面数据失败") - - val background = Image.makeFromEncoded(pjskFolder.resolve("b30/b30-background.png").readBytes()) - - val avatar = Image.makeFromEncoded( - ProjectSekaiManager.resolveCardImage( - avatarBundleName, - status, - ).readBytes(), - ) - - val surface = Surface.makeRasterN32Premul(BEST30_WIDTH, BEST30_HEIGHT) - - surface.canvas.apply { - drawImage(background, 0f, 0f, Paint().apply { isAntiAlias = true }) - - drawAvatar(avatar, 25f, 35f, 128f, 10f) - - ParagraphBuilder( - ParagraphStyle().apply { - alignment = Alignment.LEFT - textStyle = FontUtil.defaultFontStyle(Color.BLACK, 35f, style = FontStyle.BOLD) - gloryFontSetting() - }, - FontUtil.fonts, - ).apply { - addTextln(user.user.userGameData.name.limit(12, "..")) - changeStyle(FontUtil.defaultFontStyle(Color.BLACK, 20f)) - addText("ID: ${user.user.userGameData.userID}") - }.build().layout(366f) - .paint(this, 180f, 45f) - - // drawBadge() - - // 左右间隔 10, 上下间隔 20 - - var x = 25f - var y = 260f - - val infoH = 125f - val infoW = 290f - - b30.forEachIndexed { i, musicResult -> - drawMusicInfo(musicResult, x, y) - - if ((i + 1) % 3 == 0) { - x = 25f - y += (infoH + 20f) - } else { - x += (infoW + 10f) - } - } - - drawTextLine( - TextLine.make( - "歌曲难度数据来源于 Project Sekai Profile", - FontUtil.defaultFont(20f, style = FontStyle.BOLD), - ), - 25f, - 1850f, - Paint().apply { - color = Color.BLACK.rgb - isAntiAlias = true - }, - ) - - drawTextLine( - TextLine.make( - "Render by Comet", - FontUtil.defaultFont(20f, style = FontStyle.BOLD), - ), - 750f, - 1850f, - Paint().apply { - color = Color.BLACK.rgb - isAntiAlias = true - }, - ) - } - - val img = surface.makeImageSnapshot() - - return ren.natsuyuk1.comet.api.message.Image( - byteArray = img.encodeToData(EncodedImageFormat.PNG, QUALITY)?.bytes - ?: throw PrintMessage("生成 Best 30 图片失败"), - ) - } - - private fun Canvas.drawAvatar(avatar: Image, x: Float, y: Float, size: Float, radius: Float) { - val rrect = RRect.makeXYWH( - x, - y, - size, - size, - radius, // 圆形弧度 - ) - - save() - clipRRect(rrect, true) - drawImageRect( - avatar, - Rect(0f, 0f, avatar.width.toFloat(), avatar.height.toFloat()), - rrect, - SamplingMode.MITCHELL, - Paint().apply { isAntiAlias = true }, - true, - ) - restore() - } - - private fun Canvas.drawBadge(badge: Image, x: Float, y: Float, size: Float, radius: Float) { - TODO() - } - - private suspend fun Canvas.drawMusicInfo( - musicResult: ProjectSekaiUserInfo.MusicResult, - x: Float, - y: Float, - ) { - // 290 x 125 - save() - drawRect(Rect.makeXYWH(x, y, 290f, 125f), Paint().apply { color = Color.WHITE.rgb }) - restore() - - val musicInfo = ProjectSekaiMusic.getMusicInfo(musicResult.musicId) ?: return - val musicLevel = ProjectSekaiManager.getSongLevel(musicResult.musicId, musicResult.musicDifficulty) - val musicAdjustLevel = ProjectSekaiManager.getSongAdjustedLevel( - musicResult.musicId, - musicResult.musicDifficulty, - musicResult.playResult, - ) - val coverFile = ProjectSekaiMusic.getMusicCover(musicInfo) - - val cover = try { - Image.makeFromEncoded(coverFile.readBytes()) - } catch (e: Exception) { - return - } - - val rrect = RRect.makeXYWH( - x + 20f, - y + 20f, - 80f, - 80f, - 0f, - ) - - save() - clipRRect(rrect, true) - drawImageRect( - cover, - Rect(0f, 0f, cover.width.toFloat(), cover.height.toFloat()), - rrect, - CubicResampler(1 / 3.0f, 1 / 3.0f), - Paint().apply { isAntiAlias = true }, - true, - ) - restore() - - drawCircle( - x + 20f, - y + 20f, - 15f, - Paint().apply { - color = if (musicResult.musicDifficulty == MusicDifficulty.MASTER) { - Color(187, 51, 238).rgb - } else { - Color(238, 67, 102).rgb - } - }, - ) - - drawString( - musicLevel.toString(), - x + 11f, - y + 20f + 4.5f, - FontUtil.defaultFont(15f, style = FontStyle.BOLD), - Paint().apply { - color = Color.WHITE.rgb - isAntiAlias = true - }, - ) - - drawTextLine( - TextLine.make( - musicInfo.title.limit(8, ".."), - FontUtil.defaultFont(20f, style = FontStyle.BOLD), - ), - x + 115f, - y + 35f, - Paint().apply { - color = Color.BLACK.rgb - mode = PaintMode.FILL - isAntiAlias = true - }, - ) - - // PJSK Profile player score 算法 - val multipier = when (musicResult.playResult) { - MusicPlayResult.ALL_PERFECT -> 8.0 - MusicPlayResult.FULL_COMBO -> 7.5 - MusicPlayResult.CLEAR -> 5.0 - } - - drawTextLine( - TextLine.make( - "${musicAdjustLevel?.fixDisplay(1) ?: "N/A"} " + - "${if (musicAdjustLevel != null) " → " + (musicAdjustLevel * multipier).toInt() else ""}", - FontUtil.defaultFont(15f), - ), - x + 115f, - y + 65f, - Paint().apply { - color = Color.BLACK.rgb - isAntiAlias = true - }, - ) - - val statusImg = Image.makeFromEncoded( - withContext(Dispatchers.IO) { - pjskFolder.resolve( - if (musicResult.playResult == MusicPlayResult.ALL_PERFECT) { - "b30/AllPerfect.png" - } else { - "b30/FullCombo.png" - }, - ).readBytes() - }, - ) - - val statusRect = Rect.makeXYWH( - x + 112f, - y + 75f, - 165f, - 30f, - ) - - save() - clipRect(statusRect, true) - drawImageRect( - statusImg, - Rect(0f, 0f, statusImg.width.toFloat(), statusImg.height.toFloat()), - statusRect.inflate(-1f), - SamplingMode.MITCHELL, - Paint().apply { isAntiAlias = true }, - true, - ) - restore() - } - - /** - * 绘制一张玩家的活动信息 - * - * @param eventId 活动 ID - * @param userData 用户活动积分数据 [ProjectSekaiUserData] - * - * @return 包装后的消息 [MessageWrapper] - */ - @OptIn(ExperimentalStdlibApi::class) - suspend fun SekaiProfileEventInfo.drawEventInfo( - eventId: Int, - userData: ProjectSekaiUserData? = null, - ): MessageWrapper { - if (rankings.isEmpty()) { - return "你还没打这期活动捏".toMessageWrapper() - } - - val now = Clock.System.now() - val profile = this.rankings.first() - val (ahead, behind) = profile.rank.getSurroundingRank() - val eventInfo = ProjectSekaiData.getCurrentEventInfo() ?: return "查询失败, 活动信息未加载".toMessageWrapper() - val eventStatus = ProjectSekaiManager.getCurrentEventStatus() - - var aheadEventStatus: SekaiProfileEventInfo? = null - var behindEventStatus: SekaiProfileEventInfo? = null - - if (ahead > 0) { - aheadEventStatus = cometClient.getSpecificRankInfo(eventId, ahead) - } - - if (behind in profile.rank.rangeUntil(1000000)) { - behindEventStatus = cometClient.getSpecificRankInfo(eventId, behind) - } - - // 获取头像内部名称 - val avatarBundleName = ProjectSekaiCard.getAssetBundleName(profile.userCard.cardId.toInt()) - - var avatarFile: File? = null - var avatar: Image? = null - - if (avatarBundleName != null) { - avatarFile = ProjectSekaiManager.resolveCardImage(avatarBundleName, profile.userCard.specialTrainingStatus) - } - - if (avatarFile?.exists() == true && avatarFile.length() != 0L) { - avatar = Image.makeFromEncoded(avatarFile.readBytes()) - } - - val userInfoText = ParagraphBuilder( - ParagraphStyle().apply { - alignment = Alignment.LEFT - textStyle = FontUtil.defaultFontStyle(Color.BLACK, 20f) - }, - FontUtil.fonts, - ).apply { - addTextln(profile.name) - addText("ID: ${profile.userId}") - }.build().layout(WIDTH.toFloat()) - - val eventInfoText = ParagraphBuilder( - ParagraphStyle().apply { - alignment = Alignment.LEFT - textStyle = FontUtil.defaultFontStyle(Color.BLACK, 18f) - }, - FontUtil.fonts, - ).apply { - addTextln("当前活动 ${eventInfo.name}") - - when (eventStatus) { - SekaiEventStatus.ONGOING -> { - addTextln( - "离活动结束还有 ${ - (eventInfo.aggregateTime.toInstant(true) - now).toFriendly(TimeUnit.SECONDS) - }", - ) - } - - SekaiEventStatus.END -> { - addTextln("当前活动已结束") - } - - else -> {} - } - }.build().layout(WIDTH.toFloat()) - - val eventTeamText = if (profile.userCheerfulCarnival.cheerfulCarnivalTeamId != null) { - ParagraphBuilder( - ParagraphStyle().apply { - alignment = Alignment.LEFT - textStyle = FontUtil.defaultFontStyle(Color.BLACK, 18f) - }, - FontUtil.fonts, - ).apply { - val teamName = ProjectSekaiI18N.getCarnivalTeamName(profile.userCheerfulCarnival.cheerfulCarnivalTeamId) - - if (teamName != null) { - addTextln("当前队伍为 $teamName") - } - }.build().layout(WIDTH.toFloat()) - } else { - null - } - - val eventScoreText = ParagraphBuilder( - ParagraphStyle().apply { - alignment = Alignment.LEFT - textStyle = FontUtil.defaultFontStyle(Color.BLACK, 18f) - }, - FontUtil.fonts, - ).apply { - addTextln("分数 ${profile.score} | 排名 ${profile.rank}") - addTextln() - - if (userData != null) { - if (userData.lastQueryScore != 0L && userData.lastQueryPosition != 0) { - val scoreDiff = profile.score - userData.lastQueryScore - val rankDiff = userData.lastQueryPosition - profile.rank - - if (scoreDiff != 0L) { - addText("↑ $scoreDiff 分 ") - } - - if (rankDiff != 0) { - addText( - (if (profile.rank < userData.lastQueryPosition) "↑ 上升" else " ↓ 下降") + - " ${rankDiff.absoluteValue} 名", - ) - } - - addTextln() - addTextln() - } - - // Refresh user pjsk score and rank - userData.updateInfo(profile.score, profile.rank) - } - - if (aheadEventStatus != null) { - val aheadScore = aheadEventStatus.getScore() - - if (aheadScore != -1L) { - val aheadScoreStr = aheadScore.getBetterNumber() - val delta = (aheadScore - profile.score).getBetterNumber() - addTextln("上一档排名 $ahead 的分数为 $aheadScoreStr, 相差 $delta") - } else { - addTextln("上一档排名 $ahead 暂无数据") - } - } - - if (behindEventStatus != null) { - val behindScore = behindEventStatus.getScore() - - if (behindScore != -1L) { - val targetScore = behindScore.getBetterNumber() - val deltaScore = (profile.score - behindScore).getBetterNumber() - addTextln("下一档排名 $behind 的分数为 $targetScore, 相差 $deltaScore") - } else { - addTextln("下一档排名 $behind 暂无数据") - } - } - - addTextln() - - addTextln("数据来自 Unibot API / PJSK Profile") - addText("Render by Comet") - }.build().layout(WIDTH.toFloat()) - - val surface = Surface.makeRasterN32Premul( - WIDTH, - ( - AVATAR_SIZE + DEFAULT_PADDING * 2.5 + eventInfoText.height + eventScoreText.height + ( - eventTeamText?.height - ?: 0f - ) - ).toInt(), - ) - - surface.canvas.apply { - clear(Color.WHITE.rgb) - - if (avatar != null) { - val rrect = RRect.makeXYWH( - 20f, - 20f, - AVATAR_SIZE.toFloat(), - AVATAR_SIZE.toFloat(), - 10f, // 圆形弧度 - ) - - save() - clipRRect(rrect, true) - drawImageRect( - avatar, - Rect(0f, 0f, avatar.width.toFloat(), avatar.height.toFloat()), - rrect, - FilterMipmap(FilterMode.LINEAR, MipmapMode.NEAREST), - null, - true, - ) // 80 x 80 - restore() - } - - userInfoText.paint( - this, - DEFAULT_PADDING * 2f + AVATAR_SIZE, - DEFAULT_PADDING.toFloat(), - ) - - eventInfoText.paint( - this, - DEFAULT_PADDING.toFloat(), - (AVATAR_SIZE + DEFAULT_PADDING * 2).toFloat(), - ) - - var extraY = 0f - - if (eventInfo.eventType == "cheerful_carnival" && - profile.userCheerfulCarnival.cheerfulCarnivalTeamId != null && - eventTeamText != null - ) { - val teamNum = if (profile.userCheerfulCarnival.cheerfulCarnivalTeamId % 2 == 0) 2 else 1 - val teamIcon = ProjectSekaiEvent.getEventTeamImage(teamNum) - var extraX = 0f - - if (teamIcon != null) { - val teamIconImg = Image.makeFromEncoded(teamIcon.readBytes()) - val rect = Rect.makeXYWH( - DEFAULT_PADDING.toFloat(), - AVATAR_SIZE + DEFAULT_PADDING * 2 + eventInfoText.height, - 30f, - 30f, - ) - - save() - clipRect(rect, true) - drawImageRect( - teamIconImg, - Rect(0f, 0f, teamIconImg.width.toFloat(), teamIconImg.height.toFloat()), - rect, - FilterMipmap(FilterMode.LINEAR, MipmapMode.NEAREST), - null, - true, - ) - restore() - - extraX = rect.width - } - - eventTeamText.paint( - this, - DEFAULT_PADDING + extraX + 10f, - AVATAR_SIZE + DEFAULT_PADDING * 2 + eventInfoText.height, - ) - - extraY = eventTeamText.height - } - - eventScoreText.paint( - this, - DEFAULT_PADDING.toFloat(), - AVATAR_SIZE + DEFAULT_PADDING * 2 + eventInfoText.height + extraY, - ) - } - - val image = surface.makeImageSnapshot() - val data = image.encodeToData(EncodedImageFormat.JPEG, QUALITY) ?: return buildMessageWrapper { - appendText("生成图片失败!") - } - - return buildMessageWrapper { - appendElement(data.bytes.asImage()) - } - } - /** * 绘制歌曲谱面 * @@ -683,16 +101,9 @@ object ProjectSekaiImageService { "${musicInfo.title} - ${musicInfo.lyricist}", ) changeStyle(FontUtil.defaultFontStyle(Color.BLACK, 38f)) - val info = ProjectSekaiMusicDifficulty.getMusicDifficulty(musicInfo.id).find { - it.musicDifficulty == difficulty - } + val info = ProjectSekaiMusicDifficulty.getDifficulty(musicInfo.id, difficulty) addText( - "${difficulty.name} [Lv.${ - ProjectSekaiManager.getSongLevel( - musicInfo.id, - difficulty, - ) - }] | 共 ${info?.totalNoteCount} 个键", + "${difficulty.name} [Lv.${info?.playLevel}] | 共 ${info?.totalNoteCount} 个键", ) }.build().layout((bg.width + DEFAULT_PADDING).toFloat()) diff --git a/comet-core/src/test/kotlin/ren/natsuyuk1/comet/test/draw/TestProjectSekaiDraw.kt b/comet-core/src/test/kotlin/ren/natsuyuk1/comet/test/draw/TestProjectSekaiDraw.kt index 358662561..6a53b1ccd 100644 --- a/comet-core/src/test/kotlin/ren/natsuyuk1/comet/test/draw/TestProjectSekaiDraw.kt +++ b/comet-core/src/test/kotlin/ren/natsuyuk1/comet/test/draw/TestProjectSekaiDraw.kt @@ -2,32 +2,16 @@ package ren.natsuyuk1.comet.test.draw import io.ktor.client.call.* import io.ktor.client.request.* -import io.ktor.utils.io.core.* -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.withContext -import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction -import org.junit.jupiter.api.AfterAll import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.TestInstance -import ren.natsuyuk1.comet.api.database.DatabaseManager -import ren.natsuyuk1.comet.api.message.Image -import ren.natsuyuk1.comet.consts.cometClient -import ren.natsuyuk1.comet.network.thirdparty.projectsekai.ProjectSekaiAPI.getUserEventInfo -import ren.natsuyuk1.comet.network.thirdparty.projectsekai.ProjectSekaiAPI.getUserInfo import ren.natsuyuk1.comet.network.thirdparty.projectsekai.objects.MusicDifficulty -import ren.natsuyuk1.comet.objects.pjsk.ProjectSekaiDataTable import ren.natsuyuk1.comet.objects.pjsk.local.ProjectSekaiMusic import ren.natsuyuk1.comet.service.ProjectSekaiManager import ren.natsuyuk1.comet.service.image.ProjectSekaiImageService -import ren.natsuyuk1.comet.service.image.ProjectSekaiImageService.drawEventInfo -import ren.natsuyuk1.comet.test.initTestDatabase import ren.natsuyuk1.comet.test.isCI -import ren.natsuyuk1.comet.test.network.client -import ren.natsuyuk1.comet.test.print import ren.natsuyuk1.comet.utils.file.absPath import ren.natsuyuk1.comet.utils.skiko.SkikoHelper -import java.io.File import kotlin.coroutines.EmptyCoroutineContext import kotlin.test.Test import kotlin.test.assertNotNull @@ -45,9 +29,6 @@ class TestProjectSekaiDraw { @BeforeAll fun init() { - initTestDatabase() - DatabaseManager.loadTables(ProjectSekaiDataTable) - runBlocking { SkikoHelper.loadSkiko() ProjectSekaiManager.init(EmptyCoroutineContext) @@ -65,24 +46,6 @@ class TestProjectSekaiDraw { } } - @Test - fun testAvatarDraw() { - if (isCI()) return - - runBlocking { - val info = cometClient.getUserEventInfo(eventID, id) - - val res = newSuspendedTransaction { - info.drawEventInfo(eventID) - } - - val tfile = File.createTempFile("pjsk_userinfo_test_", ".png") - res.find()?.byteArray?.let { tfile.writeBytes(it) } - - println(tfile.absPath) - } - } - @Test fun testChartDraw() { if (isCI()) return @@ -99,33 +62,4 @@ class TestProjectSekaiDraw { assertTrue { image?.length() != 0L } } } - - @Test - fun testBest30Draw() { - if (isCI()) return - - runBlocking { - val userInfo = client.getUserInfo(id) - val b30 = userInfo.getBest30Songs() - - val result = ProjectSekaiImageService.drawBest30( - userInfo, - b30, - ) - - assertTrue(result != null) - - withContext(Dispatchers.IO) { - File.createTempFile("pjsk_b30_test_", ".png").apply { - writeBytes(result.byteArray!!) - print() - } - } - } - } - - @AfterAll - fun cleanup() { - DatabaseManager.close() - } } diff --git a/comet-core/src/test/kotlin/ren/natsuyuk1/comet/test/network/thirdparty/projectsekai/TestProjectSekaiAPI.kt b/comet-core/src/test/kotlin/ren/natsuyuk1/comet/test/network/thirdparty/projectsekai/TestProjectSekaiAPI.kt index efbd892ee..394865bce 100644 --- a/comet-core/src/test/kotlin/ren/natsuyuk1/comet/test/network/thirdparty/projectsekai/TestProjectSekaiAPI.kt +++ b/comet-core/src/test/kotlin/ren/natsuyuk1/comet/test/network/thirdparty/projectsekai/TestProjectSekaiAPI.kt @@ -11,28 +11,15 @@ package ren.natsuyuk1.comet.test.network.thirdparty.projectsekai import io.ktor.client.plugins.* import kotlinx.coroutines.runBlocking -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json -import org.jetbrains.exposed.sql.deleteAll -import org.jetbrains.exposed.sql.transactions.transaction import org.junit.jupiter.api.AfterAll import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.TestInstance import ren.natsuyuk1.comet.api.database.DatabaseManager -import ren.natsuyuk1.comet.api.platform.CometPlatform -import ren.natsuyuk1.comet.api.user.CometUser -import ren.natsuyuk1.comet.api.user.UserTable import ren.natsuyuk1.comet.network.thirdparty.projectsekai.ProjectSekaiAPI.getEventList -import ren.natsuyuk1.comet.network.thirdparty.projectsekai.ProjectSekaiAPI.getRankPredictionInfo -import ren.natsuyuk1.comet.network.thirdparty.projectsekai.ProjectSekaiAPI.getSpecificRankInfo -import ren.natsuyuk1.comet.network.thirdparty.projectsekai.ProjectSekaiAPI.getUserEventInfo import ren.natsuyuk1.comet.network.thirdparty.projectsekai.ProjectSekaiAPI.getUserInfo -import ren.natsuyuk1.comet.network.thirdparty.projectsekai.objects.kit33.toMessageWrapper import ren.natsuyuk1.comet.network.thirdparty.projectsekai.objects.toMessageWrapper -import ren.natsuyuk1.comet.network.thirdparty.projectsekai.toMessageWrapper import ren.natsuyuk1.comet.objects.pjsk.ProjectSekaiData import ren.natsuyuk1.comet.objects.pjsk.ProjectSekaiDataTable -import ren.natsuyuk1.comet.objects.pjsk.ProjectSekaiUserData import ren.natsuyuk1.comet.objects.pjsk.ProjectSekaiUserDataTable import ren.natsuyuk1.comet.service.ProjectSekaiManager import ren.natsuyuk1.comet.test.initTestDatabase @@ -41,7 +28,6 @@ import ren.natsuyuk1.comet.test.network.client import ren.natsuyuk1.comet.test.print import kotlin.coroutines.EmptyCoroutineContext import kotlin.test.Test -import kotlin.test.assertTrue private val logger = mu.KotlinLogging.logger {} @@ -67,56 +53,6 @@ class TestProjectSekaiAPI { // Welcome to add me as friend :D private val id = 210043933010767872L - @Test - fun testEventProfileFetch() { - if (isCI()) { - return - } - - runBlocking { - val user = CometUser.create(id, CometPlatform.TEST) - - ProjectSekaiUserData.createData(user.id.value, this@TestProjectSekaiAPI.id) - - val data = transaction { - ProjectSekaiData.all().forEach { - it.print() - } - ProjectSekaiUserData.getUserPJSKData(user.id.value) - } ?: return@runBlocking - - client.getUserEventInfo(eventID, id) - .also { it.toMessageWrapper(data, eventID).print() } - - transaction { - ProjectSekaiUserDataTable.deleteAll() - UserTable.deleteAll() - } - } - } - - @Test - fun testEventRankingWithoutDatabaseFetch() { - if (isCI()) { - return - } - - runBlocking { - client.getUserEventInfo(eventID, id).toMessageWrapper(null, eventID).print() - } - } - - @Test - fun testEventRankingPositionFetch() { - if (isCI()) { - return - } - - runBlocking { - client.getSpecificRankInfo(eventID, 100).toMessageWrapper(null, eventID).print() - } - } - @Test fun testEventListFetch() { if (isCI()) { @@ -126,20 +62,6 @@ class TestProjectSekaiAPI { runBlocking { println(client.getEventList()) } } - @Test - fun testRankPredictionFetch() { - if (isCI()) { - return - } - - runBlocking { - try { - client.getRankPredictionInfo().toMessageWrapper().print() - } catch (_: ServerResponseException) { - } - } - } - @Test fun testUserInfoFetch() { if (isCI()) { @@ -151,18 +73,6 @@ class TestProjectSekaiAPI { } } - @Test - fun testB30() { - if (isCI()) return - - runBlocking { - val b30 = client.getUserInfo(id).getBest30Songs() - Json.encodeToString(b30).print() - - assertTrue { b30.size == 30 } - } - } - @AfterAll fun cleanup() { DatabaseManager.close()