From 34387d9779dc3d662fab25ef2773efdc26f5da6f Mon Sep 17 00:00:00 2001
From: KisaragiEffective <kisaragi.effective@gmail.com>
Date: Thu, 1 Apr 2021 18:15:56 +0900
Subject: [PATCH 01/73] =?UTF-8?q?[update]=20subhome=E3=82=92=E3=82=B5?=
 =?UTF-8?q?=E3=83=96=E3=82=B7=E3=82=B9=E3=83=86=E3=83=A0=E3=81=A8=E3=81=97?=
 =?UTF-8?q?=E3=81=A6=E5=88=87=E3=82=8A=E5=87=BA=E3=81=99?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../unchama/seichiassist/SeichiAssist.scala   | 12 ++-
 .../seichiassist/data/player/PlayerData.scala | 34 +-------
 .../seichiassist/data/subhome/SubHome.java    | 24 ------
 .../unchama/seichiassist/menus/HomeMenu.scala | 78 +++++++++--------
 .../seichiassist/menus/TopLevelRouter.scala   |  4 +-
 .../subsystems/subhome/System.scala           | 32 +++++++
 .../bukkit/command}/SubHomeCommand.scala      | 76 ++++++++++++-----
 .../subsystems/subhome/domain/SubHome.scala   | 12 +++
 .../infrastructure/SubHomePersistence.scala   | 84 +++++++++++++++++++
 .../seichiassist/task/PlayerDataLoading.scala | 29 -------
 .../task/PlayerDataSaveTask.scala             | 32 -------
 11 files changed, 243 insertions(+), 174 deletions(-)
 delete mode 100644 src/main/scala/com/github/unchama/seichiassist/data/subhome/SubHome.java
 create mode 100644 src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/System.scala
 rename src/main/scala/com/github/unchama/seichiassist/{commands => subsystems/subhome/bukkit/command}/SubHomeCommand.scala (60%)
 create mode 100644 src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/domain/SubHome.scala
 create mode 100644 src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/infrastructure/SubHomePersistence.scala

diff --git a/src/main/scala/com/github/unchama/seichiassist/SeichiAssist.scala b/src/main/scala/com/github/unchama/seichiassist/SeichiAssist.scala
index cdd40dfcc2..11439b3144 100644
--- a/src/main/scala/com/github/unchama/seichiassist/SeichiAssist.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/SeichiAssist.scala
@@ -54,6 +54,8 @@ import com.github.unchama.seichiassist.subsystems.mana.{ManaApi, ManaReadApi}
 import com.github.unchama.seichiassist.subsystems.managedfly.ManagedFlyApi
 import com.github.unchama.seichiassist.subsystems.present.infrastructure.GlobalPlayerAccessor
 import com.github.unchama.seichiassist.subsystems.seasonalevents.api.SeasonalEventsAPI
+import com.github.unchama.seichiassist.subsystems.subhome.bukkit.command.SubHomeCommand
+import com.github.unchama.seichiassist.subsystems.subhome.infrastructure.SubHomeReadAPI
 import com.github.unchama.seichiassist.task.PlayerDataSaveTask
 import com.github.unchama.seichiassist.task.global._
 import com.github.unchama.util.{ActionStatus, ClassUtils}
@@ -357,6 +359,14 @@ class SeichiAssist extends JavaPlugin() {
     subsystems.present.System.wired
   }
 
+  lazy val subhomeSystem: subhome.System[IO] = {
+    import PluginExecutionContexts.{asyncShift, onMainThread}
+
+    implicit val effectEnvironment: EffectEnvironment = DefaultEffectEnvironment
+    implicit val concurrentEffect: ConcurrentEffect[IO] = IO.ioConcurrentEffect(asyncShift)
+    subhome.System.wired
+  }
+
   //endregion
 
   private implicit val _akkaSystem: ActorSystem = ConfiguredActorSystemProvider("reference.conf").provide()
@@ -457,6 +467,7 @@ class SeichiAssist extends JavaPlugin() {
     implicit val fourDimensionalPocketApi: FourDimensionalPocketApi[IO, Player] = fourDimensionalPocketSystem.api
     implicit val gachaPointApi: GachaPointApi[IO, SyncIO, Player] = gachaPointSystem.api
     implicit val manaApi: ManaApi[IO, SyncIO, Player] = manaSystem.manaApi
+    implicit val subHomeReadApi: SubHomeReadAPI[IO] = subhomeSystem.api
 
     val menuRouter = TopLevelRouter.apply
     import menuRouter.canOpenStickMenu
@@ -486,7 +497,6 @@ class SeichiAssist extends JavaPlugin() {
       "rmp" -> RmpCommand.executor,
       "shareinv" -> ShareInvCommand.executor,
       "halfguard" -> HalfBlockProtectCommand.executor,
-      "subhome" -> SubHomeCommand.executor,
       "gtfever" -> GiganticFeverCommand.executor,
       "minehead" -> new MineHeadCommand().executor,
       "x-transfer" -> RegionOwnerTransferCommand.executor,
diff --git a/src/main/scala/com/github/unchama/seichiassist/data/player/PlayerData.scala b/src/main/scala/com/github/unchama/seichiassist/data/player/PlayerData.scala
index 378c364a49..353810b834 100644
--- a/src/main/scala/com/github/unchama/seichiassist/data/player/PlayerData.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/data/player/PlayerData.scala
@@ -7,9 +7,9 @@ import com.github.unchama.seichiassist._
 import com.github.unchama.seichiassist.achievement.Nicknames
 import com.github.unchama.seichiassist.data.GridTemplate
 import com.github.unchama.seichiassist.data.player.settings.PlayerSettings
-import com.github.unchama.seichiassist.data.subhome.SubHome
 import com.github.unchama.seichiassist.minestack.MineStackUsageHistory
 import com.github.unchama.seichiassist.subsystems.breakcount.domain.level.SeichiStarLevel
+import com.github.unchama.seichiassist.subsystems.subhome.domain.SubHome
 import com.github.unchama.seichiassist.task.VotingFairyTask
 import com.github.unchama.seichiassist.util.Util
 import com.github.unchama.seichiassist.util.Util.DirectionType
@@ -146,8 +146,6 @@ class PlayerData(
   //グリッド式保護関連
   private var claimUnit = ClaimUnit(0, 0, 0, 0)
 
-  def subHomeEntries: Set[(Int, SubHome)] = subHomeMap.toSet
-
   def gridChunkAmount: Int = (claimUnit.ahead + claimUnit.behind + 1) * (claimUnit.right + claimUnit.left + 1)
 
   //オフラインかどうか
@@ -384,36 +382,6 @@ class PlayerData(
     else SeichiAssist.seichiAssistConfig.getDropExplevel(10)
   }
 
-  //サブホームの位置をセットする
-  def setSubHomeLocation(location: Location, subHomeIndex: Int): Unit = {
-    if (subHomeIndex >= 0 && subHomeIndex < SeichiAssist.seichiAssistConfig.getSubHomeMax) {
-      val currentSubHome = this.subHomeMap.get(subHomeIndex)
-      val currentSubHomeName = currentSubHome.map(_.name).orNull
-
-      this.subHomeMap(subHomeIndex) = new SubHome(location, currentSubHomeName)
-    }
-  }
-
-  def setSubHomeName(name: String, subHomeIndex: Int): Unit = {
-    if (subHomeIndex >= 0 && subHomeIndex < SeichiAssist.seichiAssistConfig.getSubHomeMax) {
-      val currentSubHome = this.subHomeMap.getOrElse(subHomeIndex, return)
-
-      this.subHomeMap(subHomeIndex) = new SubHome(currentSubHome.getLocation, name)
-    }
-  }
-
-  // サブホームの位置を読み込む
-  def getSubHomeLocation(subHomeIndex: Int): Option[Location] = {
-    val subHome = this.subHomeMap.get(subHomeIndex)
-    subHome.map(_.getLocation)
-  }
-
-  def getSubHomeName(subHomeIndex: Int): String = {
-    val subHome = this.subHomeMap.get(subHomeIndex)
-    val subHomeName = subHome.map(_.name)
-    subHomeName.getOrElse(s"サブホームポイント${subHomeIndex + 1}")
-  }
-
   def canBreakHalfBlock: Boolean = this.allowBreakingHalfBlocks
 
   def canGridExtend(directionType: DirectionType, world: String): Boolean = {
diff --git a/src/main/scala/com/github/unchama/seichiassist/data/subhome/SubHome.java b/src/main/scala/com/github/unchama/seichiassist/data/subhome/SubHome.java
deleted file mode 100644
index 290c6d1c46..0000000000
--- a/src/main/scala/com/github/unchama/seichiassist/data/subhome/SubHome.java
+++ /dev/null
@@ -1,24 +0,0 @@
-package com.github.unchama.seichiassist.data.subhome;
-
-import org.bukkit.Location;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-
-/**
- * サブホームオブジェクトのクラス
- */
-// TODO Scalaize and make this case class
-public class SubHome {
-    public final @Nullable String name;
-    private final @NotNull Location location;
-
-    public SubHome(@NotNull Location location, @Nullable String name) {
-        this.location = location;
-        this.name = name;
-    }
-
-    public Location getLocation() {
-        // BukkitのLocationはミュータブルなのでコピーして返す必要がある
-        return location.clone();
-    }
-}
diff --git a/src/main/scala/com/github/unchama/seichiassist/menus/HomeMenu.scala b/src/main/scala/com/github/unchama/seichiassist/menus/HomeMenu.scala
index 155b19787e..e5b1bf3985 100644
--- a/src/main/scala/com/github/unchama/seichiassist/menus/HomeMenu.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/menus/HomeMenu.scala
@@ -1,11 +1,13 @@
 package com.github.unchama.seichiassist.menus
 
-import cats.effect.IO
+import cats.effect.{ConcurrentEffect, IO}
 import com.github.unchama.itemstackbuilder.IconItemStackBuilder
 import com.github.unchama.menuinventory.router.CanOpen
 import com.github.unchama.menuinventory.slot.button.Button
 import com.github.unchama.menuinventory.slot.button.action.LeftClickButtonEffect
 import com.github.unchama.menuinventory.{ChestSlotRef, Menu, MenuFrame, MenuSlotLayout}
+import com.github.unchama.seichiassist.subsystems.subhome.domain.SubHome
+import com.github.unchama.seichiassist.subsystems.subhome.infrastructure.SubHomeReadAPI
 import com.github.unchama.seichiassist.{ManagedWorld, SeichiAssist}
 import com.github.unchama.targetedeffect._
 import com.github.unchama.targetedeffect.player.PlayerEffects._
@@ -25,7 +27,8 @@ object HomeMenu extends Menu {
   import com.github.unchama.seichiassist.concurrent.PluginExecutionContexts.onMainThread
   import eu.timepit.refined.auto._
 
-  class Environment(implicit val ioCanOpenConfirmationMenu: IO CanOpen ConfirmationMenu)
+  class Environment(implicit val ioCanOpenConfirmationMenu: IO CanOpen ConfirmationMenu,
+                    val ioCanReadSubHome: SubHomeReadAPI[IO])
 
   /**
    * メニューのサイズとタイトルに関する情報
@@ -62,13 +65,14 @@ object HomeMenu extends Menu {
     }
 
     import cats.implicits._
-
+    import com.github.unchama.seichiassist.concurrent.PluginExecutionContexts.asyncShift
     val dynamicPartComputation = (for {
       subHomeNumber <- 1 to SeichiAssist.seichiAssistConfig.getSubHomeMax
     } yield {
       val column = refineV[Interval.ClosedOpen[0, 9]](subHomeNumber + 1)
+      implicit val ioCanReadSubHome: SubHomeReadAPI[IO] = environment.ioCanReadSubHome
       column match {
-        case Right(value) => ChestSlotRef(1, value) -> setSubHomeNameButton(subHomeNumber)
+        case Right(value) => ChestSlotRef(1, value) -> setSubHomeNameButton[IO](subHomeNumber)
         case Left(_) => throw new RuntimeException("This branch should not be reached.")
       }
     }.sequence)
@@ -154,40 +158,44 @@ object HomeMenu extends Menu {
   }
 
   private case class ButtonComputations(player: Player) {
+    def setSubHomeNameButton[F[_] : SubHomeReadAPI : ConcurrentEffect](subHomeNumber: Int): IO[Button] = {
+      import cats.implicits._
+      val program = for {
+        subhomeOpt <- SubHomeReadAPI[F].get(player.getUniqueId, subHomeNumber - 1)
+      } yield {
+        val lore = subhomeOpt match {
+          case None => List(s"${GRAY}サブホームポイント$subHomeNumber", s"${GRAY}ポイント未設定")
+          case Some(SubHome(location, name)) =>
+            val worldName = ManagedWorld.fromBukkitWorld(location.getWorld).map(_.japaneseName)
+              .getOrElse(location.getWorld.getName)
+            List(
+              s"${GRAY}サブホームポイント${subHomeNumber}は",
+              s"$GRAY$name",
+              s"${GRAY}と名付けられています",
+              s"$DARK_RED${UNDERLINE}クリックで名称変更",
+              s"${DARK_GRAY}command->[/subhome name $subHomeNumber]",
+              s"$GRAY$worldName x:${location.getBlockX} y:${location.getBlockY} z:${location.getBlockZ}"
+            )
+        }
 
-    import player._
-
-    def setSubHomeNameButton(subHomeNumber: Int): IO[Button] = IO {
-      val openerData = SeichiAssist.playermap(getUniqueId)
-      val maybeLocation = openerData.getSubHomeLocation(subHomeNumber - 1)
-      val lore = maybeLocation match {
-        case None => List(s"${GRAY}サブホームポイント$subHomeNumber", s"${GRAY}ポイント未設定")
-        case Some(location) =>
-          val worldName = ManagedWorld.fromBukkitWorld(location.getWorld).map(_.japaneseName)
-            .getOrElse(location.getWorld.getName)
-          List(
-            s"${GRAY}サブホームポイント${subHomeNumber}は",
-            s"$GRAY${openerData.getSubHomeName(subHomeNumber - 1)}",
-            s"${GRAY}と名付けられています",
-            s"$DARK_RED${UNDERLINE}クリックで名称変更",
-            s"${DARK_GRAY}command->[/subhome name $subHomeNumber]",
-            s"$GRAY$worldName x:${location.getBlockX} y:${location.getBlockY} z:${location.getBlockZ}"
-          )
+        Button(
+          new IconItemStackBuilder(Material.PAPER)
+            .title(s"$YELLOW$UNDERLINE${BOLD}サブホームポイント${subHomeNumber}の情報")
+            .lore(lore)
+            .build(),
+          LeftClickButtonEffect {
+            SequentialEffect(
+              FocusedSoundEffect(Sound.BLOCK_STONE_BUTTON_CLICK_ON, 1f, 1f),
+              CommandEffect(s"subhome name $subHomeNumber"),
+              closeInventoryEffect
+            )
+          }
+        )
       }
 
-      Button(
-        new IconItemStackBuilder(Material.PAPER)
-          .title(s"$YELLOW$UNDERLINE${BOLD}サブホームポイント${subHomeNumber}の情報")
-          .lore(lore)
-          .build(),
-        LeftClickButtonEffect {
-          SequentialEffect(
-            FocusedSoundEffect(Sound.BLOCK_STONE_BUTTON_CLICK_ON, 1f, 1f),
-            CommandEffect(s"subhome name $subHomeNumber"),
-            closeInventoryEffect
-          )
-        }
-      )
+      import cats.effect.implicits._
+
+      program.toIO
     }
   }
 
diff --git a/src/main/scala/com/github/unchama/seichiassist/menus/TopLevelRouter.scala b/src/main/scala/com/github/unchama/seichiassist/menus/TopLevelRouter.scala
index c91694e359..f0f19c915d 100644
--- a/src/main/scala/com/github/unchama/seichiassist/menus/TopLevelRouter.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/menus/TopLevelRouter.scala
@@ -18,6 +18,7 @@ import com.github.unchama.seichiassist.subsystems.fourdimensionalpocket.FourDime
 import com.github.unchama.seichiassist.subsystems.gachapoint.GachaPointApi
 import com.github.unchama.seichiassist.subsystems.mana.ManaApi
 import com.github.unchama.seichiassist.subsystems.ranking.RankingApi
+import com.github.unchama.seichiassist.subsystems.subhome.infrastructure.{SubHomeAPI, SubHomeReadAPI}
 import io.chrisdavenport.cats.effect.time.JavaTime
 import org.bukkit.entity.Player
 
@@ -40,7 +41,8 @@ object TopLevelRouter {
             gachaPointApi: GachaPointApi[IO, SyncIO, Player],
             fastDiggingEffectApi: FastDiggingEffectApi[IO, Player],
             fastDiggingSettingsApi: FastDiggingSettingsApi[IO, Player],
-            fourDimensionalPocketApi: FourDimensionalPocketApi[IO, Player]): TopLevelRouter[IO] = new TopLevelRouter[IO] {
+            fourDimensionalPocketApi: FourDimensionalPocketApi[IO, Player],
+            subHomeReadApi: SubHomeReadAPI[IO]): TopLevelRouter[IO] = new TopLevelRouter[IO] {
     implicit lazy val seichiRankingMenuEnv: SeichiRankingMenu.Environment = new SeichiRankingMenu.Environment
     implicit lazy val secondPageEnv: SecondPage.Environment = new SecondPage.Environment
     implicit lazy val mineStackMainMenuEnv: MineStackMainMenu.Environment = new MineStackMainMenu.Environment
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/System.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/System.scala
new file mode 100644
index 0000000000..4e5a4d7c52
--- /dev/null
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/System.scala
@@ -0,0 +1,32 @@
+package com.github.unchama.seichiassist.subsystems.subhome
+
+import cats.effect.ConcurrentEffect
+import com.github.unchama.concurrent.NonServerThreadContextShift
+import com.github.unchama.seichiassist.SeichiAssist.Scopes.globalChatInterceptionScope
+import com.github.unchama.seichiassist.meta.subsystem.Subsystem
+import com.github.unchama.seichiassist.subsystems.subhome.bukkit.command.SubHomeCommand
+import com.github.unchama.seichiassist.subsystems.subhome.infrastructure.{SubHomeAPI, SubHomePersistence, SubHomeReadAPI, SubHomeWriteAPI}
+import org.bukkit.command.TabExecutor
+
+trait System[F[_]] extends Subsystem[F] {
+  def api: SubHomeAPI[F]
+}
+
+object System {
+  def wired[
+    F[_]
+    : ConcurrentEffect
+    : NonServerThreadContextShift
+  ]: System[F] = new System[F] {
+    val persistence = new SubHomePersistence[F]()
+    override def api: SubHomeAPI[F] = persistence
+
+    private implicit val writer: SubHomeWriteAPI[F] = api
+    private implicit val reader: SubHomeReadAPI[F] = api
+
+    override val commands: Map[String, TabExecutor] =
+      Map(
+        "subhome" -> SubHomeCommand.executor
+      )
+  }
+}
diff --git a/src/main/scala/com/github/unchama/seichiassist/commands/SubHomeCommand.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/bukkit/command/SubHomeCommand.scala
similarity index 60%
rename from src/main/scala/com/github/unchama/seichiassist/commands/SubHomeCommand.scala
rename to src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/bukkit/command/SubHomeCommand.scala
index a36260dc2d..34b195e264 100644
--- a/src/main/scala/com/github/unchama/seichiassist/commands/SubHomeCommand.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/bukkit/command/SubHomeCommand.scala
@@ -1,13 +1,19 @@
-package com.github.unchama.seichiassist.commands
+package com.github.unchama.seichiassist.subsystems.subhome.bukkit.command
 
 import cats.data.Kleisli
-import cats.effect.IO
+import cats.effect.{ConcurrentEffect, IO}
+import cats.implicits._
+import cats.effect.implicits._
+
 import com.github.unchama.chatinterceptor.CancellationReason.Overridden
 import com.github.unchama.chatinterceptor.ChatInterceptionScope
+import com.github.unchama.concurrent.NonServerThreadContextShift
 import com.github.unchama.contextualexecutor.builder.Parsers
 import com.github.unchama.contextualexecutor.executors.{BranchedExecutor, EchoExecutor}
 import com.github.unchama.seichiassist.SeichiAssist
 import com.github.unchama.seichiassist.commands.contextual.builder.BuilderTemplates.playerCommandBuilder
+import com.github.unchama.seichiassist.subsystems.subhome.domain.SubHome
+import com.github.unchama.seichiassist.subsystems.subhome.infrastructure.{SubHomeReadAPI, SubHomeWriteAPI}
 import com.github.unchama.targetedeffect.commandsender.MessageEffect
 import org.bukkit.ChatColor._
 import org.bukkit.command.TabExecutor
@@ -36,34 +42,58 @@ object SubHomeCommand {
       ),
       onMissingArguments = printDescriptionExecutor
     )
-  private val warpExecutor = argsAndSenderConfiguredBuilder
+  private def warpExecutor[
+    F[_]
+    : ConcurrentEffect
+    : NonServerThreadContextShift
+    : SubHomeReadAPI
+  ] = argsAndSenderConfiguredBuilder
     .execution { context =>
       val subHomeId = context.args.parsed.head.asInstanceOf[Int]
       val player = context.sender
 
-      val subHomeLocation = SeichiAssist.playermap(player.getUniqueId).getSubHomeLocation(subHomeId - 1)
-      subHomeLocation match {
-        case None => IO(MessageEffect(s"サブホームポイント${subHomeId}が設定されてません"))
-        case Some(location) => IO {
-          player.teleport(location)
-          MessageEffect(s"サブホームポイント${subHomeId}にワープしました")
+      val eff = for {
+        _ <- NonServerThreadContextShift[F].shift
+        subHomeLocation <- SubHomeReadAPI[F].get(player.getUniqueId, subHomeId - 1)
+      } yield {
+        subHomeLocation match {
+          case None => MessageEffect(s"サブホームポイント${subHomeId}が設定されてません")
+          case Some(SubHome(location, _)) =>
+            player.teleport(location)
+            MessageEffect(s"サブホームポイント${subHomeId}にワープしました")
         }
       }
+
+      eff.toIO
     }
     .build()
-  private val setExecutor = argsAndSenderConfiguredBuilder
+  private def setExecutor[
+    F[_]
+    : ConcurrentEffect
+    : NonServerThreadContextShift
+    : SubHomeWriteAPI
+  ] = argsAndSenderConfiguredBuilder
     .execution { context =>
       val subHomeId = context.args.parsed.head.asInstanceOf[Int]
       val player = context.sender
-      val playerData = SeichiAssist.playermap(player.getUniqueId)
 
-      playerData.setSubHomeLocation(player.getLocation, subHomeId - 1)
+      val eff = for {
+        _ <- NonServerThreadContextShift[F].shift
+        _ <- SubHomeWriteAPI[F].updateLocation(player.getUniqueId, subHomeId - 1, player.getLocation)
+      } yield {
+        MessageEffect(s"現在位置をサブホームポイント${subHomeId}に設定しました")
+      }
 
-      IO(MessageEffect(s"現在位置をサブホームポイント${subHomeId}に設定しました"))
+      eff.toIO
     }
     .build()
 
-  private def nameExecutor(implicit scope: ChatInterceptionScope) = argsAndSenderConfiguredBuilder
+  private def nameExecutor[
+    F[_]
+    : ConcurrentEffect
+    : NonServerThreadContextShift
+    : SubHomeWriteAPI
+  ](implicit scope: ChatInterceptionScope) = argsAndSenderConfiguredBuilder
     .execution { context =>
       val subHomeId = context.args.parsed.head.asInstanceOf[Int]
 
@@ -87,15 +117,17 @@ object SubHomeCommand {
             )
           )
 
-        import cats.implicits._
         import com.github.unchama.generic.syntax._
 
         sendInterceptionMessage.followedBy(Kleisli { player =>
-          val playerData = SeichiAssist.playermap(player.getUniqueId)
+          val uuid = player.getUniqueId
 
-          scope.interceptFrom(player.getUniqueId).flatMap {
+          scope.interceptFrom(uuid).flatMap {
             case Left(newName) =>
-              IO { playerData.setSubHomeName(newName, subHomeId - 1) } *>
+              val eff = for {
+                _ <- SubHomeWriteAPI[F].updateName(uuid, subHomeId - 1, newName)
+              } yield {}
+              eff.toIO *>
                 sendCompletionMessage(newName)(player)
             case Right(Overridden) => sendCancellationMessage(player)
             case Right(_) => IO.pure(())
@@ -105,7 +137,13 @@ object SubHomeCommand {
     }
     .build()
 
-  def executor(implicit scope: ChatInterceptionScope): TabExecutor = BranchedExecutor(
+  def executor[
+    F[_]
+    : SubHomeReadAPI
+    : SubHomeWriteAPI
+    : ConcurrentEffect
+    : NonServerThreadContextShift
+  ](implicit scope: ChatInterceptionScope): TabExecutor = BranchedExecutor(
     Map(
       "warp" -> warpExecutor,
       "set" -> setExecutor,
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/domain/SubHome.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/domain/SubHome.scala
new file mode 100644
index 0000000000..368f919948
--- /dev/null
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/domain/SubHome.scala
@@ -0,0 +1,12 @@
+package com.github.unchama.seichiassist.subsystems.subhome.domain
+
+import org.bukkit.Location
+
+/**
+ * サブホームオブジェクトのクラス
+ */
+case class SubHome(location: Location, name: String) {
+  def getLocation = { // BukkitのLocationはミュータブルなのでコピーして返す必要がある
+    location.clone
+  }
+}
\ No newline at end of file
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/infrastructure/SubHomePersistence.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/infrastructure/SubHomePersistence.scala
new file mode 100644
index 0000000000..27a5236bdb
--- /dev/null
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/infrastructure/SubHomePersistence.scala
@@ -0,0 +1,84 @@
+package com.github.unchama.seichiassist.subsystems.subhome.infrastructure
+
+import cats.effect.Sync
+import com.github.unchama.seichiassist.SeichiAssist
+import com.github.unchama.seichiassist.subsystems.subhome.domain.SubHome
+import com.github.unchama.seichiassist.subsystems.subhome.infrastructure.SubHomeAPI.{PlayerIdentifier, SubHomeNumber}
+import org.bukkit.{Bukkit, Location}
+import scalikejdbc._
+
+class SubHomePersistence[F[_]: Sync] extends SubHomeAPI[F] {
+  final val table = "seichiassist.sub_home"
+  val serverId = SeichiAssist.seichiAssistConfig.getServerNum
+  override def get(player: PlayerIdentifier, number: SubHomeNumber): F[Option[SubHome]] = {
+    Sync[F].delay {
+      DB.readOnly { implicit session =>
+        sql"""SELECT id, name, location_x, location_y, location_z, world_name FROM $table
+             WHERE player = ${player.toString} AND server_id = $serverId AND id = $number"""
+          .map(extractSubHome)
+          // もしかすると見つからないかもしれない
+          .first()
+          .apply()
+      }
+    }
+  }
+
+  override def list(player: PlayerIdentifier): F[Map[SubHomeNumber, SubHome]] = Sync[F].delay {
+    DB.readOnly { implicit session =>
+      sql"""SELECT id, name, location_x, location_y, location_z, world_name FROM $table"""
+        .map(rs => {
+          (
+            rs.int("id"),
+            extractSubHome(rs)
+          )
+        })
+        .list()
+        .apply()
+    }.toMap
+  }
+
+  override def updateLocation(player: PlayerIdentifier, number: SubHomeNumber, location: Location): F[Unit] =
+    Sync[F].delay {
+      val x = location.getX.toInt
+      val y = location.getY.toInt
+      val z = location.getZ.toInt
+      val worldName = location.getWorld.getName
+      DB.localTx { implicit session =>
+        // 重複したとき、もとのエントリを残す必要はないので黙って上書きする
+        sql"""insert into $table
+             |(player_uuid,server_id,id,location_x,location_y,location_z,world_name) values
+             |(${player.toString},$serverId,$number,$x,$y,$z,$worldName)
+             |on duplicate key update
+             |location_x = values(location_x),
+             |location_y = values(location_y),
+             |location_z = values(location_z),
+             |world_name = values(world_name)"""
+          .stripMargin('|')
+          .execute()
+          .apply()
+      }
+    }
+
+  override def updateName(player: PlayerIdentifier, number: SubHomeNumber, name: String): F[Unit] =
+    Sync[F].delay {
+      DB.localTx { implicit session =>
+        sql"""insert into $table
+             |(player_uuid,server_id,id,name) values
+             |(${player.toString},$serverId,$number,$name)
+             |on duplicate key update
+             |name = values(name)"""
+          .stripMargin('|')
+          .execute()
+          .apply()
+      }
+    }
+
+  private def extractSubHome(rs: WrappedResultSet): SubHome = {
+    val x = rs.int("location_x")
+    val y = rs.int("location_y")
+    val z = rs.int("location_z")
+    val world = Bukkit.getWorld(rs.string("world_name"))
+    val homeName = rs.string("name")
+    new SubHome(new Location(world, x, y, z), homeName)
+  }
+}
diff --git a/src/main/scala/com/github/unchama/seichiassist/task/PlayerDataLoading.scala b/src/main/scala/com/github/unchama/seichiassist/task/PlayerDataLoading.scala
index c6bc2e4886..d31c62cd64 100644
--- a/src/main/scala/com/github/unchama/seichiassist/task/PlayerDataLoading.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/task/PlayerDataLoading.scala
@@ -51,34 +51,6 @@ object PlayerDataLoading {
       stmt.executeUpdate(loginInfoUpdateCommand)
     }
 
-    def loadSubHomeData(stmt: Statement): Unit = {
-      val subHomeDataQuery = ("select * from "
-        + db + "." + DatabaseConstants.SUB_HOME_TABLENAME + " where "
-        + s"player_uuid = '$stringUuid' and "
-        + "server_id = " + config.getServerNum)
-
-      stmt.executeQuery(subHomeDataQuery).recordIteration { lrs: ResultSet =>
-        import lrs._
-        val subHomeId = getInt("id")
-        val subHomeName = getString("name")
-        val locationX = getInt("location_x")
-        val locationY = getInt("location_y")
-        val locationZ = getInt("location_z")
-        val worldName = getString("world_name")
-
-        val world = Bukkit.getWorld(worldName)
-
-        if (world != null) {
-          val location = new Location(world, locationX.toDouble, locationY.toDouble, locationZ.toDouble)
-
-          playerData.setSubHomeLocation(location, subHomeId)
-          playerData.setSubHomeName(subHomeName, subHomeId)
-        } else {
-          println(s"Resetting $playerName's subhome $subHomeName($subHomeId) in $worldName - world name not found.")
-        }
-      }
-    }
-
     def loadMineStack(stmt: Statement): Unit = {
       val mineStackDataQuery = ("select * from "
         + db + "." + DatabaseConstants.MINESTACK_TABLENAME + " where "
@@ -340,7 +312,6 @@ object PlayerDataLoading {
       updateLoginInfo(newStmt)
       loadGridTemplate(newStmt)
       loadMineStack(newStmt)
-      loadSubHomeData(newStmt)
     }
 
     timer.sendLapTimeMessage(s"$GREEN${playerName}のプレイヤーデータ読込完了")
diff --git a/src/main/scala/com/github/unchama/seichiassist/task/PlayerDataSaveTask.scala b/src/main/scala/com/github/unchama/seichiassist/task/PlayerDataSaveTask.scala
index c90c62a7ef..10dd09854b 100644
--- a/src/main/scala/com/github/unchama/seichiassist/task/PlayerDataSaveTask.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/task/PlayerDataSaveTask.scala
@@ -22,7 +22,6 @@ object PlayerDataSaveTask {
    */
   def savePlayerData[F[_] : Sync](player: Player, playerdata: PlayerData): F[Unit] = {
     val databaseGateway = SeichiAssist.databaseGateway
-    val serverId = SeichiAssist.seichiAssistConfig.getServerNum
 
     def updatePlayerMineStack(stmt: Statement): Unit = {
       val playerUuid = playerdata.uuid.toString
@@ -39,36 +38,6 @@ object PlayerDataSaveTask {
       }
     }
 
-    def updateSubHome(): Unit = {
-      val playerUuid = playerdata.uuid.toString
-      playerdata.subHomeEntries.foreach { case (subHomeId, subHome) =>
-        val subHomeLocation = subHome.getLocation
-
-        val template = ("insert into seichiassist.sub_home"
-          + "(player_uuid,server_id,id,name,location_x,location_y,location_z,world_name) values "
-          + "(?,?,?,?,?,?,?,?) "
-          + "on duplicate key update "
-          + "name = values(name), "
-          + "location_x = values(location_x), "
-          + "location_y = values(location_y), "
-          + "location_z = values(location_z), "
-          + "world_name = values(world_name)")
-
-        Using(databaseGateway.con.prepareStatement(template)) { statement =>
-          statement.setString(1, playerUuid)
-          statement.setInt(2, serverId)
-          statement.setInt(3, subHomeId)
-          statement.setString(4, subHome.name)
-          statement.setInt(5, subHomeLocation.getX.toInt)
-          statement.setInt(6, subHomeLocation.getY.toInt)
-          statement.setInt(7, subHomeLocation.getZ.toInt)
-          statement.setString(8, subHomeLocation.getWorld.getName)
-
-          statement.executeUpdate()
-        }
-      }
-    }
-
     def updateGridTemplate(stmt: Statement): Unit = {
       val playerUuid = playerdata.uuid.toString
 
@@ -209,7 +178,6 @@ object PlayerDataSaveTask {
         updatePlayerDataColumns(localStatement)
         updatePlayerMineStack(localStatement)
         updateGridTemplate(localStatement)
-        updateSubHome()
         ActionStatus.Ok
       } catch {
         case exception: SQLException =>

From 1826c808f92ef3f7ec837c5f896b506786c42a30 Mon Sep 17 00:00:00 2001
From: KisaragiEffective <kisaragi.effective@gmail.com>
Date: Mon, 19 Apr 2021 20:46:20 +0900
Subject: [PATCH 02/73] [fix] compile error

---
 .../seichiassist/menus/TopLevelRouter.scala   |  2 +-
 .../subsystems/subhome/System.scala           |  6 +++---
 .../subsystems/subhome/domain/SubHome.scala   |  4 ++++
 .../infrastructure/SubHomePersistence.scala   | 19 ++++++++++---------
 .../infrastructure/SubHomeReadAPI.scala       | 12 ++++++++++++
 .../infrastructure/SubHomeWriteAPI.scala      | 13 +++++++++++++
 6 files changed, 43 insertions(+), 13 deletions(-)
 create mode 100644 src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/infrastructure/SubHomeReadAPI.scala
 create mode 100644 src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/infrastructure/SubHomeWriteAPI.scala

diff --git a/src/main/scala/com/github/unchama/seichiassist/menus/TopLevelRouter.scala b/src/main/scala/com/github/unchama/seichiassist/menus/TopLevelRouter.scala
index f0f19c915d..53bccd6436 100644
--- a/src/main/scala/com/github/unchama/seichiassist/menus/TopLevelRouter.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/menus/TopLevelRouter.scala
@@ -18,7 +18,7 @@ import com.github.unchama.seichiassist.subsystems.fourdimensionalpocket.FourDime
 import com.github.unchama.seichiassist.subsystems.gachapoint.GachaPointApi
 import com.github.unchama.seichiassist.subsystems.mana.ManaApi
 import com.github.unchama.seichiassist.subsystems.ranking.RankingApi
-import com.github.unchama.seichiassist.subsystems.subhome.infrastructure.{SubHomeAPI, SubHomeReadAPI}
+import com.github.unchama.seichiassist.subsystems.subhome.infrastructure.SubHomeReadAPI
 import io.chrisdavenport.cats.effect.time.JavaTime
 import org.bukkit.entity.Player
 
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/System.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/System.scala
index 4e5a4d7c52..b3e43289b0 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/System.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/System.scala
@@ -5,11 +5,11 @@ import com.github.unchama.concurrent.NonServerThreadContextShift
 import com.github.unchama.seichiassist.SeichiAssist.Scopes.globalChatInterceptionScope
 import com.github.unchama.seichiassist.meta.subsystem.Subsystem
 import com.github.unchama.seichiassist.subsystems.subhome.bukkit.command.SubHomeCommand
-import com.github.unchama.seichiassist.subsystems.subhome.infrastructure.{SubHomeAPI, SubHomePersistence, SubHomeReadAPI, SubHomeWriteAPI}
+import com.github.unchama.seichiassist.subsystems.subhome.infrastructure.{SubHomePersistence, SubHomeReadAPI, SubHomeWriteAPI}
 import org.bukkit.command.TabExecutor
 
 trait System[F[_]] extends Subsystem[F] {
-  def api: SubHomeAPI[F]
+  def api: SubHomeReadAPI[F] with SubHomeWriteAPI[F]
 }
 
 object System {
@@ -19,7 +19,7 @@ object System {
     : NonServerThreadContextShift
   ]: System[F] = new System[F] {
     val persistence = new SubHomePersistence[F]()
-    override def api: SubHomeAPI[F] = persistence
+    override def api: SubHomeReadAPI[F] with SubHomeWriteAPI[F] = persistence
 
     private implicit val writer: SubHomeWriteAPI[F] = api
     private implicit val reader: SubHomeReadAPI[F] = api
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/domain/SubHome.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/domain/SubHome.scala
index 368f919948..924ed2a4d1 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/domain/SubHome.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/domain/SubHome.scala
@@ -9,4 +9,8 @@ case class SubHome(location: Location, name: String) {
   def getLocation = { // BukkitのLocationはミュータブルなのでコピーして返す必要がある
     location.clone
   }
+}
+
+object SubHome {
+  type ID = Int
 }
\ No newline at end of file
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/infrastructure/SubHomePersistence.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/infrastructure/SubHomePersistence.scala
index 27a5236bdb..e904aaa63d 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/infrastructure/SubHomePersistence.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/infrastructure/SubHomePersistence.scala
@@ -3,18 +3,19 @@ package com.github.unchama.seichiassist.subsystems.subhome.infrastructure
 import cats.effect.Sync
 import com.github.unchama.seichiassist.SeichiAssist
 import com.github.unchama.seichiassist.subsystems.subhome.domain.SubHome
-import com.github.unchama.seichiassist.subsystems.subhome.infrastructure.SubHomeAPI.{PlayerIdentifier, SubHomeNumber}
 import org.bukkit.{Bukkit, Location}
 import scalikejdbc._
 
-class SubHomePersistence[F[_]: Sync] extends SubHomeAPI[F] {
+import java.util.UUID
+
+class SubHomePersistence[F[_]: Sync] extends SubHomeReadAPI[F] with SubHomeWriteAPI[F] {
   final val table = "seichiassist.sub_home"
-  val serverId = SeichiAssist.seichiAssistConfig.getServerNum
-  override def get(player: PlayerIdentifier, number: SubHomeNumber): F[Option[SubHome]] = {
+  private val serverId = SeichiAssist.seichiAssistConfig.getServerNum
+  override def get(player: UUID, id: SubHome.ID): F[Option[SubHome]] = {
     Sync[F].delay {
       DB.readOnly { implicit session =>
         sql"""SELECT id, name, location_x, location_y, location_z, world_name FROM $table
-             WHERE player = ${player.toString} AND server_id = $serverId AND id = $number"""
+             WHERE player = ${player.toString} AND server_id = $serverId AND id = $id"""
           .map(extractSubHome)
           // もしかすると見つからないかもしれない
           .first()
@@ -23,7 +24,7 @@ class SubHomePersistence[F[_]: Sync] extends SubHomeAPI[F] {
     }
   }
 
-  override def list(player: PlayerIdentifier): F[Map[SubHomeNumber, SubHome]] = Sync[F].delay {
+  override def list(player: UUID): F[Map[SubHome.ID, SubHome]] = Sync[F].delay {
     DB.readOnly { implicit session =>
       sql"""SELECT id, name, location_x, location_y, location_z, world_name FROM $table"""
         .map(rs => {
@@ -37,7 +38,7 @@ class SubHomePersistence[F[_]: Sync] extends SubHomeAPI[F] {
     }.toMap
   }
 
-  override def updateLocation(player: PlayerIdentifier, number: SubHomeNumber, location: Location): F[Unit] =
+  override def updateLocation(player: UUID, id: SubHome.ID, location: Location): F[Unit] =
     Sync[F].delay {
       val x = location.getX.toInt
       val y = location.getY.toInt
@@ -47,7 +48,7 @@ class SubHomePersistence[F[_]: Sync] extends SubHomeAPI[F] {
         // 重複したとき、もとのエントリを残す必要はないので黙って上書きする
         sql"""insert into $table
              |(player_uuid,server_id,id,location_x,location_y,location_z,world_name) values
-             |(${player.toString},$serverId,$number,$x,$y,$z,$worldName)
+             |(${player.toString},$serverId,$id,$x,$y,$z,$worldName)
              |on duplicate key update
              |location_x = values(location_x),
              |location_y = values(location_y),
@@ -59,7 +60,7 @@ class SubHomePersistence[F[_]: Sync] extends SubHomeAPI[F] {
       }
     }
 
-  override def updateName(player: PlayerIdentifier, number: SubHomeNumber, name: String): F[Unit] =
+  override def updateName(player: UUID, number: SubHome.ID, name: String): F[Unit] =
     Sync[F].delay {
       DB.localTx { implicit session =>
         sql"""insert into $table
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/infrastructure/SubHomeReadAPI.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/infrastructure/SubHomeReadAPI.scala
new file mode 100644
index 0000000000..9da5a95303
--- /dev/null
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/infrastructure/SubHomeReadAPI.scala
@@ -0,0 +1,12 @@
+package com.github.unchama.seichiassist.subsystems.subhome.infrastructure
+
+import com.github.unchama.seichiassist.subsystems.subhome.domain.SubHome
+import simulacrum.typeclass
+
+import java.util.UUID
+
+@typeclass trait SubHomeReadAPI[F[_]] {
+  def get(player: UUID, number: SubHome.ID): F[Option[SubHome]]
+
+  def list(player: UUID): F[Map[SubHome.ID, SubHome]]
+}
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/infrastructure/SubHomeWriteAPI.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/infrastructure/SubHomeWriteAPI.scala
new file mode 100644
index 0000000000..16b1293b25
--- /dev/null
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/infrastructure/SubHomeWriteAPI.scala
@@ -0,0 +1,13 @@
+package com.github.unchama.seichiassist.subsystems.subhome.infrastructure
+
+import com.github.unchama.seichiassist.subsystems.subhome.domain.SubHome
+import org.bukkit.Location
+import simulacrum.typeclass
+
+import java.util.UUID
+
+@typeclass trait SubHomeWriteAPI[F[_]] {
+  def updateLocation(player: UUID, id: SubHome.ID, location: Location): F[Unit]
+
+  def updateName(player: UUID, id: SubHome.ID, name: String): F[Unit]
+}

From d649a39902baaa7beb66b9a0e0e4fea44650218a Mon Sep 17 00:00:00 2001
From: Kory <6561358+kory33@users.noreply.github.com>
Date: Tue, 18 May 2021 16:45:08 +0900
Subject: [PATCH 03/73] =?UTF-8?q?[update]=20API=E3=81=AE=E3=83=91=E3=83=83?=
 =?UTF-8?q?=E3=82=B1=E3=83=BC=E3=82=B8=E3=82=92=E7=A7=BB=E5=8B=95=E3=81=99?=
 =?UTF-8?q?=E3=82=8B?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../scala/com/github/unchama/seichiassist/SeichiAssist.scala  | 2 +-
 .../com/github/unchama/seichiassist/menus/HomeMenu.scala      | 2 +-
 .../github/unchama/seichiassist/menus/TopLevelRouter.scala    | 2 +-
 .../subhome/{infrastructure => }/SubHomeReadAPI.scala         | 2 +-
 .../subhome/{infrastructure => }/SubHomeWriteAPI.scala        | 2 +-
 .../unchama/seichiassist/subsystems/subhome/System.scala      | 2 +-
 .../subsystems/subhome/bukkit/command/SubHomeCommand.scala    | 4 ++--
 .../subhome/infrastructure/SubHomePersistence.scala           | 1 +
 8 files changed, 9 insertions(+), 8 deletions(-)
 rename src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/{infrastructure => }/SubHomeReadAPI.scala (79%)
 rename src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/{infrastructure => }/SubHomeWriteAPI.scala (82%)

diff --git a/src/main/scala/com/github/unchama/seichiassist/SeichiAssist.scala b/src/main/scala/com/github/unchama/seichiassist/SeichiAssist.scala
index 5ca1a3f72f..3ce93a8af7 100644
--- a/src/main/scala/com/github/unchama/seichiassist/SeichiAssist.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/SeichiAssist.scala
@@ -54,8 +54,8 @@ import com.github.unchama.seichiassist.subsystems.mana.{ManaApi, ManaReadApi}
 import com.github.unchama.seichiassist.subsystems.managedfly.ManagedFlyApi
 import com.github.unchama.seichiassist.subsystems.present.infrastructure.GlobalPlayerAccessor
 import com.github.unchama.seichiassist.subsystems.seasonalevents.api.SeasonalEventsAPI
+import com.github.unchama.seichiassist.subsystems.subhome.SubHomeReadAPI
 import com.github.unchama.seichiassist.subsystems.subhome.bukkit.command.SubHomeCommand
-import com.github.unchama.seichiassist.subsystems.subhome.infrastructure.SubHomeReadAPI
 import com.github.unchama.seichiassist.task.PlayerDataSaveTask
 import com.github.unchama.seichiassist.task.global._
 import com.github.unchama.util.{ActionStatus, ClassUtils}
diff --git a/src/main/scala/com/github/unchama/seichiassist/menus/HomeMenu.scala b/src/main/scala/com/github/unchama/seichiassist/menus/HomeMenu.scala
index e5b1bf3985..fc111fb947 100644
--- a/src/main/scala/com/github/unchama/seichiassist/menus/HomeMenu.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/menus/HomeMenu.scala
@@ -6,8 +6,8 @@ import com.github.unchama.menuinventory.router.CanOpen
 import com.github.unchama.menuinventory.slot.button.Button
 import com.github.unchama.menuinventory.slot.button.action.LeftClickButtonEffect
 import com.github.unchama.menuinventory.{ChestSlotRef, Menu, MenuFrame, MenuSlotLayout}
+import com.github.unchama.seichiassist.subsystems.subhome.SubHomeReadAPI
 import com.github.unchama.seichiassist.subsystems.subhome.domain.SubHome
-import com.github.unchama.seichiassist.subsystems.subhome.infrastructure.SubHomeReadAPI
 import com.github.unchama.seichiassist.{ManagedWorld, SeichiAssist}
 import com.github.unchama.targetedeffect._
 import com.github.unchama.targetedeffect.player.PlayerEffects._
diff --git a/src/main/scala/com/github/unchama/seichiassist/menus/TopLevelRouter.scala b/src/main/scala/com/github/unchama/seichiassist/menus/TopLevelRouter.scala
index 53bccd6436..16b4558921 100644
--- a/src/main/scala/com/github/unchama/seichiassist/menus/TopLevelRouter.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/menus/TopLevelRouter.scala
@@ -18,7 +18,7 @@ import com.github.unchama.seichiassist.subsystems.fourdimensionalpocket.FourDime
 import com.github.unchama.seichiassist.subsystems.gachapoint.GachaPointApi
 import com.github.unchama.seichiassist.subsystems.mana.ManaApi
 import com.github.unchama.seichiassist.subsystems.ranking.RankingApi
-import com.github.unchama.seichiassist.subsystems.subhome.infrastructure.SubHomeReadAPI
+import com.github.unchama.seichiassist.subsystems.subhome.SubHomeReadAPI
 import io.chrisdavenport.cats.effect.time.JavaTime
 import org.bukkit.entity.Player
 
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/infrastructure/SubHomeReadAPI.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/SubHomeReadAPI.scala
similarity index 79%
rename from src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/infrastructure/SubHomeReadAPI.scala
rename to src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/SubHomeReadAPI.scala
index 9da5a95303..27b7b9132b 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/infrastructure/SubHomeReadAPI.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/SubHomeReadAPI.scala
@@ -1,4 +1,4 @@
-package com.github.unchama.seichiassist.subsystems.subhome.infrastructure
+package com.github.unchama.seichiassist.subsystems.subhome
 
 import com.github.unchama.seichiassist.subsystems.subhome.domain.SubHome
 import simulacrum.typeclass
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/infrastructure/SubHomeWriteAPI.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/SubHomeWriteAPI.scala
similarity index 82%
rename from src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/infrastructure/SubHomeWriteAPI.scala
rename to src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/SubHomeWriteAPI.scala
index 16b1293b25..8a68551964 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/infrastructure/SubHomeWriteAPI.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/SubHomeWriteAPI.scala
@@ -1,4 +1,4 @@
-package com.github.unchama.seichiassist.subsystems.subhome.infrastructure
+package com.github.unchama.seichiassist.subsystems.subhome
 
 import com.github.unchama.seichiassist.subsystems.subhome.domain.SubHome
 import org.bukkit.Location
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/System.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/System.scala
index b3e43289b0..3f56fa4c2a 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/System.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/System.scala
@@ -5,7 +5,7 @@ import com.github.unchama.concurrent.NonServerThreadContextShift
 import com.github.unchama.seichiassist.SeichiAssist.Scopes.globalChatInterceptionScope
 import com.github.unchama.seichiassist.meta.subsystem.Subsystem
 import com.github.unchama.seichiassist.subsystems.subhome.bukkit.command.SubHomeCommand
-import com.github.unchama.seichiassist.subsystems.subhome.infrastructure.{SubHomePersistence, SubHomeReadAPI, SubHomeWriteAPI}
+import com.github.unchama.seichiassist.subsystems.subhome.infrastructure.SubHomePersistence
 import org.bukkit.command.TabExecutor
 
 trait System[F[_]] extends Subsystem[F] {
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/bukkit/command/SubHomeCommand.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/bukkit/command/SubHomeCommand.scala
index 34b195e264..4ce5c47296 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/bukkit/command/SubHomeCommand.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/bukkit/command/SubHomeCommand.scala
@@ -4,7 +4,6 @@ import cats.data.Kleisli
 import cats.effect.{ConcurrentEffect, IO}
 import cats.implicits._
 import cats.effect.implicits._
-
 import com.github.unchama.chatinterceptor.CancellationReason.Overridden
 import com.github.unchama.chatinterceptor.ChatInterceptionScope
 import com.github.unchama.concurrent.NonServerThreadContextShift
@@ -12,8 +11,9 @@ import com.github.unchama.contextualexecutor.builder.Parsers
 import com.github.unchama.contextualexecutor.executors.{BranchedExecutor, EchoExecutor}
 import com.github.unchama.seichiassist.SeichiAssist
 import com.github.unchama.seichiassist.commands.contextual.builder.BuilderTemplates.playerCommandBuilder
+import com.github.unchama.seichiassist.subsystems.subhome.{SubHomeReadAPI, SubHomeWriteAPI}
 import com.github.unchama.seichiassist.subsystems.subhome.domain.SubHome
-import com.github.unchama.seichiassist.subsystems.subhome.infrastructure.{SubHomeReadAPI, SubHomeWriteAPI}
+import com.github.unchama.seichiassist.subsystems.subhome.infrastructure.SubHomeWriteAPI
 import com.github.unchama.targetedeffect.commandsender.MessageEffect
 import org.bukkit.ChatColor._
 import org.bukkit.command.TabExecutor
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/infrastructure/SubHomePersistence.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/infrastructure/SubHomePersistence.scala
index e904aaa63d..6f3d117dc8 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/infrastructure/SubHomePersistence.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/infrastructure/SubHomePersistence.scala
@@ -2,6 +2,7 @@ package com.github.unchama.seichiassist.subsystems.subhome.infrastructure
 
 import cats.effect.Sync
 import com.github.unchama.seichiassist.SeichiAssist
+import com.github.unchama.seichiassist.subsystems.subhome.{SubHomeReadAPI, SubHomeWriteAPI}
 import com.github.unchama.seichiassist.subsystems.subhome.domain.SubHome
 import org.bukkit.{Bukkit, Location}
 import scalikejdbc._

From be47b19d665143234d55ec185ddf9899ce26acd9 Mon Sep 17 00:00:00 2001
From: Kory <6561358+kory33@users.noreply.github.com>
Date: Tue, 18 May 2021 16:50:06 +0900
Subject: [PATCH 04/73] =?UTF-8?q?[update]=20=E3=83=86=E3=83=BC=E3=83=96?=
 =?UTF-8?q?=E3=83=AB=E5=90=8D=E3=82=92=E5=B1=95=E9=96=8B=E3=81=99=E3=82=8B?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

こうしないと静的チェックが効かない
---
 .../infrastructure/SubHomePersistence.scala        | 14 +++++++-------
 1 file changed, 7 insertions(+), 7 deletions(-)

diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/infrastructure/SubHomePersistence.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/infrastructure/SubHomePersistence.scala
index 6f3d117dc8..185390ecbf 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/infrastructure/SubHomePersistence.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/infrastructure/SubHomePersistence.scala
@@ -10,13 +10,13 @@ import scalikejdbc._
 import java.util.UUID
 
 class SubHomePersistence[F[_]: Sync] extends SubHomeReadAPI[F] with SubHomeWriteAPI[F] {
-  final val table = "seichiassist.sub_home"
   private val serverId = SeichiAssist.seichiAssistConfig.getServerNum
+
   override def get(player: UUID, id: SubHome.ID): F[Option[SubHome]] = {
     Sync[F].delay {
       DB.readOnly { implicit session =>
-        sql"""SELECT id, name, location_x, location_y, location_z, world_name FROM $table
-             WHERE player = ${player.toString} AND server_id = $serverId AND id = $id"""
+        sql"""SELECT id, name, location_x, location_y, location_z, world_name FROM seichiassist.sub_home
+              WHERE name = ${player.toString} AND server_id = $serverId AND id = $id"""
           .map(extractSubHome)
           // もしかすると見つからないかもしれない
           .first()
@@ -27,7 +27,7 @@ class SubHomePersistence[F[_]: Sync] extends SubHomeReadAPI[F] with SubHomeWrite
 
   override def list(player: UUID): F[Map[SubHome.ID, SubHome]] = Sync[F].delay {
     DB.readOnly { implicit session =>
-      sql"""SELECT id, name, location_x, location_y, location_z, world_name FROM $table"""
+      sql"""SELECT id, name, location_x, location_y, location_z, world_name FROM seichiassist.sub_home"""
         .map(rs => {
           (
             rs.int("id"),
@@ -47,7 +47,7 @@ class SubHomePersistence[F[_]: Sync] extends SubHomeReadAPI[F] with SubHomeWrite
       val worldName = location.getWorld.getName
       DB.localTx { implicit session =>
         // 重複したとき、もとのエントリを残す必要はないので黙って上書きする
-        sql"""insert into $table
+        sql"""insert into seichiassist.sub_home
              |(player_uuid,server_id,id,location_x,location_y,location_z,world_name) values
              |(${player.toString},$serverId,$id,$x,$y,$z,$worldName)
              |on duplicate key update
@@ -64,8 +64,8 @@ class SubHomePersistence[F[_]: Sync] extends SubHomeReadAPI[F] with SubHomeWrite
   override def updateName(player: UUID, number: SubHome.ID, name: String): F[Unit] =
     Sync[F].delay {
       DB.localTx { implicit session =>
-        sql"""insert into $table
-             |(player_uuid,server_id,id,name) values
+        sql"""insert into seichiassist.sub_home
+             |(player_uuid, server_id, id, name) values
              |(${player.toString},$serverId,$number,$name)
              |on duplicate key update
              |name = values(name)"""

From f3fd6cc95e396fcc4a6bf50ba56b2413b06ec6cd Mon Sep 17 00:00:00 2001
From: Kory <6561358+kory33@users.noreply.github.com>
Date: Tue, 18 May 2021 16:54:41 +0900
Subject: [PATCH 05/73] =?UTF-8?q?[fix]=20SQL=E3=82=AF=E3=82=A8=E3=83=AA?=
 =?UTF-8?q?=E3=82=92=E3=83=95=E3=82=A9=E3=83=BC=E3=83=9E=E3=83=83=E3=83=88?=
 =?UTF-8?q?+=E4=BF=AE=E6=AD=A3=E3=81=99=E3=82=8B?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../infrastructure/SubHomePersistence.scala   | 24 +++++++++----------
 1 file changed, 12 insertions(+), 12 deletions(-)

diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/infrastructure/SubHomePersistence.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/infrastructure/SubHomePersistence.scala
index 185390ecbf..075e926f83 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/infrastructure/SubHomePersistence.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/infrastructure/SubHomePersistence.scala
@@ -48,13 +48,13 @@ class SubHomePersistence[F[_]: Sync] extends SubHomeReadAPI[F] with SubHomeWrite
       DB.localTx { implicit session =>
         // 重複したとき、もとのエントリを残す必要はないので黙って上書きする
         sql"""insert into seichiassist.sub_home
-             |(player_uuid,server_id,id,location_x,location_y,location_z,world_name) values
-             |(${player.toString},$serverId,$id,$x,$y,$z,$worldName)
-             |on duplicate key update
-             |location_x = values(location_x),
-             |location_y = values(location_y),
-             |location_z = values(location_z),
-             |world_name = values(world_name)"""
+             |(player_uuid, server_id, id, location_x, location_y, location_z, world_name) values
+             |  (${player.toString}, $serverId, $id, $x, $y, $z, $worldName)
+             |    on duplicate key update
+             |      location_x = values(location_x),
+             |      location_y = values(location_y),
+             |      location_z = values(location_z),
+             |      world_name = values(world_name)"""
           .stripMargin('|')
           .execute()
           .apply()
@@ -64,11 +64,11 @@ class SubHomePersistence[F[_]: Sync] extends SubHomeReadAPI[F] with SubHomeWrite
   override def updateName(player: UUID, number: SubHome.ID, name: String): F[Unit] =
     Sync[F].delay {
       DB.localTx { implicit session =>
-        sql"""insert into seichiassist.sub_home
-             |(player_uuid, server_id, id, name) values
-             |(${player.toString},$serverId,$number,$name)
-             |on duplicate key update
-             |name = values(name)"""
+        sql"""update seichiassist.sub_home set name = $name where
+             |  player_uuid = ${player.toString} and
+             |  server_id = $serverId and
+             |  id = $number
+             |"""
           .stripMargin('|')
           .execute()
           .apply()

From f3d1c7256350e528f6808574f284f4ae5b5ad375 Mon Sep 17 00:00:00 2001
From: Kory <6561358+kory33@users.noreply.github.com>
Date: Tue, 18 May 2021 18:11:48 +0900
Subject: [PATCH 06/73] =?UTF-8?q?[update]=20@typeclass=E3=82=A2=E3=83=8E?=
 =?UTF-8?q?=E3=83=86=E3=83=BC=E3=82=B7=E3=83=A7=E3=83=B3=E3=82=92=E5=89=8A?=
 =?UTF-8?q?=E9=99=A4=E3=81=97plain=E3=81=AB=E6=9B=B8=E3=81=8F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../seichiassist/subsystems/subhome/SubHomeReadAPI.scala  | 8 +++++++-
 .../seichiassist/subsystems/subhome/SubHomeWriteAPI.scala | 8 +++++++-
 .../subhome/bukkit/command/SubHomeCommand.scala           | 1 -
 3 files changed, 14 insertions(+), 3 deletions(-)

diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/SubHomeReadAPI.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/SubHomeReadAPI.scala
index 27b7b9132b..5c62b11b2e 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/SubHomeReadAPI.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/SubHomeReadAPI.scala
@@ -5,8 +5,14 @@ import simulacrum.typeclass
 
 import java.util.UUID
 
-@typeclass trait SubHomeReadAPI[F[_]] {
+trait SubHomeReadAPI[F[_]] {
   def get(player: UUID, number: SubHome.ID): F[Option[SubHome]]
 
   def list(player: UUID): F[Map[SubHome.ID, SubHome]]
 }
+
+object SubHomeReadAPI {
+
+  def apply[F[_]](implicit ev: SubHomeReadAPI[F]): SubHomeReadAPI[F] = ev
+
+}
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/SubHomeWriteAPI.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/SubHomeWriteAPI.scala
index 8a68551964..f73bd22702 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/SubHomeWriteAPI.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/SubHomeWriteAPI.scala
@@ -6,8 +6,14 @@ import simulacrum.typeclass
 
 import java.util.UUID
 
-@typeclass trait SubHomeWriteAPI[F[_]] {
+trait SubHomeWriteAPI[F[_]] {
   def updateLocation(player: UUID, id: SubHome.ID, location: Location): F[Unit]
 
   def updateName(player: UUID, id: SubHome.ID, name: String): F[Unit]
 }
+
+object SubHomeWriteAPI {
+
+  def apply[F[_]](implicit ev: SubHomeWriteAPI[F]): SubHomeWriteAPI[F] = ev
+
+}
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/bukkit/command/SubHomeCommand.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/bukkit/command/SubHomeCommand.scala
index 4ce5c47296..c3c7847a6f 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/bukkit/command/SubHomeCommand.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/bukkit/command/SubHomeCommand.scala
@@ -13,7 +13,6 @@ import com.github.unchama.seichiassist.SeichiAssist
 import com.github.unchama.seichiassist.commands.contextual.builder.BuilderTemplates.playerCommandBuilder
 import com.github.unchama.seichiassist.subsystems.subhome.{SubHomeReadAPI, SubHomeWriteAPI}
 import com.github.unchama.seichiassist.subsystems.subhome.domain.SubHome
-import com.github.unchama.seichiassist.subsystems.subhome.infrastructure.SubHomeWriteAPI
 import com.github.unchama.targetedeffect.commandsender.MessageEffect
 import org.bukkit.ChatColor._
 import org.bukkit.command.TabExecutor

From 0594de8a1a1fb59983e83a8471c3243f75671bdc Mon Sep 17 00:00:00 2001
From: Kory <6561358+kory33@users.noreply.github.com>
Date: Tue, 18 May 2021 18:23:59 +0900
Subject: [PATCH 07/73] =?UTF-8?q?[clean]=20=E3=82=B7=E3=82=B9=E3=83=86?=
 =?UTF-8?q?=E3=83=A0=E3=81=AE=E5=88=9D=E6=9C=9F=E5=8C=96=E3=81=AE=E3=82=B3?=
 =?UTF-8?q?=E3=83=BC=E3=83=89=E3=82=92=E5=8D=98=E7=B4=94=E5=8C=96=E3=81=99?=
 =?UTF-8?q?=E3=82=8B?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../subsystems/subhome/SubHomeReadAPI.scala   |  2 ++
 .../subsystems/subhome/SubHomeWriteAPI.scala  |  2 ++
 .../subsystems/subhome/System.scala           | 19 +++++++++----------
 3 files changed, 13 insertions(+), 10 deletions(-)

diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/SubHomeReadAPI.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/SubHomeReadAPI.scala
index 5c62b11b2e..b724b21093 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/SubHomeReadAPI.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/SubHomeReadAPI.scala
@@ -6,9 +6,11 @@ import simulacrum.typeclass
 import java.util.UUID
 
 trait SubHomeReadAPI[F[_]] {
+
   def get(player: UUID, number: SubHome.ID): F[Option[SubHome]]
 
   def list(player: UUID): F[Map[SubHome.ID, SubHome]]
+
 }
 
 object SubHomeReadAPI {
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/SubHomeWriteAPI.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/SubHomeWriteAPI.scala
index f73bd22702..f34b6852e1 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/SubHomeWriteAPI.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/SubHomeWriteAPI.scala
@@ -7,9 +7,11 @@ import simulacrum.typeclass
 import java.util.UUID
 
 trait SubHomeWriteAPI[F[_]] {
+
   def updateLocation(player: UUID, id: SubHome.ID, location: Location): F[Unit]
 
   def updateName(player: UUID, id: SubHome.ID, name: String): F[Unit]
+
 }
 
 object SubHomeWriteAPI {
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/System.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/System.scala
index 3f56fa4c2a..12110f6b47 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/System.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/System.scala
@@ -9,7 +9,7 @@ import com.github.unchama.seichiassist.subsystems.subhome.infrastructure.SubHome
 import org.bukkit.command.TabExecutor
 
 trait System[F[_]] extends Subsystem[F] {
-  def api: SubHomeReadAPI[F] with SubHomeWriteAPI[F]
+  val api: SubHomeReadAPI[F] with SubHomeWriteAPI[F]
 }
 
 object System {
@@ -17,16 +17,15 @@ object System {
     F[_]
     : ConcurrentEffect
     : NonServerThreadContextShift
-  ]: System[F] = new System[F] {
+  ]: System[F] = {
     val persistence = new SubHomePersistence[F]()
-    override def api: SubHomeReadAPI[F] with SubHomeWriteAPI[F] = persistence
 
-    private implicit val writer: SubHomeWriteAPI[F] = api
-    private implicit val reader: SubHomeReadAPI[F] = api
-
-    override val commands: Map[String, TabExecutor] =
-      Map(
-        "subhome" -> SubHomeCommand.executor
-      )
+    new System[F] {
+      override implicit val api: SubHomeReadAPI[F] with SubHomeWriteAPI[F] = persistence
+      override val commands: Map[String, TabExecutor] =
+        Map(
+          "subhome" -> SubHomeCommand.executor
+        )
+    }
   }
 }

From 1c664b52a1a85391959ffbef8ee011e80351ac49 Mon Sep 17 00:00:00 2001
From: Kory <6561358+kory33@users.noreply.github.com>
Date: Wed, 19 May 2021 06:09:45 +0900
Subject: [PATCH 08/73] =?UTF-8?q?[update]=20=E3=83=89=E3=83=A1=E3=82=A4?=
 =?UTF-8?q?=E3=83=B3=E3=81=8CBukkit=E3=81=AB=E4=BE=9D=E5=AD=98=E3=81=97?=
 =?UTF-8?q?=E3=81=AA=E3=81=84=E3=82=88=E3=81=86=E3=81=AB=E3=81=99=E3=82=8B?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../subsystems/subhome/domain/SubHome.scala   |  7 +++
 .../subsystems/subhome/domain/SubHomeId.scala |  3 +
 .../subhome/domain/SubHomePersistence.scala   | 58 +++++++++++++++++++
 3 files changed, 68 insertions(+)
 create mode 100644 src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/domain/SubHomeId.scala
 create mode 100644 src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/domain/SubHomePersistence.scala

diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/domain/SubHome.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/domain/SubHome.scala
index 924ed2a4d1..4755eac944 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/domain/SubHome.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/domain/SubHome.scala
@@ -11,6 +11,13 @@ case class SubHome(location: Location, name: String) {
   }
 }
 
+case class SubHomeLocation(worldName: String, x: Int, y: Int, z: Int)
+
+/**
+ * サブホームオブジェクトのクラス
+ */
+case class SubHomeV2(name: String, location: SubHomeLocation)
+
 object SubHome {
   type ID = Int
 }
\ No newline at end of file
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/domain/SubHomeId.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/domain/SubHomeId.scala
new file mode 100644
index 0000000000..1f3d9232e9
--- /dev/null
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/domain/SubHomeId.scala
@@ -0,0 +1,3 @@
+package com.github.unchama.seichiassist.subsystems.subhome.domain
+
+case class SubHomeId(value: Int) extends AnyVal
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/domain/SubHomePersistence.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/domain/SubHomePersistence.scala
new file mode 100644
index 0000000000..8f259f9e94
--- /dev/null
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/domain/SubHomePersistence.scala
@@ -0,0 +1,58 @@
+package com.github.unchama.seichiassist.subsystems.subhome.domain
+
+import cats.{Functor, Monad}
+
+import java.util.UUID
+
+/**
+ * サブホームの永続化された情報。
+ *
+ * この情報はサーバー間で共有されることを想定していない。
+ * 例えばサーバー(s1, s2)、プレーヤーのUUID(u)があった時、s1でlist(u)をした結果とs2でlist(u)をした結果は一般に異なる。
+ * これは、サブホームをサーバー間で共有しないという仕様に基づくものである。
+ */
+trait SubHomePersistence[F[_]] {
+
+  import cats.implicits._
+
+  /**
+   * 指定したUUIDのプレーヤーが現在のサーバーにて設定しているすべてのサブホームを取得する。
+   */
+  def list(ownerUuid: UUID): F[Map[SubHomeId, SubHomeV2]]
+
+  /**
+   * サブホームを登録する。idの範囲などのバリデーションはしない。
+   *
+   * すでにサブホームが指定されたidで存在した場合、サブホームを上書きする。
+   */
+  def upsert(ownerUuid: UUID, id: SubHomeId)(subHome: SubHomeV2): F[Unit]
+
+  /**
+   * 所有者のUUIDとサブホームのIDから単一のサブホームを取得する。
+   */
+  final def get(ownerUuid: UUID, id: SubHomeId)(implicit F: Functor[F]): F[Option[SubHomeV2]] =
+    list(ownerUuid).map(_.get(id))
+
+  /**
+   * 指定されたサブホームをnon-atomicに更新する。存在しないサブホームが指定された場合何も行わない。
+   *
+   * 作用の結果として更新が行われたかどうかを示すBooleanを返す。
+   */
+  final def alter(ownerUuid: UUID, id: SubHomeId)(f: SubHomeV2 => SubHomeV2)(implicit F: Monad[F]): F[Boolean] =
+    for {
+      current <- get(ownerUuid, id)
+      _ <- current match {
+        case Some(currentSubHome) => upsert(ownerUuid, id)(f(currentSubHome))
+        case None => F.unit
+      }
+    } yield current.nonEmpty
+
+  /**
+   * 指定されたサブホームをnon-atomicにリネームする。存在しないサブホームが指定された場合何も行わない。
+   *
+   * 作用の結果として更新が行われたかどうかを示すBooleanを返す。
+   */
+  final def rename(ownerUuid: UUID, id: SubHomeId)(newName: String)(implicit F: Monad[F]): F[Boolean] =
+    alter(ownerUuid, id)(_.copy(name = newName))
+
+}

From 36d25e210dca362601a0ec88711571e3489c5531 Mon Sep 17 00:00:00 2001
From: Kory <6561358+kory33@users.noreply.github.com>
Date: Wed, 19 May 2021 06:10:11 +0900
Subject: [PATCH 09/73] =?UTF-8?q?[update]=20SubHomePersistence=E3=81=AE?=
 =?UTF-8?q?=E5=AE=9F=E8=A3=85=E3=82=92=E8=BF=BD=E5=8A=A0=E3=81=99=E3=82=8B?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../JdbcSubHomePersistence.scala              | 54 +++++++++++++++++++
 1 file changed, 54 insertions(+)
 create mode 100644 src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/infrastructure/JdbcSubHomePersistence.scala

diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/infrastructure/JdbcSubHomePersistence.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/infrastructure/JdbcSubHomePersistence.scala
new file mode 100644
index 0000000000..12d24396bf
--- /dev/null
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/infrastructure/JdbcSubHomePersistence.scala
@@ -0,0 +1,54 @@
+package com.github.unchama.seichiassist.subsystems.subhome.infrastructure
+
+import cats.effect.Sync
+import com.github.unchama.seichiassist.SeichiAssist
+import com.github.unchama.seichiassist.subsystems.subhome.domain.{SubHome, SubHomeId, SubHomeLocation, SubHomePersistence, SubHomeV2}
+import com.github.unchama.seichiassist.subsystems.subhome.{SubHomeReadAPI, SubHomeWriteAPI}
+import com.github.unchama.targetedeffect.player
+import org.bukkit.{Bukkit, Location}
+import scalikejdbc._
+
+import java.util.UUID
+
+class JdbcSubHomePersistence[F[_]: Sync] extends SubHomePersistence[F] {
+  private val serverId = SeichiAssist.seichiAssistConfig.getServerNum
+
+  override def upsert(ownerUuid: UUID, id: SubHomeId)(subHome: SubHomeV2): F[Unit] = {
+    Sync[F].delay {
+      DB.readOnly { implicit session =>
+        val SubHomeLocation(worldName, x, y, z) = subHome.location
+
+        sql"""insert into seichiassist.sub_home
+             |(player_uuid, server_id, id, name, location_x, location_y, location_z, world_name) values
+             |  (${ownerUuid.toString}, $serverId, $id, ${subHome.name}, $x, $y, $z, $worldName)
+             |    on duplicate key update
+             |      name = values(name),
+             |      location_x = values(location_x),
+             |      location_y = values(location_y),
+             |      location_z = values(location_z),
+             |      world_name = values(world_name)"""
+          .update()
+          .apply()
+      }
+    }
+  }
+
+  override def list(ownerUuid: UUID): F[Map[SubHomeId, SubHomeV2]] = Sync[F].delay {
+    DB.readOnly { implicit session =>
+      sql"""SELECT id, name, location_x, location_y, location_z, world_name FROM seichiassist.sub_home"""
+        .map(rs => (SubHomeId(rs.int("id")), extractSubHome(rs)))
+        .list().apply()
+    }.toMap
+  }
+
+  private def extractSubHome(rs: WrappedResultSet): SubHomeV2 =
+    SubHomeV2(
+      rs.string("name"),
+      SubHomeLocation(
+        rs.string("world_name"),
+        rs.int("location_x"),
+        rs.int("location_y"),
+        rs.int("location_z")
+      )
+    )
+}

From f88d904bcc4f30039930eef2272385cef0873427 Mon Sep 17 00:00:00 2001
From: Kory <6561358+kory33@users.noreply.github.com>
Date: Wed, 19 May 2021 07:10:48 +0900
Subject: [PATCH 10/73] =?UTF-8?q?[update]=20=E6=96=B0=E3=81=97=E3=81=84Sub?=
 =?UTF-8?q?Home=E3=81=AE=E3=83=A2=E3=83=87=E3=83=AB=E3=81=A7=E5=AE=9F?=
 =?UTF-8?q?=E8=A3=85=E3=82=92=E7=BD=AE=E3=81=8D=E6=8F=9B=E3=81=88=E3=82=8B?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../seichiassist/data/player/PlayerData.scala |  2 -
 .../unchama/seichiassist/menus/HomeMenu.scala | 20 +++--
 .../subsystems/subhome/SubHomeAPI.scala       | 36 ++++++++
 .../subsystems/subhome/SubHomeReadAPI.scala   | 20 -----
 .../subsystems/subhome/SubHomeWriteAPI.scala  | 21 -----
 .../subsystems/subhome/System.scala           | 21 ++++-
 .../bukkit/command/SubHomeCommand.scala       | 22 +++--
 .../subsystems/subhome/domain/SubHome.scala   | 17 +---
 .../subsystems/subhome/domain/SubHomeId.scala |  6 +-
 .../subhome/domain/SubHomePersistence.scala   |  8 +-
 .../JdbcSubHomePersistence.scala              | 19 ++--
 .../infrastructure/SubHomePersistence.scala   | 86 -------------------
 12 files changed, 99 insertions(+), 179 deletions(-)
 create mode 100644 src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/SubHomeAPI.scala
 delete mode 100644 src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/SubHomeReadAPI.scala
 delete mode 100644 src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/SubHomeWriteAPI.scala
 delete mode 100644 src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/infrastructure/SubHomePersistence.scala

diff --git a/src/main/scala/com/github/unchama/seichiassist/data/player/PlayerData.scala b/src/main/scala/com/github/unchama/seichiassist/data/player/PlayerData.scala
index 353810b834..34b550bb9c 100644
--- a/src/main/scala/com/github/unchama/seichiassist/data/player/PlayerData.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/data/player/PlayerData.scala
@@ -9,7 +9,6 @@ import com.github.unchama.seichiassist.data.GridTemplate
 import com.github.unchama.seichiassist.data.player.settings.PlayerSettings
 import com.github.unchama.seichiassist.minestack.MineStackUsageHistory
 import com.github.unchama.seichiassist.subsystems.breakcount.domain.level.SeichiStarLevel
-import com.github.unchama.seichiassist.subsystems.subhome.domain.SubHome
 import com.github.unchama.seichiassist.task.VotingFairyTask
 import com.github.unchama.seichiassist.util.Util
 import com.github.unchama.seichiassist.util.Util.DirectionType
@@ -52,7 +51,6 @@ class PlayerData(
   //プレイヤー名
   val lowercaseName: String = name.toLowerCase()
 
-  private val subHomeMap: mutable.Map[Int, SubHome] = mutable.HashMap[Int, SubHome]()
   private val dummyDate = new GregorianCalendar(2100, 1, 1, 0, 0, 0)
   //チェスト破壊トグル
   var chestflag = true
diff --git a/src/main/scala/com/github/unchama/seichiassist/menus/HomeMenu.scala b/src/main/scala/com/github/unchama/seichiassist/menus/HomeMenu.scala
index fc111fb947..dffb4eaa35 100644
--- a/src/main/scala/com/github/unchama/seichiassist/menus/HomeMenu.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/menus/HomeMenu.scala
@@ -7,7 +7,7 @@ import com.github.unchama.menuinventory.slot.button.Button
 import com.github.unchama.menuinventory.slot.button.action.LeftClickButtonEffect
 import com.github.unchama.menuinventory.{ChestSlotRef, Menu, MenuFrame, MenuSlotLayout}
 import com.github.unchama.seichiassist.subsystems.subhome.SubHomeReadAPI
-import com.github.unchama.seichiassist.subsystems.subhome.domain.SubHome
+import com.github.unchama.seichiassist.subsystems.subhome.domain.{SubHomeId, SubHome}
 import com.github.unchama.seichiassist.{ManagedWorld, SeichiAssist}
 import com.github.unchama.targetedeffect._
 import com.github.unchama.targetedeffect.player.PlayerEffects._
@@ -160,20 +160,26 @@ object HomeMenu extends Menu {
   private case class ButtonComputations(player: Player) {
     def setSubHomeNameButton[F[_] : SubHomeReadAPI : ConcurrentEffect](subHomeNumber: Int): IO[Button] = {
       import cats.implicits._
+
+      val subHomeId = SubHomeId(subHomeNumber)
+
       val program = for {
-        subhomeOpt <- SubHomeReadAPI[F].get(player.getUniqueId, subHomeNumber - 1)
+        subhomeOpt <- SubHomeReadAPI[F].get(player.getUniqueId, subHomeId)
       } yield {
         val lore = subhomeOpt match {
-          case None => List(s"${GRAY}サブホームポイント$subHomeNumber", s"${GRAY}ポイント未設定")
+          case None => List(s"${GRAY}サブホームポイント$subHomeId", s"${GRAY}ポイント未設定")
           case Some(SubHome(location, name)) =>
-            val worldName = ManagedWorld.fromBukkitWorld(location.getWorld).map(_.japaneseName)
-              .getOrElse(location.getWorld.getName)
+            val worldName =
+              ManagedWorld
+                .fromBukkitWorld(location.getWorld).map(_.japaneseName)
+                .getOrElse(location.getWorld.getName)
+
             List(
-              s"${GRAY}サブホームポイント${subHomeNumber}は",
+              s"${GRAY}サブホームポイント${subHomeId}は",
               s"$GRAY$name",
               s"${GRAY}と名付けられています",
               s"$DARK_RED${UNDERLINE}クリックで名称変更",
-              s"${DARK_GRAY}command->[/subhome name $subHomeNumber]",
+              s"${DARK_GRAY}command->[/subhome name $subHomeId]",
               s"$GRAY$worldName x:${location.getBlockX} y:${location.getBlockY} z:${location.getBlockZ}"
             )
         }
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/SubHomeAPI.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/SubHomeAPI.scala
new file mode 100644
index 0000000000..793d38fcf2
--- /dev/null
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/SubHomeAPI.scala
@@ -0,0 +1,36 @@
+package com.github.unchama.seichiassist.subsystems.subhome
+
+import com.github.unchama.seichiassist.subsystems.subhome.domain.{SubHomeId, SubHome}
+import org.bukkit.Location
+
+import java.util.UUID
+
+trait SubHomeReadAPI[F[_]] {
+
+  def list(ownerUuid: UUID): F[Map[SubHomeId, SubHome]]
+
+  def get(ownerUuid: UUID, id: SubHomeId): F[Option[SubHome]]
+
+}
+
+object SubHomeReadAPI {
+
+  def apply[F[_]](implicit ev: SubHomeReadAPI[F]): SubHomeReadAPI[F] = ev
+
+}
+
+trait SubHomeWriteAPI[F[_]] {
+
+  def updateLocation(ownerUuid: UUID, id: SubHomeId, location: Location): F[Unit]
+
+  def updateName(ownerUuid: UUID, id: SubHomeId, name: String): F[Unit]
+
+}
+
+object SubHomeWriteAPI {
+
+  def apply[F[_]](implicit ev: SubHomeWriteAPI[F]): SubHomeWriteAPI[F] = ev
+
+}
+
+trait SubHomeAPI[F[_]] extends SubHomeReadAPI[F] with SubHomeWriteAPI[F]
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/SubHomeReadAPI.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/SubHomeReadAPI.scala
deleted file mode 100644
index b724b21093..0000000000
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/SubHomeReadAPI.scala
+++ /dev/null
@@ -1,20 +0,0 @@
-package com.github.unchama.seichiassist.subsystems.subhome
-
-import com.github.unchama.seichiassist.subsystems.subhome.domain.SubHome
-import simulacrum.typeclass
-
-import java.util.UUID
-
-trait SubHomeReadAPI[F[_]] {
-
-  def get(player: UUID, number: SubHome.ID): F[Option[SubHome]]
-
-  def list(player: UUID): F[Map[SubHome.ID, SubHome]]
-
-}
-
-object SubHomeReadAPI {
-
-  def apply[F[_]](implicit ev: SubHomeReadAPI[F]): SubHomeReadAPI[F] = ev
-
-}
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/SubHomeWriteAPI.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/SubHomeWriteAPI.scala
deleted file mode 100644
index f34b6852e1..0000000000
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/SubHomeWriteAPI.scala
+++ /dev/null
@@ -1,21 +0,0 @@
-package com.github.unchama.seichiassist.subsystems.subhome
-
-import com.github.unchama.seichiassist.subsystems.subhome.domain.SubHome
-import org.bukkit.Location
-import simulacrum.typeclass
-
-import java.util.UUID
-
-trait SubHomeWriteAPI[F[_]] {
-
-  def updateLocation(player: UUID, id: SubHome.ID, location: Location): F[Unit]
-
-  def updateName(player: UUID, id: SubHome.ID, name: String): F[Unit]
-
-}
-
-object SubHomeWriteAPI {
-
-  def apply[F[_]](implicit ev: SubHomeWriteAPI[F]): SubHomeWriteAPI[F] = ev
-
-}
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/System.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/System.scala
index 12110f6b47..94c0e2e62e 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/System.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/System.scala
@@ -5,9 +5,13 @@ import com.github.unchama.concurrent.NonServerThreadContextShift
 import com.github.unchama.seichiassist.SeichiAssist.Scopes.globalChatInterceptionScope
 import com.github.unchama.seichiassist.meta.subsystem.Subsystem
 import com.github.unchama.seichiassist.subsystems.subhome.bukkit.command.SubHomeCommand
-import com.github.unchama.seichiassist.subsystems.subhome.infrastructure.SubHomePersistence
+import com.github.unchama.seichiassist.subsystems.subhome.domain.{SubHomeId, SubHome}
+import com.github.unchama.seichiassist.subsystems.subhome.infrastructure.JdbcSubHomePersistence
+import org.bukkit.Location
 import org.bukkit.command.TabExecutor
 
+import java.util.UUID
+
 trait System[F[_]] extends Subsystem[F] {
   val api: SubHomeReadAPI[F] with SubHomeWriteAPI[F]
 }
@@ -18,10 +22,21 @@ object System {
     : ConcurrentEffect
     : NonServerThreadContextShift
   ]: System[F] = {
-    val persistence = new SubHomePersistence[F]()
+    import cats.implicits._
+
+    val persistence = new JdbcSubHomePersistence[F]()
 
     new System[F] {
-      override implicit val api: SubHomeReadAPI[F] with SubHomeWriteAPI[F] = persistence
+      override implicit val api: SubHomeAPI[F] = new SubHomeAPI[F] {
+        override def updateLocation(ownerUuid: UUID, id: SubHomeId, location: Location): F[Unit] =
+          ??? // TODO semantics not clear
+        override def updateName(ownerUuid: UUID, id: SubHomeId, name: String): F[Unit] =
+          persistence.rename(ownerUuid, id)(name).as(())
+        override def get(ownerUuid: UUID, id: SubHomeId): F[Option[SubHome]] =
+          persistence.get(ownerUuid, id)
+        override def list(ownerUuid: UUID): F[Map[SubHomeId, SubHome]] =
+          persistence.list(ownerUuid)
+      }
       override val commands: Map[String, TabExecutor] =
         Map(
           "subhome" -> SubHomeCommand.executor
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/bukkit/command/SubHomeCommand.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/bukkit/command/SubHomeCommand.scala
index c3c7847a6f..354e7215f5 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/bukkit/command/SubHomeCommand.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/bukkit/command/SubHomeCommand.scala
@@ -1,9 +1,9 @@
 package com.github.unchama.seichiassist.subsystems.subhome.bukkit.command
 
 import cats.data.Kleisli
+import cats.effect.implicits._
 import cats.effect.{ConcurrentEffect, IO}
 import cats.implicits._
-import cats.effect.implicits._
 import com.github.unchama.chatinterceptor.CancellationReason.Overridden
 import com.github.unchama.chatinterceptor.ChatInterceptionScope
 import com.github.unchama.concurrent.NonServerThreadContextShift
@@ -11,8 +11,8 @@ import com.github.unchama.contextualexecutor.builder.Parsers
 import com.github.unchama.contextualexecutor.executors.{BranchedExecutor, EchoExecutor}
 import com.github.unchama.seichiassist.SeichiAssist
 import com.github.unchama.seichiassist.commands.contextual.builder.BuilderTemplates.playerCommandBuilder
+import com.github.unchama.seichiassist.subsystems.subhome.domain.{SubHomeId, SubHome}
 import com.github.unchama.seichiassist.subsystems.subhome.{SubHomeReadAPI, SubHomeWriteAPI}
-import com.github.unchama.seichiassist.subsystems.subhome.domain.SubHome
 import com.github.unchama.targetedeffect.commandsender.MessageEffect
 import org.bukkit.ChatColor._
 import org.bukkit.command.TabExecutor
@@ -31,7 +31,9 @@ object SubHomeCommand {
       )
     )
   )
+
   private val subHomeMax = SeichiAssist.seichiAssistConfig.getSubHomeMax
+
   private val argsAndSenderConfiguredBuilder = playerCommandBuilder
     .argumentsParsers(
       List(
@@ -41,6 +43,7 @@ object SubHomeCommand {
       ),
       onMissingArguments = printDescriptionExecutor
     )
+
   private def warpExecutor[
     F[_]
     : ConcurrentEffect
@@ -48,17 +51,17 @@ object SubHomeCommand {
     : SubHomeReadAPI
   ] = argsAndSenderConfiguredBuilder
     .execution { context =>
-      val subHomeId = context.args.parsed.head.asInstanceOf[Int]
+      val subHomeId = SubHomeId(context.args.parsed.head.asInstanceOf[Int])
       val player = context.sender
 
       val eff = for {
         _ <- NonServerThreadContextShift[F].shift
-        subHomeLocation <- SubHomeReadAPI[F].get(player.getUniqueId, subHomeId - 1)
+        subHomeLocation <- SubHomeReadAPI[F].get(player.getUniqueId, subHomeId)
       } yield {
         subHomeLocation match {
           case None => MessageEffect(s"サブホームポイント${subHomeId}が設定されてません")
           case Some(SubHome(location, _)) =>
-            player.teleport(location)
+            player.teleport(location) // TODO これは副作用
             MessageEffect(s"サブホームポイント${subHomeId}にワープしました")
         }
       }
@@ -66,6 +69,7 @@ object SubHomeCommand {
       eff.toIO
     }
     .build()
+
   private def setExecutor[
     F[_]
     : ConcurrentEffect
@@ -73,12 +77,12 @@ object SubHomeCommand {
     : SubHomeWriteAPI
   ] = argsAndSenderConfiguredBuilder
     .execution { context =>
-      val subHomeId = context.args.parsed.head.asInstanceOf[Int]
+      val subHomeId = SubHomeId(context.args.parsed.head.asInstanceOf[Int])
       val player = context.sender
 
       val eff = for {
         _ <- NonServerThreadContextShift[F].shift
-        _ <- SubHomeWriteAPI[F].updateLocation(player.getUniqueId, subHomeId - 1, player.getLocation)
+        _ <- SubHomeWriteAPI[F].updateLocation(player.getUniqueId, subHomeId, player.getLocation)
       } yield {
         MessageEffect(s"現在位置をサブホームポイント${subHomeId}に設定しました")
       }
@@ -94,7 +98,7 @@ object SubHomeCommand {
     : SubHomeWriteAPI
   ](implicit scope: ChatInterceptionScope) = argsAndSenderConfiguredBuilder
     .execution { context =>
-      val subHomeId = context.args.parsed.head.asInstanceOf[Int]
+      val subHomeId = SubHomeId(context.args.parsed.head.asInstanceOf[Int])
 
       IO.pure {
         val sendInterceptionMessage =
@@ -124,7 +128,7 @@ object SubHomeCommand {
           scope.interceptFrom(uuid).flatMap {
             case Left(newName) =>
               val eff = for {
-                _ <- SubHomeWriteAPI[F].updateName(uuid, subHomeId - 1, newName)
+                _ <- SubHomeWriteAPI[F].updateName(uuid, subHomeId, newName)
               } yield {}
               eff.toIO *>
                 sendCompletionMessage(newName)(player)
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/domain/SubHome.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/domain/SubHome.scala
index 4755eac944..f2bae749e5 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/domain/SubHome.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/domain/SubHome.scala
@@ -1,23 +1,8 @@
 package com.github.unchama.seichiassist.subsystems.subhome.domain
 
-import org.bukkit.Location
-
-/**
- * サブホームオブジェクトのクラス
- */
-case class SubHome(location: Location, name: String) {
-  def getLocation = { // BukkitのLocationはミュータブルなのでコピーして返す必要がある
-    location.clone
-  }
-}
-
 case class SubHomeLocation(worldName: String, x: Int, y: Int, z: Int)
 
 /**
  * サブホームオブジェクトのクラス
  */
-case class SubHomeV2(name: String, location: SubHomeLocation)
-
-object SubHome {
-  type ID = Int
-}
\ No newline at end of file
+case class SubHome(name: String, location: SubHomeLocation)
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/domain/SubHomeId.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/domain/SubHomeId.scala
index 1f3d9232e9..7fa0cacdc5 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/domain/SubHomeId.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/domain/SubHomeId.scala
@@ -1,3 +1,7 @@
 package com.github.unchama.seichiassist.subsystems.subhome.domain
 
-case class SubHomeId(value: Int) extends AnyVal
+case class SubHomeId(value: Int) extends AnyVal {
+
+  override def toString: String = value.toString
+
+}
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/domain/SubHomePersistence.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/domain/SubHomePersistence.scala
index 8f259f9e94..81a26eef25 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/domain/SubHomePersistence.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/domain/SubHomePersistence.scala
@@ -18,19 +18,19 @@ trait SubHomePersistence[F[_]] {
   /**
    * 指定したUUIDのプレーヤーが現在のサーバーにて設定しているすべてのサブホームを取得する。
    */
-  def list(ownerUuid: UUID): F[Map[SubHomeId, SubHomeV2]]
+  def list(ownerUuid: UUID): F[Map[SubHomeId, SubHome]]
 
   /**
    * サブホームを登録する。idの範囲などのバリデーションはしない。
    *
    * すでにサブホームが指定されたidで存在した場合、サブホームを上書きする。
    */
-  def upsert(ownerUuid: UUID, id: SubHomeId)(subHome: SubHomeV2): F[Unit]
+  def upsert(ownerUuid: UUID, id: SubHomeId)(subHome: SubHome): F[Unit]
 
   /**
    * 所有者のUUIDとサブホームのIDから単一のサブホームを取得する。
    */
-  final def get(ownerUuid: UUID, id: SubHomeId)(implicit F: Functor[F]): F[Option[SubHomeV2]] =
+  final def get(ownerUuid: UUID, id: SubHomeId)(implicit F: Functor[F]): F[Option[SubHome]] =
     list(ownerUuid).map(_.get(id))
 
   /**
@@ -38,7 +38,7 @@ trait SubHomePersistence[F[_]] {
    *
    * 作用の結果として更新が行われたかどうかを示すBooleanを返す。
    */
-  final def alter(ownerUuid: UUID, id: SubHomeId)(f: SubHomeV2 => SubHomeV2)(implicit F: Monad[F]): F[Boolean] =
+  final def alter(ownerUuid: UUID, id: SubHomeId)(f: SubHome => SubHome)(implicit F: Monad[F]): F[Boolean] =
     for {
       current <- get(ownerUuid, id)
       _ <- current match {
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/infrastructure/JdbcSubHomePersistence.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/infrastructure/JdbcSubHomePersistence.scala
index 12d24396bf..79421f78ab 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/infrastructure/JdbcSubHomePersistence.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/infrastructure/JdbcSubHomePersistence.scala
@@ -2,10 +2,7 @@ package com.github.unchama.seichiassist.subsystems.subhome.infrastructure
 
 import cats.effect.Sync
 import com.github.unchama.seichiassist.SeichiAssist
-import com.github.unchama.seichiassist.subsystems.subhome.domain.{SubHome, SubHomeId, SubHomeLocation, SubHomePersistence, SubHomeV2}
-import com.github.unchama.seichiassist.subsystems.subhome.{SubHomeReadAPI, SubHomeWriteAPI}
-import com.github.unchama.targetedeffect.player
-import org.bukkit.{Bukkit, Location}
+import com.github.unchama.seichiassist.subsystems.subhome.domain.{SubHomeId, SubHomeLocation, SubHomePersistence, SubHome}
 import scalikejdbc._
 
 import java.util.UUID
@@ -13,14 +10,15 @@ import java.util.UUID
 class JdbcSubHomePersistence[F[_]: Sync] extends SubHomePersistence[F] {
   private val serverId = SeichiAssist.seichiAssistConfig.getServerNum
 
-  override def upsert(ownerUuid: UUID, id: SubHomeId)(subHome: SubHomeV2): F[Unit] = {
+  override def upsert(ownerUuid: UUID, id: SubHomeId)(subHome: SubHome): F[Unit] = {
     Sync[F].delay {
       DB.readOnly { implicit session =>
         val SubHomeLocation(worldName, x, y, z) = subHome.location
 
+        // NOTE 2021/05/19: 何故かDB上のIDは1少ない。つまり、ID 1のサブホームはDB上ではid=0である。
         sql"""insert into seichiassist.sub_home
              |(player_uuid, server_id, id, name, location_x, location_y, location_z, world_name) values
-             |  (${ownerUuid.toString}, $serverId, $id, ${subHome.name}, $x, $y, $z, $worldName)
+             |  (${ownerUuid.toString}, $serverId, ${id.value - 1}, ${subHome.name}, $x, $y, $z, $worldName)
              |    on duplicate key update
              |      name = values(name),
              |      location_x = values(location_x),
@@ -33,16 +31,17 @@ class JdbcSubHomePersistence[F[_]: Sync] extends SubHomePersistence[F] {
     }
   }
 
-  override def list(ownerUuid: UUID): F[Map[SubHomeId, SubHomeV2]] = Sync[F].delay {
+  override def list(ownerUuid: UUID): F[Map[SubHomeId, SubHome]] = Sync[F].delay {
     DB.readOnly { implicit session =>
+      // NOTE 2021/05/19: 何故かDB上のIDは1少ない。つまり、ID 1のサブホームはDB上ではid=0である。
       sql"""SELECT id, name, location_x, location_y, location_z, world_name FROM seichiassist.sub_home"""
-        .map(rs => (SubHomeId(rs.int("id")), extractSubHome(rs)))
+        .map(rs => (SubHomeId(rs.int("id") + 1), extractSubHome(rs)))
         .list().apply()
     }.toMap
   }
 
-  private def extractSubHome(rs: WrappedResultSet): SubHomeV2 =
-    SubHomeV2(
+  private def extractSubHome(rs: WrappedResultSet): SubHome =
+    SubHome(
       rs.string("name"),
       SubHomeLocation(
         rs.string("world_name"),
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/infrastructure/SubHomePersistence.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/infrastructure/SubHomePersistence.scala
deleted file mode 100644
index 075e926f83..0000000000
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/infrastructure/SubHomePersistence.scala
+++ /dev/null
@@ -1,86 +0,0 @@
-package com.github.unchama.seichiassist.subsystems.subhome.infrastructure
-
-import cats.effect.Sync
-import com.github.unchama.seichiassist.SeichiAssist
-import com.github.unchama.seichiassist.subsystems.subhome.{SubHomeReadAPI, SubHomeWriteAPI}
-import com.github.unchama.seichiassist.subsystems.subhome.domain.SubHome
-import org.bukkit.{Bukkit, Location}
-import scalikejdbc._
-
-import java.util.UUID
-
-class SubHomePersistence[F[_]: Sync] extends SubHomeReadAPI[F] with SubHomeWriteAPI[F] {
-  private val serverId = SeichiAssist.seichiAssistConfig.getServerNum
-
-  override def get(player: UUID, id: SubHome.ID): F[Option[SubHome]] = {
-    Sync[F].delay {
-      DB.readOnly { implicit session =>
-        sql"""SELECT id, name, location_x, location_y, location_z, world_name FROM seichiassist.sub_home
-              WHERE name = ${player.toString} AND server_id = $serverId AND id = $id"""
-          .map(extractSubHome)
-          // もしかすると見つからないかもしれない
-          .first()
-          .apply()
-      }
-    }
-  }
-
-  override def list(player: UUID): F[Map[SubHome.ID, SubHome]] = Sync[F].delay {
-    DB.readOnly { implicit session =>
-      sql"""SELECT id, name, location_x, location_y, location_z, world_name FROM seichiassist.sub_home"""
-        .map(rs => {
-          (
-            rs.int("id"),
-            extractSubHome(rs)
-          )
-        })
-        .list()
-        .apply()
-    }.toMap
-  }
-
-  override def updateLocation(player: UUID, id: SubHome.ID, location: Location): F[Unit] =
-    Sync[F].delay {
-      val x = location.getX.toInt
-      val y = location.getY.toInt
-      val z = location.getZ.toInt
-      val worldName = location.getWorld.getName
-      DB.localTx { implicit session =>
-        // 重複したとき、もとのエントリを残す必要はないので黙って上書きする
-        sql"""insert into seichiassist.sub_home
-             |(player_uuid, server_id, id, location_x, location_y, location_z, world_name) values
-             |  (${player.toString}, $serverId, $id, $x, $y, $z, $worldName)
-             |    on duplicate key update
-             |      location_x = values(location_x),
-             |      location_y = values(location_y),
-             |      location_z = values(location_z),
-             |      world_name = values(world_name)"""
-          .stripMargin('|')
-          .execute()
-          .apply()
-      }
-    }
-
-  override def updateName(player: UUID, number: SubHome.ID, name: String): F[Unit] =
-    Sync[F].delay {
-      DB.localTx { implicit session =>
-        sql"""update seichiassist.sub_home set name = $name where
-             |  player_uuid = ${player.toString} and
-             |  server_id = $serverId and
-             |  id = $number
-             |"""
-          .stripMargin('|')
-          .execute()
-          .apply()
-      }
-    }
-
-  private def extractSubHome(rs: WrappedResultSet): SubHome = {
-    val x = rs.int("location_x")
-    val y = rs.int("location_y")
-    val z = rs.int("location_z")
-    val world = Bukkit.getWorld(rs.string("world_name"))
-    val homeName = rs.string("name")
-    new SubHome(new Location(world, x, y, z), homeName)
-  }
-}

From f627bf2b8bb575453168e01842504723d6702422 Mon Sep 17 00:00:00 2001
From: Kory <6561358+kory33@users.noreply.github.com>
Date: Wed, 19 May 2021 07:41:16 +0900
Subject: [PATCH 11/73] =?UTF-8?q?[update]=20SubHome=E3=81=AE=E3=83=A2?=
 =?UTF-8?q?=E3=83=87=E3=83=AB=E3=82=92=E6=94=B9=E5=96=84=E3=81=99=E3=82=8B?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../github/unchama/seichiassist/data/player/PlayerData.scala  | 1 -
 .../seichiassist/subsystems/subhome/domain/SubHome.scala      | 2 +-
 .../subsystems/subhome/domain/SubHomePersistence.scala        | 2 +-
 .../subhome/infrastructure/JdbcSubHomePersistence.scala       | 4 ++--
 .../github/unchama/seichiassist/task/PlayerDataLoading.scala  | 2 --
 5 files changed, 4 insertions(+), 7 deletions(-)

diff --git a/src/main/scala/com/github/unchama/seichiassist/data/player/PlayerData.scala b/src/main/scala/com/github/unchama/seichiassist/data/player/PlayerData.scala
index 34b550bb9c..b19fefd0fa 100644
--- a/src/main/scala/com/github/unchama/seichiassist/data/player/PlayerData.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/data/player/PlayerData.scala
@@ -70,7 +70,6 @@ class PlayerData(
   var gachacooldownflag = true
   //インベントリ共有ボタン連打防止用
   var shareinvcooldownflag = true
-  var selectHomeNum = 0
   var samepageflag = false //実績ショップ用
 
   //endregion
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/domain/SubHome.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/domain/SubHome.scala
index f2bae749e5..386ee7b1bb 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/domain/SubHome.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/domain/SubHome.scala
@@ -5,4 +5,4 @@ case class SubHomeLocation(worldName: String, x: Int, y: Int, z: Int)
 /**
  * サブホームオブジェクトのクラス
  */
-case class SubHome(name: String, location: SubHomeLocation)
+case class SubHome(name: Option[String], location: SubHomeLocation)
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/domain/SubHomePersistence.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/domain/SubHomePersistence.scala
index 81a26eef25..7bf9dc1127 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/domain/SubHomePersistence.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/domain/SubHomePersistence.scala
@@ -53,6 +53,6 @@ trait SubHomePersistence[F[_]] {
    * 作用の結果として更新が行われたかどうかを示すBooleanを返す。
    */
   final def rename(ownerUuid: UUID, id: SubHomeId)(newName: String)(implicit F: Monad[F]): F[Boolean] =
-    alter(ownerUuid, id)(_.copy(name = newName))
+    alter(ownerUuid, id)(_.copy(name = Some(newName)))
 
 }
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/infrastructure/JdbcSubHomePersistence.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/infrastructure/JdbcSubHomePersistence.scala
index 79421f78ab..a06811fcaa 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/infrastructure/JdbcSubHomePersistence.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/infrastructure/JdbcSubHomePersistence.scala
@@ -18,7 +18,7 @@ class JdbcSubHomePersistence[F[_]: Sync] extends SubHomePersistence[F] {
         // NOTE 2021/05/19: 何故かDB上のIDは1少ない。つまり、ID 1のサブホームはDB上ではid=0である。
         sql"""insert into seichiassist.sub_home
              |(player_uuid, server_id, id, name, location_x, location_y, location_z, world_name) values
-             |  (${ownerUuid.toString}, $serverId, ${id.value - 1}, ${subHome.name}, $x, $y, $z, $worldName)
+             |  (${ownerUuid.toString}, $serverId, ${id.value - 1}, ${subHome.name.orNull}, $x, $y, $z, $worldName)
              |    on duplicate key update
              |      name = values(name),
              |      location_x = values(location_x),
@@ -42,7 +42,7 @@ class JdbcSubHomePersistence[F[_]: Sync] extends SubHomePersistence[F] {
 
   private def extractSubHome(rs: WrappedResultSet): SubHome =
     SubHome(
-      rs.string("name"),
+      rs.stringOpt("name"),
       SubHomeLocation(
         rs.string("world_name"),
         rs.int("location_x"),
diff --git a/src/main/scala/com/github/unchama/seichiassist/task/PlayerDataLoading.scala b/src/main/scala/com/github/unchama/seichiassist/task/PlayerDataLoading.scala
index d31c62cd64..e35071f641 100644
--- a/src/main/scala/com/github/unchama/seichiassist/task/PlayerDataLoading.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/task/PlayerDataLoading.scala
@@ -207,8 +207,6 @@ object PlayerDataLoading {
           serializedInventory != null && serializedInventory != ""
         }
 
-        playerData.selectHomeNum = 0
-
         //実績、二つ名の情報
         playerData.p_vote_forT = rs.getInt("p_vote")
         playerData.giveachvNo = rs.getInt("giveachvNo")

From ad21ec5143d607f8bdf4a666aedf865321d02106 Mon Sep 17 00:00:00 2001
From: Kory <6561358+kory33@users.noreply.github.com>
Date: Wed, 19 May 2021 07:52:15 +0900
Subject: [PATCH 12/73] =?UTF-8?q?[update]=20JdbcSubHomePersistence?=
 =?UTF-8?q?=E3=81=8C=E5=B8=B8=E3=81=ABshift=E3=81=99=E3=82=8B=E3=82=88?=
 =?UTF-8?q?=E3=81=86=E3=81=AB=E3=81=99=E3=82=8B?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../JdbcSubHomePersistence.scala              | 53 ++++++++++---------
 1 file changed, 29 insertions(+), 24 deletions(-)

diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/infrastructure/JdbcSubHomePersistence.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/infrastructure/JdbcSubHomePersistence.scala
index a06811fcaa..f9289a9e7e 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/infrastructure/JdbcSubHomePersistence.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/infrastructure/JdbcSubHomePersistence.scala
@@ -1,17 +1,20 @@
 package com.github.unchama.seichiassist.subsystems.subhome.infrastructure
 
 import cats.effect.Sync
+import com.github.unchama.concurrent.NonServerThreadContextShift
 import com.github.unchama.seichiassist.SeichiAssist
-import com.github.unchama.seichiassist.subsystems.subhome.domain.{SubHomeId, SubHomeLocation, SubHomePersistence, SubHome}
+import com.github.unchama.seichiassist.subsystems.subhome.domain.{SubHome, SubHomeId, SubHomeLocation, SubHomePersistence}
 import scalikejdbc._
 
 import java.util.UUID
 
-class JdbcSubHomePersistence[F[_]: Sync] extends SubHomePersistence[F] {
+class JdbcSubHomePersistence[F[_]: Sync: NonServerThreadContextShift] extends SubHomePersistence[F] {
   private val serverId = SeichiAssist.seichiAssistConfig.getServerNum
 
-  override def upsert(ownerUuid: UUID, id: SubHomeId)(subHome: SubHome): F[Unit] = {
-    Sync[F].delay {
+  import cats.implicits._
+
+  override def upsert(ownerUuid: UUID, id: SubHomeId)(subHome: SubHome): F[Unit] =
+    NonServerThreadContextShift[F].shift >> Sync[F].delay[Unit] {
       DB.readOnly { implicit session =>
         val SubHomeLocation(worldName, x, y, z) = subHome.location
 
@@ -29,25 +32,27 @@ class JdbcSubHomePersistence[F[_]: Sync] extends SubHomePersistence[F] {
           .apply()
       }
     }
-  }
-
-  override def list(ownerUuid: UUID): F[Map[SubHomeId, SubHome]] = Sync[F].delay {
-    DB.readOnly { implicit session =>
-      // NOTE 2021/05/19: 何故かDB上のIDは1少ない。つまり、ID 1のサブホームはDB上ではid=0である。
-      sql"""SELECT id, name, location_x, location_y, location_z, world_name FROM seichiassist.sub_home"""
-        .map(rs => (SubHomeId(rs.int("id") + 1), extractSubHome(rs)))
-        .list().apply()
-    }.toMap
-  }
 
-  private def extractSubHome(rs: WrappedResultSet): SubHome =
-    SubHome(
-      rs.stringOpt("name"),
-      SubHomeLocation(
-        rs.string("world_name"),
-        rs.int("location_x"),
-        rs.int("location_y"),
-        rs.int("location_z")
-      )
-    )
+  override def list(ownerUuid: UUID): F[Map[SubHomeId, SubHome]] =
+    NonServerThreadContextShift[F].shift >> Sync[F].delay {
+      DB.readOnly { implicit session =>
+        // NOTE 2021/05/19: 何故かDB上のIDは1少ない。つまり、ID 1のサブホームはDB上ではid=0である。
+        sql"""SELECT id, name, location_x, location_y, location_z, world_name FROM seichiassist.sub_home"""
+          .map(rs =>
+            (
+              SubHomeId(rs.int("id") + 1),
+              SubHome(
+                rs.stringOpt("name"),
+                SubHomeLocation(
+                  rs.string("world_name"),
+                  rs.int("location_x"),
+                  rs.int("location_y"),
+                  rs.int("location_z")
+                )
+              )
+            )
+          )
+          .list().apply()
+      }.toMap
+    }
 }

From c54542e28695066f1d77d01ce284fa505be0382d Mon Sep 17 00:00:00 2001
From: Kory <6561358+kory33@users.noreply.github.com>
Date: Fri, 30 Jul 2021 08:15:39 +0900
Subject: [PATCH 13/73] =?UTF-8?q?[fix]=20=E3=82=B3=E3=83=B3=E3=83=91?=
 =?UTF-8?q?=E3=82=A4=E3=83=AB=E3=82=A8=E3=83=A9=E3=83=BC=E3=82=92=E4=BF=AE?=
 =?UTF-8?q?=E6=AD=A3=E3=81=99=E3=82=8B?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../unchama/seichiassist/menus/HomeMenu.scala       | 13 +++++++------
 .../unchama/seichiassist/menus/TopLevelRouter.scala |  6 ++----
 .../subhome/bukkit/command/SubHomeCommand.scala     |  8 +++++---
 3 files changed, 14 insertions(+), 13 deletions(-)

diff --git a/src/main/scala/com/github/unchama/seichiassist/menus/HomeMenu.scala b/src/main/scala/com/github/unchama/seichiassist/menus/HomeMenu.scala
index dffb4eaa35..a031f90482 100644
--- a/src/main/scala/com/github/unchama/seichiassist/menus/HomeMenu.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/menus/HomeMenu.scala
@@ -7,7 +7,7 @@ import com.github.unchama.menuinventory.slot.button.Button
 import com.github.unchama.menuinventory.slot.button.action.LeftClickButtonEffect
 import com.github.unchama.menuinventory.{ChestSlotRef, Menu, MenuFrame, MenuSlotLayout}
 import com.github.unchama.seichiassist.subsystems.subhome.SubHomeReadAPI
-import com.github.unchama.seichiassist.subsystems.subhome.domain.{SubHomeId, SubHome}
+import com.github.unchama.seichiassist.subsystems.subhome.domain.{SubHome, SubHomeId}
 import com.github.unchama.seichiassist.{ManagedWorld, SeichiAssist}
 import com.github.unchama.targetedeffect._
 import com.github.unchama.targetedeffect.player.PlayerEffects._
@@ -168,11 +168,12 @@ object HomeMenu extends Menu {
       } yield {
         val lore = subhomeOpt match {
           case None => List(s"${GRAY}サブホームポイント$subHomeId", s"${GRAY}ポイント未設定")
-          case Some(SubHome(location, name)) =>
-            val worldName =
+          case Some(SubHome(name, location)) =>
+            val worldName = {
               ManagedWorld
-                .fromBukkitWorld(location.getWorld).map(_.japaneseName)
-                .getOrElse(location.getWorld.getName)
+                .fromName(location.worldName).map(_.japaneseName)
+                .getOrElse(location.worldName)
+            }
 
             List(
               s"${GRAY}サブホームポイント${subHomeId}は",
@@ -180,7 +181,7 @@ object HomeMenu extends Menu {
               s"${GRAY}と名付けられています",
               s"$DARK_RED${UNDERLINE}クリックで名称変更",
               s"${DARK_GRAY}command->[/subhome name $subHomeId]",
-              s"$GRAY$worldName x:${location.getBlockX} y:${location.getBlockY} z:${location.getBlockZ}"
+              s"$GRAY$worldName x:${location.x} y:${location.y} z:${location.z}"
             )
         }
 
diff --git a/src/main/scala/com/github/unchama/seichiassist/menus/TopLevelRouter.scala b/src/main/scala/com/github/unchama/seichiassist/menus/TopLevelRouter.scala
index 8ab6c46f63..eb82503812 100644
--- a/src/main/scala/com/github/unchama/seichiassist/menus/TopLevelRouter.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/menus/TopLevelRouter.scala
@@ -15,15 +15,14 @@ import com.github.unchama.seichiassist.subsystems.breakcount.BreakCountAPI
 import com.github.unchama.seichiassist.subsystems.breakcount.domain.SeichiAmountData
 import com.github.unchama.seichiassist.subsystems.breakcountbar.BreakCountBarAPI
 import com.github.unchama.seichiassist.subsystems.buildcount.domain.playerdata.BuildAmountData
+import com.github.unchama.seichiassist.subsystems.discordnotification.DiscordNotificationAPI
 import com.github.unchama.seichiassist.subsystems.fastdiggingeffect.{FastDiggingEffectApi, FastDiggingSettingsApi}
 import com.github.unchama.seichiassist.subsystems.fourdimensionalpocket.FourDimensionalPocketApi
 import com.github.unchama.seichiassist.subsystems.gachapoint.GachaPointApi
 import com.github.unchama.seichiassist.subsystems.mana.ManaApi
-import com.github.unchama.seichiassist.subsystems.ranking.RankingApi
-import com.github.unchama.seichiassist.subsystems.subhome.SubHomeReadAPI
-import com.github.unchama.seichiassist.subsystems.discordnotification.DiscordNotificationAPI
 import com.github.unchama.seichiassist.subsystems.ranking.api.AssortedRankingApi
 import com.github.unchama.seichiassist.subsystems.ranking.domain.values.{LoginTime, VoteCount}
+import com.github.unchama.seichiassist.subsystems.subhome.SubHomeReadAPI
 import io.chrisdavenport.cats.effect.time.JavaTime
 import org.bukkit.entity.Player
 
@@ -53,7 +52,6 @@ object TopLevelRouter {
             subHomeReadApi: SubHomeReadAPI[IO]): TopLevelRouter[IO] = new TopLevelRouter[IO] {
     import assortedRankingApi._
 
-    implicit lazy val seichiRankingMenuEnv: SeichiRankingMenu.Environment = new SeichiRankingMenu.Environment
     implicit lazy val secondPageEnv: SecondPage.Environment = new SecondPage.Environment
     implicit lazy val mineStackMainMenuEnv: MineStackMainMenu.Environment = new MineStackMainMenu.Environment
     implicit lazy val categorizedMineStackMenuEnv: CategorizedMineStackMenu.Environment = new CategorizedMineStackMenu.Environment
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/bukkit/command/SubHomeCommand.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/bukkit/command/SubHomeCommand.scala
index 354e7215f5..baefb1e576 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/bukkit/command/SubHomeCommand.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/bukkit/command/SubHomeCommand.scala
@@ -11,11 +11,12 @@ import com.github.unchama.contextualexecutor.builder.Parsers
 import com.github.unchama.contextualexecutor.executors.{BranchedExecutor, EchoExecutor}
 import com.github.unchama.seichiassist.SeichiAssist
 import com.github.unchama.seichiassist.commands.contextual.builder.BuilderTemplates.playerCommandBuilder
-import com.github.unchama.seichiassist.subsystems.subhome.domain.{SubHomeId, SubHome}
+import com.github.unchama.seichiassist.subsystems.subhome.domain.{SubHome, SubHomeId}
 import com.github.unchama.seichiassist.subsystems.subhome.{SubHomeReadAPI, SubHomeWriteAPI}
 import com.github.unchama.targetedeffect.commandsender.MessageEffect
 import org.bukkit.ChatColor._
 import org.bukkit.command.TabExecutor
+import org.bukkit.{Bukkit, Location}
 
 object SubHomeCommand {
   private val printDescriptionExecutor = new EchoExecutor(
@@ -60,8 +61,9 @@ object SubHomeCommand {
       } yield {
         subHomeLocation match {
           case None => MessageEffect(s"サブホームポイント${subHomeId}が設定されてません")
-          case Some(SubHome(location, _)) =>
-            player.teleport(location) // TODO これは副作用
+          case Some(SubHome(_, location)) =>
+            // TODO これは副作用
+            player.teleport(new Location(Bukkit.getWorld(location.worldName), location.x, location.y, location.z))
             MessageEffect(s"サブホームポイント${subHomeId}にワープしました")
         }
       }

From 7bbcfc77f1ea61a43fb09fe72517212ff605a88d Mon Sep 17 00:00:00 2001
From: Lucky <lucky3028@users.noreply.github.com>
Date: Mon, 2 Aug 2021 15:47:25 +0900
Subject: [PATCH 14/73] =?UTF-8?q?[Fix]=20=E4=BF=AE=E7=B9=95=E3=81=AE?=
 =?UTF-8?q?=E6=9C=AC=E3=82=92=E3=82=B3=E3=83=94=E3=83=BC=E3=81=97=E3=81=A6?=
 =?UTF-8?q?=E3=82=82=E9=80=9A=E5=B8=B8=E9=80=9A=E3=82=8A=E4=BD=BF=E3=81=88?=
 =?UTF-8?q?=E3=81=A6=E3=81=97=E3=81=BE=E3=81=86=E5=95=8F=E9=A1=8C=E3=82=92?=
 =?UTF-8?q?=E4=BF=AE=E6=AD=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../seasonalevents/anniversary/AnniversaryItemData.scala | 9 ++++++++-
 1 file changed, 8 insertions(+), 1 deletion(-)

diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/seasonalevents/anniversary/AnniversaryItemData.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/seasonalevents/anniversary/AnniversaryItemData.scala
index 2cc9d03838..b2590acd46 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/seasonalevents/anniversary/AnniversaryItemData.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/seasonalevents/anniversary/AnniversaryItemData.scala
@@ -8,8 +8,9 @@ import org.bukkit.Bukkit
 import org.bukkit.ChatColor._
 import org.bukkit.Material._
 import org.bukkit.enchantments.Enchantment
+import org.bukkit.inventory.meta.BookMeta.Generation
+import org.bukkit.inventory.meta.{BookMeta, ItemMeta, SkullMeta}
 import org.bukkit.inventory.{ItemFlag, ItemStack}
-import org.bukkit.inventory.meta.SkullMeta
 
 import scala.jdk.CollectionConverters._
 import scala.util.chaining._
@@ -100,6 +101,12 @@ object AnniversaryItemData {
   def isMendingBook(item: ItemStack): Boolean =
     item != null && item.getType == WRITTEN_BOOK && {
       new NBTItem(item).getByte(NBTTagConstants.typeIdTag) == 2
+    } && isOriginalBook(item.getItemMeta)
+
+  private def isOriginalBook(meta: ItemMeta) =
+    meta match {
+      case bookMeta: BookMeta => bookMeta.hasGeneration && bookMeta.getGeneration == Generation.ORIGINAL
+      case _ => false
     }
 
   //endregion

From 4d0a52882aa4f3ecfe7ea140a7787052ac011749 Mon Sep 17 00:00:00 2001
From: Lucky <lucky3028@users.noreply.github.com>
Date: Mon, 2 Aug 2021 15:57:06 +0900
Subject: [PATCH 15/73] =?UTF-8?q?[Add]=20ItemMeta=E3=82=92=E3=82=82?=
 =?UTF-8?q?=E3=81=A3=E3=81=A6=E3=81=84=E3=82=8B=E3=81=8B=E3=81=A9=E3=81=86?=
 =?UTF-8?q?=E3=81=8B=E3=81=AE=E7=A2=BA=E8=AA=8D=E3=82=92=E3=81=99=E3=82=8B?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../seasonalevents/anniversary/AnniversaryItemData.scala        | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/seasonalevents/anniversary/AnniversaryItemData.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/seasonalevents/anniversary/AnniversaryItemData.scala
index b2590acd46..92eea6154e 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/seasonalevents/anniversary/AnniversaryItemData.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/seasonalevents/anniversary/AnniversaryItemData.scala
@@ -101,7 +101,7 @@ object AnniversaryItemData {
   def isMendingBook(item: ItemStack): Boolean =
     item != null && item.getType == WRITTEN_BOOK && {
       new NBTItem(item).getByte(NBTTagConstants.typeIdTag) == 2
-    } && isOriginalBook(item.getItemMeta)
+    } && item.hasItemMeta && isOriginalBook(item.getItemMeta)
 
   private def isOriginalBook(meta: ItemMeta) =
     meta match {

From 3070219873f4024c7038906837791a8e9e88dc2d Mon Sep 17 00:00:00 2001
From: Lucky <lucky3028@users.noreply.github.com>
Date: Mon, 2 Aug 2021 15:58:38 +0900
Subject: [PATCH 16/73] =?UTF-8?q?[Fix]=20=E3=83=AC=E3=83=93=E3=83=A5?=
 =?UTF-8?q?=E3=83=BC=E3=82=92=E5=8F=97=E3=81=91=E3=81=9F=E5=86=85=E5=AE=B9?=
 =?UTF-8?q?=E3=81=AE=E4=BF=AE=E6=AD=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../anniversary/AnniversaryItemData.scala       | 17 +++++++++--------
 1 file changed, 9 insertions(+), 8 deletions(-)

diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/seasonalevents/anniversary/AnniversaryItemData.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/seasonalevents/anniversary/AnniversaryItemData.scala
index 92eea6154e..b4bcc7f985 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/seasonalevents/anniversary/AnniversaryItemData.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/seasonalevents/anniversary/AnniversaryItemData.scala
@@ -98,16 +98,17 @@ object AnniversaryItemData {
       .pipe(_.getItem)
   }
 
-  def isMendingBook(item: ItemStack): Boolean =
+  def isMendingBook(item: ItemStack): Boolean = {
+    def isOriginal(meta: ItemMeta) =
+      meta match {
+        case bookMeta: BookMeta => bookMeta.hasGeneration && bookMeta.getGeneration == Generation.ORIGINAL
+        case _ => false
+      }
+
     item != null && item.getType == WRITTEN_BOOK && {
       new NBTItem(item).getByte(NBTTagConstants.typeIdTag) == 2
-    } && item.hasItemMeta && isOriginalBook(item.getItemMeta)
-
-  private def isOriginalBook(meta: ItemMeta) =
-    meta match {
-      case bookMeta: BookMeta => bookMeta.hasGeneration && bookMeta.getGeneration == Generation.ORIGINAL
-      case _ => false
-    }
+    } && item.hasItemMeta && isOriginal(item.getItemMeta)
+  }
 
   //endregion
 

From e2db25654c06d5c4f53be9bc36974826b2352aa0 Mon Sep 17 00:00:00 2001
From: Kory <6561358+kory33@users.noreply.github.com>
Date: Mon, 2 Aug 2021 16:46:48 +0900
Subject: [PATCH 17/73] =?UTF-8?q?[update]=20rename=E6=99=82=E3=81=AB?=
 =?UTF-8?q?=E3=82=B5=E3=83=96=E3=83=9B=E3=83=BC=E3=83=A0=E3=81=8C=E5=AD=98?=
 =?UTF-8?q?=E5=9C=A8=E3=81=97=E3=81=A6=E3=81=84=E3=81=AA=E3=81=8B=E3=81=A3?=
 =?UTF-8?q?=E3=81=9F=E5=A0=B4=E5=90=88=E3=81=AE=E6=8C=99=E5=8B=95=E3=82=92?=
 =?UTF-8?q?=E6=98=8E=E7=A4=BA=E7=9A=84=E3=81=AB=E8=A8=98=E8=BF=B0=E3=81=99?=
 =?UTF-8?q?=E3=82=8B?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../subsystems/subhome/SubHomeAPI.scala       |  7 +-
 .../subsystems/subhome/System.scala           | 14 ++-
 .../subhome/bukkit/LocationCodec.scala        | 24 ++++++
 .../subhome/bukkit/TeleportEffect.scala       | 14 +++
 .../bukkit/command/SubHomeCommand.scala       | 85 ++++++++++---------
 .../subhome/domain/OperationResult.scala      |  9 ++
 .../subhome/domain/SubHomePersistence.scala   |  9 +-
 7 files changed, 105 insertions(+), 57 deletions(-)
 create mode 100644 src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/bukkit/LocationCodec.scala
 create mode 100644 src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/bukkit/TeleportEffect.scala
 create mode 100644 src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/domain/OperationResult.scala

diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/SubHomeAPI.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/SubHomeAPI.scala
index 793d38fcf2..36d6eab12d 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/SubHomeAPI.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/SubHomeAPI.scala
@@ -1,7 +1,6 @@
 package com.github.unchama.seichiassist.subsystems.subhome
 
-import com.github.unchama.seichiassist.subsystems.subhome.domain.{SubHomeId, SubHome}
-import org.bukkit.Location
+import com.github.unchama.seichiassist.subsystems.subhome.domain.{OperationResult, SubHome, SubHomeId, SubHomeLocation}
 
 import java.util.UUID
 
@@ -21,9 +20,9 @@ object SubHomeReadAPI {
 
 trait SubHomeWriteAPI[F[_]] {
 
-  def updateLocation(ownerUuid: UUID, id: SubHomeId, location: Location): F[Unit]
+  def upsertLocation(ownerUuid: UUID, id: SubHomeId, location: SubHomeLocation): F[Unit]
 
-  def updateName(ownerUuid: UUID, id: SubHomeId, name: String): F[Unit]
+  def rename(ownerUuid: UUID, id: SubHomeId)(name: String): F[OperationResult.RenameResult]
 
 }
 
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/System.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/System.scala
index 94c0e2e62e..752880de03 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/System.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/System.scala
@@ -5,9 +5,9 @@ import com.github.unchama.concurrent.NonServerThreadContextShift
 import com.github.unchama.seichiassist.SeichiAssist.Scopes.globalChatInterceptionScope
 import com.github.unchama.seichiassist.meta.subsystem.Subsystem
 import com.github.unchama.seichiassist.subsystems.subhome.bukkit.command.SubHomeCommand
-import com.github.unchama.seichiassist.subsystems.subhome.domain.{SubHomeId, SubHome}
+import com.github.unchama.seichiassist.subsystems.subhome.domain.OperationResult.RenameResult
+import com.github.unchama.seichiassist.subsystems.subhome.domain.{SubHome, SubHomeId, SubHomeLocation}
 import com.github.unchama.seichiassist.subsystems.subhome.infrastructure.JdbcSubHomePersistence
-import org.bukkit.Location
 import org.bukkit.command.TabExecutor
 
 import java.util.UUID
@@ -22,16 +22,14 @@ object System {
     : ConcurrentEffect
     : NonServerThreadContextShift
   ]: System[F] = {
-    import cats.implicits._
-
     val persistence = new JdbcSubHomePersistence[F]()
 
     new System[F] {
       override implicit val api: SubHomeAPI[F] = new SubHomeAPI[F] {
-        override def updateLocation(ownerUuid: UUID, id: SubHomeId, location: Location): F[Unit] =
-          ??? // TODO semantics not clear
-        override def updateName(ownerUuid: UUID, id: SubHomeId, name: String): F[Unit] =
-          persistence.rename(ownerUuid, id)(name).as(())
+        override def upsertLocation(ownerUuid: UUID, id: SubHomeId, location: SubHomeLocation): F[Unit] =
+          persistence.upsert(ownerUuid, id)(SubHome(None, location))
+        override def rename(ownerUuid: UUID, id: SubHomeId)(name: String): F[RenameResult] =
+          persistence.rename(ownerUuid, id)(name)
         override def get(ownerUuid: UUID, id: SubHomeId): F[Option[SubHome]] =
           persistence.get(ownerUuid, id)
         override def list(ownerUuid: UUID): F[Map[SubHomeId, SubHome]] =
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/bukkit/LocationCodec.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/bukkit/LocationCodec.scala
new file mode 100644
index 0000000000..69fbc82467
--- /dev/null
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/bukkit/LocationCodec.scala
@@ -0,0 +1,24 @@
+package com.github.unchama.seichiassist.subsystems.subhome.bukkit
+
+import com.github.unchama.seichiassist.subsystems.subhome.domain.SubHomeLocation
+import org.bukkit.{Bukkit, Location}
+
+/**
+ * [[org.bukkit.Location]] と [[SubHomeLocation]] との相互変換を実現するコーデック。
+ *
+ * [[SubHomeLocation]] は [[org.bukkit.Location]] よりも真に情報量が少ない(ワールドへの参照を持っていない)ため、
+ * [[SubHomeLocation]] から [[org.bukkit.Location]] への変換は [[Option]] として返ってくる。
+ */
+object LocationCodec {
+
+  def fromBukkitLocation(location: Location): SubHomeLocation = {
+    SubHomeLocation(location.getWorld.getName, location.getBlockX, location.getBlockY, location.getBlockZ)
+  }
+
+  def toBukkitLocation(location: SubHomeLocation): Option[Location] = {
+    val world = Bukkit.getWorld(location.worldName)
+
+    Option.when(world != null)(new Location(world, location.x, location.y, location.z))
+  }
+
+}
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/bukkit/TeleportEffect.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/bukkit/TeleportEffect.scala
new file mode 100644
index 0000000000..95c5282086
--- /dev/null
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/bukkit/TeleportEffect.scala
@@ -0,0 +1,14 @@
+package com.github.unchama.seichiassist.subsystems.subhome.bukkit
+
+import cats.data.Kleisli
+import cats.effect.Sync
+import org.bukkit.Location
+import org.bukkit.entity.Player
+
+object TeleportEffect {
+  def to[F[_]: Sync](location: Location): Kleisli[F, Player, Unit] = {
+    Kleisli { player =>
+      Sync[F].delay(player.teleport(location))
+    }
+  }
+}
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/bukkit/command/SubHomeCommand.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/bukkit/command/SubHomeCommand.scala
index baefb1e576..41a9ea323a 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/bukkit/command/SubHomeCommand.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/bukkit/command/SubHomeCommand.scala
@@ -3,7 +3,6 @@ package com.github.unchama.seichiassist.subsystems.subhome.bukkit.command
 import cats.data.Kleisli
 import cats.effect.implicits._
 import cats.effect.{ConcurrentEffect, IO}
-import cats.implicits._
 import com.github.unchama.chatinterceptor.CancellationReason.Overridden
 import com.github.unchama.chatinterceptor.ChatInterceptionScope
 import com.github.unchama.concurrent.NonServerThreadContextShift
@@ -11,14 +10,17 @@ import com.github.unchama.contextualexecutor.builder.Parsers
 import com.github.unchama.contextualexecutor.executors.{BranchedExecutor, EchoExecutor}
 import com.github.unchama.seichiassist.SeichiAssist
 import com.github.unchama.seichiassist.commands.contextual.builder.BuilderTemplates.playerCommandBuilder
+import com.github.unchama.seichiassist.subsystems.subhome.bukkit.{LocationCodec, TeleportEffect}
 import com.github.unchama.seichiassist.subsystems.subhome.domain.{SubHome, SubHomeId}
-import com.github.unchama.seichiassist.subsystems.subhome.{SubHomeReadAPI, SubHomeWriteAPI}
+import com.github.unchama.seichiassist.subsystems.subhome.{SubHomeAPI, SubHomeReadAPI, SubHomeWriteAPI}
 import com.github.unchama.targetedeffect.commandsender.MessageEffect
 import org.bukkit.ChatColor._
 import org.bukkit.command.TabExecutor
-import org.bukkit.{Bukkit, Location}
 
 object SubHomeCommand {
+
+  import cats.implicits._
+
   private val printDescriptionExecutor = new EchoExecutor(
     MessageEffect(
       List(
@@ -45,6 +47,20 @@ object SubHomeCommand {
       onMissingArguments = printDescriptionExecutor
     )
 
+  def executor[
+    F[_]
+    : SubHomeAPI
+    : ConcurrentEffect
+    : NonServerThreadContextShift
+  ](implicit scope: ChatInterceptionScope): TabExecutor = BranchedExecutor(
+    Map(
+      "warp" -> warpExecutor,
+      "set" -> setExecutor,
+      "name" -> nameExecutor
+    ),
+    whenArgInsufficient = Some(printDescriptionExecutor), whenBranchNotFound = Some(printDescriptionExecutor)
+  ).asNonBlockingTabExecutor()
+
   private def warpExecutor[
     F[_]
     : ConcurrentEffect
@@ -62,9 +78,16 @@ object SubHomeCommand {
         subHomeLocation match {
           case None => MessageEffect(s"サブホームポイント${subHomeId}が設定されてません")
           case Some(SubHome(_, location)) =>
-            // TODO これは副作用
-            player.teleport(new Location(Bukkit.getWorld(location.worldName), location.x, location.y, location.z))
-            MessageEffect(s"サブホームポイント${subHomeId}にワープしました")
+            LocationCodec.toBukkitLocation(location) match {
+              case Some(bukkitLocation) =>
+                TeleportEffect.to[IO](bukkitLocation) >>
+                  MessageEffect(s"サブホームポイント${subHomeId}にワープしました")
+              case None =>
+                MessageEffect(List(
+                  s"${RED}サブホームポイントへのワープに失敗しました",
+                  s"${RED}登録先のワールドが削除された可能性があります"
+                ))
+            }
         }
       }
 
@@ -84,10 +107,12 @@ object SubHomeCommand {
 
       val eff = for {
         _ <- NonServerThreadContextShift[F].shift
-        _ <- SubHomeWriteAPI[F].updateLocation(player.getUniqueId, subHomeId, player.getLocation)
-      } yield {
-        MessageEffect(s"現在位置をサブホームポイント${subHomeId}に設定しました")
-      }
+        _ <- SubHomeWriteAPI[F].upsertLocation(
+          player.getUniqueId,
+          subHomeId,
+          LocationCodec.fromBukkitLocation(player.getLocation)
+        )
+      } yield MessageEffect(s"現在位置をサブホームポイント${subHomeId}に設定しました")
 
       eff.toIO
     }
@@ -104,23 +129,19 @@ object SubHomeCommand {
 
       IO.pure {
         val sendInterceptionMessage =
-          MessageEffect(
-            List(
-              s"サブホームポイント${subHomeId}に設定する名前をチャットで入力してください",
-              s"$YELLOW※入力されたチャット内容は他のプレイヤーには見えません"
-            )
-          )
+          MessageEffect(List(
+            s"サブホームポイント${subHomeId}に設定する名前をチャットで入力してください",
+            s"$YELLOW※入力されたチャット内容は他のプレイヤーには見えません"
+          ))
 
         val sendCancellationMessage =
           MessageEffect(s"${YELLOW}入力がキャンセルされました。")
 
         def sendCompletionMessage(inputName: String) =
-          MessageEffect(
-            List(
-              s"${GREEN}サブホームポイント${subHomeId}の名前を",
-              s"$GREEN${inputName}に更新しました"
-            )
-          )
+          MessageEffect(List(
+            s"${GREEN}サブホームポイント${subHomeId}の名前を",
+            s"$GREEN${inputName}に更新しました"
+          ))
 
         import com.github.unchama.generic.syntax._
 
@@ -129,10 +150,7 @@ object SubHomeCommand {
 
           scope.interceptFrom(uuid).flatMap {
             case Left(newName) =>
-              val eff = for {
-                _ <- SubHomeWriteAPI[F].updateName(uuid, subHomeId, newName)
-              } yield {}
-              eff.toIO *>
+              SubHomeWriteAPI[F].rename(uuid, subHomeId)(newName).toIO >>
                 sendCompletionMessage(newName)(player)
             case Right(Overridden) => sendCancellationMessage(player)
             case Right(_) => IO.pure(())
@@ -141,19 +159,4 @@ object SubHomeCommand {
       }
     }
     .build()
-
-  def executor[
-    F[_]
-    : SubHomeReadAPI
-    : SubHomeWriteAPI
-    : ConcurrentEffect
-    : NonServerThreadContextShift
-  ](implicit scope: ChatInterceptionScope): TabExecutor = BranchedExecutor(
-    Map(
-      "warp" -> warpExecutor,
-      "set" -> setExecutor,
-      "name" -> nameExecutor
-    ),
-    whenArgInsufficient = Some(printDescriptionExecutor), whenBranchNotFound = Some(printDescriptionExecutor)
-  ).asNonBlockingTabExecutor()
 }
\ No newline at end of file
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/domain/OperationResult.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/domain/OperationResult.scala
new file mode 100644
index 0000000000..4877505065
--- /dev/null
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/domain/OperationResult.scala
@@ -0,0 +1,9 @@
+package com.github.unchama.seichiassist.subsystems.subhome.domain
+
+object OperationResult {
+  sealed trait RenameResult
+  object RenameResult {
+    case object Done extends RenameResult
+    case object NotFound extends RenameResult
+  }
+}
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/domain/SubHomePersistence.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/domain/SubHomePersistence.scala
index 7bf9dc1127..dbc63d21c8 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/domain/SubHomePersistence.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/domain/SubHomePersistence.scala
@@ -1,6 +1,7 @@
 package com.github.unchama.seichiassist.subsystems.subhome.domain
 
 import cats.{Functor, Monad}
+import com.github.unchama.seichiassist.subsystems.subhome.domain.OperationResult.RenameResult
 
 import java.util.UUID
 
@@ -49,10 +50,10 @@ trait SubHomePersistence[F[_]] {
 
   /**
    * 指定されたサブホームをnon-atomicにリネームする。存在しないサブホームが指定された場合何も行わない。
-   *
-   * 作用の結果として更新が行われたかどうかを示すBooleanを返す。
    */
-  final def rename(ownerUuid: UUID, id: SubHomeId)(newName: String)(implicit F: Monad[F]): F[Boolean] =
-    alter(ownerUuid, id)(_.copy(name = Some(newName)))
+  final def rename(ownerUuid: UUID, id: SubHomeId)(newName: String)(implicit F: Monad[F]): F[OperationResult.RenameResult] =
+    alter(ownerUuid, id)(_.copy(name = Some(newName))).map { r =>
+      if (r) RenameResult.Done else RenameResult.NotFound
+    }
 
 }

From e7217490b76b3727b190e2903730e2fed1589a78 Mon Sep 17 00:00:00 2001
From: Kory <6561358+kory33@users.noreply.github.com>
Date: Mon, 2 Aug 2021 18:04:12 +0900
Subject: [PATCH 18/73] =?UTF-8?q?[fix]=20subhomeSystem=E3=81=8C=E7=B9=8B?=
 =?UTF-8?q?=E3=81=8C=E3=81=A3=E3=81=A6=E3=81=84=E3=81=AA=E3=81=84=E3=81=AE?=
 =?UTF-8?q?=E3=82=92=E4=BF=AE=E6=AD=A3=E3=81=99=E3=82=8B?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../unchama/seichiassist/SeichiAssist.scala    | 18 +++++++++---------
 1 file changed, 9 insertions(+), 9 deletions(-)

diff --git a/src/main/scala/com/github/unchama/seichiassist/SeichiAssist.scala b/src/main/scala/com/github/unchama/seichiassist/SeichiAssist.scala
index c5fcf2d5b6..c894f13266 100644
--- a/src/main/scala/com/github/unchama/seichiassist/SeichiAssist.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/SeichiAssist.scala
@@ -58,7 +58,6 @@ import com.github.unchama.seichiassist.subsystems.managedfly.ManagedFlyApi
 import com.github.unchama.seichiassist.subsystems.present.infrastructure.GlobalPlayerAccessor
 import com.github.unchama.seichiassist.subsystems.seasonalevents.api.SeasonalEventsAPI
 import com.github.unchama.seichiassist.subsystems.subhome.SubHomeReadAPI
-import com.github.unchama.seichiassist.subsystems.subhome.bukkit.command.SubHomeCommand
 import com.github.unchama.seichiassist.task.PlayerDataSaveTask
 import com.github.unchama.seichiassist.task.global._
 import com.github.unchama.util.{ActionStatus, ClassUtils}
@@ -304,6 +303,14 @@ class SeichiAssist extends JavaPlugin() {
     subsystems.discordnotification.System.wired[IO](seichiAssistConfig.discordNotificationConfiguration)
   }
 
+  lazy val subhomeSystem: subhome.System[IO] = {
+    import PluginExecutionContexts.asyncShift
+
+    implicit val effectEnvironment: EffectEnvironment = DefaultEffectEnvironment
+    implicit val concurrentEffect: ConcurrentEffect[IO] = IO.ioConcurrentEffect(asyncShift)
+    subhome.System.wired
+  }
+
   private lazy val wiredSubsystems: List[Subsystem[IO]] = List(
     mebiusSystem,
     expBottleStackSystem,
@@ -321,6 +328,7 @@ class SeichiAssist extends JavaPlugin() {
     fourDimensionalPocketSystem,
     gachaPointSystem,
     discordNotificationSystem,
+    subhomeSystem
   )
 
   private lazy val buildAssist: BuildAssist = {
@@ -371,14 +379,6 @@ class SeichiAssist extends JavaPlugin() {
     subsystems.present.System.wired
   }
 
-  lazy val subhomeSystem: subhome.System[IO] = {
-    import PluginExecutionContexts.{asyncShift, onMainThread}
-
-    implicit val effectEnvironment: EffectEnvironment = DefaultEffectEnvironment
-    implicit val concurrentEffect: ConcurrentEffect[IO] = IO.ioConcurrentEffect(asyncShift)
-    subhome.System.wired
-  }
-
   //endregion
 
   private implicit val _akkaSystem: ActorSystem = ConfiguredActorSystemProvider("reference.conf").provide()

From 3c946d4463782f71f0be8f0acecb3da7df778789 Mon Sep 17 00:00:00 2001
From: Kory <6561358+kory33@users.noreply.github.com>
Date: Mon, 2 Aug 2021 18:10:05 +0900
Subject: [PATCH 19/73] =?UTF-8?q?[fix]=20JdbcSubHomePersistence=E3=81=A7?=
 =?UTF-8?q?=E3=81=AEupsert=E3=82=92readOnly=E3=81=AB=E3=81=97=E3=81=AA?=
 =?UTF-8?q?=E3=81=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../subhome/infrastructure/JdbcSubHomePersistence.scala         | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/infrastructure/JdbcSubHomePersistence.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/infrastructure/JdbcSubHomePersistence.scala
index f9289a9e7e..00aa194d2b 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/infrastructure/JdbcSubHomePersistence.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/infrastructure/JdbcSubHomePersistence.scala
@@ -15,7 +15,7 @@ class JdbcSubHomePersistence[F[_]: Sync: NonServerThreadContextShift] extends Su
 
   override def upsert(ownerUuid: UUID, id: SubHomeId)(subHome: SubHome): F[Unit] =
     NonServerThreadContextShift[F].shift >> Sync[F].delay[Unit] {
-      DB.readOnly { implicit session =>
+      DB.localTx { implicit session =>
         val SubHomeLocation(worldName, x, y, z) = subHome.location
 
         // NOTE 2021/05/19: 何故かDB上のIDは1少ない。つまり、ID 1のサブホームはDB上ではid=0である。

From e2541a654952bf1add6f830cca95cdb7f46c574b Mon Sep 17 00:00:00 2001
From: Kory <6561358+kory33@users.noreply.github.com>
Date: Mon, 2 Aug 2021 18:15:14 +0900
Subject: [PATCH 20/73] =?UTF-8?q?[fix]=20=E3=82=AF=E3=82=A8=E3=83=AA?=
 =?UTF-8?q?=E3=82=92stripMargin=E3=81=99=E3=82=8B=E3=82=88=E3=81=86?=
 =?UTF-8?q?=E3=81=AB?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../subhome/infrastructure/JdbcSubHomePersistence.scala      | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/infrastructure/JdbcSubHomePersistence.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/infrastructure/JdbcSubHomePersistence.scala
index 00aa194d2b..332b164871 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/infrastructure/JdbcSubHomePersistence.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/infrastructure/JdbcSubHomePersistence.scala
@@ -28,6 +28,7 @@ class JdbcSubHomePersistence[F[_]: Sync: NonServerThreadContextShift] extends Su
              |      location_y = values(location_y),
              |      location_z = values(location_z),
              |      world_name = values(world_name)"""
+          .stripMargin
           .update()
           .apply()
       }
@@ -52,7 +53,9 @@ class JdbcSubHomePersistence[F[_]: Sync: NonServerThreadContextShift] extends Su
               )
             )
           )
-          .list().apply()
+          .stripMargin
+          .list()
+          .apply()
       }.toMap
     }
 }

From 2440f53e78fffd02b9ccb60aa229f530134d3588 Mon Sep 17 00:00:00 2001
From: Kory <6561358+kory33@users.noreply.github.com>
Date: Mon, 2 Aug 2021 20:32:00 +0900
Subject: [PATCH 21/73] =?UTF-8?q?[update]=20=E3=83=9D=E3=82=A4=E3=83=B3?=
 =?UTF-8?q?=E3=83=88=E6=9C=AA=E8=A8=AD=E5=AE=9A=E3=81=AE=E6=99=82/subhome?=
 =?UTF-8?q?=20name=E3=81=8C=E3=82=A8=E3=83=A9=E3=83=BC=E3=82=92=E5=87=BA?=
 =?UTF-8?q?=E3=81=99=E3=82=88=E3=81=86=E3=81=AB=E3=81=99=E3=82=8B?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../subsystems/subhome/SubHomeAPI.scala       |  4 +
 .../bukkit/command/SubHomeCommand.scala       | 74 +++++++++++--------
 2 files changed, 46 insertions(+), 32 deletions(-)

diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/SubHomeAPI.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/SubHomeAPI.scala
index 36d6eab12d..089504bba3 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/SubHomeAPI.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/SubHomeAPI.scala
@@ -1,5 +1,6 @@
 package com.github.unchama.seichiassist.subsystems.subhome
 
+import cats.Functor
 import com.github.unchama.seichiassist.subsystems.subhome.domain.{OperationResult, SubHome, SubHomeId, SubHomeLocation}
 
 import java.util.UUID
@@ -10,6 +11,9 @@ trait SubHomeReadAPI[F[_]] {
 
   def get(ownerUuid: UUID, id: SubHomeId): F[Option[SubHome]]
 
+  final def configured(ownerUuid: UUID, id: SubHomeId)(implicit F: Functor[F]): F[Boolean] =
+    F.map(get(ownerUuid, id))(_.nonEmpty)
+
 }
 
 object SubHomeReadAPI {
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/bukkit/command/SubHomeCommand.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/bukkit/command/SubHomeCommand.scala
index 41a9ea323a..b5dd06e1ea 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/bukkit/command/SubHomeCommand.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/bukkit/command/SubHomeCommand.scala
@@ -1,6 +1,6 @@
 package com.github.unchama.seichiassist.subsystems.subhome.bukkit.command
 
-import cats.data.Kleisli
+import cats.Monad
 import cats.effect.implicits._
 import cats.effect.{ConcurrentEffect, IO}
 import com.github.unchama.chatinterceptor.CancellationReason.Overridden
@@ -11,8 +11,10 @@ import com.github.unchama.contextualexecutor.executors.{BranchedExecutor, EchoEx
 import com.github.unchama.seichiassist.SeichiAssist
 import com.github.unchama.seichiassist.commands.contextual.builder.BuilderTemplates.playerCommandBuilder
 import com.github.unchama.seichiassist.subsystems.subhome.bukkit.{LocationCodec, TeleportEffect}
+import com.github.unchama.seichiassist.subsystems.subhome.domain.OperationResult.RenameResult
 import com.github.unchama.seichiassist.subsystems.subhome.domain.{SubHome, SubHomeId}
 import com.github.unchama.seichiassist.subsystems.subhome.{SubHomeAPI, SubHomeReadAPI, SubHomeWriteAPI}
+import com.github.unchama.targetedeffect.TargetedEffect
 import com.github.unchama.targetedeffect.commandsender.MessageEffect
 import org.bukkit.ChatColor._
 import org.bukkit.command.TabExecutor
@@ -47,6 +49,10 @@ object SubHomeCommand {
       onMissingArguments = printDescriptionExecutor
     )
 
+  private def subHomeNotSetMessage(id: SubHomeId): List[String] = List(
+    s"${YELLOW}指定されたサブホームポイントが設定されていません。"
+  )
+
   def executor[
     F[_]
     : SubHomeAPI
@@ -122,41 +128,45 @@ object SubHomeCommand {
     F[_]
     : ConcurrentEffect
     : NonServerThreadContextShift
-    : SubHomeWriteAPI
+    : SubHomeAPI
   ](implicit scope: ChatInterceptionScope) = argsAndSenderConfiguredBuilder
     .execution { context =>
       val subHomeId = SubHomeId(context.args.parsed.head.asInstanceOf[Int])
 
-      IO.pure {
-        val sendInterceptionMessage =
-          MessageEffect(List(
-            s"サブホームポイント${subHomeId}に設定する名前をチャットで入力してください",
-            s"$YELLOW※入力されたチャット内容は他のプレイヤーには見えません"
-          ))
-
-        val sendCancellationMessage =
-          MessageEffect(s"${YELLOW}入力がキャンセルされました。")
-
-        def sendCompletionMessage(inputName: String) =
-          MessageEffect(List(
-            s"${GREEN}サブホームポイント${subHomeId}の名前を",
-            s"$GREEN${inputName}に更新しました"
-          ))
-
-        import com.github.unchama.generic.syntax._
-
-        sendInterceptionMessage.followedBy(Kleisli { player =>
-          val uuid = player.getUniqueId
-
-          scope.interceptFrom(uuid).flatMap {
-            case Left(newName) =>
-              SubHomeWriteAPI[F].rename(uuid, subHomeId)(newName).toIO >>
-                sendCompletionMessage(newName)(player)
-            case Right(Overridden) => sendCancellationMessage(player)
-            case Right(_) => IO.pure(())
-          }
-        })
-      }
+      val player = context.sender
+      val uuid = player.getUniqueId
+
+      val instruction = List(
+        s"サブホームポイント${subHomeId}に設定する名前をチャットで入力してください",
+        s"$YELLOW※入力されたチャット内容は他のプレイヤーには見えません"
+      )
+
+      def doneMessage(inputName: String) = List(
+        s"${GREEN}サブホームポイント${subHomeId}の名前を",
+        s"$GREEN${inputName}に更新しました"
+      )
+
+      val cancelledInputMessage = List(
+        s"${YELLOW}入力がキャンセルされました。"
+      )
+
+      for {
+        _ <- Monad[IO].ifM(SubHomeReadAPI[F].configured(uuid, subHomeId).toIO)(
+          MessageEffect(instruction)(player) >>
+            scope.interceptFrom(uuid).flatMap {
+              case Left(newName) =>
+                SubHomeWriteAPI[F].rename(uuid, subHomeId)(newName).toIO.flatMap {
+                  case RenameResult.Done =>
+                    MessageEffect(doneMessage(newName))(player)
+                  case RenameResult.NotFound =>
+                    MessageEffect(subHomeNotSetMessage(subHomeId))(player)
+                }
+              case Right(Overridden) => MessageEffect(cancelledInputMessage)(player)
+              case Right(_) => IO.unit
+            },
+          MessageEffect(subHomeNotSetMessage(subHomeId))(player)
+        )
+      } yield TargetedEffect.emptyEffect
     }
     .build()
 }
\ No newline at end of file

From 808c74839f594a3baf9f50c62793ee33d5481782 Mon Sep 17 00:00:00 2001
From: Kory <6561358+kory33@users.noreply.github.com>
Date: Mon, 2 Aug 2021 20:38:03 +0900
Subject: [PATCH 22/73] =?UTF-8?q?[fix]=20=E3=82=B5=E3=83=96=E3=83=9B?=
 =?UTF-8?q?=E3=83=BC=E3=83=A0=E3=81=AE=E5=90=8D=E5=89=8D=E3=81=8COption?=
 =?UTF-8?q?=E3=81=A8=E3=81=97=E3=81=A6=E8=A1=A8=E7=A4=BA=E3=81=95=E3=82=8C?=
 =?UTF-8?q?=E3=81=A6=E3=81=84=E3=81=9F=E3=81=AE=E3=82=92=E4=BF=AE=E6=AD=A3?=
 =?UTF-8?q?=E3=81=99=E3=82=8B?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../unchama/seichiassist/menus/HomeMenu.scala | 24 +++++++++++++++----
 1 file changed, 19 insertions(+), 5 deletions(-)

diff --git a/src/main/scala/com/github/unchama/seichiassist/menus/HomeMenu.scala b/src/main/scala/com/github/unchama/seichiassist/menus/HomeMenu.scala
index a031f90482..73fbe13783 100644
--- a/src/main/scala/com/github/unchama/seichiassist/menus/HomeMenu.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/menus/HomeMenu.scala
@@ -168,21 +168,35 @@ object HomeMenu extends Menu {
       } yield {
         val lore = subhomeOpt match {
           case None => List(s"${GRAY}サブホームポイント$subHomeId", s"${GRAY}ポイント未設定")
-          case Some(SubHome(name, location)) =>
+          case Some(SubHome(optionName, location)) =>
             val worldName = {
               ManagedWorld
                 .fromName(location.worldName).map(_.japaneseName)
                 .getOrElse(location.worldName)
             }
 
-            List(
-              s"${GRAY}サブホームポイント${subHomeId}は",
-              s"$GRAY$name",
-              s"${GRAY}と名付けられています",
+            val nameStatus = optionName match {
+              case Some(name) => List(
+                s"${GRAY}サブホームポイント${subHomeId}は",
+                s"$GRAY$name",
+                s"${GRAY}と名付けられています",
+              )
+              case None => List(
+                s"${GRAY}サブホームポイント${subHomeId}は",
+                s"${GRAY}名前が未設定です",
+              )
+            }
+
+            val commandInfo = List(
               s"$DARK_RED${UNDERLINE}クリックで名称変更",
               s"${DARK_GRAY}command->[/subhome name $subHomeId]",
+            )
+
+            val coordinates = List(
               s"$GRAY$worldName x:${location.x} y:${location.y} z:${location.z}"
             )
+
+            nameStatus ++ commandInfo ++ coordinates
         }
 
         Button(

From 27cbeeb4a33add4fe195738ea7d348dae9633ef3 Mon Sep 17 00:00:00 2001
From: Kory <6561358+kory33@users.noreply.github.com>
Date: Mon, 2 Aug 2021 22:10:27 +0900
Subject: [PATCH 23/73] =?UTF-8?q?[fix]=20subhome=20set=E3=81=A7=E5=90=8D?=
 =?UTF-8?q?=E5=89=8D=E3=81=8C=E6=B6=88=E3=81=88=E3=81=AA=E3=81=84=E3=82=88?=
 =?UTF-8?q?=E3=81=86=E3=81=AB=E3=81=99=E3=82=8B?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../subsystems/subhome/SubHomeAPI.scala             |  2 +-
 .../seichiassist/subsystems/subhome/System.scala    |  4 ++--
 .../subhome/bukkit/command/SubHomeCommand.scala     |  8 +++-----
 .../subhome/domain/SubHomePersistence.scala         | 13 +++++++++++++
 4 files changed, 19 insertions(+), 8 deletions(-)

diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/SubHomeAPI.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/SubHomeAPI.scala
index 089504bba3..ff4547d97e 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/SubHomeAPI.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/SubHomeAPI.scala
@@ -24,7 +24,7 @@ object SubHomeReadAPI {
 
 trait SubHomeWriteAPI[F[_]] {
 
-  def upsertLocation(ownerUuid: UUID, id: SubHomeId, location: SubHomeLocation): F[Unit]
+  def upsertLocation(ownerUuid: UUID, id: SubHomeId)(location: SubHomeLocation): F[Unit]
 
   def rename(ownerUuid: UUID, id: SubHomeId)(name: String): F[OperationResult.RenameResult]
 
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/System.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/System.scala
index 752880de03..c44dadd21e 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/System.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/System.scala
@@ -26,8 +26,8 @@ object System {
 
     new System[F] {
       override implicit val api: SubHomeAPI[F] = new SubHomeAPI[F] {
-        override def upsertLocation(ownerUuid: UUID, id: SubHomeId, location: SubHomeLocation): F[Unit] =
-          persistence.upsert(ownerUuid, id)(SubHome(None, location))
+        override def upsertLocation(ownerUuid: UUID, id: SubHomeId)(location: SubHomeLocation): F[Unit] =
+          persistence.upsertLocation(ownerUuid, id)(location)
         override def rename(ownerUuid: UUID, id: SubHomeId)(name: String): F[RenameResult] =
           persistence.rename(ownerUuid, id)(name)
         override def get(ownerUuid: UUID, id: SubHomeId): F[Option[SubHome]] =
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/bukkit/command/SubHomeCommand.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/bukkit/command/SubHomeCommand.scala
index b5dd06e1ea..eaa25d45f7 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/bukkit/command/SubHomeCommand.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/bukkit/command/SubHomeCommand.scala
@@ -111,13 +111,11 @@ object SubHomeCommand {
       val subHomeId = SubHomeId(context.args.parsed.head.asInstanceOf[Int])
       val player = context.sender
 
+      val subHomeLocation = LocationCodec.fromBukkitLocation(player.getLocation)
+
       val eff = for {
         _ <- NonServerThreadContextShift[F].shift
-        _ <- SubHomeWriteAPI[F].upsertLocation(
-          player.getUniqueId,
-          subHomeId,
-          LocationCodec.fromBukkitLocation(player.getLocation)
-        )
+        _ <- SubHomeWriteAPI[F].upsertLocation(player.getUniqueId, subHomeId)(subHomeLocation)
       } yield MessageEffect(s"現在位置をサブホームポイント${subHomeId}に設定しました")
 
       eff.toIO
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/domain/SubHomePersistence.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/domain/SubHomePersistence.scala
index dbc63d21c8..6fe9150e75 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/domain/SubHomePersistence.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/subhome/domain/SubHomePersistence.scala
@@ -34,6 +34,19 @@ trait SubHomePersistence[F[_]] {
   final def get(ownerUuid: UUID, id: SubHomeId)(implicit F: Functor[F]): F[Option[SubHome]] =
     list(ownerUuid).map(_.get(id))
 
+  /**
+   * 指定されたidのサブホームを登録する。idの範囲などのバリデーションはしない。
+   *
+   * サブホームがすでに存在した場合、古いサブホームの名前を新しいサブホームへと引き継ぐ。
+   */
+  final def upsertLocation(ownerUuid: UUID, id: SubHomeId)(location: SubHomeLocation)
+                          (implicit F: Monad[F]): F[Unit] =
+    for {
+      old <- get(ownerUuid, id)
+      newSubHome = SubHome(old.flatMap(_.name), location)
+      _ <- upsert(ownerUuid, id)(newSubHome)
+    } yield ()
+
   /**
    * 指定されたサブホームをnon-atomicに更新する。存在しないサブホームが指定された場合何も行わない。
    *

From e43641bbf3063b096801d74b0c7d87e0ec7099d9 Mon Sep 17 00:00:00 2001
From: Kory <6561358+kory33@users.noreply.github.com>
Date: Mon, 2 Aug 2021 22:15:08 +0900
Subject: [PATCH 24/73] =?UTF-8?q?[fix]=20presentSystem=E3=81=8CwiredSubsys?=
 =?UTF-8?q?tem=E3=81=AB=E5=85=A5=E3=81=A3=E3=81=A6=E3=81=84=E3=81=AA?=
 =?UTF-8?q?=E3=81=84=E3=81=AE=E3=82=92=E4=BF=AE=E6=AD=A3=E3=81=99=E3=82=8B?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../unchama/seichiassist/SeichiAssist.scala   | 21 ++++++++++---------
 1 file changed, 11 insertions(+), 10 deletions(-)

diff --git a/src/main/scala/com/github/unchama/seichiassist/SeichiAssist.scala b/src/main/scala/com/github/unchama/seichiassist/SeichiAssist.scala
index c894f13266..b06043e8d1 100644
--- a/src/main/scala/com/github/unchama/seichiassist/SeichiAssist.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/SeichiAssist.scala
@@ -311,6 +311,15 @@ class SeichiAssist extends JavaPlugin() {
     subhome.System.wired
   }
 
+  lazy val presentSystem: Subsystem[IO] = {
+    import PluginExecutionContexts.{asyncShift, onMainThread}
+
+    implicit val effectEnvironment: EffectEnvironment = DefaultEffectEnvironment
+    implicit val concurrentEffect: ConcurrentEffect[IO] = IO.ioConcurrentEffect(asyncShift)
+    implicit val uuidToLastSeenName: UuidToLastSeenName[IO] = new GlobalPlayerAccessor[IO]
+    subsystems.present.System.wired
+  }
+
   private lazy val wiredSubsystems: List[Subsystem[IO]] = List(
     mebiusSystem,
     expBottleStackSystem,
@@ -328,7 +337,8 @@ class SeichiAssist extends JavaPlugin() {
     fourDimensionalPocketSystem,
     gachaPointSystem,
     discordNotificationSystem,
-    subhomeSystem
+    subhomeSystem,
+    presentSystem
   )
 
   private lazy val buildAssist: BuildAssist = {
@@ -370,15 +380,6 @@ class SeichiAssist extends JavaPlugin() {
     )
   }
 
-  lazy val presentSystem: Subsystem[IO] = {
-    import PluginExecutionContexts.{asyncShift, onMainThread}
-
-    implicit val effectEnvironment: EffectEnvironment = DefaultEffectEnvironment
-    implicit val concurrentEffect: ConcurrentEffect[IO] = IO.ioConcurrentEffect(asyncShift)
-    implicit val uuidToLastSeenName: UuidToLastSeenName[IO] = new GlobalPlayerAccessor[IO]
-    subsystems.present.System.wired
-  }
-
   //endregion
 
   private implicit val _akkaSystem: ActorSystem = ConfiguredActorSystemProvider("reference.conf").provide()

From c94ab8cef1270d09eb73405d70746e2c38152e85 Mon Sep 17 00:00:00 2001
From: Kory <6561358+kory33@users.noreply.github.com>
Date: Mon, 2 Aug 2021 22:25:38 +0900
Subject: [PATCH 25/73] [fix] fix #1049

---
 .../bukkit/GiftItemInterpreter.scala               | 14 ++++----------
 1 file changed, 4 insertions(+), 10 deletions(-)

diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/seichilevelupgift/bukkit/GiftItemInterpreter.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/seichilevelupgift/bukkit/GiftItemInterpreter.scala
index 329a521e7d..682d21ed36 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/seichilevelupgift/bukkit/GiftItemInterpreter.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/seichilevelupgift/bukkit/GiftItemInterpreter.scala
@@ -1,18 +1,14 @@
 package com.github.unchama.seichiassist.subsystems.seichilevelupgift.bukkit
 
 import cats.data.Kleisli
-import cats.effect.{IO, LiftIO, Sync, SyncIO}
-import cats.kernel.Monoid
-import com.github.unchama.seichiassist.SeichiAssist
+import cats.effect.Sync
 import com.github.unchama.minecraft.actions.OnMinecraftServerThread
-import com.github.unchama.seichiassist.concurrent.PluginExecutionContexts.onMainThread
 import com.github.unchama.seichiassist.data.{GachaSkullData, ItemData}
 import com.github.unchama.seichiassist.subsystems.seichilevelupgift.domain.Gift
 import com.github.unchama.seichiassist.subsystems.seichilevelupgift.domain.Gift.Item
 import com.github.unchama.seichiassist.util.Util.grantItemStacksEffect
-import com.github.unchama.targetedeffect.{SequentialEffect, TargetedEffect}
-import com.github.unchama.targetedeffect.commandsender.{MessageEffect, MessageEffectF}
-import org.bukkit.command.CommandSender
+import com.github.unchama.targetedeffect.SequentialEffect
+import com.github.unchama.targetedeffect.commandsender.MessageEffectF
 import org.bukkit.entity.Player
 
 /**
@@ -28,11 +24,9 @@ class GiftItemInterpreter[F[_] : OnMinecraftServerThread : Sync] extends (Gift.I
       case Item.Elsa => ItemData.getElsa(1)
     }
 
-    import cats.implicits._
-    import cats.effect.implicits._
     SequentialEffect(
       MessageEffectF[F]("レベルアップ記念のアイテムを配布しました。"),
-      grantItemStacksEffect[F]()
+      grantItemStacksEffect[F](itemStack)
     )
   }
 

From 638852c870b6d8e84c15fb4efc3f92c9060f06fe Mon Sep 17 00:00:00 2001
From: Kory <6561358+kory33@users.noreply.github.com>
Date: Mon, 2 Aug 2021 22:42:23 +0900
Subject: [PATCH 26/73] [fix] fix #1105

---
 .../seichiassist/subsystems/breakcountbar/System.scala |  6 ++----
 .../ExpBarSynchronizationRepositoryTemplate.scala      | 10 ++++++----
 .../seichiassist/subsystems/manabar/System.scala       |  2 +-
 .../application/ManaBarSynchronizationRepository.scala | 10 ++++++----
 4 files changed, 15 insertions(+), 13 deletions(-)

diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/breakcountbar/System.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/breakcountbar/System.scala
index 0cd44c3496..ed011dd155 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/breakcountbar/System.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/breakcountbar/System.scala
@@ -57,10 +57,8 @@ object System {
         ContextCoercion {
           BukkitRepositoryControls.createHandles(
             RepositoryDefinition.Phased.TwoPhased(
-              ExpBarSynchronizationRepositoryTemplate.initialization[G, F, Player](
-                breakCountReadAPI.seichiAmountUpdates,
-                visibilityValues
-              )(CreateFreshBossBar.in[G, F]),
+              ExpBarSynchronizationRepositoryTemplate
+                .initialization[G, F, Player](breakCountReadAPI, visibilityValues)(CreateFreshBossBar.in[G, F]),
               ExpBarSynchronizationRepositoryTemplate.finalization[G, F, Player]
             )
           )
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/breakcountbar/application/ExpBarSynchronizationRepositoryTemplate.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/breakcountbar/application/ExpBarSynchronizationRepositoryTemplate.scala
index fe26524515..bafa03ef2f 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/breakcountbar/application/ExpBarSynchronizationRepositoryTemplate.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/breakcountbar/application/ExpBarSynchronizationRepositoryTemplate.scala
@@ -9,7 +9,7 @@ import com.github.unchama.generic.effect.EffectExtra
 import com.github.unchama.generic.effect.stream.StreamExtra
 import com.github.unchama.minecraft.algebra.HasUuid
 import com.github.unchama.minecraft.objects.MinecraftBossBar
-import com.github.unchama.seichiassist.subsystems.breakcount.domain.SeichiAmountData
+import com.github.unchama.seichiassist.subsystems.breakcount.BreakCountReadAPI
 import com.github.unchama.seichiassist.subsystems.breakcountbar.domain.BreakCountBarVisibility
 import io.chrisdavenport.log4cats.ErrorLogger
 
@@ -38,7 +38,7 @@ object ExpBarSynchronizationRepositoryTemplate {
     G[_] : Sync,
     F[_] : ConcurrentEffect : ContextCoercion[G, *[_]] : ErrorLogger,
     Player: HasUuid,
-  ](breakCountValues: fs2.Stream[F, (Player, SeichiAmountData)],
+  ](breakCountReadAPI: BreakCountReadAPI[F, G, Player],
     visibilityValues: fs2.Stream[F, (Player, BreakCountBarVisibility)])
    (createFreshBossBar: G[BossBarWithPlayer[F, Player]])
   : TwoPhasedRepositoryInitialization[G, Player, RepositoryValueType[F, Player]] =
@@ -46,8 +46,10 @@ object ExpBarSynchronizationRepositoryTemplate {
       for {
         bossBar <- createFreshBossBar
 
-        synchronization = breakCountValues
-          .through(StreamExtra.valuesWithKeyOfSameUuidAs(player))
+        synchronization = fs2.Stream
+          .eval(breakCountReadAPI.seichiAmountDataRepository(player).read)
+          .translate(ContextCoercion.asFunctionK[G, F])
+          .append(breakCountReadAPI.seichiAmountUpdates.through(StreamExtra.valuesWithKeyOfSameUuidAs(player)))
           .evalTap(BreakCountBarManipulation.write(_, bossBar))
 
         switching = visibilityValues
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/manabar/System.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/manabar/System.scala
index e8f755a588..afbfecd0cb 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/manabar/System.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/manabar/System.scala
@@ -20,7 +20,7 @@ object System {
     import com.github.unchama.minecraft.bukkit.algebra.BukkitPlayerHasUuid.instance
 
     val definition =
-      ManaBarSynchronizationRepository.withContext(manaApi.manaAmountUpdates)(CreateFreshBossBar.in[G, F])
+      ManaBarSynchronizationRepository.withContext(manaApi)(CreateFreshBossBar.in[G, F])
 
     BukkitRepositoryControls.createHandles(definition).map { control =>
       new Subsystem[F] {
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/manabar/application/ManaBarSynchronizationRepository.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/manabar/application/ManaBarSynchronizationRepository.scala
index 340532d425..a0650b8e0d 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/manabar/application/ManaBarSynchronizationRepository.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/manabar/application/ManaBarSynchronizationRepository.scala
@@ -8,7 +8,7 @@ import com.github.unchama.generic.effect.EffectExtra
 import com.github.unchama.generic.effect.stream.StreamExtra
 import com.github.unchama.minecraft.algebra.HasUuid
 import com.github.unchama.minecraft.objects.MinecraftBossBar
-import com.github.unchama.seichiassist.subsystems.mana.domain.LevelCappedManaAmount
+import com.github.unchama.seichiassist.subsystems.mana.ManaReadApi
 import io.chrisdavenport.log4cats.ErrorLogger
 
 object ManaBarSynchronizationRepository {
@@ -21,7 +21,7 @@ object ManaBarSynchronizationRepository {
     G[_] : Sync,
     F[_] : ConcurrentEffect : ContextCoercion[G, *[_]] : ErrorLogger,
     Player: HasUuid
-  ](manaValues: fs2.Stream[F, (Player, LevelCappedManaAmount)])
+  ](manaApi: ManaReadApi[F, G, Player])
    (createFreshBossBar: G[BossBarWithPlayer[F, Player]]): RepositoryDefinition[G, Player, _] = {
     FiberAdjoinedRepositoryDefinition.extending {
       RepositoryDefinition.Phased.SinglePhased
@@ -30,8 +30,10 @@ object ManaBarSynchronizationRepository {
       val (bossBar, promise) = pair
 
       val synchronization =
-        manaValues
-          .through(StreamExtra.valuesWithKeyOfSameUuidAs(player))
+        fs2.Stream
+          .eval(manaApi.readManaAmount(player))
+          .translate(ContextCoercion.asFunctionK[G, F])
+          .append(manaApi.manaAmountUpdates.through(StreamExtra.valuesWithKeyOfSameUuidAs(player)))
           .evalTap(ManaBarManipulation.write[F](_, bossBar))
 
       val programToRunAsync =

From 9295260347677127b595aacff3d2e091e141efd3 Mon Sep 17 00:00:00 2001
From: Kory <6561358+kory33@users.noreply.github.com>
Date: Mon, 2 Aug 2021 23:10:44 +0900
Subject: [PATCH 27/73] =?UTF-8?q?[clean]=20interpolation=E3=82=92=E9=81=BF?=
 =?UTF-8?q?=E3=81=91=E3=82=8B=E3=81=9F=E3=82=81=E3=81=AB=E3=82=82=E3=83=86?=
 =?UTF-8?q?=E3=83=BC=E3=83=96=E3=83=AB=E5=90=8D=E3=81=AA=E3=81=A9=E3=82=92?=
 =?UTF-8?q?=E3=83=8F=E3=83=BC=E3=83=89=E3=82=B3=E3=83=BC=E3=83=89=E3=81=99?=
 =?UTF-8?q?=E3=82=8B?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../JdbcBackedPresentPersistence.scala        | 54 +++++++++----------
 1 file changed, 24 insertions(+), 30 deletions(-)

diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/present/infrastructure/JdbcBackedPresentPersistence.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/present/infrastructure/JdbcBackedPresentPersistence.scala
index a52c54ae78..a21a08c60f 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/present/infrastructure/JdbcBackedPresentPersistence.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/present/infrastructure/JdbcBackedPresentPersistence.scala
@@ -2,9 +2,9 @@ package com.github.unchama.seichiassist.subsystems.present.infrastructure
 
 import cats.effect.Sync
 import com.github.unchama.seichiassist.subsystems.present.domain.PresentClaimingState
-import eu.timepit.refined.numeric.{NonNegative, Positive}
 import eu.timepit.refined.api.Refined
 import eu.timepit.refined.auto._
+import eu.timepit.refined.numeric.Positive
 import org.bukkit.inventory.ItemStack
 import scalikejdbc._
 
@@ -15,25 +15,20 @@ import java.util.UUID
  */
 class JdbcBackedPresentPersistence[F[_] : Sync] extends PresentPersistence[F] {
   override type PresentID = Int
-  private final val definitionTable = "present"
-  private final val stateTable = "present_state"
-  private final val claimingStateColumn = "claimed"
-  private final val presentIdColumn = "present_id"
-  private final val itemStackColumn = "itemstack"
 
   override def define(itemstack: ItemStack): F[PresentID] = Sync[F].delay {
     val stackAsBlob = ItemStackBlobProxy.itemStackToBlob(itemstack)
     DB.localTx { implicit session =>
       // プレゼントのIDはauto_incrementなので明示的に指定しなくて良い
-      sql"""INSERT INTO $definitionTable ($itemStackColumn) VALUES ($stackAsBlob)"""
+      sql"""INSERT INTO present (itemstack) VALUES ($stackAsBlob)"""
         .execute()
         .apply()
 
       val newPresentID = DB.readOnly { implicit session =>
         // ここで、itemstackは同じItemStackであるプレゼントが複数存在させたいケースを考慮して、UNIQUEではない。
         // 他方、present_idは主キーであり、AUTO_INCREMENTであることから単調増加なので、単純にMAXを取れば良い。
-        sql"""SELECT MAX($presentIdColumn) FROM $definitionTable WHERE $itemStackColumn = $stackAsBlob"""
-          .map { rs => rs.int(presentIdColumn) }
+        sql"""SELECT MAX(present_id) FROM present WHERE itemstack = $stackAsBlob"""
+          .map { rs => rs.int("present_id") }
           .first()
           .apply()
           // 上でINSERTしてるんだから、必ず見つかるはず
@@ -51,12 +46,12 @@ class JdbcBackedPresentPersistence[F[_] : Sync] extends PresentPersistence[F] {
    */
   override def delete(presentId: PresentID): F[Unit] = Sync[F].delay {
     DB.localTx { implicit session =>
-      // 制約をかけているので$stateTableの方から先に消さないと整合性エラーを吐く
-      sql"""DELETE $stateTable WHERE $presentIdColumn = $presentId"""
+      // 制約をかけているのでpresent_stateの方から先に消さないと整合性エラーを吐く
+      sql"""DELETE FROM present_state WHERE present_id = $presentId"""
         .execute()
         .apply()
 
-      sql"""DELETE $definitionTable WHERE $presentIdColumn = $presentId"""
+      sql"""DELETE FROM present WHERE present_id = $presentId"""
         .execute()
         .apply()
     }
@@ -70,7 +65,7 @@ class JdbcBackedPresentPersistence[F[_] : Sync] extends PresentPersistence[F] {
       .toSeq
 
     DB.localTx { implicit session =>
-      sql"""INSERT INTO $stateTable VALUES (?, ?, ?)"""
+      sql"""INSERT INTO present_state VALUES (?, ?, ?)"""
         .batch(initialValues: _*)
         .apply()
     }
@@ -81,7 +76,7 @@ class JdbcBackedPresentPersistence[F[_] : Sync] extends PresentPersistence[F] {
 
     DB.localTx { implicit session =>
       // https://discord.com/channels/237758724121427969/565935041574731807/824107651985834004
-      sql"""DELETE FROM $stateTable WHERE $presentIdColumn = $presentID AND uuid IN ($scopeAsSQL)"""
+      sql"""DELETE FROM present_state WHERE present_id = $presentID AND uuid IN ($scopeAsSQL)"""
         .execute()
         .apply()
     }
@@ -89,7 +84,7 @@ class JdbcBackedPresentPersistence[F[_] : Sync] extends PresentPersistence[F] {
 
   override def markAsClaimed(presentId: PresentID, player: UUID): F[Unit] = Sync[F].delay {
     DB.localTx { implicit session =>
-      sql"""UPDATE $stateTable SET $claimingStateColumn = TRUE WHERE uuid = ${player.toString} AND $presentIdColumn = $presentId"""
+      sql"""UPDATE present_state SET claimed = TRUE WHERE uuid = ${player.toString} AND present_id = $presentId"""
         .update()
         .apply()
     }
@@ -97,10 +92,10 @@ class JdbcBackedPresentPersistence[F[_] : Sync] extends PresentPersistence[F] {
 
   override def mapping: F[Map[PresentID, ItemStack]] = Sync[F].delay {
     DB.readOnly { implicit session =>
-      sql"""SELECT $presentIdColumn, $itemStackColumn FROM $definitionTable"""
+      sql"""SELECT present_id, itemstack FROM present"""
         .map { rs =>
           (
-            rs.int(presentIdColumn),
+            rs.int("present_id"),
             unwrapItemStack(rs)
           )
         }
@@ -118,10 +113,11 @@ class JdbcBackedPresentPersistence[F[_] : Sync] extends PresentPersistence[F] {
       // ページネーションはIDを列挙するときにすでに完了している
       val associatedEntries = DB.readOnly { implicit session =>
         sql"""
-              SELECT $presentIdColumn, $claimingStateColumn
-        FROM $stateTable
-        WHERE uuid = ${player.toString} AND $presentIdColumn IN ($idSliceWithPagination)
-"""
+             |SELECT present_id, claimed
+             |FROM present_state
+             |WHERE uuid = ${player.toString} AND present_id IN ($idSliceWithPagination)
+        """
+          .stripMargin
           .map(wrapResultForState)
           .toList()
           .apply()
@@ -139,7 +135,7 @@ class JdbcBackedPresentPersistence[F[_] : Sync] extends PresentPersistence[F] {
       validPresentIDs = validPresentMapping.keys
     } yield {
       val associatedEntries = DB.readOnly { implicit session =>
-        sql"""SELECT $presentIdColumn, $claimingStateColumn FROM $stateTable WHERE uuid = ${player.toString}"""
+        sql"""SELECT present_id, claimed FROM present_state WHERE uuid = ${player.toString}"""
           .map(wrapResultForState)
           .list()
           .apply()
@@ -152,7 +148,7 @@ class JdbcBackedPresentPersistence[F[_] : Sync] extends PresentPersistence[F] {
 
   override def lookup(presentID: PresentID): F[Option[ItemStack]] = Sync[F].delay {
     DB.readOnly { implicit session =>
-      sql"""SELECT $itemStackColumn FROM $definitionTable WHERE $presentIdColumn = $presentID"""
+      sql"""SELECT itemstack FROM present WHERE present_id = $presentID"""
         .map(unwrapItemStack)
         .first()
         .apply()
@@ -163,10 +159,8 @@ class JdbcBackedPresentPersistence[F[_] : Sync] extends PresentPersistence[F] {
     Sync[F].delay {
       val offset = (page - 1) * perPage
       DB.readOnly { implicit session =>
-        sql"""SELECT $presentIdColumn ORDER BY $presentIdColumn LIMIT $perPage OFFSET $offset"""
-          .map { rs =>
-            rs.int(presentIdColumn)
-          }
+        sql"""SELECT present_id FROM present ORDER BY present_id LIMIT $perPage OFFSET $offset"""
+          .map { _.int("present_id") }
           .toList()
           .apply()
           .toSet
@@ -174,16 +168,16 @@ class JdbcBackedPresentPersistence[F[_] : Sync] extends PresentPersistence[F] {
     }
 
   private def wrapResultForState(rs: WrappedResultSet): (Int, PresentClaimingState) = {
-    val claimState = if (rs.boolean(claimingStateColumn))
+    val claimState = if (rs.boolean("claimed"))
       PresentClaimingState.Claimed
     else
       PresentClaimingState.NotClaimed
 
-    (rs.int(presentIdColumn), claimState)
+    (rs.int("present_id"), claimState)
   }
 
   private def unwrapItemStack(rs: WrappedResultSet): ItemStack = {
-    ItemStackBlobProxy.blobToItemStack(rs.string(itemStackColumn))
+    ItemStackBlobProxy.blobToItemStack(rs.string("itemstack"))
   }
 
   private def filledEntries(knownState: Map[PresentID, PresentClaimingState], validGlobalId: Iterable[PresentID]) = {

From d8bbe0552ce866c9d1b06c9924d93a16e29a7dc3 Mon Sep 17 00:00:00 2001
From: Kory <6561358+kory33@users.noreply.github.com>
Date: Tue, 3 Aug 2021 07:54:23 +0900
Subject: [PATCH 28/73] =?UTF-8?q?[update]=20=E7=94=9F=E6=88=90=E3=81=95?=
 =?UTF-8?q?=E3=82=8C=E3=81=9F=E3=82=AD=E3=83=BC=E3=82=92updateAndReturnGen?=
 =?UTF-8?q?eratedKey=E3=81=A7=E6=8C=81=E3=81=A3=E3=81=A6=E3=81=8F=E3=82=8B?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../db/migration/V1.7.2__Widen_present_id.sql | 23 ++++++++++++++
 .../PresentPersistence.scala                  | 17 +++++-----
 .../JdbcBackedPresentPersistence.scala        | 31 +++++--------------
 3 files changed, 39 insertions(+), 32 deletions(-)
 create mode 100644 src/main/resources/db/migration/V1.7.2__Widen_present_id.sql
 rename src/main/scala/com/github/unchama/seichiassist/subsystems/present/{infrastructure => domain}/PresentPersistence.scala (91%)

diff --git a/src/main/resources/db/migration/V1.7.2__Widen_present_id.sql b/src/main/resources/db/migration/V1.7.2__Widen_present_id.sql
new file mode 100644
index 0000000000..979469faf2
--- /dev/null
+++ b/src/main/resources/db/migration/V1.7.2__Widen_present_id.sql
@@ -0,0 +1,23 @@
+use seichiassist;
+
+-- このマイグレーションが走る時点ではpresentシステムが正常に動作しておらず、
+-- データが入っていないため問題なし
+-- foreign key制約によってalter table出来なかったため作り直している
+DROP TABLE present_state;
+DROP TABLE present;
+
+-- プレゼントIDと実体の対応付け
+CREATE TABLE IF NOT EXISTS present(
+  present_id bigint PRIMARY KEY auto_increment,
+  itemstack  blob NOT NULL
+);
+
+-- プレイヤーがプレゼントを受け取ったかどうかをモデリングする
+CREATE TABLE IF NOT EXISTS present_state(
+  present_id bigint   NOT NULL,
+  uuid       char(36) NOT NULL,
+  claimed    boolean  NOT NULL,
+
+  PRIMARY KEY(present_id, uuid),
+  FOREIGN KEY present_id_in_present_state_must_exist_in_presents_table(present_id) REFERENCES present(present_id)
+);
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/present/infrastructure/PresentPersistence.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/present/domain/PresentPersistence.scala
similarity index 91%
rename from src/main/scala/com/github/unchama/seichiassist/subsystems/present/infrastructure/PresentPersistence.scala
rename to src/main/scala/com/github/unchama/seichiassist/subsystems/present/domain/PresentPersistence.scala
index ee61c688ca..beb0a1edea 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/present/infrastructure/PresentPersistence.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/present/domain/PresentPersistence.scala
@@ -1,9 +1,7 @@
-package com.github.unchama.seichiassist.subsystems.present.infrastructure
+package com.github.unchama.seichiassist.subsystems.present.domain
 
-import com.github.unchama.seichiassist.subsystems.present.domain.PresentClaimingState
 import eu.timepit.refined.api.Refined
 import eu.timepit.refined.numeric.Positive
-import org.bukkit.inventory.ItemStack
 
 import java.util.UUID
 
@@ -13,8 +11,8 @@ import java.util.UUID
  *   - 引数で渡される`PresentID`は対応するプレゼントが存在し、一意でなければならない
  *   - 返り値としての`PresentID`は対応するプレゼントが存在する
  */
-trait PresentPersistence[F[_]] {
-  type PresentID
+trait PresentPersistence[F[_], ItemStack] {
+  type PresentID = Long
 
   /**
    * 指定した[[ItemStack]]に対応するプレゼントを新しく定義する。
@@ -26,6 +24,7 @@ trait PresentPersistence[F[_]] {
 
   /**
    * 指定したPresentIDに対応するプレゼントを消去する。
+   *
    * @param presentID プレゼントID
    */
   def delete(presentID: PresentID): F[Unit]
@@ -51,7 +50,7 @@ trait PresentPersistence[F[_]] {
   /**
    * 永続化層でプレゼントを受け取ったことにする。
    *
-   * @param player プレイヤーのUUID
+   * @param player    プレイヤーのUUID
    * @param presentID プレゼントID
    * @return 永続化層への書き込みを行う作用
    */
@@ -77,15 +76,15 @@ trait PresentPersistence[F[_]] {
    *
    * この時 `pp.mappingWithPagination(A, 1, 5)` を呼び出すと、作用の中で計算される結果は次のとおりになる:
    *
-   *    `Map(1 -> Claimed, 2 -> Claimed, 3 -> Claimed, 4 -> NotClaimed, 5 -> Unavailable)`
+   * `Map(1 -> Claimed, 2 -> Claimed, 3 -> Claimed, 4 -> NotClaimed, 5 -> Unavailable)`
    *
    * 備考:
    *   - 実装によっては、[[fetchState]]などを呼び出して既知のエントリを全列挙する可能性がある。
    *   - このメソッドは一貫性のために[[fetchState]]のドキュメントにある制約を継承する。
    *
-   * @param player 調べる対象のプレイヤー
+   * @param player  調べる対象のプレイヤー
    * @param perPage ページごとのエントリの数
-   * @param page ページ、1オリジン
+   * @param page    ページ、1オリジン
    * @return ページネーションを計算して返す作用
    */
   def fetchStateWithPagination(player: UUID, perPage: Int Refined Positive, page: Int Refined Positive): F[Map[PresentID, PresentClaimingState]]
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/present/infrastructure/JdbcBackedPresentPersistence.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/present/infrastructure/JdbcBackedPresentPersistence.scala
index a21a08c60f..5613f763d3 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/present/infrastructure/JdbcBackedPresentPersistence.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/present/infrastructure/JdbcBackedPresentPersistence.scala
@@ -1,7 +1,7 @@
 package com.github.unchama.seichiassist.subsystems.present.infrastructure
 
 import cats.effect.Sync
-import com.github.unchama.seichiassist.subsystems.present.domain.PresentClaimingState
+import com.github.unchama.seichiassist.subsystems.present.domain.{PresentClaimingState, PresentPersistence}
 import eu.timepit.refined.api.Refined
 import eu.timepit.refined.auto._
 import eu.timepit.refined.numeric.Positive
@@ -13,31 +13,16 @@ import java.util.UUID
 /**
  * [[PresentPersistence]]のJDBC実装。この実装は[[PresentPersistence]]の制約を引き継ぐ。
  */
-class JdbcBackedPresentPersistence[F[_] : Sync] extends PresentPersistence[F] {
-  override type PresentID = Int
-
+class JdbcBackedPresentPersistence[F[_] : Sync] extends PresentPersistence[F, ItemStack] {
   override def define(itemstack: ItemStack): F[PresentID] = Sync[F].delay {
     val stackAsBlob = ItemStackBlobProxy.itemStackToBlob(itemstack)
     DB.localTx { implicit session =>
       // プレゼントのIDはauto_incrementなので明示的に指定しなくて良い
       sql"""INSERT INTO present (itemstack) VALUES ($stackAsBlob)"""
-        .execute()
+        .updateAndReturnGeneratedKey("present_id")
         .apply()
-
-      val newPresentID = DB.readOnly { implicit session =>
-        // ここで、itemstackは同じItemStackであるプレゼントが複数存在させたいケースを考慮して、UNIQUEではない。
-        // 他方、present_idは主キーであり、AUTO_INCREMENTであることから単調増加なので、単純にMAXを取れば良い。
-        sql"""SELECT MAX(present_id) FROM present WHERE itemstack = $stackAsBlob"""
-          .map { rs => rs.int("present_id") }
-          .first()
-          .apply()
-          // 上でINSERTしてるんだから、必ず見つかるはず
-          .get
       }
-
-      newPresentID
     }
-  }
 
   /**
    * 指定したPresentIDに対応するプレゼントを物理消去する。
@@ -95,7 +80,7 @@ class JdbcBackedPresentPersistence[F[_] : Sync] extends PresentPersistence[F] {
       sql"""SELECT present_id, itemstack FROM present"""
         .map { rs =>
           (
-            rs.int("present_id"),
+            rs.long("present_id"),
             unwrapItemStack(rs)
           )
         }
@@ -105,7 +90,7 @@ class JdbcBackedPresentPersistence[F[_] : Sync] extends PresentPersistence[F] {
     }
   }
 
-  override def fetchStateWithPagination(player: UUID, perPage: Int Refined Positive, page: Int Refined Positive): F[Map[Int, PresentClaimingState]] = {
+  override def fetchStateWithPagination(player: UUID, perPage: Int Refined Positive, page: Int Refined Positive): F[Map[PresentID, PresentClaimingState]] = {
     import cats.implicits._
     for {
       idSliceWithPagination <- idSliceWithPagination(perPage, page)
@@ -160,20 +145,20 @@ class JdbcBackedPresentPersistence[F[_] : Sync] extends PresentPersistence[F] {
       val offset = (page - 1) * perPage
       DB.readOnly { implicit session =>
         sql"""SELECT present_id FROM present ORDER BY present_id LIMIT $perPage OFFSET $offset"""
-          .map { _.int("present_id") }
+          .map { _.long("present_id") }
           .toList()
           .apply()
           .toSet
       }
     }
 
-  private def wrapResultForState(rs: WrappedResultSet): (Int, PresentClaimingState) = {
+  private def wrapResultForState(rs: WrappedResultSet): (Long, PresentClaimingState) = {
     val claimState = if (rs.boolean("claimed"))
       PresentClaimingState.Claimed
     else
       PresentClaimingState.NotClaimed
 
-    (rs.int("present_id"), claimState)
+    (rs.long("present_id"), claimState)
   }
 
   private def unwrapItemStack(rs: WrappedResultSet): ItemStack = {

From b205f4cc903c940a154a960994953caf91bcca62 Mon Sep 17 00:00:00 2001
From: Kory <6561358+kory33@users.noreply.github.com>
Date: Tue, 3 Aug 2021 07:54:56 +0900
Subject: [PATCH 29/73] =?UTF-8?q?[clean]=20PresentCommand=E3=81=8C?=
 =?UTF-8?q?=E5=AE=9F=E8=A3=85=E3=81=A7=E3=81=AA=E3=81=8Finterface=E3=81=AB?=
 =?UTF-8?q?=E4=BE=9D=E5=AD=98=E3=81=99=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../bukkit/command/PresentCommand.scala       | 22 ++++++++++---------
 1 file changed, 12 insertions(+), 10 deletions(-)

diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/present/bukkit/command/PresentCommand.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/present/bukkit/command/PresentCommand.scala
index e24596dab2..e9b5b0a388 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/present/bukkit/command/PresentCommand.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/present/bukkit/command/PresentCommand.scala
@@ -11,8 +11,7 @@ import com.github.unchama.contextualexecutor.executors.{BranchedExecutor, EchoEx
 import com.github.unchama.minecraft.actions.OnMinecraftServerThread
 import com.github.unchama.seichiassist.commands.contextual.builder.BuilderTemplates.playerCommandBuilder
 import com.github.unchama.seichiassist.domain.actions.UuidToLastSeenName
-import com.github.unchama.seichiassist.subsystems.present.domain.PresentClaimingState
-import com.github.unchama.seichiassist.subsystems.present.infrastructure.JdbcBackedPresentPersistence
+import com.github.unchama.seichiassist.subsystems.present.domain.{PresentClaimingState, PresentPersistence}
 import com.github.unchama.seichiassist.util.Util
 import com.github.unchama.targetedeffect.commandsender.MessageEffect
 import com.github.unchama.targetedeffect.{SequentialEffect, TargetedEffect}
@@ -22,6 +21,7 @@ import eu.timepit.refined.auto._
 import eu.timepit.refined.numeric.Positive
 import org.bukkit.command.TabExecutor
 import org.bukkit.entity.Player
+import org.bukkit.inventory.ItemStack
 import org.bukkit.{ChatColor, Material}
 
 /**
@@ -59,7 +59,7 @@ class PresentCommand(implicit val ioOnMainThread: OnMinecraftServerThread[IO]) {
    *
    * 出力: 定義が成功した場合は、割り振られたアイテムのIDを表示する。失敗した場合は、適切なエラーメッセージを表示する。
    */
-  private def defineExecutor[F[_] : ConcurrentEffect : NonServerThreadContextShift](implicit persistence: JdbcBackedPresentPersistence[F]) =
+  private def defineExecutor[F[_] : ConcurrentEffect : NonServerThreadContextShift](implicit persistence: PresentPersistence[F, ItemStack]) =
     playerCommandBuilder
       .argumentsParsers(List())
       .execution { context =>
@@ -92,7 +92,7 @@ class PresentCommand(implicit val ioOnMainThread: OnMinecraftServerThread[IO]) {
    *
    * 出力: 操作の結果とそれに伴うメッセージ。
    */
-  private def deleteExecutor[F[_] : ConcurrentEffect : NonServerThreadContextShift](implicit persistence: JdbcBackedPresentPersistence[F]) =
+  private def deleteExecutor[F[_] : ConcurrentEffect : NonServerThreadContextShift](implicit persistence: PresentPersistence[F, ItemStack]) =
     playerCommandBuilder
       .argumentsParsers(List(presentIdParser))
       .execution { context =>
@@ -125,7 +125,7 @@ class PresentCommand(implicit val ioOnMainThread: OnMinecraftServerThread[IO]) {
    * 備考:
    *   - †: スペース区切り。
    */
-  private def grantRightExecutor[F[_] : ConcurrentEffect : NonServerThreadContextShift](implicit persistence: JdbcBackedPresentPersistence[F], globalPlayerAccessor: UuidToLastSeenName[F]) =
+  private def grantRightExecutor[F[_] : ConcurrentEffect : NonServerThreadContextShift](implicit persistence: PresentPersistence[F, ItemStack], globalPlayerAccessor: UuidToLastSeenName[F]) =
     playerCommandBuilder
       .argumentsParsers(
         List(
@@ -178,7 +178,7 @@ class PresentCommand(implicit val ioOnMainThread: OnMinecraftServerThread[IO]) {
    * 備考:
    *   - ✝: スペース区切り。
    */
-  private def revokeRightExecutor[F[_] : ConcurrentEffect : NonServerThreadContextShift](implicit persistence: JdbcBackedPresentPersistence[F], globalPlayerAccessor: UuidToLastSeenName[F]) =
+  private def revokeRightExecutor[F[_] : ConcurrentEffect : NonServerThreadContextShift](implicit persistence: PresentPersistence[F, ItemStack], globalPlayerAccessor: UuidToLastSeenName[F]) =
     playerCommandBuilder
       .argumentsParsers(
         List(
@@ -228,7 +228,7 @@ class PresentCommand(implicit val ioOnMainThread: OnMinecraftServerThread[IO]) {
    *
    * 出力: 受け取った場合は、その旨表示する。失敗した場合は、適切なエラーメッセージを表示する。
    */
-  private def claimExecutor[F[_] : ConcurrentEffect : NonServerThreadContextShift](implicit persistence: JdbcBackedPresentPersistence[F]) =
+  private def claimExecutor[F[_] : ConcurrentEffect : NonServerThreadContextShift](implicit persistence: PresentPersistence[F, ItemStack]) =
     playerCommandBuilder
       .argumentsParsers(List(presentIdParser))
       .execution { context =>
@@ -275,7 +275,7 @@ class PresentCommand(implicit val ioOnMainThread: OnMinecraftServerThread[IO]) {
    * 構文:
    *   - /present state
    */
-  private def showStateExecutor[F[_] : ConcurrentEffect : NonServerThreadContextShift](implicit persistence: JdbcBackedPresentPersistence[F]) = playerCommandBuilder
+  private def showStateExecutor[F[_] : ConcurrentEffect : NonServerThreadContextShift](implicit persistence: PresentPersistence[F, ItemStack]) = playerCommandBuilder
     .execution { context =>
       val eff = for {
         // off-main-thread
@@ -304,7 +304,7 @@ class PresentCommand(implicit val ioOnMainThread: OnMinecraftServerThread[IO]) {
    *
    *   - /present list &lt;page: PositiveInt&gt;
    */
-  private def listExecutor[F[_] : ConcurrentEffect : NonServerThreadContextShift](implicit persistence: JdbcBackedPresentPersistence[F]) =
+  private def listExecutor[F[_] : ConcurrentEffect : NonServerThreadContextShift](implicit persistence: PresentPersistence[F, ItemStack]) =
     playerCommandBuilder
       .argumentsParsers(List(Parsers.closedRangeInt(1, Int.MaxValue, MessageEffect("ページ数には1以上の数を指定してください。"))))
       .execution { context =>
@@ -359,7 +359,9 @@ class PresentCommand(implicit val ioOnMainThread: OnMinecraftServerThread[IO]) {
     )
   )
 
-  def executor[F[_] : ConcurrentEffect : NonServerThreadContextShift](implicit persistence: JdbcBackedPresentPersistence[F], globalPlayerAccessor: UuidToLastSeenName[F]): TabExecutor = BranchedExecutor(
+  def executor[
+    F[_] : ConcurrentEffect : NonServerThreadContextShift
+  ](implicit persistence: PresentPersistence[F, ItemStack], globalPlayerAccessor: UuidToLastSeenName[F]): TabExecutor = BranchedExecutor(
     Map(
       "define" -> defineExecutor,
       "delete" -> deleteExecutor,

From 16f9192a6c0989e83e99a57067e81ab216862879 Mon Sep 17 00:00:00 2001
From: Kory <6561358+kory33@users.noreply.github.com>
Date: Tue, 3 Aug 2021 08:20:45 +0900
Subject: [PATCH 30/73] =?UTF-8?q?[fix]=20updateAndReturnGeneratedKey?=
 =?UTF-8?q?=E3=81=AEindex=E6=8C=87=E5=AE=9A=E3=82=92=E6=B6=88=E3=81=99?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../present/infrastructure/JdbcBackedPresentPersistence.scala   | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/present/infrastructure/JdbcBackedPresentPersistence.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/present/infrastructure/JdbcBackedPresentPersistence.scala
index 5613f763d3..6673e96452 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/present/infrastructure/JdbcBackedPresentPersistence.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/present/infrastructure/JdbcBackedPresentPersistence.scala
@@ -19,7 +19,7 @@ class JdbcBackedPresentPersistence[F[_] : Sync] extends PresentPersistence[F, It
     DB.localTx { implicit session =>
       // プレゼントのIDはauto_incrementなので明示的に指定しなくて良い
       sql"""INSERT INTO present (itemstack) VALUES ($stackAsBlob)"""
-        .updateAndReturnGeneratedKey("present_id")
+        .updateAndReturnGeneratedKey
         .apply()
       }
     }

From e8a870601cc11f14dcce27fb4877e44fc582f498 Mon Sep 17 00:00:00 2001
From: Kory <6561358+kory33@users.noreply.github.com>
Date: Tue, 3 Aug 2021 12:18:59 +0900
Subject: [PATCH 31/73] =?UTF-8?q?[update]=20=E5=BC=95=E6=95=B0=E3=81=8C?=
 =?UTF-8?q?=E8=B6=B3=E3=82=8A=E3=81=AA=E3=81=8B=E3=81=A3=E3=81=9F=E6=99=82?=
 =?UTF-8?q?=E3=81=AE=E3=83=A1=E3=83=83=E3=82=BB=E3=83=BC=E3=82=B8=E3=82=92?=
 =?UTF-8?q?=E6=98=8E=E7=A2=BA=E3=81=AB?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/main/resources/plugin.yml                 |   2 +-
 .../executors/EchoExecutor.scala              |   2 +-
 .../executors/TraverseExecutor.scala          |  12 +
 .../bukkit/command/PresentCommand.scala       | 640 ++++++++++--------
 4 files changed, 357 insertions(+), 299 deletions(-)
 create mode 100644 src/main/scala/com/github/unchama/contextualexecutor/executors/TraverseExecutor.scala

diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml
index e9c064fc2a..6236a907fe 100644
--- a/src/main/resources/plugin.yml
+++ b/src/main/resources/plugin.yml
@@ -105,7 +105,7 @@ commands:
     permission-message: *denied
   present:
     description: send a gift
-    usage: /<command> [player name]
+    usage: /present <操作> (詳細については/present helpを参照してください)
     permission-message: *denied
   hat:
     description: set item in hand to head
diff --git a/src/main/scala/com/github/unchama/contextualexecutor/executors/EchoExecutor.scala b/src/main/scala/com/github/unchama/contextualexecutor/executors/EchoExecutor.scala
index afe06994e7..d5dec4d92f 100644
--- a/src/main/scala/com/github/unchama/contextualexecutor/executors/EchoExecutor.scala
+++ b/src/main/scala/com/github/unchama/contextualexecutor/executors/EchoExecutor.scala
@@ -8,6 +8,6 @@ import org.bukkit.command.CommandSender
 /**
  * 実行されたときに[effect]を送り返すだけの[ContextualExecutor].
  */
-class EchoExecutor(private val effect: TargetedEffect[CommandSender]) extends ContextualExecutor {
+case class EchoExecutor(effect: TargetedEffect[CommandSender]) extends ContextualExecutor {
   override def executeWith(rawContext: RawCommandContext): IO[Unit] = effect(rawContext.sender)
 }
\ No newline at end of file
diff --git a/src/main/scala/com/github/unchama/contextualexecutor/executors/TraverseExecutor.scala b/src/main/scala/com/github/unchama/contextualexecutor/executors/TraverseExecutor.scala
new file mode 100644
index 0000000000..617b3a0ede
--- /dev/null
+++ b/src/main/scala/com/github/unchama/contextualexecutor/executors/TraverseExecutor.scala
@@ -0,0 +1,12 @@
+package com.github.unchama.contextualexecutor.executors
+
+import cats.effect.IO
+import com.github.unchama.contextualexecutor.{ContextualExecutor, RawCommandContext}
+
+case class TraverseExecutor(executors: List[ContextualExecutor]) extends ContextualExecutor {
+  import cats.implicits._
+
+  override def executeWith(commandContext: RawCommandContext): IO[Unit] = {
+    executors.traverse(_.executeWith(commandContext)).void
+  }
+}
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/present/bukkit/command/PresentCommand.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/present/bukkit/command/PresentCommand.scala
index e9b5b0a388..3e6b8a5506 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/present/bukkit/command/PresentCommand.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/present/bukkit/command/PresentCommand.scala
@@ -7,10 +7,11 @@ import cats.implicits._
 import com.github.unchama.concurrent.NonServerThreadContextShift
 import com.github.unchama.contextualexecutor.ContextualExecutor
 import com.github.unchama.contextualexecutor.builder.Parsers
-import com.github.unchama.contextualexecutor.executors.{BranchedExecutor, EchoExecutor}
+import com.github.unchama.contextualexecutor.executors.{BranchedExecutor, EchoExecutor, TraverseExecutor}
 import com.github.unchama.minecraft.actions.OnMinecraftServerThread
 import com.github.unchama.seichiassist.commands.contextual.builder.BuilderTemplates.playerCommandBuilder
 import com.github.unchama.seichiassist.domain.actions.UuidToLastSeenName
+import com.github.unchama.seichiassist.subsystems.present.domain.OperationResult.DeleteResult
 import com.github.unchama.seichiassist.subsystems.present.domain.{PresentClaimingState, PresentPersistence}
 import com.github.unchama.seichiassist.util.Util
 import com.github.unchama.targetedeffect.commandsender.MessageEffect
@@ -48,332 +49,377 @@ class PresentCommand(implicit val ioOnMainThread: OnMinecraftServerThread[IO]) {
 
   private val noPermissionMessage = MessageEffect("You don't have the permission.")
 
-  /**
-   * 概要: メインハンドに持っているアイテムをプレゼントとして定義する。
-   * プレイヤーがプレゼントを受け取ることができるようになるには、必ずプレゼントを定義しなければならない。
-   *
-   * 権限ノード: `seichiassist.present.define`
-   *
-   * 構文:
-   *   - /present define
-   *
-   * 出力: 定義が成功した場合は、割り振られたアイテムのIDを表示する。失敗した場合は、適切なエラーメッセージを表示する。
-   */
-  private def defineExecutor[F[_] : ConcurrentEffect : NonServerThreadContextShift](implicit persistence: PresentPersistence[F, ItemStack]) =
-    playerCommandBuilder
-      .argumentsParsers(List())
-      .execution { context =>
-        val player = context.sender
-        if (!player.hasPermission("seichiassist.present.define")) {
-          IO.pure(noPermissionMessage)
-        } else {
-          val mainHandItem = player.getInventory.getItemInMainHand
-          if (mainHandItem.getType eq Material.AIR) {
-            // おそらくこれは意図した動作ではないのでエラーメッセージを表示する
-            IO.pure(MessageEffect("メインハンドに何も持っていません。プレゼントを定義するためには、メインハンドに対象アイテムを持ってください。"))
-          } else {
+  private object SubCommands {
+    object State {
+      val help: EchoExecutor = EchoExecutor(MessageEffect(List(
+        "/present state",
+        "    対象となっている全てのプレゼントを表示します"
+      )))
+
+      /**
+       * 概要: 全てのプレゼントのうち、実行プレイヤーが対象となっているプレゼントの受け取り状況を表示する。
+       * 実行プレイヤーが対象ではないプレゼントは表示されない。
+       *
+       * 構文:
+       *   - /present state
+       */
+      def executor[F[_] : ConcurrentEffect : NonServerThreadContextShift](implicit persistence: PresentPersistence[F, ItemStack]): ContextualExecutor = playerCommandBuilder
+        .execution { context =>
+          val eff = for {
+            // off-main-thread
+            _ <- NonServerThreadContextShift[F].shift
+            state <- persistence.fetchState(context.sender.getUniqueId)
+          } yield {
+            val mes = state
+              .toList
+              // 配布対象外のプレゼントを除外
+              .filter { case (_, state) => state != PresentClaimingState.Unavailable }
+              .map { case (id, state) =>
+                s"ID=$id: ${decoratePresentState(state)}"
+              }
+              .filter(_.nonEmpty)
+            MessageEffect(mes)
+          }
+
+          eff.toIO
+        }
+        .build()
+    }
+
+    object ListSubCommand {
+      val help: EchoExecutor = EchoExecutor(MessageEffect(List(
+        s"${ChatColor.GRAY}コマンドの構文: /present list <ページ数>"
+      )))
+
+      /**
+       * 概要: 実行プレイヤーと全てのプレゼントの受け取り状況をページネーションと共に表示する
+       *
+       * 構文:
+       *
+       *   - /present list &lt;page: PositiveInt&gt;
+       */
+      def executor[F[_] : ConcurrentEffect : NonServerThreadContextShift](implicit persistence: PresentPersistence[F, ItemStack]): ContextualExecutor =
+        playerCommandBuilder
+          .argumentsParsers(
+            List(
+              Parsers.closedRangeInt(1, Int.MaxValue, MessageEffect("ページ数には1以上の数を指定してください。"))
+            ),
+            onMissingArguments = help
+          )
+          .execution { context =>
+            val perPage: Int Refined Positive = 10
+            val page = refineV[Positive](context.args.parsed.head.asInstanceOf[Int]) match {
+              // argumentsParsersで1以上を指定しているのでここでコケることはないはず
+              case Left(l) => throw new AssertionError(s"positive int: failed. message: $l")
+              case Right(v) => v
+            }
+            val player = context.sender.getUniqueId
             val eff = for {
               _ <- NonServerThreadContextShift[F].shift
-              presentID <- persistence.define(mainHandItem)
+              states <- persistence.fetchStateWithPagination(player, perPage, page)
+              messageLine = states
+                .map { case (id, state) =>
+                  s"ID=$id: ${decoratePresentState(state)}"
+                }
+                .toList
             } yield {
-              MessageEffect(s"メインハンドに持ったアイテムをプレゼントとして定義しました。IDは${presentID}です。")
+              MessageEffect(messageLine)
             }
             eff.toIO
           }
-        }
-
-      }
-      .build()
-
-  /**
-   * 概要: 指定したプレゼントを消去する。対応が失われるため、このコマンドの実行が完了した後、プレイヤーはそのプレゼントを受け取ることができなくなる。
-   *
-   * 権限ノード: `seichiassist.present.delete`
-   *
-   * 出力: 操作の結果とそれに伴うメッセージ。
-   */
-  private def deleteExecutor[F[_] : ConcurrentEffect : NonServerThreadContextShift](implicit persistence: PresentPersistence[F, ItemStack]) =
-    playerCommandBuilder
-      .argumentsParsers(List(presentIdParser))
-      .execution { context =>
-        if (!context.sender.hasPermission("seichiassist.present.delete")) {
-          IO.pure(noPermissionMessage)
-        } else {
-          val presentId = context.args.parsed.head.asInstanceOf[Int]
-          val eff = for {
-            _ <- NonServerThreadContextShift[F].shift
-            _ <- persistence.delete(presentId)
-          } yield MessageEffect(s"IDが${presentId}のプレゼントの消去は正常に行われました。")
+          .build()
+    }
 
-          eff.toIO
-        }
-      }
-      .build()
+    object Claim {
+      val help: EchoExecutor = EchoExecutor(MessageEffect(List(
+        "/present claim <プレゼントID>",
+        "    プレゼントを受け取ります",
+      )))
 
-  /**
-   * 概要: プレイヤーが指定されたプレゼントを受け取れるようにする。
-   *
-   * 権限ノード: `seichiassist.present.grant`
-   *
-   * 出力: 操作の結果とそれに伴うメッセージ
-   *
-   * コマンド構文:
-   *
-   *   - /present grant &lt;presentId: PresentID&gt; player &lt;...players^†^: PlayerName&gt;
-   *   - /present grant &lt;presentId: PresentID&gt; all
-   *
-   * 備考:
-   *   - †: スペース区切り。
-   */
-  private def grantRightExecutor[F[_] : ConcurrentEffect : NonServerThreadContextShift](implicit persistence: PresentPersistence[F, ItemStack], globalPlayerAccessor: UuidToLastSeenName[F]) =
-    playerCommandBuilder
-      .argumentsParsers(
-        List(
-          presentIdParser,
-          presentScopeModeParser,
-        )
-      )
-      .execution { context =>
-        if (!context.sender.hasPermission("seichiassist.present.grant")) {
-          IO.pure(noPermissionMessage)
-        } else {
-          // Parserを通した段階でargs[0]は "player" | "all" になっているのでこれでOK
-          val List(_presentId, mode) = context.args.parsed
-          val presentId = _presentId.asInstanceOf[Int]
-          val isGlobal = mode.asInstanceOf[String] == "all"
-          val eff = for {
-            _ <- NonServerThreadContextShift[F].shift
-            // TODO: 以下の処理は多分共通化できるがうまい方法が思いつかない
-            globalUUID2Name <- globalPlayerAccessor.entries
-            // 可変長引数には対応していないので`yetToBeParsed`を使う
-            restArg = context.args
-              // プレイヤー名は /[A-Za-z0-9_]{,16}/であるため空白が誤って解釈されることはない
-              .yetToBeParsed
-              // 連続した空白を消去
-              .filter(_.nonEmpty)
-            target = if (isGlobal)
-              globalUUID2Name.keys
-            else
-              globalUUID2Name.filter { case (_, name) => restArg.contains(name) }.keys
-            errorIfNobody = if (target.isEmpty) Some(MessageEffect("対象のプレイヤーが存在しません!")) else None
-            _ <- persistence.grant(presentId, target.toSet)
-          } yield errorIfNobody.getOrElse(MessageEffect(s"プレゼントIDが${presentId}のプレゼントを受け取れるプレイヤーを追加することに成功しました。"))
+      /**
+       * 概要: 指定されたIDのプレゼントを受け取れるかどうかテストする。
+       * 受け取れる場合は、プレイヤーのインベントリにアイテムを追加する。
+       * 受け取れない場合は、エラーメッセージを表示する。
+       *
+       * 構文:
+       *   - /present claim &lt;presentId: PresentID&gt;
+       *
+       * 出力: 受け取った場合は、その旨表示する。失敗した場合は、適切なエラーメッセージを表示する。
+       */
+      def executor[F[_] : ConcurrentEffect : NonServerThreadContextShift](implicit persistence: PresentPersistence[F, ItemStack]): ContextualExecutor =
+        playerCommandBuilder
+          .argumentsParsers(List(presentIdParser), onMissingArguments = help)
+          .execution { context =>
+            val player = context.sender.getUniqueId
+            val presentId = context.args.parsed.head.asInstanceOf[Int]
 
-          eff.toIO
-        }
-      }
-      .build()
+            // この明示的な型変数の指定は必要
+            val eff: F[TargetedEffect[Player]] = for {
+              _ <- NonServerThreadContextShift[F].shift
+              states <- persistence.fetchState(player)
+              claimState = states.getOrElse(presentId, PresentClaimingState.Unavailable)
+              effect <- claimState match {
+                case PresentClaimingState.Claimed =>
+                  Monad[F].pure(MessageEffect(s"ID: ${presentId}のプレゼントはすでに受け取っています。"))
+                case PresentClaimingState.NotClaimed =>
+                  for {
+                    _ <- persistence.markAsClaimed(presentId, player)
+                    item <- persistence.lookup(presentId)
+                  } yield {
+                    // 注釈: この明示的な型変数の指定は必要
+                    // see: https://discord.com/channels/237758724121427969/565935041574731807/823495317499805776
+                    item.fold[TargetedEffect[Player]](
+                      MessageEffect(s"ID: ${presentId}のプレゼントは存在しません。IDをお確かめください。")
+                    ) { item =>
+                      SequentialEffect(
+                        Util.grantItemStacksEffect[IO](item),
+                        MessageEffect(s"ID: ${presentId}のプレゼントを付与しました。")
+                      )
+                    }
+                  }
+                case PresentClaimingState.Unavailable =>
+                  Monad[F].pure(MessageEffect(s"ID: ${presentId}のプレゼントは存在しないか、あるいは配布対象ではありません。"))
+              }
+            } yield effect
 
-  /**
-   * 概要: プレイヤーがプレゼントを受け取れないようにする。
-   *
-   * 権限ノード: `seichiassist.present.revoke`
-   *
-   * 構文:
-   *   - /present revoke &lt;presentId: PresentID&gt; player &lt;...players^✝^: PlayerName&gt;
-   *   - /present revoke &lt;presentId: PresentID&gt; all
-   *
-   * 出力: 操作の結果とそれに伴うメッセージ。
-   *
-   * 備考:
-   *   - ✝: スペース区切り。
-   */
-  private def revokeRightExecutor[F[_] : ConcurrentEffect : NonServerThreadContextShift](implicit persistence: PresentPersistence[F, ItemStack], globalPlayerAccessor: UuidToLastSeenName[F]) =
-    playerCommandBuilder
-      .argumentsParsers(
-        List(
-          presentIdParser,
-          presentScopeModeParser,
-        )
-      )
-      .execution { context =>
-        if (!context.sender.hasPermission("seichiassist.present.revoke")) {
-          IO.pure(noPermissionMessage)
-        } else {
-          val args = context.args
-          val presentId = args.parsed.head.asInstanceOf[Int]
-          val isGlobal = args.parsed(1).asInstanceOf[String] == "all"
-          val eff = for {
-            _ <- NonServerThreadContextShift[F].shift
-            globalUUID2Name <- globalPlayerAccessor.entries
-            // 可変長引数には対応していないので`yetToBeParsed`を使う
-            restArg = args
-              // プレイヤー名は /[A-Za-z0-9_]{,16}/であるため空白が誤って解釈されることはない
-              .yetToBeParsed
-              // 連続した空白を消去
-              .filter(_.nonEmpty)
-            target = if (isGlobal)
-              globalUUID2Name.keys
-            else
-              globalUUID2Name.filter { case (_, name) => restArg.contains(name) }.keys
-            errorIfNobody = if (target.isEmpty) Some(MessageEffect("対象のプレイヤーが存在しません!")) else None
-            _ <- persistence.revoke(presentId, target.toSet)
-          } yield {
-            errorIfNobody.getOrElse(
-              MessageEffect(s"プレゼントIDが${presentId}のプレゼントを受け取ることができるプレイヤーの削除に成功しました。")
-            )
+            eff.toIO
           }
-          eff.toIO
-        }
-      }
-      .build()
+          .build()
+    }
 
-  /**
-   * 概要: 指定されたIDのプレゼントを受け取れるかどうかテストする。
-   * 受け取れる場合は、プレイヤーのインベントリにアイテムを追加する。
-   * 受け取れない場合は、エラーメッセージを表示する。
-   *
-   * 構文:
-   *   - /present claim &lt;presentId: PresentID&gt;
-   *
-   * 出力: 受け取った場合は、その旨表示する。失敗した場合は、適切なエラーメッセージを表示する。
-   */
-  private def claimExecutor[F[_] : ConcurrentEffect : NonServerThreadContextShift](implicit persistence: PresentPersistence[F, ItemStack]) =
-    playerCommandBuilder
-      .argumentsParsers(List(presentIdParser))
-      .execution { context =>
-        val player = context.sender.getUniqueId
-        val presentId = context.args.parsed.head.asInstanceOf[Int]
+    object Define {
+      val help: EchoExecutor = EchoExecutor(MessageEffect(List(
+        "/present define",
+        "    プレゼントを手に持っているアイテムで定義します",
+      )))
 
-        // この明示的な型変数の指定は必要
-        val eff: F[TargetedEffect[Player]] = for {
-          _ <- NonServerThreadContextShift[F].shift
-          states <- persistence.fetchState(player)
-          claimState = states.getOrElse(presentId, PresentClaimingState.Unavailable)
-          effect <- claimState match {
-            case PresentClaimingState.Claimed =>
-              Monad[F].pure(MessageEffect(s"ID: ${presentId}のプレゼントはすでに受け取っています。"))
-            case PresentClaimingState.NotClaimed =>
-              for {
-                _ <- persistence.markAsClaimed(presentId, player)
-                item <- persistence.lookup(presentId)
-              } yield {
-                // 注釈: この明示的な型変数の指定は必要
-                // see: https://discord.com/channels/237758724121427969/565935041574731807/823495317499805776
-                item.fold[TargetedEffect[Player]](
-                  MessageEffect(s"ID: ${presentId}のプレゼントは存在しません。IDをお確かめください。")
-                ) { item =>
-                  SequentialEffect(
-                    Util.grantItemStacksEffect[IO](item),
-                    MessageEffect(s"ID: ${presentId}のプレゼントを付与しました。")
-                  )
+      /**
+       * 概要: メインハンドに持っているアイテムをプレゼントとして定義する。
+       * プレイヤーがプレゼントを受け取ることができるようになるには、必ずプレゼントを定義しなければならない。
+       *
+       * 権限ノード: `seichiassist.present.define`
+       *
+       * 構文:
+       *   - /present define
+       *
+       * 出力: 定義が成功した場合は、割り振られたアイテムのIDを表示する。失敗した場合は、適切なエラーメッセージを表示する。
+       */
+      def executor[F[_] : ConcurrentEffect : NonServerThreadContextShift](implicit persistence: PresentPersistence[F, ItemStack]): ContextualExecutor =
+        playerCommandBuilder
+          .execution { context =>
+            val player = context.sender
+            if (player.hasPermission("seichiassist.present.define")) {
+              val mainHandItem = player.getInventory.getItemInMainHand
+              if (mainHandItem.getType eq Material.AIR) {
+                // おそらくこれは意図した動作ではないのでエラーメッセージを表示する
+                IO.pure(MessageEffect("メインハンドに何も持っていません。プレゼントを定義するためには、メインハンドに対象アイテムを持ってください。"))
+              } else {
+                val eff = for {
+                  _ <- NonServerThreadContextShift[F].shift
+                  presentID <- persistence.define(mainHandItem)
+                } yield {
+                  MessageEffect(s"メインハンドに持ったアイテムをプレゼントとして定義しました。IDは${presentID}です。")
                 }
+                eff.toIO
               }
-            case PresentClaimingState.Unavailable =>
-              Monad[F].pure(MessageEffect(s"ID: ${presentId}のプレゼントは存在しないか、あるいは配布対象ではありません。"))
+            } else {
+              IO.pure(noPermissionMessage)
+            }
           }
-        } yield effect
+          .build()
+    }
 
-        eff.toIO
-      }
-      .build()
+    object Delete {
+      val help: EchoExecutor = EchoExecutor(MessageEffect(List(
+        "/present delete <プレゼントID>",
+        "    プレゼントを削除します",
+      )))
+
+      /**
+       * 概要: 指定したプレゼントを消去する。
+       * 対応が失われるため、このコマンドの実行が完了した後、プレイヤーはそのプレゼントを受け取ることができなくなる。
+       *
+       * 権限ノード: `seichiassist.present.delete`
+       *
+       * 出力: 操作の結果とそれに伴うメッセージ。
+       */
+      def executor[F[_] : ConcurrentEffect : NonServerThreadContextShift](implicit persistence: PresentPersistence[F, ItemStack]): ContextualExecutor =
+        playerCommandBuilder
+          .argumentsParsers(List(presentIdParser), onMissingArguments = help)
+          .execution { context =>
+            if (!context.sender.hasPermission("seichiassist.present.delete")) {
+              IO.pure(noPermissionMessage)
+            } else {
+              val presentId = context.args.parsed.head.asInstanceOf[Int]
+              val eff = for {
+                _ <- NonServerThreadContextShift[F].shift
+                result <- persistence.delete(presentId)
+              } yield result match {
+                case DeleteResult.Done =>
+                  MessageEffect(s"IDが${presentId}のプレゼントの消去は正常に行われました。")
+                case DeleteResult.NotFount =>
+                  MessageEffect(s"IDが${presentId}のプレゼントは存在しませんでした。")
+              }
 
-  /**
-   * 概要: 全てのプレゼントのうち、実行プレイヤーが対象となっているプレゼントの受け取り状況を表示する。
-   * 実行プレイヤーが対象ではないプレゼントは表示されない。
-   *
-   * 構文:
-   *   - /present state
-   */
-  private def showStateExecutor[F[_] : ConcurrentEffect : NonServerThreadContextShift](implicit persistence: PresentPersistence[F, ItemStack]) = playerCommandBuilder
-    .execution { context =>
-      val eff = for {
-        // off-main-thread
-        _ <- NonServerThreadContextShift[F].shift
-        state <- persistence.fetchState(context.sender.getUniqueId)
-      } yield {
-        val mes = state
-          .toList
-          // 配布対象外のプレゼントを除外
-          .filter { case (_, state) => state != PresentClaimingState.Unavailable }
-          .map { case (id, state) =>
-            s"ID=$id: ${decoratePresentState(state)}"
+              eff.toIO
+            }
           }
-          .filter(_.nonEmpty)
-        MessageEffect(mes)
-      }
+          .build()
+    }
+
+    object Grant {
+      val help: EchoExecutor = EchoExecutor(MessageEffect(List(
+        "/present grant <プレゼントID> all|(player <...プレーヤー名>)",
+        "    プレゼントを受け取れるプレイヤーを追加します",
+      )))
+
+      /**
+       * 概要: プレイヤーが指定されたプレゼントを受け取れるようにする。
+       *
+       * 権限ノード: `seichiassist.present.grant`
+       *
+       * 出力: 操作の結果とそれに伴うメッセージ
+       *
+       * コマンド構文:
+       *
+       *   - /present grant &lt;presentId: PresentID&gt; player &lt;...players^†^: PlayerName&gt;
+       *   - /present grant &lt;presentId: PresentID&gt; all
+       *
+       * 備考:
+       *   - †: スペース区切り。
+       */
+      def executor[F[_] : ConcurrentEffect : NonServerThreadContextShift](implicit persistence: PresentPersistence[F, ItemStack], globalPlayerAccessor: UuidToLastSeenName[F]): ContextualExecutor =
+        playerCommandBuilder
+          .argumentsParsers(List(presentIdParser, presentScopeModeParser), onMissingArguments = help)
+          .execution { context =>
+            if (context.sender.hasPermission("seichiassist.present.grant")) {
+              // Parserを通した段階でargs[0]は "player" | "all" になっているのでこれでOK
+              val List(_presentId, mode) = context.args.parsed
+              val presentId = _presentId.asInstanceOf[Int]
+              val isGlobal = mode.asInstanceOf[String] == "all"
+              val eff = for {
+                _ <- NonServerThreadContextShift[F].shift
+                // TODO: 以下の処理は多分共通化できるがうまい方法が思いつかない
+                globalUUID2Name <- globalPlayerAccessor.entries
+                // 可変長引数には対応していないので`yetToBeParsed`を使う
+                restArg = context.args
+                  // プレイヤー名は /[A-Za-z0-9_]{,16}/であるため空白が誤って解釈されることはない
+                  .yetToBeParsed
+                  // 連続した空白を消去
+                  .filter(_.nonEmpty)
+                target = if (isGlobal)
+                  globalUUID2Name.keys
+                else
+                  globalUUID2Name.filter { case (_, name) => restArg.contains(name) }.keys
+                errorIfNobody = if (target.isEmpty) Some(MessageEffect("対象のプレイヤーが存在しません!")) else None
+                _ <- persistence.grant(presentId, target.toSet)
+              } yield errorIfNobody.getOrElse(MessageEffect(s"プレゼントIDが${presentId}のプレゼントを受け取れるプレイヤーを追加することに成功しました。"))
 
-      eff.toIO
+              eff.toIO
+            } else {
+              IO.pure(noPermissionMessage)
+            }
+          }
+          .build()
     }
-    .build()
 
-  /**
-   * 概要: 実行プレイヤーと全てのプレゼントの受け取り状況をページネーションと共に表示する
-   *
-   * 構文:
-   *
-   *   - /present list &lt;page: PositiveInt&gt;
-   */
-  private def listExecutor[F[_] : ConcurrentEffect : NonServerThreadContextShift](implicit persistence: PresentPersistence[F, ItemStack]) =
-    playerCommandBuilder
-      .argumentsParsers(List(Parsers.closedRangeInt(1, Int.MaxValue, MessageEffect("ページ数には1以上の数を指定してください。"))))
-      .execution { context =>
-        val perPage: Int Refined Positive = 10
-        val page = refineV[Positive](context.args.parsed.head.asInstanceOf[Int]) match {
-          // argumentsParsersで1以上を指定しているのでここでコケることはないはず
-          case Left(l) => throw new AssertionError(s"positive int: failed. message: $l")
-          case Right(v) => v
-        }
-        val player = context.sender.getUniqueId
-        val eff = for {
-          _ <- NonServerThreadContextShift[F].shift
-          states <- persistence.fetchStateWithPagination(player, perPage, page)
-          messageLine = states
-            .map { case (id, state) =>
-              s"ID=$id: ${decoratePresentState(state)}"
+    object Revoke {
+      val help: EchoExecutor = EchoExecutor(MessageEffect(List(
+        "/present revoke <プレゼントID> all|(player <...プレーヤー名>)",
+        "    プレゼントを受け取れるプレイヤーを削除します"
+      )))
+
+      /**
+       * 概要: プレイヤーがプレゼントを受け取れないようにする。
+       *
+       * 権限ノード: `seichiassist.present.revoke`
+       *
+       * 構文:
+       *   - /present revoke &lt;presentId: PresentID&gt; player &lt;...players^✝^: PlayerName&gt;
+       *   - /present revoke &lt;presentId: PresentID&gt; all
+       *
+       * 出力: 操作の結果とそれに伴うメッセージ。
+       *
+       * 備考:
+       *   - ✝: スペース区切り。
+       */
+      def executor[F[_] : ConcurrentEffect : NonServerThreadContextShift](implicit persistence: PresentPersistence[F, ItemStack], globalPlayerAccessor: UuidToLastSeenName[F]): ContextualExecutor =
+        playerCommandBuilder
+          .argumentsParsers(List(presentIdParser, presentScopeModeParser), onMissingArguments = help)
+          .execution { context =>
+            if (context.sender.hasPermission("seichiassist.present.revoke")) {
+              val args = context.args
+              val presentId = args.parsed.head.asInstanceOf[Int]
+              val isGlobal = args.parsed(1).asInstanceOf[String] == "all"
+              val eff = for {
+                _ <- NonServerThreadContextShift[F].shift
+                globalUUID2Name <- globalPlayerAccessor.entries
+                // 可変長引数には対応していないので`yetToBeParsed`を使う
+                restArg = args
+                  // プレイヤー名は /[A-Za-z0-9_]{,16}/であるため空白が誤って解釈されることはない
+                  .yetToBeParsed
+                  // 連続した空白を消去
+                  .filter(_.nonEmpty)
+                target = if (isGlobal)
+                  globalUUID2Name.keys
+                else
+                  globalUUID2Name.filter { case (_, name) => restArg.contains(name) }.keys
+                errorIfNobody = if (target.isEmpty) Some(MessageEffect("対象のプレイヤーが存在しません!")) else None
+                _ <- persistence.revoke(presentId, target.toSet)
+              } yield {
+                errorIfNobody.getOrElse(
+                  MessageEffect(s"プレゼントIDが${presentId}のプレゼントを受け取ることができるプレイヤーの削除に成功しました。")
+                )
+              }
+              eff.toIO
+            } else {
+              IO.pure(noPermissionMessage)
             }
-            .toList
-        } yield {
-          MessageEffect(messageLine)
-        }
-        eff.toIO
-      }
-      .build()
+          }
+          .build()
+    }
 
-  /**
-   * 概要: マニュアルを表示する。
-   *
-   * 引数:
-   *   - /present help
-   */
-  def helpExecutor: ContextualExecutor = new EchoExecutor(
-    MessageEffect(
-      List(
-        "/present define",
-        "    プレゼントを定義します",
-        "/present delete",
-        "    プレゼントを削除します",
-        "/present grant",
-        "    プレゼントを受け取れるプレイヤーを追加します",
-        "/present revoke",
-        "    プレゼントを受け取れるプレイヤーを削除します",
-        "/present claim",
-        "    プレゼントを受け取ります",
-        "/present list",
-        "    全てのプレゼントを表示します",
-        "/present state",
-        "    対象となっている全てのプレゼントを表示します",
-        "/present help",
-        "    このメッセージを表示します"
-      )
-    )
-  )
+    object Help {
+      /**
+       * 概要: マニュアルを表示する。
+       *
+       * 引数:
+       *   - /present help
+       */
+      def executor: ContextualExecutor = {
+        TraverseExecutor(List(
+          SubCommands.State.help,
+          SubCommands.ListSubCommand.help,
+          SubCommands.Claim.help,
+          EchoExecutor(MessageEffect(List(
+            "/present help",
+            "    このメッセージを表示します",
+            s"${ChatColor.GRAY}==== [管理者用コマンド] ====",
+          ))),
+          SubCommands.Define.help,
+          SubCommands.Delete.help,
+          SubCommands.Grant.help,
+          SubCommands.Revoke.help,
+        ))
+      }
+    }
+  }
 
   def executor[
     F[_] : ConcurrentEffect : NonServerThreadContextShift
   ](implicit persistence: PresentPersistence[F, ItemStack], globalPlayerAccessor: UuidToLastSeenName[F]): TabExecutor = BranchedExecutor(
     Map(
-      "define" -> defineExecutor,
-      "delete" -> deleteExecutor,
-      "grant" -> grantRightExecutor,
-      "revoke" -> revokeRightExecutor,
-      "claim" -> claimExecutor,
-      "list" -> listExecutor,
-      "state" -> showStateExecutor,
-      "help" -> helpExecutor,
+      "define" -> SubCommands.Define.executor,
+      "delete" -> SubCommands.Delete.executor,
+      "grant" -> SubCommands.Grant.executor,
+      "revoke" -> SubCommands.Revoke.executor,
+      "claim" -> SubCommands.Claim.executor,
+      "list" -> SubCommands.ListSubCommand.executor,
+      "state" -> SubCommands.State.executor,
+      "help" -> SubCommands.Help.executor,
     ),
-    Some(helpExecutor),
-    Some(helpExecutor)
+    Some(SubCommands.Help.executor),
+    Some(SubCommands.Help.executor)
   ).asNonBlockingTabExecutor()
 
   private def decoratePresentState(state: PresentClaimingState): String = state match {

From e7ffca15584bdd11ba152fcf27ea9493d1129456 Mon Sep 17 00:00:00 2001
From: Kory <6561358+kory33@users.noreply.github.com>
Date: Tue, 3 Aug 2021 12:20:06 +0900
Subject: [PATCH 32/73] =?UTF-8?q?[update]=20delete=E6=99=82=E3=81=AE?=
 =?UTF-8?q?=E3=83=A1=E3=83=83=E3=82=BB=E3=83=BC=E3=82=B8=E3=82=92=E3=82=88?=
 =?UTF-8?q?=E3=82=8A=E3=82=AF=E3=83=AA=E3=82=A2=E3=81=AB=E3=81=99=E3=82=8B?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../present/domain/OperationResult.scala           |  9 +++++++++
 .../present/domain/PresentPersistence.scala        |  3 ++-
 .../JdbcBackedPresentPersistence.scala             | 14 +++++++++-----
 3 files changed, 20 insertions(+), 6 deletions(-)
 create mode 100644 src/main/scala/com/github/unchama/seichiassist/subsystems/present/domain/OperationResult.scala

diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/present/domain/OperationResult.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/present/domain/OperationResult.scala
new file mode 100644
index 0000000000..ddb926eeea
--- /dev/null
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/present/domain/OperationResult.scala
@@ -0,0 +1,9 @@
+package com.github.unchama.seichiassist.subsystems.present.domain
+
+object OperationResult {
+  sealed trait DeleteResult
+  object DeleteResult {
+    case object Done extends DeleteResult
+    case object NotFount extends DeleteResult
+  }
+}
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/present/domain/PresentPersistence.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/present/domain/PresentPersistence.scala
index beb0a1edea..fb07d50e7b 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/present/domain/PresentPersistence.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/present/domain/PresentPersistence.scala
@@ -1,5 +1,6 @@
 package com.github.unchama.seichiassist.subsystems.present.domain
 
+import com.github.unchama.seichiassist.subsystems.present.domain.OperationResult.DeleteResult
 import eu.timepit.refined.api.Refined
 import eu.timepit.refined.numeric.Positive
 
@@ -27,7 +28,7 @@ trait PresentPersistence[F[_], ItemStack] {
    *
    * @param presentID プレゼントID
    */
-  def delete(presentID: PresentID): F[Unit]
+  def delete(presentID: PresentID): F[DeleteResult]
 
   /**
    * 指定したUUIDを持つプレイヤーに対して`presentID`で指定されたプレゼントを受け取ることができるようにする。
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/present/infrastructure/JdbcBackedPresentPersistence.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/present/infrastructure/JdbcBackedPresentPersistence.scala
index 6673e96452..f6a6ad585b 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/present/infrastructure/JdbcBackedPresentPersistence.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/present/infrastructure/JdbcBackedPresentPersistence.scala
@@ -1,6 +1,7 @@
 package com.github.unchama.seichiassist.subsystems.present.infrastructure
 
 import cats.effect.Sync
+import com.github.unchama.seichiassist.subsystems.present.domain.OperationResult.DeleteResult
 import com.github.unchama.seichiassist.subsystems.present.domain.{PresentClaimingState, PresentPersistence}
 import eu.timepit.refined.api.Refined
 import eu.timepit.refined.auto._
@@ -29,16 +30,19 @@ class JdbcBackedPresentPersistence[F[_] : Sync] extends PresentPersistence[F, It
    *
    * @param presentId プレゼントID
    */
-  override def delete(presentId: PresentID): F[Unit] = Sync[F].delay {
+  override def delete(presentId: PresentID): F[DeleteResult] = Sync[F].delay {
     DB.localTx { implicit session =>
       // 制約をかけているのでpresent_stateの方から先に消さないと整合性エラーを吐く
       sql"""DELETE FROM present_state WHERE present_id = $presentId"""
         .execute()
         .apply()
 
-      sql"""DELETE FROM present WHERE present_id = $presentId"""
-        .execute()
-        .apply()
+      val deletedRows =
+        sql"""DELETE FROM present WHERE present_id = $presentId"""
+          .update()
+          .apply()
+
+      if (deletedRows == 1) DeleteResult.Done else DeleteResult.NotFount
     }
   }
 
@@ -144,7 +148,7 @@ class JdbcBackedPresentPersistence[F[_] : Sync] extends PresentPersistence[F, It
     Sync[F].delay {
       val offset = (page - 1) * perPage
       DB.readOnly { implicit session =>
-        sql"""SELECT present_id FROM present ORDER BY present_id LIMIT $perPage OFFSET $offset"""
+        sql"""SELECT present_id FROM present ORDER BY present_id LIMIT ${perPage.value} OFFSET $offset"""
           .map { _.long("present_id") }
           .toList()
           .apply()

From 68324459d8fabbc19be7119fe1065ff1094889f9 Mon Sep 17 00:00:00 2001
From: Kory <6561358+kory33@users.noreply.github.com>
Date: Tue, 3 Aug 2021 12:25:07 +0900
Subject: [PATCH 33/73] =?UTF-8?q?[update]=20/present=20list=E3=81=AEhelp?=
 =?UTF-8?q?=E3=82=92=E5=85=83=E3=81=AB=E6=88=BB=E3=81=99?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../subsystems/present/bukkit/command/PresentCommand.scala     | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/present/bukkit/command/PresentCommand.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/present/bukkit/command/PresentCommand.scala
index 3e6b8a5506..2675e28013 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/present/bukkit/command/PresentCommand.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/present/bukkit/command/PresentCommand.scala
@@ -88,7 +88,8 @@ class PresentCommand(implicit val ioOnMainThread: OnMinecraftServerThread[IO]) {
 
     object ListSubCommand {
       val help: EchoExecutor = EchoExecutor(MessageEffect(List(
-        s"${ChatColor.GRAY}コマンドの構文: /present list <ページ数>"
+        "/present list",
+        "    全てのプレゼントを表示します",
       )))
 
       /**

From d41f778947ae6d8f3e52721634bb0db410fcf129 Mon Sep 17 00:00:00 2001
From: Kory <6561358+kory33@users.noreply.github.com>
Date: Tue, 3 Aug 2021 12:29:51 +0900
Subject: [PATCH 34/73] =?UTF-8?q?[update]=20/present=20grant/revoke?=
 =?UTF-8?q?=E3=81=AE=E7=B5=90=E6=9E=9C=E3=83=A1=E3=83=83=E3=82=BB=E3=83=BC?=
 =?UTF-8?q?=E3=82=B8=E3=82=92=E8=AA=AD=E3=81=BF=E3=82=84=E3=81=99=E3=81=8F?=
 =?UTF-8?q?=E3=81=99=E3=82=8B?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../present/bukkit/command/PresentCommand.scala       | 11 +++++++----
 1 file changed, 7 insertions(+), 4 deletions(-)

diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/present/bukkit/command/PresentCommand.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/present/bukkit/command/PresentCommand.scala
index 2675e28013..adb658a801 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/present/bukkit/command/PresentCommand.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/present/bukkit/command/PresentCommand.scala
@@ -314,7 +314,10 @@ class PresentCommand(implicit val ioOnMainThread: OnMinecraftServerThread[IO]) {
                   globalUUID2Name.filter { case (_, name) => restArg.contains(name) }.keys
                 errorIfNobody = if (target.isEmpty) Some(MessageEffect("対象のプレイヤーが存在しません!")) else None
                 _ <- persistence.grant(presentId, target.toSet)
-              } yield errorIfNobody.getOrElse(MessageEffect(s"プレゼントIDが${presentId}のプレゼントを受け取れるプレイヤーを追加することに成功しました。"))
+              } yield
+                errorIfNobody.getOrElse(MessageEffect(
+                  s"プレゼント(id: $presentId)を受け取れるプレイヤーを追加しました。"
+                ))
 
               eff.toIO
             } else {
@@ -368,9 +371,9 @@ class PresentCommand(implicit val ioOnMainThread: OnMinecraftServerThread[IO]) {
                 errorIfNobody = if (target.isEmpty) Some(MessageEffect("対象のプレイヤーが存在しません!")) else None
                 _ <- persistence.revoke(presentId, target.toSet)
               } yield {
-                errorIfNobody.getOrElse(
-                  MessageEffect(s"プレゼントIDが${presentId}のプレゼントを受け取ることができるプレイヤーの削除に成功しました。")
-                )
+                errorIfNobody.getOrElse(MessageEffect(
+                  s"プレゼント(id: $presentId)を受け取れるプレイヤーを削除しました。"
+                ))
               }
               eff.toIO
             } else {

From 14aaf4f670c0d63beb31d3ed7bb3e99a13a2b074 Mon Sep 17 00:00:00 2001
From: KisaragiEffective <kisaragi.effective@gmail.com>
Date: Tue, 3 Aug 2021 17:31:25 +0900
Subject: [PATCH 35/73] [fix] fix #1137

---
 .../JdbcBackedPresentPersistence.scala        | 28 +++++++++++++------
 1 file changed, 20 insertions(+), 8 deletions(-)

diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/present/infrastructure/JdbcBackedPresentPersistence.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/present/infrastructure/JdbcBackedPresentPersistence.scala
index f6a6ad585b..1878a26c67 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/present/infrastructure/JdbcBackedPresentPersistence.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/present/infrastructure/JdbcBackedPresentPersistence.scala
@@ -22,8 +22,8 @@ class JdbcBackedPresentPersistence[F[_] : Sync] extends PresentPersistence[F, It
       sql"""INSERT INTO present (itemstack) VALUES ($stackAsBlob)"""
         .updateAndReturnGeneratedKey
         .apply()
-      }
     }
+  }
 
   /**
    * 指定したPresentIDに対応するプレゼントを物理消去する。
@@ -60,15 +60,27 @@ class JdbcBackedPresentPersistence[F[_] : Sync] extends PresentPersistence[F, It
     }
   }
 
-  override def revoke(presentID: PresentID, players: Set[UUID]): F[Unit] = Sync[F].delay {
-    val scopeAsSQL = players.map(_.toString)
-
-    DB.localTx { implicit session =>
-      // https://discord.com/channels/237758724121427969/565935041574731807/824107651985834004
-      sql"""DELETE FROM present_state WHERE present_id = $presentID AND uuid IN ($scopeAsSQL)"""
-        .execute()
+  override def revoke(presentID: PresentID, players: Set[UUID]): F[Unit] = {
+    val existence = DB.readOnly { implicit session =>
+      sql"""SELECT present_state FROM present_state WHERE present_id = $presentID"""
+        .map(_ => ()) // avoid NoExtractor
+        .first()
         .apply()
     }
+
+    import cats.Applicative
+    existence.fold(Applicative[F].pure(())) { _ =>
+      Sync[F].delay {
+        val scopeAsSQL = players.map(_.toString)
+
+        DB.localTx { implicit session =>
+          // https://discord.com/channels/237758724121427969/565935041574731807/824107651985834004
+          sql"""DELETE FROM present_state WHERE present_id = $presentID AND uuid IN ($scopeAsSQL)"""
+            .execute()
+            .apply()
+        }
+      }
+    }
   }
 
   override def markAsClaimed(presentId: PresentID, player: UUID): F[Unit] = Sync[F].delay {

From be853edb4f53c9da141158714e68a5bf6f2b2e17 Mon Sep 17 00:00:00 2001
From: KisaragiEffective <kisaragi.effective@gmail.com>
Date: Tue, 3 Aug 2021 17:44:55 +0900
Subject: [PATCH 36/73] [fix] fix #1132

---
 .../present/bukkit/command/PresentCommand.scala    | 14 ++++++++++++--
 1 file changed, 12 insertions(+), 2 deletions(-)

diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/present/bukkit/command/PresentCommand.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/present/bukkit/command/PresentCommand.scala
index adb658a801..bcaa2abb82 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/present/bukkit/command/PresentCommand.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/present/bukkit/command/PresentCommand.scala
@@ -70,7 +70,7 @@ class PresentCommand(implicit val ioOnMainThread: OnMinecraftServerThread[IO]) {
             _ <- NonServerThreadContextShift[F].shift
             state <- persistence.fetchState(context.sender.getUniqueId)
           } yield {
-            val mes = state
+            val presents = state
               .toList
               // 配布対象外のプレゼントを除外
               .filter { case (_, state) => state != PresentClaimingState.Unavailable }
@@ -78,7 +78,17 @@ class PresentCommand(implicit val ioOnMainThread: OnMinecraftServerThread[IO]) {
                 s"ID=$id: ${decoratePresentState(state)}"
               }
               .filter(_.nonEmpty)
-            MessageEffect(mes)
+
+            val lines = if (presents.isEmpty) {
+              List("対象のプレゼントが存在しません")
+            } else {
+              List(
+                "対象のプレゼント一覧:",
+                "------------------"
+              ) ::: presents
+            }
+
+            MessageEffect(lines)
           }
 
           eff.toIO

From a0c1d39b76f075ef5e9cd61cbabce3556f02c04b Mon Sep 17 00:00:00 2001
From: KisaragiEffective <kisaragi.effective@gmail.com>
Date: Tue, 3 Aug 2021 17:46:19 +0900
Subject: [PATCH 37/73] [fix] fix #1136

---
 .../present/infrastructure/JdbcBackedPresentPersistence.scala    | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/present/infrastructure/JdbcBackedPresentPersistence.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/present/infrastructure/JdbcBackedPresentPersistence.scala
index 1878a26c67..de3c60a7cd 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/present/infrastructure/JdbcBackedPresentPersistence.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/present/infrastructure/JdbcBackedPresentPersistence.scala
@@ -117,6 +117,7 @@ class JdbcBackedPresentPersistence[F[_] : Sync] extends PresentPersistence[F, It
              |SELECT present_id, claimed
              |FROM present_state
              |WHERE uuid = ${player.toString} AND present_id IN ($idSliceWithPagination)
+             |ORDER BY present_id
         """
           .stripMargin
           .map(wrapResultForState)

From 38a348bca7d97afed0926870345d5feae7134654 Mon Sep 17 00:00:00 2001
From: KisaragiEffective <kisaragi.effective@gmail.com>
Date: Tue, 3 Aug 2021 19:31:29 +0900
Subject: [PATCH 38/73] [fix] fix #1133

---
 .../bukkit/command/PresentCommand.scala       | 13 ++++++------
 .../domain/PaginationRejectReason.scala       | 10 +++++++++
 .../JdbcBackedPresentPersistence.scala        | 21 ++++++++++++-------
 3 files changed, 31 insertions(+), 13 deletions(-)
 create mode 100644 src/main/scala/com/github/unchama/seichiassist/subsystems/present/domain/PaginationRejectReason.scala

diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/present/bukkit/command/PresentCommand.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/present/bukkit/command/PresentCommand.scala
index bcaa2abb82..389722b98d 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/present/bukkit/command/PresentCommand.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/present/bukkit/command/PresentCommand.scala
@@ -12,7 +12,7 @@ import com.github.unchama.minecraft.actions.OnMinecraftServerThread
 import com.github.unchama.seichiassist.commands.contextual.builder.BuilderTemplates.playerCommandBuilder
 import com.github.unchama.seichiassist.domain.actions.UuidToLastSeenName
 import com.github.unchama.seichiassist.subsystems.present.domain.OperationResult.DeleteResult
-import com.github.unchama.seichiassist.subsystems.present.domain.{PresentClaimingState, PresentPersistence}
+import com.github.unchama.seichiassist.subsystems.present.domain.{PaginationRejectReason, PresentClaimingState, PresentPersistence}
 import com.github.unchama.seichiassist.util.Util
 import com.github.unchama.targetedeffect.commandsender.MessageEffect
 import com.github.unchama.targetedeffect.{SequentialEffect, TargetedEffect}
@@ -128,11 +128,12 @@ class PresentCommand(implicit val ioOnMainThread: OnMinecraftServerThread[IO]) {
             val eff = for {
               _ <- NonServerThreadContextShift[F].shift
               states <- persistence.fetchStateWithPagination(player, perPage, page)
-              messageLine = states
-                .map { case (id, state) =>
-                  s"ID=$id: ${decoratePresentState(state)}"
-                }
-                .toList
+              messageLine = states.fold({
+                case PaginationRejectReason.TooLargePage(max) =>
+                  List(s"ページ数が大きすぎます。${max}ページ以下にしてください")
+              }, b => b.map { case (id, state) =>
+                s"ID=$id: ${decoratePresentState(state)}"
+              }.toList)
             } yield {
               MessageEffect(messageLine)
             }
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/present/domain/PaginationRejectReason.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/present/domain/PaginationRejectReason.scala
new file mode 100644
index 0000000000..8aa1df34e7
--- /dev/null
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/present/domain/PaginationRejectReason.scala
@@ -0,0 +1,10 @@
+package com.github.unchama.seichiassist.subsystems.present.domain
+
+import eu.timepit.refined.api.Refined
+import eu.timepit.refined.numeric.Positive
+
+sealed trait PaginationRejectReason
+
+object PaginationRejectReason {
+  case class TooLargePage(exceptedMax: Int Refined Positive) extends PaginationRejectReason
+}
\ No newline at end of file
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/present/infrastructure/JdbcBackedPresentPersistence.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/present/infrastructure/JdbcBackedPresentPersistence.scala
index de3c60a7cd..05cda78aa0 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/present/infrastructure/JdbcBackedPresentPersistence.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/present/infrastructure/JdbcBackedPresentPersistence.scala
@@ -111,13 +111,20 @@ class JdbcBackedPresentPersistence[F[_] : Sync] extends PresentPersistence[F, It
     for {
       idSliceWithPagination <- idSliceWithPagination(perPage, page)
     } yield {
-      // ページネーションはIDを列挙するときにすでに完了している
-      val associatedEntries = DB.readOnly { implicit session =>
-        sql"""
-             |SELECT present_id, claimed
-             |FROM present_state
-             |WHERE uuid = ${player.toString} AND present_id IN ($idSliceWithPagination)
-             |ORDER BY present_id
+      if (idSliceWithPagination.isEmpty) {
+        for {
+          entries <- fetchState(player)
+        } yield {
+          Left(PaginationRejectReason.TooLargePage(Math.ceil(entries.size.toDouble / perPage).toInt))
+        }
+      } else {
+        // ページネーションはIDを列挙するときにすでに完了している
+        val associatedEntries = DB.readOnly { implicit session =>
+          sql"""
+               |SELECT present_id, claimed
+               |FROM present_state
+               |WHERE uuid = ${player.toString} AND present_id IN ($idSliceWithPagination)
+               |ORDER BY present_id
         """
           .stripMargin
           .map(wrapResultForState)

From f5207ce289f83557a0c204f9553215182574ddb5 Mon Sep 17 00:00:00 2001
From: KisaragiEffective <kisaragi.effective@gmail.com>
Date: Tue, 3 Aug 2021 19:33:37 +0900
Subject: [PATCH 39/73] =?UTF-8?q?[update]=20=E3=83=89=E3=82=AD=E3=83=A5?=
 =?UTF-8?q?=E3=83=A1=E3=83=B3=E3=83=88=E5=8F=8A=E3=81=B3=E3=82=B9=E3=82=BF?=
 =?UTF-8?q?=E3=82=A4=E3=83=AB?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../present/domain/PresentPersistence.scala   | 26 ++++++++++++-------
 1 file changed, 17 insertions(+), 9 deletions(-)

diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/present/domain/PresentPersistence.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/present/domain/PresentPersistence.scala
index fb07d50e7b..3e3d7c646c 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/present/domain/PresentPersistence.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/present/domain/PresentPersistence.scala
@@ -8,8 +8,8 @@ import java.util.UUID
 
 /**
  * プレゼントシステムに関する永続化のインターフェースを規定するトレイト。
- * 他で指定がない限り、以下の条件・制約を満たす。違反した時の動作は未定義である:
- *   - 引数で渡される`PresentID`は対応するプレゼントが存在し、一意でなければならない
+ * 他で指定がない限り、以下の条件および制約を満たす。違反した時の動作は未定義である:
+ *   - 引数で渡される`PresentID`は対応するプレゼントが存在し、その多重度は
  *   - 返り値としての`PresentID`は対応するプレゼントが存在する
  */
 trait PresentPersistence[F[_], ItemStack] {
@@ -31,8 +31,9 @@ trait PresentPersistence[F[_], ItemStack] {
   def delete(presentID: PresentID): F[DeleteResult]
 
   /**
-   * 指定したUUIDを持つプレイヤーに対して`presentID`で指定されたプレゼントを受け取ることができるようにする。
-   *
+   * 指定したUUIDを持つプレイヤー群に対して`presentID`で指定されたプレゼントを受け取ることができるようにする。
+   * このメソッドは同じプレイヤーとプレゼントIDで呼び出された場合はべき等である。また、すでに受取可能なプレイヤーが
+   * `players`の中に入っていた場合は、そのプレイヤーについての受取可能にする処理をスキップする。
    * @param presentID 対象のプレゼントID
    * @param players   受け取ることができるようにするプレイヤーのUUID
    * @return 永続化層への書き込みを行う作用
@@ -40,7 +41,7 @@ trait PresentPersistence[F[_], ItemStack] {
   def grant(presentID: PresentID, players: Set[UUID]): F[Unit]
 
   /**
-   * 指定したUUIDを持つプレイヤーが`presentID`で指定されたプレゼントを受け取ることができないようにする。
+   * 指定したUUIDを持つプレイヤー群が`presentID`で指定されたプレゼントを受け取ることができないようにする。
    *
    * @param presentID 対象のプレゼントID
    * @param players   受け取ることができないようにするプレイヤーのUUID
@@ -66,7 +67,8 @@ trait PresentPersistence[F[_], ItemStack] {
 
   /**
    * ページネーション付きでプレイヤーがプレゼントを受け取ることができるかどうか列挙する。
-   * このときの出現順序は、[[PresentID]]が最も若いエントリから先に出現する。
+   * このときのページネーションは、[[PresentID]]が最も若いエントリから先に出現するように行われるが、
+   * ページネーションされたMap内での各エントリの出現順序は未規定である。
    *
    * 例として以下のような状況を仮定する:
    *   - 既知のPresentIDとItemStackのエントリ: `List((1, aaa), (3, ccc), (6, fff), (4, ddd), (5, eee), (2, bbb))`
@@ -80,18 +82,24 @@ trait PresentPersistence[F[_], ItemStack] {
    * `Map(1 -> Claimed, 2 -> Claimed, 3 -> Claimed, 4 -> NotClaimed, 5 -> Unavailable)`
    *
    * 備考:
-   *   - 実装によっては、[[fetchState]]などを呼び出して既知のエントリを全列挙する可能性がある。
+   *   - 実装によっては、[[fetchState]]などを呼び出して有効なエントリを全列挙する可能性がある。
    *   - このメソッドは一貫性のために[[fetchState]]のドキュメントにある制約を継承する。
+   *   - 最終インデックスが有効なプレゼントの総数を超えるとき、作用はLeftを返さなければならない。
+   *   - 最終インデックスが有効なプレゼントの総数を超えないとき、作用はRightを返さなければならない。
    *
    * @param player  調べる対象のプレイヤー
    * @param perPage ページごとのエントリの数
    * @param page    ページ、1オリジン
    * @return ページネーションを計算して返す作用
    */
-  def fetchStateWithPagination(player: UUID, perPage: Int Refined Positive, page: Int Refined Positive): F[Map[PresentID, PresentClaimingState]]
+  def fetchStateWithPagination(
+                                player: UUID,
+                                perPage: Int Refined Positive,
+                                page: Int Refined Positive
+                              ): F[Either[PaginationRejectReason, Map[PresentID, PresentClaimingState]]]
 
   /**
-   * プレイヤーがプレゼントを受け取ることができるかどうか列挙する。このとき、計算されるMapは次の性質を持つ:
+   * プレイヤーがプレゼントを受け取ることができるかどうか列挙する。このとき、計算されるMapは次の性質を満たす:
    *
    *  - すでに受け取ったプレゼントに対応するPresentIDに対して[[PresentClaimingState.Claimed]]がマッピングされる
    *  - 受け取ることができるが、まだ受け取っていないプレゼントに対応するPresentIDに対して[[PresentClaimingState.NotClaimed]]がマッピングされる

From 37e71f6943680313944410c27d7ed571d2fdd666 Mon Sep 17 00:00:00 2001
From: KisaragiEffective <kisaragi.effective@gmail.com>
Date: Tue, 3 Aug 2021 19:34:02 +0900
Subject: [PATCH 40/73] [fix] fix #1134

---
 .../JdbcBackedPresentPersistence.scala        | 60 ++++++++++++-------
 1 file changed, 40 insertions(+), 20 deletions(-)

diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/present/infrastructure/JdbcBackedPresentPersistence.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/present/infrastructure/JdbcBackedPresentPersistence.scala
index 05cda78aa0..47069e1f99 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/present/infrastructure/JdbcBackedPresentPersistence.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/present/infrastructure/JdbcBackedPresentPersistence.scala
@@ -2,7 +2,7 @@ package com.github.unchama.seichiassist.subsystems.present.infrastructure
 
 import cats.effect.Sync
 import com.github.unchama.seichiassist.subsystems.present.domain.OperationResult.DeleteResult
-import com.github.unchama.seichiassist.subsystems.present.domain.{PresentClaimingState, PresentPersistence}
+import com.github.unchama.seichiassist.subsystems.present.domain.{PaginationRejectReason, PresentClaimingState, PresentPersistence}
 import eu.timepit.refined.api.Refined
 import eu.timepit.refined.auto._
 import eu.timepit.refined.numeric.Positive
@@ -46,17 +46,32 @@ class JdbcBackedPresentPersistence[F[_] : Sync] extends PresentPersistence[F, It
     }
   }
 
-  override def grant(presentID: PresentID, players: Set[UUID]): F[Unit] = Sync[F].delay {
-    import scala.collection.Seq.iterableFactory
-
-    val initialValues = players
-      .map { uuid => Seq(presentID, uuid.toString, false) }
-      .toSeq
-
-    DB.localTx { implicit session =>
-      sql"""INSERT INTO present_state VALUES (?, ?, ?)"""
-        .batch(initialValues: _*)
-        .apply()
+  override def grant(presentID: PresentID, players: Set[UUID]): F[Unit] = {
+    import cats.implicits._
+    for {
+      alreadyAddedPlayers <- Sync[F].delay {
+        DB.readOnly { implicit session =>
+          sql"""SELECT uuid FROM present_state WHERE present_id = $presentID"""
+            .map(x => UUID.fromString(x.string("uuid")))
+            .list()
+            .apply()
+        }
+      }
+    } yield {
+      import scala.collection.Seq.iterableFactory
+
+      val initialValues = players
+        // すでに存在しているプレゼントIDとプレイヤーの組をINSERT
+        // すると整合性違反になるためフィルタ
+        .filterNot { alreadyAddedPlayers contains _ }
+        .map { uuid => Seq(presentID, uuid.toString, false) }
+        .toSeq
+
+      DB.localTx { implicit session =>
+        sql"""INSERT INTO present_state VALUES (?, ?, ?)"""
+          .batch(initialValues: _*)
+          .apply()
+      }
     }
   }
 
@@ -106,7 +121,11 @@ class JdbcBackedPresentPersistence[F[_] : Sync] extends PresentPersistence[F, It
     }
   }
 
-  override def fetchStateWithPagination(player: UUID, perPage: Int Refined Positive, page: Int Refined Positive): F[Map[PresentID, PresentClaimingState]] = {
+  override def fetchStateWithPagination(
+                                         player: UUID,
+                                         perPage: Int Refined Positive,
+                                         page: Int Refined Positive
+                                       ): F[Either[PaginationRejectReason, Map[PresentID, PresentClaimingState]]] = {
     import cats.implicits._
     for {
       idSliceWithPagination <- idSliceWithPagination(perPage, page)
@@ -126,14 +145,15 @@ class JdbcBackedPresentPersistence[F[_] : Sync] extends PresentPersistence[F, It
                |WHERE uuid = ${player.toString} AND present_id IN ($idSliceWithPagination)
                |ORDER BY present_id
         """
-          .stripMargin
-          .map(wrapResultForState)
-          .toList()
-          .apply()
-          .toMap
-      }
+            .stripMargin
+            .map(wrapResultForState)
+            .toList()
+            .apply()
+            .toMap
+        }
 
-      filledEntries(associatedEntries, idSliceWithPagination)
+        Right(filledEntries(associatedEntries, idSliceWithPagination))
+      }
     }
   }
 

From 915f338d8f0b87ed540284621a13d55e26e6ddbc Mon Sep 17 00:00:00 2001
From: KisaragiEffective <kisaragi.effective@gmail.com>
Date: Tue, 3 Aug 2021 20:41:46 +0900
Subject: [PATCH 41/73] [fix] fix #1135

---
 .../bukkit/command/PresentCommand.scala       | 13 ++--
 .../present/domain/GrantRejectReason.scala    |  7 ++
 .../domain/PaginationRejectReason.scala       |  7 +-
 .../present/domain/PresentPersistence.scala   |  2 +-
 .../JdbcBackedPresentPersistence.scala        | 69 ++++++++++++-------
 5 files changed, 63 insertions(+), 35 deletions(-)
 create mode 100644 src/main/scala/com/github/unchama/seichiassist/subsystems/present/domain/GrantRejectReason.scala

diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/present/bukkit/command/PresentCommand.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/present/bukkit/command/PresentCommand.scala
index 389722b98d..22b96ce74a 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/present/bukkit/command/PresentCommand.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/present/bukkit/command/PresentCommand.scala
@@ -12,7 +12,7 @@ import com.github.unchama.minecraft.actions.OnMinecraftServerThread
 import com.github.unchama.seichiassist.commands.contextual.builder.BuilderTemplates.playerCommandBuilder
 import com.github.unchama.seichiassist.domain.actions.UuidToLastSeenName
 import com.github.unchama.seichiassist.subsystems.present.domain.OperationResult.DeleteResult
-import com.github.unchama.seichiassist.subsystems.present.domain.{PaginationRejectReason, PresentClaimingState, PresentPersistence}
+import com.github.unchama.seichiassist.subsystems.present.domain.{GrantRejectReason, PaginationRejectReason, PresentClaimingState, PresentPersistence}
 import com.github.unchama.seichiassist.util.Util
 import com.github.unchama.targetedeffect.commandsender.MessageEffect
 import com.github.unchama.targetedeffect.{SequentialEffect, TargetedEffect}
@@ -323,12 +323,15 @@ class PresentCommand(implicit val ioOnMainThread: OnMinecraftServerThread[IO]) {
                   globalUUID2Name.keys
                 else
                   globalUUID2Name.filter { case (_, name) => restArg.contains(name) }.keys
-                errorIfNobody = if (target.isEmpty) Some(MessageEffect("対象のプレイヤーが存在しません!")) else None
-                _ <- persistence.grant(presentId, target.toSet)
+                errorIfNobody = Option.when(target.isEmpty) { MessageEffect("対象のプレイヤーが存在しません!") }
+                grantError <- persistence.grant(presentId, target.toSet)
               } yield
-                errorIfNobody.getOrElse(MessageEffect(
+                errorIfNobody.getOrElse(grantError.map {
+                  case GrantRejectReason.NoSuchPresentID =>
+                    MessageEffect("指定されたプレゼントIDは存在しません!")
+                }.getOrElse(MessageEffect(
                   s"プレゼント(id: $presentId)を受け取れるプレイヤーを追加しました。"
-                ))
+                )))
 
               eff.toIO
             } else {
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/present/domain/GrantRejectReason.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/present/domain/GrantRejectReason.scala
new file mode 100644
index 0000000000..97a6dc14f1
--- /dev/null
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/present/domain/GrantRejectReason.scala
@@ -0,0 +1,7 @@
+package com.github.unchama.seichiassist.subsystems.present.domain
+
+sealed trait GrantRejectReason
+
+object GrantRejectReason {
+  case object NoSuchPresentID extends GrantRejectReason
+}
\ No newline at end of file
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/present/domain/PaginationRejectReason.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/present/domain/PaginationRejectReason.scala
index 8aa1df34e7..110faf28d6 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/present/domain/PaginationRejectReason.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/present/domain/PaginationRejectReason.scala
@@ -1,10 +1,7 @@
 package com.github.unchama.seichiassist.subsystems.present.domain
 
-import eu.timepit.refined.api.Refined
-import eu.timepit.refined.numeric.Positive
-
 sealed trait PaginationRejectReason
 
 object PaginationRejectReason {
-  case class TooLargePage(exceptedMax: Int Refined Positive) extends PaginationRejectReason
-}
\ No newline at end of file
+  case class TooLargePage(exceptedMax: Long) extends PaginationRejectReason
+}
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/present/domain/PresentPersistence.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/present/domain/PresentPersistence.scala
index 3e3d7c646c..6314f95360 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/present/domain/PresentPersistence.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/present/domain/PresentPersistence.scala
@@ -38,7 +38,7 @@ trait PresentPersistence[F[_], ItemStack] {
    * @param players   受け取ることができるようにするプレイヤーのUUID
    * @return 永続化層への書き込みを行う作用
    */
-  def grant(presentID: PresentID, players: Set[UUID]): F[Unit]
+  def grant(presentID: PresentID, players: Set[UUID]): F[Option[GrantRejectReason]]
 
   /**
    * 指定したUUIDを持つプレイヤー群が`presentID`で指定されたプレゼントを受け取ることができないようにする。
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/present/infrastructure/JdbcBackedPresentPersistence.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/present/infrastructure/JdbcBackedPresentPersistence.scala
index 47069e1f99..15f422bc21 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/present/infrastructure/JdbcBackedPresentPersistence.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/present/infrastructure/JdbcBackedPresentPersistence.scala
@@ -1,8 +1,9 @@
 package com.github.unchama.seichiassist.subsystems.present.infrastructure
 
+import cats.Applicative
 import cats.effect.Sync
 import com.github.unchama.seichiassist.subsystems.present.domain.OperationResult.DeleteResult
-import com.github.unchama.seichiassist.subsystems.present.domain.{PaginationRejectReason, PresentClaimingState, PresentPersistence}
+import com.github.unchama.seichiassist.subsystems.present.domain.{GrantRejectReason, PaginationRejectReason, PresentClaimingState, PresentPersistence}
 import eu.timepit.refined.api.Refined
 import eu.timepit.refined.auto._
 import eu.timepit.refined.numeric.Positive
@@ -46,33 +47,47 @@ class JdbcBackedPresentPersistence[F[_] : Sync] extends PresentPersistence[F, It
     }
   }
 
-  override def grant(presentID: PresentID, players: Set[UUID]): F[Unit] = {
+  override def grant(presentID: PresentID, players: Set[UUID]): F[Option[GrantRejectReason]] = {
     import cats.implicits._
     for {
-      alreadyAddedPlayers <- Sync[F].delay {
+      exists <- Sync[F].delay {
         DB.readOnly { implicit session =>
+          sql"""SELECT present_id FROM present"""
+            .map(x => x.long("present_id"))
+            .list()
+            .apply()
+        }.contains(presentID)
+      }
+    } yield {
+      if (exists) {
+        val alreadyAddedPlayers = DB.readOnly { implicit session =>
           sql"""SELECT uuid FROM present_state WHERE present_id = $presentID"""
             .map(x => UUID.fromString(x.string("uuid")))
             .list()
             .apply()
         }
-      }
-    } yield {
-      import scala.collection.Seq.iterableFactory
-
-      val initialValues = players
-        // すでに存在しているプレゼントIDとプレイヤーの組をINSERT
-        // すると整合性違反になるためフィルタ
-        .filterNot { alreadyAddedPlayers contains _ }
-        .map { uuid => Seq(presentID, uuid.toString, false) }
-        .toSeq
-
-      DB.localTx { implicit session =>
-        sql"""INSERT INTO present_state VALUES (?, ?, ?)"""
-          .batch(initialValues: _*)
-          .apply()
+
+        import scala.collection.Seq.iterableFactory
+
+        val initialValues = players
+          // すでに存在しているプレゼントIDとプレイヤーの組をINSERT
+          // すると整合性違反になるためフィルタ
+          .filterNot { alreadyAddedPlayers contains _ }
+          .map { uuid => Seq(presentID, uuid.toString, false) }
+          .toSeq
+
+        DB.localTx { implicit session =>
+          sql"""INSERT INTO present_state VALUES (?, ?, ?)"""
+            .batch(initialValues: _*)
+            .apply()
+        }
+
+        None
+      } else {
+        Some(GrantRejectReason.NoSuchPresentID)
       }
     }
+
   }
 
   override def revoke(presentID: PresentID, players: Set[UUID]): F[Unit] = {
@@ -83,7 +98,6 @@ class JdbcBackedPresentPersistence[F[_] : Sync] extends PresentPersistence[F, It
         .apply()
     }
 
-    import cats.Applicative
     existence.fold(Applicative[F].pure(())) { _ =>
       Sync[F].delay {
         val scopeAsSQL = players.map(_.toString)
@@ -129,13 +143,10 @@ class JdbcBackedPresentPersistence[F[_] : Sync] extends PresentPersistence[F, It
     import cats.implicits._
     for {
       idSliceWithPagination <- idSliceWithPagination(perPage, page)
+      count <- computeValidPresentCount
     } yield {
       if (idSliceWithPagination.isEmpty) {
-        for {
-          entries <- fetchState(player)
-        } yield {
-          Left(PaginationRejectReason.TooLargePage(Math.ceil(entries.size.toDouble / perPage).toInt))
-        }
+        Left(PaginationRejectReason.TooLargePage(Math.ceil(count.toDouble / perPage).toLong))
       } else {
         // ページネーションはIDを列挙するときにすでに完了している
         val associatedEntries = DB.readOnly { implicit session =>
@@ -213,4 +224,14 @@ class JdbcBackedPresentPersistence[F[_] : Sync] extends PresentPersistence[F, It
     val globalEntries = validGlobalId.map(id => (id, PresentClaimingState.Unavailable)).toMap
     globalEntries ++ knownState
   }
+
+  private def computeValidPresentCount: F[Long] = Sync[F].delay {
+    DB.readOnly { implicit session =>
+      sql"""SELECT COUNT(*) AS c FROM present"""
+        .map(rs => rs.long("c"))
+        .first()
+        .apply()
+        .get // safe
+    }
+  }
 }

From fdfb0e732393f50bea8ca43223e0f24ca781743b Mon Sep 17 00:00:00 2001
From: Kisaragi <48310258+KisaragiEffective@users.noreply.github.com>
Date: Tue, 3 Aug 2021 20:44:32 +0900
Subject: [PATCH 42/73] Update
 src/main/scala/com/github/unchama/seichiassist/subsystems/present/domain/PresentPersistence.scala

---
 .../subsystems/present/domain/PresentPersistence.scala          | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/present/domain/PresentPersistence.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/present/domain/PresentPersistence.scala
index 6314f95360..5a7a9f1576 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/present/domain/PresentPersistence.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/present/domain/PresentPersistence.scala
@@ -9,7 +9,7 @@ import java.util.UUID
 /**
  * プレゼントシステムに関する永続化のインターフェースを規定するトレイト。
  * 他で指定がない限り、以下の条件および制約を満たす。違反した時の動作は未定義である:
- *   - 引数で渡される`PresentID`は対応するプレゼントが存在し、その多重度は
+ *   - 引数で渡される`PresentID`は対応するプレゼントが存在し、その多重度は1対1である
  *   - 返り値としての`PresentID`は対応するプレゼントが存在する
  */
 trait PresentPersistence[F[_], ItemStack] {

From e22050539b893738e9cb14f8e8ac80a1fd2504b6 Mon Sep 17 00:00:00 2001
From: KisaragiEffective <kisaragi.effective@gmail.com>
Date: Tue, 3 Aug 2021 21:20:28 +0900
Subject: [PATCH 43/73] [fix] DeleteResult.NotFound

---
 .../subsystems/present/bukkit/command/PresentCommand.scala      | 2 +-
 .../subsystems/present/domain/OperationResult.scala             | 2 +-
 .../present/infrastructure/JdbcBackedPresentPersistence.scala   | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/present/bukkit/command/PresentCommand.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/present/bukkit/command/PresentCommand.scala
index 22b96ce74a..e669166752 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/present/bukkit/command/PresentCommand.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/present/bukkit/command/PresentCommand.scala
@@ -269,7 +269,7 @@ class PresentCommand(implicit val ioOnMainThread: OnMinecraftServerThread[IO]) {
               } yield result match {
                 case DeleteResult.Done =>
                   MessageEffect(s"IDが${presentId}のプレゼントの消去は正常に行われました。")
-                case DeleteResult.NotFount =>
+                case DeleteResult.NotFound =>
                   MessageEffect(s"IDが${presentId}のプレゼントは存在しませんでした。")
               }
 
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/present/domain/OperationResult.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/present/domain/OperationResult.scala
index ddb926eeea..ea10305e35 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/present/domain/OperationResult.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/present/domain/OperationResult.scala
@@ -4,6 +4,6 @@ object OperationResult {
   sealed trait DeleteResult
   object DeleteResult {
     case object Done extends DeleteResult
-    case object NotFount extends DeleteResult
+    case object NotFound extends DeleteResult
   }
 }
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/present/infrastructure/JdbcBackedPresentPersistence.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/present/infrastructure/JdbcBackedPresentPersistence.scala
index 15f422bc21..1c7971bc28 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/present/infrastructure/JdbcBackedPresentPersistence.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/present/infrastructure/JdbcBackedPresentPersistence.scala
@@ -43,7 +43,7 @@ class JdbcBackedPresentPersistence[F[_] : Sync] extends PresentPersistence[F, It
           .update()
           .apply()
 
-      if (deletedRows == 1) DeleteResult.Done else DeleteResult.NotFount
+      if (deletedRows == 1) DeleteResult.Done else DeleteResult.NotFound
     }
   }
 

From f3a7cdd41a008436d2f335401d78154fa00601cb Mon Sep 17 00:00:00 2001
From: Kory <6561358+kory33@users.noreply.github.com>
Date: Wed, 4 Aug 2021 22:09:28 +0900
Subject: [PATCH 44/73] =?UTF-8?q?[fix]=20docker-compose.yml=E3=81=A7?=
 =?UTF-8?q?=E3=81=AEport=E3=82=92=E5=85=A8=E3=81=A6quote=E3=81=99=E3=82=8B?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 docker-compose.yml | 16 ++++++++--------
 1 file changed, 8 insertions(+), 8 deletions(-)

diff --git a/docker-compose.yml b/docker-compose.yml
index f27ab626d7..884372aa0e 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -21,8 +21,8 @@ services:
     volumes:
       - spigot_a:/spigot/
     ports:
-      - 25566:25565
-      - 7091:7091
+      - "25566:25565"
+      - "7091:7091"
     environment:
       - JMX_PORT=7091
       - JMX_BINDING=0.0.0.0
@@ -47,8 +47,8 @@ services:
     volumes:
       - spigot_b:/spigot/
     ports:
-      - 25567:25565
-      - 7092:7091
+      - "25567:25565"
+      - "7092:7091"
     environment:
       - JMX_PORT=7091
       - JMX_BINDING=0.0.0.0
@@ -68,7 +68,7 @@ services:
       context: .
       dockerfile: ./docker/bungeecord/Dockerfile
     ports:
-      - 25565:25577
+      - "25565:25577"
     networks:
       - seichi
     environment:
@@ -84,7 +84,7 @@ services:
       context: .
       dockerfile: ./docker/bungeecord/Dockerfile
     ports:
-      - 25564:25577
+      - "25564:25577"
     networks:
       - seichi
     environment:
@@ -108,7 +108,7 @@ services:
     networks:
       - seichi
     ports:
-      - 8080:80
+      - "8080:80"
     depends_on:
       - db
   redis:
@@ -131,4 +131,4 @@ services:
     volumes:
       - db-data:/var/lib/mysql
     ports:
-      - 3306:3306
+      - "3306:3306"

From 14f24b1de00d1a933eb2556552b5ca1fb8eeaa17 Mon Sep 17 00:00:00 2001
From: Kory <6561358+kory33@users.noreply.github.com>
Date: Thu, 5 Aug 2021 14:38:36 +0900
Subject: [PATCH 45/73] =?UTF-8?q?[update]=20/present=20list=E3=81=AE?=
 =?UTF-8?q?=E8=AA=AC=E6=98=8E=E6=96=87=E3=82=92=E3=82=88=E3=82=8A=E6=AD=A3?=
 =?UTF-8?q?=E7=A2=BA=E3=81=AB=E3=81=99=E3=82=8B?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../subsystems/present/bukkit/command/PresentCommand.scala    | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/present/bukkit/command/PresentCommand.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/present/bukkit/command/PresentCommand.scala
index adb658a801..eb89b9016a 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/present/bukkit/command/PresentCommand.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/present/bukkit/command/PresentCommand.scala
@@ -88,8 +88,8 @@ class PresentCommand(implicit val ioOnMainThread: OnMinecraftServerThread[IO]) {
 
     object ListSubCommand {
       val help: EchoExecutor = EchoExecutor(MessageEffect(List(
-        "/present list",
-        "    全てのプレゼントを表示します",
+        "/present list <ページ数>",
+        "    全てのプレゼントをページに分けて表示します",
       )))
 
       /**

From de43ee72984b0f68e007ea1be6d19d1bb62a6fda Mon Sep 17 00:00:00 2001
From: KisaragiEffective <kisaragi.effective@gmail.com>
Date: Thu, 5 Aug 2021 19:14:38 +0900
Subject: [PATCH 46/73] [update] apply review

---
 .../bukkit/command/PresentCommand.scala       | 20 +++--
 .../present/domain/PresentPersistence.scala   | 12 +--
 .../present/domain/RevokeWarning.scala        |  8 ++
 .../JdbcBackedPresentPersistence.scala        | 84 ++++++++++---------
 4 files changed, 70 insertions(+), 54 deletions(-)
 create mode 100644 src/main/scala/com/github/unchama/seichiassist/subsystems/present/domain/RevokeWarning.scala

diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/present/bukkit/command/PresentCommand.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/present/bukkit/command/PresentCommand.scala
index e669166752..ecd834367f 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/present/bukkit/command/PresentCommand.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/present/bukkit/command/PresentCommand.scala
@@ -12,7 +12,7 @@ import com.github.unchama.minecraft.actions.OnMinecraftServerThread
 import com.github.unchama.seichiassist.commands.contextual.builder.BuilderTemplates.playerCommandBuilder
 import com.github.unchama.seichiassist.domain.actions.UuidToLastSeenName
 import com.github.unchama.seichiassist.subsystems.present.domain.OperationResult.DeleteResult
-import com.github.unchama.seichiassist.subsystems.present.domain.{GrantRejectReason, PaginationRejectReason, PresentClaimingState, PresentPersistence}
+import com.github.unchama.seichiassist.subsystems.present.domain.{GrantRejectReason, PaginationRejectReason, PresentClaimingState, PresentPersistence, RevokeWarning}
 import com.github.unchama.seichiassist.util.Util
 import com.github.unchama.targetedeffect.commandsender.MessageEffect
 import com.github.unchama.targetedeffect.{SequentialEffect, TargetedEffect}
@@ -83,8 +83,7 @@ class PresentCommand(implicit val ioOnMainThread: OnMinecraftServerThread[IO]) {
               List("対象のプレゼントが存在しません")
             } else {
               List(
-                "対象のプレゼント一覧:",
-                "------------------"
+                s"${ChatColor.GRAY}${ChatColor.UNDERLINE}対象のプレゼント一覧:${ChatColor.RESET}",
               ) ::: presents
             }
 
@@ -383,11 +382,18 @@ class PresentCommand(implicit val ioOnMainThread: OnMinecraftServerThread[IO]) {
                 else
                   globalUUID2Name.filter { case (_, name) => restArg.contains(name) }.keys
                 errorIfNobody = if (target.isEmpty) Some(MessageEffect("対象のプレイヤーが存在しません!")) else None
-                _ <- persistence.revoke(presentId, target.toSet)
+                warning <- persistence.revoke(presentId, target.toSet)
               } yield {
-                errorIfNobody.getOrElse(MessageEffect(
-                  s"プレゼント(id: $presentId)を受け取れるプレイヤーを削除しました。"
-                ))
+                errorIfNobody.getOrElse {
+                  warning.map {
+                    case RevokeWarning.NoSuchPresentID => MessageEffect("そのようなプレゼントIDはありません!")
+                    case RevokeWarning.NoPlayers => MessageEffect("対象となるプレイヤーが存在しません!")
+                  }.getOrElse {
+                    MessageEffect(
+                      s"プレゼント(id: $presentId)を受け取れるプレイヤーを削除しました。"
+                    )
+                  }
+                }
               }
               eff.toIO
             } else {
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/present/domain/PresentPersistence.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/present/domain/PresentPersistence.scala
index 5a7a9f1576..5d05d0f985 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/present/domain/PresentPersistence.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/present/domain/PresentPersistence.scala
@@ -47,7 +47,7 @@ trait PresentPersistence[F[_], ItemStack] {
    * @param players   受け取ることができないようにするプレイヤーのUUID
    * @return 永続化層への書き込みを行う作用
    */
-  def revoke(presentID: PresentID, players: Set[UUID]): F[Unit]
+  def revoke(presentID: PresentID, players: Set[UUID]): F[Option[RevokeWarning]]
 
   /**
    * 永続化層でプレゼントを受け取ったことにする。
@@ -67,8 +67,8 @@ trait PresentPersistence[F[_], ItemStack] {
 
   /**
    * ページネーション付きでプレイヤーがプレゼントを受け取ることができるかどうか列挙する。
-   * このときのページネーションは、[[PresentID]]が最も若いエントリから先に出現するように行われるが、
-   * ページネーションされたMap内での各エントリの出現順序は未規定である。
+   * このときのページネーションは、[[PresentID]]が最も若いエントリから先に出現するように行われる。
+   * また、ページネーションされたListの中での出現順序も、[[PresentID]]が最も若いエントリから先に出現する。
    *
    * 例として以下のような状況を仮定する:
    *   - 既知のPresentIDとItemStackのエントリ: `List((1, aaa), (3, ccc), (6, fff), (4, ddd), (5, eee), (2, bbb))`
@@ -79,7 +79,7 @@ trait PresentPersistence[F[_], ItemStack] {
    *
    * この時 `pp.mappingWithPagination(A, 1, 5)` を呼び出すと、作用の中で計算される結果は次のとおりになる:
    *
-   * `Map(1 -> Claimed, 2 -> Claimed, 3 -> Claimed, 4 -> NotClaimed, 5 -> Unavailable)`
+   * `List(1 -> Claimed, 2 -> Claimed, 3 -> Claimed, 4 -> NotClaimed, 5 -> Unavailable)`
    *
    * 備考:
    *   - 実装によっては、[[fetchState]]などを呼び出して有効なエントリを全列挙する可能性がある。
@@ -96,7 +96,7 @@ trait PresentPersistence[F[_], ItemStack] {
                                 player: UUID,
                                 perPage: Int Refined Positive,
                                 page: Int Refined Positive
-                              ): F[Either[PaginationRejectReason, Map[PresentID, PresentClaimingState]]]
+                              ): F[Either[PaginationRejectReason, List[(PresentID, PresentClaimingState)]]]
 
   /**
    * プレイヤーがプレゼントを受け取ることができるかどうか列挙する。このとき、計算されるMapは次の性質を満たす:
@@ -112,7 +112,7 @@ trait PresentPersistence[F[_], ItemStack] {
   def fetchState(player: UUID): F[Map[PresentID, PresentClaimingState]]
 
   /**
-   * 指定したプレゼントIDからプレゼントを引き出す。
+   * 指定したプレゼントIDでプレゼントを検索する。
    *
    * @param presentID プレゼントID
    * @return 存在する場合は`Some[ItemStack]`、存在しない場合は`None`を返す作用
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/present/domain/RevokeWarning.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/present/domain/RevokeWarning.scala
new file mode 100644
index 0000000000..a724c97ac1
--- /dev/null
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/present/domain/RevokeWarning.scala
@@ -0,0 +1,8 @@
+package com.github.unchama.seichiassist.subsystems.present.domain
+
+sealed trait RevokeWarning
+
+object RevokeWarning {
+  case object NoSuchPresentID extends RevokeWarning
+  case object NoPlayers extends RevokeWarning
+}
\ No newline at end of file
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/present/infrastructure/JdbcBackedPresentPersistence.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/present/infrastructure/JdbcBackedPresentPersistence.scala
index 1c7971bc28..8dcf41ab8f 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/present/infrastructure/JdbcBackedPresentPersistence.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/present/infrastructure/JdbcBackedPresentPersistence.scala
@@ -3,7 +3,7 @@ package com.github.unchama.seichiassist.subsystems.present.infrastructure
 import cats.Applicative
 import cats.effect.Sync
 import com.github.unchama.seichiassist.subsystems.present.domain.OperationResult.DeleteResult
-import com.github.unchama.seichiassist.subsystems.present.domain.{GrantRejectReason, PaginationRejectReason, PresentClaimingState, PresentPersistence}
+import com.github.unchama.seichiassist.subsystems.present.domain.{GrantRejectReason, PaginationRejectReason, PresentClaimingState, PresentPersistence, RevokeWarning}
 import eu.timepit.refined.api.Refined
 import eu.timepit.refined.auto._
 import eu.timepit.refined.numeric.Positive
@@ -49,7 +49,7 @@ class JdbcBackedPresentPersistence[F[_] : Sync] extends PresentPersistence[F, It
 
   override def grant(presentID: PresentID, players: Set[UUID]): F[Option[GrantRejectReason]] = {
     import cats.implicits._
-    for {
+    val program = for {
       exists <- Sync[F].delay {
         DB.readOnly { implicit session =>
           sql"""SELECT present_id FROM present"""
@@ -60,54 +60,58 @@ class JdbcBackedPresentPersistence[F[_] : Sync] extends PresentPersistence[F, It
       }
     } yield {
       if (exists) {
-        val alreadyAddedPlayers = DB.readOnly { implicit session =>
-          sql"""SELECT uuid FROM present_state WHERE present_id = $presentID"""
-            .map(x => UUID.fromString(x.string("uuid")))
-            .list()
-            .apply()
-        }
-
-        import scala.collection.Seq.iterableFactory
-
-        val initialValues = players
-          // すでに存在しているプレゼントIDとプレイヤーの組をINSERT
-          // すると整合性違反になるためフィルタ
-          .filterNot { alreadyAddedPlayers contains _ }
-          .map { uuid => Seq(presentID, uuid.toString, false) }
-          .toSeq
-
-        DB.localTx { implicit session =>
-          sql"""INSERT INTO present_state VALUES (?, ?, ?)"""
-            .batch(initialValues: _*)
-            .apply()
+        Sync[F].delay {
+          val alreadyAddedPlayers = DB.readOnly { implicit session =>
+            sql"""SELECT uuid FROM present_state WHERE present_id = $presentID"""
+              .map(x => UUID.fromString(x.string("uuid")))
+              .list()
+              .apply()
+          }
+
+          import scala.collection.Seq.iterableFactory
+
+          val initialValues = players
+            // すでに存在しているプレゼントIDとプレイヤーの組をINSERT
+            // すると整合性違反になるためフィルタ
+            .filterNot { alreadyAddedPlayers contains _ }
+            .map { uuid => Seq(presentID, uuid.toString, false) }
+            .toSeq
+
+          DB.localTx { implicit session =>
+            sql"""
+                  INSERT INTO present_state VALUES (?, ?, ?)
+                  ON DUPLICATE KEY UPDATE present_id=present_id
+                 """
+              .batch(initialValues: _*)
+              .apply()
+          }
+
+          None
         }
-
-        None
       } else {
-        Some(GrantRejectReason.NoSuchPresentID)
+        // 型推論
+        Applicative[F].pure(Some(GrantRejectReason.NoSuchPresentID: GrantRejectReason))
       }
     }
 
+    program.flatten
   }
 
-  override def revoke(presentID: PresentID, players: Set[UUID]): F[Unit] = {
-    val existence = DB.readOnly { implicit session =>
-      sql"""SELECT present_state FROM present_state WHERE present_id = $presentID"""
-        .map(_ => ()) // avoid NoExtractor
-        .first()
-        .apply()
-    }
-
-    existence.fold(Applicative[F].pure(())) { _ =>
+  override def revoke(presentID: PresentID, players: Set[UUID]): F[Option[RevokeWarning]] = {
+    if (players.isEmpty) {
+      Applicative[F].pure(Some(RevokeWarning.NoPlayers))
+    } else {
       Sync[F].delay {
         val scopeAsSQL = players.map(_.toString)
 
-        DB.localTx { implicit session =>
+        val deleteCount = DB.localTx { implicit session =>
           // https://discord.com/channels/237758724121427969/565935041574731807/824107651985834004
           sql"""DELETE FROM present_state WHERE present_id = $presentID AND uuid IN ($scopeAsSQL)"""
-            .execute()
+            .update()
             .apply()
         }
+
+        Option.when(deleteCount == 0) { RevokeWarning.NoSuchPresentID }
       }
     }
   }
@@ -139,7 +143,7 @@ class JdbcBackedPresentPersistence[F[_] : Sync] extends PresentPersistence[F, It
                                          player: UUID,
                                          perPage: Int Refined Positive,
                                          page: Int Refined Positive
-                                       ): F[Either[PaginationRejectReason, Map[PresentID, PresentClaimingState]]] = {
+                                       ): F[Either[PaginationRejectReason, List[(PresentID, PresentClaimingState)]]] = {
     import cats.implicits._
     for {
       idSliceWithPagination <- idSliceWithPagination(perPage, page)
@@ -160,7 +164,6 @@ class JdbcBackedPresentPersistence[F[_] : Sync] extends PresentPersistence[F, It
             .map(wrapResultForState)
             .toList()
             .apply()
-            .toMap
         }
 
         Right(filledEntries(associatedEntries, idSliceWithPagination))
@@ -179,7 +182,6 @@ class JdbcBackedPresentPersistence[F[_] : Sync] extends PresentPersistence[F, It
           .map(wrapResultForState)
           .list()
           .apply()
-          .toMap
       }
 
       filledEntries(associatedEntries, validPresentIDs)
@@ -220,7 +222,7 @@ class JdbcBackedPresentPersistence[F[_] : Sync] extends PresentPersistence[F, It
     ItemStackBlobProxy.blobToItemStack(rs.string("itemstack"))
   }
 
-  private def filledEntries(knownState: Map[PresentID, PresentClaimingState], validGlobalId: Iterable[PresentID]) = {
+  private def filledEntries(knownState: List[(PresentID, PresentClaimingState)], validGlobalId: Iterable[PresentID]) = {
     val globalEntries = validGlobalId.map(id => (id, PresentClaimingState.Unavailable)).toMap
     globalEntries ++ knownState
   }
@@ -228,7 +230,7 @@ class JdbcBackedPresentPersistence[F[_] : Sync] extends PresentPersistence[F, It
   private def computeValidPresentCount: F[Long] = Sync[F].delay {
     DB.readOnly { implicit session =>
       sql"""SELECT COUNT(*) AS c FROM present"""
-        .map(rs => rs.long("c"))
+        .map(_.long("c"))
         .first()
         .apply()
         .get // safe

From 8b6e2f0f297d625889cfabfa3eb8c41510c255c8 Mon Sep 17 00:00:00 2001
From: KisaragiEffective <kisaragi.effective@gmail.com>
Date: Fri, 6 Aug 2021 18:00:25 +0900
Subject: [PATCH 47/73] [fix] compile error

---
 .../JdbcBackedPresentPersistence.scala            | 15 ++++++++-------
 1 file changed, 8 insertions(+), 7 deletions(-)

diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/present/infrastructure/JdbcBackedPresentPersistence.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/present/infrastructure/JdbcBackedPresentPersistence.scala
index 8dcf41ab8f..d43786098a 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/present/infrastructure/JdbcBackedPresentPersistence.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/present/infrastructure/JdbcBackedPresentPersistence.scala
@@ -86,11 +86,12 @@ class JdbcBackedPresentPersistence[F[_] : Sync] extends PresentPersistence[F, It
               .apply()
           }
 
-          None
+          // 型推論
+          None: Option[GrantRejectReason]
         }
       } else {
         // 型推論
-        Applicative[F].pure(Some(GrantRejectReason.NoSuchPresentID: GrantRejectReason))
+        Applicative[F].pure(Some(GrantRejectReason.NoSuchPresentID: GrantRejectReason): Option[GrantRejectReason])
       }
     }
 
@@ -166,7 +167,7 @@ class JdbcBackedPresentPersistence[F[_] : Sync] extends PresentPersistence[F, It
             .apply()
         }
 
-        Right(filledEntries(associatedEntries, idSliceWithPagination))
+        Right(filledEntries(associatedEntries, idSliceWithPagination).toList)
       }
     }
   }
@@ -184,7 +185,7 @@ class JdbcBackedPresentPersistence[F[_] : Sync] extends PresentPersistence[F, It
           .apply()
       }
 
-      filledEntries(associatedEntries, validPresentIDs)
+      filledEntries(associatedEntries, validPresentIDs).toMap
     }
   }
 
@@ -222,9 +223,9 @@ class JdbcBackedPresentPersistence[F[_] : Sync] extends PresentPersistence[F, It
     ItemStackBlobProxy.blobToItemStack(rs.string("itemstack"))
   }
 
-  private def filledEntries(knownState: List[(PresentID, PresentClaimingState)], validGlobalId: Iterable[PresentID]) = {
-    val globalEntries = validGlobalId.map(id => (id, PresentClaimingState.Unavailable)).toMap
-    globalEntries ++ knownState
+  private def filledEntries(knownState: List[(PresentID, PresentClaimingState)], validGlobalId: Iterable[PresentID]): Iterable[(PresentID, PresentClaimingState)] = {
+    val globalEntries = validGlobalId.map(id => (id, PresentClaimingState.Unavailable))
+    (globalEntries ++ knownState)
   }
 
   private def computeValidPresentCount: F[Long] = Sync[F].delay {

From 95f68c286cebf9408d77dab0136a37ed9dcf7de2 Mon Sep 17 00:00:00 2001
From: KisaragiEffective <kisaragi.effective@gmail.com>
Date: Fri, 6 Aug 2021 21:19:57 +0900
Subject: [PATCH 48/73] [update] apply review (upsert)

---
 .../infrastructure/JdbcBackedPresentPersistence.scala       | 6 ++----
 1 file changed, 2 insertions(+), 4 deletions(-)

diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/present/infrastructure/JdbcBackedPresentPersistence.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/present/infrastructure/JdbcBackedPresentPersistence.scala
index d43786098a..f6d8160f26 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/present/infrastructure/JdbcBackedPresentPersistence.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/present/infrastructure/JdbcBackedPresentPersistence.scala
@@ -71,16 +71,14 @@ class JdbcBackedPresentPersistence[F[_] : Sync] extends PresentPersistence[F, It
           import scala.collection.Seq.iterableFactory
 
           val initialValues = players
-            // すでに存在しているプレゼントIDとプレイヤーの組をINSERT
-            // すると整合性違反になるためフィルタ
-            .filterNot { alreadyAddedPlayers contains _ }
             .map { uuid => Seq(presentID, uuid.toString, false) }
             .toSeq
 
           DB.localTx { implicit session =>
+            // upsert - これによってfilterなしで整合性違反を起こすことはなくなる
             sql"""
                   INSERT INTO present_state VALUES (?, ?, ?)
-                  ON DUPLICATE KEY UPDATE present_id=present_id
+                  ON DUPLICATE KEY UPDATE present_id=present_id, uuid=uuid
                  """
               .batch(initialValues: _*)
               .apply()

From 3a44230b155e19c823ff99c13e51f0bfd69e9828 Mon Sep 17 00:00:00 2001
From: KisaragiEffective <kisaragi.effective@gmail.com>
Date: Sat, 7 Aug 2021 14:46:17 +0900
Subject: [PATCH 49/73] [update] close #1126

---
 .../github/unchama/seichiassist/listener/EntityListener.scala    | 1 +
 .../unchama/seichiassist/listener/PlayerBlockBreakListener.scala | 1 +
 2 files changed, 2 insertions(+)

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 c54a52fcee..0e90dc4945 100644
--- a/src/main/scala/com/github/unchama/seichiassist/listener/EntityListener.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/listener/EntityListener.scala
@@ -48,6 +48,7 @@ class EntityListener(implicit effectEnvironment: EffectEnvironment,
 
     //整地ワールドでは重力値によるキャンセル判定を行う(スキル判定より先に判定させること)
     if (BreakUtil.getGravity(player, block, isAssault = false) > 3) {
+      player.playSound(player.getLocation, Sound.BLOCK_ANVIL_FALL, 0.0F, -1.0F)
       player.sendMessage(ChatColor.RED + "整地ワールドでは必ず上から掘ってください。")
       return
     }
diff --git a/src/main/scala/com/github/unchama/seichiassist/listener/PlayerBlockBreakListener.scala b/src/main/scala/com/github/unchama/seichiassist/listener/PlayerBlockBreakListener.scala
index adcfd64d45..8d5f9d80c1 100644
--- a/src/main/scala/com/github/unchama/seichiassist/listener/PlayerBlockBreakListener.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/listener/PlayerBlockBreakListener.scala
@@ -52,6 +52,7 @@ class PlayerBlockBreakListener(implicit effectEnvironment: EffectEnvironment,
     if (!MaterialSets.gravityMaterials.contains(block.getType) &&
       !MaterialSets.cancelledMaterials.contains(block.getType) && gravity > 15) {
 
+      player.playSound(player.getLocation, Sound.BLOCK_ANVIL_FALL, 0.0F, -1.0F)
       player.sendMessage(s"${RED}整地ワールドでは必ず上から掘ってください。")
       event.setCancelled(true)
       return

From be337e7b9f4e8461065e2c57d7661f4ce98621ba Mon Sep 17 00:00:00 2001
From: Lucky <lucky3028@users.noreply.github.com>
Date: Mon, 9 Aug 2021 15:11:32 +0900
Subject: [PATCH 50/73] =?UTF-8?q?[Remove]=20ItemStack=E3=81=8CTITAN?=
 =?UTF-8?q?=E3=81=8B=E3=81=A9=E3=81=86=E3=81=8B=E5=88=A4=E5=AE=9A=E3=81=99?=
 =?UTF-8?q?=E3=82=8B=E9=96=A2=E6=95=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../scala/com/github/unchama/seichiassist/util/Util.scala   | 6 ------
 1 file changed, 6 deletions(-)

diff --git a/src/main/scala/com/github/unchama/seichiassist/util/Util.scala b/src/main/scala/com/github/unchama/seichiassist/util/Util.scala
index f95751edcd..250f496fe9 100644
--- a/src/main/scala/com/github/unchama/seichiassist/util/Util.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/util/Util.scala
@@ -495,11 +495,6 @@ object Util {
     Some(block.getDrops.asScala.head.tap(_.setDurability(SkullType.PLAYER.ordinal.toShort)))
   }
 
-  def isLimitedTitanItem(itemstack: ItemStack): Boolean = {
-    itemstack.getType == Material.DIAMOND_AXE &&
-      isContainedInLore(itemstack, "特別なタイタンをあなたに♡")
-  }
-
   /**
    * 指定された`String`が指定された[[ItemStack]]のloreに含まれているかどうか
    *
@@ -511,7 +506,6 @@ object Util {
     if (!itemStack.hasItemMeta || !itemStack.getItemMeta.hasLore) false
     else loreIndexOf(itemStack.getItemMeta.getLore.asScala.toList, sentence) >= 0
 
-
   /**
    * loreを捜査して、要素の中に`find`が含まれているかを調べる。
    *

From 6483ddccc795d2150077a0b44493642ef4f634e2 Mon Sep 17 00:00:00 2001
From: Lucky <lucky3028@users.noreply.github.com>
Date: Mon, 9 Aug 2021 15:11:53 +0900
Subject: [PATCH 51/73] =?UTF-8?q?[Remove]=20=E4=BF=AE=E7=B9=95=E3=83=A1?=
 =?UTF-8?q?=E3=83=8B=E3=83=A5=E3=83=BC=E3=83=9C=E3=82=BF=E3=83=B3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../github/unchama/seichiassist/menus/stickmenu/SecondPage.scala | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/main/scala/com/github/unchama/seichiassist/menus/stickmenu/SecondPage.scala b/src/main/scala/com/github/unchama/seichiassist/menus/stickmenu/SecondPage.scala
index e50aad1c45..1538c558fd 100644
--- a/src/main/scala/com/github/unchama/seichiassist/menus/stickmenu/SecondPage.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/menus/stickmenu/SecondPage.scala
@@ -57,7 +57,6 @@ object SecondPage extends Menu {
       ChestSlotRef(0, 8) -> hubCommandButton,
       ChestSlotRef(3, 0) -> CommonButtons.openStickMenu,
       ChestSlotRef(3, 3) -> recycleBinButton,
-      ChestSlotRef(3, 7) -> titanConversionButton,
       ChestSlotRef(3, 8) -> appleConversionButton
     )
 

From 84461f31e6a42bdad1ed02ffd48c04fbbd6718a1 Mon Sep 17 00:00:00 2001
From: Lucky <lucky3028@users.noreply.github.com>
Date: Mon, 9 Aug 2021 15:12:10 +0900
Subject: [PATCH 52/73] =?UTF-8?q?[Remove]=20=E4=BF=AE=E7=B9=95=E3=83=A1?=
 =?UTF-8?q?=E3=83=8B=E3=83=A5=E3=83=BC=E3=81=8C=E9=96=89=E3=81=98=E3=82=89?=
 =?UTF-8?q?=E3=82=8C=E3=81=9F=E6=99=82=E3=81=AE=E5=87=A6=E7=90=86?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../listener/PlayerInventoryListener.scala    | 41 -------------------
 1 file changed, 41 deletions(-)

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 08f8f927f4..c5dcd9b25c 100644
--- a/src/main/scala/com/github/unchama/seichiassist/listener/PlayerInventoryListener.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/listener/PlayerInventoryListener.scala
@@ -364,47 +364,6 @@ class PlayerInventoryListener(implicit effectEnvironment: EffectEnvironment,
 
   }
 
-  @EventHandler
-  def onTitanRepairEvent(event: InventoryCloseEvent): Unit = {
-    val player = event.getPlayer.asInstanceOf[Player]
-    //エラー分岐
-    val inventory = event.getInventory
-
-    //インベントリサイズが36でない時終了
-    if (inventory.row != 4) {
-      return
-    }
-    if (inventory.getTitle == GOLD.toString + "" + BOLD + "修繕したい限定タイタンを入れてネ") {
-      //インベントリの中身を取得
-      val item = inventory.getContents
-
-      var count = 0
-      //for文で1個ずつ対象アイテムか見る
-      //インベントリを一個ずつ見ていくfor文
-      for (m <- item) {
-        if (m != null) {
-          if (m.getItemMeta.hasLore) {
-            if (Util.isLimitedTitanItem(m)) {
-              m.setDurability(1.toShort)
-              count += 1
-            }
-          }
-
-          if (!Util.isPlayerInventoryFull(player)) {
-            Util.addItem(player, m)
-          } else {
-            Util.dropItem(player, m)
-          }
-        }
-      }
-      if (count < 1) {
-        player.sendMessage(GREEN.toString + "限定タイタンを認識しませんでした。すべてのアイテムを返却します")
-      } else {
-        player.sendMessage(GREEN.toString + "限定タイタンを" + count + "個認識し、修繕しました。")
-      }
-    }
-  }
-
   //投票ptメニュー
   @EventHandler
   def onVotingMenuEvent(event: InventoryClickEvent): Unit = {

From ebb8c8ca87c064a540ec2346dd0926453bff53ac Mon Sep 17 00:00:00 2001
From: Kisaragi <48310258+KisaragiEffective@users.noreply.github.com>
Date: Mon, 9 Aug 2021 22:56:42 +0900
Subject: [PATCH 53/73] =?UTF-8?q?[update]=20=E3=83=87=E3=83=90=E3=83=83?=
 =?UTF-8?q?=E3=82=B0=E9=AF=96=E3=81=AE=E3=82=A2=E3=83=89=E3=83=AC=E3=82=B9?=
 =?UTF-8?q?=E6=9B=B4=E6=96=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 README.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/README.md b/README.md
index e39e8c3565..92b0a3980e 100644
--- a/README.md
+++ b/README.md
@@ -87,7 +87,7 @@ masterブランチは本番環境に反映されます。
 
 ## AutoRelease
 - developブランチが更新されると、そのコードを基に実行用jarがビルドされ、デバッグ環境に配布されます。デバッグ環境はjarの配布を検知すると自動で再起動し、最新のjarを使用して稼働します。
-  - デバッグ環境へは、Minecraft Java Editionで`play.seichi.click`に接続し、`T`キーでチャットを開き、`/server deb112`と入力して`Enter`を押すとアクセスできます。
+  - デバッグ環境へは、Minecraft Java Editionで`play-debug.seichi.click`に接続し、`T`キーでチャットを開き、`/server deb112`と入力して`Enter`を押すとアクセスできます。
 - masterブランチが更新されると、そのコードを基に実行用jarがビルドされ、本番環境に配布されます。本番環境は翌再起動時に自動で最新のjarを取り込んで稼働します。
 - jar以外の自動リリースは未対応です(config.ymlなど)。運営チームへ更新を依頼する必要があります。
   - 各サーバーや環境で共通で構わないパラメータはconfig.ymlを読まず、コードへの直接実装を推奨します。

From 54cb867eda0a9e0bf23b888bbcd270810daa08bd Mon Sep 17 00:00:00 2001
From: KisaragiEffective <kisaragi.effective@gmail.com>
Date: Wed, 25 Aug 2021 22:28:55 +0900
Subject: [PATCH 54/73] =?UTF-8?q?[update]=20=E5=BB=BA=E7=AF=89=E3=82=B5?=
 =?UTF-8?q?=E3=83=BC=E3=83=90=E3=83=BC=E3=81=AF=E3=82=82=E3=81=86=E5=AD=98?=
 =?UTF-8?q?=E5=9C=A8=E3=81=97=E3=81=AA=E3=81=84=E3=81=AE=E3=81=A7=E3=82=B5?=
 =?UTF-8?q?=E3=83=BC=E3=83=90=E3=83=BC=E9=96=93=E7=A7=BB=E5=8B=95=E3=83=9C?=
 =?UTF-8?q?=E3=82=BF=E3=83=B3=E3=81=8B=E3=82=89=E8=A1=A8=E8=A8=98=E3=82=92?=
 =?UTF-8?q?=E5=89=8A=E9=99=A4?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../github/unchama/seichiassist/menus/stickmenu/FirstPage.scala  | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/main/scala/com/github/unchama/seichiassist/menus/stickmenu/FirstPage.scala b/src/main/scala/com/github/unchama/seichiassist/menus/stickmenu/FirstPage.scala
index a4dea4d0c9..4310efca48 100644
--- a/src/main/scala/com/github/unchama/seichiassist/menus/stickmenu/FirstPage.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/menus/stickmenu/FirstPage.scala
@@ -549,7 +549,6 @@ object FirstPage extends Menu {
     def teleportServerButton(implicit ioCanOpenServerSwitchMenu: IO CanOpen ServerSwitchMenu.type): Button = {
       val buttonLore = List(
         s"$GRAY・各サバイバルサーバー",
-        s"$GRAY・建築サーバー",
         s"$GRAY・公共施設サーバー",
         s"${GRAY}間を移動する時に使います",
         s"$DARK_RED${UNDERLINE}クリックして開く"

From 6a429658f547be353fc3a7273d000e2ccb225033 Mon Sep 17 00:00:00 2001
From: KisaragiEffective <kisaragi.effective@gmail.com>
Date: Wed, 25 Aug 2021 22:55:27 +0900
Subject: [PATCH 55/73] =?UTF-8?q?[fix]=20Y5=E4=BA=8C=E6=AE=B5=E3=82=B9?=
 =?UTF-8?q?=E3=83=A9=E3=83=96=E3=81=AEY=E5=BA=A7=E6=A8=99=E5=88=A4?=
 =?UTF-8?q?=E5=AE=9A=E3=81=8C=E6=8A=9C=E3=81=91=E3=81=A6=E3=81=84=E3=81=9F?=
 =?UTF-8?q?=E3=81=9F=E3=82=81=E8=BF=BD=E5=8A=A0=20fix=20#1118?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../unchama/seichiassist/listener/Y5DoubleSlabCanceller.scala   | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/src/main/scala/com/github/unchama/seichiassist/listener/Y5DoubleSlabCanceller.scala b/src/main/scala/com/github/unchama/seichiassist/listener/Y5DoubleSlabCanceller.scala
index 16bb2c7256..4d9a6a493f 100644
--- a/src/main/scala/com/github/unchama/seichiassist/listener/Y5DoubleSlabCanceller.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/listener/Y5DoubleSlabCanceller.scala
@@ -12,6 +12,7 @@ object Y5DoubleSlabCanceller extends Listener {
    *   - プレイヤーが手に持っているブロックが焼き石のハーフブロックである
    *   - 対象座標の改変後ブロックが焼き石の二段重ねハーフブロックである
    *   - 対象座標が整地ワールドを指している
+   *   - 対象ブロックのY座標が5である
    * @see https://github.com/GiganticMinecraft/SeichiAssist/issues/775
    * @param event 対象イベント
    */
@@ -23,6 +24,7 @@ object Y5DoubleSlabCanceller extends Listener {
     if (event.getBlockPlaced.getType ne Material.DOUBLE_STEP) return
     if (event.getBlockPlaced.getData != 0) return
     if (!event.getBlockPlaced.getWorld.isSeichi) return
+    if (event.getBlockPlaced.getY != 5) return
     event.setCancelled(true)
     event.getPlayer.sendMessage("Y5のハーフブロックを二段重ねにすることはできません。")
   }

From d7c001110bf4e24f0dab308498f437c55171daa3 Mon Sep 17 00:00:00 2001
From: KisaragiEffective <kisaragi.effective@gmail.com>
Date: Wed, 25 Aug 2021 23:01:48 +0900
Subject: [PATCH 56/73] [fix] fix #1138

---
 build.sbt | 24 ++++++++++++------------
 1 file changed, 12 insertions(+), 12 deletions(-)

diff --git a/build.sbt b/build.sbt
index a6401c3cba..6a370c0850 100644
--- a/build.sbt
+++ b/build.sbt
@@ -81,8 +81,8 @@ val dependenciesToEmbed = Seq(
 addCompilerPlugin("org.typelevel" %% "kind-projector" % "0.11.0" cross CrossVersion.full)
 
 // localDependenciesはprovidedとして扱い、jarに埋め込まない
-assemblyExcludedJars in assembly := {
-  (fullClasspath in assembly).value
+assembly / assemblyExcludedJars := {
+  (assembly / fullClasspath).value
     .filter { a =>
       def directoryContainsFile(directory: File, file: File) =
         file.absolutePath.startsWith(directory.absolutePath)
@@ -102,33 +102,33 @@ val filesToBeReplacedInResourceFolder = Seq("plugin.yml")
 
 val filteredResourceGenerator = taskKey[Seq[File]]("Resource generator to filter resources")
 
-filteredResourceGenerator in Compile :=
+Compile / filteredResourceGenerator :=
   filterResources(
     filesToBeReplacedInResourceFolder,
     tokenReplacementMap.value,
-    (resourceManaged in Compile).value, (resourceDirectory in Compile).value
+    (Compile / resourceManaged).value, (Compile / resourceDirectory).value
   )
 
-resourceGenerators in Compile += (filteredResourceGenerator in Compile)
+Compile / resourceGenerators += (Compile / filteredResourceGenerator)
 
-unmanagedResources in Compile += baseDirectory.value / "LICENSE"
+Compile / unmanagedResources += baseDirectory.value / "LICENSE"
 
 // トークン置換を行ったファイルをunmanagedResourcesのコピーから除外する
-excludeFilter in unmanagedResources :=
-  filesToBeReplacedInResourceFolder.foldLeft((excludeFilter in unmanagedResources).value)(_.||(_))
+unmanagedResources / excludeFilter :=
+  filesToBeReplacedInResourceFolder.foldLeft((unmanagedResources / excludeFilter).value)(_.||(_))
 
 logLevel := Level.Debug
 
 // ScalaPBの設定
-PB.protoSources in Compile := Seq(baseDirectory.value / "protocol")
-PB.targets in Compile := Seq(scalapb.gen() -> (sourceManaged in Compile).value / "scalapb")
+Compile / PB.protoSources := Seq(baseDirectory.value / "protocol")
+Compile / PB.targets := Seq(scalapb.gen() -> (Compile / sourceManaged).value / "scalapb")
 
-testOptions in Test += Tests.Argument("-oS")
+Compile / testOptions += Tests.Argument("-oS")
 
 lazy val root = (project in file("."))
   .settings(
     name := "SeichiAssist",
-    assemblyOutputPath in assembly := baseDirectory.value / "target" / "build" / s"SeichiAssist.jar",
+    assembly / assemblyOutputPath := baseDirectory.value / "target" / "build" / s"SeichiAssist.jar",
     libraryDependencies := providedDependencies ++ testDependencies ++ dependenciesToEmbed,
     excludeDependencies := Seq(
       ExclusionRule(organization = "org.bukkit", name = "bukkit")

From 9de049b9bc7a07df4a98582d366846c6f9db9262 Mon Sep 17 00:00:00 2001
From: KisaragiEffective <kisaragi.effective@gmail.com>
Date: Mon, 30 Aug 2021 23:41:47 +0900
Subject: [PATCH 57/73] [fix] fix #1150

---
 .../unchama/seichiassist/menus/ranking/RankingMenu.scala      | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/main/scala/com/github/unchama/seichiassist/menus/ranking/RankingMenu.scala b/src/main/scala/com/github/unchama/seichiassist/menus/ranking/RankingMenu.scala
index a31d412b33..fbd9a4b80c 100644
--- a/src/main/scala/com/github/unchama/seichiassist/menus/ranking/RankingMenu.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/menus/ranking/RankingMenu.scala
@@ -97,10 +97,10 @@ object RankingMenuTemplates {
   val vote: RankingMenuTemplate[VoteCount] = new RankingMenuTemplate[VoteCount] {
     override val rankingName: String = "投票神ランキング"
     override def recordDataLore(data: VoteCount): List[String] = List(
-      s"$RESET${GREEN}総投票回数:${data}回"
+      s"$RESET${GREEN}総投票回数:${data.value}回"
     )
     override def combinedDataLore(data: VoteCount): List[String] = List(
-      s"$RESET${AQUA}全プレイヤー総投票回数: ${data}回"
+      s"$RESET${AQUA}全プレイヤー総投票回数: ${data.value}回"
     )
   }
 }

From ae9a91375a836474feaa3c3ed0bd878fa6782efd Mon Sep 17 00:00:00 2001
From: KisaragiEffective <kisaragi.effective@gmail.com>
Date: Mon, 30 Aug 2021 23:47:59 +0900
Subject: [PATCH 58/73] [fix] fix #1148

---
 .../unchama/seichiassist/listener/RegionInventoryListener.scala | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/main/scala/com/github/unchama/seichiassist/listener/RegionInventoryListener.scala b/src/main/scala/com/github/unchama/seichiassist/listener/RegionInventoryListener.scala
index f4f3d4fce8..d9fe0e632e 100644
--- a/src/main/scala/com/github/unchama/seichiassist/listener/RegionInventoryListener.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/listener/RegionInventoryListener.scala
@@ -282,7 +282,7 @@ object RegionInventoryListener {
       case Util.Direction.EAST =>
         (
           new Location(world, start_x - 15 * behindUnitAmount, 0.0, start_z + 15 * leftsideUnitAmount),
-          new Location(world, end_x + 15 * aheadUnitAmount, 256.0, end_z + 15 * rightsideUnitAmount)
+          new Location(world, end_x - 15 * aheadUnitAmount, 256.0, end_z + 15 * rightsideUnitAmount)
         )
 
       case Util.Direction.SOUTH =>

From 96e2e9b4b901617268d06a493bbf5ee68fe388f1 Mon Sep 17 00:00:00 2001
From: KisaragiEffective <kisaragi.effective@gmail.com>
Date: Fri, 3 Sep 2021 20:19:18 +0900
Subject: [PATCH 59/73] [fix] fix #1154

---
 .../subsystems/ranking/domain/values/LoginTime.scala            | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/ranking/domain/values/LoginTime.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/ranking/domain/values/LoginTime.scala
index 1dcef194e3..5f6836e566 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/ranking/domain/values/LoginTime.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/ranking/domain/values/LoginTime.scala
@@ -5,7 +5,7 @@ import cats.kernel.{Monoid, Order}
 /**
  * TODO: rankingサブシステムから移す
  */
-case class LoginTime(inTick: Int) {
+case class LoginTime(inTick: Long) {
   val formatted = s"${inTick / 3600 / 20}時間${inTick / 60 / 20 % 60}分${inTick / 20 % 60}秒"
 }
 

From a50e729b8ff7894280abcead29b3666890109ac6 Mon Sep 17 00:00:00 2001
From: Moca3 <white@nagi.be>
Date: Sat, 11 Sep 2021 21:45:18 +0900
Subject: [PATCH 60/73] =?UTF-8?q?[change]=E3=83=B4=E3=82=A7=E3=83=B3?=
 =?UTF-8?q?=E3=83=80=E3=83=BC=E3=82=B9=E3=82=AD=E3=83=AB=E3=81=AE=E8=AA=AC?=
 =?UTF-8?q?=E6=98=8E=E3=82=92=E5=A4=89=E6=9B=B4=E3=81=99=E3=82=8B?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../unchama/seichiassist/menus/skill/ActiveSkillMenu.scala      | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/main/scala/com/github/unchama/seichiassist/menus/skill/ActiveSkillMenu.scala b/src/main/scala/com/github/unchama/seichiassist/menus/skill/ActiveSkillMenu.scala
index 0de1867cee..6e60b03532 100644
--- a/src/main/scala/com/github/unchama/seichiassist/menus/skill/ActiveSkillMenu.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/menus/skill/ActiveSkillMenu.scala
@@ -298,7 +298,7 @@ object ActiveSkillMenu extends Menu {
                   List(
                     requiredPointDescription,
                     s"$RESET${DARK_RED}水凝固/熔岩凝固の双方を扱える者にのみ発現する上位凝固スキル",
-                    s"$RESET${DARK_RED}アサルト・アーマーの発現には影響しない",
+                    s"$RESET${DARK_RED}アサルト・アーマーの取得には必要ない",
                     s"$RESET$AQUA${UNDERLINE}クリックで解除"
                   )
                 case SeichiSkill.AssaultArmor =>

From e532201674b7965c5bd0959569dacd26a665d552 Mon Sep 17 00:00:00 2001
From: Tea <72652320+LargoGreenTea@users.noreply.github.com>
Date: Wed, 15 Sep 2021 16:51:36 +0900
Subject: [PATCH 61/73] =?UTF-8?q?[add]#1027,#1159=20=E3=82=92=E9=81=A9?=
 =?UTF-8?q?=E7=94=A8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../scala/com/github/unchama/seichiassist/MaterialSets.scala   | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/src/main/scala/com/github/unchama/seichiassist/MaterialSets.scala b/src/main/scala/com/github/unchama/seichiassist/MaterialSets.scala
index cee0207fce..b60f1dab0f 100644
--- a/src/main/scala/com/github/unchama/seichiassist/MaterialSets.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/MaterialSets.scala
@@ -41,6 +41,9 @@ object MaterialSets {
     Material.WOODEN_DOOR, Material.ACACIA_DOOR, Material.BIRCH_DOOR, Material.DARK_OAK_DOOR,
     Material.JUNGLE_DOOR, Material.SPRUCE_DOOR,
     Material.SMOOTH_STAIRS, Material.BREWING_STAND, Material.WOOD_STEP, Material.TNT,
+    //#1027,#1159
+    Material.WOOD_STEP, Material.WOOD_DOUBLE_STEP,
+    Material.DISPENSER, Material.PISTON_STICKY_BASE, Material.STRING
   ) ++ fortuneMaterials
 
   // これらのマテリアルを持つブロックは破壊を整地量に計上しない

From 1ded53c547d174345fe7742ec9a55c4b724d71a8 Mon Sep 17 00:00:00 2001
From: KisaragiEffective <kisaragi.effective@gmail.com>
Date: Mon, 20 Sep 2021 03:37:51 +0900
Subject: [PATCH 62/73] [update] bump sbt to 1.5.5

---
 README.md                | 2 +-
 project/build.properties | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/README.md b/README.md
index 92b0a3980e..597fc663b1 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@
 - [Intellij IDEA](https://www.jetbrains.com/idea/) などの統合開発環境
 - [AdoptOpenJDK 1.8](https://adoptopenjdk.net/?variant=openjdk8&jvmVariant=hotspot)
 - [Scala 2.13](https://www.scala-lang.org/download/)
-- [sbt 1.3.7](https://www.scala-sbt.org/1.x/docs/Setup.html)
+- [sbt 1.5.5](https://www.scala-sbt.org/1.x/docs/Setup.html)
 - Spigot 1.12.2
 
 ## 前提プラグイン
diff --git a/project/build.properties b/project/build.properties
index dbae93bcfd..10fd9eee04 100644
--- a/project/build.properties
+++ b/project/build.properties
@@ -1 +1 @@
-sbt.version=1.4.9
+sbt.version=1.5.5

From 20c1150b66d67e4b2be3cd9d8a493ed458b4a4b9 Mon Sep 17 00:00:00 2001
From: Lucky <lucky3028@users.noreply.github.com>
Date: Thu, 23 Sep 2021 21:17:45 +0900
Subject: [PATCH 63/73] =?UTF-8?q?[Add]=2021=E5=84=84=E4=BC=81=E7=94=BB?=
 =?UTF-8?q?=E5=A0=B1=E9=85=AC=E3=81=AE=E4=B8=80=E9=83=A8=E3=81=AE=E3=82=A2?=
 =?UTF-8?q?=E3=82=A4=E3=83=86=E3=83=A0=E3=81=AB=E8=80=90=E4=B9=85=E5=8A=9B?=
 =?UTF-8?q?5=E3=81=A8=E4=BF=AE=E7=B9=95=E3=82=A8=E3=83=B3=E3=83=81?=
 =?UTF-8?q?=E3=83=A3=E3=83=B3=E3=83=88=E3=82=92=E4=BB=98=E4=B8=8E?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../SeichiAssistItemMigrations.scala          |  3 +-
 ...0_AddEnchantsTo2_1billionRewardItems.scala | 68 +++++++++++++++++++
 2 files changed, 70 insertions(+), 1 deletion(-)
 create mode 100644 src/main/scala/com/github/unchama/seichiassist/subsystems/itemmigration/migrations/V1_4_0_AddEnchantsTo2_1billionRewardItems.scala

diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/itemmigration/migrations/SeichiAssistItemMigrations.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/itemmigration/migrations/SeichiAssistItemMigrations.scala
index 9401d0b895..1643aa25e2 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/itemmigration/migrations/SeichiAssistItemMigrations.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/itemmigration/migrations/SeichiAssistItemMigrations.scala
@@ -17,7 +17,8 @@ object SeichiAssistItemMigrations {
     V1_0_0_MigrateMebiusToNewCodec.migration,
     V1_1_0_AddUnbreakableToNarutoRemake.migration,
     V1_2_0_FixTypoOf4thAnniversaryGT.migration,
-    V1_3_0_RemoveUnnecessaryLoreOfHalloweenItem.migration
+    V1_3_0_RemoveUnnecessaryLoreOfHalloweenItem.migration,
+    V1_4_0_AddEnchantsTo2_1billionRewardItems.migration
   ))
 
 }
diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/itemmigration/migrations/V1_4_0_AddEnchantsTo2_1billionRewardItems.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/itemmigration/migrations/V1_4_0_AddEnchantsTo2_1billionRewardItems.scala
new file mode 100644
index 0000000000..b8752e9d51
--- /dev/null
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/itemmigration/migrations/V1_4_0_AddEnchantsTo2_1billionRewardItems.scala
@@ -0,0 +1,68 @@
+package com.github.unchama.seichiassist.subsystems.itemmigration.migrations
+
+import com.github.unchama.itemmigration.bukkit.util.MigrationHelper
+import com.github.unchama.itemmigration.domain.{ItemMigration, ItemMigrationVersionNumber}
+import org.bukkit.ChatColor._
+import org.bukkit.enchantments.Enchantment
+import org.bukkit.inventory.ItemStack
+
+import scala.jdk.CollectionConverters._
+
+/**
+ * 2021年に行われた5周年記念イベントの中の、21億企画の目標達成報酬として配布されたアイテムに修繕と耐久力5エンチャントを付与する
+ */
+object V1_4_0_AddEnchantsTo2_1billionRewardItems {
+
+  // 年の判別用
+  private val commonLore = s"${WHITE}21/07/22に開催された5周年記念企画"
+
+  // アイテム名だけだと、21億企画報酬ではなく以前のイベントで交換可能だった当該アイテムも含んでしまいかねないので、説明文も確認する
+  private val titanReplicaName = Seq(
+    RED -> "T",
+    GOLD -> "I",
+    YELLOW -> "T",
+    GREEN -> "A",
+    BLUE -> "N ", // スペースは故意
+    WHITE -> "Replica II"
+  ).map { case (color, str) => s"$color$BOLD$ITALIC$str" }.mkString
+  private val titanReplicaLore = s"${WHITE}30億/1日を突破した記念に配布されたものです。"
+
+  private val gaeaReplicaName = Seq(
+    RED -> "G",
+    GOLD -> "A",
+    YELLOW -> "E",
+    GREEN -> "A ", // スペースは故意
+    WHITE -> "Whiteday Replica"
+  ).map { case (color, str) => s"$color$BOLD$ITALIC$str" }.mkString
+  private val gaeaReplicaLore = s"${WHITE}35億/1日を突破した記念に配布されたものです。"
+
+  def migration: ItemMigration = ItemMigration(
+    ItemMigrationVersionNumber(1, 4, 0),
+    MigrationHelper.delegateConversionForContainers(migrationFunction)
+  )
+
+  import eu.timepit.refined.auto._
+
+  def migrationFunction(itemStack: ItemStack): ItemStack = {
+    if (!is2_1billionRewardItems(itemStack)) return itemStack
+
+    import scala.util.chaining._
+
+    itemStack.clone().addEnchantments(Map(Enchantment.MENDING -> 1, Enchantment.DURABILITY -> 5).asJava)
+  }
+
+  def is2_1billionRewardItems(itemStack: ItemStack): Boolean = {
+    if (itemStack == null || !itemStack.hasItemMeta || !itemStack.getItemMeta.hasDisplayName || !itemStack.getItemMeta.hasLore) return false
+    isRewardGaeaReplica(itemStack) || isRewardTitanReplica(itemStack)
+  }
+
+  def isRewardTitanReplica(item: ItemStack): Boolean = {
+    val lores = item.getItemMeta.getLore.asScala
+    item.getItemMeta.getDisplayName == titanReplicaName && lores.contains(titanReplicaLore) && lores.contains(commonLore)
+  }
+
+  def isRewardGaeaReplica(item: ItemStack): Boolean = {
+    val lores = item.getItemMeta.getLore.asScala
+    item.getItemMeta.getDisplayName == gaeaReplicaName && lores.contains(gaeaReplicaLore) && lores.contains(commonLore)
+  }
+}

From d18f1f315ff1a32c76c59c58078f24ea8a2edf1a Mon Sep 17 00:00:00 2001
From: Lucky <lucky3028@users.noreply.github.com>
Date: Thu, 23 Sep 2021 21:19:33 +0900
Subject: [PATCH 64/73] =?UTF-8?q?[Remove]=20=E4=B8=8D=E8=A6=81=E3=81=AAimp?=
 =?UTF-8?q?ort?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../migrations/V1_4_0_AddEnchantsTo2_1billionRewardItems.scala  | 2 --
 1 file changed, 2 deletions(-)

diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/itemmigration/migrations/V1_4_0_AddEnchantsTo2_1billionRewardItems.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/itemmigration/migrations/V1_4_0_AddEnchantsTo2_1billionRewardItems.scala
index b8752e9d51..70bc52647d 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/itemmigration/migrations/V1_4_0_AddEnchantsTo2_1billionRewardItems.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/itemmigration/migrations/V1_4_0_AddEnchantsTo2_1billionRewardItems.scala
@@ -46,8 +46,6 @@ object V1_4_0_AddEnchantsTo2_1billionRewardItems {
   def migrationFunction(itemStack: ItemStack): ItemStack = {
     if (!is2_1billionRewardItems(itemStack)) return itemStack
 
-    import scala.util.chaining._
-
     itemStack.clone().addEnchantments(Map(Enchantment.MENDING -> 1, Enchantment.DURABILITY -> 5).asJava)
   }
 

From d2e57ded22059f8fe62f0b073943c558ba75eeda Mon Sep 17 00:00:00 2001
From: Lucky <lucky3028@users.noreply.github.com>
Date: Thu, 23 Sep 2021 21:23:22 +0900
Subject: [PATCH 65/73] =?UTF-8?q?[Fix]=20import=E3=81=AE=E4=BD=8D=E7=BD=AE?=
 =?UTF-8?q?=E3=82=92=E4=BF=AE=E6=AD=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../V1_4_0_AddEnchantsTo2_1billionRewardItems.scala           | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/itemmigration/migrations/V1_4_0_AddEnchantsTo2_1billionRewardItems.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/itemmigration/migrations/V1_4_0_AddEnchantsTo2_1billionRewardItems.scala
index 70bc52647d..61deb5410a 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/itemmigration/migrations/V1_4_0_AddEnchantsTo2_1billionRewardItems.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/itemmigration/migrations/V1_4_0_AddEnchantsTo2_1billionRewardItems.scala
@@ -36,13 +36,13 @@ object V1_4_0_AddEnchantsTo2_1billionRewardItems {
   ).map { case (color, str) => s"$color$BOLD$ITALIC$str" }.mkString
   private val gaeaReplicaLore = s"${WHITE}35億/1日を突破した記念に配布されたものです。"
 
+  import eu.timepit.refined.auto._
+
   def migration: ItemMigration = ItemMigration(
     ItemMigrationVersionNumber(1, 4, 0),
     MigrationHelper.delegateConversionForContainers(migrationFunction)
   )
 
-  import eu.timepit.refined.auto._
-
   def migrationFunction(itemStack: ItemStack): ItemStack = {
     if (!is2_1billionRewardItems(itemStack)) return itemStack
 

From 8540862191ac722849c39b4bcf3da54c1211048e Mon Sep 17 00:00:00 2001
From: Lucky <lucky3028@users.noreply.github.com>
Date: Thu, 23 Sep 2021 21:27:09 +0900
Subject: [PATCH 66/73] =?UTF-8?q?[Fix]=20=E3=82=B3=E3=83=B3=E3=83=91?=
 =?UTF-8?q?=E3=82=A4=E3=83=AB=E3=82=A8=E3=83=A9=E3=83=BC?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../V1_4_0_AddEnchantsTo2_1billionRewardItems.scala       | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/itemmigration/migrations/V1_4_0_AddEnchantsTo2_1billionRewardItems.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/itemmigration/migrations/V1_4_0_AddEnchantsTo2_1billionRewardItems.scala
index 61deb5410a..67b7e048b4 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/itemmigration/migrations/V1_4_0_AddEnchantsTo2_1billionRewardItems.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/itemmigration/migrations/V1_4_0_AddEnchantsTo2_1billionRewardItems.scala
@@ -46,7 +46,13 @@ object V1_4_0_AddEnchantsTo2_1billionRewardItems {
   def migrationFunction(itemStack: ItemStack): ItemStack = {
     if (!is2_1billionRewardItems(itemStack)) return itemStack
 
-    itemStack.clone().addEnchantments(Map(Enchantment.MENDING -> 1, Enchantment.DURABILITY -> 5).asJava)
+    import scala.util.chaining._
+
+    itemStack.clone().tap { item =>
+      import item._
+      addEnchantment(Enchantment.MENDING, 1)
+      addEnchantment(Enchantment.DURABILITY, 5)
+    }
   }
 
   def is2_1billionRewardItems(itemStack: ItemStack): Boolean = {

From 9474ff235d4ddd85817bd4670fa8fe704eaadbdb Mon Sep 17 00:00:00 2001
From: Lucky <lucky3028@users.noreply.github.com>
Date: Thu, 23 Sep 2021 22:32:16 +0900
Subject: [PATCH 67/73] =?UTF-8?q?[Fix]=20=E9=96=A2=E6=95=B0=E5=90=8D?=
 =?UTF-8?q?=E3=82=92=E3=82=8F=E3=81=8B=E3=82=8A=E3=82=84=E3=81=99=E3=81=8F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../V1_4_0_AddEnchantsTo2_1billionRewardItems.scala    | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/src/main/scala/com/github/unchama/seichiassist/subsystems/itemmigration/migrations/V1_4_0_AddEnchantsTo2_1billionRewardItems.scala b/src/main/scala/com/github/unchama/seichiassist/subsystems/itemmigration/migrations/V1_4_0_AddEnchantsTo2_1billionRewardItems.scala
index 67b7e048b4..ba1bbd33c6 100644
--- a/src/main/scala/com/github/unchama/seichiassist/subsystems/itemmigration/migrations/V1_4_0_AddEnchantsTo2_1billionRewardItems.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/subsystems/itemmigration/migrations/V1_4_0_AddEnchantsTo2_1billionRewardItems.scala
@@ -44,7 +44,7 @@ object V1_4_0_AddEnchantsTo2_1billionRewardItems {
   )
 
   def migrationFunction(itemStack: ItemStack): ItemStack = {
-    if (!is2_1billionRewardItems(itemStack)) return itemStack
+    if (!is21billionRewardItems(itemStack)) return itemStack
 
     import scala.util.chaining._
 
@@ -55,17 +55,17 @@ object V1_4_0_AddEnchantsTo2_1billionRewardItems {
     }
   }
 
-  def is2_1billionRewardItems(itemStack: ItemStack): Boolean = {
+  def is21billionRewardItems(itemStack: ItemStack): Boolean = {
     if (itemStack == null || !itemStack.hasItemMeta || !itemStack.getItemMeta.hasDisplayName || !itemStack.getItemMeta.hasLore) return false
-    isRewardGaeaReplica(itemStack) || isRewardTitanReplica(itemStack)
+    isRewardedGaeaReplica(itemStack) || isRewardedTitanReplica(itemStack)
   }
 
-  def isRewardTitanReplica(item: ItemStack): Boolean = {
+  def isRewardedTitanReplica(item: ItemStack): Boolean = {
     val lores = item.getItemMeta.getLore.asScala
     item.getItemMeta.getDisplayName == titanReplicaName && lores.contains(titanReplicaLore) && lores.contains(commonLore)
   }
 
-  def isRewardGaeaReplica(item: ItemStack): Boolean = {
+  def isRewardedGaeaReplica(item: ItemStack): Boolean = {
     val lores = item.getItemMeta.getLore.asScala
     item.getItemMeta.getDisplayName == gaeaReplicaName && lores.contains(gaeaReplicaLore) && lores.contains(commonLore)
   }

From 56c6a37b077e5b7f264bc36b70a42d37f50d6d51 Mon Sep 17 00:00:00 2001
From: soundofhorizon <ooshiba0206@gmail.com>
Date: Fri, 24 Sep 2021 11:04:30 +0900
Subject: [PATCH 68/73] =?UTF-8?q?[ADD]Season=20Achieve(10=E6=9C=88-12?=
 =?UTF-8?q?=E6=9C=88)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../achievement/NicknameMapping.scala         | 13 +++++-
 .../seichiassist/achievement/Nicknames.scala  | 13 +++++-
 .../achievement/SeichiAchievement.scala       | 42 +++++++++++++++++--
 .../group/AchievementGroupMenu.scala          |  2 +-
 4 files changed, 64 insertions(+), 6 deletions(-)

diff --git a/src/main/scala/com/github/unchama/seichiassist/achievement/NicknameMapping.scala b/src/main/scala/com/github/unchama/seichiassist/achievement/NicknameMapping.scala
index 2a409ac232..412becb5ab 100644
--- a/src/main/scala/com/github/unchama/seichiassist/achievement/NicknameMapping.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/achievement/NicknameMapping.scala
@@ -196,7 +196,18 @@ object NicknameMapping {
     case No_9033 => NicknameCombination(Some(9033), Some(9903), Some(9033))
     case No_9034 => NicknameCombination(Some(9034), None, Some(9034))
     case No_9035 => NicknameCombination(Some(9035), Some(9905), Some(9035))
-    case No_9036 => NicknameCombination(Some(9036), None, Some(9036))
+    case No_9036 => NicknameCombination(Some(9036), Some(9903), Some(9036))
+    case No_9037 => NicknameCombination(Some(9037), None, Some(9037))
+    case No_9038 => NicknameCombination(Some(9038), None, Some(9038))
+    case No_9039 => NicknameCombination(Some(9039), None, Some(9039))
+    case No_9040 => NicknameCombination(Some(9040), None, Some(9040))
+    case No_9041 => NicknameCombination(Some(9041), None, Some(9041))
+    case No_9042 => NicknameCombination(Some(9042), Some(9905), Some(9042))
+    case No_9043 => NicknameCombination(Some(9043), None, Some(9043))
+    case No_9044 => NicknameCombination(Some(9044), Some(9044), Some(9044))
+    case No_9045 => NicknameCombination(Some(9045), None, Some(9045))
+    case No_9046 => NicknameCombination(Some(9046), None, None)
+    case No_9047 => NicknameCombination(Some(9047), None, Some(9047))
 
     case No_8001 => NicknameCombination(Some(8001), Some(9905), Some(8001))
     case No_8002 => NicknameCombination(Some(8002), Some(9905), Some(8002))
diff --git a/src/main/scala/com/github/unchama/seichiassist/achievement/Nicknames.scala b/src/main/scala/com/github/unchama/seichiassist/achievement/Nicknames.scala
index 9f175aa053..00c10ecce8 100644
--- a/src/main/scala/com/github/unchama/seichiassist/achievement/Nicknames.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/achievement/Nicknames.scala
@@ -194,7 +194,18 @@ object Nicknames {
     9033 -> HeadTail(s"団子", s"団子"),
     9034 -> HeadTail(s"全額", s"投資"),
     9035 -> HeadTail(s"無尽蔵", s"気力"),
-    9036 -> HeadTail(s"猛毒", s"直撃"),
+    9036 -> HeadTail(s"兎", s"一緒に"),
+    9037 -> HeadTail(s"月", s"うさぎ"),
+    9038 -> HeadTail(s"美しい", s"装い"),
+    9039 -> HeadTail(s"猛毒", s"直撃"),
+    9040 -> HeadTail(s"衣", s"替え"),
+    9041 -> HeadTail(s"体力", s"勝負"),
+    9042 -> HeadTail(s"トパーズ", s"輝き"),
+    9043 -> HeadTail(s"千歳", s"飴"),
+    9044 -> FullSet(s"いい", s"肉の", s"日"),
+    9045 -> HeadTail(s"大", s"掃除"),
+    9046 -> HeadOnly(s"歳暮"),
+    9047 -> HeadTail(s"聖なる", s"夜"),
     // 前後パーツ(購入用)
     9801 -> HeadTail(s"お兄さん", s"お兄さん"),
     9802 -> HeadTail(s"戦隊", s"戦隊"),
diff --git a/src/main/scala/com/github/unchama/seichiassist/achievement/SeichiAchievement.scala b/src/main/scala/com/github/unchama/seichiassist/achievement/SeichiAchievement.scala
index 5da244a286..01d44b4b17 100644
--- a/src/main/scala/com/github/unchama/seichiassist/achievement/SeichiAchievement.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/achievement/SeichiAchievement.scala
@@ -1,19 +1,22 @@
 package com.github.unchama.seichiassist.achievement
 
-import java.time.{DayOfWeek, Month}
-
 import cats.effect.IO
 import com.github.unchama.seichiassist.achievement.NamedHoliday.SpringEquinoxDay
 import enumeratum.{Enum, EnumEntry}
 import org.bukkit.entity.Player
 
+import java.time.{DayOfWeek, Month}
+
 sealed abstract class SeichiAchievement extends EnumEntry {
   val id: Int
 }
 
 object SeichiAchievement extends Enum[SeichiAchievement] {
+
   sealed trait Unlockable
+
   sealed trait AutoUnlocked
+
   sealed trait ManuallyUnlocked
 
   sealed abstract class Normal[P] extends SeichiAchievement with Unlockable {
@@ -224,17 +227,50 @@ object SeichiAchievement extends Enum[SeichiAchievement] {
   case object No_9023 extends NormalManual(9023, playedOn(Month.JUNE, 3, DayOfWeek.SUNDAY, "父の日"))
   case object No_9024 extends NormalManual(9024, playedOn(Month.JUNE, 29, "とある生誕の日"))
   case object No_9025 extends NormalManual(9025, playedIn(Month.JULY))
+
   case object No_9026 extends NormalManual(9026, playedOn(Month.JULY, 7, "七夕"))
+
   case object No_9027 extends NormalManual(9027, playedOn(Month.JULY, 17, "とある東京の日"))
+
   case object No_9028 extends NormalManual(9028, playedOn(Month.JULY, 29, "とある肉の日"))
+
   case object No_9029 extends NormalManual(9029, playedIn(Month.AUGUST))
+
   case object No_9030 extends NormalManual(9030, playedOn(Month.AUGUST, 7, "とあるバナナの日"))
+
   case object No_9031 extends NormalManual(9031, playedOn(Month.AUGUST, 21, "とあるJDの日"))
+
   case object No_9032 extends NormalManual(9032, playedOn(Month.AUGUST, 29, "とある焼肉の日"))
+
   case object No_9033 extends NormalManual(9033, playedIn(Month.SEPTEMBER))
+
   case object No_9034 extends NormalManual(9034, playedOn(Month.SEPTEMBER, 2, "とあるくじの日"))
+
   case object No_9035 extends NormalManual(9035, playedOn(Month.SEPTEMBER, 12, "とあるマラソンの日"))
-  case object No_9036 extends NormalManual(9036, playedOn(Month.SEPTEMBER, 29, "とあるふぐの日"))
+
+  case object No_9036 extends NormalManual(9036, playedOn(Month.SEPTEMBER, 15, "とある月見の日"))
+
+  case object No_9037 extends NormalManual(9037, playedOn(Month.SEPTEMBER, 21, "とある中秋の日"))
+
+  case object No_9038 extends NormalManual(9038, playedOn(Month.SEPTEMBER, 21, "とあるファッションショーの日"))
+
+  case object No_9039 extends NormalManual(9039, playedOn(Month.SEPTEMBER, 29, "とあるふぐの日"))
+
+  case object No_9040 extends NormalManual(9040, playedIn(Month.OCTOBER))
+
+  case object No_9041 extends NormalManual(9041, playedOn(Month.OCTOBER, 10, "とあるスポーツの日"))
+
+  case object No_9042 extends NormalManual(9042, playedIn(Month.NOVEMBER))
+
+  case object No_9043 extends NormalManual(9043, playedOn(Month.NOVEMBER, 15, "とある七五三の日"))
+
+  case object No_9044 extends NormalManual(9044, playedOn(Month.NOVEMBER, 29, "とある特上の肉の日"))
+
+  case object No_9045 extends NormalManual(9045, playedIn(Month.DECEMBER))
+
+  case object No_9046 extends NormalManual(9046, playedOn(Month.DECEMBER, 1, "とある年の暮れの日"))
+
+  case object No_9047 extends NormalManual(9047, playedOn(Month.DECEMBER, 25, "とあるクリスマスの日"))
 
   val values: IndexedSeq[SeichiAchievement] = findValues
 
diff --git a/src/main/scala/com/github/unchama/seichiassist/menus/achievement/group/AchievementGroupMenu.scala b/src/main/scala/com/github/unchama/seichiassist/menus/achievement/group/AchievementGroupMenu.scala
index 896b8519bb..bd75dea838 100644
--- a/src/main/scala/com/github/unchama/seichiassist/menus/achievement/group/AchievementGroupMenu.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/menus/achievement/group/AchievementGroupMenu.scala
@@ -53,7 +53,7 @@ object AchievementGroupMenu {
       AchievementEntry.within(8001 to 8003)
 
     case Anniversaries =>
-      AchievementEntry.within(9001 to 9036)
+      AchievementEntry.within(9001 to 9047)
 
     case MebiusBreeder =>
       AchievementEntry.within(0 until 0)

From d85c89805d6a467f8d17fffee9ca3cb2c8f29a1d Mon Sep 17 00:00:00 2001
From: soundofhorizon <ooshiba0206@gmail.com>
Date: Fri, 24 Sep 2021 11:06:17 +0900
Subject: [PATCH 69/73] =?UTF-8?q?[REMOVE]=E4=B8=8D=E8=A6=81=E3=81=AA?=
 =?UTF-8?q?=E6=94=B9=E8=A1=8C=E3=82=92=E5=89=8A=E9=99=A4?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../achievement/SeichiAchievement.scala       | 22 -------------------
 1 file changed, 22 deletions(-)

diff --git a/src/main/scala/com/github/unchama/seichiassist/achievement/SeichiAchievement.scala b/src/main/scala/com/github/unchama/seichiassist/achievement/SeichiAchievement.scala
index 01d44b4b17..0b078344f4 100644
--- a/src/main/scala/com/github/unchama/seichiassist/achievement/SeichiAchievement.scala
+++ b/src/main/scala/com/github/unchama/seichiassist/achievement/SeichiAchievement.scala
@@ -227,49 +227,27 @@ object SeichiAchievement extends Enum[SeichiAchievement] {
   case object No_9023 extends NormalManual(9023, playedOn(Month.JUNE, 3, DayOfWeek.SUNDAY, "父の日"))
   case object No_9024 extends NormalManual(9024, playedOn(Month.JUNE, 29, "とある生誕の日"))
   case object No_9025 extends NormalManual(9025, playedIn(Month.JULY))
-
   case object No_9026 extends NormalManual(9026, playedOn(Month.JULY, 7, "七夕"))
-
   case object No_9027 extends NormalManual(9027, playedOn(Month.JULY, 17, "とある東京の日"))
-
   case object No_9028 extends NormalManual(9028, playedOn(Month.JULY, 29, "とある肉の日"))
-
   case object No_9029 extends NormalManual(9029, playedIn(Month.AUGUST))
-
   case object No_9030 extends NormalManual(9030, playedOn(Month.AUGUST, 7, "とあるバナナの日"))
-
   case object No_9031 extends NormalManual(9031, playedOn(Month.AUGUST, 21, "とあるJDの日"))
-
   case object No_9032 extends NormalManual(9032, playedOn(Month.AUGUST, 29, "とある焼肉の日"))
-
   case object No_9033 extends NormalManual(9033, playedIn(Month.SEPTEMBER))
-
   case object No_9034 extends NormalManual(9034, playedOn(Month.SEPTEMBER, 2, "とあるくじの日"))
-
   case object No_9035 extends NormalManual(9035, playedOn(Month.SEPTEMBER, 12, "とあるマラソンの日"))
-
   case object No_9036 extends NormalManual(9036, playedOn(Month.SEPTEMBER, 15, "とある月見の日"))
-
   case object No_9037 extends NormalManual(9037, playedOn(Month.SEPTEMBER, 21, "とある中秋の日"))
-
   case object No_9038 extends NormalManual(9038, playedOn(Month.SEPTEMBER, 21, "とあるファッションショーの日"))
-
   case object No_9039 extends NormalManual(9039, playedOn(Month.SEPTEMBER, 29, "とあるふぐの日"))
-
   case object No_9040 extends NormalManual(9040, playedIn(Month.OCTOBER))
-
   case object No_9041 extends NormalManual(9041, playedOn(Month.OCTOBER, 10, "とあるスポーツの日"))
-
   case object No_9042 extends NormalManual(9042, playedIn(Month.NOVEMBER))
-
   case object No_9043 extends NormalManual(9043, playedOn(Month.NOVEMBER, 15, "とある七五三の日"))
-
   case object No_9044 extends NormalManual(9044, playedOn(Month.NOVEMBER, 29, "とある特上の肉の日"))
-
   case object No_9045 extends NormalManual(9045, playedIn(Month.DECEMBER))
-
   case object No_9046 extends NormalManual(9046, playedOn(Month.DECEMBER, 1, "とある年の暮れの日"))
-
   case object No_9047 extends NormalManual(9047, playedOn(Month.DECEMBER, 25, "とあるクリスマスの日"))
 
   val values: IndexedSeq[SeichiAchievement] = findValues

From d0c98564f818ad6a826478d9dbcb04f2c80d651c Mon Sep 17 00:00:00 2001
From: Kory | Ryosuke Kondo <6561358+kory33@users.noreply.github.com>
Date: Wed, 29 Sep 2021 01:28:52 +0100
Subject: [PATCH 70/73] =?UTF-8?q?[chore]=20=E3=82=AD=E3=83=A3=E3=83=83?=
 =?UTF-8?q?=E3=82=B7=E3=83=A5=E4=BB=98=E3=81=8D=E3=83=93=E3=83=AB=E3=83=89?=
 =?UTF-8?q?=E3=81=AE=E3=82=AD=E3=83=A3=E3=83=83=E3=82=B7=E3=83=A5=E3=82=92?=
 =?UTF-8?q?=E6=B6=88=E3=81=97=E9=A3=9B=E3=81=B0=E3=81=99?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .github/workflows/build_and_deploy.yml | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/.github/workflows/build_and_deploy.yml b/.github/workflows/build_and_deploy.yml
index a58457df6b..df16b8e9c6 100644
--- a/.github/workflows/build_and_deploy.yml
+++ b/.github/workflows/build_and_deploy.yml
@@ -23,7 +23,7 @@ jobs:
         uses: actions/cache@v2
         env:
           cache-name: cache-build-dependencies
-          cache-version: v-2
+          cache-version: v-3
         with:
           # sbt等は$HOMEではなくユーザーディレクトリを見ているようで、
           # GH Actionsでの ~ は /github/home/ に展開されるにもかかわらず
@@ -47,7 +47,7 @@ jobs:
         uses: actions/cache@v2
         env:
           cache-name: cache-build
-          cache-version: v-2
+          cache-version: v-3
         with:
           path: |
             target

From 88a9830b76a6adf66e5293cad60d874ecc549688 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com>
Date: Wed, 29 Sep 2021 09:13:29 +0000
Subject: [PATCH 71/73] [bump] 13 -> 14

---
 build.sbt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/build.sbt b/build.sbt
index 6a370c0850..81563f22b5 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 := "13"
+ThisBuild / version := "14"
 ThisBuild / organization := "click.seichi"
 ThisBuild / description := "ギガンティック☆整地鯖の独自要素を司るプラグイン"
 

From 7b4a3ce5d8d81f0c22e824461a740f37b5a1e6b8 Mon Sep 17 00:00:00 2001
From: Kory <6561358+kory33@users.noreply.github.com>
Date: Wed, 29 Sep 2021 19:25:11 +0900
Subject: [PATCH 72/73] [fix] pass nmsWorldServer to World.getChunkAtCoordinate
 method

---
 .../unchama/util/nms/v1_12_2/world/WorldChunkSaving.scala       | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/main/scala/com/github/unchama/util/nms/v1_12_2/world/WorldChunkSaving.scala b/src/main/scala/com/github/unchama/util/nms/v1_12_2/world/WorldChunkSaving.scala
index 898648c509..4e4625a1ef 100644
--- a/src/main/scala/com/github/unchama/util/nms/v1_12_2/world/WorldChunkSaving.scala
+++ b/src/main/scala/com/github/unchama/util/nms/v1_12_2/world/WorldChunkSaving.scala
@@ -159,7 +159,7 @@ object WorldChunkSaving {
       val entityChunkZ = Entity.chunkZ(entity)
 
       if (Entity.loadedToAChunk(entity) && World.isChunkLoaded(nmsWorldServer)(entityChunkX, entityChunkZ)) {
-        val chunk = World.getChunkAtCoordinate(world)(entityChunkX, entityChunkZ)
+        val chunk = World.getChunkAtCoordinate(nmsWorldServer)(entityChunkX, entityChunkZ)
 
         Chunk.untrackEntity(chunk)(entity)
       }

From 962463ed0cf7eb8daa0f4b651de3ef82f27aebee Mon Sep 17 00:00:00 2001
From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com>
Date: Wed, 29 Sep 2021 10:27:42 +0000
Subject: [PATCH 73/73] [bump] 14 -> 15

---
 build.sbt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/build.sbt b/build.sbt
index 81563f22b5..77e8ac7498 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 := "14"
+ThisBuild / version := "15"
 ThisBuild / organization := "click.seichi"
 ThisBuild / description := "ギガンティック☆整地鯖の独自要素を司るプラグイン"