diff --git a/build.sbt b/build.sbt index 91dd6c6eaf..9a69519b19 100644 --- a/build.sbt +++ b/build.sbt @@ -6,7 +6,7 @@ import java.io._ ThisBuild / scalaVersion := "2.13.1" // ThisBuild / version はGitHub Actionsによって自動更新される。 // 次の行は ThisBuild / version := "(\d*)" の形式でなければならない。 -ThisBuild / version := "31" +ThisBuild / version := "32" ThisBuild / organization := "click.seichi" ThisBuild / description := "ギガンティック☆整地鯖の独自要素を司るプラグイン" @@ -141,6 +141,7 @@ lazy val root = (project in file(".")) "-deprecation", "-Ypatmat-exhaust-depth", "320", "-Ymacro-annotations", + "-Ywarn-unused", ), javacOptions ++= Seq("-encoding", "utf8") ) diff --git a/src/main/resources/db/migration/V1.7.5__Create_player_rate_limit_table.sql b/src/main/resources/db/migration/V1.7.5__Create_player_rate_limit_table.sql new file mode 100644 index 0000000000..5771dac93a --- /dev/null +++ b/src/main/resources/db/migration/V1.7.5__Create_player_rate_limit_table.sql @@ -0,0 +1,6 @@ +CREATE TABLE IF NOT EXISTS build_count_rate_limit( + uuid CHAR(36) PRIMARY KEY NOT NULL, + -- 注: DECIMALは(全体の桁数, 小数点以下の桁数)というフォーマットである + available_permission DECIMAL(17, 5) UNSIGNED NOT NULL, + updated_date DATETIME NOT NULL +); diff --git a/src/main/scala/com/github/unchama/datarepository/definitions/RefDictBackedRepositoryDefinition.scala b/src/main/scala/com/github/unchama/datarepository/definitions/RefDictBackedRepositoryDefinition.scala index 3cf688e81b..6c4d22036e 100644 --- a/src/main/scala/com/github/unchama/datarepository/definitions/RefDictBackedRepositoryDefinition.scala +++ b/src/main/scala/com/github/unchama/datarepository/definitions/RefDictBackedRepositoryDefinition.scala @@ -30,6 +30,23 @@ object RefDictBackedRepositoryDefinition { RepositoryDefinition.Phased.SinglePhased.withoutTappingAction(initialization, finalization) } + def usingUuidRefDictWithoutDefault[ + F[_] : Monad, Player, R + ](refDict: RefDict[F, UUID, R]): RepositoryDefinition.Phased.SinglePhased[F, Player, Option[R]] = { + val initialization: SinglePhasedRepositoryInitialization[F, Option[R]] = + (uuid, _) => refDict + .read(uuid) + .map(PrefetchResult.Success.apply) + + val finalization: RepositoryFinalization[F, UUID, Option[R]] = + RepositoryFinalization.withoutAnyFinalization( + (uuid, optR) => + optR.fold(Monad[F].pure(()))(r => refDict.write(uuid, r)) + ) + + RepositoryDefinition.Phased.SinglePhased.withoutTappingAction(initialization, finalization) + } + def usingUuidRefDict[F[_] : Monad, Player, R](refDict: RefDict[F, UUID, R]) (defaultValue: R): RepositoryDefinition.Phased.SinglePhased[F, Player, R] = diff --git a/src/main/scala/com/github/unchama/generic/ratelimiting/FixedWindowRateLimiter.scala b/src/main/scala/com/github/unchama/generic/ratelimiting/FixedWindowRateLimiter.scala index c49caff113..1fc35c6ae0 100644 --- a/src/main/scala/com/github/unchama/generic/ratelimiting/FixedWindowRateLimiter.scala +++ b/src/main/scala/com/github/unchama/generic/ratelimiting/FixedWindowRateLimiter.scala @@ -5,6 +5,7 @@ import cats.effect.concurrent.Ref import cats.effect.{Concurrent, ConcurrentEffect, IO, Sync, Timer} import com.github.unchama.generic.ContextCoercion import com.github.unchama.generic.algebra.typeclasses.OrderedMonus +import com.github.unchama.generic.algebra.typeclasses.OrderedMonus._ import scala.concurrent.duration.FiniteDuration import scala.ref.WeakReference @@ -19,11 +20,12 @@ object FixedWindowRateLimiter { F[_] : ConcurrentEffect : Timer, G[_] : Sync : ContextCoercion[*[_], F], A: OrderedMonus - ](maxPermits: A, resetDuration: FiniteDuration): G[RateLimiter[G, A]] = { + ](maxPermits: A, resetDuration: FiniteDuration, firstPermits: Option[A] = None): G[RateLimiter[G, A]] = { val zero = OrderedMonus[A].empty + val initialCount = maxPermits |-| firstPermits.getOrElse(maxPermits) for { - countRef <- Ref.of[G, A](zero) + countRef <- Ref.of[G, A](initialCount) rateLimiter = RateLimiter.fromCountRef(countRef)(maxPermits) refreshPermits = countRef.set(zero).coerceTo[F] diff --git a/src/main/scala/com/github/unchama/generic/ratelimiting/RateLimiter.scala b/src/main/scala/com/github/unchama/generic/ratelimiting/RateLimiter.scala index eb82adf850..673ee08c59 100644 --- a/src/main/scala/com/github/unchama/generic/ratelimiting/RateLimiter.scala +++ b/src/main/scala/com/github/unchama/generic/ratelimiting/RateLimiter.scala @@ -1,5 +1,6 @@ package com.github.unchama.generic.ratelimiting +import cats.Functor import cats.effect.concurrent.Ref import com.github.unchama.generic.algebra.typeclasses.OrderedMonus @@ -34,6 +35,12 @@ trait RateLimiter[F[_], A] { */ def requestPermission(a: A): F[A] + /** + * 次にリセットされるまで送っても良いリクエスト量を取得する作用を返す。 + * この作用の発火が、送って良いリクエスト量に影響することはない。 + * @return [[A]] によって記述される、次にリセットされるまで送っても良いリクエスト量を取得する作用 + */ + def peekAvailablePermissions: F[A] } object RateLimiter { @@ -44,7 +51,7 @@ object RateLimiter { /** * 送信したリクエスト数を保持する参照セルの情報を見る [[RateLimiter]] を作成する。 */ - def fromCountRef[F[_], A: OrderedMonus](countRef: Ref[F, A])(maxCount: A): RateLimiter[F, A] = + def fromCountRef[F[_]: Functor, A: OrderedMonus](countRef: Ref[F, A])(maxCount: A): RateLimiter[F, A] = new RateLimiter[F, A] { override protected val A: OrderedMonus[A] = implicitly @@ -53,6 +60,8 @@ object RateLimiter { val newCount = (count |+| a) min maxCount (newCount, newCount |-| count) } + + override def peekAvailablePermissions: F[A] = countRef.get.map(maxCount |-| _) } } diff --git a/src/main/scala/com/github/unchama/minecraft/algebra/HasUuid.scala b/src/main/scala/com/github/unchama/minecraft/algebra/HasUuid.scala index 3db36d331f..f6270f744d 100644 --- a/src/main/scala/com/github/unchama/minecraft/algebra/HasUuid.scala +++ b/src/main/scala/com/github/unchama/minecraft/algebra/HasUuid.scala @@ -14,4 +14,6 @@ object HasUuid { def apply[T](implicit ev: HasUuid[T]): HasUuid[T] = ev + implicit val trivial: HasUuid[UUID] = x => x + } diff --git a/src/main/scala/com/github/unchama/seichiassist/SeichiAssist.scala b/src/main/scala/com/github/unchama/seichiassist/SeichiAssist.scala index 35b63ae59f..d6df560b2d 100644 --- a/src/main/scala/com/github/unchama/seichiassist/SeichiAssist.scala +++ b/src/main/scala/com/github/unchama/seichiassist/SeichiAssist.scala @@ -28,7 +28,7 @@ import com.github.unchama.seichiassist.MaterialSets.BlockBreakableBySkill import com.github.unchama.seichiassist.SeichiAssist.seichiAssistConfig import com.github.unchama.seichiassist.bungee.BungeeReceiver import com.github.unchama.seichiassist.commands._ -import com.github.unchama.seichiassist.commands.legacy.{DonationCommand, GachaCommand, VoteCommand} +import com.github.unchama.seichiassist.commands.legacy.{DonationCommand, GachaCommand} import com.github.unchama.seichiassist.concurrent.PluginExecutionContexts import com.github.unchama.seichiassist.concurrent.PluginExecutionContexts.asyncShift import com.github.unchama.seichiassist.data.player.PlayerData @@ -208,6 +208,8 @@ class SeichiAssist extends JavaPlugin() { implicit val configuration: subsystems.buildcount.application.Configuration = seichiAssistConfig.buildCountConfiguration + implicit val syncIoClock: Clock[SyncIO] = Clock.create + subsystems.buildcount.System.wired[IO, SyncIO](loggerF).unsafeRunSync() } @@ -508,7 +510,7 @@ class SeichiAssist extends JavaPlugin() { // コマンドの登録 Map( "gacha" -> new GachaCommand(), - "vote" -> new VoteCommand, + "vote" -> VoteCommand.executor, "donation" -> new DonationCommand, "map" -> MapCommand.executor, "ef" -> new EffectCommand(fastDiggingEffectSystem.settingsApi).executor, diff --git a/src/main/scala/com/github/unchama/seichiassist/commands/VoteCommand.scala b/src/main/scala/com/github/unchama/seichiassist/commands/VoteCommand.scala new file mode 100644 index 0000000000..80cd1ef976 --- /dev/null +++ b/src/main/scala/com/github/unchama/seichiassist/commands/VoteCommand.scala @@ -0,0 +1,59 @@ +package com.github.unchama.seichiassist.commands + +import cats.data.Kleisli +import cats.effect.IO +import com.github.unchama.contextualexecutor.builder.Parsers +import com.github.unchama.seichiassist.SeichiAssist +import com.github.unchama.seichiassist.commands.contextual.builder.BuilderTemplates.playerCommandBuilder +import com.github.unchama.seichiassist.util.Util +import com.github.unchama.targetedeffect.{TargetedEffect, UnfocusedEffect} +import com.github.unchama.targetedeffect.commandsender.MessageEffect +import org.bukkit.ChatColor.{GREEN, RED, YELLOW} +import org.bukkit.command.{CommandSender, TabExecutor} + +object VoteCommand { + sealed trait Operation + case object Record extends Operation + + val usageEchoEcexutor: TargetedEffect[CommandSender] = MessageEffect(List( + s"$RED/vote record <プレイヤー名>", + "投票特典配布用コマンドです" + )) + val executor: TabExecutor = playerCommandBuilder + .argumentsParsers( + List( + Parsers.fromOptionParser({ + case "record" => Some(Record) + case _ => None + }, usageEchoEcexutor), + Parsers.identity + ) + ) + .execution(context => { + val args = context.args.parsed + val command: Operation = args.head.asInstanceOf + val name: String = args(1).asInstanceOf + command match { + case Record => { + //引数が2つの時の処理 + val lowerCasePlayerName = Util.getName(name) + //プレイヤーオンライン、オフラインにかかわらずsqlに送信(マルチ鯖におけるコンフリクト防止の為) + IO { + for { + _ <- MessageEffect(s"$YELLOW${lowerCasePlayerName}の投票特典配布処理開始…") + _ <- UnfocusedEffect { + SeichiAssist.databaseGateway.playerDataManipulator.incrementVotePoint(lowerCasePlayerName) + } + k = if (SeichiAssist.databaseGateway.playerDataManipulator.addChainVote(lowerCasePlayerName)) { + MessageEffect(s"${GREEN}連続投票数の記録に成功") + } else { + MessageEffect(s"${RED}連続投票数の記録に失敗") + } + } yield k + } + } + } + }) + .build() + .asNonBlockingTabExecutor() +} diff --git a/src/main/scala/com/github/unchama/seichiassist/commands/legacy/VoteCommand.scala b/src/main/scala/com/github/unchama/seichiassist/commands/legacy/VoteCommand.scala deleted file mode 100644 index fd48b61d46..0000000000 --- a/src/main/scala/com/github/unchama/seichiassist/commands/legacy/VoteCommand.scala +++ /dev/null @@ -1,48 +0,0 @@ -package com.github.unchama.seichiassist.commands.legacy - -import com.github.unchama.seichiassist.SeichiAssist -import com.github.unchama.seichiassist.database.DatabaseGateway -import com.github.unchama.seichiassist.util.Util -import org.bukkit.ChatColor._ -import org.bukkit.command.{Command, CommandExecutor, CommandSender} - -class VoteCommand extends CommandExecutor { - override def onCommand(sender: CommandSender, command: Command, label: String, args: Array[String]): Boolean = { - val databaseGateway: DatabaseGateway = SeichiAssist.databaseGateway - - def notifyRecordCommandUsage(): Unit = { - sender.sendMessage(Array( - s"$RED/vote record <プレイヤー名>", - "投票特典配布用コマンドです(マルチ鯖対応済)" - )) - } - - def printHelp(): Unit = { - notifyRecordCommandUsage() - } - - if (args.length == 0) { - printHelp() - } else if (args(0).equalsIgnoreCase("record")) { - if (args.length != 2) { //引数が2つでない時の処理 - notifyRecordCommandUsage() - } else { - //引数が2つの時の処理 - val lowerCasePlayerName = Util.getName(args(1)) - //プレイヤーオンライン、オフラインにかかわらずsqlに送信(マルチ鯖におけるコンフリクト防止の為) - sender.sendMessage(s"$YELLOW${lowerCasePlayerName}の投票特典配布処理開始…") - //mysqlにも書き込んどく - databaseGateway.playerDataManipulator.incrementVotePoint(lowerCasePlayerName) - - if (databaseGateway.playerDataManipulator.addChainVote(lowerCasePlayerName)) - sender.sendMessage(s"${GREEN}連続投票数の記録に成功") - else - sender.sendMessage(s"${RED}連続投票数の記録に失敗") - } - } else { - printHelp() - } - - true - } -} diff --git a/src/main/scala/com/github/unchama/seichiassist/listener/EntityListener.scala b/src/main/scala/com/github/unchama/seichiassist/listener/EntityListener.scala index e9a020ed8e..b1dd94eca3 100644 --- a/src/main/scala/com/github/unchama/seichiassist/listener/EntityListener.scala +++ b/src/main/scala/com/github/unchama/seichiassist/listener/EntityListener.scala @@ -187,6 +187,6 @@ class EntityListener(implicit effectEnvironment: EffectEnvironment, //プレイヤーが整地ワールドに居ない場合終了 if (!player.getWorld.isSeichi) return val GBTR = new GiganticBerserkTask - GBTR.PlayerKillEnemy(player, entity) + GBTR.PlayerKillEnemy(player) } } \ No newline at end of file diff --git a/src/main/scala/com/github/unchama/seichiassist/listener/PlayerInventoryListener.scala b/src/main/scala/com/github/unchama/seichiassist/listener/PlayerInventoryListener.scala index 5d2799636d..37f8c8cb33 100644 --- a/src/main/scala/com/github/unchama/seichiassist/listener/PlayerInventoryListener.scala +++ b/src/main/scala/com/github/unchama/seichiassist/listener/PlayerInventoryListener.scala @@ -473,6 +473,7 @@ class PlayerInventoryListener(implicit effectEnvironment: EffectEnvironment, } else if (itemstackcurrent.getType == Material.BOOK_AND_QUILL) { // 投票リンク表示 player.sendMessage(RED.toString + "" + UNDERLINE + "https://minecraft.jp/servers/54d3529e4ddda180780041a7/vote") + player.sendMessage(RED.toString + "" + UNDERLINE + "https://monocraft.net/servers/Cf3BffNIRMERDNbAfWQm") player.playSound(player.getLocation, Sound.BLOCK_STONE_BUTTON_CLICK_ON, 1f, 1f) player.closeInventory() } else if (isSkull && itemstackcurrent.getItemMeta.asInstanceOf[SkullMeta].getOwner == "MHF_ArrowLeft") { diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/buildcount/System.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/buildcount/System.scala index 0a9c595add..2b3a223e51 100644 --- a/src/main/scala/com/github/unchama/seichiassist/subsystems/buildcount/System.scala +++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/buildcount/System.scala @@ -1,21 +1,24 @@ package com.github.unchama.seichiassist.subsystems.buildcount -import cats.effect.{ConcurrentEffect, SyncEffect, Timer} +import cats.effect.{Clock, ConcurrentEffect, SyncEffect, Timer} import com.github.unchama.concurrent.NonServerThreadContextShift import com.github.unchama.datarepository.KeyedDataRepository import com.github.unchama.datarepository.bukkit.player.BukkitRepositoryControls import com.github.unchama.datarepository.template.RepositoryDefinition import com.github.unchama.generic.ContextCoercion import com.github.unchama.generic.effect.concurrent.ReadOnlyRef +import com.github.unchama.generic.ratelimiting.RateLimiter import com.github.unchama.seichiassist.meta.subsystem.Subsystem import com.github.unchama.seichiassist.subsystems.buildcount.application.actions.{ClassifyPlayerWorld, IncrementBuildExpWhenBuiltByHand, IncrementBuildExpWhenBuiltWithSkill} import com.github.unchama.seichiassist.subsystems.buildcount.application.application.{BuildAmountDataRepositoryDefinition, RateLimiterRepositoryDefinitions} import com.github.unchama.seichiassist.subsystems.buildcount.application.{BuildExpMultiplier, Configuration} import com.github.unchama.seichiassist.subsystems.buildcount.bukkit.actions.ClassifyBukkitPlayerWorld import com.github.unchama.seichiassist.subsystems.buildcount.bukkit.listeners.BuildExpIncrementer +import com.github.unchama.seichiassist.subsystems.buildcount.domain.explevel.BuildExpAmount import com.github.unchama.seichiassist.subsystems.buildcount.domain.playerdata.BuildAmountData -import com.github.unchama.seichiassist.subsystems.buildcount.infrastructure.JdbcBuildAmountDataPersistence +import com.github.unchama.seichiassist.subsystems.buildcount.infrastructure.{JdbcBuildAmountDataPersistence, JdbcBuildAmountRateLimitPersistence} import com.github.unchama.util.logging.log4cats.PrefixedLogger +import io.chrisdavenport.cats.effect.time.JavaTime import io.chrisdavenport.log4cats.Logger import org.bukkit.entity.Player import org.bukkit.event.Listener @@ -34,19 +37,21 @@ object System { def wired[ F[_] : ConcurrentEffect : NonServerThreadContextShift : Timer, - G[_] : SyncEffect : ContextCoercion[*[_], F] + G[_] : SyncEffect : ContextCoercion[*[_], F] : Clock ](rootLogger: Logger[F]) (implicit configuration: Configuration): G[System[F, G]] = { import com.github.unchama.minecraft.bukkit.actions.SendBukkitMessage._ implicit val expMultiplier: BuildExpMultiplier = configuration.multipliers implicit val persistence: JdbcBuildAmountDataPersistence[G] = new JdbcBuildAmountDataPersistence[G]() + implicit val rateLimitPersistence: JdbcBuildAmountRateLimitPersistence[G, F] = new JdbcBuildAmountRateLimitPersistence[G, F]() implicit val logger: Logger[F] = PrefixedLogger[F]("BuildAssist-BuildAmount")(rootLogger) + implicit val javaTimeG: JavaTime[G] = JavaTime.fromClock for { rateLimiterRepositoryControls <- BukkitRepositoryControls.createHandles( - RepositoryDefinition.Phased.SinglePhased.withoutTappingAction( + RepositoryDefinition.Phased.SinglePhased.withoutTappingAction[G, Player, RateLimiter[G, BuildExpAmount]]( RateLimiterRepositoryDefinitions.initialization[F, G], RateLimiterRepositoryDefinitions.finalization[G, UUID] ) diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/buildcount/application/application/RateLimiterRepositoryDefinitions.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/buildcount/application/application/RateLimiterRepositoryDefinitions.scala index 3638290638..1001912fbb 100644 --- a/src/main/scala/com/github/unchama/seichiassist/subsystems/buildcount/application/application/RateLimiterRepositoryDefinitions.scala +++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/buildcount/application/application/RateLimiterRepositoryDefinitions.scala @@ -1,12 +1,24 @@ package com.github.unchama.seichiassist.subsystems.buildcount.application.application +import cats.Monad import cats.effect.{ConcurrentEffect, Sync, Timer} +import cats.implicits._ +import com.github.unchama.datarepository.definitions.RefDictBackedRepositoryDefinition import com.github.unchama.datarepository.template.finalization.RepositoryFinalization import com.github.unchama.datarepository.template.initialization.SinglePhasedRepositoryInitialization import com.github.unchama.generic.ContextCoercion +import com.github.unchama.generic.ContextCoercion._ +import com.github.unchama.generic.algebra.typeclasses.OrderedMonus import com.github.unchama.generic.ratelimiting.{FixedWindowRateLimiter, RateLimiter} +import com.github.unchama.minecraft.algebra.HasUuid import com.github.unchama.seichiassist.subsystems.buildcount.application.Configuration +import com.github.unchama.seichiassist.subsystems.buildcount.domain.BuildAmountRateLimiterSnapshot import com.github.unchama.seichiassist.subsystems.buildcount.domain.explevel.BuildExpAmount +import com.github.unchama.seichiassist.subsystems.buildcount.domain.playerdata.BuildAmountRateLimitPersistence +import io.chrisdavenport.cats.effect.time.JavaTime + +import java.time.ZoneId +import java.util.concurrent.TimeUnit object RateLimiterRepositoryDefinitions { @@ -14,16 +26,57 @@ object RateLimiterRepositoryDefinitions { def initialization[ F[_] : ConcurrentEffect : Timer, - G[_] : Sync : ContextCoercion[*[_], F] - ](implicit config: Configuration): SinglePhasedRepositoryInitialization[G, RateLimiter[G, BuildExpAmount]] = - SinglePhasedRepositoryInitialization.withSupplier { - FixedWindowRateLimiter.in[F, G, BuildExpAmount]( - config.oneMinuteBuildExpLimit, - 1.minute - ) - } + G[_] : Sync: ContextCoercion[*[_], F] : JavaTime + ]( + implicit config: Configuration, + persistence: BuildAmountRateLimitPersistence[G] + ): SinglePhasedRepositoryInitialization[G, RateLimiter[G, BuildExpAmount]] = { + val max = config.oneMinuteBuildExpLimit + val span = 1.minute + val rateLimiter = FixedWindowRateLimiter.in[F, G, BuildExpAmount](max, span) - def finalization[F[_] : Sync, Player]: RepositoryFinalization[F, Player, RateLimiter[F, BuildExpAmount]] = - RepositoryFinalization.trivial + val maxValueWithCurrentTimeG = BuildAmountRateLimiterSnapshot.now[G](max) + RefDictBackedRepositoryDefinition + .usingUuidRefDictWithoutDefault(persistence) + .initialization + .extendPreparation { (_, _) => + loadedRecordOpt => { + for { + currentLocalTime <- JavaTime[G].getLocalDateTime(ZoneId.systemDefault()) + initialPermitCount = loadedRecordOpt.fold(max) { loadedRecord => + val duration = FiniteDuration( + java.time.Duration + .between(loadedRecord.recordTime, currentLocalTime) + .toNanos, + TimeUnit.NANOSECONDS + ) + // NOTE: これはファイナライゼーションされたときのレートリミッターと + // イニシャライゼーションで作成されるレートリミッターが起動した時刻の差が + // 規定時間の整数倍になっているとは限らないので多少の誤差を発生させることがある。 + // しかし、とりあえず趣旨を達成するためにこの実装を使う。 + // 必要であれば再度編集して同期を取るようにすること。 + if (duration >= span) { + // expired + max + } else { + loadedRecord.amount + } + } + rateLimiter <- FixedWindowRateLimiter.in[F, G, BuildExpAmount](max, span, Some(initialPermitCount)) + } yield rateLimiter + } + } + } + def finalization[ + F[_] : Sync : JavaTime, + Player: HasUuid + ](implicit config: Configuration, persistence: BuildAmountRateLimitPersistence[F]): RepositoryFinalization[F, Player, RateLimiter[F, BuildExpAmount]] = + RepositoryFinalization.withoutAnyFinalization { case (p, rateLimiter) => + for { + currentRecord <- rateLimiter.peekAvailablePermissions + persistenceRecord <- BuildAmountRateLimiterSnapshot.now(currentRecord) + _ <- persistence.write(HasUuid[Player].of(p), persistenceRecord) + } yield () + } } diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/buildcount/domain/BuildAmountRateLimiterSnapshot.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/buildcount/domain/BuildAmountRateLimiterSnapshot.scala new file mode 100644 index 0000000000..3d68683f75 --- /dev/null +++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/buildcount/domain/BuildAmountRateLimiterSnapshot.scala @@ -0,0 +1,22 @@ +package com.github.unchama.seichiassist.subsystems.buildcount.domain + +import cats.Functor +import cats.implicits._ +import com.github.unchama.seichiassist.subsystems.buildcount.domain.explevel.BuildExpAmount +import io.chrisdavenport.cats.effect.time.JavaTime + +import java.time.{LocalDateTime, ZoneId} + +/** + * `RateLimiter[F, BuildExpAmount]`によって保持された残量について日時付きで保存するクラス + * @param amount そのタイムスライスにおけるリクエスト量の上限 + * @param recordTime 取得した時間 + */ +case class BuildAmountRateLimiterSnapshot(amount: BuildExpAmount, recordTime: LocalDateTime) + +object BuildAmountRateLimiterSnapshot { + def now[F[_]: JavaTime: Functor](buildExpAmount: BuildExpAmount): F[BuildAmountRateLimiterSnapshot] = { + JavaTime[F].getLocalDateTime(ZoneId.systemDefault()) + .map(ldt => BuildAmountRateLimiterSnapshot(buildExpAmount, ldt)) + } +} diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/buildcount/domain/playerdata/BuildAmountRateLimitPersistence.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/buildcount/domain/playerdata/BuildAmountRateLimitPersistence.scala new file mode 100644 index 0000000000..87a2a1c57b --- /dev/null +++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/buildcount/domain/playerdata/BuildAmountRateLimitPersistence.scala @@ -0,0 +1,8 @@ +package com.github.unchama.seichiassist.subsystems.buildcount.domain.playerdata + +import com.github.unchama.generic.RefDict +import com.github.unchama.seichiassist.subsystems.buildcount.domain.BuildAmountRateLimiterSnapshot + +import java.util.UUID + +trait BuildAmountRateLimitPersistence[F[_]] extends RefDict[F, UUID, BuildAmountRateLimiterSnapshot] diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/buildcount/infrastructure/JdbcBuildAmountRateLimitPersistence.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/buildcount/infrastructure/JdbcBuildAmountRateLimitPersistence.scala new file mode 100644 index 0000000000..9f716094b4 --- /dev/null +++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/buildcount/infrastructure/JdbcBuildAmountRateLimitPersistence.scala @@ -0,0 +1,43 @@ +package com.github.unchama.seichiassist.subsystems.buildcount.infrastructure + +import cats.effect.{ConcurrentEffect, Sync, Timer} +import com.github.unchama.generic.ContextCoercion +import com.github.unchama.seichiassist.subsystems.buildcount.application.Configuration +import com.github.unchama.seichiassist.subsystems.buildcount.domain.BuildAmountRateLimiterSnapshot +import com.github.unchama.seichiassist.subsystems.buildcount.domain.explevel.BuildExpAmount +import com.github.unchama.seichiassist.subsystems.buildcount.domain.playerdata.BuildAmountRateLimitPersistence +import scalikejdbc._ + +import java.util.UUID + +class JdbcBuildAmountRateLimitPersistence[ + SyncContext[_]: ContextCoercion[*[_], ConcurrentContext], + ConcurrentContext[_]: ConcurrentEffect: Timer +](implicit F: Sync[SyncContext], config: Configuration) + extends BuildAmountRateLimitPersistence[SyncContext] { + + override def read(key: UUID): SyncContext[Option[BuildAmountRateLimiterSnapshot]] = + F.delay { + DB.localTx { implicit session => + sql"select available_permission from build_count_rate_limit where uuid = ${key.toString}" + .stripMargin + .map { rs => + val exp = BuildExpAmount(rs.bigDecimal("available_permission")) + val ldt = rs.localDateTime("record_date") + + BuildAmountRateLimiterSnapshot(exp, ldt) + } + .first().apply() + } + } + + override def write(key: UUID, value: BuildAmountRateLimiterSnapshot): SyncContext[Unit] = F.delay { + DB.localTx { implicit session => + sql""" + |update build_count_rate_limit set available_permission = ${value.amount.toPlainString}, record_date = ${value.recordTime} + |where uuid = ${key.toString}""" + .stripMargin + .update().apply() + } + } +} diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/seasonalevents/commands/EventCommand.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/seasonalevents/commands/EventCommand.scala index 73b4409b20..6bdfc81bec 100644 --- a/src/main/scala/com/github/unchama/seichiassist/subsystems/seasonalevents/commands/EventCommand.scala +++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/seasonalevents/commands/EventCommand.scala @@ -7,11 +7,14 @@ import com.github.unchama.seichiassist.subsystems.seasonalevents.anniversary.Ann import com.github.unchama.seichiassist.subsystems.seasonalevents.christmas.ChristmasItemData._ import com.github.unchama.seichiassist.subsystems.seasonalevents.halloween.HalloweenItemData._ import com.github.unchama.seichiassist.subsystems.seasonalevents.newyear.NewYearItemData._ +import com.github.unchama.seichiassist.subsystems.seasonalevents.valentine.ValentineItemData.cookieOf import com.github.unchama.seichiassist.util.Util import com.github.unchama.targetedeffect.TargetedEffect._ import org.bukkit.command.TabExecutor import org.bukkit.entity.Player +import java.util.UUID + class EventCommand(implicit ioOnMainThread: OnMinecraftServerThread[IO]) { import com.github.unchama.targetedeffect._ @@ -46,6 +49,9 @@ class EventCommand(implicit ioOnMainThread: OnMinecraftServerThread[IO]) { anniversaryShovel ) + def valentineGrantEffect: TargetedEffect[Player] = + Util.grantItemStacksEffect(cookieOf("kinton", UUID.fromString("85dd5867-db09-4a2f-bae7-8d38d5a9c547"))) + val executor: TabExecutor = playerCommandBuilder .execution { context => val effect = context.args.yetToBeParsed match { @@ -53,6 +59,7 @@ class EventCommand(implicit ioOnMainThread: OnMinecraftServerThread[IO]) { case "christmas" :: _ => christsmasGrantEffect case "newyear" :: _ => newYearGrantEffect case "halloween" :: _ => halloweenGrantEffect + case "valentine" :: _ => valentineGrantEffect case _ => emptyEffect } diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/seasonalevents/valentine/ValentineItemData.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/seasonalevents/valentine/ValentineItemData.scala index 8d64a249ea..f9e33b3a4d 100644 --- a/src/main/scala/com/github/unchama/seichiassist/subsystems/seasonalevents/valentine/ValentineItemData.scala +++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/seasonalevents/valentine/ValentineItemData.scala @@ -3,7 +3,6 @@ package com.github.unchama.seichiassist.subsystems.seasonalevents.valentine import com.github.unchama.seichiassist.subsystems.seasonalevents.valentine.Valentine.{END_DATE, EVENT_YEAR} import de.tr7zw.itemnbtapi.NBTItem import org.bukkit.ChatColor._ -import org.bukkit.entity.Player import org.bukkit.inventory.ItemStack import org.bukkit.inventory.meta.SkullMeta import org.bukkit.{Bukkit, Material} @@ -66,8 +65,7 @@ object ValentineItemData { //region GiftedCookie -> 棒メニューでもらえるやつ - def cookieOf(player: Player): ItemStack = { - val playerName = player.getName + def cookieOf(playerName: String, playerUuid: UUID): ItemStack = { val loreList = { val header = List( "", @@ -90,7 +88,7 @@ object ValentineItemData { import item._ setByte(NBTTagConstants.typeIdTag, 2.toByte) setObject(NBTTagConstants.expiryDateTag, END_DATE) - setObject(NBTTagConstants.producerUuidTag, player.getUniqueId) + setObject(NBTTagConstants.producerUuidTag, playerUuid) setString(NBTTagConstants.producerNameTag, playerName) } .pipe(_.getItem) @@ -105,19 +103,19 @@ object ValentineItemData { Option(new NBTItem(item).getObject(NBTTagConstants.producerUuidTag, classOf[UUID])) def deathMessages(playerName: String, cookieProducerName: String): Seq[String] = Seq( - s"${playerName}は${cookieProducerName}のチョコレートを食べた!猟奇的な味だった。", - s"$playerName!${cookieProducerName}からのチョコだと思ったかい?ざぁんねんっ!", + s"${playerName}は${cookieProducerName}のチョコチップクッキーを食べた!猟奇的な味だった。", + s"$playerName!${cookieProducerName}からのチョコチップクッキーだと思ったかい?ざぁんねんっ!", s"${playerName}は${cookieProducerName}のプレゼントで鼻血が止まらない!(計画通り)", - s"${playerName}は${cookieProducerName}のチョコレートを頬張ったまま息絶えた!", - s"${playerName}は${cookieProducerName}のチョコにアレが入っているとはを知らずに食べた…", - s"${playerName}は${cookieProducerName}のチョコなんか食ってないであくしろはたらけ", + s"${playerName}は${cookieProducerName}のチョコチップクッキーを頬張ったまま息絶えた!", + s"${playerName}は${cookieProducerName}のチョコチップクッキーにアレが入っているとはを知らずに食べた…", + s"${playerName}は${cookieProducerName}のチョコチップクッキーなんか食ってないであくしろはたらけ", s"${cookieProducerName}は${playerName}に日頃の恨みを晴らした!スッキリ!", s"${cookieProducerName}による${playerName}への痛恨の一撃!ハッピーバレンタインッ!", s"${cookieProducerName}は${playerName}が食べる姿を、満面の笑みで見つめている!", s"${cookieProducerName}は悪くない!${playerName}が悪いんだっ!", s"${cookieProducerName}は${playerName}を討伐した!", s"こうして${cookieProducerName}のイタズラでまた1人${playerName}が社畜となった。", - s"おい聞いたか!${cookieProducerName}が${playerName}にチョコ送ったらしいぞー!" + s"おい聞いたか!${cookieProducerName}が${playerName}にチョコチップクッキー送ったらしいぞー!" ) //endregion diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/seasonalevents/valentine/ValentineListener.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/seasonalevents/valentine/ValentineListener.scala index 95f22ef3a6..ad366bbff5 100644 --- a/src/main/scala/com/github/unchama/seichiassist/subsystems/seasonalevents/valentine/ValentineListener.scala +++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/seasonalevents/valentine/ValentineListener.scala @@ -102,7 +102,7 @@ class ValentineListener[ val effects = if (hasNotJoinedBeforeYet) SequentialEffect( - grantItemStacksEffect(cookieOf(player)), + grantItemStacksEffect(cookieOf(player.getName, playerUuid)), MessageEffect(s"${AQUA}チョコチップクッキーを付与しました。"), FocusedSoundEffect(Sound.BLOCK_ANVIL_PLACE, 1.0f, 1.0f)) else TargetedEffect.emptyEffect diff --git a/src/main/scala/com/github/unchama/seichiassist/task/GiganticBerserkTask.scala b/src/main/scala/com/github/unchama/seichiassist/task/GiganticBerserkTask.scala index 2dfe40790e..dab4b76f82 100644 --- a/src/main/scala/com/github/unchama/seichiassist/task/GiganticBerserkTask.scala +++ b/src/main/scala/com/github/unchama/seichiassist/task/GiganticBerserkTask.scala @@ -10,7 +10,7 @@ import com.github.unchama.seichiassist.util.Util import com.github.unchama.seichiassist.{LevelThresholds, SeichiAssist} import org.bukkit.ChatColor._ import org.bukkit.Sound -import org.bukkit.entity.{LivingEntity, Player} +import org.bukkit.entity.Player import scala.util.Random @@ -20,7 +20,7 @@ class GiganticBerserkTask { : ConcurrentEffect : NonServerThreadContextShift : DiscordNotificationAPI - ](p: Player, e: LivingEntity)(implicit manaApi: ManaApi[IO, SyncIO, Player]): Unit = { + ](p: Player)(implicit manaApi: ManaApi[IO, SyncIO, Player]): Unit = { val player = p val uuid = p.getUniqueId val playerdata = SeichiAssist.playermap(uuid) @@ -54,9 +54,6 @@ class GiganticBerserkTask { //進化待機状態の場合終了 if (playerdata.giganticBerserk.canEvolve) return - // 棘の鎧で倒した場合終了 - if (Util.isEntityKilledByThornsEnchant(e)) return - // stage * level val level = playerdata.giganticBerserk.level val n = (playerdata.giganticBerserk.stage * 10) + level diff --git a/src/test/scala/com/github/unchama/generic/ratelimiting/RateLimiterPermissionPeekSpec.scala b/src/test/scala/com/github/unchama/generic/ratelimiting/RateLimiterPermissionPeekSpec.scala new file mode 100644 index 0000000000..91e476bd3b --- /dev/null +++ b/src/test/scala/com/github/unchama/generic/ratelimiting/RateLimiterPermissionPeekSpec.scala @@ -0,0 +1,71 @@ +package com.github.unchama.generic.ratelimiting + +import cats.effect.{SyncIO, Timer} +import cats.implicits._ +import com.github.unchama.generic.algebra.typeclasses.OrderedMonus +import com.github.unchama.generic.ContextCoercion._ +import com.github.unchama.testutil.concurrent.tests.{ConcurrentEffectTest, TaskDiscreteEventually} +import com.github.unchama.testutil.execution.MonixTestSchedulerTests +import eu.timepit.refined.api.Refined +import eu.timepit.refined.numeric.NonNegative +import eu.timepit.refined.refineV +import eu.timepit.refined.auto._ +import monix.catnap.SchedulerEffect +import monix.eval.Task +import monix.execution.ExecutionModel +import monix.execution.schedulers.TestScheduler +import org.scalatest.matchers.should.Matchers +import org.scalatest.time.Span +import org.scalatest.time.SpanSugar.convertIntToGrainOfTime +import org.scalatest.wordspec.AnyWordSpec +import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks + +class RateLimiterPermissionPeekSpec extends AnyWordSpec + with ScalaCheckPropertyChecks + with Matchers + with TaskDiscreteEventually + with ConcurrentEffectTest + with MonixTestSchedulerTests { + implicit override val patienceConfig: PatienceConfig = PatienceConfig(timeout = 5.seconds, interval = 10.millis) + implicit override val discreteEventuallyConfig: DiscreteEventuallyConfig = DiscreteEventuallyConfig(10000) + + implicit val monixScheduler: TestScheduler = TestScheduler(ExecutionModel.SynchronousExecution) + implicit val monixTimer: Timer[Task] = SchedulerEffect.timer(monixScheduler) + + type Natural = Int Refined NonNegative + + implicit val intOrderedMonus: OrderedMonus[Natural] = new OrderedMonus[Natural] { + override def |-|(x: Natural, y: Natural): Natural = + if (x >= y) refineV[NonNegative](x - y).getOrElse(throw new RuntimeException) + else 0 + + override def empty: Natural = 0 + + override def combine(x: Natural, y: Natural): Natural = + refineV[NonNegative](x + y).getOrElse(throw new RuntimeException) + + override def compare(x: Natural, y: Natural): Int = x.value.compare(y.value) + } + + "RateLimiter" should { + "keep equality of permits with another RateLimiter which has not been peeked" in { + val maxPermits: Natural = 100 + val sleepPeriod = 5.seconds + // any2stringadd!!! :rage: + val period = sleepPeriod plus 5.seconds + val program = for { + rateLimiterA <- FixedWindowRateLimiter.in[Task, Task, Natural](maxPermits, period) + rateLimiterB <- FixedWindowRateLimiter.in[Task, Task, Natural](maxPermits, period) + _ <- rateLimiterA.peekAvailablePermissions + _ <- monixTimer.sleep(sleepPeriod) + peekA <- rateLimiterA.peekAvailablePermissions + peekB <- rateLimiterB.peekAvailablePermissions + } yield { + assert(peekA == peekB) + () + } + + awaitForProgram(runConcurrent(program)(100), sleepPeriod) + } + } +}