summaryrefslogtreewikicommitdiff
path: root/bukkit/src
diff options
context:
space:
mode:
authorFreya Murphy <freya@freyacat.org>2026-03-26 23:15:33 -0400
committerFreya Murphy <freya@freyacat.org>2026-03-27 23:09:23 -0400
commitf8322cd21cde68a72b05efbad3a05b8e67c0bdd0 (patch)
treed7e60bc8fedadc8fa7ae725571cad1f398eaf6dc /bukkit/src
downloadkenshinshideandseek2-f8322cd21cde68a72b05efbad3a05b8e67c0bdd0.tar.gz
kenshinshideandseek2-f8322cd21cde68a72b05efbad3a05b8e67c0bdd0.tar.bz2
kenshinshideandseek2-f8322cd21cde68a72b05efbad3a05b8e67c0bdd0.zip
initial
Diffstat (limited to 'bukkit/src')
-rw-r--r--bukkit/src/Board.kt126
-rw-r--r--bukkit/src/Inventory.kt59
-rw-r--r--bukkit/src/Item.kt97
-rw-r--r--bukkit/src/Player.kt232
-rw-r--r--bukkit/src/Plugin.kt87
-rw-r--r--bukkit/src/Shim.kt180
-rw-r--r--bukkit/src/World.kt157
-rw-r--r--bukkit/src/disguise/Disguise.kt244
-rw-r--r--bukkit/src/disguise/Disguiser.kt43
-rw-r--r--bukkit/src/disguise/EntityHider.kt58
-rw-r--r--bukkit/src/event/BreakListener.kt56
-rw-r--r--bukkit/src/event/ChatListener.kt29
-rw-r--r--bukkit/src/event/CommandListener.kt29
-rw-r--r--bukkit/src/event/DamageListener.kt56
-rw-r--r--bukkit/src/event/InteractListener.kt50
-rw-r--r--bukkit/src/event/InventoryListener.kt61
-rw-r--r--bukkit/src/event/JoinLeaveListener.kt52
-rw-r--r--bukkit/src/event/MovementListener.kt71
-rw-r--r--bukkit/src/event/PacketListener.kt110
-rw-r--r--bukkit/src/event/PlayerListener.kt59
-rw-r--r--bukkit/src/event/RespawnListener.kt41
-rw-r--r--bukkit/src/packet/BlockChangePacket.kt20
-rw-r--r--bukkit/src/packet/EntityDestroyPacket.kt13
-rw-r--r--bukkit/src/packet/EntityMetadataPacket.kt17
-rw-r--r--bukkit/src/packet/EntityTeleportPacket.kt19
25 files changed, 1966 insertions, 0 deletions
diff --git a/bukkit/src/Board.kt b/bukkit/src/Board.kt
new file mode 100644
index 0000000..041d39f
--- /dev/null
+++ b/bukkit/src/Board.kt
@@ -0,0 +1,126 @@
+@file:Suppress("DEPRECATION")
+
+package cat.freya.khs.bukkit
+
+import cat.freya.khs.game.Board as KhsBoard
+import java.util.UUID
+import org.bukkit.scoreboard.DisplaySlot
+import org.bukkit.scoreboard.NameTagVisibility
+import org.bukkit.scoreboard.Objective
+import org.bukkit.scoreboard.Scoreboard as BukkitBoard
+import org.bukkit.scoreboard.Team as BukkitTeam
+
+class BukkitKhsTeam(val shim: BukkitKhsShim, val inner: BukkitTeam) : KhsBoard.Team {
+ override var prefix: String
+ get() = inner.prefix
+ set(prefix: String) {
+ inner.prefix = formatText(prefix)
+ }
+
+ enum class NameTagsVisible {
+ FOR_OWN_TEAM,
+ FOR_OTHER_TEAMS,
+ NEVER,
+ }
+
+ // options
+ override var canCollide: Boolean
+ get() {
+ if (!shim.supports(9)) return false
+ return inner.getOption(BukkitTeam.Option.COLLISION_RULE) ==
+ BukkitTeam.OptionStatus.NEVER
+ }
+ set(b: Boolean) {
+ if (shim.supports(9)) {
+ val v = if (b) BukkitTeam.OptionStatus.ALWAYS else BukkitTeam.OptionStatus.NEVER
+ inner.setOption(BukkitTeam.Option.COLLISION_RULE, v)
+ }
+ }
+
+ override var nameTagsVisible: Boolean
+ get() {
+ if (shim.supports(9)) {
+ return inner.getOption(BukkitTeam.Option.NAME_TAG_VISIBILITY) !=
+ BukkitTeam.OptionStatus.NEVER
+ } else {
+ return inner.nameTagVisibility != NameTagVisibility.NEVER
+ }
+ }
+ set(b: Boolean) {
+ if (shim.supports(9)) {
+ val v =
+ if (b) BukkitTeam.OptionStatus.FOR_OWN_TEAM else BukkitTeam.OptionStatus.NEVER
+ inner.setOption(BukkitTeam.Option.NAME_TAG_VISIBILITY, v)
+ } else {
+ val v = if (b) NameTagVisibility.HIDE_FOR_OTHER_TEAMS else NameTagVisibility.NEVER
+ inner.nameTagVisibility = v
+ }
+ }
+
+ // players
+ override var players: Set<UUID>
+ get() = inner.entries.map { shim.getPlayer(it)?.uuid }.filterNotNull().toSet()
+ set(new: Set<UUID>) {
+ for (entry in inner.entries) {
+ val player = shim.plugin.server.getPlayer(entry) ?: null
+ if (!new.contains(player?.uniqueId)) inner.removeEntry(entry)
+ }
+ for (uuid in new) {
+ val player = shim.plugin.server.getPlayer(uuid) ?: continue
+ inner.addEntry(player.name)
+ }
+ }
+}
+
+class BukkitKhsBoard(val shim: BukkitKhsShim, val inner: BukkitBoard) : KhsBoard {
+ private var objective: Objective? = null
+ private var blanks: Int = 0
+
+ private fun resetObjective() {
+ if (shim.supports(13)) {
+ objective = inner.registerNewObjective("Scoreboard", "dummy", "")
+ } else {
+ objective = inner.registerNewObjective("Scoreboard", "dummy")
+ }
+ blanks = 0
+ }
+
+ private fun addLine(i: Int, line: String) {
+ val score = objective?.getScore(formatText(line))
+ score?.setScore(i + 1)
+ }
+
+ private fun addBlank(i: Int) {
+ blanks++
+ addLine(i, " ".repeat(blanks))
+ }
+
+ override fun setText(title: String, text: List<String>) {
+ resetObjective()
+
+ // set title
+ objective?.displayName = formatText(title)
+
+ // set content
+ for ((i, line) in text.withIndex()) {
+ if (line.trim().isEmpty()) {
+ addBlank(i)
+ continue
+ }
+
+ addLine(i, line)
+ }
+ }
+
+ override fun getTeam(name: String): KhsBoard.Team {
+ runCatching { inner.registerNewTeam(name) }
+ val team = inner.getTeam(name) ?: error("failed to make team ?!?")
+ return BukkitKhsTeam(shim, team)
+ }
+
+ override fun display(uuid: UUID) {
+ val player = shim.getPlayer(uuid) ?: return
+ objective?.setDisplaySlot(DisplaySlot.SIDEBAR)
+ (player as BukkitKhsPlayer).inner.setScoreboard(inner)
+ }
+}
diff --git a/bukkit/src/Inventory.kt b/bukkit/src/Inventory.kt
new file mode 100644
index 0000000..2a1d7ea
--- /dev/null
+++ b/bukkit/src/Inventory.kt
@@ -0,0 +1,59 @@
+package cat.freya.khs.bukkit
+
+import cat.freya.khs.player.Inventory as KhsInventory
+import cat.freya.khs.player.PlayerInventory as KhsPlayerInventory
+import cat.freya.khs.world.Item
+import org.bukkit.inventory.Inventory as BukkitInventory
+import org.bukkit.inventory.PlayerInventory as BukkitPlayerInventory
+
+open class BukkitKhsInventory(
+ open val shim: BukkitKhsShim,
+ open val inner: BukkitInventory,
+ open override val title: String?,
+) : KhsInventory {
+ override fun get(index: UInt): Item? = inner.getItem(index.toInt())?.let { toKhsItem(it) }
+
+ override fun set(index: UInt, item: Item) =
+ inner.setItem(index.toInt(), (item as BukkitKhsItem).inner)
+
+ override fun remove(item: Item) = inner.remove((item as BukkitKhsItem).inner)
+
+ override var contents: List<Item?>
+ get() = inner.contents.map { toKhsItem(it) }
+ set(contents: List<Item?>) =
+ inner.setContents(contents.map { (it as BukkitKhsItem).inner }.toTypedArray())
+
+ override fun clear() {
+ inner.clear()
+ }
+}
+
+class BukkitKhsPlayerInventory(
+ override val shim: BukkitKhsShim,
+ override val inner: BukkitPlayerInventory,
+ override val title: String?,
+) : BukkitKhsInventory(shim, inner, title), KhsPlayerInventory {
+ override var helmet: Item?
+ get() = toKhsItem(inner.helmet)
+ set(item: Item?) {
+ inner.helmet = (item as? BukkitKhsItem)?.inner
+ }
+
+ override var chestplate: Item?
+ get() = toKhsItem(inner.chestplate)
+ set(item: Item?) {
+ inner.chestplate = (item as? BukkitKhsItem)?.inner
+ }
+
+ override var leggings: Item?
+ get() = toKhsItem(inner.leggings)
+ set(item: Item?) {
+ inner.leggings = (item as? BukkitKhsItem)?.inner
+ }
+
+ override var boots: Item?
+ get() = toKhsItem(inner.boots)
+ set(item: Item?) {
+ inner.boots = (item as? BukkitKhsItem)?.inner
+ }
+}
diff --git a/bukkit/src/Item.kt b/bukkit/src/Item.kt
new file mode 100644
index 0000000..40646e2
--- /dev/null
+++ b/bukkit/src/Item.kt
@@ -0,0 +1,97 @@
+package cat.freya.khs.bukkit
+
+import cat.freya.khs.config.EffectConfig
+import cat.freya.khs.config.ItemConfig
+import cat.freya.khs.world.Effect as KhsEffect
+import cat.freya.khs.world.Item as KhsItem
+import com.cryptomorin.xseries.XItemStack
+import com.cryptomorin.xseries.XMaterial
+import kotlin.collections.emptyMap
+import org.bukkit.configuration.file.YamlConfiguration
+import org.bukkit.inventory.ItemStack
+import org.bukkit.inventory.meta.SkullMeta
+import org.bukkit.potion.PotionEffect
+
+class BukkitKhsItem(val inner: ItemStack, override val config: ItemConfig) : KhsItem {
+ override val name: String? = inner.itemMeta?.displayName
+ override val material: String = inner.type?.name ?: "NONE"
+
+ override fun clone(): KhsItem = BukkitKhsItem(inner, config)
+
+ override fun similar(config: ItemConfig): Boolean {
+ var item = parseBukkitItem(config) ?: return false
+ return inner.isSimilar(item.inner)
+ }
+
+ override fun similar(material: String): Boolean {
+ val xMaterial = XMaterial.matchXMaterial(material).orElse(null) ?: return false
+ return xMaterial.isSimilar(inner)
+ }
+}
+
+class BukkitKhsEffect(val inner: PotionEffect, override val config: EffectConfig) : KhsEffect {
+ @Suppress("DEPRECATION") override val name: String? = inner.type.name
+
+ override fun clone(): KhsEffect = BukkitKhsEffect(inner, config)
+}
+
+fun parseBukkitItem(itemConfig: ItemConfig): BukkitKhsItem? {
+ var config = YamlConfiguration().createSection("temp")
+ var materialParts = itemConfig.material.uppercase().split(":")
+ var material = materialParts.first()
+
+ // set name and material
+ config.set("name", itemConfig.name?.let { formatText(it) })
+ config.set("material", material)
+ if (!itemConfig.lore.isEmpty()) config.set("lore", itemConfig.lore.map { formatText(it) })
+ config.set("unbreakable", itemConfig.unbreakable ?: false)
+
+ // parse enchantments
+ var enchantments = YamlConfiguration().createSection("enchantments")
+ for ((enchantment, value) in itemConfig.enchantments) enchantments.set(
+ enchantment,
+ value.toInt(),
+ )
+ config.set("enchants", enchantments)
+
+ // set custom model data (1.14+)
+ if (itemConfig.modelData != null) config.set("model-data", itemConfig.modelData?.toInt())
+
+ // TODO: potions are broken on 1.8
+
+ // set potion data
+ if (material.endsWith("POTION")) {
+ var potionType = materialParts.getOrNull(1) ?: "AKWARD"
+ config.set("base-type", potionType)
+ }
+
+ val item = runCatching {
+ val item = (XItemStack.Deserializer()).withConfig(config).read() ?: return null
+
+ // set player head owner (if skull)
+ if (itemConfig.owner != null && itemConfig.material == "PLAYER_HEAD") {
+ val meta = item.itemMeta as SkullMeta
+ meta.setOwner(itemConfig.owner)
+ item.itemMeta = meta
+ }
+
+ BukkitKhsItem(item, itemConfig)
+ }
+
+ return item.getOrDefault(null)
+}
+
+fun toKhsItem(inner: ItemStack?): BukkitKhsItem? {
+ if (inner == null) return null
+
+ val config = ItemConfig()
+ config.name = inner.itemMeta?.displayName
+ config.material = inner.type?.name ?: "NONE"
+ config.lore = inner.itemMeta?.lore ?: listOf()
+ @Suppress("DEPRECATION")
+ config.enchantments =
+ inner.itemMeta?.enchants?.mapKeys { it.key.name }?.mapValues { it.value.toUInt() }
+ ?: emptyMap()
+ config.unbreakable = inner.itemMeta?.isUnbreakable
+ return BukkitKhsItem(inner, config)
+}
diff --git a/bukkit/src/Player.kt b/bukkit/src/Player.kt
new file mode 100644
index 0000000..a551ee5
--- /dev/null
+++ b/bukkit/src/Player.kt
@@ -0,0 +1,232 @@
+package cat.freya.khs.bukkit
+
+import cat.freya.khs.bukkit.packet.EntityMetadataPacket
+import cat.freya.khs.player.Inventory as KhsInventory
+import cat.freya.khs.player.Player as KhsPlayer
+import cat.freya.khs.player.Player.GameMode as KhsGameMode
+import cat.freya.khs.player.PlayerInventory as KhsPlayerInventory
+import cat.freya.khs.world.Effect
+import cat.freya.khs.world.Location
+import cat.freya.khs.world.Position
+import cat.freya.khs.world.World as KhsWorld
+import com.cryptomorin.xseries.XMaterial
+import com.cryptomorin.xseries.XSound
+import com.cryptomorin.xseries.messages.ActionBar
+import com.cryptomorin.xseries.messages.Titles
+import com.google.common.io.ByteStreams
+import org.bukkit.Color
+import org.bukkit.FireworkEffect
+import org.bukkit.GameMode as BukkitGameMode
+import org.bukkit.attribute.Attribute
+import org.bukkit.entity.EntityType
+import org.bukkit.entity.Firework
+import org.bukkit.entity.Player as BukkitPlayer
+import org.bukkit.potion.PotionEffect
+import org.bukkit.potion.PotionEffectType
+import org.bukkit.util.Vector
+
+class BukkitKhsPlayer(val shim: BukkitKhsShim, val inner: BukkitPlayer) : KhsPlayer {
+ override val uuid = inner.uniqueId
+ override val name = inner.name
+
+ override val location: Location
+ get() {
+ val loc = inner.location
+ return Location(loc.x, loc.y, loc.z, inner.world.name.intern())
+ }
+
+ override val world: KhsWorld?
+ get() = shim.getWorld(location.worldName)
+
+ override var health: Double
+ get() = inner.health
+ set(v: Double) {
+ inner.health = v
+ }
+
+ override var hunger: UInt
+ get() = inner.foodLevel.toUInt()
+ set(v: UInt) {
+ inner.foodLevel = v.toInt()
+ }
+
+ override fun heal() {
+ if (shim.supports(9)) {
+ val attribName = if (shim.supports(21, 6)) "MAX_HEALTH" else "GENERIC_MAX_HEALTH"
+ var attrib = inner.getAttribute(Attribute.valueOf(attribName))
+ health = attrib?.value ?: 20.0
+ } else {
+ @Suppress("DEPRECATION")
+ health = inner.maxHealth
+ }
+ }
+
+ override var allowFlight
+ get() = inner.allowFlight
+ set(v: Boolean) {
+ inner.allowFlight = v
+ }
+
+ override var flying
+ get() = inner.isFlying
+ set(flying: Boolean) {
+ if (this.flying != flying) inner.setFallDistance(0f)
+ runCatching { inner.setFlying(flying) }
+ }
+
+ override fun teleport(position: Position) {
+ val loc = Location(position.x, position.y, position.z, inner.world.name)
+ teleport(loc)
+ }
+
+ override fun teleport(location: Location) {
+ var world = shim.plugin.server.getWorld(location.worldName)
+ if (world == null) {
+ // attempt to load the world
+ val loader = shim.getWorldLoader(location.worldName)
+ loader.load()
+ world = shim.plugin.server.getWorld(location.worldName)
+ }
+ val x = location.x
+ val y = location.y
+ val z = location.z
+ val pos = org.bukkit.Location(world, x, y, z)
+
+ // sanity check
+ if (world == null) {
+ shim.logger.warning("Could not teleport $name to $x,$y,$z in ${location.worldName}")
+ return
+ }
+
+ inner.teleport(pos)
+ }
+
+ override fun sendToServer(server: String) {
+ val out = ByteStreams.newDataOutput()
+ out.writeUTF("Connect")
+ out.writeUTF(shim.plugin.khs.config.leaveServer)
+ inner.sendPluginMessage(shim.plugin, "BungeeCord", out.toByteArray())
+ }
+
+ override val inventory: KhsPlayerInventory
+ get() = BukkitKhsPlayerInventory(shim, inner.inventory, null)
+
+ override fun showInventory(inv: KhsInventory) {
+ inner.openInventory((inv as BukkitKhsInventory).inner)
+ }
+
+ override fun closeInventory() {
+ inner.closeInventory()
+ }
+
+ override fun clearEffects() {
+ for (effect in inner.activePotionEffects) inner.removePotionEffect(effect.type)
+ }
+
+ override fun giveEffect(effect: Effect) {
+ inner.addPotionEffect((effect as BukkitKhsEffect).inner)
+ }
+
+ override fun setSpeed(amplifier: UInt) {
+ inner.addPotionEffect(PotionEffect(PotionEffectType.SPEED, 1000000, 5, false, false))
+ }
+
+ override fun setGlow(target: KhsPlayer, glow: Boolean) {
+ val entity = (target as BukkitKhsPlayer).inner
+ val packet = EntityMetadataPacket(entity, glow)
+ packet.send(inner)
+ }
+
+ override fun setHidden(target: KhsPlayer, hidden: Boolean) {
+ var other = (target as BukkitKhsPlayer).inner
+ if (shim.supports(12, 2)) {
+ if (hidden) inner.hidePlayer(shim.plugin, other)
+ else inner.showPlayer(shim.plugin, other)
+ } else {
+ @Suppress("DEPRECATION")
+ if (hidden) inner.hidePlayer(other) else inner.showPlayer(other)
+ }
+ }
+
+ override fun message(message: String) {
+ inner.sendMessage(formatText(message))
+ }
+
+ override fun actionBar(message: String) {
+ ActionBar.clearActionBar(inner)
+ ActionBar.sendActionBar(inner, formatText(message))
+ }
+
+ override fun title(title: String, subTitle: String) {
+ Titles.clearTitle(inner)
+ Titles.sendTitle(inner, 10, 40, 10, formatText(title), formatText(subTitle))
+ }
+
+ override fun playSound(sound: String, volume: Double, pitch: Double) {
+ XSound.REGISTRY.getByName(sound).ifPresent {
+ it.play(inner, volume.toFloat(), pitch.toFloat())
+ }
+ }
+
+ override fun isDisguised(): Boolean = shim.plugin.disguiser.getDisguise(inner) != null
+
+ override fun disguise(material: String) {
+ runCatching {
+ val xmat = XMaterial.matchXMaterial(material).orElse(null) ?: return
+ val mat = xmat.get() ?: return
+ shim.plugin.disguiser.disguise(inner, mat)
+ }
+ }
+
+ override fun revealDisguise() {
+ shim.plugin.disguiser.reveal(inner)
+ }
+
+ override fun hasPermission(permission: String): Boolean {
+ return inner.hasPermission(permission)
+ }
+
+ override fun setGameMode(gameMode: KhsGameMode) {
+ inner.setGameMode(
+ when (gameMode) {
+ KhsGameMode.CREATIVE -> BukkitGameMode.CREATIVE
+ KhsGameMode.SURVIVAL -> BukkitGameMode.SURVIVAL
+ KhsGameMode.ADVENTURE -> BukkitGameMode.ADVENTURE
+ KhsGameMode.SPECTATOR -> BukkitGameMode.SPECTATOR
+ }
+ )
+ }
+
+ override fun hideBoards() {
+ val manager = shim.plugin.server.scoreboardManager ?: return
+ inner.setScoreboard(manager.mainScoreboard)
+ }
+
+ override fun taunt() {
+ val world = this.world?.let { it as BukkitKhsWorld } ?: return
+ val loc = org.bukkit.Location(world.inner, location.x, location.y, location.z)
+
+ // spawn firework
+ val fwMatName = if (shim.supports(13)) "FIREWORK_ROCKET" else "FIREWORK"
+ val fwMat = EntityType.valueOf(fwMatName)
+ val fw = world.inner.spawnEntity(loc, fwMat) as Firework
+ fw.setVelocity(Vector(0, 1, 0))
+
+ // make it pretty
+ val meta = fw.fireworkMeta
+ meta.setPower(4)
+ meta.addEffect(
+ FireworkEffect.builder()
+ .withColor(Color.BLUE)
+ .withColor(Color.RED)
+ .withColor(Color.YELLOW)
+ .with(FireworkEffect.Type.STAR)
+ .with(FireworkEffect.Type.BALL)
+ .with(FireworkEffect.Type.BALL_LARGE)
+ .flicker(true)
+ .withTrail()
+ .build()
+ )
+ fw.fireworkMeta = meta
+ }
+}
diff --git a/bukkit/src/Plugin.kt b/bukkit/src/Plugin.kt
new file mode 100644
index 0000000..325d4cd
--- /dev/null
+++ b/bukkit/src/Plugin.kt
@@ -0,0 +1,87 @@
+package cat.freya.khs.bukkit
+
+import cat.freya.khs.Khs
+import cat.freya.khs.bukkit.disguise.Disguiser
+import cat.freya.khs.bukkit.disguise.EntityHider
+import cat.freya.khs.bukkit.event.*
+import org.bukkit.command.Command
+import org.bukkit.command.CommandSender
+import org.bukkit.entity.Player as BukkitPlayer
+import org.bukkit.plugin.java.JavaPlugin
+import org.bukkit.scheduler.BukkitRunnable
+
+class KhsPlugin : JavaPlugin() {
+ val shim: BukkitKhsShim = BukkitKhsShim(this)
+ val khs: Khs = Khs(shim)
+
+ // for blockhunt
+ val disguiser: Disguiser = Disguiser(this)
+ val entityHider: EntityHider = EntityHider()
+
+ override fun onEnable() {
+ khs.init()
+
+ if (!this.isEnabled()) return
+
+ // make sure onTick is run
+ object : BukkitRunnable() {
+ override fun run() {
+ khs.onTick()
+ disguiser.update()
+ }
+ }
+ .runTaskTimer(this, 0, 1)
+
+ // register bungee cord
+ server.messenger.registerOutgoingPluginChannel(this, "BungeeCord")
+
+ registerListeners()
+ }
+
+ override fun onDisable() {
+ khs.cleanup()
+ disguiser.cleanup()
+ }
+
+ private fun registerListeners() {
+ BreakListener(this)
+ ChatListener(this)
+ CommandListener(this)
+ DamageListener(this)
+ InteractListener(this)
+ InventoryListener(this)
+ JoinLeaveListener(this)
+ MovementListener(this)
+ PlayerListener(this)
+ PacketListener(this)
+ RespawnListener(this)
+ }
+
+ fun scheduleTask(fn: () -> Unit) {
+ if (!isEnabled) return
+ server.scheduler.runTask(this, fn)
+ }
+
+ override fun onCommand(
+ sender: CommandSender,
+ cmd: Command,
+ label: String,
+ args: Array<String>,
+ ): Boolean {
+ val player = sender as? BukkitPlayer ?: return false
+ val khsPlayer = BukkitKhsPlayer(shim, player)
+ khs.commandGroup.handleCommand(khsPlayer, args.toList())
+ return true
+ }
+
+ override fun onTabComplete(
+ sender: CommandSender,
+ cmd: Command,
+ label: String,
+ args: Array<String>,
+ ): List<String> {
+ val player = sender as? BukkitPlayer ?: return listOf()
+ val khsPlayer = BukkitKhsPlayer(shim, player)
+ return khs.commandGroup.handleTabComplete(khsPlayer, args.toList())
+ }
+}
diff --git a/bukkit/src/Shim.kt b/bukkit/src/Shim.kt
new file mode 100644
index 0000000..fa568f9
--- /dev/null
+++ b/bukkit/src/Shim.kt
@@ -0,0 +1,180 @@
+package cat.freya.khs.bukkit
+
+import cat.freya.khs.KhsShim
+import cat.freya.khs.Logger
+import cat.freya.khs.config.EffectConfig
+import cat.freya.khs.config.ItemConfig
+import cat.freya.khs.game.Board as KhsBoard
+import cat.freya.khs.player.Inventory as KhsInventory
+import cat.freya.khs.player.Player as KhsPlayer
+import cat.freya.khs.world.Effect as KhsEffect
+import cat.freya.khs.world.Item as KhsItem
+import cat.freya.khs.world.World as KhsWorld
+import com.cryptomorin.xseries.XMaterial
+import java.io.File
+import java.io.InputStream
+import java.util.UUID
+import kotlin.jvm.optionals.getOrNull
+import org.bukkit.ChatColor
+import org.bukkit.World as BukkitWorld
+import org.bukkit.WorldCreator
+import org.bukkit.WorldType
+import org.bukkit.potion.PotionEffect
+import org.bukkit.potion.PotionEffectType
+
+class BukkitLogger(val plugin: KhsPlugin) : Logger {
+ override fun info(message: String) = plugin.logger.info(message)
+
+ override fun warning(message: String) = plugin.logger.warning(message)
+
+ override fun error(message: String) = plugin.logger.severe(message)
+}
+
+class BukkitKhsShim(val plugin: KhsPlugin) : KhsShim {
+ override val pluginVersion: String
+ override val mcVersion: List<UInt>
+ override val platform: String = "bukkit"
+
+ init {
+ // parse mc version
+ mcVersion =
+ Regex("""MC:\s*([\d.]+)""")
+ .find(plugin.server.version)
+ ?.groupValues
+ ?.get(1)
+ ?.split('.')
+ ?.asSequence()
+ ?.mapNotNull { it.toUIntOrNull() }
+ ?.let { seq -> if (seq.firstOrNull() == 1u) seq.drop(1) else seq }
+ ?.toList() ?: emptyList()
+
+ pluginVersion = plugin.description.version
+ }
+
+ override val logger = BukkitLogger(plugin)
+
+ override val players: List<KhsPlayer>
+ get() = plugin.server.onlinePlayers.map { BukkitKhsPlayer(this, it) }
+
+ override val worlds: List<String>
+ get() =
+ plugin.server.worldContainer
+ .listFiles()
+ .filter {
+ if (!it.isDirectory) return@filter false
+
+ val session = File(it, "session.lock")
+ val level = File(it, "level.dat")
+
+ session.exists() && level.exists()
+ }
+ .map { it.name }
+
+ override val sqliteDatabasePath: String
+ get() {
+ val legacy = File(plugin.dataFolder.path, "database.db")
+ if (legacy.exists()) return legacy.path
+
+ return File(plugin.dataFolder.path, "khs.db").path
+ }
+
+ override fun readConfigFile(fileName: String): InputStream? {
+ val dir = plugin.dataFolder
+ if (!dir.exists()) {
+ dir.mkdirs() || error("Failed to make plugin config directory")
+ }
+ val file = File(dir, fileName)
+ return if (file.exists()) file.inputStream() else null
+ }
+
+ override fun writeConfigFile(fileName: String, content: String) {
+ val dir = plugin.dataFolder
+ if (!dir.exists()) {
+ dir.mkdirs() || error("Failed to make plugin config directory")
+ }
+ val file = File(dir, fileName)
+ file.writeText(content)
+ }
+
+ override fun parseMaterial(materialName: String): String? {
+ return XMaterial.matchXMaterial(materialName).getOrNull()?.get()?.toString()
+ }
+
+ override fun parseItem(itemConfig: ItemConfig): KhsItem? {
+ return parseBukkitItem(itemConfig)
+ }
+
+ override fun parseEffect(effectConfig: EffectConfig): KhsEffect? {
+ @Suppress("DEPRECATION")
+ val type = PotionEffectType.getByName(effectConfig.type.uppercase()) ?: return null
+ val inner =
+ PotionEffect(
+ type,
+ effectConfig.duration.toInt(),
+ effectConfig.amplifier.toInt(),
+ effectConfig.ambient,
+ effectConfig.particles,
+ )
+
+ return BukkitKhsEffect(inner, effectConfig)
+ }
+
+ override fun getPlayer(uuid: UUID): KhsPlayer? {
+ return plugin.server.getPlayer(uuid)?.let { BukkitKhsPlayer(this, it) }
+ }
+
+ override fun getPlayer(name: String): KhsPlayer? {
+ return plugin.server.getPlayer(name)?.let { BukkitKhsPlayer(this, it) }
+ }
+
+ override fun getWorld(worldName: String): KhsWorld? {
+ return plugin.server.getWorld(worldName)?.let { BukkitKhsWorld(this, it) }
+ }
+
+ override fun getWorldLoader(worldName: String): KhsWorld.Loader {
+ return BukkitKhsWorldLoader(plugin, worldName)
+ }
+
+ override fun createWorld(worldName: String, type: KhsWorld.Type): KhsWorld? {
+ val worldType = if (type == KhsWorld.Type.FLAT) WorldType.FLAT else WorldType.NORMAL
+ val env =
+ when (type) {
+ KhsWorld.Type.NETHER -> BukkitWorld.Environment.NETHER
+ KhsWorld.Type.END -> BukkitWorld.Environment.THE_END
+ else -> BukkitWorld.Environment.NORMAL
+ }
+ val creator = WorldCreator(worldName)
+ creator.type(worldType)
+ creator.environment(env)
+ plugin.server.createWorld(creator)
+ var world = plugin.server.getWorld(worldName) ?: return null
+ world.save()
+ return BukkitKhsWorld(plugin.shim, world)
+ }
+
+ override fun createInventory(title: String, size: UInt): KhsInventory? {
+ val inv = plugin.server.createInventory(null, size.toInt(), title)
+ return BukkitKhsInventory(this, inv, title)
+ }
+
+ override fun getBoard(name: String): KhsBoard? {
+ val board = plugin.server.scoreboardManager?.getNewScoreboard() ?: return null
+ return BukkitKhsBoard(this, board)
+ }
+
+ override fun broadcast(message: String) {
+ plugin.server.broadcastMessage(formatText(message))
+ }
+
+ override fun disable() {
+ plugin.server.pluginManager.disablePlugin(plugin)
+ }
+
+ override fun scheduleEvent(ticks: ULong, event: () -> Unit) {
+ plugin.server.scheduler.scheduleSyncDelayedTask(plugin, event, ticks.toLong())
+ }
+}
+
+fun formatText(message: String): String {
+ return ChatColor.translateAlternateColorCodes('&', message)
+}
diff --git a/bukkit/src/World.kt b/bukkit/src/World.kt
new file mode 100644
index 0000000..afda77a
--- /dev/null
+++ b/bukkit/src/World.kt
@@ -0,0 +1,157 @@
+package cat.freya.khs.bukkit
+
+import cat.freya.khs.world.Position
+import cat.freya.khs.world.World as KhsWorld
+import cat.freya.khs.world.World.Border as KhsWorldBorder
+import cat.freya.khs.world.World.Loader as KhsWorldLoader
+import java.io.File
+import java.util.Random
+import org.bukkit.GameRule
+import org.bukkit.Location as BukkitLocation
+import org.bukkit.World as BukkitWorld
+import org.bukkit.WorldBorder as BukkitWorldBorder
+import org.bukkit.WorldCreator
+import org.bukkit.WorldType
+import org.bukkit.block.Biome
+import org.bukkit.generator.BlockPopulator
+import org.bukkit.generator.ChunkGenerator
+
+class VoidGenerator : ChunkGenerator() {
+ // 1.14 And On
+ override fun getDefaultPopulators(world: BukkitWorld): List<BlockPopulator> {
+ return listOf()
+ }
+
+ override fun canSpawn(world: BukkitWorld, x: Int, z: Int): Boolean {
+ return true
+ }
+
+ override fun getFixedSpawnLocation(world: BukkitWorld, random: Random): BukkitLocation {
+ return BukkitLocation(world, 0.0, 0.0, 0.0)
+ }
+
+ // 1.13 And Prev
+ @Suppress("DEPRECATION", "OVERRIDE_DEPRECATION")
+ override fun generateChunkData(
+ world: BukkitWorld,
+ random: Random,
+ chunkX: Int,
+ chunkZ: Int,
+ biome: BiomeGrid,
+ ): ChunkData {
+ val chunkData = createChunkData(world)
+
+ for (x in 0 until 16) for (z in 0 until 16) biome.setBiome(x, z, Biome.PLAINS)
+
+ return chunkData
+ }
+
+ // 1.8
+ fun generate(world: BukkitWorld, random: Random, x: Int, z: Int): ByteArray {
+ return ByteArray(world.maxHeight / 16)
+ }
+
+ @Suppress("DEPRECATION")
+ fun generateBlockSections(
+ world: BukkitWorld,
+ random: Random,
+ x: Int,
+ z: Int,
+ biomes: ChunkGenerator.BiomeGrid,
+ ): Array<ByteArray> {
+ return Array(world.maxHeight / 16) { ByteArray(0) }
+ }
+}
+
+class BukkitKhsWorldBorder(val world: BukkitKhsWorld, val inner: BukkitWorldBorder) :
+ KhsWorldBorder {
+ override val x: Double = inner.center.x
+ override val z: Double = inner.center.z
+ override val size: Double = inner.size
+
+ override fun move(newX: Double, newZ: Double, newSize: ULong, delay: ULong) {
+ inner.setCenter(newX, newZ)
+ move(newSize, delay)
+ }
+
+ override fun move(newSize: ULong, delay: ULong) {
+ inner.setSize(newSize.toDouble(), delay.toLong())
+ }
+}
+
+class BukkitKhsWorldLoader(val plugin: KhsPlugin, val worldName: String) : KhsWorldLoader {
+ override val name: String = worldName
+ override val world: KhsWorld? = plugin.shim.getWorld(worldName)
+
+ override val dir: File
+ get() = File(plugin.server.worldContainer, name)
+
+ override val saveDir: File
+ get() = File(plugin.server.worldContainer, "hs_$name")
+
+ override val tempSaveDir: File
+ get() = File(plugin.server.worldContainer, "temp_hs_$name")
+
+ override fun load() {
+ plugin.server.createWorld(WorldCreator(name).generator(VoidGenerator()))
+ val world = plugin.server.getWorld(name)
+ if (world == null) {
+ plugin.shim.logger.error("could not load world: $name")
+ }
+ world?.setAutoSave(false)
+ if (plugin.shim.supports(21, 6)) world?.setGameRule(GameRule.LOCATOR_BAR, false)
+ }
+
+ override fun unload() {
+ val world = plugin.server.getWorld(name)
+ if (world == null) return // world already unloaded
+
+ world.players.forEach { player ->
+ val khsPlayer = BukkitKhsPlayer(plugin.shim, player)
+ plugin.khs.config.exit?.teleport(khsPlayer)
+ }
+
+ if (plugin.server.unloadWorld(name, false) == false)
+ plugin.shim.logger.error("could not unload world: $name")
+ }
+
+ override fun rollback() {
+ unload()
+ load()
+ }
+}
+
+class BukkitKhsWorld(val shim: BukkitKhsShim, val inner: BukkitWorld) : KhsWorld {
+ override val name = inner.name
+ override val type: KhsWorld.Type
+ get() {
+ val env = inner.environment
+ if (env == BukkitWorld.Environment.NETHER) return KhsWorld.Type.NETHER
+ if (env == BukkitWorld.Environment.THE_END) return KhsWorld.Type.END
+
+ @Suppress("DEPRECATION")
+ return when (inner.worldType) {
+ WorldType.NORMAL -> KhsWorld.Type.NORMAL
+ WorldType.FLAT -> KhsWorld.Type.FLAT
+ else -> KhsWorld.Type.UNKNOWN
+ }
+ }
+
+ override val minY: Int
+ get() = if (shim.supports(18)) -64 else 0
+
+ override val maxY: Int
+ get() = if (shim.supports(18)) 320 else 256
+
+ override val spawn: Position
+ get() {
+ val loc = inner.spawnLocation
+ return Position(loc.x, loc.y, loc.z)
+ }
+
+ override val border: KhsWorldBorder
+ get() = BukkitKhsWorldBorder(this, inner.worldBorder)
+
+ override val loader: KhsWorldLoader
+ get() = shim.getWorldLoader(name)
+}
diff --git a/bukkit/src/disguise/Disguise.kt b/bukkit/src/disguise/Disguise.kt
new file mode 100644
index 0000000..8c86188
--- /dev/null
+++ b/bukkit/src/disguise/Disguise.kt
@@ -0,0 +1,244 @@
+package cat.freya.khs.bukkit.disguise
+
+import cat.freya.khs.bukkit.KhsPlugin
+import cat.freya.khs.bukkit.packet.BlockChangePacket
+import cat.freya.khs.bukkit.packet.EntityTeleportPacket
+import com.cryptomorin.xseries.XSound
+import com.cryptomorin.xseries.messages.ActionBar
+import kotlin.math.round
+import org.bukkit.Location
+import org.bukkit.Material
+import org.bukkit.entity.AbstractHorse
+import org.bukkit.entity.Entity as BukkitEntity
+import org.bukkit.entity.EntityType
+import org.bukkit.entity.FallingBlock
+import org.bukkit.entity.LivingEntity
+import org.bukkit.entity.Player as BukkitPlayer
+import org.bukkit.potion.PotionEffect
+import org.bukkit.potion.PotionEffectType
+import org.bukkit.scoreboard.Team
+
+private fun makeInvisible(entity: LivingEntity) {
+ if (entity.hasPotionEffect(PotionEffectType.INVISIBILITY)) return
+
+ entity.addPotionEffect(PotionEffect(PotionEffectType.INVISIBILITY, 1_000_000, 0, false, false))
+}
+
+private fun getCollidesTeam(plugin: KhsPlugin): Team? {
+ val KHS_DISGUISE_TEAM_NAME = "KHS_disguised"
+ val scoreboard = plugin.server.scoreboardManager?.mainScoreboard ?: return null
+ val team =
+ scoreboard.getTeam(KHS_DISGUISE_TEAM_NAME)
+ ?: scoreboard.registerNewTeam(KHS_DISGUISE_TEAM_NAME)
+ team.setOption(Team.Option.COLLISION_RULE, Team.OptionStatus.NEVER)
+ team.setCanSeeFriendlyInvisibles(false)
+ return team
+}
+
+private fun setCollides(plugin: KhsPlugin, player: BukkitPlayer, collides: Boolean) {
+ if (plugin.shim.supports(9)) {
+ val team = getCollidesTeam(plugin)
+ if (collides) team?.removeEntry(player.name) else team?.addEntry(player.name)
+ } else {
+ val method = player.spigot().javaClass.getMethod("setCollidesWithEntities")
+ method.invoke(player, collides)
+ }
+}
+
+class Disguise(val plugin: KhsPlugin, val player: BukkitPlayer, val material: Material) {
+ var block: FallingBlock? = null
+ var blockLocation: Location? = null
+ var hitBox: AbstractHorse? = null
+ var timer: UInt = 0u
+
+ @Volatile var shouldBeSolid: Boolean = false
+ @Volatile var isSolid: Boolean = false
+ @Volatile var hasSolidifyingTask: Boolean = false
+
+ init {
+ // make sure the player does not collide
+ setCollides(plugin, player, false)
+ }
+
+ val entityId: Int?
+ get() = block?.entityId
+
+ val hitBoxId: Int?
+ get() = hitBox?.entityId
+
+ fun update() {
+ if (block?.isDead() != false) {
+ block?.remove()
+ respawnFallingBlock()
+ }
+
+ if (shouldBeSolid) {
+ if (!isSolid) {
+ isSolid = true
+ blockLocation = player.location.block.location
+ respawnHitbox()
+ }
+ sendBlockUpdate(blockLocation, material)
+ } else if (isSolid) {
+ isSolid = false
+ removeHitbox()
+ removeFallingBlock()
+ respawnFallingBlock()
+ sendBlockUpdate(blockLocation, Material.AIR)
+ }
+
+ updateVisiblity()
+ teleportEntity(hitBox, true)
+ teleportEntity(block, isSolid)
+
+ // do this here is it can be
+ // cleared by the core game logic
+ makeInvisible(player)
+ }
+
+ fun remove() {
+ block?.remove()
+ removeHitbox()
+ setCollides(plugin, player, true)
+ player.removePotionEffect(PotionEffectType.INVISIBILITY)
+ if (isSolid) sendBlockUpdate(blockLocation, Material.AIR)
+ }
+
+ fun removeFallingBlock() {
+ block?.remove()
+ block = null
+ }
+
+ fun respawnFallingBlock() {
+ val world = player.location.world ?: return
+ val loc = player.location.add(0.0, 1000.0, 0.0)
+
+ val block: FallingBlock? =
+ runCatching { world.spawnFallingBlock(loc, material.createBlockData()) }
+ .getOrElse { null }
+ if (block == null) return
+
+ if (plugin.shim.supports(10)) block.setGravity(false)
+
+ block.setDropItem(false)
+ block.setInvulnerable(true)
+
+ this.block = block
+ }
+
+ fun respawnHitbox() {
+ val world = player.location.world ?: return
+
+ // we only want the hitbox to be at our postion
+ // when we are solidified
+ val loc = player.location.add(0.0, 1000.0, 0.0)
+
+ val hitBox: AbstractHorse? =
+ if (plugin.shim.supports(11)) {
+ world.spawnEntity(loc, EntityType.SKELETON_HORSE) as AbstractHorse
+ } else {
+ world.spawnEntity(loc, EntityType.HORSE) as AbstractHorse
+ }
+
+ if (hitBox == null) return
+
+ if (plugin.shim.supports(10)) hitBox.setGravity(false)
+
+ val id = hitBox.uniqueId.toString()
+ if (plugin.shim.supports(9)) getCollidesTeam(plugin)?.addEntry(id)
+
+ hitBox.setAI(false)
+ hitBox.setInvulnerable(true)
+ hitBox.setCanPickupItems(false)
+ hitBox.setCollidable(false)
+ makeInvisible(hitBox)
+
+ this.hitBox = hitBox
+ }
+
+ fun removeHitbox() {
+ val hb = hitBox ?: return
+ val id = hb.uniqueId.toString()
+
+ hb.remove()
+ if (plugin.shim.supports(9)) getCollidesTeam(plugin)?.removeEntry(id)
+
+ hitBox == null
+ }
+
+ fun teleportEntity(entity: BukkitEntity?, center: Boolean) {
+ if (entity == null) return
+
+ val loc = player.location.clone()
+ if (center) {
+ loc.x = round(loc.x + 0.5) - 0.5
+ loc.y = round(loc.y)
+ loc.z = round(loc.z + 0.5) - 0.5
+ }
+
+ val packet = EntityTeleportPacket(entity, loc)
+ plugin.server.onlinePlayers.forEach { packet.send(it) }
+ }
+
+ fun sendBlockUpdate(location: Location?, material: Material) {
+ if (location == null) return
+
+ val packet = BlockChangePacket(location, material)
+ plugin.server.onlinePlayers.forEach {
+ if (it.uniqueId == player.uniqueId) return@forEach
+ packet.send(it)
+ }
+ }
+
+ fun updateVisiblity() {
+ val block = block ?: return
+ val show = !isSolid
+ plugin.server.onlinePlayers.forEach { target ->
+ if (target.uniqueId == player.uniqueId) return@forEach
+
+ if (show) {
+ plugin.entityHider.showEntity(target, block)
+ } else {
+ plugin.entityHider.hideEntity(target, block)
+ }
+ }
+ }
+
+ fun startSolidifying() {
+ if (isSolid || hasSolidifyingTask) return
+ hasSolidifyingTask = true
+ plugin.server.scheduler.scheduleSyncDelayedTask(
+ plugin,
+ { solidifyUpdate(player.location.clone(), 3u) },
+ 10,
+ )
+ }
+
+ fun solidifyUpdate(lastLocation: Location, time: UInt) {
+ val location = player.location
+
+ if ((lastLocation.world != location.world) || (lastLocation.distance(location) > 0.1)) {
+ hasSolidifyingTask = false
+ return
+ }
+
+ // we have solidified!
+ if (time == 0u) {
+ ActionBar.clearActionBar(player)
+ shouldBeSolid = true
+ hasSolidifyingTask = false
+ return
+ }
+
+ // still waiting
+ ActionBar.sendActionBar(player, "▪".repeat(time.toInt()))
+ XSound.BLOCK_NOTE_BLOCK_PLING.play(player, 1f, 1f)
+
+ // schedule next update
+ plugin.server.scheduler.scheduleSyncDelayedTask(
+ plugin,
+ { solidifyUpdate(lastLocation, time - 1u) },
+ 20,
+ )
+ }
+}
diff --git a/bukkit/src/disguise/Disguiser.kt b/bukkit/src/disguise/Disguiser.kt
new file mode 100644
index 0000000..771c8f6
--- /dev/null
+++ b/bukkit/src/disguise/Disguiser.kt
@@ -0,0 +1,43 @@
+package cat.freya.khs.bukkit.disguise
+
+import cat.freya.khs.bukkit.KhsPlugin
+import java.util.concurrent.ConcurrentHashMap
+import org.bukkit.Material
+import org.bukkit.entity.Player as BukkitPlayer
+
+class Disguiser(val plugin: KhsPlugin) {
+ val disguises = ConcurrentHashMap<BukkitPlayer, Disguise>()
+
+ fun cleanup() {
+ disguises.forEach { it.value.remove() }
+ disguises.clear()
+ }
+
+ fun getDisguise(player: BukkitPlayer): Disguise? = disguises.get(player)
+
+ fun getByEntityId(id: Int): Disguise? = disguises.values.firstOrNull { it.entityId == id }
+
+ fun getByHitboxId(id: Int): Disguise? = disguises.values.firstOrNull { it.hitBoxId == id }
+
+ fun update() {
+ for ((player, disguise) in disguises) {
+ if (!player.isOnline) {
+ disguise.remove()
+ disguises.remove(player)
+ } else {
+ disguise.update()
+ }
+ }
+ }
+
+ fun disguise(player: BukkitPlayer, material: Material) {
+ // remove old disguise (if exists)
+ reveal(player)
+ // make new one
+ disguises.put(player, Disguise(plugin, player, material))
+ }
+
+ fun reveal(player: BukkitPlayer) {
+ disguises.remove(player)?.remove()
+ }
+}
diff --git a/bukkit/src/disguise/EntityHider.kt b/bukkit/src/disguise/EntityHider.kt
new file mode 100644
index 0000000..c3ee9c3
--- /dev/null
+++ b/bukkit/src/disguise/EntityHider.kt
@@ -0,0 +1,58 @@
+package cat.freya.khs.bukkit.disguise
+
+import cat.freya.khs.bukkit.packet.EntityDestroyPacket
+import cat.freya.khs.bukkit.packet.EntityMetadataPacket
+import com.google.common.collect.HashBasedTable
+import com.google.common.collect.Table
+import org.bukkit.entity.Entity as BukkitEntity
+import org.bukkit.entity.Player as BukkitPlayer
+
+class EntityHider {
+ val map: Table<Int, Int, Boolean> = HashBasedTable.create()
+
+ // is entity visible for the observer
+ fun isVisible(observer: BukkitPlayer, entityId: Int): Boolean =
+ runCatching {
+ val observerId = observer.entityId
+ !map.contains(observerId, entityId)
+ }
+ .getOrElse { true }
+
+ // is entity visible for the observer
+ fun isVisible(observer: BukkitPlayer, entity: BukkitEntity): Boolean =
+ runCatching { isVisible(observer, entity.entityId) }.getOrElse { true }
+
+ // set if the entity is hidden for the observer
+ fun setVisible(observer: BukkitPlayer, entity: BukkitEntity, visible: Boolean) = runCatching {
+ val observerId = observer.entityId
+ val entityId = entity.entityId
+ val ret =
+ if (visible) map.put(entityId, observerId, true) else map.remove(entityId, observerId)
+ ret ?: visible
+ }
+
+ // removes a hidden entity from the map
+ fun removeEntity(entity: BukkitEntity) =
+ runCatching { entity.entityId }
+ .onSuccess { for (row in map.rowMap().values) row.remove(it) }
+
+ // removes a player from the map
+ fun removePlayer(player: BukkitPlayer) =
+ runCatching { player.entityId }.onSuccess { map.rowMap().remove(it) }
+
+ // hides and entity
+ fun hideEntity(observer: BukkitPlayer, entity: BukkitEntity) {
+ setVisible(observer, entity, false)
+
+ val packet = EntityDestroyPacket(entity)
+ packet.send(observer)
+ }
+
+ // unhides the entity
+ fun showEntity(observer: BukkitPlayer, entity: BukkitEntity) {
+ setVisible(observer, entity, true)
+
+ val packet = EntityMetadataPacket(entity, false)
+ packet.send(observer)
+ }
+}
diff --git a/bukkit/src/event/BreakListener.kt b/bukkit/src/event/BreakListener.kt
new file mode 100644
index 0000000..832aa72
--- /dev/null
+++ b/bukkit/src/event/BreakListener.kt
@@ -0,0 +1,56 @@
+package cat.freya.khs.bukkit.event
+
+import cat.freya.khs.bukkit.BukkitKhsPlayer
+import cat.freya.khs.bukkit.KhsPlugin
+import cat.freya.khs.event.BreakEvent
+import cat.freya.khs.event.onBreak
+import org.bukkit.entity.Player as BukkitPlayer
+import org.bukkit.event.EventHandler
+import org.bukkit.event.EventPriority
+import org.bukkit.event.Listener
+import org.bukkit.event.block.BlockBreakEvent
+import org.bukkit.event.entity.EntityBreakDoorEvent
+import org.bukkit.event.hanging.HangingBreakByEntityEvent
+
+class BreakListener(val plugin: KhsPlugin) : Listener {
+
+ init {
+ plugin.server.pluginManager.registerEvents(this, plugin)
+ }
+
+ @EventHandler(priority = EventPriority.HIGHEST)
+ fun onBlockBreak(event: BlockBreakEvent) {
+ val bukkitPlayer = event.player ?: return
+ val block = event.block?.type?.name ?: return
+
+ val khsPlayer = BukkitKhsPlayer(plugin.shim, bukkitPlayer)
+ val khsEvent = BreakEvent(plugin.khs, khsPlayer, block)
+ onBreak(khsEvent)
+
+ if (khsEvent.cancelled) event.setCancelled(true)
+ }
+
+ @EventHandler(priority = EventPriority.HIGHEST)
+ fun onEntityBreakDoor(event: EntityBreakDoorEvent) {
+ val bukkitPlayer = event.entity as? BukkitPlayer ?: return
+ val block = event.block?.type?.name ?: return
+
+ val khsPlayer = BukkitKhsPlayer(plugin.shim, bukkitPlayer)
+ val khsEvent = BreakEvent(plugin.khs, khsPlayer, block)
+ onBreak(khsEvent)
+
+ if (khsEvent.cancelled) event.setCancelled(true)
+ }
+
+ @EventHandler(priority = EventPriority.HIGHEST)
+ fun onHangingBreakByEntity(event: HangingBreakByEntityEvent) {
+ val bukkitPlayer = event.remover as? BukkitPlayer ?: return
+ val block = event.entity?.type?.name ?: return
+
+ val khsPlayer = BukkitKhsPlayer(plugin.shim, bukkitPlayer)
+ val khsEvent = BreakEvent(plugin.khs, khsPlayer, block)
+ onBreak(khsEvent)
+
+ if (khsEvent.cancelled) event.setCancelled(true)
+ }
+}
diff --git a/bukkit/src/event/ChatListener.kt b/bukkit/src/event/ChatListener.kt
new file mode 100644
index 0000000..530fa25
--- /dev/null
+++ b/bukkit/src/event/ChatListener.kt
@@ -0,0 +1,29 @@
+package cat.freya.khs.bukkit.event
+
+import cat.freya.khs.bukkit.BukkitKhsPlayer
+import cat.freya.khs.bukkit.KhsPlugin
+import cat.freya.khs.event.ChatEvent
+import cat.freya.khs.event.onChat
+import org.bukkit.event.EventHandler
+import org.bukkit.event.EventPriority
+import org.bukkit.event.Listener
+import org.bukkit.event.player.AsyncPlayerChatEvent
+
+class ChatListener(val plugin: KhsPlugin) : Listener {
+
+ init {
+ plugin.server.pluginManager.registerEvents(this, plugin)
+ }
+
+ @EventHandler(priority = EventPriority.LOW, ignoreCancelled = true)
+ fun onChat(event: AsyncPlayerChatEvent) {
+ val bukkitPlayer = event.player ?: return
+ val message = event.message ?: return
+
+ val khsPlayer = BukkitKhsPlayer(plugin.shim, bukkitPlayer)
+ val khsEvent = ChatEvent(plugin.khs, khsPlayer, message)
+ onChat(khsEvent)
+
+ if (khsEvent.cancelled) event.setCancelled(true)
+ }
+}
diff --git a/bukkit/src/event/CommandListener.kt b/bukkit/src/event/CommandListener.kt
new file mode 100644
index 0000000..7ed25ca
--- /dev/null
+++ b/bukkit/src/event/CommandListener.kt
@@ -0,0 +1,29 @@
+package cat.freya.khs.bukkit.event
+
+import cat.freya.khs.bukkit.BukkitKhsPlayer
+import cat.freya.khs.bukkit.KhsPlugin
+import cat.freya.khs.event.CommandEvent
+import cat.freya.khs.event.onCommand
+import org.bukkit.event.EventHandler
+import org.bukkit.event.EventPriority
+import org.bukkit.event.Listener
+import org.bukkit.event.player.PlayerCommandPreprocessEvent
+
+class CommandListener(val plugin: KhsPlugin) : Listener {
+
+ init {
+ plugin.server.pluginManager.registerEvents(this, plugin)
+ }
+
+ @EventHandler(priority = EventPriority.HIGHEST)
+ fun onPlayerCommand(event: PlayerCommandPreprocessEvent) {
+ val bukkitPlayer = event.player ?: return
+ val message = event.message ?: return
+
+ val khsPlayer = BukkitKhsPlayer(plugin.shim, bukkitPlayer)
+ val khsEvent = CommandEvent(plugin.khs, khsPlayer, message)
+ onCommand(khsEvent)
+
+ if (khsEvent.cancelled) event.setCancelled(true)
+ }
+}
diff --git a/bukkit/src/event/DamageListener.kt b/bukkit/src/event/DamageListener.kt
new file mode 100644
index 0000000..81d5d12
--- /dev/null
+++ b/bukkit/src/event/DamageListener.kt
@@ -0,0 +1,56 @@
+package cat.freya.khs.bukkit.event
+
+import cat.freya.khs.bukkit.BukkitKhsPlayer
+import cat.freya.khs.bukkit.KhsPlugin
+import cat.freya.khs.event.DamageEvent
+import cat.freya.khs.event.onDamage
+import org.bukkit.entity.Player as BukkitPlayer
+import org.bukkit.entity.Projectile
+import org.bukkit.event.EventHandler
+import org.bukkit.event.EventPriority
+import org.bukkit.event.Listener
+import org.bukkit.event.entity.EntityDamageByEntityEvent
+import org.bukkit.event.entity.EntityDamageEvent
+
+class DamageListener(val plugin: KhsPlugin) : Listener {
+
+ init {
+ plugin.server.pluginManager.registerEvents(this, plugin)
+ }
+
+ @EventHandler(priority = EventPriority.HIGHEST)
+ fun onEntityDamageByEntity(event: EntityDamageByEntityEvent) {
+ val bukkitPlayer = (event.entity as? BukkitPlayer) ?: return
+
+ // get attacker
+ val damager = event.damager
+ val attackerEntity: BukkitPlayer? =
+ when {
+ damager is Projectile -> damager.shooter as? BukkitPlayer
+ else -> damager as? BukkitPlayer
+ }
+
+ if (attackerEntity == null) {
+ onEntityDamage(event)
+ return
+ }
+
+ val khsPlayer = BukkitKhsPlayer(plugin.shim, bukkitPlayer)
+ val khsAttacker = BukkitKhsPlayer(plugin.shim, attackerEntity)
+ val khsEvent = DamageEvent(plugin.khs, khsPlayer, khsAttacker, event.damage)
+ onDamage(khsEvent)
+
+ if (khsEvent.cancelled) event.setCancelled(true)
+ }
+
+ @EventHandler(priority = EventPriority.HIGH)
+ fun onEntityDamage(event: EntityDamageEvent) {
+ val bukkitPlayer = (event.entity as? BukkitPlayer) ?: return
+
+ val khsPlayer = BukkitKhsPlayer(plugin.shim, bukkitPlayer)
+ val khsEvent = DamageEvent(plugin.khs, khsPlayer, null, event.damage)
+ onDamage(khsEvent)
+
+ if (khsEvent.cancelled) event.setCancelled(true)
+ }
+}
diff --git a/bukkit/src/event/InteractListener.kt b/bukkit/src/event/InteractListener.kt
new file mode 100644
index 0000000..cc9b2a7
--- /dev/null
+++ b/bukkit/src/event/InteractListener.kt
@@ -0,0 +1,50 @@
+package cat.freya.khs.bukkit.event
+
+import cat.freya.khs.bukkit.BukkitKhsPlayer
+import cat.freya.khs.bukkit.KhsPlugin
+import cat.freya.khs.bukkit.toKhsItem
+import cat.freya.khs.event.InteractEvent
+import cat.freya.khs.event.UseEvent
+import cat.freya.khs.event.onInteract
+import cat.freya.khs.event.onUse
+import org.bukkit.event.EventHandler
+import org.bukkit.event.EventPriority
+import org.bukkit.event.Listener
+import org.bukkit.event.block.Action
+import org.bukkit.event.player.PlayerInteractEvent
+
+class InteractListener(val plugin: KhsPlugin) : Listener {
+
+ init {
+ plugin.server.pluginManager.registerEvents(this, plugin)
+ }
+
+ @EventHandler(priority = EventPriority.HIGHEST)
+ fun onPlayerInteract(event: PlayerInteractEvent) {
+ val bukkitPlayer = event.player ?: return
+
+ val khsPlayer = BukkitKhsPlayer(plugin.shim, bukkitPlayer)
+
+ val block = event.clickedBlock?.type?.name
+ if (event.action == Action.RIGHT_CLICK_BLOCK && block != null) {
+ val khsEvent = InteractEvent(plugin.khs, khsPlayer, block)
+ onInteract(khsEvent)
+
+ if (khsEvent.cancelled) {
+ event.setCancelled(true)
+ return
+ }
+ }
+
+ val item = toKhsItem(event.item)
+ if (item != null) {
+ val khsEvent = UseEvent(plugin.khs, khsPlayer, item)
+ onUse(khsEvent)
+
+ if (khsEvent.cancelled) {
+ event.setCancelled(true)
+ return
+ }
+ }
+ }
+}
diff --git a/bukkit/src/event/InventoryListener.kt b/bukkit/src/event/InventoryListener.kt
new file mode 100644
index 0000000..4ec0a83
--- /dev/null
+++ b/bukkit/src/event/InventoryListener.kt
@@ -0,0 +1,61 @@
+package cat.freya.khs.bukkit.event
+
+import cat.freya.khs.bukkit.BukkitKhsInventory
+import cat.freya.khs.bukkit.BukkitKhsPlayer
+import cat.freya.khs.bukkit.KhsPlugin
+import cat.freya.khs.bukkit.toKhsItem
+import cat.freya.khs.event.ClickEvent
+import cat.freya.khs.event.CloseEvent
+import cat.freya.khs.event.onClick
+import cat.freya.khs.event.onClose
+import org.bukkit.entity.Player as BukkitPlayer
+import org.bukkit.event.EventHandler
+import org.bukkit.event.EventPriority
+import org.bukkit.event.Listener
+import org.bukkit.event.inventory.InventoryClickEvent
+import org.bukkit.event.inventory.InventoryCloseEvent
+import org.bukkit.event.inventory.InventoryEvent
+import org.bukkit.inventory.Inventory
+
+class InventoryListener(val plugin: KhsPlugin) : Listener {
+
+ init {
+ plugin.server.pluginManager.registerEvents(this, plugin)
+ }
+
+ private fun getInv(event: InventoryEvent): Pair<Inventory, String?> {
+ if (plugin.shim.supports(14)) {
+ var inv = event.view.topInventory
+ return inv to event.view.title
+ } else {
+ var inv = event.inventory
+ var title = inv::class.java.getMethod("getName").invoke(inv) as String
+ return inv to title
+ }
+ }
+
+ @EventHandler(priority = EventPriority.HIGHEST)
+ fun onInventoryClick(event: InventoryClickEvent) {
+ val (inventory, title) = getInv(event)
+ val bukkitPlayer = event.whoClicked as? BukkitPlayer ?: return
+ val item = toKhsItem(event.currentItem) ?: return
+
+ val khsPlayer = BukkitKhsPlayer(plugin.shim, bukkitPlayer)
+ val khsInventory = BukkitKhsInventory(plugin.shim, inventory, title)
+ val khsEvent = ClickEvent(plugin.khs, khsPlayer, khsInventory, item)
+ onClick(khsEvent)
+
+ if (khsEvent.cancelled) event.setCancelled(true)
+ }
+
+ @EventHandler(priority = EventPriority.HIGHEST)
+ fun onInventoryClose(event: InventoryCloseEvent) {
+ val (inventory, title) = getInv(event)
+ val bukkitPlayer = event.player as? BukkitPlayer ?: return
+
+ val khsPlayer = BukkitKhsPlayer(plugin.shim, bukkitPlayer)
+ val khsInventory = BukkitKhsInventory(plugin.shim, inventory, title)
+ val khsEvent = CloseEvent(plugin.khs, khsPlayer, khsInventory)
+ onClose(khsEvent)
+ }
+}
diff --git a/bukkit/src/event/JoinLeaveListener.kt b/bukkit/src/event/JoinLeaveListener.kt
new file mode 100644
index 0000000..3bb2832
--- /dev/null
+++ b/bukkit/src/event/JoinLeaveListener.kt
@@ -0,0 +1,52 @@
+package cat.freya.khs.bukkit.event
+
+import cat.freya.khs.bukkit.BukkitKhsPlayer
+import cat.freya.khs.bukkit.KhsPlugin
+import cat.freya.khs.event.JoinEvent
+import cat.freya.khs.event.KickEvent
+import cat.freya.khs.event.LeaveEvent
+import cat.freya.khs.event.onJoin
+import cat.freya.khs.event.onKick
+import cat.freya.khs.event.onLeave
+import org.bukkit.event.EventHandler
+import org.bukkit.event.EventPriority
+import org.bukkit.event.Listener
+import org.bukkit.event.player.PlayerJoinEvent
+import org.bukkit.event.player.PlayerKickEvent
+import org.bukkit.event.player.PlayerQuitEvent
+
+class JoinLeaveListener(val plugin: KhsPlugin) : Listener {
+
+ init {
+ plugin.server.pluginManager.registerEvents(this, plugin)
+ }
+
+ @EventHandler(priority = EventPriority.HIGHEST)
+ fun onPlayerJoin(event: PlayerJoinEvent) {
+ val bukkitPlayer = event.player ?: return
+ val khsPlayer = BukkitKhsPlayer(plugin.shim, bukkitPlayer)
+ val khsEvent = JoinEvent(plugin.khs, khsPlayer)
+ onJoin(khsEvent)
+ }
+
+ @EventHandler(priority = EventPriority.HIGHEST)
+ fun onPlayerQuit(event: PlayerQuitEvent) {
+ val bukkitPlayer = event.player ?: return
+ val khsPlayer = BukkitKhsPlayer(plugin.shim, bukkitPlayer)
+ val khsEvent = LeaveEvent(plugin.khs, khsPlayer)
+ onLeave(khsEvent)
+
+ // remove player from disguiser
+ plugin.entityHider.removePlayer(bukkitPlayer)
+ }
+
+ @EventHandler(priority = EventPriority.HIGHEST)
+ fun onPlayerKick(event: PlayerKickEvent) {
+ val bukkitPlayer = event.player ?: return
+ val khsPlayer = BukkitKhsPlayer(plugin.shim, bukkitPlayer)
+ val khsEvent = KickEvent(plugin.khs, khsPlayer, event.reason ?: "")
+ onKick(khsEvent)
+
+ if (khsEvent.cancelled) event.setCancelled(true)
+ }
+}
diff --git a/bukkit/src/event/MovementListener.kt b/bukkit/src/event/MovementListener.kt
new file mode 100644
index 0000000..9b80415
--- /dev/null
+++ b/bukkit/src/event/MovementListener.kt
@@ -0,0 +1,71 @@
+package cat.freya.khs.bukkit.event
+
+import cat.freya.khs.bukkit.BukkitKhsPlayer
+import cat.freya.khs.bukkit.KhsPlugin
+import cat.freya.khs.event.JumpEvent
+import cat.freya.khs.event.MoveEvent
+import cat.freya.khs.event.onJump
+import cat.freya.khs.event.onMove
+import cat.freya.khs.world.Position as KhsPosition
+import java.util.UUID
+import java.util.concurrent.ConcurrentHashMap
+import org.bukkit.Material
+import org.bukkit.entity.Player as BukkitPlayer
+import org.bukkit.event.EventHandler
+import org.bukkit.event.EventPriority
+import org.bukkit.event.Listener
+import org.bukkit.event.player.PlayerMoveEvent
+
+class MovementListener(val plugin: KhsPlugin) : Listener {
+
+ private val prevPlayersOnGround: MutableSet<UUID> = ConcurrentHashMap.newKeySet<UUID>()
+
+ init {
+ plugin.server.pluginManager.registerEvents(this, plugin)
+ }
+
+ private fun isOnGround(player: BukkitPlayer): Boolean {
+ if (plugin.shim.supports(16, 1)) {
+ val below = player.location.clone().subtract(0.0, 0.1, 0.0).block
+ return below.type.isSolid
+ } else {
+ @Suppress("DEPRECATION")
+ return player.isOnGround()
+ }
+ }
+
+ @EventHandler(priority = EventPriority.HIGHEST)
+ fun onPlayerMove(event: PlayerMoveEvent) {
+ val bukkitPlayer = event.player ?: return
+ val khsPlayer = BukkitKhsPlayer(plugin.shim, bukkitPlayer)
+
+ // check jumping
+ if (bukkitPlayer.velocity.y > 0.0) {
+ val block = bukkitPlayer.location?.block?.type
+ if (
+ block != Material.LADDER &&
+ prevPlayersOnGround.contains(bukkitPlayer.uniqueId) &&
+ isOnGround(bukkitPlayer)
+ ) {
+ // trigger jump event
+ val khsEvent = JumpEvent(plugin.khs, khsPlayer)
+ onJump(khsEvent)
+ }
+ // update set
+ if (isOnGround(bukkitPlayer)) prevPlayersOnGround.add(bukkitPlayer.uniqueId)
+ else prevPlayersOnGround.remove(bukkitPlayer.uniqueId)
+ }
+
+ val to = event.to?.let { KhsPosition(it.x, it.y, it.z) } ?: return
+ val khsEvent = MoveEvent(plugin.khs, khsPlayer, to)
+ onMove(khsEvent)
+
+ if (khsEvent.cancelled) event.setCancelled(true)
+
+ // update disguise (if exists)
+ val disguise = plugin.disguiser.getDisguise(bukkitPlayer) ?: return
+ val dest = event.to ?: return
+ if (!khsEvent.cancelled && event.from.distance(dest) > 0.1) disguise.shouldBeSolid = false
+ disguise.startSolidifying()
+ }
+}
diff --git a/bukkit/src/event/PacketListener.kt b/bukkit/src/event/PacketListener.kt
new file mode 100644
index 0000000..d49af60
--- /dev/null
+++ b/bukkit/src/event/PacketListener.kt
@@ -0,0 +1,110 @@
+package cat.freya.khs.bukkit.event
+
+import cat.freya.khs.bukkit.BukkitKhsPlayer
+import cat.freya.khs.bukkit.KhsPlugin
+import cat.freya.khs.bukkit.disguise.Disguise
+import cat.freya.khs.event.DamageEvent
+import cat.freya.khs.event.onDamage
+import com.github.retrooper.packetevents.PacketEvents
+import com.github.retrooper.packetevents.event.PacketListener as PacketListenerPE
+import com.github.retrooper.packetevents.event.PacketListenerPriority
+import com.github.retrooper.packetevents.event.PacketReceiveEvent
+import com.github.retrooper.packetevents.event.PacketSendEvent
+import com.github.retrooper.packetevents.protocol.packettype.PacketType.Play.Client.INTERACT_ENTITY
+import com.github.retrooper.packetevents.protocol.packettype.PacketType.Play.Server.*
+import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientInteractEntity
+import com.github.retrooper.packetevents.wrapper.play.server.*
+import java.util.UUID
+import org.bukkit.GameMode
+import org.bukkit.attribute.Attribute
+import org.bukkit.entity.Player as BukkitPlayer
+
+class PacketListener(val plugin: KhsPlugin) : PacketListenerPE {
+ private val debounce = mutableSetOf<UUID>()
+
+ init {
+ PacketEvents.getAPI().eventManager.registerListener(this, PacketListenerPriority.NORMAL)
+ }
+
+ // intercept entity-related packets of entities that
+ // are supposed to be hidden
+ override fun onPacketSend(event: PacketSendEvent) {
+ val player = event.getPlayer() as? BukkitPlayer ?: return
+ val entityId =
+ when (event.packetType) {
+ ENTITY_EQUIPMENT -> WrapperPlayServerEntityEquipment(event).entityId
+ ENTITY_ANIMATION -> WrapperPlayServerEntityAnimation(event).entityId
+ SPAWN_ENTITY -> WrapperPlayServerSpawnEntity(event).entityId
+ ENTITY_VELOCITY -> WrapperPlayServerEntityVelocity(event).entityId
+ ENTITY_HEAD_LOOK -> WrapperPlayServerEntityHeadLook(event).entityId
+ ENTITY_TELEPORT -> WrapperPlayServerEntityTeleport(event).entityId
+ ENTITY_STATUS -> WrapperPlayServerEntityStatus(event).entityId
+ ENTITY_METADATA -> WrapperPlayServerEntityMetadata(event).entityId
+ ENTITY_EFFECT -> WrapperPlayServerEntityEffect(event).entityId
+ REMOVE_ENTITY_EFFECT -> WrapperPlayServerRemoveEntityEffect(event).entityId
+ else -> return
+ }
+
+ if (!plugin.entityHider.isVisible(player, entityId)) {
+ event.setCancelled(true)
+ }
+ }
+
+ // check when a player is trying to attack a disguise
+ override fun onPacketReceive(event: PacketReceiveEvent) {
+ val player = event.getPlayer() as? BukkitPlayer ?: return
+
+ // we want interact event
+ if (event.packetType != INTERACT_ENTITY) return
+
+ val packet = WrapperPlayClientInteractEntity(event)
+
+ // attacking only
+ val action = packet.action ?: return
+ if (action != WrapperPlayClientInteractEntity.InteractAction.ATTACK) return
+
+ val disguise =
+ plugin.disguiser.getByEntityId(packet.entityId)
+ ?: plugin.disguiser.getByHitboxId(packet.entityId)
+ ?: return
+
+ if (disguise.player.gameMode == GameMode.CREATIVE) return
+
+ event.setCancelled(true)
+ handleAttack(disguise, player)
+ }
+
+ private fun handleAttack(disguise: Disguise, seeker: BukkitPlayer) {
+ if (disguise.player.uniqueId == seeker.uniqueId) return
+
+ val fallbackAmount = 7.0
+ val amount =
+ if (plugin.shim.supports(9)) {
+ val attribName =
+ if (plugin.shim.supports(21)) "ATTACK_DAMAGE" else "GENERIC_ATTACK_DAMAGE"
+ val attrib = Attribute.valueOf(attribName)
+ seeker.getAttribute(attrib)?.value ?: fallbackAmount
+ } else {
+ fallbackAmount // uhhh i dunno how to do this for 1.8
+ }
+
+ val debounceUUID = disguise.player.uniqueId
+
+ disguise.shouldBeSolid = false
+ if (debounce.contains(debounceUUID)) return
+
+ // trigger an attack event
+ val khsPlayer = BukkitKhsPlayer(plugin.shim, disguise.player)
+ val khsSeeker = BukkitKhsPlayer(plugin.shim, seeker)
+ val khsEvent = DamageEvent(plugin.khs, khsPlayer, khsSeeker, amount)
+ onDamage(khsEvent)
+
+ // set and soon turn off debounce
+ debounce.add(debounceUUID)
+ plugin.server.scheduler.scheduleSyncDelayedTask(
+ plugin,
+ { debounce.remove(debounceUUID) },
+ 10,
+ )
+ }
+}
diff --git a/bukkit/src/event/PlayerListener.kt b/bukkit/src/event/PlayerListener.kt
new file mode 100644
index 0000000..23b0a91
--- /dev/null
+++ b/bukkit/src/event/PlayerListener.kt
@@ -0,0 +1,59 @@
+package cat.freya.khs.bukkit.event
+
+import cat.freya.khs.bukkit.BukkitKhsPlayer
+import cat.freya.khs.bukkit.KhsPlugin
+import cat.freya.khs.bukkit.toKhsItem
+import cat.freya.khs.event.DropEvent
+import cat.freya.khs.event.HungerEvent
+import cat.freya.khs.event.RegenEvent
+import cat.freya.khs.event.onDrop
+import cat.freya.khs.event.onHunger
+import cat.freya.khs.event.onRegen
+import org.bukkit.entity.Player as BukkitPlayer
+import org.bukkit.event.EventHandler
+import org.bukkit.event.EventPriority
+import org.bukkit.event.Listener
+import org.bukkit.event.entity.EntityRegainHealthEvent
+import org.bukkit.event.entity.FoodLevelChangeEvent
+import org.bukkit.event.player.PlayerDropItemEvent
+
+class PlayerListener(val plugin: KhsPlugin) : Listener {
+
+ init {
+ plugin.server.pluginManager.registerEvents(this, plugin)
+ }
+
+ @EventHandler(priority = EventPriority.HIGHEST)
+ fun onFoodLevelChange(event: FoodLevelChangeEvent) {
+ val bukkitPlayer = event.entity as? BukkitPlayer ?: return
+ val khsPlayer = BukkitKhsPlayer(plugin.shim, bukkitPlayer)
+ val khsEvent = HungerEvent(plugin.khs, khsPlayer)
+ onHunger(khsEvent)
+
+ if (khsEvent.cancelled) event.setCancelled(true)
+ }
+
+ @EventHandler(priority = EventPriority.HIGHEST)
+ fun onEntityRegainHealth(event: EntityRegainHealthEvent) {
+ val bukkitPlayer = event.entity as? BukkitPlayer ?: return
+ val khsPlayer = BukkitKhsPlayer(plugin.shim, bukkitPlayer)
+ val natural =
+ event.regainReason == EntityRegainHealthEvent.RegainReason.SATIATED ||
+ event.regainReason == EntityRegainHealthEvent.RegainReason.REGEN
+ val khsEvent = RegenEvent(plugin.khs, khsPlayer, natural)
+ onRegen(khsEvent)
+
+ if (khsEvent.cancelled) event.setCancelled(true)
+ }
+
+ @EventHandler(priority = EventPriority.HIGHEST)
+ fun onPlayerDropItem(event: PlayerDropItemEvent) {
+ val bukkitPlayer = event.player ?: return
+ val item = toKhsItem(event.itemDrop?.itemStack) ?: return
+ val khsPlayer = BukkitKhsPlayer(plugin.shim, bukkitPlayer)
+ val khsEvent = DropEvent(plugin.khs, khsPlayer, item)
+ onDrop(khsEvent)
+
+ if (khsEvent.cancelled) event.setCancelled(true)
+ }
+}
diff --git a/bukkit/src/event/RespawnListener.kt b/bukkit/src/event/RespawnListener.kt
new file mode 100644
index 0000000..a2b4bc5
--- /dev/null
+++ b/bukkit/src/event/RespawnListener.kt
@@ -0,0 +1,41 @@
+package cat.freya.khs.bukkit.event
+
+import cat.freya.khs.bukkit.BukkitKhsPlayer
+import cat.freya.khs.bukkit.KhsPlugin
+import cat.freya.khs.event.DeathEvent
+import cat.freya.khs.event.onDeath
+import cat.freya.khs.world.Location
+import java.util.UUID
+import java.util.concurrent.ConcurrentHashMap
+import org.bukkit.event.EventHandler
+import org.bukkit.event.EventPriority
+import org.bukkit.event.Listener
+import org.bukkit.event.entity.PlayerDeathEvent
+import org.bukkit.event.player.PlayerRespawnEvent
+
+class RespawnListener(val plugin: KhsPlugin) : Listener {
+
+ private val respawnLocation: MutableMap<UUID, Location> = ConcurrentHashMap<UUID, Location>()
+
+ init {
+ plugin.server.pluginManager.registerEvents(this, plugin)
+ }
+
+ @EventHandler(priority = EventPriority.HIGHEST)
+ fun onPlayerDeath(event: PlayerDeathEvent) {
+ val bukkitPlayer = event.entity ?: return
+ val khsPlayer = BukkitKhsPlayer(plugin.shim, bukkitPlayer)
+ val khsEvent = DeathEvent(plugin.khs, khsPlayer)
+ onDeath(khsEvent)
+
+ if (khsEvent.cancelled) respawnLocation[khsPlayer.uuid] = khsPlayer.location
+ }
+
+ @EventHandler(priority = EventPriority.HIGHEST)
+ fun onPlayerRespawn(event: PlayerRespawnEvent) {
+ val bukkitPlayer = event.player ?: return
+ val khsPlayer = BukkitKhsPlayer(plugin.shim, bukkitPlayer)
+ val location = respawnLocation.remove(khsPlayer.uuid) ?: return
+ khsPlayer.teleport(location)
+ }
+}
diff --git a/bukkit/src/packet/BlockChangePacket.kt b/bukkit/src/packet/BlockChangePacket.kt
new file mode 100644
index 0000000..7d11f4e
--- /dev/null
+++ b/bukkit/src/packet/BlockChangePacket.kt
@@ -0,0 +1,20 @@
+package cat.freya.khs.bukkit.packet
+
+import com.github.retrooper.packetevents.PacketEvents
+import com.github.retrooper.packetevents.util.Vector3i
+import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerBlockChange
+import io.github.retrooper.packetevents.util.SpigotConversionUtil
+import org.bukkit.Bukkit
+import org.bukkit.Location
+import org.bukkit.Material
+import org.bukkit.entity.Player as BukkitPlayer
+
+data class BlockChangePacket(val location: Location, val material: Material) {
+ fun send(player: BukkitPlayer) {
+ val blockData = Bukkit.createBlockData(material)
+ val state = SpigotConversionUtil.fromBukkitBlockData(blockData)
+ val vector = Vector3i(location.blockX, location.blockY, location.blockZ)
+ val packet = WrapperPlayServerBlockChange(vector, state)
+ PacketEvents.getAPI().playerManager.sendPacket(player, packet)
+ }
+}
diff --git a/bukkit/src/packet/EntityDestroyPacket.kt b/bukkit/src/packet/EntityDestroyPacket.kt
new file mode 100644
index 0000000..3240b81
--- /dev/null
+++ b/bukkit/src/packet/EntityDestroyPacket.kt
@@ -0,0 +1,13 @@
+package cat.freya.khs.bukkit.packet
+
+import com.github.retrooper.packetevents.PacketEvents
+import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerDestroyEntities
+import org.bukkit.entity.Entity
+import org.bukkit.entity.Player as BukkitPlayer
+
+data class EntityDestroyPacket(val entiy: Entity) {
+ fun send(player: BukkitPlayer) {
+ val packet = WrapperPlayServerDestroyEntities(entiy.entityId)
+ PacketEvents.getAPI().playerManager.sendPacket(player, packet)
+ }
+}
diff --git a/bukkit/src/packet/EntityMetadataPacket.kt b/bukkit/src/packet/EntityMetadataPacket.kt
new file mode 100644
index 0000000..6d5978a
--- /dev/null
+++ b/bukkit/src/packet/EntityMetadataPacket.kt
@@ -0,0 +1,17 @@
+package cat.freya.khs.bukkit.packet
+
+import com.github.retrooper.packetevents.PacketEvents
+import com.github.retrooper.packetevents.protocol.entity.data.EntityData
+import com.github.retrooper.packetevents.protocol.entity.data.EntityDataTypes
+import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerEntityMetadata
+import org.bukkit.entity.Entity
+import org.bukkit.entity.Player as BukkitPlayer
+
+data class EntityMetadataPacket(val entiy: Entity, val glow: Boolean) {
+ fun send(player: BukkitPlayer) {
+ val glowingByte = if (glow) 0x40 else 0x0
+ val data = EntityData(0x0, EntityDataTypes.BYTE, glowingByte.toByte())
+ val packet = WrapperPlayServerEntityMetadata(entiy.entityId, listOf(data))
+ PacketEvents.getAPI().playerManager.sendPacket(player, packet)
+ }
+}
diff --git a/bukkit/src/packet/EntityTeleportPacket.kt b/bukkit/src/packet/EntityTeleportPacket.kt
new file mode 100644
index 0000000..7e3beb1
--- /dev/null
+++ b/bukkit/src/packet/EntityTeleportPacket.kt
@@ -0,0 +1,19 @@
+package cat.freya.khs.bukkit.packet
+
+import com.github.retrooper.packetevents.PacketEvents
+import com.github.retrooper.packetevents.util.Vector3d
+import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerEntityTeleport
+import org.bukkit.Location
+import org.bukkit.entity.Entity
+import org.bukkit.entity.Player as BukkitPlayer
+
+data class EntityTeleportPacket(val entity: Entity, val position: Location) {
+ fun send(player: BukkitPlayer) {
+ val vector = Vector3d(position.x, position.y, position.z)
+ val yaw = 0f
+ val pitch = 0f
+ val onGround = false
+ val packet = WrapperPlayServerEntityTeleport(entity.entityId, vector, yaw, pitch, onGround)
+ PacketEvents.getAPI().playerManager.sendPacket(player, packet)
+ }
+}