Skip to content

Commit

Permalink
✨ feat: Support rate limiting and cooldown
Browse files Browse the repository at this point in the history
  • Loading branch information
HatoYuze committed Jan 1, 2025
1 parent 141749d commit 538e8c0
Show file tree
Hide file tree
Showing 4 changed files with 164 additions and 16 deletions.
33 changes: 32 additions & 1 deletion src/main/kotlin/mirai/RestartLifeCommand.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@ import com.github.hatoyuze.restarter.game.LifeEngine
import com.github.hatoyuze.restarter.game.data.Talent
import com.github.hatoyuze.restarter.game.data.UserEvent
import com.github.hatoyuze.restarter.game.entity.impl.LifeSave.Companion.decodeBase64
import com.github.hatoyuze.restarter.mirai.config.CommandLimitData.checkCooldownStatus
import com.github.hatoyuze.restarter.mirai.config.CommandLimitData.dailyUserRecord
import com.github.hatoyuze.restarter.mirai.config.CommandLimitData.isUserOverLimit
import com.github.hatoyuze.restarter.mirai.config.GameConfig
import com.github.hatoyuze.restarter.mirai.config.GameConfig.Limit.Companion.isNone
import com.github.hatoyuze.restarter.mirai.config.GameConfig.Limit.Companion.userDailyGamingLimit
import com.github.hatoyuze.restarter.mirai.config.GameSaveData
import net.mamoe.mirai.console.command.CommandContext
import net.mamoe.mirai.console.command.CompositeCommand
Expand All @@ -35,6 +40,28 @@ object RestartLifeCommand : CompositeCommand(PluginMain, "remake") {
throw PermissionDeniedException()
}
}
private suspend fun CommandContext.testLimit(): Unit? {
if (userDailyGamingLimit.isNone()) return Unit
val user = sender.user ?: return Unit

if (user.isUserOverLimit()) {
if (dailyUserRecord[user.id]!! <= userDailyGamingLimit.get() + 1) {
quote("您的今日游玩次数已用完,明天再来吧?")
}
return null
}
dailyUserRecord.compute(user.id) { _, count ->
(count ?: 0) + 1
}

val status = sender.checkCooldownStatus()
if (status.isWaiting) {
quote("该功能目前还在冷却中哦~\n您还需要等待 ${status.remainingSeconds} 秒后 才能正常使用功能~")
return null
}

return Unit
}

@Description("开始一场新的人生")
@SubCommand
Expand All @@ -53,6 +80,9 @@ object RestartLifeCommand : CompositeCommand(PluginMain, "remake") {
) = commandContext.run {
commandContext.run command@{
testPermission()
testLimit() ?: return@command


val objectTalents = getTalents(true) ?: return@command

val statusChange = objectTalents.map { it.status }
Expand Down Expand Up @@ -193,6 +223,7 @@ object RestartLifeCommand : CompositeCommand(PluginMain, "remake") {
initialSpirit: Int = 0
) = commandContext.run command@{
testPermission()
testLimit() ?: return@command
val objectTalents = getTalents() ?: return@command

val statusChange = objectTalents.map { it.status }
Expand Down Expand Up @@ -393,7 +424,7 @@ ${engine.life.talents.joinToString("\n") { it.introduction }}

private fun distributeValues(values: List<Int>, pointChange: Int = 0): List<Int> {
val totalSum = values.sum()
val maxAttributePoint = GameConfig.maxAttributePoint + pointChange
val maxAttributePoint = GameConfig.maxAttributePoint.toInt() + pointChange

return when {
totalSum > maxAttributePoint -> {
Expand Down
52 changes: 52 additions & 0 deletions src/main/kotlin/mirai/config/CommandLimitData.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.github.hatoyuze.restarter.mirai.config

import com.github.hatoyuze.restarter.mirai.config.GameConfig.Limit.Companion.isNone
import net.mamoe.mirai.console.command.CommandSender
import net.mamoe.mirai.console.data.AutoSavePluginData
import net.mamoe.mirai.console.data.value
import net.mamoe.mirai.contact.User
import kotlin.time.Duration.Companion.milliseconds

object CommandLimitData : AutoSavePluginData("command_limit_status") {
val dailyUserRecord: MutableMap<Long, Int> by value(mutableMapOf())
val rateLimiting: MutableMap<Long,Long> by value(mutableMapOf())


data class RateLimitingData(
val isWaiting: Boolean,
val remainingSeconds: Int = -1
) {
companion object {
val NORMAL_STATUS = RateLimitingData(
false, -1
)
}
}

fun CommandSender.checkCooldownStatus(): RateLimitingData {
if (GameConfig.Limit.frequencyLimitSeconds.isNone()) return RateLimitingData.NORMAL_STATUS
val contact = when(GameConfig.Limit.frequencyType) {
GameConfig.Limit.ContactType.SUBJECT -> subject
GameConfig.Limit.ContactType.SENDER -> user
} ?: return RateLimitingData.NORMAL_STATUS
val currentTime = System.currentTimeMillis()

var isWaiting = true
var remainingSeconds = -1
rateLimiting.compute(contact.id) { _, cooldownTime ->
if (cooldownTime == null || currentTime >= cooldownTime) {
isWaiting = false
return@compute currentTime + GameConfig.Limit.frequencyLimitSeconds.get().inWholeMilliseconds
}

remainingSeconds = (cooldownTime - currentTime).milliseconds.inWholeSeconds.toInt()
cooldownTime
}
return RateLimitingData(isWaiting, remainingSeconds)
}

fun User.isUserOverLimit(): Boolean {
if (GameConfig.Limit.userDailyGamingLimit.isNone()) return false
return (dailyUserRecord[id] ?: 0) > GameConfig.Limit.userDailyGamingLimit.get()
}
}
47 changes: 46 additions & 1 deletion src/main/kotlin/mirai/config/GameConfig.kt
Original file line number Diff line number Diff line change
@@ -1,12 +1,55 @@
package com.github.hatoyuze.restarter.mirai.config

import kotlinx.serialization.Serializable
import net.mamoe.mirai.console.data.AutoSavePluginConfig
import net.mamoe.mirai.console.data.ValueDescription
import net.mamoe.mirai.console.data.value
import java.util.*
import kotlin.jvm.optionals.getOrNull
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds

object GameConfig : AutoSavePluginConfig("game") {

@Serializable
data class Limit(
@ValueDescription("每个用户一天内(UTC+8 00:00 时刷新)的模拟人生最高次数。为 -1 时无限制")
val userDailyGamingLimit: Int = -1,
@ValueDescription("冷却的作用单位,可选为 GROUP 或者 SENDER\n - 选用 SUBJECT 表示冷却为整个 联系人 对象(可能为群聊 或者 好友)\n - 选用 SENDER 表示冷却指令发送者频率")
val frequencyType: ContactType,
@ValueDescription("执行指令冷却的时长,单位为秒\n为 -1 时表示无冷却")
val frequencyLimitSeconds: Int = -1,
) {
@Serializable
enum class ContactType{
SUBJECT, SENDER
}
companion object {
val userDailyGamingLimit: Optional<Int>
get() {
val data = limit.userDailyGamingLimit
return when {
data < 0 -> Optional.empty()
else -> Optional.of(data)
}
}
val frequencyType: ContactType
get() = limit.frequencyType
val frequencyLimitSeconds: Optional<Duration>
get() {
val data = limit.frequencyLimitSeconds
return when {
data < 0 -> Optional.empty()
else -> Optional.of(data.seconds)
}
}

fun Optional<*>.isNone() = this.getOrNull() == null
}
}

@ValueDescription("最大总属性点,用户分配的属性点最终一定为该值(默认为20)")
val maxAttributePoint: Int by value(20)
val maxAttributePoint: UInt by value(20.toUInt())

@ValueDescription("在内存中缓存运行过程中创建的临时事件")
val enableCacheTemporaryEvent: Boolean by value(false)
Expand All @@ -23,6 +66,8 @@ object GameConfig : AutoSavePluginConfig("game") {
@ValueDescription("临时存储文件的目录\n在插件退出时,这些图片会被清除, 为空时表示插件的 data 目录")
val cachePath: String by value("")

@ValueDescription("关于人生模拟器的相关频率限制")
val limit: Limit by value(Limit(userDailyGamingLimit = -1, frequencyType = Limit.ContactType.SUBJECT, frequencyLimitSeconds = -1))

fun String?.ifNull(replacement: String) = if (this.isNullOrEmpty()) replacement else this
}
48 changes: 34 additions & 14 deletions src/main/kotlin/mirai/config/GameSaveData.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package com.github.hatoyuze.restarter.mirai.config
import com.github.hatoyuze.restarter.PluginMain
import com.github.hatoyuze.restarter.game.entity.impl.Life
import com.github.hatoyuze.restarter.game.entity.impl.LifeSave
import com.github.hatoyuze.restarter.mirai.config.CommandLimitData.dailyUserRecord
import com.github.hatoyuze.restarter.mirai.config.CommandLimitData.rateLimiting
import com.github.hatoyuze.restarter.mirai.config.GameConfig.enableGameSave
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.delay
Expand All @@ -16,9 +18,13 @@ import net.mamoe.mirai.console.data.PluginDataStorage
import net.mamoe.mirai.console.data.value
import net.mamoe.mirai.console.util.ConsoleExperimentalApi
import net.mamoe.mirai.contact.User
import java.time.Duration.between
import java.time.Instant
import java.time.ZoneId
import java.time.ZonedDateTime
import java.util.*
import kotlin.time.Duration
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.hours

object GameSaveData: AutoSavePluginData("life") {
Expand All @@ -29,9 +35,11 @@ object GameSaveData: AutoSavePluginData("life") {
@SerialName("S")
val content: LifeSave
)
private val enableDataRemover = GameConfig.dataExpireTime >= 0 && enableGameSave

val data: MutableList<Data> by value(mutableListOf())

var lastId: Int by value(100)
private var lastId: Int by value(100)


fun save(life: Life, context: User? = null): Optional<Int> {
Expand All @@ -56,24 +64,36 @@ object GameSaveData: AutoSavePluginData("life") {
override fun onInit(owner: PluginDataHolder, storage: PluginDataStorage) {
super.onInit(owner, storage)

if (GameConfig.dataExpireTime < 0|| !enableGameSave) return
val now = ZonedDateTime.now(ZoneId.of("UTC+8"))
val midnight = now.plusDays(1)
.minusHours(now.hour.toLong()).minusMinutes(now.minute.toLong()).minusSeconds(now.second + 2L)

val duration = between(now, midnight).toMillis()
PluginMain.launch(CoroutineName("GameSaveData.checkExpire")) {
delay(duration)
while (isActive) {
val dataExpireTime = GameConfig.dataExpireTime.hours
delay(dataExpireTime.div(2))
changeData()

val currentTimeUTC = Instant.now().toEpochMilli()
val maxTime = dataExpireTime.inWholeMilliseconds
delay(1.days)
}
}
}

private fun changeData() {
dailyUserRecord.clear()
rateLimiting.clear()

if (!enableDataRemover) return
val dataExpireTime = GameConfig.dataExpireTime.hours
val currentTimeUTC = Instant.now().toEpochMilli()
val maxTime = dataExpireTime.inWholeMilliseconds

val itr = data.iterator()
while (itr.hasNext()) {
val next = itr.next().content
if (next.createAtTimestampUTC + maxTime <= currentTimeUTC) {
PluginMain.logger.info("一项评分为 ${next.score} 且 创立于 ${next.createAtTimestampUTC} 的存档已过期,已自动删除")
itr.remove()
}
}
val itr = data.iterator()
while (itr.hasNext()) {
val next = itr.next().content
if (next.createAtTimestampUTC + maxTime <= currentTimeUTC) {
PluginMain.logger.info("一项评分为 ${next.score} 且 创立于 ${next.createAtTimestampUTC} 的存档已过期,已自动删除")
itr.remove()
}
}
}
Expand Down

0 comments on commit 538e8c0

Please sign in to comment.