diff --git a/build.gradle.kts b/build.gradle.kts index dee91f9..90bc762 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,4 +1,3 @@ -import io.papermc.paperweight.userdev.ReobfArtifactConfiguration import org.apache.tools.ant.filters.ReplaceTokens plugins { @@ -11,7 +10,7 @@ plugins { } group = "net.azisaba" -version = "1.20.2+6.16.9" +version = "1.20.2+6.17.0" java { toolchain.languageVersion.set(JavaLanguageVersion.of(17)) diff --git a/src/main/java/com/github/mori01231/lifecore/LifeCore.kt b/src/main/java/com/github/mori01231/lifecore/LifeCore.kt index b6f66b8..d0da149 100644 --- a/src/main/java/com/github/mori01231/lifecore/LifeCore.kt +++ b/src/main/java/com/github/mori01231/lifecore/LifeCore.kt @@ -205,6 +205,14 @@ class LifeCore : JavaPlugin() { } }, 200, 200) + Bukkit.getScheduler().runTaskTimer(this, Runnable { + customBlockManager.getLoadedStates().forEach { (location, state) -> + state.getBlock().tick(customBlockManager, location, state)?.let { + customBlockManager.setState(location.toBukkitLocation(), it) + } + } + }, 1, 1) + logger.info("LifeCore has been enabled.") } @@ -235,6 +243,7 @@ class LifeCore : JavaPlugin() { runCatching { executorService.shutdownNow() } runCatching { httpServer?.stop(1) } runCatching { gcListener.unregister() } + runCatching { customBlockManager.saveAll() } runCatching { // unregister all channel handlers @@ -256,8 +265,10 @@ class LifeCore : JavaPlugin() { preloadClass("com.github.mori01231.lifecore.LifeCore\$onDisable\$$i", false) } preloadClass("com.github.mori01231.lifecore.lib.com.charleskorn.kaml.Yaml\$encodeToString\$writer\$1") + preloadClass("com.github.mori01231.lifecore.lib.com.charleskorn.kaml.YamlOutput") preloadClass("com.github.mori01231.lifecore.lib.org.yaml.snakeyaml.Yaml") preloadClass("com.github.mori01231.lifecore.lib.org.yaml.snakeyaml.nodes.CollectionNode") + preloadClass("com.github.mori01231.lifecore.lib.org.yaml.snakeyaml.nodes.SequenceNode") } private fun preloadClass(name: String, required: Boolean = true) { diff --git a/src/main/java/com/github/mori01231/lifecore/block/BeaconCustomBlock.kt b/src/main/java/com/github/mori01231/lifecore/block/BeaconCustomBlock.kt new file mode 100644 index 0000000..0306a96 --- /dev/null +++ b/src/main/java/com/github/mori01231/lifecore/block/BeaconCustomBlock.kt @@ -0,0 +1,34 @@ +package com.github.mori01231.lifecore.block + +import com.github.mori01231.lifecore.region.WorldLocation +import org.bukkit.Material +import org.bukkit.entity.Player +import org.bukkit.event.player.PlayerInteractEvent +import org.bukkit.inventory.EquipmentSlot +import org.bukkit.potion.PotionEffectType + +class BeaconCustomBlock( + override val name: String, + override val lockFacing: Boolean, + override val axisShift: Int, + override val backgroundBlock: Material, + material: Material, + displayName: String? = null, + lore: List? = null, + private val effect: PotionEffectType, + private val amplifier: Int, + private val destroyWithoutWrench: Boolean, +) : CustomBlock(material, displayName, lore) { + var ticks = 0 + + override fun tick(manager: CustomBlockManager, pos: WorldLocation, state: CustomBlockState): CustomBlockState? { + if (ticks++ % 80 == 0) { + pos.toBukkitLocation().getNearbyEntitiesByType(Player::class.java, 250.0).forEach { player -> + player.addPotionEffect(effect.createEffect(20 * 60, amplifier)) + } + } + return super.tick(manager, pos, state) + } + + override fun canDestroy(state: CustomBlockState, wrench: Boolean): Boolean = wrench || destroyWithoutWrench +} diff --git a/src/main/java/com/github/mori01231/lifecore/block/CustomBlock.kt b/src/main/java/com/github/mori01231/lifecore/block/CustomBlock.kt index 897823b..149a010 100644 --- a/src/main/java/com/github/mori01231/lifecore/block/CustomBlock.kt +++ b/src/main/java/com/github/mori01231/lifecore/block/CustomBlock.kt @@ -1,17 +1,17 @@ package com.github.mori01231.lifecore.block +import com.github.mori01231.lifecore.region.WorldLocation import com.github.mori01231.lifecore.util.AxisX import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonElement -import net.kyori.adventure.text.Component -import net.minecraft.nbt.CompoundTag import org.bukkit.Material -import org.bukkit.craftbukkit.v1_20_R2.inventory.CraftItemStack +import org.bukkit.craftbukkit.v1_15_R1.inventory.CraftItemStack import org.bukkit.event.Listener import org.bukkit.event.block.BlockPlaceEvent import org.bukkit.event.player.PlayerInteractEvent import org.bukkit.inventory.ItemStack +import kotlin.text.contains abstract class CustomBlock( val material: Material, @@ -22,7 +22,7 @@ abstract class CustomBlock( open val lockFacing: Boolean = false open val axisShift: Int = 0 open val backgroundBlock: Material = Material.BARRIER - var customModelData = 0 + var customModelData: Int? = null internal fun handleInteract(e: PlayerInteractEvent, state: CustomBlockState) { if (state.blockName != name) error("Block name mismatch: state ${state.blockName} != block $name") @@ -45,8 +45,8 @@ abstract class CustomBlock( open fun onPlace(e: BlockPlaceEvent): CustomBlockState { val nms = CraftItemStack.asNMSCopy(e.itemInHand) - val tagString = if (nms.hasTag() && nms.tag!!.contains("BlockState")) { - nms.tag!!.getCompound("BlockState").getString("tag") + val tagString = if (nms.hasTag() && nms.tag!!.contains("CustomBlockState")) { + nms.tag!!.getCompound("CustomBlockState").getString("tag") } else { "" } @@ -70,7 +70,7 @@ abstract class CustomBlock( lore = this@CustomBlock.lore } val nms = CraftItemStack.asNMSCopy(item) - nms.orCreateTag.put("BlockState", CompoundTag().apply { + nms.orCreateTag.put("CustomBlockState", CompoundTag().apply { putString("blockName", this@CustomBlock.name) if (state != null) { putString("tag", Json.encodeToString(state.tag)) @@ -78,4 +78,6 @@ abstract class CustomBlock( }) return CraftItemStack.asCraftMirror(nms) } + + open fun tick(manager: CustomBlockManager, pos: WorldLocation, state: CustomBlockState): CustomBlockState? = null } diff --git a/src/main/java/com/github/mori01231/lifecore/block/CustomBlockManager.kt b/src/main/java/com/github/mori01231/lifecore/block/CustomBlockManager.kt index 4f55bf4..74081e9 100644 --- a/src/main/java/com/github/mori01231/lifecore/block/CustomBlockManager.kt +++ b/src/main/java/com/github/mori01231/lifecore/block/CustomBlockManager.kt @@ -2,6 +2,7 @@ package com.github.mori01231.lifecore.block import com.github.mori01231.lifecore.LifeCore import com.github.mori01231.lifecore.listener.CustomBlockListener +import com.github.mori01231.lifecore.region.WorldLocation import com.github.mori01231.lifecore.util.AxisX import com.github.mori01231.lifecore.util.LRUCache import kotlinx.serialization.json.Json @@ -17,6 +18,7 @@ import org.bukkit.entity.Entity import org.bukkit.event.HandlerList import org.bukkit.inventory.ItemStack import org.bukkit.inventory.ShapedRecipe +import org.bukkit.potion.PotionEffectType import java.io.File import java.util.* @@ -50,6 +52,15 @@ class CustomBlockManager(val plugin: LifeCore) { private fun getRegionPos(worldPos: Int) = worldPos shr 9 + fun getLoadedStates(): Map = + region.flatMap { (world, wld) -> + wld.flatMap { (_, region) -> + region.getAllStates().map { (pos, state) -> + WorldLocation(world, pos.first, pos.second, pos.third) to state + } + } + }.toMap() + fun getState(location: Location): CustomBlockState? { val region = loadRegion(location.world, getRegionPos(location.blockX), getRegionPos(location.blockZ)) return region.getState(location.blockX, location.blockY, location.blockZ) @@ -66,24 +77,23 @@ class CustomBlockManager(val plugin: LifeCore) { region.setState(location.blockX, location.blockY, location.blockZ, state) } - private fun loadRegion(world: World, x: Int, z: Int): CustomBlockRegion { - val wld = region.getOrPut(world.name) { LRUCache(100) } - val loaded = wld.getOrPut(x to z) { - val file = File(regionDir, "${world.name}/$x.$z.json") - if (file.exists()) { - plugin.logger.info("Loading region $x, $z") - Json.decodeFromString(CustomBlockRegion.serializer(), file.readText()) - } else { - plugin.logger.info("Creating region $x, $z") - CustomBlockRegion(world.name, x, z) + private fun loadRegion(world: World, x: Int, z: Int): CustomBlockRegion = + region.getOrPut(world.name) { LRUCache(100) } + .getOrPut(x to z) { + val file = File(regionDir, "${world.name}/$x.$z.json") + val loaded = if (file.exists()) { + plugin.logger.info("Loading region $x, $z") + Json.decodeFromString(CustomBlockRegion.serializer(), file.readText()) + } else { + plugin.logger.info("Creating region $x, $z") + CustomBlockRegion(world.name, x, z) + } + if (loaded.dirty) { + plugin.logger.warning("Region $x, $z was not saved properly") + loaded.save() + } + loaded } - } - if (loaded.dirty) { - plugin.logger.warning("Region $x, $z was not saved properly") - loaded.save() - } - return loaded - } fun getWrenchItem() = ItemStack(Material.STICK).apply { itemMeta = itemMeta?.apply { @@ -117,18 +127,28 @@ class CustomBlockManager(val plugin: LifeCore) { // add blocks plugin.config.getMapList("custom-blocks").forEach { map -> + val type = map["type"]?.toString() ?: "command" val blockName = map["name"]?.toString() ?: return plugin.logger.warning("name is required") val axisShift = map["axisShift"]?.toString()?.toIntOrNull() ?: 0 - val lockFacing = map["lockFacing"]?.toString()?.toBoolean() ?: false + val lockFacing = map["lockFacing"]?.toString()?.toBoolean() == true val backgroundBlock = Material.valueOf(map["backgroundBlock"]?.toString()?.uppercase() ?: "BARRIER") - val material = Material.valueOf(map["material"]?.toString()?.uppercase() ?: return plugin.logger.warning("material is required")) + val material = Material.valueOf(map["material"]?.toString()?.uppercase() ?: return plugin.logger.warning("material is required @ $blockName")) val displayName = map["displayName"]?.toString()?.let { ChatColor.translateAlternateColorCodes('&', it) } val lore = map["lore"]?.toString()?.let { ChatColor.translateAlternateColorCodes('&', it) }?.split("\n") - val customModelData = map["customModelData"]?.toString()?.toIntOrNull() ?: 0 - val commands = map["commands"] as List? - val consoleCommands = map["consoleCommands"] as List? - val destroyWithoutWrench = map["destroyWithoutWrench"]?.toString()?.toBoolean() ?: false - val block = CommandCustomBlock(blockName, lockFacing, axisShift, backgroundBlock, material, displayName, lore, commands ?: emptyList(), consoleCommands ?: emptyList(), destroyWithoutWrench) + val destroyWithoutWrench = map["destroyWithoutWrench"]?.toString()?.toBoolean() == true + val customModelData = map["customModelData"]?.toString()?.toIntOrNull() + val block = if (type == "command") { + val commands = map["commands"] as List? + val consoleCommands = map["consoleCommands"] as List? + CommandCustomBlock(blockName, lockFacing, axisShift, backgroundBlock, material, displayName, lore, commands ?: emptyList(), consoleCommands ?: emptyList(), destroyWithoutWrench) + } else if (type == "beacon") { + val effect = PotionEffectType.getByName(map["effect"]?.toString() ?: return plugin.logger.warning("effect is required @ $blockName")) + ?: return plugin.logger.warning("Unknown effect: ${map["effect"]} @ $blockName") + val amplifier = map["amplifier"]?.toString()?.toIntOrNull() ?: 0 + BeaconCustomBlock(blockName, lockFacing, axisShift, backgroundBlock, material, displayName, lore, effect, amplifier, destroyWithoutWrench) + } else { + error("Unknown block type: $type") + } block.customModelData = customModelData registerCustomBlock(block) } diff --git a/src/main/java/com/github/mori01231/lifecore/block/CustomBlockRegion.kt b/src/main/java/com/github/mori01231/lifecore/block/CustomBlockRegion.kt index 35b5b90..2d18349 100644 --- a/src/main/java/com/github/mori01231/lifecore/block/CustomBlockRegion.kt +++ b/src/main/java/com/github/mori01231/lifecore/block/CustomBlockRegion.kt @@ -19,6 +19,17 @@ data class CustomBlockRegion(val world: String, val x: Int, val z: Int) { fun unpackZ(packed: Long) = (packed and 0x1FFFFF).toInt() + fun getAllStates(): Map, CustomBlockState> { + return states.mapKeys { + val regionX = x * 512 + val regionZ = z * 512 + val x = unpackX(it.key) + regionX + val y = unpackY(it.key) + val z = unpackZ(it.key) + regionZ + Triple(x, y, z) + } + } + fun getState(x: Int, y: Int, z: Int): CustomBlockState? { val packed = packXYZ(x and 511, y, z and 511) return states[packed] diff --git a/src/main/java/com/github/mori01231/lifecore/command/GCListenerRestartExtendTimeCommand.java b/src/main/java/com/github/mori01231/lifecore/command/GCListenerRestartExtendTimeCommand.java index e862514..39875b1 100644 --- a/src/main/java/com/github/mori01231/lifecore/command/GCListenerRestartExtendTimeCommand.java +++ b/src/main/java/com/github/mori01231/lifecore/command/GCListenerRestartExtendTimeCommand.java @@ -33,7 +33,7 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command players.add(player.getUniqueId()); sender.sendMessage(ChatColor.GREEN + "投票しました。"); if (sender.hasPermission("lifecore.extend-time-immediately") || players.size() == 5) { - ScheduleRestartCommand.schedule(30); + ScheduleRestartCommand.schedule(30 * 60); } return true; } diff --git a/src/main/java/com/github/mori01231/lifecore/listener/CustomBlockListener.kt b/src/main/java/com/github/mori01231/lifecore/listener/CustomBlockListener.kt index 4b3a809..b504722 100644 --- a/src/main/java/com/github/mori01231/lifecore/listener/CustomBlockListener.kt +++ b/src/main/java/com/github/mori01231/lifecore/listener/CustomBlockListener.kt @@ -1,12 +1,11 @@ package com.github.mori01231.lifecore.listener import com.github.mori01231.lifecore.LifeCore -import net.kyori.adventure.text.Component import org.bukkit.GameMode import org.bukkit.Location import org.bukkit.Material import org.bukkit.Sound -import org.bukkit.craftbukkit.v1_20_R2.inventory.CraftItemStack +import org.bukkit.craftbukkit.v1_15_R1.inventory.CraftItemStack import org.bukkit.entity.Arrow import org.bukkit.entity.Player import org.bukkit.event.EventHandler @@ -14,13 +13,18 @@ import org.bukkit.event.EventPriority import org.bukkit.event.Listener import org.bukkit.event.block.Action import org.bukkit.event.block.BlockBreakEvent +import org.bukkit.event.block.BlockPistonExtendEvent +import org.bukkit.event.block.BlockPistonRetractEvent import org.bukkit.event.block.BlockPlaceEvent import org.bukkit.event.entity.ProjectileHitEvent import org.bukkit.event.player.PlayerInteractAtEntityEvent import org.bukkit.event.player.PlayerInteractEntityEvent import org.bukkit.event.player.PlayerInteractEvent +import kotlin.text.contains class CustomBlockListener(val plugin: LifeCore) : Listener { + private var callingEvent = false + @EventHandler fun onPlayerInteract(e: PlayerInteractEvent) { if (e.clickedBlock == null) return @@ -44,10 +48,10 @@ class CustomBlockListener(val plugin: LifeCore) : Listener { if (!nms.hasTag()) { return } - if (!nms.tag!!.contains("BlockState")) { + if (!nms.tag!!.contains("CustomBlockState")) { return } - val blockState = nms.tag!!.getCompound("BlockState") + val blockState = nms.tag!!.getCompound("CustomBlockState") val blockName = blockState.getString("blockName") val block = plugin.customBlockManager.findBlockByName(blockName) ?: return if (!e.player.hasPermission("lifecore.customblock.place.$blockName")) { @@ -61,6 +65,7 @@ class CustomBlockListener(val plugin: LifeCore) : Listener { @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) fun onBlockBreak(e: BlockBreakEvent) { + if (callingEvent) return val state = plugin.customBlockManager.getState(e.block.location) ?: return e.isCancelled = true if (!e.player.hasPermission("lifecore.customblock.destroy.${state.blockName}")) { @@ -113,6 +118,22 @@ class CustomBlockListener(val plugin: LifeCore) : Listener { } } + @EventHandler + fun onPistonExtend(e: BlockPistonExtendEvent) { + e.blocks.forEach { block -> + plugin.customBlockManager.getState(block.location) ?: return + e.isCancelled = true + } + } + + @EventHandler + fun onPistonRetract(e: BlockPistonRetractEvent) { + e.blocks.forEach { block -> + plugin.customBlockManager.getState(block.location) ?: return + e.isCancelled = true + } + } + private fun destroyAt(player: Player, location: Location, callEvent: Boolean = true) { val state = plugin.customBlockManager.getState(location) ?: return if (callEvent) { @@ -121,10 +142,12 @@ class CustomBlockListener(val plugin: LifeCore) : Listener { location.block.type = Material.BARRIER } try { + callingEvent = true if (!BlockBreakEvent(location.block, player).callEvent()) { return } } finally { + callingEvent = false if (wasAir) { location.block.type = Material.AIR } diff --git a/src/main/java/com/github/mori01231/lifecore/util/GCListener.java b/src/main/java/com/github/mori01231/lifecore/util/GCListener.java index dc7f620..8978292 100644 --- a/src/main/java/com/github/mori01231/lifecore/util/GCListener.java +++ b/src/main/java/com/github/mori01231/lifecore/util/GCListener.java @@ -96,8 +96,8 @@ public synchronized void triggerNow() { } } else if (command.startsWith("@schedulerestart ")) { try { - int delayMinutes = Integer.parseInt(command.substring("@schedulerestart ".length())); - ScheduleRestartCommand.schedule(delayMinutes); + int delaySeconds = Integer.parseInt(command.substring("@schedulerestart ".length())); + ScheduleRestartCommand.schedule(delaySeconds); TextComponent component = new TextComponent("再起動までの時間を延長するには、このメッセージをクリックしてください。"); component.setColor(ChatColor.AQUA); component.setUnderlined(true);