diff --git a/api/src/main/kotlin/org/kryptonmc/api/event/player/ItemDropEvent.kt b/api/src/main/kotlin/org/kryptonmc/api/event/player/ItemDropEvent.kt new file mode 100644 index 00000000000..50a71877879 --- /dev/null +++ b/api/src/main/kotlin/org/kryptonmc/api/event/player/ItemDropEvent.kt @@ -0,0 +1,30 @@ +/* + * This file is part of the Krypton API, licensed under the MIT license. + * + * Copyright (C) 2021 KryptonMC and the contributors to the Krypton project. + * + * This project is licensed under the terms of the MIT license. + * For more details, please reference the LICENSE file in the api top-level directory. + */ +package org.kryptonmc.api.event.player + +import org.kryptonmc.api.entity.player.Player +import org.kryptonmc.api.event.ComponentResult +import org.kryptonmc.api.event.ResultedEvent +import org.kryptonmc.api.item.ItemStack + +/** + * Called when a player attempts to drop an item. + * + * @param player the player dropping the item + * @param item the item to be dropped + */ + +public data class ItemDropEvent( + @get:JvmName("player") public val player: Player, + @get:JvmName("item") public val item: ItemStack +) : ResultedEvent { + + override var result: ComponentResult = ComponentResult.allowed() +} + diff --git a/server/src/main/kotlin/org/kryptonmc/krypton/entity/EntityFactory.kt b/server/src/main/kotlin/org/kryptonmc/krypton/entity/EntityFactory.kt index ed87b0f86fb..40dddb3c429 100644 --- a/server/src/main/kotlin/org/kryptonmc/krypton/entity/EntityFactory.kt +++ b/server/src/main/kotlin/org/kryptonmc/krypton/entity/EntityFactory.kt @@ -67,6 +67,7 @@ import org.kryptonmc.krypton.entity.projectile.KryptonSpectralArrow import org.kryptonmc.krypton.entity.projectile.KryptonThrownPotion import org.kryptonmc.krypton.entity.projectile.KryptonTrident import org.kryptonmc.krypton.entity.projectile.KryptonWitherSkull +import org.kryptonmc.krypton.entity.item.KryptonItemEntity import org.kryptonmc.krypton.registry.InternalRegistries import org.kryptonmc.krypton.util.logger import org.kryptonmc.krypton.world.KryptonWorld @@ -122,7 +123,8 @@ object EntityFactory { EntityTypes.WITHER_SKULL to ::KryptonWitherSkull, EntityTypes.WOLF to ::KryptonWolf, EntityTypes.ZOMBIE to ::KryptonZombie, - EntityTypes.FISHING_HOOK to ::KryptonFishingHook + EntityTypes.FISHING_HOOK to ::KryptonFishingHook, + EntityTypes.ITEM to ::KryptonItemEntity ) @Suppress("UNCHECKED_CAST") diff --git a/server/src/main/kotlin/org/kryptonmc/krypton/entity/KryptonEntity.kt b/server/src/main/kotlin/org/kryptonmc/krypton/entity/KryptonEntity.kt index b46d6ff9df1..93bbe33e74b 100644 --- a/server/src/main/kotlin/org/kryptonmc/krypton/entity/KryptonEntity.kt +++ b/server/src/main/kotlin/org/kryptonmc/krypton/entity/KryptonEntity.kt @@ -120,6 +120,10 @@ abstract class KryptonEntity( final override var location: Vector3d = Vector3d.ZERO final override var rotation: Vector2f = Vector2f.ZERO final override var velocity: Vector3d = Vector3d.ZERO + set(value) { + field = value + viewers.forEach { it.session.send(PacketOutEntityVelocity(this)) } + } final override var boundingBox = BoundingBox.zero() final override var dimensions = EntityDimensions.fixed(type.dimensions.width, type.dimensions.height) final override var isOnGround = true diff --git a/server/src/main/kotlin/org/kryptonmc/krypton/entity/item/KryptonItemEntity.kt b/server/src/main/kotlin/org/kryptonmc/krypton/entity/item/KryptonItemEntity.kt new file mode 100644 index 00000000000..f69cf142e82 --- /dev/null +++ b/server/src/main/kotlin/org/kryptonmc/krypton/entity/item/KryptonItemEntity.kt @@ -0,0 +1,86 @@ +package org.kryptonmc.krypton.entity.item + +import net.kyori.adventure.text.Component +import org.kryptonmc.api.entity.EntityTypes +import org.kryptonmc.api.inventory.InventoryHolder +import org.kryptonmc.api.util.BoundingBox +import org.kryptonmc.krypton.entity.KryptonEntity +import org.kryptonmc.krypton.entity.KryptonLivingEntity +import org.kryptonmc.krypton.entity.metadata.MetadataKeys +import org.kryptonmc.krypton.item.KryptonItemStack +import org.kryptonmc.krypton.packet.out.play.PacketOutCollectItem +import org.kryptonmc.krypton.util.forEachEntityInBounds +import org.kryptonmc.krypton.world.KryptonWorld +import org.kryptonmc.nbt.CompoundTag +import org.kryptonmc.nbt.util.UUID +import org.spongepowered.math.vector.Vector3d + +class KryptonItemEntity(world: KryptonWorld) : KryptonEntity(world, EntityTypes.ITEM) { + + init { + data.add(MetadataKeys.ITEM.ITEM) + } + + var age: Short = 0 + var health: Short = 5 + var pickupDelay: Short = 10 + var owner: UUID? = null + var thrower: UUID? = null + + var item: KryptonItemStack + get() = data[MetadataKeys.ITEM.ITEM] + set(value) = data.set(MetadataKeys.ITEM.ITEM, value) + override fun load(tag: CompoundTag) { + super.load(tag) + age = tag.getShort("Age") + health = tag.getShort("Health") + pickupDelay = tag.getShort("PickupDelay") + if (tag.hasUUID("Owner")) owner = tag.getUUID("Owner") + if (tag.hasUUID("Thrower")) thrower = tag.getUUID("Thrower") + val itemNBT = tag.getCompound("Item") + item = KryptonItemStack(itemNBT) + if (item.isEmpty()) remove() + } + + private fun entityPickupItem(entity: KryptonLivingEntity) { + // TODO: Reduce stack to fit a full inventory and leave remainder + if (entity is InventoryHolder) { + entity.inventory.add(item) + } + // TODO: Perhaps consider only sending this to players in range? + world.playerManager.sendToAll(PacketOutCollectItem(id, entity.id, item.amount)) + item.amount = 0 // destroy next tick + } + + override fun tick() { + if (item.isEmpty()) { + remove() + return + } + super.tick() + if (pickupDelay > 0 && pickupDelay != DELAY_NO_PICKUP) pickupDelay-- + if (pickupDelay.toInt() == 0) { + server.sendMessage(Component.text("delay 0")) + // Item pickup zone + val minimum = Vector3d.from(location.x() - 1.0, location.y() - 0.5, location.z() - 1.0) + val maximum = Vector3d.from(location.x() + 1.0, location.y() + 0.5, location.z() + 1.0) + world.entityManager.forEachEntityInBounds(BoundingBox.of(minimum, maximum)) { + if (it.isAlive && it is KryptonLivingEntity) { + // TODO: Test for mob pick up and handle item merge + server.sendMessage(Component.text("Ent found")) + server.sendMessage(it.name) + entityPickupItem(it) + return@forEachEntityInBounds + } + } + } + if (age != AGE_NO_DESPAWN) age++ + if (age >= AGE_DESPAWN) remove() + } + + companion object { + const val DELAY_NO_PICKUP: Short = 32767 // The delay value that prevents item pickup + const val AGE_NO_DESPAWN: Short = -32768 // The age value that prevents age from incrementing + const val AGE_DESPAWN: Short = 6000 // The age value that despawns the item entity + } +} diff --git a/server/src/main/kotlin/org/kryptonmc/krypton/entity/metadata/MetadataKeys.kt b/server/src/main/kotlin/org/kryptonmc/krypton/entity/metadata/MetadataKeys.kt index 563747755c3..248b9bf7005 100644 --- a/server/src/main/kotlin/org/kryptonmc/krypton/entity/metadata/MetadataKeys.kt +++ b/server/src/main/kotlin/org/kryptonmc/krypton/entity/metadata/MetadataKeys.kt @@ -60,6 +60,7 @@ object MetadataKeys { @JvmField val AXOLOTL = AxolotlKeys @JvmField val BEE = BeeKeys @JvmField val CAT = CatKeys + @JvmField val ITEM = ItemKeys @JvmField val FOX = FoxKeys @JvmField val GOAT = GoatKeys @JvmField val MOOSHROOM = MooshroomKeys @@ -227,6 +228,11 @@ object MetadataKeys { @JvmField val COLLAR_COLOR = create(22, MetadataSerializers.VAR_INT, Registries.DYE_COLORS.idOf(DyeColors.RED)) } + object ItemKeys { + + @JvmField val ITEM = create(8, MetadataSerializers.SLOT, KryptonItemStack.Factory.empty()) + } + object FoxKeys { @JvmField val TYPE = create(17, MetadataSerializers.VAR_INT, FoxType.RED.ordinal) diff --git a/server/src/main/kotlin/org/kryptonmc/krypton/entity/player/KryptonPlayer.kt b/server/src/main/kotlin/org/kryptonmc/krypton/entity/player/KryptonPlayer.kt index bad434025bf..c1fda246ce5 100644 --- a/server/src/main/kotlin/org/kryptonmc/krypton/entity/player/KryptonPlayer.kt +++ b/server/src/main/kotlin/org/kryptonmc/krypton/entity/player/KryptonPlayer.kt @@ -45,7 +45,9 @@ import org.kryptonmc.api.entity.attribute.AttributeTypes import org.kryptonmc.api.entity.player.ChatVisibility import org.kryptonmc.api.entity.player.Player import org.kryptonmc.api.event.player.ChangeGameModeEvent +import org.kryptonmc.api.event.player.ItemDropEvent import org.kryptonmc.api.event.player.PerformActionEvent +import org.kryptonmc.api.item.ItemStack import org.kryptonmc.api.item.ItemTypes import org.kryptonmc.api.permission.PermissionFunction import org.kryptonmc.api.permission.PermissionProvider @@ -69,6 +71,7 @@ import org.kryptonmc.krypton.entity.EquipmentSlot import org.kryptonmc.krypton.entity.KryptonEntity import org.kryptonmc.krypton.entity.KryptonEquipable import org.kryptonmc.krypton.entity.KryptonLivingEntity +import org.kryptonmc.krypton.entity.item.KryptonItemEntity import org.kryptonmc.krypton.entity.metadata.MetadataKeys import org.kryptonmc.krypton.inventory.KryptonPlayerInventory import org.kryptonmc.krypton.item.KryptonItemStack @@ -104,6 +107,7 @@ import org.kryptonmc.krypton.packet.out.play.PacketOutUnloadChunk import org.kryptonmc.krypton.packet.out.play.PacketOutUpdateHealth import org.kryptonmc.krypton.packet.out.play.PacketOutUpdateViewPosition import org.kryptonmc.krypton.service.KryptonVanishService +import org.kryptonmc.krypton.util.* import org.kryptonmc.krypton.statistic.KryptonStatisticsTracker import org.kryptonmc.krypton.util.BossBarManager import org.kryptonmc.krypton.util.Directions @@ -122,6 +126,7 @@ import java.net.InetSocketAddress import java.time.Instant import java.util.Locale import java.util.UUID +import java.util.concurrent.ThreadLocalRandom import kotlin.math.abs import kotlin.math.max import kotlin.math.min @@ -944,6 +949,52 @@ class KryptonPlayer( } } + fun dropHeldItem() { + val heldItem = inventory.heldItem(Hand.MAIN) + if (heldItem.isEmpty()) return + + // Update held item amount + heldItem.amount-- + inventory.setHeldItem(Hand.MAIN, heldItem) + + // drop the item + dropItem(heldItem) + } + + fun dropItem(itemStack: ItemStack) { + server.eventManager.fire(ItemDropEvent(this, itemStack)).thenAcceptAsync({ + if (!it.result.isAllowed) return@thenAcceptAsync + val singleStack = itemStack.copy() as KryptonItemStack + singleStack.amount = 1 + val itemEntity = KryptonItemEntity(world) + itemEntity.thrower = uuid + itemEntity.item = singleStack + itemEntity.location = location.add(0.0, 1.62 - 0.3, 0.0) // eye height - some magic value?? + val itemVelocity = getDirectionVector().mul(0.3) + // Glowstone's method. Seems sane however it uses guess work. I don't blame them. Just look at the vanilla source... + // https://github.com/GlowstoneMC/Glowstone/blob/dev/src/main/java/net/glowstone/entity/GlowHumanEntity.java + val random = ThreadLocalRandom.current() + val offset = 0.02 + itemVelocity.add( + random.nextDouble(offset) - offset / 2, + random.nextDouble(0.12), + random.nextDouble(offset) - offset / 2 + ) + world.entityManager.spawn(itemEntity) + itemEntity.velocity = itemVelocity + }, session.channel.eventLoop()) + return + } + + private fun getDirectionVector(): Vector3d { + // Similar to wiki.vg/bukkit impl + val y = kotlin.math.sin(Math.toRadians(rotation.y().toDouble())) + val xz = kotlin.math.cos(Math.toRadians(rotation.y().toDouble())) + val x = -xz * kotlin.math.sin(Math.toRadians(rotation.x().toDouble())) + val z = xz * kotlin.math.cos(Math.toRadians(rotation.x().toDouble())) + return Vector3d.from(x, y, z) + } + companion object { private const val FLYING_ACHIEVEMENT_MINIMUM_SPEED = 25 diff --git a/server/src/main/kotlin/org/kryptonmc/krypton/entity/player/PlayerBlockHandler.kt b/server/src/main/kotlin/org/kryptonmc/krypton/entity/player/PlayerBlockHandler.kt index 1785075aa10..af0bc52af56 100644 --- a/server/src/main/kotlin/org/kryptonmc/krypton/entity/player/PlayerBlockHandler.kt +++ b/server/src/main/kotlin/org/kryptonmc/krypton/entity/player/PlayerBlockHandler.kt @@ -19,6 +19,8 @@ package org.kryptonmc.krypton.entity.player import org.kryptonmc.api.block.Block +import org.kryptonmc.api.item.ItemStack +import org.kryptonmc.krypton.entity.item.KryptonItemEntity import org.kryptonmc.api.entity.Hand import org.kryptonmc.api.event.player.PlaceBlockEvent import org.kryptonmc.api.util.Direction @@ -36,6 +38,7 @@ import org.kryptonmc.krypton.world.block.BlockHitResult import org.kryptonmc.krypton.world.block.handler import org.kryptonmc.krypton.world.block.isGameMasterBlock import org.spongepowered.math.vector.Vector3d +import java.util.concurrent.ThreadLocalRandom import org.spongepowered.math.vector.Vector3i class PlayerBlockHandler(private val player: KryptonPlayer) { @@ -229,6 +232,7 @@ class PlayerBlockHandler(private val player: KryptonPlayer) { if (hasChanged) block.handler().destroy(world, x, y, z, block) if (isCreative) return true // We're done, since the bit after this is for mining, which doesn't happen in creative + dropLoot(block, x, y, z) val item = player.inventory.mainHand val hasCorrectTool = player.hasCorrectTool(block) item.type.handler().mineBlock(player, item, world, block, x, y, z) @@ -236,6 +240,21 @@ class PlayerBlockHandler(private val player: KryptonPlayer) { return true } + private fun dropLoot(block: Block, x: Int, y: Int, z: Int) { + // TODO: Use loot tables + val itemEntity = KryptonItemEntity(world) + val blockItem = block.asItem() ?: return + itemEntity.item = ItemStack.of(blockItem) as KryptonItemStack + val entityHeight = itemEntity.dimensions.height + val random = ThreadLocalRandom.current() + itemEntity.location = Vector3d.from( + x.toDouble()+(0.5)+random.nextDouble(-0.25, 0.25), + y.toDouble()+(0.5)+random.nextDouble(-0.25, 0.25)-(entityHeight/2f), + z.toDouble()+(0.5)+random.nextDouble(-0.25, 0.25)) + world.entityManager.spawn(itemEntity) + itemEntity.velocity = Vector3d.from(random.nextDouble() * 0.2 - 0.1, 0.2, random.nextDouble() * 0.2 - 0.1) + } + companion object { private const val MAXIMUM_DISTANCE_FROM_BLOCK = 36.0 diff --git a/server/src/main/kotlin/org/kryptonmc/krypton/network/handlers/PlayHandler.kt b/server/src/main/kotlin/org/kryptonmc/krypton/network/handlers/PlayHandler.kt index a70ccf18e54..8dcb90c3f1d 100644 --- a/server/src/main/kotlin/org/kryptonmc/krypton/network/handlers/PlayHandler.kt +++ b/server/src/main/kotlin/org/kryptonmc/krypton/network/handlers/PlayHandler.kt @@ -18,6 +18,8 @@ */ package org.kryptonmc.krypton.network.handlers + +import java.util.concurrent.ThreadLocalRandom import com.mojang.brigadier.StringReader import net.kyori.adventure.audience.MessageType import net.kyori.adventure.key.InvalidKeyException @@ -41,6 +43,8 @@ import org.kryptonmc.api.resource.ResourcePack import org.kryptonmc.api.world.GameMode import org.kryptonmc.krypton.KryptonServer import org.kryptonmc.krypton.commands.KryptonPermission +import org.kryptonmc.krypton.entity.item.KryptonItemEntity +import org.kryptonmc.krypton.entity.monster.KryptonCreeper import org.kryptonmc.krypton.entity.player.KryptonPlayer import org.kryptonmc.krypton.inventory.KryptonPlayerInventory import org.kryptonmc.krypton.item.handler @@ -276,6 +280,9 @@ class PlayHandler( PacketInPlayerDigging.Status.STARTED, PacketInPlayerDigging.Status.FINISHED, PacketInPlayerDigging.Status.CANCELLED -> { player.blockHandler.handleBlockBreak(packet) } + PacketInPlayerDigging.Status.DROP_ITEM -> { + player.dropHeldItem() + } PacketInPlayerDigging.Status.UPDATE_STATE -> { val handler = player.inventory[player.inventory.heldSlot].type.handler() if (handler !is ItemTimedHandler) return diff --git a/server/src/main/kotlin/org/kryptonmc/krypton/packet/PacketRegistry.kt b/server/src/main/kotlin/org/kryptonmc/krypton/packet/PacketRegistry.kt index b849122e6ed..9a817de2718 100644 --- a/server/src/main/kotlin/org/kryptonmc/krypton/packet/PacketRegistry.kt +++ b/server/src/main/kotlin/org/kryptonmc/krypton/packet/PacketRegistry.kt @@ -82,6 +82,7 @@ import org.kryptonmc.krypton.packet.out.play.PacketOutEntitySoundEffect import org.kryptonmc.krypton.packet.out.play.PacketOutEntityStatus import org.kryptonmc.krypton.packet.out.play.PacketOutEntityTeleport import org.kryptonmc.krypton.packet.out.play.PacketOutEntityVelocity +import org.kryptonmc.krypton.packet.out.play.PacketOutCollectItem import org.kryptonmc.krypton.packet.out.play.PacketOutHeadLook import org.kryptonmc.krypton.packet.out.play.PacketOutInitializeWorldBorder import org.kryptonmc.krypton.packet.out.play.PacketOutJoinGame @@ -245,6 +246,7 @@ object PacketRegistry { register(0x5E) register(0x5F) register(0x60) + register(0x61) register(0x62) register(0x64) register(0x66) diff --git a/server/src/main/kotlin/org/kryptonmc/krypton/packet/out/play/PacketOutCollectItem.kt b/server/src/main/kotlin/org/kryptonmc/krypton/packet/out/play/PacketOutCollectItem.kt new file mode 100644 index 00000000000..2555669a9c8 --- /dev/null +++ b/server/src/main/kotlin/org/kryptonmc/krypton/packet/out/play/PacketOutCollectItem.kt @@ -0,0 +1,37 @@ +/* + * This file is part of the Krypton project, licensed under the GNU General Public License v3.0 + * + * Copyright (C) 2021 KryptonMC and the contributors of the Krypton project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.kryptonmc.krypton.packet.out.play + +import io.netty.buffer.ByteBuf +import org.kryptonmc.krypton.packet.Packet +import org.kryptonmc.krypton.util.writeVarInt + +@JvmRecord +data class PacketOutCollectItem( + val collectedEntityId: Int, + val collectorEntityId: Int, + val count: Int +) : Packet { + + override fun write(buf: ByteBuf) { + buf.writeVarInt(collectedEntityId) + buf.writeVarInt(collectorEntityId) + buf.writeVarInt(count) + } +} diff --git a/server/src/main/kotlin/org/kryptonmc/krypton/util/world.kt b/server/src/main/kotlin/org/kryptonmc/krypton/util/world.kt index dfe49dae33f..49450795292 100644 --- a/server/src/main/kotlin/org/kryptonmc/krypton/util/world.kt +++ b/server/src/main/kotlin/org/kryptonmc/krypton/util/world.kt @@ -18,6 +18,7 @@ */ package org.kryptonmc.krypton.util +import org.kryptonmc.api.util.BoundingBox import org.kryptonmc.krypton.entity.EntityManager import org.kryptonmc.krypton.entity.KryptonEntity import org.kryptonmc.krypton.world.KryptonWorld @@ -43,6 +44,11 @@ fun EntityManager.forEachEntityInRange(location: Vector3d, viewDistance: Int, ca } } +fun EntityManager.forEachEntityInBounds(bounds: BoundingBox, callback: (KryptonEntity) -> Unit) { + val intersected = world.entities.stream().filter { bounds.intersects(it.boundingBox.move(it.location)) } + intersected.forEach(callback) +} + private fun Long.chunkX(): Int = (this and 4294967295L).toInt() private fun Long.chunkZ(): Int = (this ushr 32 and 4294967295L).toInt() diff --git a/server/src/main/kotlin/org/kryptonmc/krypton/world/block/KryptonBlock.kt b/server/src/main/kotlin/org/kryptonmc/krypton/world/block/KryptonBlock.kt index 5c2c3df5350..7a091605d47 100644 --- a/server/src/main/kotlin/org/kryptonmc/krypton/world/block/KryptonBlock.kt +++ b/server/src/main/kotlin/org/kryptonmc/krypton/world/block/KryptonBlock.kt @@ -32,6 +32,8 @@ import org.kryptonmc.api.block.property.Property import org.kryptonmc.api.fluid.Fluid import org.kryptonmc.api.item.ItemType import org.kryptonmc.api.registry.Registries +import org.kryptonmc.krypton.entity.item.KryptonItemEntity +import org.kryptonmc.krypton.registry.InternalRegistries import org.kryptonmc.krypton.util.serialization.Codecs import org.kryptonmc.krypton.util.serialization.CompoundCodec import org.kryptonmc.krypton.util.serialization.decode